NEZ-2768 feat:新增圆环图 图表类型

This commit is contained in:
zyh
2023-04-18 10:12:47 +08:00
parent 3db6415686
commit 57469d2bcf
23 changed files with 918 additions and 2227 deletions

View File

@@ -706,3 +706,9 @@ textarea {
.message-zindex{ .message-zindex{
z-index: 5000 !important; z-index: 5000 !important;
} }
.text-ellipsis{
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}

View File

@@ -715,25 +715,40 @@
stroke: $--color-text-primary;; stroke: $--color-text-primary;;
} }
.text-ellipsis{ .no-events{
pointer-events: none !important;
*{
pointer-events: none !important;
}
}
.foreign{
overflow: visible;
.foreign-label-wrap{
white-space: nowrap;
transform: translate(-50%,-50%);
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
}
}
.funnel-label{
cursor: pointer;
pointer-events: auto;
line-height: 16px;
color: $--color-text-primary;
}
.doughnut-label{
cursor: pointer;
pointer-events: auto;
font-size: 12px;
line-height: 14px;
color: #000000;
max-width: 200px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; 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;
}
}

View File

@@ -135,6 +135,7 @@ export default {
case 'stat' : case 'stat' :
case 'gauge' : case 'gauge' :
case 'pie' : case 'pie' :
case 'doughnut' :
case 'treemap' : case 'treemap' :
case 'log' : case 'log' :
case 'hexagon' : case 'hexagon' :

View File

@@ -27,6 +27,15 @@
:is-fullscreen="isFullscreen" :is-fullscreen="isFullscreen"
@chartIsNoData="chartIsNoData" @chartIsNoData="chartIsNoData"
></chart-pie> ></chart-pie>
<chart-doughnut
:ref="'chart' + chartInfo.id"
v-if="isDoughnut(chartInfo.type)"
:chart-data="chartData"
:chart-info="chartInfo"
:chart-option="chartOption"
:is-fullscreen="isFullscreen"
@chartIsNoData="chartIsNoData"
></chart-doughnut>
<chart-bar <chart-bar
:ref="'chart' + chartInfo.id" :ref="'chart' + chartInfo.id"
v-if="isChartBar(chartInfo.type)" v-if="isChartBar(chartInfo.type)"
@@ -239,6 +248,7 @@ import chartGroup from './chart/chartGroup'
import chartLog from './chart/chartLog' import chartLog from './chart/chartLog'
import chartNoData from './chart/chartNoData' import chartNoData from './chart/chartNoData'
import chartPie from './chart/chartPie' import chartPie from './chart/chartPie'
import chartDoughnut from './chart/chartDoughnut'
import chartStat from './chart/chartStat' import chartStat from './chart/chartStat'
import chartTable from './chart/chartTable' import chartTable from './chart/chartTable'
import chartText from './chart/chartText' import chartText from './chart/chartText'
@@ -253,7 +263,7 @@ import chartBubble from './chart/chartBubble'
import chartRank from './chart/chartRank' import chartRank from './chart/chartRank'
import chartSankey from './chart/chartSankey' import chartSankey from './chart/chartSankey'
import chartFunnel from './chart/chartFunnelNew' 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 { getOption, isTimeSeries, isHexagon, isUrl, isText, isChartPie, isDoughnut, 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' import lodash from 'lodash'
export default { export default {
@@ -271,6 +281,7 @@ export default {
chartLog, chartLog,
chartNoData, chartNoData,
chartPie, chartPie,
chartDoughnut,
chartStat, chartStat,
chartTable, chartTable,
chartText, chartText,
@@ -342,6 +353,7 @@ export default {
isTimeSeries, isTimeSeries,
isHexagon, isHexagon,
isChartPie, isChartPie,
isDoughnut,
isChartBar, isChartBar,
isUrl, isUrl,
isText, isText,

View File

@@ -108,86 +108,84 @@ export default {
this.$emit('chartIsNoData', this.isNoData) this.$emit('chartIsNoData', this.isNoData)
}, },
drawBubbleChart () { drawBubbleChart () {
this.$nextTick(() => { this.dispose()
this.dispose() this.svg = d3.select(`#bubble-svg-${this.chartId}`)
this.svg = d3.select(`#bubble-svg-${this.chartId}`) const svgDom = document.getElementById(`bubble-svg-${this.chartId}`)
const svgDom = document.getElementById(`bubble-svg-${this.chartId}`) if (!svgDom) {
if (!svgDom) { return false
return false }
} const width = svgDom.getBoundingClientRect().width
const width = svgDom.getBoundingClientRect().width const height = svgDom.getBoundingClientRect().height
const height = svgDom.getBoundingClientRect().height // 定义布局方式
// 定义布局方式 const pack = d3.pack()
const pack = d3.pack() .size([width, height])
.size([width, height]) .padding(6)
.padding(6)
// 如果数据全为0 则设置默认值(否则图表不显示) // 如果数据全为0 则设置默认值(否则图表不显示)
let bubbleData = lodash.cloneDeep(this.bubbleData) let bubbleData = lodash.cloneDeep(this.bubbleData)
if (bubbleData.every(item => item.value == 0)) { if (bubbleData.every(item => item.value == 0)) {
bubbleData = bubbleData.map(item => { bubbleData = bubbleData.map(item => {
return { return {
...item, ...item,
value: 100 // 目的是显示气泡 与值大小无关 value: 100 // 目的是显示气泡 与值大小无关
} }
}) })
} }
const data = d3.hierarchy({ children: bubbleData }) const data = d3.hierarchy({ children: bubbleData })
.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() const nodes = pack(data).descendants()
const bubbles = this.svg.selectAll('.bubble') const bubbles = this.svg.selectAll('.bubble')
.data(nodes) .data(nodes)
.enter() .enter()
.filter(function (d) { .filter(function (d) {
return !d.children return !d.children
}) })
.append('g') .append('g')
.attr('class', 'bubble') .attr('class', 'bubble')
bubbles.append('circle') bubbles.append('circle')
.style('fill', function (d) { .style('fill', function (d) {
return d.data.background return d.data.background
}) })
.attr('cx', function (d) { .attr('cx', function (d) {
return d.x return d.x
}) })
.attr('cy', function (d) { .attr('cy', function (d) {
return d.y return d.y
}) })
.attr('r', function (d) { .attr('r', function (d) {
return d.r return d.r
}) })
bubbles.append('foreignObject') bubbles.append('foreignObject')
.attr('width', function (d) { .attr('width', function (d) {
return d.r * 2 return d.r * 2
}) })
.attr('height', function (d) { .attr('height', function (d) {
return d.r * 2 return d.r * 2
}) })
.attr('x', function (d) { .attr('x', function (d) {
return d.x - d.r return d.x - d.r
}) })
.attr('y', function (d) { .attr('y', function (d) {
return d.y - d.r return d.y - d.r
}) })
.style('font-size', function (d) { .style('font-size', function (d) {
let fontSize let fontSize
fontSize = d.r / 4 > 10 ? d.r / 4 : 0 fontSize = d.r / 4 > 10 ? d.r / 4 : 0
fontSize = fontSize > 30 ? 30 : fontSize fontSize = fontSize > 30 ? 30 : fontSize
return fontSize return fontSize
}) })
.style('border-radius', '50%') .style('border-radius', '50%')
.html((d) => { .html((d) => {
return this.bubbleFormatterLabel(d) return this.bubbleFormatterLabel(d)
}) })
bubbles.on('mouseenter', this.bubbleEnter) bubbles.on('mouseenter', this.bubbleEnter)
bubbles.on('mousemove', this.bubbleMove) bubbles.on('mousemove', this.bubbleMove)
bubbles.on('mouseleave', this.bubbleLeave) bubbles.on('mouseleave', this.bubbleLeave)
})
}, },
// 处理label // 处理label
bubbleFormatterLabel (node) { bubbleFormatterLabel (node) {

View File

@@ -0,0 +1,365 @@
<template>
<div
:class="legendPlacement"
ref="doughnut-chart-box"
class="nz-chart__component nz-chart__component--time-series"
>
<div :id="`chart-canvas-${chartId}`" class="chart__canvas"></div>
<chart-legend
v-if="hasLegend"
:chart-data="chartData"
:chart-info="chartInfo"
:legends="legends"
:is-fullscreen="isFullscreen"
@clickLegendDoughnut="clickLegendDoughnut"
></chart-legend>
<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 lodash from 'lodash'
import chartFormat from '@/components/chart/chartFormat'
import chartMixin from '@/components/chart/chartMixin'
import chartDataFormat from '@/components/chart/chartDataFormat'
import { getMetricTypeValue } from '@/components/common/js/tools'
import { initColor } from '@/components/chart/chart/tools'
import legend from '@/components/chart/chart/legend'
import { chartLegendPlacement } from '@/components/common/js/constants'
export default {
name: 'chart-doughnut',
components: {
chartLegend: legend
},
mixins: [chartMixin, chartFormat],
props: {
chartInfo: Object,
chartData: Array,
chartOption: Object,
isFullscreen: Boolean
},
computed: {
hasLegend () {
try {
return [chartLegendPlacement.bottom, chartLegendPlacement.left, chartLegendPlacement.right].indexOf(this.chartInfo.param.legend.placement) > -1
} catch (e) {
return false
}
},
legendPlacement () {
try {
switch (this.chartInfo.param.legend.placement) {
case 'left':
case 'right':
case 'bottom': {
return `nz-chart__component--${this.chartInfo.param.legend.placement}`
}
default: return ''
}
} catch (e) {
return ''
}
}
},
data () {
return {
colorList: [],
isInit: true, // 是否是初始化初始化时为true图表初始化结束后设为false
chartId: '',
doughnutData: [],
selectData: [],
tooltip: {
x: 0,
y: 0,
title: 0,
value: 0,
mapping: {},
show: false
},
svg: null
}
},
methods: {
initChart (animate) {
this.legends = []
this.initDoughnutData(this.chartInfo, this.chartData)
this.selectData = this.$loadsh.cloneDeep(this.doughnutData)
if (this.isNoData) {
return
}
/* 使用setTimeout延迟渲染图表避免样式错乱 */
setTimeout(() => {
this.drawDoughnutChart(animate)
this.isInit = false
}, 200)
},
initDoughnutData (chartInfo, originalDatas) {
this.doughnutData = []
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.doughnutData.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,
background: mapping ? mapping.color.bac : this.colorList[colorIndex]
})
colorIndex++
})
})
this.$emit('chartIsNoData', this.isNoData)
},
drawDoughnutChart (animate) {
this.dispose()
const svgDom = document.getElementById(`chart-canvas-${this.chartId}`)
if (!svgDom) {
return false
}
// 如果数据全为0 则设置默认值(否则图表不显示)
let doughnutData = lodash.cloneDeep(this.selectData)
if (doughnutData.every(item => item.value == 0)) {
doughnutData = doughnutData.map(item => {
return {
...item,
value: 100
}
})
}
const width = svgDom.getBoundingClientRect().width
const height = svgDom.getBoundingClientRect().height
this.svg = d3.select(`#chart-canvas-${this.chartId}`).append('svg').attr('height', height).attr('width', width).attr('viewBox', [-width / 2, -height / 2, width, height]).style('display', 'block')
const chart = this.svg.append('g')
const outerRadius = (Math.min(width, height) / 2) * 0.6 // outer radius of pie, in pixels
const innerRadius = outerRadius * 0.67 // inner radius of pie, in pixels (non-zero for donut)
const padAngle = 0 // angular separation between wedges
const arc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius)
const pie = d3.pie()
.padAngle(padAngle)
.sort(null)
.value(d => d.value)
const arcs = pie(doughnutData)
function doughnutOver (e, d) {
d3.select(chart.selectAll('path').nodes()[d.index])
.transition()
.attr('d', d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius * 1.05)
)
}
function doughnutOut (e, d) {
d3.select(chart.selectAll('path').nodes()[d.index])
.transition()
.attr('d', d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
)
}
const that = this
// 图形
chart.selectAll('path')
.data(arcs)
.join('path')
.attr('fill', d => d.data.background)
.attr('d', arc)
.style('cursor', 'pointer')
.classed('no-events', true)
.on('mouseover', doughnutOver)
.on('mouseout', doughnutOut)
.on('mouseenter', that.doughnutEnter)
.on('mousemove', that.doughnutMove)
.on('mouseleave', that.doughnutLeave)
.transition().duration(animate === true ? 600 : 0)
.attrTween('d', (d) => {
let currentArc = this._current
if (!currentArc) {
currentArc = { startAngle: 0, endAngle: 0 }
}
const i = d3.interpolate(currentArc, d)
this._current = i(0) // 当饼图更新时,从当前角度过渡到新角度
return function (t) {
return arc(i(t))
}
})
.on('end', function () {
d3.select(this).classed('no-events', false)
})
// 文本
chart
.selectAll('foreignObject')
.data(arcs)
.join('foreignObject')
.attr('transform', d => `translate(${arc.centroid(d)})`)
.attr('class', 'foreign')
.classed('no-events', true)
.on('mouseover', doughnutOver)
.on('mouseout', doughnutOut)
.on('mouseenter', this.doughnutEnter)
.on('mousemove', this.doughnutMove)
.on('mouseleave', this.doughnutLeave)
.style('opacity', 0)
.html((d) => {
return d.endAngle - d.startAngle > 0.25 ? this.formatterLabel(d) : ''
})
.transition('opacity').duration(animate === true ? 600 : 0)
.style('opacity', 1)
.on('end', function () {
d3.select(this).classed('no-events', false)
})
},
// 处理label
formatterLabel ({ 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 class="foreign-label-wrap">
<p class="doughnut-label" style="color: ${data.mapping && data.mapping.color && data.mapping.color.text};">
<span>${str}</span>
</p>
<p class="doughnut-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) {
return `<div class="foreign-label-wrap">
<p class="doughnut-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) {
return `<div class="foreign-label-wrap">
<p class="doughnut-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>
`
}
},
doughnutEnter (e, node) { // 移入气泡
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)
},
doughnutMove (e) { // 气泡内移动
if (this.tooltip.show) {
this.tooltip.show = true
this.setPosition(e)
}
},
doughnutLeave () { // 移出气泡
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
}
},
clickLegendDoughnut (isGrey) {
const data = this.doughnutData.filter((item, i) => !isGrey[i])
this.selectData = this.$loadsh.cloneDeep(data)
this.drawDoughnutChart(true)
},
resize () {
setTimeout(() => {
this.drawDoughnutChart(false)
}, 50)
},
dispose () {
if (this.svg) {
this.svg.selectAll('path').on('mouseover', null)
this.svg.selectAll('path').on('mouseout', null)
this.svg.selectAll('path').on('mouseenter', null)
this.svg.selectAll('path').on('mousemove', null)
this.svg.selectAll('path').on('mouseleave', null)
this.svg.selectAll('foreignObject').on('mouseover', null)
this.svg.selectAll('foreignObject').on('mouseout', null)
this.svg.selectAll('foreignObject').on('mouseenter', null)
this.svg.selectAll('foreignObject').on('mousemove', null)
this.svg.selectAll('foreignObject').on('mouseleave', null)
this.svg.remove()
this.svg = null
}
}
},
mounted () {
this.colorList = initColor(20)
this.chartInfo.loaded && this.initChart(true)
},
beforeDestroy () {
this.dispose()
}
}
</script>

View File

@@ -122,119 +122,115 @@ export default {
} else { } else {
funnelData = funnelData.filter(item => item.value !== 0) funnelData = funnelData.filter(item => item.value !== 0)
} }
this.$nextTick(() => { this.dispose()
this.dispose() // 获取svg宽高 初始化画布
// 获取svg宽高 初始化画布 const svgDom = document.getElementById(`funnel-svg-${this.chartId}`)
const svgDom = document.getElementById(`funnel-svg-${this.chartId}`) if (!svgDom) {
if (!svgDom) { return false
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
} }
const width = svgDom.getBoundingClientRect().width return d
const height = svgDom.getBoundingClientRect().height }
const margin = 20 )
const chartWidth = width - margin * 2 this.svg = d3.select(`#funnel-svg-${this.chartId}`)
const chartHeight = height - margin * 2 const chart = this.svg.append('g').attr('width', chartWidth).attr('height', chartHeight).attr('transform', `translate(${margin}, ${margin})`)
// 每个块的高度 // 渲染梯形
const trapezoidPadding = 2 const trapezoids = chart
const trapezoidsHeight = (chartHeight - trapezoidPadding * (funnelData.length - 1)) / funnelData.length .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 ? 600 : 0)
.style('opacity', 1)
.style('cursor', 'pointer')
trapezoids.exit()
.remove()
const scale = d3.scaleLinear() // 绑定交互事件
.domain([0, d3.max(funnelData, (d) => d.value)]) chart.selectAll('.trap')
.range([0, chartWidth * 0.8]) .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) {
funnelData = funnelData.map((d, i, array) => { const points = []
d.index = i points.push(-topWidth / 2 + ',' + 0)
if (i !== array.length - 1) { points.push(topWidth / 2 + ',' + 0)
d.nextValue = array[i + 1].value if (bottomWidth === 0) {
} else { points.push(0 + ',' + height)
d.nextValue = 0 } else {
} points.push(bottomWidth / 2 + ',' + height)
return d points.push(-bottomWidth / 2 + ',' + height)
} }
) return points.join(' ')
this.svg = d3.select(`#funnel-svg-${this.chartId}`) }
const chart = this.svg.append('g').attr('width', chartWidth).attr('height', chartHeight).attr('transform', `translate(${margin}, ${margin})`)
// 渲染梯形 // 渲染文本标签
const trapezoids = chart const texts = chart.select('.traps')
.append('g') .selectAll('.foreign')
.attr('class', 'traps') .data(funnelData)
.attr('transform', 'translate(' + chartWidth / 2 + ',0)') texts.enter()
.selectAll('.trap') .append('foreignObject')
.data(funnelData) .attr('class', 'foreign')
trapezoids.enter() .merge(texts)
.append('polygon') .html((d) => this.formatterLabel(d, trapezoidsHeight))
.attr('class', (d) => 'trap trap-' + d.index) .attr('x', 0)
.merge(trapezoids) .attr('y', (d, i) => i * (trapezoidPadding + trapezoidsHeight) + trapezoidsHeight / 2)
.attr('points', (d) => getPoints(scale(d.value), scale(d.nextValue), trapezoidsHeight)) // 绑定交互事件
.attr('transform', (d, i) => 'translate(0,' + i * (trapezoidPadding + trapezoidsHeight) + ')') .on('mouseover', (e, d) => {
.attr('fill', (d) => d.background) chart.select('.trap-' + d.index).transition('fill').attr('fill', this.shade(d.background, 0.2))
.style('opacity', 0) this.chartEnter(e, d)
.transition('opacity').duration(animate === true || this.isInit ? 600 : 0) })
.style('opacity', 1) .on('mousemove', this.chartMove)
.style('cursor', 'pointer') .on('mouseleave', (e, d) => {
trapezoids.exit() chart.select('.trap-' + d.index).transition('fill').attr('fill', d.background)
.remove() this.chartLeave()
})
.style('opacity', 0)
.transition('opacity').duration(animate === true ? 600 : 0)
.style('opacity', 1)
texts.exit()
.remove()
// 绑定交互事件 this.isInit = false
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 // 处理label
formatterLabel (data, height) { formatterLabel (data, height) {
@@ -257,7 +253,7 @@ export default {
if (height < 32) { if (height < 32) {
return '' return ''
} }
return `<div class="funnel-label-wrap"> return `<div class="foreign-label-wrap">
<p class="funnel-label" style="color: ${data.mapping && data.mapping.color && data.mapping.color.text};"> <p class="funnel-label" style="color: ${data.mapping && data.mapping.color && data.mapping.color.text};">
<span>${str}</span> <span>${str}</span>
</p> </p>
@@ -271,7 +267,7 @@ export default {
if (height < 16) { if (height < 16) {
return '' return ''
} }
return `<div class="funnel-label-wrap"> return `<div class="foreign-label-wrap">
<p class="funnel-label" style="color: ${data.mapping && data.mapping.color && data.mapping.color.text};"> <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> <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> <span>${str}</span>
@@ -282,7 +278,7 @@ export default {
if (height < 16) { if (height < 16) {
return '' return ''
} }
return `<div class="funnel-label-wrap"> return `<div class="foreign-label-wrap">
<p class="funnel-label" style="color: ${data.mapping && data.mapping.color && data.mapping.color.text};"> <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> <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> <span>${valueStr}</span>

View File

@@ -115,114 +115,112 @@ export default {
}, },
drawRankChart () { drawRankChart () {
this.$nextTick(() => { this.dispose()
this.dispose() // 获取svg宽高 初始化画布
// 获取svg宽高 初始化画布 const svgDom = document.getElementById(`rank-svg-${this.chartId}`)
const svgDom = document.getElementById(`rank-svg-${this.chartId}`) if (!svgDom) {
if (!svgDom) { return false
return false }
} const width = svgDom.getBoundingClientRect().width
const width = svgDom.getBoundingClientRect().width // 柱子高度
// 柱子高度 const barHeight = 24
const barHeight = 24 // 柱子间隔
// 柱子间隔 const margin = 40
const margin = 40 // 计算svg高度
// 计算svg高度 const height = this.rankData.length * (barHeight + margin) + margin
const height = this.rankData.length * (barHeight + margin) + margin this.svg = d3.select(`#rank-svg-${this.chartId}`).attr('height', height)
this.svg = d3.select(`#rank-svg-${this.chartId}`).attr('height', height) const bodyX = 50
const bodyX = 50 const bodyWidth = width - 3 * bodyX
const bodyWidth = width - 3 * bodyX
// 从大到小排序 // 从大到小排序
let rankData = lodash.cloneDeep(this.rankData) let rankData = lodash.cloneDeep(this.rankData)
rankData.sort((a, b) => b.value - a.value) rankData.sort((a, b) => b.value - a.value)
rankData = rankData.map((item, index) => { rankData = rankData.map((item, index) => {
return { return {
rank: index, rank: index,
background: item.mapping ? item.mapping.color.bac : this.colorList[index], background: item.mapping ? item.mapping.color.bac : this.colorList[index],
...item ...item
}
})
// 尺度转换
const scaleX = d3.scaleLinear()
.domain([0, d3.max(rankData, (d) => parseFloat(d.value))])
.range([0, bodyWidth])
// 柱子最小宽度
const minWidth = 2
// 渲染柱形
const bars = this.svg.append('g')
.attr('transform', `translate(${bodyX})`)
.selectAll()
.data(rankData)
bars.enter()
.append('rect')
.merge(bars)
.attr('x', 0)
.attr('y', (d) => {
return (d.rank * barHeight) + (d.rank + 1) * margin
})
.attr('height', barHeight)
.attr('fill', (d) => d.background)
.attr('width', (d) => {
return scaleX(d.value) > minWidth ? scaleX(d.value) : minWidth
})
.style('cursor', 'pointer')
.on('mouseenter', this.rankEnter)
.on('mousemove', this.rankMove)
.on('mouseleave', this.rankLeave)
bars.exit().remove()
// 文本标签
this.svg.append('g')
.attr('transform', `translate(${bodyX})`)
.selectAll()
.data(rankData)
.enter()
.append('foreignObject')
.on('mouseenter', this.rankEnter)
.on('mousemove', this.rankMove)
.on('mouseleave', this.rankLeave)
.html((d) => {
return this.rankFormatterLabel(d)
})
.attr('x', (d) => {
let x = 0
if (scaleX(d.value) > minWidth) {
x = scaleX(d.value) + 10
} else {
x = minWidth + 10
}
return x
})
.attr('y', (d) => {
return (d.rank * barHeight) + (d.rank + 1) * margin
})
.attr('height', barHeight)
.style('cursor', 'pointer')
.style('overflow', 'visible')
// 生成标签和矩形
this.svg.append('g')
.attr('transform', `translate(${bodyX})`)
.selectAll('text')
.data(rankData)
.enter()
.append('text')
.attr('class', 'chart-label-text')
.attr('x', 4)
.attr('y', d => {
return (d.rank * barHeight) + (d.rank + 1) * margin - 12
})
.text(d => {
if (this.chartInfo.param.text !== 'all' && this.chartInfo.param.text !== 'legend') {
return ''
} else {
return d.alias
} }
}) })
// 尺度转换
const scaleX = d3.scaleLinear()
.domain([0, d3.max(rankData, (d) => parseFloat(d.value))])
.range([0, bodyWidth])
// 柱子最小宽度
const minWidth = 2
// 渲染柱形
const bars = this.svg.append('g')
.attr('transform', `translate(${bodyX})`)
.selectAll()
.data(rankData)
bars.enter()
.append('rect')
.merge(bars)
.attr('x', 0)
.attr('y', (d) => {
return (d.rank * barHeight) + (d.rank + 1) * margin
})
.attr('height', barHeight)
.attr('fill', (d) => d.background)
.attr('width', (d) => {
return scaleX(d.value) > minWidth ? scaleX(d.value) : minWidth
})
.style('cursor', 'pointer')
.on('mouseenter', this.rankEnter)
.on('mousemove', this.rankMove)
.on('mouseleave', this.rankLeave)
bars.exit().remove()
// 文本标签
this.svg.append('g')
.attr('transform', `translate(${bodyX})`)
.selectAll()
.data(rankData)
.enter()
.append('foreignObject')
.on('mouseenter', this.rankEnter)
.on('mousemove', this.rankMove)
.on('mouseleave', this.rankLeave)
.html((d) => {
return this.rankFormatterLabel(d)
})
.attr('x', (d) => {
let x = 0
if (scaleX(d.value) > minWidth) {
x = scaleX(d.value) + 10
} else {
x = minWidth + 10
}
return x
})
.attr('y', (d) => {
return (d.rank * barHeight) + (d.rank + 1) * margin
})
.attr('height', barHeight)
.style('cursor', 'pointer')
.style('overflow', 'visible')
// 生成标签和矩形
this.svg.append('g')
.attr('transform', `translate(${bodyX})`)
.selectAll('text')
.data(rankData)
.enter()
.append('text')
.attr('class', 'chart-label-text')
.attr('x', 4)
.attr('y', d => {
return (d.rank * barHeight) + (d.rank + 1) * margin - 12
})
.text(d => {
if (this.chartInfo.param.text !== 'all' && this.chartInfo.param.text !== 'legend') {
return ''
} else {
return d.alias
}
})
})
}, },
// 处理label // 处理label

View File

@@ -136,181 +136,179 @@ export default {
}, },
drawSankeyChart () { drawSankeyChart () {
this.$nextTick(() => { this.dispose()
this.dispose() // 获取svg宽高 初始化画布
// 获取svg宽高 初始化画布 const svgDom = document.getElementById(`sankey-svg-${this.chartId}`)
const svgDom = document.getElementById(`sankey-svg-${this.chartId}`) if (!svgDom) {
if (!svgDom) { return false
return false }
} const width = svgDom.getBoundingClientRect().width
const width = svgDom.getBoundingClientRect().width const height = svgDom.getBoundingClientRect().height
const height = svgDom.getBoundingClientRect().height const margin1 = 100
const margin1 = 100 const margin2 = 50
const margin2 = 50 this.svg = d3.select(`#sankey-svg-${this.chartId}`)
this.svg = d3.select(`#sankey-svg-${this.chartId}`) const chart = this.svg.append('g').attr('transform', `translate(${margin2}, ${margin2})`)
const chart = this.svg.append('g').attr('transform', `translate(${margin2}, ${margin2})`)
// 创建桑基图生成器 // 创建桑基图生成器
const sankey = d3Sankey const sankey = d3Sankey
.sankey() .sankey()
.nodeWidth(20) .nodeWidth(20)
.nodePadding(20) .nodePadding(20)
.size([width - 2 * margin1, height - 2 * margin2]) .size([width - 2 * margin1, height - 2 * margin2])
.nodeId((d) => d.node) .nodeId((d) => d.node)
const nodesData = lodash.cloneDeep(this.nodesData) const nodesData = lodash.cloneDeep(this.nodesData)
const linksData = lodash.cloneDeep(this.linksData) const linksData = lodash.cloneDeep(this.linksData)
// 判断数据是否全部为0 // 判断数据是否全部为0
let allZero = false let allZero = false
if (linksData.every(item => item.value == 0)) { if (linksData.every(item => item.value == 0)) {
linksData.forEach(item => { linksData.forEach(item => {
item.value = 100 // 目的是显示图表 与值大小无关 item.value = 100 // 目的是显示图表 与值大小无关
})
allZero = true
}
const { nodes, links } = sankey({
nodes: nodesData,
links: linksData
}) })
allZero = true
}
// 设置节点颜色 const { nodes, links } = sankey({
nodes.forEach((item, index) => { nodes: nodesData,
if (index >= 20) { links: linksData
const colorRandom = randomcolor()
this.colorList.push(colorRandom)
}
const mapping = this.selectMapping(item.value, this.chartInfo.param.valueMapping, this.chartInfo.param.enable && this.chartInfo.param.enable.valueMapping)
item.mapping = mapping
item.background = mapping ? mapping.color.bac : this.colorList[index]
const decimals = this.chartInfo.param.decimals || 2
item.showValue = chartDataFormat.getUnit(this.chartInfo.unit ? this.chartInfo.unit : 2).compute(!allZero ? item.value : 0, null, -1, decimals)
})
// 创建一个连线绘制组,绑定连线数据(links)
chart
.append('g')
.attr('fill', 'none')
.selectAll()
.data(links)
.join('path')
.attr('linkNodes', (d) => { // 设置与当前连线相连的节点(必须以字母开头)
return 'i-' + d.source.index + ' ' + 'i-' + d.target.index
})
.attr('d', d3Sankey.sankeyLinkHorizontal())
.attr('stroke', (d) => {
return d.source.background
})
.attr('stroke-width', (d) => d.width || 1)
.style('stroke-opacity', '0.5')
.attr('cursor', 'pointer')
.style('transition', 'all 0.3s')
// 创建一个节点绘制组,绑定节点数据(nodes)。
chart
.append('g')
.selectAll()
.data(nodes)
.join('g')
.attr('class', 'node')
.attr('linkNodes', (d) => { // 设置与当前节点相连的节点(必须以字母开头)
let nodeStr = ''
d.targetLinks.forEach(link => {
nodeStr += 'i-' + link.source.index + ' '
})
nodeStr += 'i-' + d.index
d.sourceLinks.forEach(link => {
nodeStr += ' ' + 'i-' + link.target.index
})
return nodeStr
})
.attr('index', (d) => { // 设置标识(必须以字母开头)
return 'i-' + d.index
})
.append('rect')
.attr('fill', (d, i) => {
return d.background
})
.attr('x', (d) => d.x0)
.attr('y', (d) => d.y0)
.attr('height', (d) => d.y1 - d.y0 || 2)
.attr('width', (d) => d.x1 - d.x0)
.attr('cursor', 'pointer')
.style('transition', 'all 0.3s')
// 节点添加文字
chart
.selectAll('.node')
.append('foreignObject')
// .attr('width', 20)
.attr('height', function (d) { return d.y1 - d.y0 })
.attr('x', function (d) { return d.x0 + 30 })
.attr('y', function (d) { return d.y0 })
.style('overflow', 'visible')
.style('cursor', 'pointer')
.style('transition', 'all 0.3s')
.html((d) => {
return this.sankeyFormatterLabel(d)
})
// 划过连线
chart.selectAll('path')
.on('mouseover', (e, d) => {
chart.selectAll('.node, path').style('fill-opacity', '0.1').style('stroke-opacity', '0.1')
chart.selectAll('.node').selectAll('foreignObject').style('opacity', '0.1')
const hoverNodes = d3.select(e.target).style('stroke-opacity', '0.8').attr('linkNodes').split(' ')
hoverNodes.forEach((index) => {
chart.selectAll('[index=' + index + ']').style('fill-opacity', '1').selectAll('foreignObject').style('opacity', '1')
})
// 显示悬浮框
this.tooltip.title = d.source.node + ' ——> ' + d.target.node
this.tooltip.value = d.showValue
this.tooltip.mapping = ''
this.tooltip.show = true
this.setPosition(e)
})
.on('mousemove', (e) => {
if (this.tooltip.show) {
this.setPosition(e)
}
})
.on('mouseleave', () => {
chart.selectAll('.node, path').style('fill-opacity', '1').style('stroke-opacity', '0.5')
chart.selectAll('.node').selectAll('foreignObject').style('opacity', '1')
// 隐藏悬浮框
this.tooltip.show = false
})
// 划过节点
chart.selectAll('.node')
.on('mouseover', (e, d) => {
chart.selectAll('.node, path').style('fill-opacity', '0.1').style('stroke-opacity', '0.1')
chart.selectAll('.node').selectAll('foreignObject').style('opacity', '0.1')
chart.selectAll('[linkNodes~=' + 'i-' + d.index + ']')
.style('fill-opacity', '1')
.style('stroke-opacity', '0.8')
.selectAll('foreignObject')
.style('opacity', '1')
// 显示悬浮框
this.tooltip.title = d.node
this.tooltip.value = d.showValue
this.tooltip.mapping = d.mapping
this.tooltip.show = true
this.setPosition(e)
})
.on('mousemove', (e) => {
if (this.tooltip.show) {
this.setPosition(e)
}
})
.on('mouseleave', () => {
chart.selectAll('.node, path').style('fill-opacity', '1').style('stroke-opacity', '0.5')
chart.selectAll('.node').selectAll('foreignObject').style('opacity', '1')
// 隐藏悬浮框
this.tooltip.show = false
})
}) })
// 设置节点颜色
nodes.forEach((item, index) => {
if (index >= 20) {
const colorRandom = randomcolor()
this.colorList.push(colorRandom)
}
const mapping = this.selectMapping(item.value, this.chartInfo.param.valueMapping, this.chartInfo.param.enable && this.chartInfo.param.enable.valueMapping)
item.mapping = mapping
item.background = mapping ? mapping.color.bac : this.colorList[index]
const decimals = this.chartInfo.param.decimals || 2
item.showValue = chartDataFormat.getUnit(this.chartInfo.unit ? this.chartInfo.unit : 2).compute(!allZero ? item.value : 0, null, -1, decimals)
})
// 创建一个连线绘制组,绑定连线数据(links)
chart
.append('g')
.attr('fill', 'none')
.selectAll()
.data(links)
.join('path')
.attr('linkNodes', (d) => { // 设置与当前连线相连的节点(必须以字母开头)
return 'i-' + d.source.index + ' ' + 'i-' + d.target.index
})
.attr('d', d3Sankey.sankeyLinkHorizontal())
.attr('stroke', (d) => {
return d.source.background
})
.attr('stroke-width', (d) => d.width || 1)
.style('stroke-opacity', '0.5')
.attr('cursor', 'pointer')
.style('transition', 'all 0.3s')
// 创建一个节点绘制组,绑定节点数据(nodes)。
chart
.append('g')
.selectAll()
.data(nodes)
.join('g')
.attr('class', 'node')
.attr('linkNodes', (d) => { // 设置与当前节点相连的节点(必须以字母开头)
let nodeStr = ''
d.targetLinks.forEach(link => {
nodeStr += 'i-' + link.source.index + ' '
})
nodeStr += 'i-' + d.index
d.sourceLinks.forEach(link => {
nodeStr += ' ' + 'i-' + link.target.index
})
return nodeStr
})
.attr('index', (d) => { // 设置标识(必须以字母开头)
return 'i-' + d.index
})
.append('rect')
.attr('fill', (d, i) => {
return d.background
})
.attr('x', (d) => d.x0)
.attr('y', (d) => d.y0)
.attr('height', (d) => d.y1 - d.y0 || 2)
.attr('width', (d) => d.x1 - d.x0)
.attr('cursor', 'pointer')
.style('transition', 'all 0.3s')
// 节点添加文字
chart
.selectAll('.node')
.append('foreignObject')
// .attr('width', 20)
.attr('height', function (d) { return d.y1 - d.y0 })
.attr('x', function (d) { return d.x0 + 30 })
.attr('y', function (d) { return d.y0 })
.style('overflow', 'visible')
.style('cursor', 'pointer')
.style('transition', 'all 0.3s')
.html((d) => {
return this.sankeyFormatterLabel(d)
})
// 划过连线
chart.selectAll('path')
.on('mouseover', (e, d) => {
chart.selectAll('.node, path').style('fill-opacity', '0.1').style('stroke-opacity', '0.1')
chart.selectAll('.node').selectAll('foreignObject').style('opacity', '0.1')
const hoverNodes = d3.select(e.target).style('stroke-opacity', '0.8').attr('linkNodes').split(' ')
hoverNodes.forEach((index) => {
chart.selectAll('[index=' + index + ']').style('fill-opacity', '1').selectAll('foreignObject').style('opacity', '1')
})
// 显示悬浮框
this.tooltip.title = d.source.node + ' ——> ' + d.target.node
this.tooltip.value = d.showValue
this.tooltip.mapping = ''
this.tooltip.show = true
this.setPosition(e)
})
.on('mousemove', (e) => {
if (this.tooltip.show) {
this.setPosition(e)
}
})
.on('mouseleave', () => {
chart.selectAll('.node, path').style('fill-opacity', '1').style('stroke-opacity', '0.5')
chart.selectAll('.node').selectAll('foreignObject').style('opacity', '1')
// 隐藏悬浮框
this.tooltip.show = false
})
// 划过节点
chart.selectAll('.node')
.on('mouseover', (e, d) => {
chart.selectAll('.node, path').style('fill-opacity', '0.1').style('stroke-opacity', '0.1')
chart.selectAll('.node').selectAll('foreignObject').style('opacity', '0.1')
chart.selectAll('[linkNodes~=' + 'i-' + d.index + ']')
.style('fill-opacity', '1')
.style('stroke-opacity', '0.8')
.selectAll('foreignObject')
.style('opacity', '1')
// 显示悬浮框
this.tooltip.title = d.node
this.tooltip.value = d.showValue
this.tooltip.mapping = d.mapping
this.tooltip.show = true
this.setPosition(e)
})
.on('mousemove', (e) => {
if (this.tooltip.show) {
this.setPosition(e)
}
})
.on('mouseleave', () => {
chart.selectAll('.node, path').style('fill-opacity', '1').style('stroke-opacity', '0.5')
chart.selectAll('.node').selectAll('foreignObject').style('opacity', '1')
// 隐藏悬浮框
this.tooltip.show = false
})
}, },
setPosition (e) { setPosition (e) {

View File

@@ -1,177 +0,0 @@
class Colorizer {
/**
* @return {void}
*/
constructor () {
this.hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i
this.instanceId = null
this.labelFill = null
this.scale = null
}
/**
* @param {string} instanceId
*
* @return {void}
*/
setInstanceId (instanceId) {
this.instanceId = instanceId
}
/**
* @param {string} fill
*
* @return {void}
*/
setLabelFill (fill) {
this.labelFill = fill
}
/**
* @param {function|Array} scale
*
* @return {void}
*/
setScale (scale) {
this.scale = scale
}
/**
* Given a raw data block, return an appropriate color for the block.
*
* @param {string} fill
* @param {Number} index
* @param {string} fillType
*
* @return {Object}
*/
getBlockFill (fill, index, fillType) {
const raw = this.getBlockRawFill(fill, index)
return {
raw,
actual: this.getBlockActualFill(raw, index, fillType)
}
}
/**
* Return the raw hex color for the block.
*
* @param {string} fill
* @param {Number} index
*
* @return {string}
*/
getBlockRawFill (fill, index) {
// Use the block's color, if set and valid
if (this.hexExpression.test(fill)) {
return fill
}
// Otherwise, attempt to use the array scale
if (Array.isArray(this.scale)) {
return this.scale[index]
}
// Finally, use a functional scale
return this.scale(index)
}
/**
* Return the actual background for the block.
*
* @param {string} raw
* @param {Number} index
* @param {string} fillType
*
* @return {string}
*/
getBlockActualFill (raw, index, fillType) {
if (fillType === 'solid') {
return raw
}
return `url(#${this.getGradientId(index)})`
}
/**
* Return the gradient ID for the given index.
*
* @param {Number} index
*
* @return {string}
*/
getGradientId (index) {
return `${this.instanceId}-gradient-${index}`
}
/**
* Given a raw data block, return an appropriate label color.
*
* @param {string} labelFill
*
* @return {string}
*/
getLabelColor (labelFill) {
return this.hexExpression.test(labelFill) ? labelFill : this.labelFill
}
/**
* 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)}`
}
/**
* 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 = this.expandHex(hex)
}
const f = parseInt(hex, 16)
/* eslint-disable no-bitwise */
const R = f >> 16
const G = (f >> 8) & 0x00FF
const B = f & 0x0000FF
/* eslint-enable */
return { R, G, B }
}
/**
* Expands a three character hex code to six characters.
*
* @param {string} hex
*
* @return {string}
*/
expandHex (hex) {
return hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
}
}
export default Colorizer

File diff suppressed because it is too large Load Diff

View File

@@ -1,75 +0,0 @@
class Formatter {
/**
* Register the format function.
*
* @param {string|function} format
*
* @return {function}
*/
getFormatter (format) {
if (typeof format === 'function') {
return format
}
return (label, value, formattedValue) => (
this.stringFormatter(label, value, formattedValue, format)
)
}
/**
* Format the given value according to the data point or the format.
*
* @param {string} label
* @param {number} value
* @param {*} formattedValue
* @param {function} formatter
*
* @return string
*/
format ({ label, value, formattedValue = null }, formatter) {
return formatter(label, value, formattedValue, arguments[0])
}
/**
* Format the string according to a simple expression.
*
* {l}: label
* {v}: raw value
* {f}: formatted value
*
* @param {string} label
* @param {number} value
* @param {*} formattedValue
* @param {string} expression
*
* @return {string}
*/
stringFormatter (label, value, formattedValue, expression) {
let formatted = formattedValue
// Attempt to use supplied formatted value
// Otherwise, use the default
if (formattedValue === null) {
formatted = this.getDefaultFormattedValue(value)
}
return expression
.split('{l}')
.join(label)
.split('{v}')
.join(value)
.split('{f}')
.join(formatted)
}
/**
* @param {number} value
*
* @return {string}
*/
getDefaultFormattedValue (value) {
return value.toLocaleString()
}
}
export default Formatter

View File

@@ -1,245 +0,0 @@
class Navigator {
/**
* Given a list of path commands, returns the compiled description.
*
* @param {Array} commands
*
* @return {string}
*/
plot (commands) {
let path = ''
commands.forEach((command) => {
path += `${command[0]}${command[1]},${command[2]} `
})
return path.replace(/ +/g, ' ').trim()
}
/**
* @param {Object} dimensions
* @param {boolean} isValueOverlay
*
* @return {Array}
*/
makeCurvedPaths (dimensions, isValueOverlay = false) {
const points = this.makeBezierPoints(dimensions)
if (isValueOverlay) {
return this.makeBezierPath(points, dimensions.ratio)
}
return this.makeBezierPath(points)
}
/**
* @param {Number} centerX
* @param {Number} prevLeftX
* @param {Number} prevRightX
* @param {Number} prevHeight
* @param {Number} nextLeftX
* @param {Number} nextRightX
* @param {Number} nextHeight
* @param {Number} curveHeight
*
* @return {Object}
*/
makeBezierPoints ({
centerX,
prevLeftX,
prevRightX,
prevHeight,
nextLeftX,
nextRightX,
nextHeight,
curveHeight
}) {
return {
p00: {
x: prevLeftX,
y: prevHeight
},
p01: {
x: centerX,
y: prevHeight + (curveHeight / 2)
},
p02: {
x: prevRightX,
y: prevHeight
},
p10: {
x: nextLeftX,
y: nextHeight
},
p11: {
x: centerX,
y: nextHeight + curveHeight
},
p12: {
x: nextRightX,
y: nextHeight
}
}
}
/**
* @param {Object} p00
* @param {Object} p01
* @param {Object} p02
* @param {Object} p10
* @param {Object} p11
* @param {Object} p12
* @param {Number} ratio
*
* @return {Array}
*/
makeBezierPath ({
p00,
p01,
p02,
p10,
p11,
p12
}, ratio = 1) {
const curve0 = this.getQuadraticBezierCurve(p00, p01, p02, ratio)
const curve1 = this.getQuadraticBezierCurve(p10, p11, p12, ratio)
return [
// Top Bezier curve
[curve0.p0.x, curve0.p0.y, 'M'],
[curve0.p1.x, curve0.p1.y, 'Q'],
[curve0.p2.x, curve0.p2.y, ''],
// Right line
[curve1.p2.x, curve1.p2.y, 'L'],
// Bottom Bezier curve
[curve1.p2.x, curve1.p2.y, 'M'],
[curve1.p1.x, curve1.p1.y, 'Q'],
[curve1.p0.x, curve1.p0.y, ''],
// Left line
[curve0.p0.x, curve0.p0.y, 'L']
]
}
/**
* @param {Object} p0
* @param {Object} p1
* @param {Object} p2
* @param {Number} t
*
* @return {Object}
*/
getQuadraticBezierCurve (p0, p1, p2, t = 1) {
// Quadratic Bezier curve syntax: M(P0) Q(P1) P2
// Where P0, P2 are the curve endpoints and P1 is the control point
// More generally, at 0 <= t <= 1, we have the following:
// Q0(t), which varies linearly from P0 to P1
// Q1(t), which varies linearly from P1 to P2
// B(t), which is interpolated linearly between Q0(t) and Q1(t)
// For an intermediate curve at 0 <= t <= 1:
// P1(t) = Q0(t)
// P2(t) = B(t)
return {
p0,
p1: {
x: this.getLinearInterpolation(p0, p1, t, 'x'),
y: this.getLinearInterpolation(p0, p1, t, 'y')
},
p2: {
x: this.getQuadraticInterpolation(p0, p1, p2, t, 'x'),
y: this.getQuadraticInterpolation(p0, p1, p2, t, 'y')
}
}
}
/**
* @param {Object} p0
* @param {Object} p1
* @param {Number} t
* @param {string} axis
*
* @return {Number}
*/
getLinearInterpolation (p0, p1, t, axis) {
return p0[axis] + (t * (p1[axis] - p0[axis]))
}
/**
* @param {Object} p0
* @param {Object} p1
* @param {Object} p2
* @param {Number} t
* @param {string} axis
*
* @return {Number}
*/
getQuadraticInterpolation (p0, p1, p2, t, axis) {
return (((1 - t) ** 2) * p0[axis]) +
(2 * (1 - t) * t * p1[axis]) +
((t ** 2) * p2[axis])
}
/**
* @param {Number} prevLeftX
* @param {Number} prevRightX
* @param {Number} prevHeight
* @param {Number} nextLeftX
* @param {Number} nextRightX
* @param {Number} nextHeight
* @param {Number} ratio
* @param {boolean} isValueOverlay
*
* @return {Object}
*/
makeStraightPaths ({
prevLeftX,
prevRightX,
prevHeight,
nextLeftX,
nextRightX,
nextHeight,
ratio
}, isValueOverlay = false) {
if (isValueOverlay) {
const lengthTop = (prevRightX - prevLeftX)
const lengthBtm = (nextRightX - nextLeftX)
let rightSideTop = (lengthTop * (ratio || 0)) + prevLeftX
let rightSideBtm = (lengthBtm * (ratio || 0)) + nextLeftX
// Overlay should not be longer than the max length of the path
rightSideTop = Math.min(rightSideTop, lengthTop)
rightSideBtm = Math.min(rightSideBtm, lengthBtm)
return [
// Start position
[prevLeftX, prevHeight, 'M'],
// Move to right
[rightSideTop, prevHeight, 'L'],
// Move down
[rightSideBtm, nextHeight, 'L'],
// Move to left
[nextLeftX, nextHeight, 'L'],
// Wrap back to top
[prevLeftX, prevHeight, 'L']
]
}
return [
// Start position
[prevLeftX, prevHeight, 'M'],
// Move to right
[prevRightX, prevHeight, 'L'],
// Move down
[nextRightX, nextHeight, 'L'],
// Move to left
[nextLeftX, nextHeight, 'L'],
// Wrap back to top
[prevLeftX, prevHeight, 'L']
]
}
}
export default Navigator

View File

@@ -1,78 +0,0 @@
class Utils {
/**
* Determine whether the given parameter is an extendable object.
*
* @param {*} a
*
* @return {boolean}
*/
static isExtendableObject (a) {
return typeof a === 'object' && a !== null && !Array.isArray(a)
}
/**
* Extends an object with the members of another.
*
* @param {Object} a The object to be extended.
* @param {Object} b The object to clone from.
*
* @return {Object}
*/
static extend (a, b) {
let result = {}
// If a is non-trivial, extend the result with it
if (Object.keys(a).length > 0) {
result = Utils.extend({}, a)
}
// Copy over the properties in b into a
Object.keys(b).forEach((prop) => {
if (Utils.isExtendableObject(b[prop])) {
if (Utils.isExtendableObject(a[prop])) {
result[prop] = Utils.extend(a[prop], b[prop])
} else {
result[prop] = Utils.extend({}, b[prop])
}
} else {
result[prop] = b[prop]
}
})
return result
}
/**
* Convert the legacy block array to a block object.
*
* @param {Array} block
*
* @returns {Object}
*/
static convertLegacyBlock (block) {
return {
label: block[0],
value: Utils.getRawBlockCount(block),
formattedValue: Array.isArray(block[1]) ? block[1][1] : null,
backgroundColor: block[2],
labelColor: block[3]
}
}
/**
* Given a raw data block, return its count.
*
* @param {Array} block
*
* @return {Number}
*/
static getRawBlockCount (block) {
if (Array.isArray(block)) {
return Array.isArray(block[1]) ? block[1][0] : block[1]
}
return block.value
}
}
export default Utils

View File

@@ -1,2 +0,0 @@
// Export default to provide support for non-ES6 solutions
module.exports = require('./d3-funnel/D3Funnel').default

View File

@@ -139,6 +139,10 @@ export default {
this.clickLegendTreemap(legendName, index, hasGrey, curIsGrey, currentIsTheOnlyOneHighlight) this.clickLegendTreemap(legendName, index, hasGrey, curIsGrey, currentIsTheOnlyOneHighlight)
return return
} }
if (this.chartInfo.type === 'doughnut') {
this.clickLegendDoughnut(legendName, index, hasGrey, curIsGrey, currentIsTheOnlyOneHighlight)
return
}
if (echarts) { if (echarts) {
// 判断timeSeries类型图表 先取消多表联动 // 判断timeSeries类型图表 先取消多表联动
@@ -228,6 +232,16 @@ export default {
}) })
} }
}, },
clickLegendDoughnut (legendName, index, hasGrey, curIsGrey, currentIsTheOnlyOneHighlight) {
if (!hasGrey) { // 1.除当前legend外全置灰
this.isGrey = this.isGrey.map((g, i) => i !== index)
} else if (currentIsTheOnlyOneHighlight) { // 2.全高亮
this.isGrey = this.isGrey.map(() => false)
} else { // 对应高亮
this.$set(this.isGrey, index, !this.isGrey[index])
}
this.$emit('clickLegendDoughnut', this.isGrey)
},
// 四舍五入保留2位小数不够位数则用0替补 // 四舍五入保留2位小数不够位数则用0替补
keepTwoDecimalFull (num) { keepTwoDecimalFull (num) {
let result = parseFloat(num) let result = parseFloat(num)

View File

@@ -15,7 +15,7 @@ const chartPieOption = {
series: [ series: [
{ {
type: 'pie', type: 'pie',
radius: '55%', radius: '60%',
center: ['50%', '50%'], center: ['50%', '50%'],
avoidLabelOverlap: false, avoidLabelOverlap: false,
zlevel: 1, zlevel: 1,

View File

@@ -73,6 +73,9 @@ export function isHexagon (type) {
export function isChartPie (type) { export function isChartPie (type) {
return type === chartType.pie return type === chartType.pie
} }
export function isDoughnut (type) {
return type === chartType.doughnut
}
export function isChartBar (type) { export function isChartBar (type) {
return type === chartType.bar return type === chartType.bar
} }

View File

@@ -285,6 +285,10 @@ export const chart = {
value: 'pie', value: 'pie',
label: i18n.t('dashboard.dashboard.chartForm.typeVal.pie.label') label: i18n.t('dashboard.dashboard.chartForm.typeVal.pie.label')
}, },
{
value: 'doughnut',
label: i18n.t('dashboard.dashboard.chartForm.typeVal.doughnut.label')
},
{ {
value: 'table', value: 'table',
label: i18n.t('dashboard.dashboard.chartForm.typeVal.table.label') label: i18n.t('dashboard.dashboard.chartForm.typeVal.table.label')
@@ -317,18 +321,10 @@ export const chart = {
value: 'treemap', value: 'treemap',
label: i18n.t('dashboard.dashboard.chartForm.typeVal.treemap.label') label: i18n.t('dashboard.dashboard.chartForm.typeVal.treemap.label')
}, },
{
value: 'pie',
label: i18n.t('dashboard.dashboard.chartForm.typeVal.pie.label')
},
{ {
value: 'log', value: 'log',
label: i18n.t('dashboard.dashboard.chartForm.typeVal.log.label') label: i18n.t('dashboard.dashboard.chartForm.typeVal.log.label')
}, },
{
value: 'table',
label: i18n.t('dashboard.dashboard.chartForm.typeVal.table.label')
},
{ {
value: 'map', value: 'map',
label: i18n.t('dashboard.dashboard.chartForm.typeVal.map.label') label: i18n.t('dashboard.dashboard.chartForm.typeVal.map.label')
@@ -478,6 +474,7 @@ export const chartType = {
stat: 'stat', stat: 'stat',
gauge: 'gauge', gauge: 'gauge',
pie: 'pie', pie: 'pie',
doughnut: 'doughnut',
treemap: 'treemap', treemap: 'treemap',
log: 'log', log: 'log',
text: 'text', text: 'text',

View File

@@ -1271,7 +1271,8 @@ export default {
case 'bar': case 'bar':
case 'treemap': case 'treemap':
case 'pie': case 'pie':
if (this.oldType === 'bar' || this.oldType === 'treemap' || this.oldType === 'pie') { case 'doughnut':
if (this.oldType === 'bar' || this.oldType === 'treemap' || this.oldType === 'pie' || this.oldType === 'doughnut') {
break break
} }
this.chartConfig.param = { this.chartConfig.param = {

View File

@@ -55,6 +55,7 @@ export default {
case 'treemap': case 'treemap':
case 'gauge': case 'gauge':
case 'pie': case 'pie':
case 'doughnut':
return false return false
default: return false default: return false
} }
@@ -72,6 +73,7 @@ export default {
case 'treemap': case 'treemap':
case 'gauge': case 'gauge':
case 'pie': case 'pie':
case 'doughnut':
case 'bubble': case 'bubble':
case 'rank': case 'rank':
case 'sankey': case 'sankey':
@@ -87,6 +89,7 @@ export default {
case 'point': case 'point':
case 'treemap': case 'treemap':
case 'pie': case 'pie':
case 'doughnut':
case 'bar': case 'bar':
return true return true
case 'table': case 'table':
@@ -109,6 +112,7 @@ export default {
case 'gauge': case 'gauge':
case 'treemap': case 'treemap':
case 'pie': case 'pie':
case 'doughnut':
case 'bar': case 'bar':
return false return false
default: return false default: return false
@@ -136,6 +140,7 @@ export default {
case 'gauge': case 'gauge':
case 'treemap': case 'treemap':
case 'pie': case 'pie':
case 'doughnut':
case 'bubble': case 'bubble':
case 'rank': case 'rank':
case 'sankey': case 'sankey':
@@ -150,6 +155,7 @@ export default {
case 'bar': case 'bar':
case 'treemap': case 'treemap':
case 'pie': case 'pie':
case 'doughnut':
case 'stat': case 'stat':
case 'hexagon': case 'hexagon':
case 'gauge': case 'gauge':
@@ -167,6 +173,7 @@ export default {
case 'bar': case 'bar':
case 'treemap': case 'treemap':
case 'pie': case 'pie':
case 'doughnut':
case 'stat': case 'stat':
case 'hexagon': case 'hexagon':
case 'gauge': case 'gauge':
@@ -202,6 +209,7 @@ export default {
case 'bar': case 'bar':
case 'treemap': case 'treemap':
case 'pie': case 'pie':
case 'doughnut':
case 'stat': case 'stat':
case 'hexagon': case 'hexagon':
case 'gauge': case 'gauge':

View File

@@ -247,6 +247,10 @@ export default {
id: 'pie', id: 'pie',
name: this.$t('dashboard.dashboard.chartForm.typeVal.pie.label') name: this.$t('dashboard.dashboard.chartForm.typeVal.pie.label')
}, },
{
id: 'doughnut',
name: this.$t('dashboard.dashboard.chartForm.typeVal.doughnut.label')
},
{ {
id: 'bubble', id: 'bubble',
name: this.$t('dashboard.dashboard.chartForm.typeVal.bubble.label') name: this.$t('dashboard.dashboard.chartForm.typeVal.bubble.label')
@@ -306,6 +310,10 @@ export default {
id: 'pie', id: 'pie',
name: this.$t('dashboard.dashboard.chartForm.typeVal.pie.label') name: this.$t('dashboard.dashboard.chartForm.typeVal.pie.label')
}, },
{
id: 'doughnut',
name: this.$t('dashboard.dashboard.chartForm.typeVal.doughnut.label')
},
{ {
id: 'bubble', id: 'bubble',
name: this.$t('dashboard.dashboard.chartForm.typeVal.bubble.label') name: this.$t('dashboard.dashboard.chartForm.typeVal.bubble.label')

View File

@@ -890,6 +890,10 @@ export default {
id: 'pie', id: 'pie',
name: this.$t('dashboard.dashboard.chartForm.typeVal.pie.label') name: this.$t('dashboard.dashboard.chartForm.typeVal.pie.label')
}, },
{
id: 'doughnut',
name: this.$t('dashboard.dashboard.chartForm.typeVal.doughnut.label')
},
{ {
id: 'bubble', id: 'bubble',
name: this.$t('dashboard.dashboard.chartForm.typeVal.bubble.label') name: this.$t('dashboard.dashboard.chartForm.typeVal.bubble.label')
@@ -1001,7 +1005,8 @@ export default {
case 'bar': case 'bar':
case 'treemap': case 'treemap':
case 'pie': case 'pie':
if (this.oldType === 'bar' || this.oldType === 'treemap' || this.oldType === 'pie') { case 'doughnut':
if (this.oldType === 'bar' || this.oldType === 'treemap' || this.oldType === 'pie' || this.oldType === 'doughnut') {
break break
} }
this.chartConfig.param = { this.chartConfig.param = {