diff --git a/nezha-fronted/src/assets/css/common.scss b/nezha-fronted/src/assets/css/common.scss index 79a91fda2..b714a36d4 100644 --- a/nezha-fronted/src/assets/css/common.scss +++ b/nezha-fronted/src/assets/css/common.scss @@ -706,3 +706,9 @@ textarea { .message-zindex{ z-index: 5000 !important; } + +.text-ellipsis{ + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} \ No newline at end of file diff --git a/nezha-fronted/src/assets/css/components/chart/chart.scss b/nezha-fronted/src/assets/css/components/chart/chart.scss index 26f2b6d49..921c8f3ee 100644 --- a/nezha-fronted/src/assets/css/components/chart/chart.scss +++ b/nezha-fronted/src/assets/css/components/chart/chart.scss @@ -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; - } -} +} \ No newline at end of file diff --git a/nezha-fronted/src/components/chart/ChartScreenHeader.vue b/nezha-fronted/src/components/chart/ChartScreenHeader.vue index 437d9593b..c84238e87 100644 --- a/nezha-fronted/src/components/chart/ChartScreenHeader.vue +++ b/nezha-fronted/src/components/chart/ChartScreenHeader.vue @@ -135,6 +135,7 @@ export default { case 'stat' : case 'gauge' : case 'pie' : + case 'doughnut' : case 'treemap' : case 'log' : case 'hexagon' : diff --git a/nezha-fronted/src/components/chart/chart.vue b/nezha-fronted/src/components/chart/chart.vue index b16ad81bd..0c89c035a 100644 --- a/nezha-fronted/src/components/chart/chart.vue +++ b/nezha-fronted/src/components/chart/chart.vue @@ -27,6 +27,15 @@ :is-fullscreen="isFullscreen" @chartIsNoData="chartIsNoData" > + { - 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) { diff --git a/nezha-fronted/src/components/chart/chart/chartDoughnut.vue b/nezha-fronted/src/components/chart/chart/chartDoughnut.vue new file mode 100644 index 000000000..0b292b7c7 --- /dev/null +++ b/nezha-fronted/src/components/chart/chart/chartDoughnut.vue @@ -0,0 +1,365 @@ + + + diff --git a/nezha-fronted/src/components/chart/chart/chartFunnelNew.vue b/nezha-fronted/src/components/chart/chart/chartFunnelNew.vue index 1d61f27fa..591fabd0a 100644 --- a/nezha-fronted/src/components/chart/chart/chartFunnelNew.vue +++ b/nezha-fronted/src/components/chart/chart/chartFunnelNew.vue @@ -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 `
+ return `

${str}

@@ -271,7 +267,7 @@ export default { if (height < 16) { return '' } - return `
+ return `

${str} @@ -282,7 +278,7 @@ export default { if (height < 16) { return '' } - return `

+ return `

${valueStr} diff --git a/nezha-fronted/src/components/chart/chart/chartRank.vue b/nezha-fronted/src/components/chart/chart/chartRank.vue index 0a15d7e78..dd8c832c5 100644 --- a/nezha-fronted/src/components/chart/chart/chartRank.vue +++ b/nezha-fronted/src/components/chart/chart/chartRank.vue @@ -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 diff --git a/nezha-fronted/src/components/chart/chart/chartSankey.vue b/nezha-fronted/src/components/chart/chart/chartSankey.vue index 89b587d19..dd25a5391 100644 --- a/nezha-fronted/src/components/chart/chart/chartSankey.vue +++ b/nezha-fronted/src/components/chart/chart/chartSankey.vue @@ -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) { diff --git a/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Colorizer.js b/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Colorizer.js deleted file mode 100644 index c6c0db49a..000000000 --- a/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Colorizer.js +++ /dev/null @@ -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 diff --git a/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/D3Funnel.js b/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/D3Funnel.js deleted file mode 100644 index e4b849453..000000000 --- a/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/D3Funnel.js +++ /dev/null @@ -1,1157 +0,0 @@ -import { easeLinear } from 'd3-ease' -import { range } from 'd3-array' -import { scaleOrdinal } from 'd3-scale' -import { schemeCategory10 } from 'd3-scale-chromatic' -import { select } from 'd3-selection' -import 'd3-transition' -import { nanoid } from 'nanoid' - -import Colorizer from './Colorizer' -import Formatter from './Formatter' -import Navigator from './Navigator' -import Utils from './Utils' - -class D3Funnel { - static defaults = { - chart: { - width: 350, - height: 400, - bottomWidth: 1 / 3, - bottomPinch: 0, - inverted: false, - horizontal: false, - animate: 0, - curve: { - enabled: false, - height: 20, - shade: -0.4 - }, - totalCount: null, - customAnimate:true - }, - block: { - dynamicHeight: false, - dynamicSlope: false, - barOverlay: false, - fill: { - scale: scaleOrdinal(schemeCategory10).domain(range(0, 10)), - type: 'solid' - }, - minHeight: 0, - highlight: false - }, - label: { - enabled: true, - fontFamily: null, - fontSize: '14px', - fill: '#fff', - format: '{l}: {f}' - }, - tooltip: { - enabled: false, - format: '{l}: {f}' - }, - events: { - click: { - block: null - }, - mouseenter: { - block: null - }, - mousemove: { - block: null - }, - mouseleave: { - block: null - } - } - }; - - /** - * @param {string|HTMLElement} selector A selector for the container element. - * - * @return {void} - */ - constructor (selector) { - this.container = select(selector).node() - - this.colorizer = new Colorizer() - this.formatter = new Formatter() - this.navigator = new Navigator() - - this.id = null - - // Bind event handlers - this.onMouseOver = this.onMouseOver.bind(this) - this.onMouseOut = this.onMouseOut.bind(this) - } - - /** - * Remove the funnel and its events from the DOM. - * - * @return {void} - */ - destroy () { - const container = select(this.container) - - // D3's remove method appears to be sufficient for removing the events - container.selectAll('svg').remove() - - // Remove other elements from container - container.selectAll('*').remove() - - // Remove inner text from container - container.text('') - } - - /** - * Draw the chart inside the container with the data and configuration - * specified. This will remove any previous SVG elements in the container - * and draw a new funnel chart on top of it. - * - * @param {Array} data A list of rows containing a category, a count, - * and optionally a color (in hex). - * @param {Object} options An optional configuration object to override - * defaults. See the docs. - * - * @return {void} - */ - draw (data, options = {}) { - this.destroy() - - this.initialize(data, options) - - this.drawOntoDom() - } - - /** - * Initialize and calculate important variables for drawing the chart. - * - * @param {Array} data - * @param {Object} options - * - * @return {void} - */ - initialize (data, options) { - this.validateData(data) - - const settings = this.getSettings(options) - - this.id = `d3-funnel-${nanoid()}` - - // Set labels - this.labelFormatter = this.formatter.getFormatter(settings.label.format) - this.tooltipFormatter = this.formatter.getFormatter(settings.tooltip.format) - - // Set color scales - this.colorizer.setInstanceId(this.id) - this.colorizer.setLabelFill(settings.label.fill) - this.colorizer.setScale(settings.block.fill.scale) - - // Initialize funnel chart settings - this.settings = { - width: settings.chart.width, - height: settings.chart.height, - bottomWidth: settings.chart.width * settings.chart.bottomWidth, - bottomPinch: settings.chart.bottomPinch, - isInverted: settings.chart.inverted, - isCurved: settings.chart.curve.enabled, - curveHeight: settings.chart.curve.height, - curveShade: settings.chart.curve.shade, - addValueOverlay: settings.block.barOverlay, - animation: settings.chart.animate, - totalCount: settings.chart.totalCount, - fillType: settings.block.fill.type, - hoverEffects: settings.block.highlight, - dynamicHeight: settings.block.dynamicHeight, - dynamicSlope: settings.block.dynamicSlope, - minHeight: settings.block.minHeight, - label: settings.label, - tooltip: settings.tooltip, - onBlockClick: settings.events.click.block, - onBlockEnter: settings.events.mouseenter.block, - onBlockMove: settings.events.mousemove.block, - onBlockLeave: settings.events.mouseleave.block, - customAnimate:settings.chart.customAnimate //opacity过渡 - } - - this.setBlocks(data) - } - - /** - * @param {Array} data - * - * @return void - */ - validateData (data) { - if (Array.isArray(data) === false) { - throw new Error('Data must be an array.') - } - - if (data.length === 0) { - throw new Error('Data array must contain at least one element.') - } - - if (typeof data[0] !== 'object') { - throw new Error('Data array elements must be an object.') - } - - if ( - (Array.isArray(data[0]) && data[0].length < 2) || - (Array.isArray(data[0]) === false && ( - data[0].label === undefined || data[0].value === undefined - )) - ) { - throw new Error('Data array elements must contain a label and value.') - } - } - - /** - * @param {Object} options - * - * @return {Object} - */ - getSettings (options) { - const containerDimensions = this.getContainerDimensions() - const defaults = this.getDefaultSettings(containerDimensions) - - // Prepare the configuration settings based on the defaults - let settings = Utils.extend({}, defaults) - - // Override default settings with user options - settings = Utils.extend(settings, options) - - // Account for any percentage-based dimensions - settings.chart = { - ...settings.chart, - ...this.castDimensions(settings, containerDimensions) - } - - return settings - } - - /** - * Return default settings. - * - * @param {Object} containerDimensions - * - * @return {Object} - */ - getDefaultSettings (containerDimensions) { - const settings = D3Funnel.defaults - - // Set the default width and height based on the container - settings.chart = { - ...settings.chart, - ...containerDimensions - } - - return settings - } - - /** - * Get the width/height dimensions of the container. - * - * @return {{width: Number, height: Number}} - */ - getContainerDimensions () { - const dimensions = { - width: parseFloat(select(this.container).style('width')), - height: parseFloat(select(this.container).style('height')) - }; - - // Remove container dimensions that resolve to zero - ['width', 'height'].forEach((direction) => { - if (dimensions[direction] === 0) { - delete dimensions[direction] - } - }) - - return dimensions - } - - /** - * Cast dimensions into tangible or meaningful numbers. - * - * @param {Object} chart - * @param {Object} containerDimensions - * - * @return {{width: Number, height: Number}} - */ - castDimensions ({ chart }, containerDimensions) { - const dimensions = {} - - Object.keys(containerDimensions).forEach((direction) => { - const chartDimension = chart[direction] - const containerDimension = containerDimensions[direction] - - if (/%$/.test(String(chartDimension))) { - // Convert string into a percentage of the container - dimensions[direction] = (parseFloat(chartDimension) / 100) * containerDimension - } else if (chartDimension <= 0) { - // If case of non-positive number, set to a usable number - dimensions[direction] = D3Funnel.defaults.chart[direction] - } else { - dimensions[direction] = chartDimension - } - }) - - return dimensions - } - - /** - * Register the raw data into a standard block format and pre-calculate - * some values. - * - * @param {Array} data - * - * @return void - */ - setBlocks (data) { - const totalCount = this.getTotalCount(data) - - this.blocks = this.standardizeData(data, totalCount) - } - - /** - * Return the total count of all blocks. - * - * @param {Array} data - * - * @return {Number} - */ - getTotalCount (data) { - if (this.settings.totalCount !== null) { - return this.settings.totalCount || 0 - } - - return data.reduce((a, b) => a + Utils.getRawBlockCount(b), 0) - } - - /** - * Convert the raw data into a standardized format. - * - * @param {Array} data - * @param {Number} totalCount - * - * @return {Array} - */ - standardizeData (data, totalCount) { - return data.map((rawBlock, index) => { - const block = Array.isArray(rawBlock) ? Utils.convertLegacyBlock(rawBlock) : rawBlock - const ratio = totalCount > 0 ? (block.value / totalCount || 0) : 1 / data.length - - return { - index, - ratio, - value: block.value, - height: this.settings.height * ratio, - fill: this.colorizer.getBlockFill( - block.backgroundColor, - index, - this.settings.fillType - ), - label: { - enabled: !block.hideLabel, - raw: block.label, - formatted: this.formatter.format(block, this.labelFormatter), - color: this.colorizer.getLabelColor(block.labelColor) - }, - tooltip: { - enabled: block.enabled, - formatted: this.formatter.format(block, this.tooltipFormatter) - }, - data: Utils.extend({}, block) - } - }) - } - - /** - * Draw the chart onto the DOM. - * - * @return {void} - */ - drawOntoDom () { - // Add the SVG - this.svg = select(this.container) - .append('svg') - .attr('id', this.id) - .attr('width', this.settings.width) - .attr('height', this.settings.height); - - [this.blockPaths, this.overlayPaths] = this.makePaths() - - // Define color gradients - if (this.settings.fillType === 'gradient') { - this.defineColorGradients(this.svg) - } - - // Add top oval if curved - if (this.settings.isCurved) { - this.drawTopOval(this.svg, this.blockPaths) - } - - // Add each block - this.drawBlock(0) - } - - /** - * Create the paths to be used to define the discrete funnel blocks and - * returns the results in an array. - * - * @return {Array, Array} - */ - makePaths () { - // Calculate the important fixed positions - const bottomLeftX = (this.settings.width - this.settings.bottomWidth) / 2 - const centerX = this.settings.width / 2 - - let paths = [] - let overlayPaths = [] - - // Calculate change in x, y direction - this.dx = this.getDx(bottomLeftX) - this.dy = this.getDy() - - // Initialize velocity - let { dx, dy } = this - - // Initialize starting positions - let prevLeftX = 0 - let prevRightX = this.settings.width - let prevHeight = 0 - - // Start from the bottom for inverted - if (this.settings.isInverted) { - prevLeftX = bottomLeftX - prevRightX = this.settings.width - bottomLeftX - } - - // Initialize next positions - let nextLeftX = 0 - let nextRightX = 0 - let nextHeight = 0 - - // Move down if there is an initial curve - if (this.settings.isCurved) { - prevHeight = this.settings.curveHeight / 2 - } - - let totalHeight = this.settings.height - - // This is greedy in that the block will have a guaranteed height - // and the remaining is shared among the ratio, instead of being - // shared according to the remaining minus the guaranteed - if (this.settings.minHeight !== 0) { - totalHeight = this.settings.height - (this.settings.minHeight * this.blocks.length) - } - - let slopeHeight = this.settings.height - - // Correct slope height if there are blocks being pinched (and thus - // requiring a sharper curve) - if (this.settings.bottomPinch > 0) { - this.blocks.forEach((block, i) => { - let height = (totalHeight * block.ratio) - - // Add greedy minimum height - if (this.settings.minHeight !== 0) { - height += this.settings.minHeight - } - - // Account for any curvature - if (this.settings.isCurved) { - height += this.settings.curveHeight / this.blocks.length - } - - if (this.settings.isInverted) { - if (i < this.settings.bottomPinch) { - slopeHeight -= height - } - } else if (i >= this.blocks.length - this.settings.bottomPinch) { - slopeHeight -= height - } - }) - } - - // The slope will determine the x points on each block iteration - // Given: slope = (y1 - y2) / (x1 - x2) - // (x1, y1) = (bottomLeftX, height) - // (x2, y2) = (0, 0) - const slope = slopeHeight / bottomLeftX - - // Create the path definition for each funnel block - // Remember to loop back to the beginning point for a closed path - this.blocks.forEach((block, i) => { - // Make heights proportional to block weight - if (this.settings.dynamicHeight) { - // Slice off the height proportional to this block - dy = totalHeight * block.ratio - - // Add greedy minimum height - if (this.settings.minHeight !== 0) { - dy += this.settings.minHeight - } - - // Account for any curvature - if (this.settings.isCurved) { - dy -= this.settings.curveHeight / this.blocks.length - } - - // Given: y = mx + b - // Given: b = 0 (when funnel), b = this.settings.height (when pyramid) - // For funnel, x_i = y_i / slope - nextLeftX = (prevHeight + dy) / slope - - // For pyramid, x_i = y_i - this.settings.height / -slope - if (this.settings.isInverted) { - nextLeftX = ((prevHeight + dy) - this.settings.height) / (-1 * slope) - } - - // If bottomWidth is 0, adjust last x position (to circumvent - // errors associated with rounding) - if (this.settings.bottomWidth === 0 && i === this.blocks.length - 1) { - // For funnel, last position is the center - nextLeftX = this.settings.width / 2 - - // For pyramid, last position is the origin - if (this.settings.isInverted) { - nextLeftX = 0 - } - } - - // If bottomWidth is same as width, stop x velocity - if (this.settings.bottomWidth === this.settings.width) { - nextLeftX = prevLeftX - } - - // Prevent NaN or Infinite values (caused by zero heights) - if (Number.isNaN(nextLeftX) || !Number.isFinite(nextLeftX)) { - nextLeftX = 0 - } - - // Calculate the shift necessary for both x points - dx = nextLeftX - prevLeftX - - if (this.settings.isInverted) { - dx = prevLeftX - nextLeftX - } - } - - // Make slope width proportional to change in block value - if (this.settings.dynamicSlope && !this.settings.isInverted) { - const nextBlockValue = this.blocks[i + 1] - ? this.blocks[i + 1].value - : block.value - - const widthRatio = nextBlockValue / block.value - dx = (1 - widthRatio) * (centerX - prevLeftX) - } - - // Stop velocity for pinched blocks - if (this.settings.bottomPinch > 0) { - // Check if we've reached the bottom of the pinch - // If so, stop changing on x - if (!this.settings.isInverted) { - if (i >= this.blocks.length - this.settings.bottomPinch) { - dx = 0 - } - // Pinch at the first blocks relating to the bottom pinch - // Revert back to normal velocity after pinch - } else { - // Revert velocity back to the initial if we are using - // static heights (prevents zero velocity if isInverted - // and bottomPinch are non trivial and dynamicHeight is - // false) - if (!this.settings.dynamicHeight) { - ({ dx } = this) - } - - dx = i < this.settings.bottomPinch ? 0 : dx - } - } - - // Calculate the position of next block - nextLeftX = prevLeftX + dx - nextRightX = prevRightX - dx - nextHeight = prevHeight + dy - - this.blocks[i].height = dy - - // Expand outward if inverted - if (this.settings.isInverted) { - nextLeftX = prevLeftX - dx - nextRightX = prevRightX + dx - } - - const dimensions = { - centerX, - prevLeftX, - prevRightX, - prevHeight, - nextLeftX, - nextRightX, - nextHeight, - curveHeight: this.settings.curveHeight, - ratio: block.ratio - } - - if (this.settings.isCurved) { - paths = [...paths, this.navigator.makeCurvedPaths(dimensions)] - - if (this.settings.addValueOverlay) { - overlayPaths = [ - ...overlayPaths, - this.navigator.makeCurvedPaths(dimensions, true) - ] - } - } else { - paths = [...paths, this.navigator.makeStraightPaths(dimensions)] - - if (this.settings.addValueOverlay) { - overlayPaths = [ - ...overlayPaths, - this.navigator.makeStraightPaths(dimensions, true) - ] - } - } - - // Set the next block's previous position - prevLeftX = nextLeftX - prevRightX = nextRightX - prevHeight = nextHeight - }) - - return [paths, overlayPaths] - } - - /** - * @param {Number} bottomLeftX - * - * @return {Number} - */ - getDx (bottomLeftX) { - // Will be sharper if there is a pinch - if (this.settings.bottomPinch > 0) { - return bottomLeftX / (this.blocks.length - this.settings.bottomPinch) - } - - return bottomLeftX / this.blocks.length - } - - /** - * @return {Number} - */ - getDy () { - // Curved chart needs reserved pixels to account for curvature - if (this.settings.isCurved) { - return (this.settings.height - this.settings.curveHeight) / this.blocks.length - } - - return this.settings.height / this.blocks.length - } - - /** - * Define the linear color gradients. - * - * @param {Object} svg - * - * @return {void} - */ - defineColorGradients (svg) { - const defs = svg.append('defs') - - // Create a gradient for each block - this.blocks.forEach((block, index) => { - const color = block.fill.raw - const shade = this.colorizer.shade(color, -0.2) - - // Create linear gradient - const gradient = defs.append('linearGradient') - .attr('id', this.colorizer.getGradientId(index)) - - // Define the gradient stops - const stops = [ - [0, shade], - [40, color], - [60, color], - [100, shade] - ] - - // Add the gradient stops - stops.forEach((stop) => { - gradient.append('stop') - .attr('offset', `${stop[0]}%`) - .attr('style', `stop-color: ${stop[1]}`) - }) - }) - } - - /** - * Draw the top oval of a curved funnel. - * - * @param {Object} svg - * @param {Array} blockPaths - * - * @return {void} - */ - drawTopOval (svg, blockPaths) { - const centerX = this.settings.width / 2 - - // Create path from top-most block - const paths = blockPaths[0] - const topCurve = paths[1][1] + (this.settings.curveHeight / 2) - - const path = this.navigator.plot([ - ['M', paths[0][0], paths[0][1]], - ['Q', centerX, topCurve], - [' ', paths[2][0], paths[2][1]], - ['M', paths[2][0], this.settings.curveHeight / 2], - ['Q', centerX, 0], - [' ', paths[0][0], this.settings.curveHeight / 2] - ]) - - // Draw top oval - svg.append('path') - .attr('fill', this.colorizer.shade(this.blocks[0].fill.raw, this.settings.curveShade)) - .attr('d', path) - } - - /** - * Draw the next block in the iteration. - * - * @param {int} index - * - * @return {void} - */ - drawBlock (index) { - if (index === this.blocks.length) { - return - } - - // Create a group just for this block - const group = this.svg.append('g') - const block = this.blocks[index] - - // Fetch path element - const path = this.getBlockPath(group, index) - - path.style('cursor', 'pointer') - - // Attach data to the element - this.attachData(path, block) - - let overlayPath = null - let pathColor = block.fill.actual - - if (this.settings.addValueOverlay) { - overlayPath = this.getOverlayPath(group, index) - this.attachData(overlayPath, block) - - // Add data attribute to distinguish between paths - path.node().setAttribute('pathType', 'background') - overlayPath.node().setAttribute('pathType', 'foreground') - - // Default path becomes background of lighter shade - pathColor = this.colorizer.shade(block.fill.raw, 0.3) - } - - // Add animation components - if (this.settings.animation !== 0) { - path.transition() - .duration(this.settings.animation) - .ease(easeLinear) - .attr('fill', pathColor) - .attr('d', this.getPathDefinition(index)) - .on('end', () => { - this.drawBlock(index + 1) - }) - } else { - path.attr('fill', pathColor) - .attr('d', this.getPathDefinition(index)) - .style('opacity', 0) - .transition('opacity').duration(this.settings.customAnimate ? 600 : 0) - .style('opacity', 1) - this.drawBlock(index + 1) - } - - // Add path overlay - if (this.settings.addValueOverlay) { - path.attr('stroke', this.blocks[index].fill.raw) - - if (this.settings.animation !== 0) { - overlayPath.transition() - .duration(this.settings.animation) - .ease(easeLinear) - .attr('fill', block.fill.actual) - .attr('d', this.getOverlayPathDefinition(index)) - } else { - overlayPath.attr('fill', block.fill.actual) - .attr('d', this.getOverlayPathDefinition(index)) - } - } - - // Add the hover events - if (this.settings.hoverEffects) { - [path, overlayPath].forEach((target) => { - if (!target) { - return - } - - target - .on('mouseover', this.onMouseOver) - .on('mouseout', this.onMouseOut) - }) - } - - // Add block click event - if (this.settings.onBlockClick !== null) { - [path, overlayPath].forEach((target) => { - if (!target) { - return - } - - target.style('cursor', 'pointer') - .on('click', this.settings.onBlockClick) - }) - } - - // 自定义tooltip - [path, overlayPath].forEach((target) => { - if (!target) { - return - } - target - .on('mouseenter', this.settings.onBlockEnter) - .on('mousemove', this.settings.onBlockMove) - .on('mouseleave', this.settings.onBlockLeave) - }) - - // Add tooltips - if (this.settings.tooltip.enabled) { - [path, overlayPath].forEach((target) => { - if (!target) { - return - } - - target.node().addEventListener('mouseout', () => { - if (this.tooltip) { - this.container.removeChild(this.tooltip) - this.tooltip = null - } - }) - target.node().addEventListener('mousemove', (e) => { - if (!this.tooltip) { - this.tooltip = document.createElement('div') - this.tooltip.setAttribute('class', 'd3-funnel-tooltip') - this.container.appendChild(this.tooltip) - } - - this.tooltip.innerText = block.tooltip.formatted - - const width = this.tooltip.offsetWidth - const height = this.tooltip.offsetHeight - const rect = this.container.getBoundingClientRect() - const heightOffset = height + 5 - const containerY = rect.y + window.scrollY - const isAbove = e.pageY - heightOffset < containerY - const top = isAbove ? e.pageY + 5 : e.pageY - heightOffset - - const styles = [ - 'display: inline-block', - 'position: absolute', - `left: ${e.pageX - (width / 2)}px`, - `top: ${top}px`, - `border: 1px solid ${block.fill.raw}`, - 'background: rgb(255,255,255,0.75)', - 'padding: 5px 15px', - 'color: #000', - 'font-size: 14px', - 'font-weight: bold', - 'text-align: center', - 'cursor: default', - 'pointer-events: none' - ] - this.tooltip.setAttribute('style', styles.join(';')) - }) - }) - } - - if (this.settings.label.enabled && block.label.enabled) { - this.addBlockLabel(group, index) - } - } - - /** - * @param {Object} group - * @param {int} index - * - * @return {Object} - */ - getBlockPath (group, index) { - const path = group.append('path') - - if (this.settings.animation !== 0) { - this.addBeforeTransition(path, index, false) - } - - return path - } - - /** - * @param {Object} group - * @param {int} index - * - * @return {Object} - */ - getOverlayPath (group, index) { - const path = group.append('path') - - if (this.settings.animation !== 0) { - this.addBeforeTransition(path, index, true) - } - - return path - } - - /** - * Set the attributes of a path element before its animation. - * - * @param {Object} path - * @param {int} index - * @param {boolean} isOverlay - * - * @return {void} - */ - addBeforeTransition (path, index, isOverlay) { - const paths = isOverlay ? this.overlayPaths[index] : this.blockPaths[index] - - let beforePath = '' - let beforeFill = '' - - // Construct the top of the trapezoid and leave the other elements - // hovering around to expand downward on animation - if (!this.settings.isCurved) { - beforePath = this.navigator.plot([ - ['M', paths[0][0], paths[0][1]], - ['L', paths[1][0], paths[1][1]], - ['L', paths[1][0], paths[1][1]], - ['L', paths[0][0], paths[0][1]] - ]) - } else { - beforePath = this.navigator.plot([ - ['M', paths[0][0], paths[0][1]], - ['Q', paths[1][0], paths[1][1]], - [' ', paths[2][0], paths[2][1]], - ['L', paths[2][0], paths[2][1]], - ['M', paths[2][0], paths[2][1]], - ['Q', paths[1][0], paths[1][1]], - [' ', paths[0][0], paths[0][1]] - ]) - } - - // Use previous fill color, if available - if (this.settings.fillType === 'solid' && index > 0) { - beforeFill = this.blocks[index - 1].fill.actual - // Otherwise use current background - } else { - beforeFill = this.blocks[index].fill.actual - } - - path.attr('d', beforePath) - .attr('fill', beforeFill) - } - - /** - * Attach data to the target element. Also attach the current node to the - * data object. - * - * @param {Object} element - * @param {Object} data - * - * @return {void} - */ - attachData (element, data) { - const nodeData = { - ...data, - node: element.node() - } - - element.data([nodeData]) - } - - /** - * @param {int} index - * - * @return {string} - */ - getPathDefinition (index) { - const commands = [] - - this.blockPaths[index].forEach((command) => { - commands.push([command[2], command[0], command[1]]) - }) - - return this.navigator.plot(commands) - } - - /** - * @param {int} index - * - * @return {string} - */ - getOverlayPathDefinition (index) { - const commands = [] - - this.overlayPaths[index].forEach((command) => { - commands.push([command[2], command[0], command[1]]) - }) - - return this.navigator.plot(commands) - } - - /** - * @param {Object} event - * @param {Object} data - * - * @return {void} - */ - onMouseOver (event, data) { - const children = event.target.parentElement.childNodes; - - // Highlight all paths within one block - [...children].forEach((node) => { - if (node.nodeName.toLowerCase() === 'path') { - const type = node.getAttribute('pathType') || '' - if (type === 'foreground') { - select(node).attr('fill', this.colorizer.shade(data.fill.raw, -0.5)) - } else { - select(node).transition('fill').attr('fill', this.colorizer.shade(data.fill.raw, 0.2)) - } - } - }) - } - - /** - * @param {Object} event - * @param {Object} data - * - * @return {void} - */ - onMouseOut (event, data) { - const children = event.target.parentElement.childNodes; - - // Restore original color for all paths of a block - [...children].forEach((node) => { - if (node.nodeName.toLowerCase() === 'path') { - const type = node.getAttribute('pathType') || '' - if (type === 'background') { - const backgroundColor = this.colorizer.shade(data.fill.raw, 0.3) - select(node).attr('fill', backgroundColor) - } else { - select(node).transition('fill').attr('fill', data.fill.actual) - } - } - }) - } - - /** - * @param {Object} group - * @param {int} index - * - * @return {void} - */ - addBlockLabel (group, index) { - const paths = this.blockPaths[index] - - const formattedLabel = this.blocks[index].label.formatted - // const fill = this.blocks[index].label.color - - // Center the text - // const x = this.settings.width / 2 - // const y = this.getTextY(paths) - - // const text = group.append('text') - // .attr('x', x) - // .attr('y', y) - // .attr('fill', fill) - // .attr('font-size', this.settings.label.fontSize) - // .attr('text-anchor', 'middle') - // .attr('dominant-baseline', 'middle') - // .attr('pointer-events', 'none') - - // // Add font-family, if exists - // if (this.settings.label.fontFamily !== null) { - // text.attr('font-family', this.settings.label.fontFamily) - // } - - // this.addLabelLines(text, formattedLabel, x) - - // 修改源码使用foreignObject支持HTML 显示图标 - const foreignObject = group.append('foreignObject') - const block = this.blocks[index] - this.attachData(foreignObject, block) - - foreignObject.attr('width', (d) => { - return paths[2][0] - paths[3][0] - }) - .attr('x', paths[3][0]) - .attr('y', paths[0][1]) - .attr('height', (d) => { - return d.height - }) - .html(d=>{ - return d.height>32?formattedLabel:'' - }) - .attr('pointer-events', 'none') - .style('opacity', 0) - .transition('opacity').duration(this.settings.customAnimate ? 800 : 0) - .style('opacity', 1) - .style('cursor', 'pointer') - } - - /** - * Add elements for each line of the formatted label. - * - * @param {Object} text - * @param {String} formattedLabel - * @param {Number} x - * - * @return {void} - */ - addLabelLines (text, formattedLabel, x) { - const lines = formattedLabel.split('\n') - const lineHeight = 20 - - // dy will signify the change from the initial height y - // We need to initially start the first line at the very top, factoring - // in the other number of lines - const initialDy = (-1 * lineHeight * (lines.length - 1)) / 2 - - lines.forEach((line, i) => { - const dy = i === 0 ? initialDy : lineHeight - - text.append('tspan').attr('x', x).attr('dy', dy).text(line) - }) - } - - /** - * Returns the y position of the given label's text. This is determined by - * taking the mean of the bases. - * - * @param {Array} paths - * - * @return {Number} - */ - getTextY (paths) { - const { isCurved, curveHeight } = this.settings - - if (isCurved) { - return ((paths[2][1] + paths[3][1]) / 2) + ((1.5 * curveHeight) / this.blocks.length) - } - - return (paths[1][1] + paths[2][1]) / 2 - } -} - -export default D3Funnel diff --git a/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Formatter.js b/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Formatter.js deleted file mode 100644 index 90daee6c5..000000000 --- a/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Formatter.js +++ /dev/null @@ -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 diff --git a/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Navigator.js b/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Navigator.js deleted file mode 100644 index 6c63d03cc..000000000 --- a/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Navigator.js +++ /dev/null @@ -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 diff --git a/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Utils.js b/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Utils.js deleted file mode 100644 index 953dd00c2..000000000 --- a/nezha-fronted/src/components/chart/chart/d3-funnel/d3-funnel/Utils.js +++ /dev/null @@ -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 diff --git a/nezha-fronted/src/components/chart/chart/d3-funnel/index.js b/nezha-fronted/src/components/chart/chart/d3-funnel/index.js deleted file mode 100644 index 1c8062f98..000000000 --- a/nezha-fronted/src/components/chart/chart/d3-funnel/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// Export default to provide support for non-ES6 solutions -module.exports = require('./d3-funnel/D3Funnel').default diff --git a/nezha-fronted/src/components/chart/chart/legend.vue b/nezha-fronted/src/components/chart/chart/legend.vue index 9eda32fd5..9da175c94 100644 --- a/nezha-fronted/src/components/chart/chart/legend.vue +++ b/nezha-fronted/src/components/chart/chart/legend.vue @@ -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) diff --git a/nezha-fronted/src/components/chart/chart/options/chartPie.js b/nezha-fronted/src/components/chart/chart/options/chartPie.js index 9c6269a7d..be8a8a370 100644 --- a/nezha-fronted/src/components/chart/chart/options/chartPie.js +++ b/nezha-fronted/src/components/chart/chart/options/chartPie.js @@ -15,7 +15,7 @@ const chartPieOption = { series: [ { type: 'pie', - radius: '55%', + radius: '60%', center: ['50%', '50%'], avoidLabelOverlap: false, zlevel: 1, diff --git a/nezha-fronted/src/components/chart/chart/tools.js b/nezha-fronted/src/components/chart/chart/tools.js index 20ae4354a..d715b36cb 100644 --- a/nezha-fronted/src/components/chart/chart/tools.js +++ b/nezha-fronted/src/components/chart/chart/tools.js @@ -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 } diff --git a/nezha-fronted/src/components/common/js/constants.js b/nezha-fronted/src/components/common/js/constants.js index eb3655bb0..fbb47076c 100644 --- a/nezha-fronted/src/components/common/js/constants.js +++ b/nezha-fronted/src/components/common/js/constants.js @@ -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', diff --git a/nezha-fronted/src/components/common/rightBox/chart/chartConfig.vue b/nezha-fronted/src/components/common/rightBox/chart/chartConfig.vue index 07c320a82..7b88b2573 100644 --- a/nezha-fronted/src/components/common/rightBox/chart/chartConfig.vue +++ b/nezha-fronted/src/components/common/rightBox/chart/chartConfig.vue @@ -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 = { diff --git a/nezha-fronted/src/components/common/rightBox/chart/chartTypeShow.js b/nezha-fronted/src/components/common/rightBox/chart/chartTypeShow.js index e344a4463..7eb5fcfcd 100644 --- a/nezha-fronted/src/components/common/rightBox/chart/chartTypeShow.js +++ b/nezha-fronted/src/components/common/rightBox/chart/chartTypeShow.js @@ -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': diff --git a/nezha-fronted/src/components/common/rightBox/chart/publicConfig.js b/nezha-fronted/src/components/common/rightBox/chart/publicConfig.js index cb3afd6bf..58b58519d 100644 --- a/nezha-fronted/src/components/common/rightBox/chart/publicConfig.js +++ b/nezha-fronted/src/components/common/rightBox/chart/publicConfig.js @@ -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') diff --git a/nezha-fronted/src/components/common/rightBox/chart/systemChartConfig.vue b/nezha-fronted/src/components/common/rightBox/chart/systemChartConfig.vue index 755e4fc1a..37e36af08 100644 --- a/nezha-fronted/src/components/common/rightBox/chart/systemChartConfig.vue +++ b/nezha-fronted/src/components/common/rightBox/chart/systemChartConfig.vue @@ -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 = {