Merge branch 'dev-3.4' of https://git.mesalab.cn/nezha/nezha-fronted into dev-3.4

This commit is contained in:
zhangyu
2022-07-22 09:35:58 +08:00
3 changed files with 207 additions and 198 deletions

View File

@@ -1,19 +1,32 @@
<template> <template>
<div <div
ref="pie-chart-box" ref="pie-chart-box"
class="nz-chart__component nz-chart__component--time-series" @mouseenter="mouseEnterChart" class="nz-chart__component"
@mouseleave="mouseLeaveChart"
> >
<div :id="`chart-canvas-${chartId}`" class="chart__canvas"></div> <div :id="`chart-canvas-${chartId}`" class="chart__canvas">
<svg :id="`bubble-svg-${chartId}`" width="100%" height="100%"></svg>
</div>
<div :class="`chart-canvas-tooltip-${chartId}`" :id="`chart-canvas-tooltip-${chartId}`" class="chart-canvas-tooltip" :style="{left:tooltip.x+'px',top:tooltip.y+'px'}" v-if="tooltip.show">
<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>
</div>
</div> </div>
</template> </template>
<script> <script>
import chartMixin from '@/components/chart/chartMixin' import chartMixin from '@/components/chart/chartMixin'
import chartFormat from '@/components/chart/chartFormat' import chartFormat from '@/components/chart/chartFormat'
import * as echarts from 'echarts'
import * as d3 from 'd3' import * as d3 from 'd3'
import { getChart, setChart } from '@/components/common/js/common'
import { getMetricTypeValue } from '@/components/common/js/tools' import { getMetricTypeValue } from '@/components/common/js/tools'
import chartDataFormat from '@/components/chart/chartDataFormat' import chartDataFormat from '@/components/chart/chartDataFormat'
import { initColor } from '@/components/chart/chart/tools' import { initColor } from '@/components/chart/chart/tools'
@@ -31,58 +44,49 @@ export default {
isFullscreen: Boolean isFullscreen: Boolean
}, },
computed: { computed: {
}, },
data () { data () {
return { return {
colorList: [], colorList: [],
chartDot: 2, chartDot: 2,
isInit: true, // 是否是初始化初始化时为true图表初始化结束后设为false isInit: true, // 是否是初始化初始化时为true图表初始化结束后设为false
chartId: '' chartId: '',
bubbleData: [],
tooltip: {
x: 0,
y: 0,
title: 0,
value: 0,
mapping: {},
show: false
}
} }
}, },
methods: { methods: {
initChart (chartOption = this.chartOption) { initChart () {
this.legends = [] this.initBubbleData(this.chartInfo, this.chartData)
const parentNode = {
id: 'parentNode',
name: 'parentNode',
depth: -1
}
chartOption.dataset.source = [parentNode, ...this.initBubbleData(this.chartInfo, [], this.chartData)]
if (this.isNoData) { if (this.isNoData) {
return return
} }
chartOption.series[0].renderItem = this.renderItem
chartOption.tooltip.formatter = this.formatterFunc
chartOption.tooltip.position = this.tooltipPosition
/* 使用setTimeout延迟渲染图表避免样式错乱 */ /* 使用setTimeout延迟渲染图表避免样式错乱 */
setTimeout(() => { setTimeout(() => {
const myChart = this.isInit ? echarts.init(document.getElementById(`chart-canvas-${this.chartId}`)) : getChart(this.chartId) this.drawBubbleChart()
if (!myChart) {
return
}
myChart.setOption(chartOption)
this.isInit && setChart(this.chartId, myChart) // 缓存不使用vue的data是为避免整个chart被监听导致卡顿
this.isInit = false this.isInit = false
}, 200) }, 200)
}, },
initBubbleData (chartInfo, seriesTemplate, originalDatas) { initBubbleData (chartInfo, originalDatas) {
this.bubbleData = []
let colorIndex = 0 let colorIndex = 0
const decimals = this.chartInfo.param.decimals || 2 const decimals = this.chartInfo.param.decimals || 2
const s = lodash.cloneDeep(seriesTemplate)
this.isNoData = true this.isNoData = true
originalDatas.forEach((originalData, expressionIndex) => { originalDatas.forEach((originalData, expressionIndex) => {
originalData.forEach((data, dataIndex) => { originalData.forEach((data, dataIndex) => {
this.isNoData = false this.isNoData = false
if (s) {
const value = getMetricTypeValue(data.values, chartInfo.param.statistics) const value = getMetricTypeValue(data.values, chartInfo.param.statistics)
const showValue = chartDataFormat.getUnit(chartInfo.unit ? chartInfo.unit : 2).compute(value, null, -1, decimals) const showValue = chartDataFormat.getUnit(chartInfo.unit ? chartInfo.unit : 2).compute(value, null, -1, decimals)
const mapping = this.selectMapping(value, chartInfo.param.valueMapping, chartInfo.param.enable && this.chartInfo.param.enable.valueMapping) const mapping = this.selectMapping(value, chartInfo.param.valueMapping, chartInfo.param.enable && this.chartInfo.param.enable.valueMapping)
// eslint-disable-next-line vue/no-mutating-props
mapping && this.chartOption.color && (this.chartOption.color[colorIndex] = mapping.color.bac)
const legend = this.handleLegend(chartInfo, data, expressionIndex, dataIndex, colorIndex) const legend = this.handleLegend(chartInfo, data, expressionIndex, dataIndex, colorIndex)
s.push({ this.bubbleData.push({
value: value, value: value,
realValue: value, realValue: value,
showValue: showValue, showValue: showValue,
@@ -92,153 +96,186 @@ export default {
seriesIndex: expressionIndex, seriesIndex: expressionIndex,
dataIndex: dataIndex, dataIndex: dataIndex,
mapping: mapping, mapping: mapping,
id: colorIndex, // 气泡id
background: mapping ? mapping.color.bac : this.colorList[colorIndex] // 气泡颜色 background: mapping ? mapping.color.bac : this.colorList[colorIndex] // 气泡颜色
}) })
colorIndex++ colorIndex++
}
}) })
}) })
this.$emit('chartIsNoData', this.isNoData) this.$emit('chartIsNoData', this.isNoData)
return s
}, },
formatterFunc: function (params, ticket, callback) { drawBubbleChart () {
const self = this this.$nextTick(() => {
return `<div> d3.select(`#bubble-svg-${this.chartId}`).selectAll('g').remove()// 清空作图区域
<div style="white-space:nowrap;overflow-x:hidden;text-overflow:ellipsis; min-width: 150px; max-width: 600px; line-height: 18px; font-size: 14px;"> let width
<div class="tooltip-title" style="max-width: 500px;white-space:nowrap;overflow-x:hidden;text-overflow:ellipsis;margin-bottom: 5px">${params.data.alias}</div> let height
<div style="font-size:12px;display:flex;justify-content: space-between;"> try {
<div>value</div> const chartWrap = document.getElementById(`chart-canvas-${this.chartId}`)
<div> width = chartWrap.clientWidth
<div style="display: ${params.data.mapping && params.data.mapping.icon ? 'inline-block' : 'none'}"> height = chartWrap.clientHeight
<i class="${params.data.mapping && params.data.mapping.icon}" style="color: ${params.data.mapping && params.data.mapping.color && params.data.mapping.color.icon}"></i> } catch (error) {
</div> }
<div style="display: ${params.data.mapping && params.data.mapping.display ? 'none' : 'inline-block'}">${params.data.showValue}</div> // 定义布局方式
<div style="display: ${params.data.mapping && params.data.mapping.display ? 'inline-block' : 'none'}">${self.handleDisplay(params.data.mapping.display, { ...params.data.labels, value: params.data.showValue })}</div> const pack = d3.pack()
</div> .size([width, height])
</div> .padding(6)
</div>
</div>
`
},
renderItem (params, api) {
// 如果数据全为0 则设置默认值(否则图表不显示) // 如果数据全为0 则设置默认值(否则图表不显示)
let seriesData = lodash.cloneDeep(this.chartOption.dataset.source) let bubbleData = lodash.cloneDeep(this.bubbleData)
if (seriesData.every(item => !item.value || item.value == 0)) { if (bubbleData.every(item => !item.value || item.value == 0)) {
seriesData = seriesData.map(item => { bubbleData = bubbleData.map(item => {
if (item.id !== 'parentNode') {
return { return {
...item, ...item,
value: 100 value: 100
} }
} else {
return {
...item
}
}
}) })
} }
const displayRoot = stratify() const data = d3.hierarchy({ children: bubbleData })
function stratify () {
return d3
.stratify()
.parentId(function (d) {
// 判断是否是父节点
if (d.id !== 'parentNode') {
return 'parentNode'
}
})(seriesData)
.sum(function (d) { .sum(function (d) {
return d.value || 0 return d.value || 0
}) })
.sort(function (a, b) { .sort(function (a, b) {
return b.value - a.value return b.value - a.value
}) })
} const nodes = pack(data).descendants()
function overallLayout (params, api) { const bubbles = d3.select(`#bubble-svg-${this.chartId}`).selectAll('.bubble')
const context = params.context .data(nodes)
d3 .enter()
.pack() .filter(function (d) {
.size([api.getWidth() - 2, api.getHeight() - 2]) return !d.children
.padding(6)(displayRoot)
context.nodes = {}
displayRoot.descendants().forEach(function (node) {
context.nodes[node.id] = node
}) })
} .append('g')
const context = params.context .attr('class', 'bubble')
// Only do that layout once in each time `setOption` called.
// 每次调用“setOption”时只能进行一次布局。
if (!context.layout) {
context.layout = true
overallLayout(params, api)
}
const nodePath = api.value('id')
// const nodeName = nodePath
// .slice(nodePath.lastIndexOf('.') + 1)
// .split(/(?=[A-Z][^A-Z])/g)
// .join('')
const node = context.nodes[nodePath]
if (node.id === 'parentNode') {
node.r = 0
}
if (!node) {
// Reder nothing.
return
}
const z2 = api.value('depth') * 2
return {
type: 'circle',
shape: {
cx: node.x,
cy: node.y,
r: node.r
},
transition: ['shape'],
z2: z2,
textContent: {
type: 'text',
style: {
// transition: isLeaf ? 'fontSize' : null,
text: this.pieFormatterLabel(node),
fill: node.data.mapping ? node.data.mapping.color.text : '#000000',
width: node.r * 1.3,
overflow: 'truncate',
fontSize: node.r / 3,
lineHeight: node.r / 2.2
bubbles.append('circle')
.style('fill', function (d) {
return d.data.background
})
.attr('cx', function (d) {
return d.x
})
.attr('cy', function (d) {
return d.y
})
.attr('r', function (d) {
return d.r
})
bubbles.append('foreignObject')
.attr('width', function (d) {
return d.r * 2
})
.attr('height', function (d) {
return d.r * 2
})
.attr('x', function (d) {
return d.x - d.r
})
.attr('y', function (d) {
return d.y - d.r
})
.style('font-size', function (d) {
return d.r / 3 > 10 ? d.r / 3 : 0
})
.html((d) => {
return this.bubbleFormatterLabel(d)
})
bubbles.on('mouseenter', this.bubbleEnter)
bubbles.on('mousemove', this.bubbleMove)
bubbles.on('mouseleave', this.bubbleLeave)
})
}, },
emphasis: { // 处理label
style: { bubbleFormatterLabel (node) {
// overflow: null, let str = ''
// fontSize: Math.max(node.r / 3, 12) let valueStr = ''
if (this.chartInfo.param.text === 'all') {
str += node.data.alias
valueStr = node.data.mapping && node.data.mapping.display ? this.handleDisplay(node.data.mapping.display, { ...node.data.labels, value: node.data.showValue }) : node.data.showValue
} }
if (this.chartInfo.param.text === 'value' || !this.chartInfo.param.text) {
valueStr = node.data.mapping && node.data.mapping.display ? this.handleDisplay(node.data.mapping.display, { ...node.data.labels, value: node.data.showValue }) : node.data.showValue
}
if (this.chartInfo.param.text === 'legend') {
str += node.data.alias
}
if (this.chartInfo.param.text === 'none') {
str += ''
}
if (str && valueStr) {
return `
<div style="width:100%;height: 100%;display: flex;align-items: center;justify-content: center;flex-direction: column;cursor: pointer;">
<p style="width: 80%;text-overflow: ellipsis;white-space: nowrap;overflow:hidden;text-align:center">
<span>${str}</span>
</p>
<p style="width: 80%;text-overflow: ellipsis;white-space: nowrap;overflow:hidden;text-align:center">
<i class="${node.data.mapping && node.data.mapping.icon}" style="color: ${node.data.mapping && node.data.mapping.color && node.data.mapping.color.icon};font-size:1em;"></i>
<span>${valueStr}</span>
</p>
</div>
`
} else if (str) {
return `
<div style="width:100%;height: 100%;display: flex;align-items: center;justify-content: center;flex-direction: column;cursor: pointer;">
<p style="width: 80%;text-overflow: ellipsis;white-space: nowrap;overflow:hidden;text-align:center">
<i class="${node.data.mapping && node.data.mapping.icon}" style="color: ${node.data.mapping && node.data.mapping.color && node.data.mapping.color.icon};font-size:1em;"></i>
<span>${str}</span>
</p>
</div>
`
} else if (valueStr) {
return `
<div style="width:100%;height: 100%;display: flex;align-items: center;justify-content: center;flex-direction: column;cursor: pointer;">
<p style="width: 80%;text-overflow: ellipsis;white-space: nowrap;overflow:hidden;text-align:center">
<i class="${node.data.mapping && node.data.mapping.icon}" style="color: ${node.data.mapping && node.data.mapping.color && node.data.mapping.color.icon};font-size:1em;"></i>
<span>${valueStr}</span>
</p>
</div>
`
} }
}, },
textConfig: { resize () {
position: 'inside' setTimeout(() => {
this.drawBubbleChart()
}, 50)
}, },
style: { bubbleEnter (e, node) { // 移入六边形
fill: node.data.background this.tooltip.title = node.data.alias
this.tooltip.value = node.data.showValue
this.tooltip.mapping = node.data.mapping
this.tooltip.show = true
this.setPosition(e)
}, },
emphasis: { bubbleMove (e) { // 六边形内移动
style: { if (this.tooltip.show) {
// shadowBlur: 20, this.setPosition(e)
// shadowOffsetX: 3,
// shadowOffsetY: 5,
// shadowColor: 'rgba(0,0,0,0.3)'
} }
},
bubbleLeave () {
this.tooltip.show = false
},
setPosition (e) {
const windowWidth = window.innerWidth// 窗口宽度
const windowHeight = window.innerHeight// 窗口高度
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.y = e.pageY + 15
this.tooltip.x = e.pageX + 15
} }
} }
}, },
mounted () { mounted () {
// eslint-disable-next-line vue/no-mutating-props this.colorList = initColor(20)
this.chartOption.color || (this.chartOption.color = initColor(20))
this.colorList = this.chartOption.color
try {
this.isStack = this.chartInfo.param.stack
} catch (e) {}
this.chartInfo.loaded && this.initChart(this.chartOption) this.chartInfo.loaded && this.initChart(this.chartOption)
} }
} }

