509 lines
20 KiB
Vue
509 lines
20 KiB
Vue
<template>
|
|
<div :ref="`chart-canvas-${chartId}`" style="height: 100%;width: 100%;position: relative;">
|
|
<div :id="`chart-canvas-${chartId}`" class="chart__canvas chart-svg"></div>
|
|
<div :id="`chart-canvas-tooltip-${chartId}`" class="chart-canvas-tooltip no-style-class" :class="{'chart-dataLink-tooltip':tooltip.dataLinkShow}" :style="{left:tooltip.x+'px',top:tooltip.y+'px'}" v-if="tooltip.show" v-clickoutside="clickout">
|
|
<div class="chart-canvas-tooltip-title tooltip-title">
|
|
{{tooltip.title}}
|
|
</div>
|
|
<div class="chart-canvas-tooltip-content">
|
|
<div>value</div>
|
|
<div>
|
|
<div v-if="tooltip.mapping && tooltip.mapping.icon" style="display: inline-block">
|
|
<i :class="tooltip.mapping.icon" :style="{color: tooltip.mapping.color.icon}"></i>
|
|
</div>
|
|
<div style="display: inline-block">{{tooltip.value}}</div>
|
|
</div>
|
|
</div>
|
|
<!-- dataLink -->
|
|
<div class="chart-dataLink-list" v-if="tooltip.dataLinkShow">
|
|
<div class="chart-dataLink-item" v-for="(item,index) in dataLink" :key="index" @click="linkClick(item)">
|
|
<i class="nz-icon nz-icon-link"></i>
|
|
<span>{{item.title}}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import * as d3 from 'd3'
|
|
import hexbin from '@/components/chart/chart/options/chartHexagonD3'
|
|
import chartMixin from '@/components/chart/chartMixin'
|
|
import chartFormat from '@/components/chart/chartFormat'
|
|
import { initColor } from '@/components/chart/chart/tools'
|
|
import '@svgdotjs/svg.panzoom.js'
|
|
import { getMetricTypeValue } from '@/components/common/js/tools'
|
|
import chartDataFormat from '@/components/chart/chartDataFormat'
|
|
export default {
|
|
name: 'chartHexagonD3',
|
|
mixins: [chartMixin, chartFormat],
|
|
data () {
|
|
return {
|
|
timer: null,
|
|
HexagonData: [],
|
|
boxWidth: 0,
|
|
boxHeight: 0,
|
|
boxPadding: 5,
|
|
svg: null,
|
|
spaceBetweenHexa: 3,
|
|
hexagonTimer: null
|
|
}
|
|
},
|
|
methods: {
|
|
hexbin,
|
|
initChart () {
|
|
this.setDataLink()
|
|
if (this.timer) {
|
|
clearTimeout(this.timer)
|
|
this.timer = null
|
|
}
|
|
this.timer = setTimeout(() => {
|
|
this.initHexagonData(this.chartInfo, this.chartData).then(() => {
|
|
this.getLayout().then(layout => {
|
|
this.initHexagon(layout)
|
|
}).catch(() => {
|
|
|
|
})
|
|
})
|
|
}, 200)
|
|
},
|
|
initHexagonData (chartInfo, originalDatas) {
|
|
this.HexagonData = []
|
|
this.isInit = false
|
|
const decimals = this.chartInfo.param.decimals || 2
|
|
this.isNoData = true
|
|
return new Promise(resolve => {
|
|
let colorIndex = 0
|
|
originalDatas.forEach((originalData, expressionIndex) => {
|
|
originalData.forEach((data, dataIndex) => {
|
|
this.isNoData = false
|
|
const Hexagon = {
|
|
value: '',
|
|
showValue: '',
|
|
label: {},
|
|
width: '',
|
|
height: '',
|
|
legend: '',
|
|
oldValue: '',
|
|
mapping: {
|
|
|
|
}
|
|
}
|
|
const legend = this.handleLegend(chartInfo, data, expressionIndex, dataIndex, colorIndex)
|
|
Hexagon.value = getMetricTypeValue(data.values, chartInfo.param.statistics || 'last')
|
|
Hexagon.max = chartInfo.param.max || 100
|
|
Hexagon.min = chartInfo.param.min || 0
|
|
if (Hexagon.min === Hexagon.max) {
|
|
Hexagon.min = Hexagon.max / 2
|
|
}
|
|
Hexagon.label = data.metric
|
|
Hexagon.legend = legend.alias
|
|
Hexagon.label.legend = Hexagon.legend
|
|
Hexagon.seriesIndex = expressionIndex
|
|
Hexagon.name = legend.name
|
|
Hexagon.alias = legend.alias
|
|
Hexagon.showValue = chartDataFormat.getUnit(chartInfo.unit ? chartInfo.unit : 2).compute(Hexagon.value, null, -1, decimals)
|
|
// Hexagon.value = Hexagon.showValue
|
|
Hexagon.mapping = this.selectMapping(Hexagon.value, chartInfo.param.valueMapping, chartInfo.param.enable && this.chartInfo.param.enable.valueMapping)
|
|
this.HexagonData.push(Hexagon)
|
|
colorIndex++
|
|
})
|
|
})
|
|
this.$emit('chartIsNoData', this.isNoData)
|
|
resolve()
|
|
})
|
|
},
|
|
initHexagon (layout) {
|
|
this.isInit = false
|
|
let rowIndex = 0
|
|
let colIndex = -1
|
|
const data = this.HexagonData.map(item => {
|
|
colIndex++
|
|
if (colIndex > layout.col) {
|
|
rowIndex++
|
|
colIndex = 0
|
|
}
|
|
return {
|
|
...item,
|
|
x: colIndex,
|
|
y: rowIndex,
|
|
metrics: item.label,
|
|
label: item.legend
|
|
}
|
|
})
|
|
const dom = document.getElementById(`chart-canvas-${this.chartId}`)
|
|
const child = document.getElementById('svgHex' + this.chartId)
|
|
if (dom && child) {
|
|
this.clearCache()
|
|
// dom.removeChild(child)
|
|
}
|
|
if (dom) {
|
|
const hexaRadius = layout.radius
|
|
dom.append(this.showHexagons(data, hexaRadius, layout.col, layout.row))
|
|
setTimeout(() => {
|
|
const svg = document.getElementById('svgHex' + this.chartId)
|
|
const bbox = svg.getBBox()
|
|
svg.setAttribute('viewBox', (bbox.x - 10) + ' ' + (bbox.y - 10) + ' ' + (bbox.width + 20) + ' ' + (bbox.height + 20))
|
|
svg.setAttribute('width', (bbox.width + 20) + 'px')
|
|
svg.setAttribute('height', (bbox.height + 20) + 'px')
|
|
}, 100)
|
|
}
|
|
},
|
|
showHexagons (data, hexaRadiu = 60, row, col) {
|
|
// 清除资源 释放缓存
|
|
// Initial Geometrical Calculations
|
|
const self = this
|
|
const hexaRadius = hexaRadiu
|
|
const spaceBetweenHexa = this.spaceBetweenHexa // Number of pixels of space between consecutive hexagons
|
|
const hexaWidth = hexaRadius * 2 // Calculates Width of the Hexagons with specified radius
|
|
const hexaHeight = hexaRadius * Math.sqrt(3) // Calculates Width of the Hexagons ...
|
|
const rows = row // Number of desired Rows
|
|
const cols = col // Number of desired Columns
|
|
const height = (rows + 1 / 3) * 3 / 2 * hexaRadius // Calculates height of the window with given rows and radius
|
|
const width = (cols + 1 / 2) * Math.sqrt(3) * hexaRadius // Calculates width of ...
|
|
const hexbin = this.hexbin()
|
|
.radius(hexaRadius)
|
|
.extent([[0, 0], [width, height]])
|
|
this.svg = d3.create('svg')
|
|
.attr('id', 'svgHex' + this.chartId)
|
|
.attr('width', width)
|
|
.attr('height', height)
|
|
// .on('mousemove', this.hexagonMove)
|
|
for (let i = 0; i < data.length; i++) {
|
|
const point = data[i]
|
|
const col = point.x
|
|
const row = point.y
|
|
const color = point.mapping ? point.mapping.color.bac : this.colorList[i]
|
|
const vals = self.getCenter(hexaRadius, row, col) // Gets the Center coordinates with given row and column
|
|
const x = vals[0]
|
|
const y = vals[1]
|
|
const g = this.svg.append('g')
|
|
const hexa = self.drawHexagon(g, hexaRadius, spaceBetweenHexa, x, y, hexbin) // Draws hexagon
|
|
hexa.attr('fill', color) // Paints hexagon
|
|
g.on('mouseenter', self.hexagonOver.bind(self, point))
|
|
g.on('mousemove', self.hexagonMove.bind(self, point))
|
|
g.on('mouseleave', self.hexagonOut.bind(self, point))
|
|
g.on('click', self.chartClick.bind(self, point))
|
|
self.drawText(this.svg, vals, point, color, hexaRadius, g) // 文本
|
|
data[i].fcolor = color
|
|
}
|
|
return this.svg.node()
|
|
},
|
|
hexagonOver (that, e) { // 移入六边形
|
|
if (this.tooltip.dataLinkShow) { return }
|
|
this.tooltip.title = that.alias
|
|
this.tooltip.value = that.mapping && that.mapping.display ? this.handleDisplay(that.mapping.display, { ...that.metrics, legend: that.alias, value: that.showValue }) : that.showValue
|
|
this.tooltip.mapping = that.mapping
|
|
this.tooltip.show = true
|
|
this.setPosition(e)
|
|
},
|
|
hexagonMove (that, e) { // 六边形内移动
|
|
if (this.tooltip.dataLinkShow) { return }
|
|
this.tooltip.show = true
|
|
this.setPosition(e)
|
|
},
|
|
hexagonOut () {
|
|
if (this.tooltip.dataLinkShow) { return }
|
|
this.tooltip.show = false
|
|
},
|
|
setPosition (e) {
|
|
const windowWidth = window.innerWidth// 窗口宽度
|
|
const windowHeight = window.innerHeight// 窗口高度
|
|
this.$nextTick(() => {
|
|
const box = document.getElementById(`chart-canvas-tooltip-${this.chartId}`)
|
|
if (box) {
|
|
const boxWidth = box.offsetWidth
|
|
const boxHeight = box.offsetHeight
|
|
if (e.pageX < (windowWidth / 2)) { // 说明鼠标在左边放不下提示框
|
|
this.tooltip.x = e.pageX + 15
|
|
} else {
|
|
this.tooltip.x = e.pageX - boxWidth - 15
|
|
}
|
|
if (e.pageY + 50 + boxHeight < windowHeight) { // 说明鼠标上面放不下提示框
|
|
this.tooltip.y = e.pageY + 15
|
|
} else {
|
|
this.tooltip.y = e.pageY - boxHeight - 10
|
|
}
|
|
} else {
|
|
this.tooltip.x = e.pageX + 15
|
|
this.tooltip.y = e.pageY + 15
|
|
}
|
|
})
|
|
},
|
|
formatStr (d) {
|
|
const self = this
|
|
let str = ''
|
|
|
|
str += ''
|
|
return str
|
|
},
|
|
drawHexagon (svg, radius, space, x, y, hexbin) {
|
|
const hexagon = svg.append('path')
|
|
.attr('d', hexbin.hexagon(radius - space))
|
|
.attr('stroke', '#000')
|
|
.attr('stroke-linecap', 'round')
|
|
.attr('stroke-linejoin', 'round')
|
|
.attr('stroke-width', 1)
|
|
.attr('transform', 'translate(' + x + ',' + y + ')')
|
|
.style('cursor', this.dataLink.length ? 'pointer' : 'default')
|
|
return hexagon
|
|
},
|
|
drawText (svg, vals, point, color, hexbinRadius, group) {
|
|
let str = ''
|
|
let valueStr = ''
|
|
const self = this
|
|
const hexWidth = hexbinRadius * Math.sqrt(3)
|
|
let fontSize = 24
|
|
const textColor = point.mapping ? point.mapping.color.text : this.invertColor(color)
|
|
if (this.chartInfo.param.text === 'all') {
|
|
str += point.alias
|
|
valueStr = point.mapping && point.mapping.display ? self.handleDisplay(point.mapping.display, { ...point.metrics, legend: point.alias, value: point.showValue }) : point.showValue
|
|
}
|
|
if (this.chartInfo.param.text === 'value' || !this.chartInfo.param.text) {
|
|
valueStr = point.mapping && point.mapping.display ? self.handleDisplay(point.mapping.display, { ...point.metrics, legend: point.alias, value: point.showValue }) : point.showValue
|
|
}
|
|
if (this.chartInfo.param.text === 'legend') {
|
|
str += point.alias
|
|
}
|
|
if (this.chartInfo.param.text === 'none') {
|
|
str += ''
|
|
}
|
|
fontSize = Math.floor(16 * (hexWidth / 60))
|
|
if (str && valueStr) {
|
|
const fObj = group.append('foreignObject')
|
|
.attr('width', hexWidth || 60)
|
|
.attr('height', 24)
|
|
fObj
|
|
.attr('x', vals[0] - hexWidth / 2)
|
|
.attr('y', vals[1] - 24)
|
|
// .text(str)
|
|
.attr('text-anchor', 'middle')
|
|
.attr('alignment-baseline', 'central')
|
|
.style('font-size', fontSize)
|
|
.style('fill', textColor)
|
|
.style('pointer-events', 'none')
|
|
const scrollDiv = fObj.append('xhtml:div')
|
|
scrollDiv
|
|
.html(`<div style="color:${textColor};width:${hexWidth}px;overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16px;box-sizing: border-box;padding: 0 10px;text-align: center">${str}</div>`)
|
|
// group.append('text')
|
|
// .attr('x', vals[0])
|
|
// .attr('y', vals[1] + 16)
|
|
// .text(valueStr)
|
|
// .attr('text-anchor', 'middle')
|
|
// .attr('alignment-baseline', 'central')
|
|
// .style('font-size', fontSize)
|
|
// .style('fill', textColor)
|
|
const fObj2 = group.append('foreignObject')
|
|
.attr('width', hexWidth || 60)
|
|
.attr('height', 24)
|
|
fObj2
|
|
.attr('x', vals[0] - hexWidth / 2)
|
|
.attr('y', vals[1])
|
|
// .text(str)
|
|
.attr('text-anchor', 'middle')
|
|
.attr('alignment-baseline', 'central')
|
|
.style('font-size', fontSize)
|
|
.style('fill', textColor)
|
|
.style('pointer-events', 'none')
|
|
const scrollDiv2 = fObj2.append('xhtml:div')
|
|
scrollDiv2
|
|
.html(
|
|
`<div style="color:${textColor};width:${hexWidth}px;overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16px;box-sizing: border-box;padding: 0 10px;text-align: center">
|
|
<i class="${point.mapping && point.mapping.icon}" style="color: ${point.mapping && point.mapping.color && point.mapping.color.icon}"></i> ${valueStr}
|
|
</div>`)
|
|
return
|
|
}
|
|
if (str) {
|
|
const fObj = group.append('foreignObject')
|
|
.attr('width', hexWidth || 60)
|
|
.attr('height', 24)
|
|
fObj
|
|
.attr('x', vals[0] - hexWidth / 2)
|
|
.attr('y', vals[1] - 10)
|
|
// .text(str)
|
|
.attr('text-anchor', 'middle')
|
|
.attr('alignment-baseline', 'central')
|
|
.style('font-size', fontSize)
|
|
.style('fill', textColor)
|
|
.style('pointer-events', 'none')
|
|
const scrollDiv = fObj.append('xhtml:div')
|
|
scrollDiv
|
|
.html(
|
|
`<div style="color:${textColor};width:${hexWidth}px;overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16px;box-sizing: border-box;padding: 0 10px;text-align: center">
|
|
<i class="${point.mapping && point.mapping.icon}" style="color: ${point.mapping && point.mapping.color && point.mapping.color.icon}"></i> ${str}
|
|
</div>`)
|
|
return
|
|
}
|
|
if (valueStr) {
|
|
// group.append('text')
|
|
// .attr('x', vals[0])
|
|
// .attr('y', vals[1])
|
|
// .text(valueStr)
|
|
// .attr('text-anchor', 'middle')
|
|
// .attr('alignment-baseline', 'central')
|
|
// .style('font-size', fontSize)
|
|
// .style('fill', textColor)
|
|
const fObj = group.append('foreignObject')
|
|
.attr('width', hexWidth || 60)
|
|
.attr('height', 24)
|
|
fObj
|
|
.attr('x', vals[0] - hexWidth / 2)
|
|
.attr('y', vals[1] - 10)
|
|
.attr('text-anchor', 'middle')
|
|
.attr('alignment-baseline', 'central')
|
|
.style('font-size', fontSize)
|
|
.style('fill', textColor)
|
|
.style('pointer-events', 'none')
|
|
const scrollDiv = fObj.append('xhtml:div')
|
|
scrollDiv
|
|
.html(
|
|
`<div style="color:${textColor};width:${hexWidth}px;overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16px;box-sizing: border-box;padding: 0 10px;text-align: center">
|
|
<i class="${point.mapping && point.mapping.icon}" style="color: ${point.mapping && point.mapping.color && point.mapping.color.icon}"></i> ${valueStr}
|
|
</div>`)
|
|
}
|
|
},
|
|
getCenter (hexaRadius, row, col) {
|
|
let x = hexaRadius * col * Math.sqrt(3)
|
|
// Offset each uneven row by half of a "hex-width" to the right
|
|
if (row % 2 === 1) x += (hexaRadius * Math.sqrt(3)) / 2
|
|
const y = hexaRadius * row * 1.5
|
|
return [x + hexaRadius, y + hexaRadius]
|
|
},
|
|
invertColor (hex) {
|
|
if (hex.indexOf('#') === 0) {
|
|
hex = hex.slice(1)
|
|
}
|
|
// convert 3-digit hex to 6-digits.
|
|
if (hex.length === 3) {
|
|
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
|
|
}
|
|
if (hex.length !== 6) {
|
|
// throw new Error('Invalid HEX color.');
|
|
}
|
|
const r = parseInt(hex.slice(0, 2), 16)
|
|
const g = parseInt(hex.slice(2, 4), 16)
|
|
const b = parseInt(hex.slice(4, 6), 16)
|
|
return (r * 0.299 + g * 0.587 + b * 0.114) > 186
|
|
? '#000000'
|
|
: '#FFFFFF'
|
|
},
|
|
getLayout () {
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.$refs[`chart-canvas-${this.chartId}`]) {
|
|
reject()
|
|
}
|
|
this.boxWidth = this.$refs[`chart-canvas-${this.chartId}`].offsetWidth - 2 * this.boxPadding
|
|
this.boxHeight = this.$refs[`chart-canvas-${this.chartId}`].offsetHeight - 2 * this.boxPadding
|
|
let radius = 0
|
|
let col = 0
|
|
let row = 0
|
|
for (let i = 1; i <= this.HexagonData.length; i++) {
|
|
const cols = Math.ceil(this.HexagonData.length / i)
|
|
const hexaRadiusY = Math.ceil((this.boxHeight * 2) / (3 * i + 1))
|
|
let hexaRadiusX = ''
|
|
if (i === 1) {
|
|
hexaRadiusX = Math.ceil(this.boxWidth / (Math.sqrt(3) * cols))
|
|
} else {
|
|
hexaRadiusX = Math.ceil(this.boxWidth / (Math.sqrt(3) * cols + Math.sqrt(3) / 2))
|
|
}
|
|
const rateMax = hexaRadiusX > hexaRadiusY ? hexaRadiusY : hexaRadiusX
|
|
if (rateMax > radius) {
|
|
radius = rateMax
|
|
col = cols
|
|
row = i
|
|
}
|
|
}
|
|
if (this.HexagonData.length) {
|
|
while (col * row >= this.HexagonData.length) { // 避免出现空白
|
|
row--
|
|
}
|
|
}
|
|
row++
|
|
resolve({ col, row, radius: radius - this.spaceBetweenHexa })
|
|
})
|
|
},
|
|
clearCache () {
|
|
if (this.svg) {
|
|
this.svg.on('mousemove', null)
|
|
this.svg.selectAll('g').on('mouseenter', null)
|
|
this.svg.selectAll('g').on('mouseleave', null)
|
|
this.svg.selectAll('g').on('click', null)
|
|
this.svg.remove()
|
|
this.svg = null
|
|
}
|
|
},
|
|
resize () {
|
|
if (this.hexagonTimer) {
|
|
clearTimeout(this.hexagonTimer)
|
|
this.hexagonTimer = null
|
|
}
|
|
this.hexagonTimer = setTimeout(() => {
|
|
this.getLayout().then(layout => {
|
|
this.initHexagon(layout)
|
|
}).catch(() => {
|
|
|
|
})
|
|
}, 50)
|
|
},
|
|
datalinkPosition (e) {
|
|
const windowWidth = window.innerWidth// 窗口宽度
|
|
const windowHeight = window.innerHeight// 窗口高度
|
|
this.$nextTick(() => {
|
|
const box = document.getElementById(`chart-canvas-tooltip-${this.chartId}`)
|
|
const left = e.pageX - this.$refs[`chart-canvas-${this.chartId}`].getBoundingClientRect().left
|
|
const top = e.pageY - this.$refs[`chart-canvas-${this.chartId}`].getBoundingClientRect().top
|
|
if (box) {
|
|
const boxWidth = box.offsetWidth
|
|
const boxHeight = box.offsetHeight
|
|
if (e.pageX < (windowWidth / 2)) { // 说明鼠标在左边放不下提示框
|
|
this.tooltip.x = left + 15
|
|
} else {
|
|
this.tooltip.x = left - boxWidth - 15
|
|
}
|
|
if (e.pageY + 50 + boxHeight < windowHeight) { // 说明鼠标上面放不下提示框
|
|
this.tooltip.y = top + 15
|
|
} else {
|
|
this.tooltip.y = top - boxHeight - 10
|
|
}
|
|
}
|
|
})
|
|
},
|
|
chartClick (data, e) {
|
|
if (this.dataLink.length) {
|
|
this.tooltip.title = data.alias
|
|
this.tooltip.value = data.mapping && data.mapping.display ? this.handleDisplay(data.mapping.display, { ...data.metrics, legend: data.alias, value: data.showValue }) : data.showValue
|
|
this.tooltip.mapping = data.mapping
|
|
this.tooltip.show = true
|
|
this.tooltip.dataLinkShow = true
|
|
this.tooltip.metric.labels = data.metrics
|
|
this.tooltip.metric.expressionIndex = data.seriesIndex
|
|
this.datalinkPosition(e)
|
|
}
|
|
},
|
|
clickout () {
|
|
if (this.dataLink.length) {
|
|
this.tooltip.show = false
|
|
this.tooltip.dataLinkShow = false
|
|
}
|
|
}
|
|
},
|
|
mounted () {
|
|
// eslint-disable-next-line vue/no-mutating-props
|
|
this.chartOption.color || (this.chartOption.color = initColor(20))
|
|
this.colorList = this.chartOption.color
|
|
this.chartInfo.loaded && this.initChart(this.chartOption)
|
|
},
|
|
beforeDestroy () {
|
|
if (this.timer) {
|
|
clearTimeout(this.timer)
|
|
this.timer = null
|
|
}
|
|
if (this.hexagonTimer) {
|
|
clearTimeout(this.hexagonTimer)
|
|
this.hexagonTimer = null
|
|
}
|
|
this.clearCache()
|
|
}
|
|
}
|
|
</script>
|