NEZ-2178 feat:新增 rank 排行榜图表类型

This commit is contained in:
zyh
2022-09-09 09:51:58 +08:00
parent b119b9e762
commit 919b122b74
11 changed files with 373 additions and 7 deletions

View File

@@ -634,3 +634,13 @@
.chart-label{
color: $--color-text-primary;
}
.tickTest{
font-size: 12px;
color: $--color-text-primary;
}
.tickLine{
stroke: $--background-color-empty;
}
.tickLineFirst{
stroke: $--color-text-primary;;
}

View File

@@ -35,6 +35,15 @@
:is-fullscreen="isFullscreen"
@chartIsNoData="chartIsNoData"
></chart-bubble>
<chart-rank
:ref="'chart' + chartInfo.id"
v-if="isChartRank(chartInfo.type)"
:chart-data="chartData"
:chart-info="chartInfo"
:chart-option="chartOption"
:is-fullscreen="isFullscreen"
@chartIsNoData="chartIsNoData"
></chart-rank>
<chart-bar
:ref="'chart' + chartInfo.id"
v-if="isChartBar(chartInfo.type)"
@@ -220,6 +229,7 @@ import chartLog from './chart/chartLog'
import chartNoData from './chart/chartNoData'
import chartPie from './chart/chartPie'
import chartBubble from './chart/chartBubble'
import chartRank from './chart/chartRank'
import chartStat from './chart/chartStat'
import chartTable from './chart/chartTable'
import chartText from './chart/chartText'
@@ -230,7 +240,7 @@ import chartValue from './chart/chartValue'
import chartHexagonD3 from './chart/chartHexagonD3'
import chartMap from './chart/chartMap'
import chartTopology from './chart/chartTopology'
import { getOption, isTimeSeries, isHexagon, isUrl, isText, isChartPie, isChartBubble, isChartBar, isTreemap, isLog, isStat, isDiagram, isGroup, isAutotopology, isMap, isAssetInfo, isEndpointInfo, isTable, isGauge, isSankey, isClock, isTopology } from './chart/tools'
import { getOption, isTimeSeries, isHexagon, isUrl, isText, isChartPie, isChartBubble, isChartRank, isChartBar, isTreemap, isLog, isStat, isDiagram, isGroup, isAutotopology, isMap, isAssetInfo, isEndpointInfo, isTable, isGauge, isSankey, isClock, isTopology } from './chart/tools'
import lodash from 'lodash'
export default {
@@ -250,6 +260,7 @@ export default {
chartNoData,
chartPie,
chartBubble,
chartRank,
chartStat,
chartTable,
chartText,
@@ -318,6 +329,7 @@ export default {
isHexagon,
isChartPie,
isChartBubble,
isChartRank,
isChartBar,
isUrl,
isText,

View File

@@ -1,6 +1,6 @@
<template>
<div
ref="pie-chart-box"
ref="bubble-chart-box"
class="nz-chart__component"
>
<div :id="`chart-canvas-${chartId}`" class="chart__canvas">

View File

@@ -0,0 +1,314 @@
<template>
<div
ref="rank-chart-box"
class="nz-chart__component"
>
<div :id="`chart-canvas-${chartId}`" class="chart__canvas">
<svg :id="`rank-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 chartMixin from '@/components/chart/chartMixin'
import chartFormat from '@/components/chart/chartFormat'
import * as d3 from 'd3'
import { formatScientificNotation, 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-rank',
components: {
},
mixins: [chartMixin, chartFormat],
props: {
chartInfo: Object,
chartData: Array,
chartOption: Object,
isFullscreen: Boolean
},
computed: {
},
data () {
return {
colorList: [],
isInit: true, // 是否是初始化初始化时为true图表初始化结束后设为false
chartId: '',
rankData: [],
tooltip: {
x: 0,
y: 0,
title: 0,
value: 0,
mapping: {},
show: false
}
}
},
methods: {
initChart () {
this.legends = []
this.initRankData(this.chartInfo, this.chartData)
this.isNoData = !this.rankData.length
this.$emit('chartIsNoData', this.isNoData)
if (this.isNoData) {
return
}
/* 使用setTimeout延迟渲染图表避免样式错乱 */
setTimeout(() => {
this.drawRankChart()
this.isInit = false
}, 200)
},
initRankData (chartInfo, originalDatas) {
this.rankData = []
let colorIndex = 0
const decimals = this.chartInfo.param.decimals || 2
this.isNoData = true
originalDatas.forEach((originalData, expressionIndex) => {
originalData.forEach((data, dataIndex) => {
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.rankData.push({
value: value,
realValue: value,
showValue: showValue,
name: legend.name,
alias: legend.alias,
labels: {
...data.metric,
legend: legend.alias
},
seriesIndex: expressionIndex,
dataIndex: dataIndex,
mapping: mapping
})
colorIndex++
})
})
},
drawRankChart () {
this.$nextTick(() => {
d3.select(`#rank-svg-${this.chartId}`).selectAll('g').remove()// 清空作图区域
// 获取svg宽高 初始化画布
const svgDom = document.getElementById(`rank-svg-${this.chartId}`)
if (!svgDom) {
return false
}
const width = svgDom.getBoundingClientRect().width
const height = svgDom.getBoundingClientRect().height
const bodyX = 50
const bodyY = 50
const bodyWidth = width - 2 * bodyX
const bodyHeight = height - 2 * bodyY
const svg = d3.select(`#rank-svg-${this.chartId}`)
// 从大到小排序
let rankData = lodash.cloneDeep(this.rankData)
// 过滤掉为0的数据
rankData = rankData.filter(item => item.value != 0)
rankData.sort((a, b) => b.value - a.value)
rankData = rankData.map((item, index) => {
return {
rank: index,
background: item.mapping ? item.mapping.color.bac : this.colorList[index],
...item
}
})
// 尺度转换
const scaleX = d3.scaleLinear()
.domain([0, d3.max(rankData, (d) => parseFloat(d.value))])
.range([0, bodyWidth])
const scaleY = d3.scaleBand()
.domain(rankData.map((d) => d.rank).sort((a, b) => b - a))
.range([bodyHeight, 0])
.paddingInner(0.3)
.paddingOuter(0.06)
// 渲染柱形
const bars = svg.append('g')
.attr('transform', `translate(${bodyX},${bodyY})`)
.selectAll()
.data(rankData)
bars.enter()
.append('rect')
.merge(bars)
.attr('x', scaleX(0))
.attr('y', (d) => scaleY(d.rank))
.attr('height', scaleY.bandwidth())
.attr('fill', (d) => d.background)
.attr('width', (d) => scaleX(d.value) > 10 ? scaleX(d.value) : 10)
bars.exit().remove()
// 坐标轴
const xAxis = d3.axisTop(scaleX)
.ticks(6)
.tickSize(-(bodyHeight))
// .tickSizeOuter(0)
// .tickSizeInner(0)
.tickFormat(d => {
const value = formatScientificNotation(d, 6)
let chartUnit = this.chartInfo.unit
chartUnit = chartUnit || 2
const unit = chartDataFormat.getUnit(chartUnit)
return unit.compute(value, null, -1, 2)
})
const ticks = svg.append('g')
.attr('class', 'xAxis')
.call(xAxis)
.attr('transform', `translate(${bodyX},${bodyY})`)
ticks.select('.tick:first-of-type text').remove()
ticks.selectAll('.tick text').attr('class', 'tickTest')
ticks.select('.tick:first-of-type line').attr('class', 'tickLineFirst')
ticks.selectAll('.tick:not(:first-of-type) line').attr('class', 'tickLine')
ticks.select('.domain').remove()
// 文本标签
svg.append('g')
.attr('transform', `translate(${bodyX},${bodyY})`)
.selectAll()
.data(rankData)
.enter()
.append('foreignObject')
.on('mouseenter', this.rankEnter)
.on('mousemove', this.rankMove)
.on('mouseleave', this.rankLeave)
.html((d) => {
let flag = true
// 判断柱子宽度和高度
if (scaleY.bandwidth() < 38 || scaleX(d.value) < 120) {
flag = false
}
return flag ? this.rankFormatterLabel(d) : ''
})
.attr('x', scaleX(0))
.attr('y', (d) => scaleY(d.rank))
.attr('height', scaleY.bandwidth())
.attr('width', (d) => scaleX(d.value) > 10 ? scaleX(d.value) : 10)
.style('cursor', 'pointer')
})
},
// 处理label
rankFormatterLabel (data) {
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) {
return `
<div style="width:100%;height: 100%;display: flex;align-items: flex-end;justify-content: center;flex-direction: column;color:#000000">
<p style="width:100%;padding:0 12px;box-sizing: border-box;text-overflow: ellipsis;white-space: nowrap;overflow:hidden;text-align:right;color: ${data.mapping && data.mapping.color && data.mapping.color.text}">
<span>${str}</span>
</p>
<p style="width:100%;padding:0 12px;box-sizing: border-box;text-overflow: ellipsis;white-space: nowrap;overflow:hidden;text-align:right;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) {
return `
<div style="width:100%;height: 100%;display: flex;align-items: flex-end;justify-content: center;flex-direction: column;color:#000000">
<p style="width:100%;padding:0 12px;box-sizing: border-box;text-overflow: ellipsis;white-space: nowrap;overflow:hidden;text-align:right;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) {
return `
<div style="width:100%;height: 100%;display: flex;align-items: flex-end;justify-content: center;flex-direction: column;color:#000000">
<p style="width:100%;padding:0 12px;box-sizing: border-box;text-overflow: ellipsis;white-space: nowrap;overflow:hidden;text-align:right;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>
`
}
},
rankEnter (e, data) { // 移入
this.tooltip.title = data.alias
this.tooltip.value = data.showValue
this.tooltip.mapping = data.mapping
this.tooltip.show = true
this.setPosition(e)
},
rankMove (e) { // 移动
if (this.tooltip.show) {
this.setPosition(e)
}
},
rankLeave () { // 移出
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
}
},
resize () {
setTimeout(() => {
this.drawRankChart()
}, 50)
}
},
mounted () {
this.colorList = initColor(20)
this.chartInfo.loaded && this.initChart()
}
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div
ref="pie-chart-box"
ref="sankey-chart-box"
class="nz-chart__component nz-chart__component--time-series" @mouseenter="mouseEnterChart"
@mouseleave="mouseLeaveChart"
index="300"
@@ -95,9 +95,9 @@ export default {
this.nodesData = []
const sankeyData = []
const decimals = this.chartInfo.param.decimals || 2
this.isNoData = true
originalDatas.forEach((originalData) => {
originalData.forEach((data, dataIndex) => {
this.isNoData = false
const value = getMetricTypeValue(data.values, chartInfo.param.statistics)
const obj = {
value: value,

View File

@@ -71,6 +71,9 @@ export function isChartPie (type) {
export function isChartBubble (type) {
return type === chartType.bubble
}
export function isChartRank (type) {
return type === chartType.rank
}
export function isChartBar (type) {
return type === chartType.bar
}

View File

@@ -411,6 +411,7 @@ export const chartType = {
sankey: 'sankey',
pie: 'pie',
bubble: 'bubble',
rank: 'rank',
treemap: 'treemap',
log: 'log',
text: 'text',

View File

@@ -1020,6 +1020,10 @@ export default {
id: 'bubble',
name: this.$t('dashboard.panel.chartForm.typeVal.bubble.label')
},
{
id: 'rank',
name: this.$t('dashboard.panel.chartForm.typeVal.rank.label')
},
{
id: 'log',
name: this.$t('dashboard.panel.chartForm.typeVal.log.label')
@@ -1107,7 +1111,8 @@ export default {
case 'gauge':
case 'sankey':
case 'bubble':
if (this.oldType === 'stat' || this.oldType === 'gauge' || this.oldType === 'sankey' || this.oldType === 'hexagon' || this.oldType === 'bubble') {
case 'rank':
if (this.oldType === 'stat' || this.oldType === 'gauge' || this.oldType === 'sankey' || this.oldType === 'hexagon' || this.oldType === 'bubble' || this.oldType === 'rank') {
break
}
this.chartConfig.param = {

View File

@@ -56,6 +56,7 @@ export default {
case 'gauge':
case 'pie':
case 'bubble':
case 'rank':
return false
default: return false
}
@@ -74,6 +75,7 @@ export default {
case 'gauge':
case 'pie':
case 'bubble':
case 'rank':
case 'sankey':
return true
default: return false
@@ -94,6 +96,7 @@ export default {
case 'gauge':
case 'sankey':
case 'bubble':
case 'rank':
return false
default: return false
}
@@ -112,6 +115,7 @@ export default {
case 'treemap':
case 'pie':
case 'bubble':
case 'rank':
case 'bar':
return false
default: return false
@@ -141,6 +145,7 @@ export default {
case 'treemap':
case 'pie':
case 'bubble':
case 'rank':
return true
default: return false
}
@@ -152,6 +157,7 @@ export default {
case 'treemap':
case 'pie':
case 'bubble':
case 'rank':
case 'stat':
case 'hexagon':
case 'gauge':
@@ -171,6 +177,7 @@ export default {
case 'treemap':
case 'pie':
case 'bubble':
case 'rank':
case 'stat':
case 'hexagon':
case 'gauge':
@@ -204,6 +211,7 @@ export default {
case 'treemap':
case 'pie':
case 'bubble':
case 'rank':
case 'stat':
case 'hexagon':
case 'gauge':

View File

@@ -263,6 +263,10 @@ export default {
id: 'bubble',
name: this.$t('dashboard.panel.chartForm.typeVal.bubble.label')
},
{
id: 'rank',
name: this.$t('dashboard.panel.chartForm.typeVal.rank.label')
},
{
id: 'table',
name: this.$t('dashboard.panel.chartForm.typeVal.table.label')
@@ -314,6 +318,10 @@ export default {
id: 'bubble',
name: this.$t('dashboard.panel.chartForm.typeVal.bubble.label')
},
{
id: 'rank',
name: this.$t('dashboard.panel.chartForm.typeVal.rank.label')
},
{
id: 'log',
name: this.$t('dashboard.panel.chartForm.typeVal.log.label')

View File

@@ -888,6 +888,10 @@ export default {
id: 'bubble',
name: this.$t('dashboard.panel.chartForm.typeVal.bubble.label')
},
{
id: 'rank',
name: this.$t('dashboard.panel.chartForm.typeVal.rank.label')
},
{
id: 'gauge',
name: this.$t('dashboard.panel.chartForm.typeVal.gauge.label')
@@ -954,7 +958,8 @@ export default {
case 'hexagon':
case 'gauge':
case 'bubble':
if (this.oldType === 'stat' || this.oldType === 'gauge' || this.oldType === 'hexagon' || this.oldType === 'bubble') {
case 'rank':
if (this.oldType === 'stat' || this.oldType === 'gauge' || this.oldType === 'hexagon' || this.oldType === 'bubble' || this.oldType === 'rank') {
break
}
this.chartConfig.param = {