定义组件ChartPie
<div class="chart-pie" :class="{ 'no-title': !title }">
<div class="chart-pie-title" v-if="title">
{{ title }}
</div>
<Chart v-on="$listeners" v-bind="$attrs" :option="mergedOption" ref="chartBase" />
<slot name="legend" :data="data" :percents="percents" :colors="colors">
<table class="legend-table" v-if="legend">
<tbody>
<tr v-for="(item, index) in data" :key="item[nameKey]" class="legend">
<td>
<div class="dot" :style="{ backgroundColor: colors[index] }"/>
</td>
<td class="name">
<XText font-size="48px">{{ item[nameKey] }}</XText>
</td>
<td class="align-right">
<p>
<Num font-size="56px">{{ item[valueKey] }}</Num>
<Unit v-if="unit" font-size="40px">{{ unit }}</Unit>
</p>
</td>
<td v-if="percent" class="align-right">
<Num font-size="56px" style="margin: 0;">{{ percents[index] }}%</Num>
</td>
</tr>
</tbody>
</table>
</slot>
</div>
import XText from 'XText'
import Unit from 'Unit'
import Num from 'Num'
import Chart from 'Chart'
const TinyColor = window.tinycolor.TinyColor // 颜色操作与转换的开源库
export default Vue.extend({
inheritAttrs: false,
components: {
XText,
Unit,
Num,
Chart,
},
props: {
data: {
type: Array,
required: true
},
title: {
type: String,
default: undefined
},
option: {
type: Object,
default: () => ({})
},
legend: {
type: Boolean,
default: true
},
unit: {
type: String,
default: undefined
},
percent: {
type: Boolean,
default: false
},
nameKey: {
type: String,
default: 'name'
},
valueKey: {
type: String,
default: 'value'
},
percentRoundPrecision: {
default: 2,
type: Number
}
},
data() {
return {
isMounted: false,
}
},
mounted() {
this.isMounted = true
},
computed: {
percents() {
if (!this.percent) {
return []
}
const key = this.valueKey
const data = this.data.map(d => Number(d[key]))
const sum = _.sum(data)
return data.map(d => (d / sum * 100).toFixed(this.percentRoundPrecision))
},
defaultColors() {
return [
'#52D680',
'#CE7DEB',
'#48CFF1',
'#1E95E3',
'#BADB3C',
'#87D04D',
'#EFBF59',
'#2F7AE6',
'#7991FF',
'#1CC143',
'#E17940',
'#D89B38',
'#12CFCD',
'#85AACA',
]
},
colors() {
return this.option.color || this.defaultColors
},
mergedOption() {
if (!this.isMounted) {
return
}
const chart = this.$refs.chartBase.$refs.chart
if (!chart) {
return
}
const centerX = chart.getWidth() / 2
const centerY = chart.getHeight() / 2
const color = this.colors
const nameKey = this.nameKey
const valueKey = this.valueKey
const baseOption = {
tooltip: {},
color,
series: {
type: 'pie',
radius: ['45%', '90%'],
label: { show: false },
labelLine: { show: false },
padAngle: 2,
data: this.data.map((d, i) => {
const outerColor = color[i]
const innerColor = new TinyColor(outerColor).setAlpha(0.4).toString()
return {
name: d[nameKey],
value: d[valueKey],
itemStyle: {
color: {
type: 'radial',
x: centerX,
y: centerY,
r: Math.min(centerX, centerY),
colorStops: [
{ offset: 0, color: outerColor },
{ offset: 0, color: innerColor },
{ offset: 0.8, color: innerColor },
{ offset: 0.8, color: outerColor },
{ offset: 1, color: outerColor },
],
global: true
}
}
}
})
},
}
return _.merge(baseOption, this.option)
},
},
})
// less code here
.chart-pie {
width: 100%;
height: 100%;
flex: 1;
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
'title .'
'chart legend';
align-items: center;
gap: 20px;
&.no-title {
grid-template-columns: auto 1fr;
grid-template-rows: 1fr;
grid-template-areas:
'chart legend';
}
&-title {
font-size: 54px;
color: white;
font-weight: 500;
justify-self: center;
grid-area: title;
font-family: PingFangSC;
}
.chart {
aspect-ratio: 1 / 1;
grid-area: chart;
}
.legend-table {
grid-area: legend;
white-space: nowrap;
line-height: 1;
border-collapse: collapse;
td {
padding: var(--legend-padding, 10px);
}
.dot {
width: 24px;
height: 24px;
border-radius: 50%;
}
.name {
width: 100%;
}
.align-right {
text-align: right;
}
}
}
- 其中用到的组件有Chart如下,其它组件(XText,Unit,Num)略
<VChart
class="chart"
ref="chart"
:option="mergedOption"
:loading-options="loadingOptions"
v-bind="$attrs"
v-on="$listeners"
/>
const defaultXAxisOption = {
axisLabel: {
interval: 0,
textStyle: {
fontSize: 64,
color: '#CCCCCC',
},
},
axisLine: {
lineStyle: {
color: '#4B636A',
width: 4,
},
},
axisTick: {
show: false,
},
}
const defaultYAxisOption = {
nameTextStyle: {
color: '#CCCCCC',
fontSize: 64,
},
splitLine: {
lineStyle: {
type: 'dashed',
},
},
axisTick: {
show: false,
},
axisLine: {
show: false,
},
axisLabel: {
textStyle: {
color: '#CCCCCC',
fontSize: 64,
},
},
}
const defaultLegendOption = {
lineStyle: {
width: 10,
},
textStyle: {
color: '#ffffff',
fontSize: 54,
padding: [0, 0, 0, 30],
},
itemHeight: 40,
itemWidth: 100,
itemGap: 60,
}
const defaultTooltipOption = {
textStyle: {
fontSize: 64,
color: '#fff',
},
appendToBody: true,
backgroundColor: '#0C3E5F',
borderColor: '#0CB6FF',
borderWidth: 4,
padding: 20,
}
export default Vue.extend({
components: {
VChart: window.VueECharts
},
inheritAttrs: false,
props: {
autoresize: {
type: Boolean,
default: false
},
option: {
type: Object,
default: () => ({})
}
},
data() {
return {
resizeObserver: undefined
}
},
mounted() {
if (this.autoresize) {
this.enableAutoResize()
}
},
beforeDestroy() {
this.$refs.chart?.dispose()
this.resizeObserver?.disconnect()
},
computed: {
mergedOption() {
const { tooltip, xAxis, yAxis, legend } = this.option
const baseOption = {
xAxis: this.copySingleOrArray(xAxis, defaultXAxisOption),
yAxis: this.copySingleOrArray(yAxis, defaultYAxisOption),
legend: this.copySingleOrArray(legend, defaultLegendOption),
tooltip: this.copySingleOrArray(tooltip, defaultTooltipOption),
}
_.merge(baseOption, this.option)
this.setTooltipClass(baseOption.tooltip)
return baseOption
},
loadingOptions() {
return {
maskColor: 'rgba(0, 0, 0, 0.6)',
textColor: 'white',
color: 'white',
fontSize: '86px',
fontFamily: 'PingFangSC',
spinnerRadius: 30,
lineWidth: 10,
text: ''
}
}
},
methods: {
enableAutoResize() {
this.resizeObserver = new ResizeObserver(() => {
this.$refs.chart?.resize()
})
this.resizeObserver.observe(this.$el)
},
copySingleOrArray(customOption, defaultOption) {
if (!customOption) {
return undefined
} else if (Array.isArray(customOption)) {
const result = []
let size = customOption.length
while (size--) {
result.push(_.cloneDeep(defaultOption))
}
return result
}
return _.cloneDeep(defaultOption)
},
setTooltipClass(tooltip) {
if (!tooltip) {
return
}
const items = Array.isArray(tooltip) ? tooltip : [tooltip]
for (const item of items) {
const classes = ['chart-tooltip']
if (item.className) {
classes.push(item.className)
}
item.className = classes.join(' ')
}
}
}
})
// less code here
.chart {
flex: 1;
overflow: hidden;
&-tooltip {
span {
&:first-of-type {
border-radius: 50% !important;
height: 64px !important;
width: 64px !important;
vertical-align: top;
border: 12px solid transparent;
background-clip: content-box;
}
}
}
}
使用ChartPie组件的方法
<ChartPie title="学历结构" :data="educationChartData" unit="人" />
数据结构
[{
"unit": "人",
"name": "专科",
"value": 0
}]