mand-mobile/components/chart/index.vue

298 lines
7.4 KiB
Vue

<template>
<svg class="md-chart" :viewBox="`0 0 ${width} ${height}`">
<defs>
<linearGradient
v-for="color in colors"
:key="color"
:id="`path-fill-gradient-${color}`" x1="0" x2="0" y1="0" y2="1">
<stop :style="`stop-color: ${color}`" offset="0%" stop-opacity="0.4"></stop>
<stop :style="`stop-color: ${color}`" offset="50%" stop-opacity="0.3"></stop>
<stop :style="`stop-color: ${color}`" offset="100%" stop-opacity="0.1"></stop>
</linearGradient>
</defs>
<g class="md-chart-graph" :transform="`translate(${offset.left}, ${offset.top})`">
<g class="md-chart-axis-y">
<g
v-for="(item, index) in yaxis"
:key="index"
:transform="`translate(0, ${item.offset})`"
>
<line x1="0" :x2="innerWidth" y1="0" y2="0"></line>
<text v-text="item.label" x="0" y="0" dx="-0.5em" dy="0.32em"></text>
</g>
</g>
<g class="md-chart-axis-x" :transform="`translate(0, ${innerHeight})`">
<g
v-for="(item, index) in xaxis"
:key="index"
:transform="`translate(${item.offset}, 0)`"
>
<line x1="0" x2="0" y1="0" y2="6"></line>
<text v-text="item.label" x="0" y="0" dy="2em"></text>
</g>
</g>
<g class="md-chart-paths">
<template v-for="(path, index) in paths">
<path
class="md-chart-path"
:key="`line-${index}`"
:style="path.style"
:d="path.value"
></path>
<path
v-if="path.area"
:key="`area-${index}`"
class="md-chart-path-area"
:style="path.area.style"
:d="path.area.value"
></path>
</template>
</g>
</g>
</svg>
</template>
<script>
export default {
name: 'md-chart',
props: {
labels: {
type: Array,
default: () => [],
},
datasets: {
type: Array,
default: () => [],
},
size: {
type: Array,
default: () => [480, 320],
},
max: {
type: Number,
default() {
let max = Math.max.apply(
Math,
this.datasets.map(data => {
return Math.max.apply(Math, data.values)
}),
)
let multiple = 1
while (max > 10) {
multiple *= 10
max /= 10
}
max = Math.ceil(max) * multiple
return max
},
},
min: {
type: Number,
default() {
let min = Math.min.apply(
Math,
this.datasets.map(data => {
return Math.min.apply(Math, data.values)
}),
)
let multiple = 1
while (min > 10) {
multiple *= 10
min = min / 10
}
min = Math.floor(min) * multiple
return min
},
},
lines: {
type: Number,
default: 5,
},
step: {
type: Number,
default() {
return (this.max - this.min) / this.lines
},
},
shift: {
type: Number,
default: 0.6,
},
format: {
type: Function,
default: val => val,
},
},
data() {
return {
unit: 16,
}
},
computed: {
offset() {
return {
top: 0.2 * this.unit,
bottom: 0.5 * this.unit,
left: this.shift * this.unit,
right: 0.2 * this.unit,
}
},
width() {
if (typeof this.size[0] === 'string' && this.size[0].indexOf('rem') !== -1) {
return parseFloat(this.size[0]) * this.unit
} else {
return parseFloat(this.size[0])
}
},
height() {
if (typeof this.size[1] === 'string' && this.size[1].indexOf('rem') !== -1) {
return parseFloat(this.size[1]) * this.unit
} else {
return parseFloat(this.size[1])
}
},
innerWidth() {
return this.width - this.offset.left - this.offset.right
},
innerHeight() {
return this.height - this.offset.top - this.offset.bottom
},
xaxis() {
const deltaX = this.innerWidth / (this.labels.length - 1)
const items = this.labels.map((label, index) => {
return {
offset: index * deltaX,
label: label,
}
})
return items
},
yaxis() {
const items = []
const deltaY = this.innerHeight / this.lines
for (let i = 0; i < this.lines; i++) {
items.push({
offset: i * deltaY,
label: this.format(this.max - i * this.step),
})
}
items.push({
offset: this.innerHeight,
label: this.format(this.min),
})
return items
},
lower() {
return this.max - (this.lines - 1) * this.step
},
paths() {
return this.datasets.map(data => {
const deltaX = this.innerWidth / (data.values.length - 1)
const deltaY = this.innerHeight / this.lines
const points = data.values.map((value, index) => {
if (value < this.lower) {
return {
x: index * deltaX,
y: this.innerHeight - (1 - (this.lower - value) / (this.lower - this.min)) * deltaY,
}
} else {
return {
x: index * deltaX,
y: (1 - (value - this.lower) / (this.max - this.lower)) * (this.innerHeight - deltaY),
}
}
})
const ret = {
style: {
fill: 'none',
stroke: data.color || '#fa8919',
strokeWidth: data.width || 1,
},
}
if (data.theme === 'heat') {
ret.style.stroke = `url(#path-fill-gradient-${data.color})`
} else if (data.theme === 'region') {
ret.area = {
value:
`M0,${this.innerHeight} ` +
points.map(point => `L${point.x},${point.y}`).join(' ') +
` L${points[points.length - 1].x},${this.innerHeight}`,
style: {
fill: `url(#path-fill-gradient-${data.color})`,
stroke: 'none',
},
}
}
ret.value = `M0,${points.shift().y} ` + points.map(point => `L${point.x},${point.y}`).join(' ')
return ret
})
},
colors() {
const uniqueColors = []
this.datasets.map(data => {
if (data.color && uniqueColors.indexOf(data.color) === -1) {
uniqueColors.push(data.color)
}
})
return uniqueColors
},
},
// LiftCircle Hook
mounted() {
if (document.readyState !== 'loading') {
this.$_resize()
}
document.addEventListener('DOMContentLoaded', this.$_resize)
window.addEventListener('resize', this.$_resize)
},
beforeDestroy() {
document.removeEventListener('DOMContentLoaded', this.$_resize)
window.removeEventListener('resize', this.$_resize)
},
methods: {
// MARK: private methods
$_resize() {
this.unit = parseFloat(
window.getComputedStyle(document.getElementsByTagName('html')[0]).getPropertyValue('font-size'),
)
},
},
}
</script>
<style lang="stylus">
.md-chart
line
stroke chart-line-color
stroke-width 0.5
stroke-linecap square
path
stroke chart-path-color
stroke-width 1
stroke-linecap butt
.md-chart-axis-y
text
fill chart-text-color
font-size chart-value-size
text-anchor end
.md-chart-axis-x
text
fill chart-text-color
font-size chart-label-size
text-anchor middle
</style>