Vue2 项目 ECharts 统计图表组件封装与使用实践
本文主要说明在前后端分离的 Vue2 项目 中,如何用 ECharts 实现统计图表展示,并把多种图表类型封装成可复用组件。
核心思路:页面组件(容器)负责请求后端数据并整理成固定结构 → 图表组件(纯展示)只接收 props 渲染 。
innovation.vue(创新能力)是使用组件的典型示例。
一、技术栈与依赖
- Vue 2.6.12 (前端框架,
vue-cli-service构建) - ECharts 5.1.1 (统计图表库,见
package.json依赖) - 前后端分离 :数据通过接口(
FormFunctions.invokeCustomQueryPromise等)异步获取
package.json 依赖片段:
json
{
"dependencies": {
"echarts": "5.1.1"
}
}
组件内统一按需引入方式(当前项目用的是全量引入):
js
import * as echarts from "echarts";
二、组件化设计思路
- 展示与数据解耦
- 图表组件内部不请求接口 ,只接收
data(以及高度、单位、颜色等配置)props。 - 页面组件负责调接口、把返回数据转换成「组件约定的 data 结构」,再传给图表组件。
- 图表组件内部不请求接口 ,只接收
- 一图一组件
- 每种图表类型(柱状图、折线图、饼图、雷达图、地图、组合图...)封装成一个独立
.vue组件,通过 props 控制外观。
- 每种图表类型(柱状图、折线图、饼图、雷达图、地图、组合图...)封装成一个独立
- 统一更新机制
- 用
watch: { data: { deep: true, handler } }监听数据变化,数据一变就重新initChart()重绘;也常用:key="timer"强制重建实例。
- 用
- 按需注册,不做全局注册
- 组件默认局部引入 (
import+components),不挂到Vue.component全局,避免体积膨胀与命名冲突。
- 组件默认局部引入 (
三、组件公共约定
这些图表组件虽然种类不同,但遵循一套统一约定,便于替换与组合:
| 约定 | 说明 |
|---|---|
width / height |
容器宽高(字符串,如 "100%"、"350px")。必须给容器有效高度,否则 ECharts 尺寸为 0 不显示 |
data |
图表数据,格式随图表类型而定(见「数据契约」) |
unit |
数值单位(如 件、户、%),用于 tooltip / 坐标轴 |
fontSize |
字号(默认 14) |
tips |
是否显示 tooltip / 图例可点击(默认 true) |
colorList / color |
颜色配置,支持渐变或自定义色板 |
| 初始化方式 | 三种:echarts.init(this.$el)、echarts.init(this.$refs.xxx)、echarts.init(document.getElementById(id))(用 id 方式时必须保证 id 唯一) |
| 数据更新 | watch data(deep) → 重新 setOption;部分组件用 dispose() 后重建 |
| 窗口自适应 | window.onresize = () => chart.resize() |
注意:多数老组件用
window.onresize = function(){...}覆盖式 绑定 resize。同一页面若同时存在多个图表实例,只有最后一个生效------这是历史实现的一个隐患,新组件建议改用window.addEventListener("resize", ...)或统一 resize 管理。
四、组件清单与图表类型总览
所有图表纯展示组件都放在同一个图表组件目录下,按类型分为基础图表与组合图表两类:
| 组件 | 图表类型 | 一句话说明 |
|---|---|---|
echart_barA.vue |
竖向柱状图(多系列) | 最常用柱状图,支持多系列、渐变、图例、dataZoom |
echart_barB.vue |
横向条形图(排行榜) | 名称在左、数值在右的排行条,高度按条数自适应 |
echart_line.vue |
折线图 / 面积图 | 多系列折线,带渐变面积填充,可平滑 |
echart_pieA.vue |
饼图 / 玫瑰图 | 双环饼图,支持下钻、百分比图层、"其它"合并 |
echart_radar.vue |
雷达图 | 多维度对比,支持多系列叠加、面积填充 |
echart_bar_line.vue |
柱状图 + 折线图(双 Y 轴) | 一根柱 + 一条线(右轴),带自动轮播 |
echart_barLine_compare.vue |
柱状图 + 折线面积图(对比分析) | 多组柱 + 折线,双轴,自动轮播并高亮 tooltip |
echart_barLine_group.vue |
柱状图 + 折线图(分组分析) | 与 compare 类似,折线为圆点、非面积 |
echart_barMore.vue |
多系列柱(可堆叠)+ 多折线 | 更灵活的组合图,支持堆叠与占比标签、鼠标联动 |
echart_mapC.vue |
地图(中国地图) | geo + map 系列的行政区着色图 |
五、各组件详解
5.1 echart_barA.vue ------ 竖向柱状图
- 图表类型 :
bar(多系列竖向柱)。 - props :
className/width/height(默认350px)/zoomNumber(默认 8)/zoomLock(默认 true)/tips/fontSize/unit/data/colorList。 - data 结构:
js
{
xAxis: ["2021年", "2022年", "2023年"],
series: [
{ name: "商标", data: [10, 20, 30] },
{ name: "发明专利", data: [5, 8, 12] }
]
}
- 关键实现 :
series全部type: "bar",barWidth: "8%"、barGap: "25%";- 每根柱用
echarts.graphic.LinearGradient做垂直渐变,颜色取自colorList[index]([起始色, 结束色, 描边色]); - 图例在顶部、
grid.top: "30%"(给图例留位); dataZoom由zoomLock控制:锁定则不显示滑动条。
- 适用:分类对比、逐年趋势(离散)。
完整代码
vue
<!-- 柱状图 -->
<template>
<div :class="className" :style="{ height: height, width: width }" />
</template>
<script>
import * as echarts from "echarts";
const animationDuration = 1000;
export default {
props: {
className: {
type: String,
default: "chart",
},
width: {
type: String,
default: "100%",
},
height: {
type: String,
default: "350px",
},
zoomNumber: {
type: Number,
default: 8,
},
zoomLock: {
type: Boolean,
default: true,
},
tips: {
type: Boolean,
default: true,
},
fontSize: {
type: Number,
default: 14,
},
unit: String,
data: {
type: Object,
default() {
return {
xAxis: ["区域一", "区域二", "区域三"],
series: [
{
name: "运行中",
data: [250, 150, 170],
},
],
};
},
},
colorList: {
type: Array,
default() {
return [
["#6daeff", "#4292f5", "#2a7ce3"],
["#81ebdd", "#26d6bf", "#34b2a2"],
["#fee386", "#e7b814", "#d1a407"],
["#feb7d4", "#f44e7b", "#fa2961"],
["#f39d69", "#eb7630", "#d65c12"],
["#8dc87f", "#5fae4c", "#3e9d27"],
["#ee7fef", "#d747d9", "#c627c8"],
["#8e8324", "#c1b80a", "#574525"],
];
},
},
},
data() {
return {
chart: null,
};
},
mounted() {
let self = this;
this.$nextTick(() => {
self.initChart();
});
},
beforeDestroy() {
if (!this.chart) {
return;
}
},
watch: {
data: {
handler() {
let self = this;
self.initChart();
},
deep: true,
},
},
methods: {
initChart() {
let unit = this.unit;
let color = this.colorList;
this.chart = echarts.init(this.$el);
let series = [];
let legendArray = [];
this.data.series.forEach((item, index) => {
legendArray.push(item.name);
series.push({
name: item.name,
type: "bar",
barWidth: "8%",
barGap: "25%",
animationDuration,
itemStyle: {
normal: {
borderWidth: color[index][2] ? 1 : 0,
borderColor: color[index][2] ? color[index][2] : "transparent",
color: new echarts.graphic.LinearGradient(
0, 0, 0, 1,
[
{ offset: 0, color: color[index][0] },
{ offset: 1, color: color[index][1] },
],
false
),
},
},
data: item.data,
});
});
let option = {
tooltip: {
show: this.tips,
trigger: "axis",
axisPointer: {
type: "line",
lineStyle: {
color: "#1890ff",
},
},
formatter: function (params) {
let relVal = params[0].name;
params.forEach((item) => {
relVal += "<br/>" + item.marker + item.seriesName + " ";
relVal += "<span style='float:right;font-weight: bold'>";
relVal += item.value + unit;
relVal += "</span>";
});
return relVal;
},
},
legend: {
selectedMode: this.tips,
itemWidth: 12,
itemHeight: 12,
itemGap: 13,
data: legendArray,
top: "2%",
textStyle: {
fontSize: this.fontSize,
color: "#8699AD",
},
itemStyle: {
borderWidth: 0,
},
},
grid: {
top: "30%",
bottom: "30",
left: "0%",
right: "0%",
containLabel: true,
},
xAxis: [
{
type: "category",
data: this.data.xAxis,
axisLine: {
lineStyle: { color: "#EEF0F4" },
},
axisTick: { show: false },
axisLabel: {
interval: 0,
textStyle: {
fontSize: this.fontSize,
color: "#8699AD",
},
},
},
],
yAxis: [
{
type: "value",
minInterval: 1,
axisTick: { show: false },
axisLine: {
lineStyle: { color: "#EEF0F4" },
},
axisLabel: {
formatter: "{value}" + unit,
textStyle: {
fontSize: this.fontSize,
color: "#8699AD",
},
},
splitLine: {
lineStyle: { color: "#EEF0F4" },
},
},
],
dataZoom: [
{
show: !this.zoomLock,
type: "slider",
startValue: 0,
bottom: 10,
height: 10,
zoomLock: this.zoomLock,
brushSelect: false,
moveHandleSize: 10,
fillerColor: "rgba(167,183,204,0.4)",
endValue: this.zoomNumber,
showDetail: false,
},
{
type: "inside",
disabled: false,
show: !this.zoomLock,
zoomLock: this.zoomLock,
zoomOnMouseWheel: false,
moveOnMouseMove: false,
moveOnMouseWheel: true,
},
],
series: series,
};
this.chart.setOption(option);
},
},
};
</script>
<style scoped>
</style>
5.2 echart_barB.vue ------ 横向条形图(排行榜)
- 图表类型 :
bar(横向,yAxis类目 +inverse: true实现从上到下排序)。 - props :
className/width/zoomNumber/zoomLock/tips/fontSize/unit/data(Array)。 - data 结构:
js
[
{ name: "企业A", value: 53, sum: 50 },
{ name: "企业B", value: 44, sum: 60 }
]
- 关键实现 :
- 没有
heightprop ,高度按数据条数自适应:this.height = data.length * 50 + "px"(在watch中设置); xAxis.show: false;yAxis用两个隐藏轴,第二个轴的axisLabel.formatter显示data[index].value(数值跟条走);- 柱子横向渐变(
#2998f6 → #5baeea → #5EDEE5),showBackground: true显示底槽,label显示名称。
- 没有
- 适用:企业/区域排行榜 Top N。
完整代码
vue
<!-- 横向条形图(排行榜) -->
<template>
<div :class="className" :style="{ height: height, width: width }" />
</template>
<script>
import * as echarts from "echarts";
export default {
props: {
className: {
type: String,
default: "chart",
},
width: {
type: String,
default: "100%",
},
zoomNumber: {
type: Number,
default: 8,
},
zoomLock: {
type: Boolean,
default: true,
},
tips: {
type: Boolean,
default: true,
},
fontSize: {
type: Number,
default: 14,
},
unit: String,
data: {
type: Array,
default() {
return [
{ name: "item1", value: 23, sum: 50 },
{ name: "item2", value: 44, sum: 60 },
];
},
},
},
data() {
return {
chart: null,
height: null,
};
},
mounted() {},
beforeDestroy() {
if (!this.chart) {
return;
}
},
watch: {
data: {
handler() {
if (this.data.length > 0) {
let self = this;
this.height = this.data.length * 50 + "px";
this.chart = null;
self.$nextTick(() => {
self.initChart();
});
}
},
deep: true,
immediate: true,
},
},
methods: {
initChart() {
let data = this.data;
this.chart = echarts.init(this.$el);
// 取出某个字段的数组
let getValuesByKey = (list, key) => {
let field = key || "value";
let result = [];
if (list) {
list.forEach((item) => {
result.push(item[field]);
});
}
return result;
};
let option = {
grid: {
top: "2%",
bottom: -15,
right: 20,
left: 0,
containLabel: false,
},
xAxis: {
show: false,
},
yAxis: [
{
triggerEvent: true,
show: true,
inverse: true,
data: getValuesByKey(data, "name"),
axisLine: { show: false },
splitLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false },
},
{
triggerEvent: true,
show: true,
inverse: true,
data: getValuesByKey(data, "name"),
axisLine: { show: false },
splitLine: { show: false },
axisTick: { show: false },
axisLabel: {
interval: 0,
shadowOffsetX: "20px",
color: "#2998f6",
align: "right",
verticalAlign: "bottom",
lineHeight: 30,
fontSize: 13,
formatter: function (value, index) {
return data[index].value;
},
},
},
],
series: [
{
name: "条",
type: "bar",
showBackground: true,
barBorderRadius: 30,
yAxisIndex: 0,
data: data,
barWidth: 12,
itemStyle: {
normal: {
color: new echarts.graphic.LinearGradient(
0, 0, 1, 0,
[
{ offset: 0, color: "#2998f6" },
{ offset: 0.7, color: "#5ba8ea" },
{ offset: 1, color: "#5EDEE5" },
],
false
),
barBorderRadius: 10,
},
barBorderRadius: 4,
},
label: {
normal: {
color: "#666",
show: true,
position: [0, "-20px"],
textStyle: {
fontSize: 14,
fontWeight: "bold",
},
formatter: function (item) {
return item.name;
},
},
},
},
],
};
this.chart.setOption(option);
},
},
};
</script>
<style scoped>
</style>
5.3 echart_line.vue ------ 折线图 / 面积图
- 图表类型 :
line(多系列,带渐变面积)。 - props :
height(默认350px)/width/id(默认'ec')/fontSize/tips/unit/data。 - data 结构:
js
{
xAxis: ["1月", "2月", "3月"],
series: [
{ name: "销售额", data: [10, 20, 15], unit: "万元" },
{ name: "利润", data: [3, 5, 4], nosmooth: true } // 加 nosmooth 则该系列不平滑
]
}
- 关键实现 :
type: "line",symbolSize: 1;默认smooth: true(e.nosmooth时为折线);areaStyle用颜色列表做垂直渐变,opacity: 0.05;tooltip自定义formatter,支持每个系列单独配置单位。
- 适用:连续时间趋势。
完整代码
vue
<template>
<div class="Echart_line">
<div class="bg"></div>
<div class="tb" :id="id" ref="chartRef" :style="{ width: width, height: height }"></div>
</div>
</template>
<script>
import * as echarts from "echarts";
export default {
name: "Echart_line",
props: {
height: {
type: String,
default: "350px",
},
width: {
type: String,
default: "100%",
},
id: {
type: String,
default: "ec",
},
fontSize: {
type: Number,
default: 14,
},
tips: {
type: Boolean,
default: true,
},
unit: {
type: String,
default: "",
},
data: {
type: Object,
default() {
return {
xAxis: [],
series: [],
};
},
},
},
data() {
return {
myChart: {},
};
},
watch: {
data() {
this.init();
},
},
mounted() {
let self = this;
self.init();
window.addEventListener("resize", function () {
self.myChart.resize();
});
},
methods: {
init() {
let self = this;
let unit = this.unit;
this.myChart = echarts.init(this.$refs.chartRef);
let colorList = [
"#3E7BF4", "#FAAD13", "#FF3F70", "#ffdb5c", "#ff9f7f",
"#fb7293", "#e7bcf3", "#3CC1F3",
];
let series = [];
this.data.series.forEach((item, index) => {
series.push({
name: item.name,
type: "line",
data: item.data,
symbolSize: 1,
smooth: !item.nosmooth,
areaStyle: { // 区域填充渐变颜色
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: colorList[index] },
{ offset: 1, color: "rgba(255, 221, 211, 0)" },
],
global: false,
},
opacity: 0.05,
},
});
});
let option = {
legend: {
show: true,
selectedMode: this.tips,
icon: "roundRect",
itemWidth: 20,
itemHeight: 4,
itemGap: 20,
top: 0,
right: 20,
textStyle: {
fontSize: this.fontSize,
color: "#7270A6",
fontFamily: "OPPOSans-M",
},
},
grid: {
left: 30,
right: 30,
top: 50,
bottom: 0,
containLabel: true,
},
color: colorList,
xAxis: [
{
type: "category",
boundaryGap: false,
data: this.data.xAxis,
axisLine: { lineStyle: { color: "#EEF0F4" } },
axisTick: { show: false },
axisLabel: {
textStyle: {
fontSize: this.fontSize,
color: "#999",
},
},
},
],
yAxis: [
{
type: "value",
minInterval: 1,
name: this.unit,
nameTextStyle: { color: "#999999" },
axisTick: { show: false },
axisLine: { lineStyle: { color: "#EEF0F4" } },
axisLabel: {
formatter: "{value}",
textStyle: {
fontSize: this.fontSize,
color: "#999",
},
},
splitLine: { lineStyle: { color: "#EEF0F4" } },
},
],
tooltip: {
show: this.tips,
confine: true,
trigger: "axis",
formatter: function (params) {
let relVal = params[0].name;
let handled = [];
params.forEach((item) => {
let unitStr = unit;
if (handled.includes(item.seriesName)) {
return;
}
handled.push(item.seriesName);
let matched = self.data.series.filter(
(p) => p.name === item.seriesName
);
if (matched.length > 0 && matched[0].unit) {
unitStr = matched[0].unit;
}
relVal += "<br/>" + item.marker + item.seriesName + " ";
relVal += "<span style='float:right;font-weight: bold'>";
relVal += item.value + unitStr;
relVal += "</span>";
});
return relVal;
},
},
series: series,
};
this.myChart.setOption(option);
},
},
};
</script>
<style scoped lang="scss">
</style>
5.4 echart_pieA.vue ------ 饼图 / 玫瑰图
- 图表类型 :
pie,可选玫瑰图(roseType: 'radius',默认开启)。 - props :
id(默认Ec_时间戳)/data/radius/radius1/center/className/styleName/width/height/fontSize/color/tips/unit/formatter/roseType/label/labelColor/labelFormatter/labelLine/toolbox/restore/percent/filter。 - data 结构:
js
[
{ label: "商标", name: "商标", value: 30, id: "1", pid: "0" },
{ label: "发明专利", name: "发明专利", value: 20, id: "2", pid: "0" }
]
- 关键实现 :
- 双环效果 :主环(
radius)+ 内环占位(radius1,半透明白); getData():filter过滤value == 0;按pid过滤实现下钻 ;只保留前 7 项,其余合并为「其它」;restore: true时启用工具箱「重置」按钮 + 点击下钻(pid);percent: true时追加一层pie在扇区内显示百分比;- 用
document.getElementById(this.id)初始化,必须传唯一 id。
- 双环效果 :主环(
- 适用:占比、构成、可下钻的分类结构。
完整代码
vue
<template>
<div class="Echart_pie" :class="className" :style="styleName">
<div class="bg" />
<div class="tb" :id="id" :style="{ width: width, height: height }" />
</div>
</template>
<script>
import * as echarts from "echarts";
export default {
name: "EchartPie",
props: {
// 唯一标识
id: { type: String, default: "Ec_" + new Date().getTime() },
// 数据来源
data: { type: Array, default: function () { return []; } },
// 主要展示层
radius: { type: Array, default: function () { return ["30%", "90%"]; } },
radius1: { type: Array, default: function () { return ["30%", "60%"]; } },
center: { type: Array, default: function () { return ["50%", "50%"]; } },
// 自定义样式
className: { type: String, default: "" },
styleName: { type: String, default: "" },
// 宽高
width: { type: String, default: "100%" },
height: { type: String, default: "100%" },
// 字体大小
fontSize: { type: Number, default: 14 },
// 颜色配置
color: {
type: Array,
default: function () {
return [
"#4292f5", "#26d6bf", "#e7b814", "#f44e7b",
"#eb7630", "#5fae4c", "#d747d9", "#c1b80a",
];
},
},
// 浮框
tips: { type: Boolean, default: true },
// 单位
unit: { type: String, default: "" },
// 浮框自定义方法
formatter: { type: Function, default: null },
// roseType
roseType: { type: Boolean, default: true },
// label
label: { type: Boolean, default: false },
labelColor: { type: String, default: "inherit" },
labelFormatter: { type: Function, default: null },
labelLine: { type: Boolean, default: false },
// 是否展示可视化的工具箱
toolbox: { type: Boolean, default: false },
// 下钻事件
restore: { type: Boolean, default: false },
// 百分比图层
percent: { type: Boolean, default: false },
// 过滤空值
filter: { type: Boolean, default: true },
},
data() {
return {
myChart: {},
pid: "0",
};
},
watch: {
data() {
this.init(this.getData());
},
pid() {
this.init(this.getData());
window.eventBus.$emit("EchartPie1", this.pid);
},
},
mounted() {
let self = this;
self.init(self.getData());
window.addEventListener("resize", function () {
self.myChart.resize();
});
if (this.restore) {
this.myChart.on("click", function (params) {
if (params.data.id && params.data.pid === "0") {
self.pid = params.data.id;
}
});
}
},
methods: {
getData() {
let pid = this.pid;
let data = this.filter
? this.data.filter((item) => item.value != 0)
: this.data;
if (pid != null) {
data = data.filter(
(item) => !item.pid || String(item.pid) === String(pid)
);
}
let result = [];
let other = { name: "其它", value: 0 };
data.forEach((item, index) => {
if (index < 7) {
result.push({ name: item.label, value: item.value });
} else {
other.value += item.value;
}
});
if (other.value > 0) {
result.push(other);
}
return result;
},
init(data) {
let self = this;
this.myChart = echarts.init(document.getElementById(this.id));
let colorList = self.color;
let option = {
legend: {
selectedMode: this.tips,
itemWidth: 12,
itemHeight: 12,
itemGap: 13,
y: "center",
top: "2%",
textStyle: {
fontSize: this.fontSize,
color: "#8699AD",
},
},
grid: {
left: 0,
right: "0%",
},
toolbox: { // 可视化的工具箱
show: self.toolbox,
feature: {
myTool1: {
show: self.restore,
title: "重置",
icon: "path://M969.916221 501.771031c-11.116175 0-20.128433 9.012258-20.128433 20.128433 0 57.860802-11.328 113.980961-33.668821 166.799934-21.583574 51.027164-52.485375 96.860025-91.848894 136.222521-39.362496 39.36352-85.195358 70.26532-136.222521 91.848894-52.819996 22.340821-108.940155 33.668821-166.800957 33.668821s-113.979938-11.328-166.79891-33.668821c-51.027164-21.583574-96.859002-52.485375-136.221498-91.847871-39.362496-39.36352-70.264297-85.195358-91.846848-136.223544-22.340821-52.818973-33.667797-108.939131-33.667797-166.799934s11.328-113.980961 33.667797-166.799934c21.582551-51.027164 52.485375-96.859002 91.846848-136.221498 39.362496-39.362496 85.194334-70.264297 136.221498-91.846848 52.818973-22.340821 108.939131-33.667797 166.79891-33.667797 88.588641 0 174.554548 27.421536 246.359944 77.895091L662.939711 188.895161c-10.961656 1.847068-18.351973 12.231579-16.503882 23.193235 1.655709 9.830903 10.17678 16.787338 19.824511 16.787338 1.110287 0 2.237971-0.092098 3.368724-0.283456l151.254889-25.487487c10.961656-1.847068 18.351973-12.230556 16.503882-23.193235L813.903981 40.545438c-1.847068-10.961656-12.229532-18.349927-23.193235-16.503882-10.961656 1.847068-18.351973 12.230556-16.503882 23.193235l15.182794 90.107227C711.11185 82.737376 617.58985 53.105498 521.246595 53.105498c-63.277162 0-124.673487 12.397355-182.482101 36.848233-55.82647 23.612791-105.95824 57.411571-149.005662 100.457969-43.046399 43.046399-76.845179 93.179193-100.457969 149.004639C64.851007 397.224953 52.453652 458.621279 52.453652 521.898441c0 63.277162 12.397355 124.673487 36.848233 182.483124 23.612791 55.82647 57.411571 105.959264 100.457969 149.006685 43.046399 43.046399 93.179193 76.846202 149.005662 100.458993 57.808614 24.451902 119.203916 36.849256 182.482101 36.849256 63.278185 0 124.674511-12.397355 182.483124-36.849256 55.82647-23.612791 105.959264-57.411571 149.006685-100.458993 43.046399-43.047422 76.846202-93.181239 100.458993-149.006685 24.451902-57.808614 36.849256-119.204939 36.849256-182.483124C990.044654 510.782266 981.032396 501.771031 969.916221 501.771031z",
onclick: () => {
self.pid = 0;
},
},
},
},
tooltip: {
show: this.tips,
confine: true,
trigger: "item",
formatter: function (params) {
if (self.formatter) {
return self.formatter(params);
} else {
return `${params.marker}${params.name}:${params.data.value}` + (self.unit || "");
}
},
},
series: [ // 主要展示层
{
radius: self.radius,
center: self.center,
type: "pie",
roseType: self.roseType ? "radius" : "",
itemStyle: {
normal: {
color: function (params) {
return colorList[params.dataIndex];
},
},
},
labelLine: {
normal: {
show: self.labelLine,
lineStyle: { color: "#E2E2E2" },
},
},
label: {
normal: {
show: self.label,
fontSize: self.fontSize,
color: self.labelColor,
formatter: self.labelFormatter
? (params) => self.labelFormatter(params)
: "{b}\n{c|{d}}%",
lineHeight: 20,
rich: {
c: {
fontWeight: "bold",
fontSize: self.fontSize,
},
},
},
},
data: data,
},
{
radius: self.radius1,
center: self.center,
type: "pie",
roseType: self.roseType ? "radius" : "",
itemStyle: {
normal: {
color: "rgba(255,255,255,0.3)",
},
},
labelLine: {
normal: { show: false },
},
label: {
normal: { show: false },
},
data: [1],
tooltip: { show: false },
},
],
};
if (self.percent) {
option.series.push({
radius: self.radius,
center: self.center,
type: "pie",
itemStyle: {
normal: {
color: "rgba(0,0,0,0)",
borderWidth: 2,
borderColor: "#fff",
},
},
labelLine: {
normal: {
show: false,
lineStyle: { color: "#E2E2E2" },
},
},
label: {
normal: {
show: true,
position: "inside",
formatter: function (params) {
let percent = params.percent ? params.percent.toFixed(2) : 0;
return parseFloat(percent) + "%";
},
textStyle: {
color: "#fff",
fontSize: self.fontSize,
},
},
},
data: data,
});
}
this.myChart.setOption(option);
},
},
};
</script>
<style scoped lang="scss">
</style>
5.5 echart_radar.vue ------ 雷达图
- 图表类型 :
radar。 - props :
height(默认450px)/width/data。 - data 结构:
js
{
nameList: [{ name: "研发", max: 100 }, { name: "生产", max: 100 }],
series: [
{ name: "本企业", data: [80, 60] },
{ name: "行业均值", data: [70, 70] }
]
}
- 关键实现 :
indicator由nameList生成(max默认 100);每个系列一套颜色 +areaStyle面积。 - 适用:多维能力对比(如企业创新能力画像)。
完整代码
vue
<template>
<div class="Echart_radar">
<div class="bg" :style="{ width: width, height: height }"></div>
<div class="tb" ref="chartRef" :style="{ width: width, height: height }"></div>
</div>
</template>
<script>
import * as echarts from "echarts";
export default {
name: "Echart_radar",
props: {
height: {
type: String,
default: "450px",
},
width: {
type: String,
default: "100%",
},
data: {
type: Object,
default() {
return {
nameList: [],
series: [],
};
},
},
},
data() {
return {
myChart: {},
color: [
{
color: "rgba(57, 126, 240, 1)",
areaColor: "rgba(170, 217, 255, 0.35)",
},
{
color: "rgb(240,167,57)",
areaColor: "rgba(255,180,170,0.35)",
},
],
};
},
watch: {
data() {
this.init();
},
},
mounted() {
let self = this;
self.init();
window.addEventListener("resize", function () {
self.myChart.resize();
});
},
methods: {
// 计算数组平均值
average(list) {
let sum = list.reduce(function (prev, curr) {
return prev + curr;
});
return (sum / list.length).toFixed(1);
},
init() {
this.myChart = echarts.init(this.$refs.chartRef);
let indicator = [];
this.data.nameList.forEach((item) => {
indicator.push({
name: item.name ? item.name : item,
max: item.max ? item.max : 100,
});
});
let series = [];
this.data.series.forEach((item, index) => {
series.push({
name: item.name,
type: "radar",
color: this.color[index].color,
label: { show: false },
areaStyle: { color: this.color[index].areaColor },
data: [{ value: item.data }],
});
});
let option = {
tooltip: {},
radar: {
name: {
formatter: "{value}",
textStyle: {
color: "#4D5A69",
fontSize: 14,
},
},
axisLine: {
lineStyle: { color: "#F0F0F0" },
},
splitLine: {
lineStyle: { color: "#F0F0F0" },
},
splitArea: {
areaStyle: {
color: ["#fff", "transparent"],
opacity: 1,
},
},
indicator: indicator,
center: ["50%", "50%"],
radius: "70%",
},
series: series,
};
this.myChart.setOption(option);
},
},
};
</script>
<style scoped lang="less">
.Echart_radar {
position: relative;
}
.bg {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
position: absolute;
}
</style>
5.6 echart_bar_line.vue ------ 柱 + 折线(双轴,带轮播)
- 图表类型 :
bar+line(折线用yAxisIndex: 1走右轴)。 - props :
height(默认200px)/dataA/dataZoom(默认 5)。 - data 结构:
js
{
id: "EC-1", // 用于 getElementById 初始化,需唯一
colors: ["#a8d3ff", "#70b0ff", "#bae4fb", "#2fd1d3"],
dataname: ["数量", "增长率"],
data: [220, 300, 500, 400, 300], // 柱
data1: [80, 70, 66, 55, 90], // 折线(右轴,%)
xAxis: [2017, 2018, 2019, 2020, 2021],
unit: "户"
}
- 关键实现 :固定两根系列(柱 + 线);
dataZoom隐藏滑动条,setInterval每 2 秒自动向后滚动一位 ;beforeDestroy清理定时器。 - 适用:总量 + 增长率/占比的组合展示。
完整代码
vue
<template>
<div class="Echart_bar" :style="{ height: height }">
<div class="tb" :id="dataA.id" :style="{ width: '100%', height: '100%' }"></div>
</div>
</template>
<script>
import * as echarts from "echarts";
export default {
name: "Echart_bar",
props: {
height: { type: String, default: "200px" },
dataA: {
id: "EC",
colors: ["#a8d3ff", "#70b0ff", "#bae4fb", "#2fd1d3"],
dataname: ["name1", "name2"],
data: [220, 300, 500, 400, 300], // 柱
data1: [80, 70, 66, 55, 90], // 折线(右轴)
xAxis: [2017, 2018, 2019, 2020, 2021],
unit: "户",
},
dataZoom: { type: Number, default: 5 },
},
data() {
return { myChart: {}, timer: null };
},
mounted() {
this.myChart = echarts.init(document.getElementById(this.dataA.id));
this.flashData();
},
beforeDestroy() {
if (this.timer) clearInterval(this.timer);
this.myChart.dispose();
},
watch: {
data() {
this.flashData();
},
dataA: {
deep: true,
handler() {
this.flashData();
},
},
},
methods: {
flashData() {
const unit = this.dataA.unit;
const option = {
textStyle: { color: "#c0c3cd", fontSize: 14 },
tooltip: {
trigger: "axis",
axisPointer: { lineStyle: { color: "#dadada" } },
formatter: function (params) {
let result = params[0].name + "<br>";
params.forEach(function (item) {
if (item.componentSubType === "line") {
result += item.marker + item.seriesName + ":" + item.value + "%<br>";
} else {
result += item.marker + item.seriesName + ":" + item.value + unit + "<br>";
}
});
return result;
},
},
toolbox: {
show: false,
feature: {
saveAsImage: { backgroundColor: "#031245" },
restore: {},
},
iconStyle: { borderColor: this.dataA.colors[1] },
},
legend: {
bottom: 0,
itemWidth: 20,
itemHeight: 6,
left: "center",
padding: 0,
textStyle: { color: "#858dc7", fontSize: 14 },
data: this.dataA.dataname,
},
grid: {
containLabel: true,
left: 0,
right: 0,
bottom: 20,
top: 10,
},
xAxis: {
type: "category",
data: this.dataA.xAxis,
boundaryGap: [10, 10],
axisTick: { show: false },
splitLine: { show: false },
axisLine: { show: true, lineStyle: { color: "#e7e7f5", width: 1 } },
axisLabel: { color: "#8699ad", fontSize: 14, interval: 0 },
},
yAxis: [
{
axisTick: { show: false },
axisLine: { show: true, lineStyle: { color: "#e7e7f5", width: 1 } },
axisLabel: { color: "#8699ad", fontSize: 14 },
splitLine: { show: true, lineStyle: { color: "#e7e7f5", type: "dashed" } },
},
{
position: "right",
axisTick: { show: false },
axisLine: { show: true, lineStyle: { color: "#e7e7f5", width: 1 } },
axisLabel: { color: "#8699ad", fontSize: 14, formatter: "{value}%" },
splitLine: { show: false },
},
],
dataZoom: [
{
xAxisIndex: 0,
show: false,
type: "slider",
startValue: 0,
endValue: this.dataZoom,
},
],
series: [
{
name: this.dataA.dataname[0],
data: this.dataA.data,
type: "bar",
barMaxWidth: "auto",
barWidth: "40%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 1, color: this.dataA.colors[0] },
{ offset: 0, color: this.dataA.colors[1] },
]),
shadowBlur: 1,
shadowColor: this.dataA.colors[2],
shadowOffsetX: 2,
shadowOffsetY: -2,
},
label: { show: false },
},
{
name: this.dataA.dataname[1],
yAxisIndex: 1,
type: "line",
smooth: false,
data: this.dataA.data1,
symbol: "circle",
symbolSize: 8,
itemStyle: {
normal: {
color: this.dataA.colors[3],
borderColor: this.dataA.colors[3],
lineStyle: { color: this.dataA.colors[3] },
},
},
},
],
animation: true,
animationDuration: 500,
};
this.myChart.setOption(option);
// 数据多于一屏时,dataZoom 自动轮播
const total = this.dataA.xAxis.length;
const scrollSize = this.dataZoom;
if (total > scrollSize) {
if (this.timer) clearInterval(this.timer);
this.timer = setInterval(() => {
if (option.dataZoom[0].endValue === total - 1) {
option.dataZoom[0].endValue = scrollSize;
option.dataZoom[0].startValue = 0;
} else {
option.dataZoom[0].endValue += 1;
option.dataZoom[0].startValue += 1;
}
this.myChart.setOption(option);
}, 2000);
}
this.myChart.resize();
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
.Echart_bar {
text-align: left;
}
</style>
5.7 echart_barLine_compare.vue(对比分析)与 echart_barLine_group.vue(分组分析)
- 图表类型 :
bar+line双轴组合,系列类型由数据决定。 - props :
className/width/height/dataZoom/dataZoomNumber/legend/grid/data。 - data 结构:
js
{
xAxis: ["区域一", "区域二"],
yAxis: { name: "单位:次", name2: "增长率:%" },
barWidth: "18%",
barGap: "60%",
series: [
{ name: "运行中", type: "bar", data: [250, 150], color: ["#0cbaec", "#037ede"] },
{ name: "增速", type: "line", data: [12, 8], color: ["#8b74ff"] }
]
}
- 两者差异:
| 组件 | 折线样式 | 自动轮播 / 高亮 |
|---|---|---|
echart_barLine_compare.vue |
平滑折线 + 面积渐变填充 | 有:每 3 秒滚动 dataZoom 并 dispatchAction(showTip) 高亮 |
echart_barLine_group.vue |
折线(圆点 symbolSize: 8)+ 无面积(areaStyle 被注释) |
轮播代码被注释,当前为静态 |
- 适用:多区域对比、分组趋势分析。
完整代码:echart_barLine_compare.vue(对比分析,含自动轮播 + 高亮)
vue
<template>
<div :class="className" :style="{ height: height, width: width }"/>
</template>
<script>
import * as echarts from "echarts";
export default {
props: {
className: { type: String, default: "chart" },
width: { type: String, default: "100%" },
height: { type: String, default: "250px" },
dataZoom: { type: Boolean, default: false },
dataZoomNumber: { type: Number, default: 99 },
legend: {
type: Object,
default() {
return { show: false, left: "center", bottom: "0" };
},
},
grid: {
type: Object,
default() {
return { top: "13%", bottom: "0", left: "5%", right: "6%" };
},
},
data: {
type: Object,
default() {
return {
xAxis: ["区域一", "区域二", "区域三", "区域四"],
yAxis: { name: "单位:次", name2: "增长率:%" },
barWidth: "18%",
barGap: "60%",
series: [
{ name: "运行中", data: [250, 150, 170, 150], color: ["#0cbaec", "#037ede", "#8b74ff"] },
{ name: "维护中", data: [300, 210, 220, 210], color: ["#f0d561", "#f7bc33", "#ffa301"] },
{ name: "异常", data: [250, 240, 190, 260], color: ["#feb7d4", "#fa2961", "#fa2961"] },
],
};
},
},
},
data() {
return { chart: null, timer: null };
},
mounted() {
this.$nextTick(() => {
setTimeout(() => {
this.initChart();
}, 1000);
});
},
beforeDestroy() {
if (this.timer) clearInterval(this.timer);
if (this.chart) this.chart.dispose();
},
watch: {
data: {
deep: true,
handler() {
this.chart.dispose();
setTimeout(() => {
this.initChart();
}, 1000);
},
},
},
methods: {
initChart() {
this.chart = echarts.init(this.$el);
let legendArray = [];
this.data.series.forEach((item) => legendArray.push(item.name));
let series = [];
this.data.series.forEach((item) => {
if (item.type === "bar") {
series.push({
name: item.name,
type: "bar",
barWidth: this.data.barWidth,
barGap: this.data.barGap,
data: item.data,
itemStyle: {
normal: {
shadowBlur: 0,
shadowColor: item.color[0],
shadowOffsetX: 0,
shadowOffsetY: 0,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: item.color[0] },
{ offset: 1, color: item.color[1] },
], false),
},
},
});
} else if (item.type === "line") {
series.push({
name: item.name,
type: "line",
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: item.color[0] },
{ offset: 1, color: "rgba(98, 227, 255, 0)" },
]),
},
data: item.data,
symbol: "circle",
yAxisIndex: 1,
symbolSize: 0,
itemStyle: {
normal: {
color: item.color[0],
borderColor: item.color[0],
lineStyle: { color: item.color[0] },
},
},
});
}
});
let option = {
legend: {
show: this.legend.show,
icon: "rect",
itemGap: 13,
data: legendArray,
left: this.legend.left,
bottom: this.legend.bottom,
itemWidth: 6,
itemHeight: 20,
},
tooltip: {
trigger: "axis",
axisPointer: { type: "shadow" },
textStyle: { color: "#222" },
backgroundColor: "rgba(255,255,255,0.9)",
borderColor: "#e2e7ec",
borderWidth: 1,
extraCssText: "z-index: 2",
},
grid: {
top: this.grid.top,
bottom: this.grid.bottom,
left: this.grid.left,
right: this.grid.right,
containLabel: true,
},
xAxis: [
{
type: "category",
data: this.data.xAxis,
axisTick: { show: false },
axisLabel: {
interval: 0,
textStyle: { color: "#000", fontSize: 12 },
},
axisLine: { lineStyle: { color: "#EEF0F4" } },
},
],
yAxis: [
{
name: this.data.yAxis.name,
type: "value",
nameTextStyle: { color: "#000", fontSize: 12 },
axisLabel: { show: true, textStyle: { color: "#000", fontSize: 12 } },
axisLine: { lineStyle: { color: "#EEF0F4" } },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#EEF0F4", type: "solid" } },
},
{
name: this.data.yAxis.name2,
type: "value",
nameTextStyle: { color: "#000", fontSize: 12 },
axisLabel: { show: true, textStyle: { color: "#000", fontSize: 12 } },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false },
},
],
dataZoom: [
{
show: this.dataZoom,
height: 12,
xAxisIndex: [0],
bottom: "8%",
startValue: 0,
endValue: this.dataZoomNumber,
handleSize: "110%",
handleStyle: { color: "#d3dee5" },
textStyle: { color: "#444" },
borderColor: "#90979c",
},
{
type: "inside",
show: true,
height: 15,
start: 1,
end: 35,
},
],
series: series,
};
this.chart.setOption(option);
// 每 3 秒自动滚动 dataZoom,并高亮提示
const self = this;
let playIndex = 0;
clearInterval(this.timer);
this.timer = setInterval(function () {
if (playIndex === 0) {
option.dataZoom[0].startValue = 0;
option.dataZoom[0].endValue = self.dataZoomNumber;
self.chart.setOption(option);
}
if (playIndex >= self.dataZoomNumber + 1) {
option.dataZoom[0].startValue += 1;
option.dataZoom[0].endValue += 1;
self.chart.setOption(option);
}
self.chart.dispatchAction({
type: "showTip",
seriesIndex: 0,
dataIndex: playIndex,
});
playIndex++;
if (playIndex >= self.data.xAxis.length) {
playIndex = 0;
}
}, 3000);
window.addEventListener("resize", () => {
this.chart.resize();
});
},
},
};
</script>
完整代码:echart_barLine_group.vue(分组分析,折线为圆点)
vue
<template>
<div :class="className" :style="{ height: height, width: width }"/>
</template>
<script>
import * as echarts from "echarts";
export default {
props: {
className: { type: String, default: "chart" },
width: { type: String, default: "100%" },
height: { type: String, default: "250px" },
dataZoom: { type: Boolean, default: false },
dataZoomNumber: { type: Number, default: 99 },
legend: {
type: Object,
default() {
return { show: false, left: "center", bottom: "0" };
},
},
grid: {
type: Object,
default() {
return { top: "13%", bottom: "0", left: "5%", right: "6%" };
},
},
data: {
type: Object,
default() {
return {
xAxis: ["区域一", "区域二", "区域三", "区域四"],
yAxis: { name: "单位:次", name2: "增长率:%" },
barWidth: "18%",
barGap: "60%",
series: [
{ name: "运行中", data: [250, 150, 170, 150], color: ["#0cbaec", "#037ede", "#8b74ff"] },
{ name: "维护中", data: [300, 210, 220, 210], color: ["#f0d561", "#f7bc33", "#ffa301"] },
{ name: "异常", data: [250, 240, 190, 260], color: ["#feb7d4", "#fa2961", "#fa2961"] },
],
};
},
},
},
data() {
return { chart: null };
},
mounted() {
this.$nextTick(() => {
setTimeout(() => {
this.initChart();
}, 1000);
});
},
beforeDestroy() {
if (this.chart) this.chart.dispose();
},
watch: {
data: {
deep: true,
handler() {
this.chart.dispose();
setTimeout(() => {
this.initChart();
}, 1000);
},
},
},
methods: {
initChart() {
this.chart = echarts.init(this.$el);
let legendArray = [];
this.data.series.forEach((item) => legendArray.push(item.name));
let series = [];
this.data.series.forEach((item) => {
if (item.type === "bar") {
series.push({
name: item.name,
type: "bar",
barWidth: this.data.barWidth,
barGap: this.data.barGap,
data: item.data,
itemStyle: {
normal: {
shadowBlur: 0,
shadowColor: item.color[0],
shadowOffsetX: 0,
shadowOffsetY: 0,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: item.color[0] },
{ offset: 1, color: item.color[1] },
], false),
},
},
});
} else if (item.type === "line") {
series.push({
name: item.name,
type: "line",
smooth: false,
data: item.data,
symbol: "circle",
yAxisIndex: 1,
symbolSize: 8,
itemStyle: {
normal: {
borderWidth: 3,
color: "#fff",
borderColor: item.color[0],
lineStyle: { color: item.color[0] },
},
},
});
}
});
let option = {
legend: {
show: this.legend.show,
icon: "rect",
itemGap: 13,
data: legendArray,
left: this.legend.left,
bottom: this.legend.bottom,
itemWidth: 6,
itemHeight: 20,
},
tooltip: {
trigger: "axis",
axisPointer: { type: "shadow" },
textStyle: { color: "#222" },
backgroundColor: "rgba(255,255,255,0.9)",
borderColor: "#e2e7ec",
borderWidth: 1,
extraCssText: "z-index: 2",
},
grid: {
top: this.grid.top,
bottom: this.grid.bottom,
left: this.grid.left,
right: this.grid.right,
containLabel: true,
},
xAxis: [
{
type: "category",
data: this.data.xAxis,
axisTick: { show: false },
axisLabel: {
interval: 0,
textStyle: { color: "#000", fontSize: 12 },
},
axisLine: { lineStyle: { color: "#EEF0F4" } },
},
],
yAxis: [
{
name: this.data.yAxis.name,
type: "value",
nameTextStyle: { color: "#000", fontSize: 12 },
axisLabel: { show: true, textStyle: { color: "#000", fontSize: 12 } },
axisLine: { lineStyle: { color: "#EEF0F4" } },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#EEF0F4", type: "solid" } },
},
{
name: this.data.yAxis.name2,
type: "value",
nameTextStyle: { color: "#000", fontSize: 12 },
axisLabel: { show: true, textStyle: { color: "#000", fontSize: 12 } },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false },
},
],
dataZoom: [
{
show: this.dataZoom,
height: 12,
xAxisIndex: [0],
bottom: "8%",
startValue: 0,
endValue: this.dataZoomNumber,
handleSize: "110%",
handleStyle: { color: "#d3dee5" },
textStyle: { color: "#444" },
borderColor: "#90979c",
},
{
type: "inside",
show: true,
height: 15,
start: 1,
end: 35,
},
],
series: series,
};
this.chart.setOption(option);
// 说明:与 compare 版不同,本组件未启用 dataZoom 自动轮播
window.addEventListener("resize", () => {
this.chart.resize();
});
},
},
};
</script>
5.8 echart_barMore.vue ------ 多系列柱(可堆叠)+ 多折线
- 图表类型 :
bar(可stack堆叠)+line(右轴)灵活组合。 - props :
height/leP/dataZoom/barWidth/barGap/dataA/colorGroub。 - data 结构:
js
{
id: "EC-more", // 需唯一
inverse: false,
dataName: [["系列1", "系列2"]],
dataZoom: 10,
xAxis: [2017, 2018, 2019, 2020, 2021],
unit: "户",
dataBar: [ // 柱:可多个
{ colors: ["#a8d3ff", "#70b0ff", "#bae4fb"], dataname: "数量",
data: [220, 300, 500], dataPer: [22, 30, 50], stack: null, labelShow: true }
],
dataLine: [ // 折线:可多个
{ colors: "#a8d3ff", dataname: "增速", data: [12, 8, 6], dataPer: [] }
]
}
- 关键实现 :
dataBar→bar(渐变、支持stack堆叠、labelShow显示dataPer占比);dataLine→line(yAxisIndex: 1);- 无数据时 用
showNone覆盖「暂无数据」占位图; mouseover / mouseout通过$emit('show-ser', { series_i })向外部广播鼠标所在项,便于与页面其他区域联动。
- 适用:复杂的多维度指标组合面板。
5.9 echart_mapC.vue ------ 地图(中国地图)
- 图表类型 :
map(行政区着色,geo+series.type='map')。 - props :
height(默认 815)/width/id/fontSize/title/color/tips/data。 - data 结构:
js
[
{ name: "广东省", value: 20 },
{ name: "北京市", value: 40 }
]
- 关键实现 :
echarts.registerMap('china', chinaJson)注册地图(需自行准备中国地图的 GeoJSON);visualMap连续型(show: false)做数值→颜色映射;series.map区域渐变填充,emphasis高亮(黄色)。
- 适用:按省份/区域展示分布(choropleth)。
完整代码
vue
<template>
<div class="Echart_map">
<div class="tb" ref="chartRef" :style="{ width: width, height: height + 'px' }"></div>
</div>
</template>
<script>
import * as echarts from "echarts";
import china from "@/assets/js/china.json";
export default {
name: "Echart_map",
props: {
height: { type: Number, default: 815 },
width: { type: String, default: "100%" },
id: { type: String, default: "ec" },
fontSize: { type: Number, default: 14 },
title: { type: String, default: "title" },
color: {
type: Array,
default() {
return ["#1EB96B", "#FE8441", "#FD5439"];
},
},
tips: { type: Boolean, default: true },
data: {
type: Array,
default() {
return [
{ name: "广东省", value: 20 },
{ name: "北京市", value: 40 },
{ name: "重庆市", value: 100 },
{ name: "上海市", value: 210 },
];
},
},
},
data() {
return { myChart: {} };
},
watch: {
data: {
handler() {
let self = this;
this.$nextTick(() => {
self.init();
self.myChart.resize();
});
},
immediate: true,
deep: true,
},
},
mounted() {
this.init();
let self = this;
window.addEventListener("resize", function () {
self.myChart.resize();
});
},
methods: {
init() {
this.myChart = echarts.init(this.$refs.chartRef);
let mapJson = china;
let legendData = [];
let series = [
{
type: "map",
zlevel: 20,
map: "china",
roam: false,
zoom: 1,
showLegendSymbol: false,
label: {
show: true,
color: "#FFF",
},
itemStyle: {
normal: {
areaColor: {
type: "linear",
x: 0, y: 0, x2: 0, y2: 1600,
colorStops: [
{ offset: 0, color: "#A0D6FE" },
{ offset: 1, color: "#A0D6FE" },
],
global: true,
},
borderColor: "#fff",
borderWidth: 2,
},
emphasis: {
areaColor: "#F6E232",
label: { show: true },
},
},
data: this.data,
},
];
echarts.registerMap("china", mapJson);
let option = {
tooltip: { show: false },
visualMap: {
show: false,
type: "continuous",
showLabel: true,
left: "20",
min: 0,
max: 10,
inRange: {
color: ["#91D0FF", "#0375FF"],
},
},
legend: {
show: true,
orient: "vertical",
icon: "roundRect",
itemWidth: 16,
itemHeight: 16,
itemGap: 20,
bottom: "0%",
left: "0%",
textStyle: {
fontSize: 14,
color: "#2F3757",
fontFamily: "OPPOSans-M",
},
data: legendData,
},
geo: {
show: true,
map: "china",
selectedMode: "single",
zoom: mapJson.scale || 1,
roam: false,
label: {
normal: { show: false, color: "#FFF" },
emphasis: { show: false },
},
itemStyle: {
normal: {
areaColor: {
type: "linear",
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: "#9ED5FF" },
{ offset: 1, color: "#3a95fd" },
],
global: true,
},
borderColor: "#fff",
borderWidth: 0.2,
},
},
},
series: series,
};
this.myChart.setOption(option);
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
.bg {
position: absolute;
background: #F9FAFE;
border-radius: 4px;
transform: translateX(-100%);
}
</style>
六、使用示例:innovation.vue(创新能力)
innovation.vue(创新能力)是一个完整的「页面容器 + 图表组件」示例,展示两个不同类型图表(柱状图 + 饼图)的接入方式。
6.1 引入并注册组件(局部注册)
js
import echart_barA from "@/components/echart_barA.vue";
import echart_pieA from "@/components/echart_pieA.vue";
export default {
name: "innovation",
props: ["id", "data"],
components: {
echart_barA, echart_pieA
},
// ...
};
6.2 模板中使用(传 data / 单位 / 颜色 / 尺寸)
html
<a-row :gutter="24">
<a-col :span="12">
<h2 class="titB"><b>知识产权授权分布</b></h2>
<a-empty v-if="!echartBarData.timer"/>
<echart_barA v-else :key="echartBarData.timer" :data="echartBarData" :colorList="colorList" unit="件"/>
</a-col>
<a-col :span="12">
<h2 class="titB"><b>知识产权分布</b></h2>
<a-empty v-if="echartPieData.length == 0"/>
<div v-else>
<echart_pieA :data="echartPieData" :labelLine="true" :roseType="false" unit="件"
:radius='["35%", "55%"]' :radius1='["35%", "45%"]' :center='["50%", "55%"]'
:height="'350px'" :label="true" :color="colorList.map(item => item[0])" />
</div>
</a-col>
</a-row>
要点:
- 柱状图 用
:key="echartBarData.timer":数据更新时timer变化 → 组件整体重建,避免旧实例残留; - 饼图 传入
colorList.map(item => item[0]),把共享色板转成饼图需要的颜色数组; - 均用
<a-empty>做无数据兜底。
6.3 获取数据并组装(后端返回 → 组件结构)
图表数据来自后端接口 。为便于文档自包含,下面用一份手动模拟的数据 代替真实接口返回;实际项目中,把 mockData 换成你的请求结果即可(例如 getApi(params).then(res => this.buildCharts(res.data)))。
js
// 模拟后端返回的数据(真实项目里由接口返回)
const mockData = [
{ year: 2022, type: "trademark", value: 10 },
{ year: 2022, type: "inventionPatent", value: 5 },
{ year: 2023, type: "trademark", value: 15 },
{ year: 2023, type: "inventionPatent", value: 8 },
{ year: 2024, type: "trademark", value: 20 },
{ year: 2024, type: "inventionPatent", value: 12 },
];
export default {
name: "innovation",
props: ["id", "data"],
data() {
return {
currentYear: 2024, // 当前年份
yearCount: 2, // 展示近 3 年
echartBarData: { timer: null, xAxis: [], series: [] },
echartPieData: [],
};
},
mounted() {
this.loadData();
},
methods: {
// 真实项目中:调用接口拿到数据后交给 buildCharts 组装
loadData() {
// 例如:getApi({ id: this.id, yearCount: this.yearCount }).then(res => this.buildCharts(res.data));
this.buildCharts(mockData);
},
// 把后端原始数组整理成图表组件需要的结构
buildCharts(list) {
const { currentYear, yearCount } = this;
const xAxis = [];
const echartPieData = [];
const series = [
{ key: "trademark", name: "商标", data: [], unit: "件" },
{ key: "inventionPatent", name: "发明专利", data: [], unit: "件" },
// ...其余类型
];
for (let y = currentYear - yearCount; y <= currentYear; y++) {
xAxis.push(y + "年");
const yearRows = list.filter(p => p.year === y);
series.forEach(item => {
const hit = yearRows.find(p => p.type === item.key);
const value = hit ? hit.value : 0;
if (y === currentYear) {
// 当前年份的数据同时用于饼图
echartPieData.push({ name: item.name, label: item.name, value });
}
item.data.push(value);
});
}
// 组装成组件约定结构,并更新时间戳触发重绘
Object.assign(this.echartBarData, {
timer: new Date().getTime(), xAxis, series,
});
this.echartPieData = echartPieData;
},
},
};
数据流回顾 :后端接口 → 拿到原始数组 → 整理为 { xAxis, series }(柱状图)与 [{ name, label, value }](饼图)→ 通过 props 交给图表组件渲染。
七、数据契约汇总
| 组件 | data 类型 |
结构要点 |
|---|---|---|
echart_barA |
Object | { xAxis: string[], series: [{ name, data: number[] }] } |
echart_barB |
Array | [{ name, value, sum }] |
echart_line |
Object | { xAxis: string[], series: [{ name, data: number[], unit?, nosmooth? }] } |
echart_pieA |
Array | [{ name, label, value, id?, pid? }] |
echart_radar |
Object | { nameList: [{ name, max }], series: [{ name, data: number[] }] } |
echart_bar_line |
Object(dataA) |
{ id, colors, dataname, data, data1, xAxis, unit } |
echart_barLine_compare / fzfx |
Object | `{ xAxis, yAxis:{name,name2}, barWidth, barGap, series:[{ name, type:'bar' |
echart_barMore |
Object(dataA) |
{ id, xAxis, unit, dataBar:[...], dataLine:[...] } |
echart_mapC |
Array | [{ name, value }] |
八、新增一个图表组件的步骤
- 在图表组件目录下新建
echart_xxx.vue; - 模板放一个容器
div,:style="{ width, height }"; import * as echarts from "echarts",在mounted里echarts.init(...);- 定义
props(至少包含data、width、height,按需加unit、fontSize、颜色等); - 写
init():把 props 转换成 EChartsoption,chart.setOption(option); watch data(deep)→ 重新渲染(必要时先dispose()再init());window.onresize(或addEventListener)调用chart.resize();- 在页面组件中
import并局部注册,接好接口数据后传data。
九、注意事项与常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 图表不显示 / 尺寸为 0 | 容器没有有效高度(父级高度为 0 或未给 height) |
给组件传 height,并确保父容器有高度 |
| 数据更新了图表没变 | 只改了对象内部而没触发重绘 | 用 watch data(deep) 重绘,或改 :key="timer" 强制重建 |
| 同页面多个图表只有一个 resize 生效 | 老组件用 window.onresize = ... 覆盖绑定 |
改用 window.addEventListener("resize", ...) 或统一管理 |
| 用 id 初始化的图表串图 | 多个实例传了相同 id |
id 必须唯一(可加时间戳/序号) |
| 切换 tab 后图表空白 | 切换时元素隐藏,resize 时尺寸为 0 | 切换后 $nextTick 再 chart.resize() |
| 内存/定时器泄漏 | 组件销毁时未清理 setInterval / 实例 |
beforeDestroy 里 clearInterval、必要时 dispose() |
十、小结
- 图表能力以「纯展示组件 + 页面容器供数 」的方式组织:所有图表组件放在同一个目录下 ,包含基础图表 (柱状、横向条、折线、饼/玫瑰、雷达)与组合图表(柱+折线、多系列柱+折线、地图)。
- 组件通过统一的 props 约定(
data / width / height / unit / fontSize / colorList)和watch + resize机制,实现「换数据即换图」。 - 使用时遵循:页面调接口 → 组装成组件约定的
data结构 → 传给对应图表组件;无数据用a-empty兜底,数据频繁变化可用:key触发重建。