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{
z-index: 5000 !important;
}
.text-ellipsis{
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}

View File

@@ -715,25 +715,40 @@
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;
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;
}
}

View File

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

View File

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

View File

@@ -108,86 +108,84 @@ export default {
this.$emit('chartIsNoData', this.isNoData)
},
drawBubbleChart () {
this.$nextTick(() => {
this.dispose()
this.svg = d3.select(`#bubble-svg-${this.chartId}`)
const svgDom = document.getElementById(`bubble-svg-${this.chartId}`)
if (!svgDom) {
return false
}
const width = svgDom.getBoundingClientRect().width
const height = svgDom.getBoundingClientRect().height
// 定义布局方式
const pack = d3.pack()
.size([width, height])
.padding(6)
this.dispose()
this.svg = d3.select(`#bubble-svg-${this.chartId}`)
const svgDom = document.getElementById(`bubble-svg-${this.chartId}`)
if (!svgDom) {
return false
}
const width = svgDom.getBoundingClientRect().width
const height = svgDom.getBoundingClientRect().height
// 定义布局方式
const pack = d3.pack()
.size([width, height])
.padding(6)
// 如果数据全为0 则设置默认值(否则图表不显示)
let bubbleData = lodash.cloneDeep(this.bubbleData)
if (bubbleData.every(item => item.value == 0)) {
bubbleData = bubbleData.map(item => {
return {
...item,
value: 100 // 目的是显示气泡 与值大小无关
}
})
}
const data = d3.hierarchy({ children: bubbleData })
.sum(function (d) {
return d.value || 0
})
.sort(function (a, b) {
return b.value - a.value
})
const nodes = pack(data).descendants()
const bubbles = this.svg.selectAll('.bubble')
.data(nodes)
.enter()
.filter(function (d) {
return !d.children
})
.append('g')
.attr('class', 'bubble')
let bubbleData = lodash.cloneDeep(this.bubbleData)
if (bubbleData.every(item => item.value == 0)) {
bubbleData = bubbleData.map(item => {
return {
...item,
value: 100 // 目的是显示气泡 与值大小无关
}
})
}
const data = d3.hierarchy({ children: bubbleData })
.sum(function (d) {
return d.value || 0
})
.sort(function (a, b) {
return b.value - a.value
})
const nodes = pack(data).descendants()
const bubbles = this.svg.selectAll('.bubble')
.data(nodes)
.enter()
.filter(function (d) {
return !d.children
})
.append('g')
.attr('class', 'bubble')
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) {
let fontSize
fontSize = d.r / 4 > 10 ? d.r / 4 : 0
fontSize = fontSize > 30 ? 30 : fontSize
return fontSize
})
.style('border-radius', '50%')
.html((d) => {
return this.bubbleFormatterLabel(d)
})
bubbles.on('mouseenter', this.bubbleEnter)
bubbles.on('mousemove', this.bubbleMove)
bubbles.on('mouseleave', this.bubbleLeave)
})
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) {
let fontSize
fontSize = d.r / 4 > 10 ? d.r / 4 : 0
fontSize = fontSize > 30 ? 30 : fontSize
return fontSize
})
.style('border-radius', '50%')
.html((d) => {
return this.bubbleFormatterLabel(d)
})
bubbles.on('mouseenter', this.bubbleEnter)
bubbles.on('mousemove', this.bubbleMove)
bubbles.on('mouseleave', this.bubbleLeave)
},
// 处理label
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 {
funnelData = funnelData.filter(item => item.value !== 0)
}
this.$nextTick(() => {
this.dispose()
// 获取svg宽高 初始化画布
const svgDom = document.getElementById(`funnel-svg-${this.chartId}`)
if (!svgDom) {
return false
this.dispose()
// 获取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
}
const width = svgDom.getBoundingClientRect().width
const height = svgDom.getBoundingClientRect().height
const margin = 20
const chartWidth = width - margin * 2
const chartHeight = height - margin * 2
return d
}
)
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 trapezoidPadding = 2
const trapezoidsHeight = (chartHeight - trapezoidPadding * (funnelData.length - 1)) / funnelData.length
// 渲染梯形
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 ? 600 : 0)
.style('opacity', 1)
.style('cursor', 'pointer')
trapezoids.exit()
.remove()
const scale = d3.scaleLinear()
.domain([0, d3.max(funnelData, (d) => d.value)])
.range([0, chartWidth * 0.8])
// 绑定交互事件
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()
})
// 数据处理
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
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)
}
)
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})`)
return points.join(' ')
}
// 渲染梯形
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()
// 渲染文本标签
const texts = chart.select('.traps')
.selectAll('.foreign')
.data(funnelData)
texts.enter()
.append('foreignObject')
.attr('class', 'foreign')
.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('opacity', 0)
.transition('opacity').duration(animate === true ? 600 : 0)
.style('opacity', 1)
texts.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
})
this.isInit = false
},
// 处理label
formatterLabel (data, height) {
@@ -257,7 +253,7 @@ export default {
if (height < 32) {
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};">
<span>${str}</span>
</p>
@@ -271,7 +267,7 @@ export default {
if (height < 16) {
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};">
<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>
@@ -282,7 +278,7 @@ export default {
if (height < 16) {
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};">
<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>

View File

@@ -115,114 +115,112 @@ export default {
},
drawRankChart () {
this.$nextTick(() => {
this.dispose()
// 获取svg宽高 初始化画布
const svgDom = document.getElementById(`rank-svg-${this.chartId}`)
if (!svgDom) {
return false
}
const width = svgDom.getBoundingClientRect().width
// 柱子高度
const barHeight = 24
// 柱子间隔
const margin = 40
// 计算svg高度
const height = this.rankData.length * (barHeight + margin) + margin
this.svg = d3.select(`#rank-svg-${this.chartId}`).attr('height', height)
const bodyX = 50
const bodyWidth = width - 3 * bodyX
this.dispose()
// 获取svg宽高 初始化画布
const svgDom = document.getElementById(`rank-svg-${this.chartId}`)
if (!svgDom) {
return false
}
const width = svgDom.getBoundingClientRect().width
// 柱子高度
const barHeight = 24
// 柱子间隔
const margin = 40
// 计算svg高度
const height = this.rankData.length * (barHeight + margin) + margin
this.svg = d3.select(`#rank-svg-${this.chartId}`).attr('height', height)
const bodyX = 50
const bodyWidth = width - 3 * bodyX
// 从大到小排序
let rankData = lodash.cloneDeep(this.rankData)
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
// 从大到小排序
let rankData = lodash.cloneDeep(this.rankData)
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 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