View File

@@ -1,23 +0,0 @@
const chartBubble = {
dataset: {
source: []
},
tooltip: {
show: true,
trigger: 'item',
confine: false,
extraCssText: 'z-index:1000;',
z: 9,
animation: false,
appendToBody: true,
className: 'chart-bubble'
},
hoverLayerThreshold: Infinity,
series: [{
type: 'custom',
renderItem: undefined,
progressive: 0,
coordinateSystem: 'none'
}]
}
export default chartBubble

View File

@@ -1,7 +1,6 @@
import { chartType } from '@/components/common/js/constants' import { chartType } from '@/components/common/js/constants'
import chartBarOption from './options/chartBar' import chartBarOption from './options/chartBar'
import chartPieOption from './options/chartPie' import chartPieOption from './options/chartPie'
import chartBubbleOption from './options/chartBubble'
import lodash from 'lodash' import lodash from 'lodash'
import { import {
chartTimeSeriesLineOption, chartTimeSeriesLineOption,
@@ -40,10 +39,6 @@ export function getOption (type) {
chartOption = lodash.cloneDeep(chartPieOption) chartOption = lodash.cloneDeep(chartPieOption)
break break
} }
case chartType.bubble: {
chartOption = lodash.cloneDeep(chartBubbleOption)
break
}
case chartType.treemap: { case chartType.treemap: {
chartOption = lodash.cloneDeep(chartTreemapOption) chartOption = lodash.cloneDeep(chartTreemapOption)
break break