DemuMesDataV/components/flylineChart/index.vue

510 lines
13 KiB
Vue

<template>
<div
class="dv-flyline-chart"
ref="dv-flyline-chart"
:style="`background-image: url(${mergedConfig ? mergedConfig.bgImgUrl : ''})`"
@click="consoleClickPos"
>
<svg v-if="mergedConfig" :width="width" :height="height">
<defs>
<radialGradient
:id="gradientId"
cx="50%" cy="50%" r="50%"
>
<stop
offset="0%" stop-color="#fff"
stop-opacity="1"
/>
<stop
offset="100%" stop-color="#fff"
stop-opacity="0"
/>
</radialGradient>
<radialGradient
:id="gradient2Id"
cx="50%" cy="50%" r="50%"
>
<stop
offset="0%" stop-color="#fff"
stop-opacity="0"
/>
<stop
offset="100%" stop-color="#fff"
stop-opacity="1"
/>
</radialGradient>
<circle
v-if="paths[0]"
:id="`circle${paths[0].toString()}`"
:cx="paths[0][2][0]"
:cy="paths[0][2][1]"
>
<animate
attributeName="r"
:values="`1;${mergedConfig.halo.radius}`"
:dur="mergedConfig.halo.duration / 10 + 's'"
repeatCount="indefinite"
/>
<animate
attributeName="opacity"
values="1;0"
:dur="mergedConfig.halo.duration / 10 + 's'"
repeatCount="indefinite"
/>
</circle>
</defs>
<image
v-if="paths[0]"
:xlink:href="mergedConfig.centerPointImg.url"
:width="mergedConfig.centerPointImg.width"
:height="mergedConfig.centerPointImg.height"
:x="paths[0][2][0] - mergedConfig.centerPointImg.width / 2"
:y="paths[0][2][1] - mergedConfig.centerPointImg.height / 2"
/>
<mask :id="`maskhalo${paths[0].toString()}`">
<use
v-if="paths[0]"
:xlink:href="`#circle${paths[0].toString()}`"
:fill="`url(#${gradient2Id})`"
/>
</mask>
<use
v-if="paths[0] && mergedConfig.halo.show"
:xlink:href="`#circle${paths[0].toString()}`"
:fill="mergedConfig.halo.color"
:mask="`url(#maskhalo${paths[0].toString()})`"
/>
<g
v-for="(path, i) in paths"
:key="i"
>
<defs>
<path
:id="`path${path.toString()}`"
:ref="`path${i}`"
:d="`M${path[0].toString()} Q${path[1].toString()} ${path[2].toString()}`"
fill="transparent"
/>
</defs>
<use
:xlink:href="`#path${path.toString()}`"
:stroke-width="mergedConfig.lineWidth"
:stroke="mergedConfig.orbitColor"
/>
<use
v-if="lengths[i]"
:xlink:href="`#path${path.toString()}`"
:stroke-width="mergedConfig.lineWidth"
:stroke="mergedConfig.flylineColor"
:mask="`url(#mask${unique}${path.toString()})`"
>
<animate
attributeName="stroke-dasharray"
:from="`0, ${lengths[i]}`"
:to="`${lengths[i]}, 0`"
:dur="times[i] || 0"
repeatCount="indefinite"
/>
</use>
<mask :id="`mask${unique}${path.toString()}`">
<circle cx="0" cy="0" :r="mergedConfig.flylineRadius" :fill="`url(#${gradientId})`">
<animateMotion
:dur="times[i] || 0"
:path="`M${path[0].toString()} Q${path[1].toString()} ${path[2].toString()}`"
rotate="auto"
repeatCount="indefinite"
/>
</circle>
</mask>
<image
:xlink:href="mergedConfig.pointsImg.url"
:width="mergedConfig.pointsImg.width"
:height="mergedConfig.pointsImg.height"
:x="path[0][0] - mergedConfig.pointsImg.width / 2"
:y="path[0][1] - mergedConfig.pointsImg.height / 2"
/>
<text
:style="`fontSize:${mergedConfig.text.fontSize}px;`"
:fill="mergedConfig.text.color"
:x="path[0][0] + mergedConfig.text.offset[0]"
:y="path[0][1] + mergedConfig.text.offset[1]"
>
{{ texts[i] }}
</text>
</g>
</svg>
</div>
</template>
<script>
import { deepMerge } from '@jiaminghi/charts/lib/util/index'
import { deepClone } from '@jiaminghi/c-render/lib/plugin/util'
import { randomExtend, getPointDistance } from '../../util/index'
import autoResize from '../../mixins/autoResize.js'
export default {
name: 'PercentPond',
mixins: [autoResize],
props: {
config: {
type: Object,
default: () => ({})
},
dev: {
type: Boolean,
default: false
}
},
data () {
return {
ref: 'dv-flyline-chart',
unique: Math.random(),
maskId: `flyline-mask-id-${(new Date()).getTime()}`,
maskCircleId: `mask-circle-id-${(new Date()).getTime()}`,
gradientId: `gradient-id-${(new Date()).getTime()}`,
gradient2Id: `gradient2-id-${(new Date()).getTime()}`,
defaultConfig: {
/**
* @description Flyline chart center point
* @type {Array<Number>}
* @default centerPoint = [0, 0]
*/
centerPoint: [0, 0],
/**
* @description Flyline start points
* @type {Array<Array<Number>>}
* @default points = []
* @example points = [[10, 10], [100, 100]]
*/
points: [],
/**
* @description Flyline width
* @type {Number}
* @default lineWidth = 1
*/
lineWidth: 1,
/**
* @description Orbit color
* @type {String}
* @default orbitColor = 'rgba(103, 224, 227, .2)'
*/
orbitColor: 'rgba(103, 224, 227, .2)',
/**
* @description Flyline color
* @type {String}
* @default orbitColor = '#ffde93'
*/
flylineColor: '#ffde93',
/**
* @description K value
* @type {Number}
* @default k = -0.5
* @example k = -1 ~ 1
*/
k: -0.5,
/**
* @description Flyline curvature
* @type {Number}
* @default curvature = 5
*/
curvature: 5,
/**
* @description Flyline radius
* @type {Number}
* @default flylineRadius = 100
*/
flylineRadius: 100,
/**
* @description Flyline animation duration
* @type {Array<Number>}
* @default duration = [20, 30]
*/
duration: [20, 30],
/**
* @description Relative points position
* @type {Boolean}
* @default relative = true
*/
relative: true,
/**
* @description Back ground image url
* @type {String}
* @default bgImgUrl = ''
* @example bgImgUrl = './img/bg.jpg'
*/
bgImgUrl: '',
/**
* @description Text configuration
* @type {Object}
*/
text: {
/**
* @description Text offset
* @type {Array<Number>}
* @default offset = [0, 15]
*/
offset: [0, 15],
/**
* @description Text color
* @type {String}
* @default color = '#ffdb5c'
*/
color: '#ffdb5c',
/**
* @description Text font size
* @type {Number}
* @default fontSize = 12
*/
fontSize: 12
},
/**
* @description Halo configuration
* @type {Object}
*/
halo: {
/**
* @description Weather to show halo
* @type {Boolean}
* @default show = true
* @example show = true | false
*/
show: true,
/**
* @description Halo animation duration (10 = 1s)
* @type {Number}
* @default duration = 30
*/
duration: 30,
/**
* @description Halo color
* @type {String}
* @default color = '#fb7293'
*/
color: '#fb7293',
/**
* @description Halo max radius
* @type {Number}
* @default radius = 120
*/
radius: 120
},
/**
* @description Center point img configuration
* @type {Object}
*/
centerPointImg: {
/**
* @description Center point img width
* @type {Number}
* @default width = 40
*/
width: 40,
/**
* @description Center point img height
* @type {Number}
* @default height = 40
*/
height: 40,
/**
* @description Center point img url
* @type {String}
* @default url = ''
*/
url: ''
},
/**
* @description Points img configuration
* @type {Object}
* @default radius = 120
*/
pointsImg: {
/**
* @description Points img width
* @type {Number}
* @default width = 15
*/
width: 15,
/**
* @description Points img height
* @type {Number}
* @default height = 15
*/
height: 15,
/**
* @description Points img url
* @type {String}
* @default url = ''
*/
url: ''
}
},
mergedConfig: null,
paths: [],
lengths: [],
times: [],
texts: []
}
},
watch: {
config () {
const { calcData } = this
calcData()
}
},
methods: {
afterAutoResizeMixinInit () {
const { calcData } = this
calcData()
},
onResize () {
const { calcData } = this
calcData()
},
async calcData () {
const { mergeConfig, createFlylinePaths, calcLineLengths } = this
mergeConfig()
createFlylinePaths()
await calcLineLengths()
const { calcTimes, calcTexts } = this
calcTimes()
calcTexts()
},
mergeConfig () {
let { config, defaultConfig } = this
const mergedConfig = deepMerge(deepClone(defaultConfig, true), config || {})
const { points } = mergedConfig
mergedConfig.points = points.map(item => {
if (item instanceof Array) {
return { position: item, text: '' }
}
return item
})
this.mergedConfig = mergedConfig
},
createFlylinePaths () {
const { getPath, mergedConfig, width, height } = this
let { centerPoint, points, relative } = mergedConfig
points = points.map(({ position }) => position)
if (relative) {
centerPoint = [width * centerPoint[0], height * centerPoint[1]]
points = points.map(([x, y]) => [width * x, height * y])
}
this.paths = points.map(point => getPath(centerPoint, point))
},
getPath (center, point) {
const { getControlPoint } = this
const controlPoint = getControlPoint(center, point)
return [point, controlPoint, center]
},
getControlPoint ([sx, sy], [ex, ey]) {
const { getKLinePointByx, mergedConfig } = this
const { curvature, k } = mergedConfig
const [mx, my] = [(sx + ex) / 2, (sy + ey) / 2]
const distance = getPointDistance([sx, sy], [ex, ey])
const targetLength = distance / curvature
const disDived = targetLength / 2
let [dx, dy] = [mx, my]
do {
dx += disDived
dy = getKLinePointByx(k, [mx, my], dx)[1]
} while (getPointDistance([mx, my], [dx, dy]) < targetLength)
return [dx, dy]
},
getKLinePointByx (k, [lx, ly], x) {
const y = ly - k * lx + k * x
return [x, y]
},
async calcLineLengths () {
const { $nextTick, paths, $refs } = this
await $nextTick()
this.lengths = paths.map((foo, i) => $refs[`path${i}`][0].getTotalLength())
},
calcTimes () {
const { duration, points } = this.mergedConfig
this.times = points.map(foo => randomExtend(...duration) / 10)
},
calcTexts () {
const { points } = this.mergedConfig
this.texts = points.map(({ text }) => text)
},
consoleClickPos ({ offsetX, offsetY }) {
const { width, height, dev } = this
if (!dev) return
const relativeX = (offsetX / width).toFixed(2)
const relativeY = (offsetY / height).toFixed(2)
console.warn(`dv-flyline-chart DEV: \n Click Position is [${offsetX}, ${offsetY}] \n Relative Position is [${relativeX}, ${relativeY}]`)
}
}
}
</script>
<style lang="less">
.dv-flyline-chart {
display: flex;
flex-direction: column;
background-size: 100% 100%;
polyline {
transition: all 0.3s;
}
text {
text-anchor: middle;
dominant-baseline: middle;
}
}
</style>