View File

@@ -136,181 +136,179 @@ export default {
},
drawSankeyChart () {
this.$nextTick(() => {
this.dispose()
// 获取svg宽高 初始化画布
const svgDom = document.getElementById(`sankey-svg-${this.chartId}`)
if (!svgDom) {
return false
}
const width = svgDom.getBoundingClientRect().width
const height = svgDom.getBoundingClientRect().height
const margin1 = 100
const margin2 = 50
this.svg = d3.select(`#sankey-svg-${this.chartId}`)
const chart = this.svg.append('g').attr('transform', `translate(${margin2}, ${margin2})`)
this.dispose()
// 获取svg宽高 初始化画布
const svgDom = document.getElementById(`sankey-svg-${this.chartId}`)
if (!svgDom) {
return false
}
const width = svgDom.getBoundingClientRect().width
const height = svgDom.getBoundingClientRect().height
const margin1 = 100
const margin2 = 50
this.svg = d3.select(`#sankey-svg-${this.chartId}`)
const chart = this.svg.append('g').attr('transform', `translate(${margin2}, ${margin2})`)
// 创建桑基图生成器
const sankey = d3Sankey
.sankey()
.nodeWidth(20)
.nodePadding(20)
.size([width - 2 * margin1, height - 2 * margin2])
.nodeId((d) => d.node)
const nodesData = lodash.cloneDeep(this.nodesData)
const linksData = lodash.cloneDeep(this.linksData)
// 创建桑基图生成器
const sankey = d3Sankey
.sankey()
.nodeWidth(20)
.nodePadding(20)
.size([width - 2 * margin1, height - 2 * margin2])
.nodeId((d) => d.node)
const nodesData = lodash.cloneDeep(this.nodesData)
const linksData = lodash.cloneDeep(this.linksData)
// 判断数据是否全部为0
let allZero = false
if (linksData.every(item => item.value == 0)) {
linksData.forEach(item => {
item.value = 100 // 目的是显示图表 与值大小无关
})
allZero = true
}
const { nodes, links } = sankey({
nodes: nodesData,
links: linksData
// 判断数据是否全部为0
let allZero = false
if (linksData.every(item => item.value == 0)) {
linksData.forEach(item => {
item.value = 100 // 目的是显示图表 与值大小无关
})
allZero = true
}
// 设置节点颜色
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
})
const { nodes, links } = sankey({
nodes: nodesData,
links: linksData
})
// 设置节点颜色
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) {

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)
return
}
if (this.chartInfo.type === 'doughnut') {
this.clickLegendDoughnut(legendName, index, hasGrey, curIsGrey, currentIsTheOnlyOneHighlight)
return
}
if (echarts) {
// 判断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替补
keepTwoDecimalFull (num) {
let result = parseFloat(num)

View File

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

View File

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

View File

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

View File

@@ -1271,7 +1271,8 @@ export default {
case 'bar':
case 'treemap':
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
}
this.chartConfig.param = {

View File

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

View File

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

View File

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