NEZ-2591 fix:重构漏斗图
This commit is contained in:
@@ -697,4 +697,22 @@
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.funnel-label-wrap{
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
transform: translate(-50%,-50%);
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.funnel-label{
|
||||
cursor: pointer;
|
||||
width: min-content;
|
||||
pointer-events: auto;
|
||||
line-height: 16px;
|
||||
color: $--color-text-primary;
|
||||
transition: all 0.7s;
|
||||
}
|
||||
}
|
||||
@@ -250,7 +250,7 @@ import chartTopology from './chart/chartTopology'
|
||||
import chartBubble from './chart/chartBubble'
|
||||
import chartRank from './chart/chartRank'
|
||||
import chartSankey from './chart/chartSankey'
|
||||
import chartFunnel from './chart/chartFunnel'
|
||||
import chartFunnel from './chart/chartFunnelNew'
|
||||
import { getOption, isTimeSeries, isHexagon, isUrl, isText, isChartPie, isChartBar, isTreemap, isLog, isStat, isDiagram, isGroup, isAutotopology, isMap, isAssetInfo, isEndpointInfo, isTable, isGauge, isClock, isTopology, isChartBubble, isChartRank, isSankey, isFunnel } from './chart/tools'
|
||||
import lodash from 'lodash'
|
||||
|
||||
|
||||
377
nezha-fronted/src/components/chart/chart/chartFunnelNew.vue
Normal file
377
nezha-fronted/src/components/chart/chart/chartFunnelNew.vue
Normal file
@@ -0,0 +1,377 @@
|
||||
<template>
|
||||
<div
|
||||
ref="funnel-chart-box"
|
||||
class="nz-chart__component"
|
||||
>
|
||||
<div :id="`chart-canvas-${chartId}`" class="chart__canvas">
|
||||
<svg :id="`funnel-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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as d3 from 'd3'
|
||||
import chartMixin from '@/components/chart/chartMixin'
|
||||
import chartFormat from '@/components/chart/chartFormat'
|
||||
import { getMetricTypeValue } from '@/components/common/js/tools'
|
||||
import chartDataFormat from '@/components/chart/chartDataFormat'
|
||||
import { initColor } from '@/components/chart/chart/tools'
|
||||
import lodash from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'chart-funnel',
|
||||
components: {
|
||||
},
|
||||
mixins: [chartMixin, chartFormat],
|
||||
props: {
|
||||
chartInfo: Object,
|
||||
chartData: Array,
|
||||
chartOption: Object,
|
||||
isFullscreen: Boolean
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
colorList: [],
|
||||
isInit: true, // 是否是初始化,初始化时为true,图表初始化结束后设为false
|
||||
chartId: '',
|
||||
funnelData: [],
|
||||
tooltip: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
title: 0,
|
||||
value: 0,
|
||||
mapping: {},
|
||||
show: false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initChart (animate) {
|
||||
this.legends = []
|
||||
this.initData(this.chartInfo, this.chartData)
|
||||
if (this.isNoData) {
|
||||
return
|
||||
}
|
||||
/* 使用setTimeout延迟渲染图表,避免样式错乱 */
|
||||
setTimeout(() => {
|
||||
this.drawChart(animate)
|
||||
}, 200)
|
||||
},
|
||||
initData (chartInfo, originalDatas) {
|
||||
this.funnelData = []
|
||||
let colorIndex = 0
|
||||
const decimals = this.chartInfo.param.decimals || 2
|
||||
this.isNoData = true
|
||||
originalDatas.forEach((originalData, expressionIndex) => {
|
||||
originalData.forEach((data, dataIndex) => {
|
||||
this.isNoData = false
|
||||
const value = getMetricTypeValue(data.values, chartInfo.param.statistics)
|
||||
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 legend = this.handleLegend(chartInfo, data, expressionIndex, dataIndex, colorIndex)
|
||||
this.funnelData.push({
|
||||
value: Number(value),
|
||||
realValue: value,
|
||||
showValue: showValue,
|
||||
name: legend.name,
|
||||
alias: legend.alias,
|
||||
labels: {
|
||||
...data.metric,
|
||||
legend: legend.alias
|
||||
},
|
||||
seriesIndex: expressionIndex,
|
||||
dataIndex: dataIndex,
|
||||
mapping: mapping,
|
||||
background: mapping ? mapping.color.bac.substr(0, 7) : this.colorList[colorIndex], // 仅限十六进制
|
||||
label: legend.alias
|
||||
})
|
||||
colorIndex++
|
||||
})
|
||||
})
|
||||
this.funnelData.sort((a, b) => b.value - a.value)
|
||||
this.$emit('chartIsNoData', this.isNoData)
|
||||
},
|
||||
drawChart (animate) {
|
||||
let funnelData = lodash.cloneDeep(this.funnelData)
|
||||
const everyZeor = funnelData.every(item => item.value == 0)
|
||||
// 判断值是否全部为0
|
||||
if (everyZeor) {
|
||||
funnelData = funnelData.map(item => {
|
||||
return {
|
||||
...item,
|
||||
value: 100
|
||||
}
|
||||
})
|
||||
} else {
|
||||
funnelData = funnelData.filter(item => item.value !== 0)
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
d3.select(`#funnel-svg-${this.chartId}`).selectAll('g').remove()// 清空作图区域
|
||||
// 获取svg宽高 初始化画布
|
||||
const svgDom = document.getElementById(`funnel-svg-${this.chartId}`)
|
||||
if (!svgDom) {
|
||||
return false
|
||||
}
|
||||
const width = svgDom.getBoundingClientRect().width
|
||||
const height = svgDom.getBoundingClientRect().height
|
||||
const margin = 20
|
||||
const chartWidth = width - margin * 2
|
||||
const chartHeight = height - margin * 2
|
||||
|
||||
// 每个块的高度
|
||||
const trapezoidPadding = 2
|
||||
const trapezoidsHeight = (chartHeight - trapezoidPadding * (funnelData.length - 1)) / funnelData.length
|
||||
|
||||
const scale = d3.scaleLinear()
|
||||
.domain([0, d3.max(funnelData, (d) => d.value)])
|
||||
.range([0, chartWidth * 0.8])
|
||||
|
||||
// 数据处理
|
||||
funnelData = funnelData.map((d, i, array) => {
|
||||
d.index = i
|
||||
if (i !== array.length - 1) {
|
||||
d.nextValue = array[i + 1].value
|
||||
} else {
|
||||
d.nextValue = 0
|
||||
}
|
||||
return d
|
||||
}
|
||||
)
|
||||
const svg = d3.select(`#funnel-svg-${this.chartId}`)
|
||||
const chart = svg.append('g').attr('width', chartWidth).attr('height', chartHeight).attr('transform', `translate(${margin}, ${margin})`)
|
||||
|
||||
// 渲染梯形
|
||||
const trapezoids = chart
|
||||
.append('g')
|
||||
.attr('class', 'traps')
|
||||
.attr('transform', 'translate(' + chartWidth / 2 + ',0)')
|
||||
.selectAll('.trap')
|
||||
.data(funnelData)
|
||||
trapezoids.enter()
|
||||
.append('polygon')
|
||||
.attr('class', (d) => 'trap trap-' + d.index)
|
||||
.merge(trapezoids)
|
||||
.attr('points', (d) => getPoints(scale(d.value), scale(d.nextValue), trapezoidsHeight))
|
||||
.attr('transform', (d, i) => 'translate(0,' + i * (trapezoidPadding + trapezoidsHeight) + ')')
|
||||
.attr('fill', (d) => d.background)
|
||||
.style('opacity', 0)
|
||||
.transition('opacity').duration(animate === true || this.isInit ? 600 : 0)
|
||||
.style('opacity', 1)
|
||||
.style('cursor', 'pointer')
|
||||
trapezoids.exit()
|
||||
.remove()
|
||||
|
||||
// 绑定交互事件
|
||||
chart.selectAll('.trap')
|
||||
.on('mouseover', (e, d) => {
|
||||
// 划过变色
|
||||
d3.select(e.target).transition('fill').attr('fill', this.shade(d.background, 0.2))
|
||||
this.chartEnter(e, d)
|
||||
})
|
||||
.on('mousemove', this.chartMove)
|
||||
.on('mouseleave', (e, d) => {
|
||||
d3.select(e.target).transition('fill').attr('fill', d.background)
|
||||
this.chartLeave()
|
||||
})
|
||||
|
||||
function getPoints (topWidth, bottomWidth, height) {
|
||||
const points = []
|
||||
points.push(-topWidth / 2 + ',' + 0)
|
||||
points.push(topWidth / 2 + ',' + 0)
|
||||
if (bottomWidth === 0) {
|
||||
points.push(0 + ',' + height)
|
||||
} else {
|
||||
points.push(bottomWidth / 2 + ',' + height)
|
||||
points.push(-bottomWidth / 2 + ',' + height)
|
||||
}
|
||||
return points.join(' ')
|
||||
}
|
||||
|
||||
// 渲染文本标签
|
||||
const texts = chart.select('.traps')
|
||||
.selectAll('.label')
|
||||
.data(funnelData)
|
||||
texts.enter()
|
||||
.append('foreignObject')
|
||||
.attr('class', 'label')
|
||||
.merge(texts)
|
||||
.html((d) => this.formatterLabel(d, trapezoidsHeight))
|
||||
.attr('x', 0)
|
||||
.attr('y', (d, i) => i * (trapezoidPadding + trapezoidsHeight) + trapezoidsHeight / 2)
|
||||
// 绑定交互事件
|
||||
.on('mouseover', (e, d) => {
|
||||
chart.select('.trap-' + d.index).transition('fill').attr('fill', this.shade(d.background, 0.2))
|
||||
this.chartEnter(e, d)
|
||||
})
|
||||
.on('mousemove', this.chartMove)
|
||||
.on('mouseleave', (e, d) => {
|
||||
chart.select('.trap-' + d.index).transition('fill').attr('fill', d.background)
|
||||
this.chartLeave()
|
||||
})
|
||||
.style('overflow', 'visible')
|
||||
.style('position', 'relative')
|
||||
.style('opacity', 0)
|
||||
.transition('opacity').duration(animate === true || this.isInit ? 600 : 0)
|
||||
.style('opacity', 1)
|
||||
texts.exit()
|
||||
.remove()
|
||||
|
||||
this.isInit = false
|
||||
})
|
||||
},
|
||||
// 处理label
|
||||
formatterLabel (data, height) {
|
||||
let str = ''
|
||||
let valueStr = ''
|
||||
if (this.chartInfo.param.text === 'all') {
|
||||
str += data.alias
|
||||
valueStr = data.mapping && data.mapping.display ? this.handleDisplay(data.mapping.display, { ...data.labels, value: data.showValue }) : data.showValue
|
||||
}
|
||||
if (this.chartInfo.param.text === 'value' || !this.chartInfo.param.text) {
|
||||
valueStr = data.mapping && data.mapping.display ? this.handleDisplay(data.mapping.display, { ...data.labels, value: data.showValue }) : data.showValue
|
||||
}
|
||||
if (this.chartInfo.param.text === 'legend') {
|
||||
str += data.alias
|
||||
}
|
||||
if (this.chartInfo.param.text === 'none') {
|
||||
str += ''
|
||||
}
|
||||
if (str && valueStr) {
|
||||
if (height < 32) {
|
||||
return ''
|
||||
}
|
||||
return `<div class="funnel-label-wrap">
|
||||
<p class="funnel-label" style="color: ${data.mapping && data.mapping.color && data.mapping.color.text};">
|
||||
<span>${str}</span>
|
||||
</p>
|
||||
<p class="funnel-label" style="color: ${data.mapping && data.mapping.color && data.mapping.color.text};">
|
||||
<i class="${data.mapping && data.mapping.icon}" style="color: ${data.mapping && data.mapping.color && data.mapping.color.icon};font-size:1em;"></i>
|
||||
<span>${valueStr}</span>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
} else if (str) {
|
||||
if (height < 16) {
|
||||
return ''
|
||||
}
|
||||
return `<div class="funnel-label-wrap">
|
||||
<p class="funnel-label" style="color: ${data.mapping && data.mapping.color && data.mapping.color.text};">
|
||||
<i class="${data.mapping && data.mapping.icon}" style="color: ${data.mapping && data.mapping.color && data.mapping.color.icon};font-size:1em;"></i>
|
||||
<span>${str}</span>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
} else if (valueStr) {
|
||||
if (height < 16) {
|
||||
return ''
|
||||
}
|
||||
return `<div class="funnel-label-wrap">
|
||||
<p class="funnel-label" style="color: ${data.mapping && data.mapping.color && data.mapping.color.text};">
|
||||
<i class="${data.mapping && data.mapping.icon}" style="color: ${data.mapping && data.mapping.color && data.mapping.color.icon};font-size:1em;"></i>
|
||||
<span>${valueStr}</span>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
},
|
||||
resize () {
|
||||
setTimeout(() => {
|
||||
this.drawChart()
|
||||
}, 50)
|
||||
},
|
||||
chartEnter (e, data) {
|
||||
this.tooltip.title = data.alias
|
||||
this.tooltip.value = data.showValue
|
||||
this.tooltip.mapping = data.mapping
|
||||
this.tooltip.show = true
|
||||
this.setPosition(e)
|
||||
},
|
||||
chartMove (e) {
|
||||
this.setPosition(e)
|
||||
},
|
||||
chartLeave () {
|
||||
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
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Convert a hex color to an RGB object.
|
||||
*
|
||||
* @param {string} color
|
||||
*
|
||||
* @returns {{R: Number, G: number, B: number}}
|
||||
*/
|
||||
hexToRgb (color) {
|
||||
let hex = color.slice(1)
|
||||
if (hex.length === 3) {
|
||||
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
|
||||
}
|
||||
const f = parseInt(hex, 16)
|
||||
const R = f >> 16
|
||||
const G = (f >> 8) & 0x00FF
|
||||
const B = f & 0x0000FF
|
||||
return { R, G, B }
|
||||
},
|
||||
/**
|
||||
* Shade a color to the given percentage.
|
||||
*
|
||||
* @param {string} color A hex color.
|
||||
* @param {number} shade The shade adjustment. Can be positive or negative.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
shade (color, shade) {
|
||||
const { R, G, B } = this.hexToRgb(color)
|
||||
const t = shade < 0 ? 0 : 255
|
||||
const p = shade < 0 ? shade * -1 : shade
|
||||
|
||||
const converted = 0x1000000 +
|
||||
((Math.round((t - R) * p) + R) * 0x10000) +
|
||||
((Math.round((t - G) * p) + G) * 0x100) +
|
||||
(Math.round((t - B) * p) + B)
|
||||
|
||||
return `#${converted.toString(16).slice(1)}`
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.colorList = initColor(20)
|
||||
this.chartInfo.loaded && this.initChart(true)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user