封装一个3D饼图组件 发表于 2024-07-13 最近因为工作需要,封装了一个 3D 饼图组件。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480<template> <div class="container" style="width: 100%; height: 100%"> <div id="chartsContent" style="width: 100%; height: 100%" ref="chartsContent" ></div> <div class="info" v-if="chartOptions.title && total"> <div class="name">{{ chartOptions.title }}</div> <div class="value">{{ total }}</div> </div> </div></template><script> import * as echarts from 'echarts' import 'echarts-gl' export default { data() { return { // title: "设备总数", selectedIndex: '', hoveredIndex: '', chartInstance: null, // chartData: [ // { // name: "4G执法记录仪", // value: 160, // itemStyle: { // color: "#25C9FF", // }, // }, // { // name: "酒精测试仪", // value: 144, // itemStyle: { // color: "#07FFDA", // }, // }, // { // name: "4G无线传图", // value: 104, // itemStyle: { // color: "#FFAF36", // }, // }, // ], } }, props: { chartOptions: { type: Object, default: () => ({}), }, }, computed: { total() { return this.chartOptions.chartData.reduce((pre, cur) => { return pre + cur.value }, 0) }, }, watch: { chartOptions: { deep: true, handler() { this.updateChart() }, }, }, methods: { handleResize() { if (this.chartInstance) { this.chartInstance.resize() } }, getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h) { // 计算 const midRatio = (startRatio + endRatio) / 2 const startRadian = startRatio * Math.PI * 2 const endRadian = endRatio * Math.PI * 2 const midRadian = midRatio * Math.PI * 2 // 如果只有一个扇形,则不实现选中效果。 if (startRatio === 0 && endRatio === 1) { // eslint-disable-next-line no-param-reassign isSelected = false } // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3) // eslint-disable-next-line no-param-reassign k = typeof k !== 'undefined' ? k : 1 / 3 // 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0) const offsetX = isSelected ? Math.cos(midRadian) * 0.5 : 0 const offsetY = isSelected ? Math.sin(midRadian) * 0.5 : 0 // 计算高亮效果的放大比例(未高亮,则比例为 1) const hoverRate = isHovered ? 1.05 : 1 // 返回曲面参数方程 return { u: { min: -Math.PI, // min: 0, max: Math.PI * 3, step: Math.PI / 50, }, v: { min: 0, max: Math.PI * 2, step: Math.PI / 20, }, x(u, v) { if (u < startRadian) { return ( offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate ) } if (u > endRadian) { return ( offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate ) } return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate }, y(u, v) { if (u < startRadian) { return ( offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate ) } if (u > endRadian) { return ( offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate ) } return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate }, z(u, v) { if (u < -Math.PI * 0.5) { return Math.sin(u) } if (u > Math.PI * 2.5) { return Math.sin(u) * h * 0.1 } // 当前图形的高度是Z根据h(每个value的值决定的) return Math.sin(v) > 0 ? 1 * h * 0.1 : -1 }, } }, // 生成模拟 3D 饼图的配置项 getPie3D(pieData, internalDiameterRatio) { const series = [] // 总和 let sumValue = 0 let startValue = 0 let endValue = 0 const legendData = [] const k = typeof internalDiameterRatio !== 'undefined' ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio) : 1 / 3 // 为每一个饼图数据,生成一个 series-surface 配置 for (let i = 0; i < pieData.length; i += 1) { sumValue += pieData[i].value const seriesItem = { name: typeof pieData[i].name === 'undefined' ? `series${i}` : pieData[i].name, type: 'surface', parametric: true, wireframe: { show: false, }, pieData: pieData[i], pieStatus: { selected: false, hovered: false, k, }, } if (typeof pieData[i].itemStyle !== 'undefined') { const { itemStyle } = pieData[i] // eslint-disable-next-line no-unused-expressions typeof pieData[i].itemStyle.color !== 'undefined' ? (itemStyle.color = pieData[i].itemStyle.color) : null // eslint-disable-next-line no-unused-expressions typeof pieData[i].itemStyle.opacity !== 'undefined' ? (itemStyle.opacity = pieData[i].itemStyle.opacity) : null seriesItem.itemStyle = itemStyle } series.push(seriesItem) } // 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数, // 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。 for (let i = 0; i < series.length; i += 1) { endValue = startValue + series[i].pieData.value series[i].pieData.startRatio = startValue / sumValue series[i].pieData.endRatio = endValue / sumValue series[i].parametricEquation = this.getParametricEquation( series[i].pieData.startRatio, series[i].pieData.endRatio, false, false, k, // 我这里做了一个处理,使除了第一个之外的值都是10 // series[i].pieData.value === series[0].pieData.value ? 35 : 10 8.8 ) startValue = endValue legendData.push(series[i].name) } // 准备待返回的配置项,把准备好的 legendData、series 传入。 const option = { // animation: false, tooltip: { formatter: (params) => { if (params.seriesName !== 'mouseoutSeries') { return `${ params.seriesName }<br/><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${ params.color };"></span>${option.series[params.seriesIndex].pieData.value}` } return '' }, }, legend: { show: true, itemGap: 15, // data: ['10%以内:25%', '10%-20%:25%','20%-30%:25%','30%以上:25%'], left: '0px', top: '75%', itemWidth: 10, itemHeight: 10, data: this.chartOptions.chartData.map((item, index) => { return { name: item.name, textStyle: { color: '#fff', }, } }), }, xAxis3D: { min: -1, max: 1, }, yAxis3D: { min: -1, max: 1, }, zAxis3D: { min: -1, max: 1, }, grid3D: { show: false, boxHeight: 20, top: 0, viewControl: { // 3d效果可以放大、旋转等,请自己去查看官方配置 alpha: 20, beta: 100, rotateSensitivity: 0, zoomSensitivity: 0, panSensitivity: 0, autoRotate: false, distance: 300, }, light: { main: { color: 'rgb( 255, 255,255)', // 主光源的颜色。 shadow: false, // 主光源是否投射阴影 alpha: 40, // 主光源绕 x 轴,即上下旋转的角度 }, }, // 后处理特效可以为画面添加高光、景深、环境光遮蔽(SSAO)、调色等效果。可以让整个画面更富有质感。 // postEffect: { // // 配置这项会出现锯齿,请自己去查看官方配置有办法解决 // enable: false, // bloom: { // enable: true, // bloomIntensity: 0.8, // }, // SSAO: { // enable: true, // quality: 'medium', // radius: 5, // }, // // temporalSuperSampling: { // // enable: true, // // }, // }, }, series: series, } return option }, generateChartOption() {}, updateChart() { const option = this.getPie3D(this.chartOptions.chartData, 0) this.chartInstance.setOption(option, true) }, initChart() { const chartContainer = this.$refs.chartsContent this.chartInstance = echarts.init(chartContainer) this.updateChart() }, }, mounted() { window.addEventListener('resize', this.handleResize) this.initChart() // 生成扇形的曲面参数方程,用于 series-surface.parametricEquation let selectedIndex = '' // 监听 mouseover,近似实现高亮(放大)效果 this.chartInstance.on('mouseover', (params) => { const option = this.getPie3D(this.chartOptions.chartData, 0) // 准备重新渲染扇形所需的参数 let isSelected let isHovered let startRatio let endRatio let k let i // 如果触发 mouseover 的扇形当前已高亮,则不做操作 if (this.hoveredIndex === params.seriesIndex) { return // 否则进行高亮及必要的取消高亮操作 } else { // 如果当前有高亮的扇形,取消其高亮状态(对 option 更新) if (this.hoveredIndex !== '') { // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 false。 isSelected = option.series[this.hoveredIndex].pieStatus.selected isHovered = false startRatio = option.series[this.hoveredIndex].pieData.startRatio endRatio = option.series[this.hoveredIndex].pieData.endRatio k = option.series[this.hoveredIndex].pieStatus.k i = option.series[this.hoveredIndex].pieData.value === option.series[0].pieData.value ? 35 : 10 // 对当前点击的扇形,执行取消高亮操作(对 option 更新) option.series[this.hoveredIndex].parametricEquation = this.getParametricEquation( startRatio, endRatio, isSelected, isHovered, k, // i 8.8 ) option.series[this.hoveredIndex].pieStatus.hovered = isHovered // 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空 this.hoveredIndex = '' } // 如果触发 mouseover 的扇形不是透明圆环,将其高亮(对 option 更新) if (params.seriesName !== 'mouseoutSeries') { // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。 isSelected = option.series[params.seriesIndex].pieStatus.selected isHovered = true startRatio = option.series[params.seriesIndex].pieData.startRatio endRatio = option.series[params.seriesIndex].pieData.endRatio k = option.series[params.seriesIndex].pieStatus.k // 对当前点击的扇形,执行高亮操作(对 option 更新) option.series[params.seriesIndex].parametricEquation = this.getParametricEquation( startRatio, endRatio, isSelected, isHovered, k, // option.series[params.seriesIndex].pieData.value + 5 8.8 ) option.series[params.seriesIndex].pieStatus.hovered = isHovered // 记录上次高亮的扇形对应的系列号 seriesIndex this.hoveredIndex = params.seriesIndex } // 使用更新后的 option,渲染图表 this.chartInstance.setOption(option, true) } }) // 修正取消高亮失败的 bug this.chartInstance.on('globalout', () => { const option = this.getPie3D(this.chartOptions.chartData, 0) if (this.hoveredIndex !== '') { // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。 let isSelected = option.series[this.hoveredIndex].pieStatus.selected let isHovered = false let k = option.series[this.hoveredIndex].pieStatus.k let startRatio = option.series[this.hoveredIndex].pieData.startRatio let endRatio = option.series[this.hoveredIndex].pieData.endRatio // 对当前点击的扇形,执行取消高亮操作(对 option 更新) let i = option.series[this.hoveredIndex].pieData.value === option.series[0].pieData.value ? 35 : 10 option.series[this.hoveredIndex].parametricEquation = this.getParametricEquation( startRatio, endRatio, isSelected, isHovered, k, 8.8 ) option.series[this.hoveredIndex].pieStatus.hovered = isHovered // 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空 this.hoveredIndex = '' } // 使用更新后的 option,渲染图表 this.chartInstance.setOption(option, true) }) }, beforeDestroy() { window.removeEventListener('resize', this.handleResize) if (this.chartInstance) { this.chartInstance.dispose() } }, }</script><style lang="scss" scoped> .container { position: relative; .info { position: absolute; width: 100%; top: 10%; left: 0; font-size: 16px; color: #fff; .name { text-align: center; } .value { text-align: center; color: #06b6f8; } } }</style>