一、项目介绍
最近做项目,甲方要求在柱状图里加上水波图的效果,真是太有趣了,太有创意啦。马上动手实现。
思路
echart提供了一个renderItem
方法,可以帮助我们自定义每个series的配置。我们分析柱状图跟水波图,就是由一个矩形跟一个波浪路径组成,因此我们可以返回一个图形数组来表现当前项。
二、了解renderItem
renderItem
函数是自定义系列的核心,它负责将数据项(dataItem)转换为可视化的图形元素。ECharts 会为 series.data 中的每个数据项调用一次 renderItem
函数。主要优势在于:
- 可以自由绘制各种图形元素
- ECharts 会自动管理图形的创建、删除、动画等细节
- 可以与其他组件(如 dataZoom、visualMap)无缝联动
参数
renderItem
函数接收两个参数:params
和 api
//params 包含当前数据信息和坐标系信息:
{
context: {}, // 可供开发者暂存东西的对象
seriesId: string, // 本系列 ID
seriesName: string, // 本系列名称
seriesIndex: number, // 本系列索引
dataIndex: number, // 数据项在原始数据中的索引
dataIndexInside: number, // 数据项在当前可见数据窗口中的索引
dataInsideLength: number, // 当前可见数据长度
coordSys: { // 坐标系信息,类型不同结构也不同
type: 'cartesian2d' | 'polar' | 'geo' | 'calendar' | 'singleAxis',
// 不同坐标系下的具体属性...
}
}
api 参数提供了一系列方法:
api.value(index)
- 获取数据项中指定维度的值api.coord(valueArray)
- 将数据值转换为坐标系上的点api.size(valueArray)
- 获取坐标系上一段数值范围对应的像素长度api.style(styleOverrides)
- 获取或覆盖默认样式
返回值
renderItem 函数需要返回一个图形元素定义对象
{
type: string, // 图形类型,如'rect','circle','sector','polygon'等
shape: object, // 图形形状定义
style: object, // 图形样式
extra: object, // 额外信息,可在事件处理器中访问
children: array, // 子图形(当type为'group'时)
// 其他可选属性...
}
三、实现
首先要实现基本配置,柱子的高度为数据的总数,水波的位置为已处理数据,提示窗展示名称、已处理、未处理、总数等数据。
// 数据配置 - 包含已处理和未处理的数据
const chartData = [
{ name: "车辆只进不出", processed: 60, unprocessed: 62, total: 122 },
{ name: "预警模型2", processed: 72, unprocessed: 82, total: 154 },
{ name: "预警模型3", processed: 71, unprocessed: 91, total: 162 }
];
// 创建水波柱状图配置
const createOption = () => {
return {
backgroundColor: 'transparent', //echart背景为透明
animation: false,
tooltip: { //提示窗配置
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function(params) {
const data = chartData[params[0].dataIndex];
return `${params[0].name}<br/>已处理: ${data.processed}<br/>未处理: ${data.unprocessed}<br/>总计:${data.total}`;
}
},
grid: { //图表位置占比配置,尽量居中
left: '3%',
right: '4%',
bottom: '12%',
top:'5%',
containLabel: true
},
xAxis: { //配置x轴
type: 'category',
data: chartData.map(item => item.name),
axisLabel: {
color: '#fff',
fontSize: 12
},
axisLine: {
lineStyle: {
color: '#fff'
}
}
},
yAxis: { //配置y轴
type: 'value',
axisLabel: {
color: '#fff',
fontSize: 12,
},
axisLine: {
lineStyle: {
color: '#fff'
}
},
splitLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.1)'
}
}
},
series:series //配置数据项
};
};
自定义数据项,通过rederItem
方法返回一个矩形跟一个波浪路径
// 水波动画时间
let animationTime = 0;
let series = [
{
name: '水波柱状图',
type: 'custom',
renderItem: (params, api) => {
const categoryIndex = api.value(0); //当前项索引
const totalValue = api.value(1); //当前项的值
const processedValue = chartData[categoryIndex].processed; //已完成的值
const start = api.coord([api.value(0), 0]); //开始的坐标位置,返回[x,y]坐标
const end = api.coord([api.value(0), totalValue]); //结束的坐位位置
const height = end[1] - start[1]; //高度
const width = 40; //宽度
const rectShape = { //定义矩形的形状
x: start[0] - width / 2,
y: start[1],
width: width,
height: height
};
// 计算水波位置 - 基于已处理数量占总数的比例
const waterLevel = processedValue / totalValue;
const wavePath = createWavePath(rectShape, waterLevel, animationTime);
return { //返回值
type: 'group',
children: [
{
type: 'rect', //矩形
shape: rectShape,
style: {
fill: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(24, 144, 255, 0.8)' },
{ offset: 0.5, color: 'rgba(64, 169, 255, 0.6)' },
{ offset: 1, color: 'rgba(9, 109, 217, 0.4)' }
]
},
stroke: 'rgba(24, 144, 255, 0.3)',
lineWidth: 1
}
},
{
type: 'path', //水波路径
shape: {
pathData: wavePath
},
style: {
fill: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(24, 144, 255, 0.8)' },
{ offset: 0.5, color: 'rgba(64, 169, 255, 0.6)' },
{ offset: 0.5, color: 'rgba(64, 169, 255, 0.6)' },
{ offset: 1, color: 'rgba(9, 109, 217, 0.4)' }
]
}
},
z: 10
}
]
};
},
data: chartData.map(item => item.total),
z: 10
}
]
实现水波的方法,生成一个svg的路径。SVG 路径字符串是描述矢量图形的重要方式,下面我将详细介绍如何生成 SVG 路径字符串
基本 SVG 路径命令
命令 | 含义 | 示例 |
---|---|---|
M | 移动到 (MoveTo) | M 10,20 |
L | 直线到 (LineTo) | L 30,40 |
C | 三次贝塞尔曲线 (Cubic Bezier) | C x1,y1 x2,y2 x,y |
Q | 二次贝塞尔曲线 (Quadratic Bezier) | Q x1,y1 x,y |
Z | 闭合路径 (ClosePath) | Z |
// 创建水波路径
const createWavePath = (rect, waterLevel, time) => {
const { x, y, width, height } = rect;
const waterHeight = height * waterLevel; //水波的高度=柱子高度*百分比
const waterY = y + height - waterHeight; //水波的y轴位置
const waveLength = width;
const waveHeight = 3;
const frequency = 1;
// 从底部开始绘制路径
let path = `M ${x} ${y + height}`;
// 绘制左侧边线到水波位置
path += ` L ${x} ${waterY}`;
// 绘制水波顶部
for (let i = 0; i <= width; i += 2) {
const waveX = x + i;
// 使用正弦函数计算Y坐标
const waveY = waterY + Math.sin((i / waveLength) * Math.PI * frequency + time) * waveHeight;
path += ` L ${waveX} ${waveY}`;
}
// 绘制右侧边线回到底部
path += ` L ${x + width} ${y + height}`;
// 闭合路径
path += ` Z`;
return path;
};
创建echart图标,并实现水波的动画效果
// 创建图表
const createChart = () => {
const container = chartRef.value;
chartInstance = echarts.init(container);
chartInstance.setOption(createOption());
// 启动水波动画
const animate = () => {
animationTime += 0.1;
if (chartInstance) {
chartInstance.setOption({series:series});
}
requestAnimationFrame(animate);
};
animate();
};
onMounted(() => {
createChart();
});
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
最终效果: