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