这不最近我司设计又又给我整活了,甩给我几张炫酷的图表效果图,看得我眼花缭乱;笔者瞅了一眼ECharts常规配置项,果然直接配置是实现不了的。不过没关系,这次正好深入ECharts的renderItem函数,它可以说是ECharts中实现高度自定义图表的"终极武器"。
renderItem
想要实现上述效果,我们首先对echart的renderItem函数有一定的了解;这个函数是Echarts用来实现一些自定义形状(custom series)的核心渲染函数,它可以允许开发者完全控制图形的绘制逻辑,而不仅局限于官方提供的图形;
首先它的使用方式也很简单,将type定义成custom,然后传入renderItem函数就可以了:
typescript
var options = {
series: [
{
type: "custom",
name: "名称",
data: dataArray,
renderItem: function (params, api) {
// 返回你想要的图形
},
},
],
}
对于每一项data数据,都会调用renderItem进行渲染;在最近新发布Echarts6+的特性中,还可以对renderItem函数进行封装注册,在series使用时支持字符串的写法:
typescript
const renderItem = (params, api) => {
return {
// 定义图形
}
};
echarts.registerCustomSeries('bubble', renderItem);
var options = {
series: [
{
type: "custom",
name: "名称",
data: dataArray,
renderItem: 'bubble',
},
],
}
我们看到,不管函数怎么封装,它接受两个参数,这两个参数对于我们后续绘制图形起着至关重要的作用,我们下面就好好说道说道这个函数。
params
params参数包含了当前数据信息和坐标系的信息,它提供了下面几个属性:
typescript
{
// 触发此次重绘的 action 的 type。
actionType: null,
// 一个可供开发者暂存东西的对象。生命周期只为:当前次的渲染
context: {},
// 坐标系信息,不同的坐标系中,coordSys里的信息不一样
coordSys: {
type: "cartesian2d",
x: 200,
y: 20,
width: 743,
height: 297
},
// data中数据项的index
dataIndex: 2,
// 数据项在当前坐标系中可见的数据的 index(即 dataZoom 当前窗口中的数据的 index)
dataIndexInside: 2,
// 维度数据
encode: { x: ... y: ... },
// 本系列 id
seriesId: "",
// 本系列 index
seriesIndex: 0,
// 本系列 name
seriesName: "",
}
params参数一般配合api中的函数使用。
api
第二个参数比较常用,就是api
参数,它是一个CustomSeriesRenderItemAPI类型的对象,上面挂载有很多的函数供开发者调用,主要用来将数据和坐标进行转换使用。
value函数
这么多函数,我们挑选几个重要的函数讲解一下;首先看下value
函数,它的作用是取出当前维度dimension的数据值,我们看下实际的案例:
typescript
var options = {
series: [
{
data: [10, 20, 30],
type: "custom",
renderItem: function (params, api) {
// 0,1,2
console.log(api.value(0))
// 10,20,30
console.log(api.value(1))
}
}
]
}
我们上面模拟了一组简单的数据,api.value(0)
获取的是第一个维度的数据,这里数据是一维数组,第一个维度一般是X轴,因此循环输入在X轴上的顺序0,1,2;而api.value(1)
获取的是第二个维度的数据,一般是Y轴,因此循环输出对应data的值。
如果我们把数据的维度扩大一个维度,比如散点图会用到二维数组,那么api.value(dimension)
就输出对应维度的数据。
typescript
var options = {
series: [
{
data: [
[10, 100],
[20, 200],
[30, 300],
],
type: "custom",
renderItem: function (params, api) {
// 10,20,30
console.log(api.value(0))
// 100,200,300
console.log(api.value(1))
}
}
]
}
那么聪明的小伙伴可能就会想到了,如果我的数据不是纯数值,而是类似下面的对象数组,那么请问api.value该如何返回?
typescript
var options = {
series: [
{
data: [
{ value: 1048, name: 'Search Engine' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' }
],
}
]
}
其实这样的对象数据结构本质上也是一维数组,因此和一维数组的返回是相同的。
coord函数
了解了api.value函数的用法之后,下面就是coord
函数,根据名字我们能猜测到,它的作用是定位,不过千万可别小瞧了这个函数,它可是我们下面需要用到的一个最重要的坐标转换函数;它可以将我们value中获取到的数据,转换成画布上的坐标;coord
函数的定义如下:
typescript
type Coord = [number, number]; // [x, y] 或 [radius, angle]
api.coord(
dataValue: Coord | number[], // 输入数据值
clamp?: boolean // 是否限制在坐标系内(默认false)
): Coord; // 返回画布像素坐标 [pxX, pxY]
coord函数比较难理解,我们还是通过上面的的例子来具体看下;比如我们根据[10, 20, 30]
这样的一个数据,使用自定义图形展示一个柱状图;首先我们需要使用value函数获取x、y轴的值:
typescript
{
renderItem: function (params, api) {
const xValue = api.value(0); // 类目索引
const yValue = api.value(1); // 数值
// ...
}
}
这里对api.value(0)和api.value(1)用法有所疑问的小伙伴可以回到上一节再看一下。
接着使用coord返回数据点所在的x、y坐标以及基准线坐标的坐标:
typescript
{
renderItem: function (params, api) {
const [x, y] = api.coord([xValue, yValue]); // 数据点坐标
const [x0, y0] = api.coord([xValue, 0]); // 基准线坐标
// ...
}
}
这里代码比较抽象,我们知道,在echarts坐标系中,原点的位置在左下角;而在canvas画布坐标系下,原点的位置是位于左上角;但是,而我们在renderItem函数中返回的图形是基于canvas坐标系,coord的定位作用正是将echarts的数据转换为在canvas上的位置,可以说是连接起了两个图形系统的重要桥梁,我们通过一张示意图来更好的理解:

上面获取的[x,y]和[x0,y0]正是我们的数据10,20,30在每次循环调用时转换为canvas下的坐标位置。
理解了coord的作用,我们再来画柱状图就非常简单了:
typescript
const boxWidth = 20
{
renderItem: function (params, api) {
// ...
return {
type: "rect",
shape: {
// 向左偏移10px
x: x - boxWidth / 2,
// 顶部对齐数据点
y,
// 固定宽度
width: boxWidth,
// 高度 = 基准线y - 数据点y
height: y0 - y,
},
style:{
fill: api.visual("color"),
}
};
}
}
这里最终的重要的就是这个高度y0 - y
的计算了,相信只要理解了上面坐标转换,这里的计算公式就很好理解,我们看下我们生成的自定义柱状图效果:

visual函数
visual
函数用于获取视觉映射结果,它能够将数据值自动映射到颜色、大小、透明度等视觉属性;它的用法如下:
typescript
api.visual(
visualType: 'color' | 'symbol' | 'symbolSize' | 'opacity' | ...,
dataIndex?: number,
payload?: object
);
第一个参数是我们想要从数据值中获取的类型,比如上面柱状图中我们就获取了每个柱子的颜色;第二个参数是数据的索引,第三个参数是一个自定义的附加数据。
再比如我们在绘制自定义散点图的时候,就可以通过visual函数获取散点的大小和颜色,呈现更好的视觉效果:
typescript
{
series: [
{
data: [
[10, 20, 5],
[40, 60, 12],
[70, 30, 8],
[80, 50, 20],
[30, 80, 15],
],
type: "custom",
renderItem: function (params, api) {
// 获取数据值(x, y, size)
const xValue = api.value(0);
const yValue = api.value(1);
// 转换为画布坐标
const [x, y] = api.coord([xValue, yValue]);
// 动态获取视觉属性
const symbolSize = api.visual("symbolSize");
// 绘制圆形(气泡)
return {
type: "circle",
shape: {
cx: x,
cy: y,
r: symbolSize / 2,
},
style: {
fill: api.visual("color"),
},
};
},
},
],
}
我们看效果如下:

返回参数
介绍完api中常用的函数,我们就来看下如何在renderItem中返回图形元素;支持以下多种图形元素:
line
(直线)rect
(矩形)circle
(圆形)ring
(圆环)sector
(扇形)arc
(圆弧)polygon
(多边形)path
(路径,可绘制任意形状)text
(文本)group
(组合多个图形)
上面的案例中我们已经返回过了rect矩形和circle圆形两种图形,type属性
定义了图形的类型;而shape属性
则定义了图形的结构,style属性
定义图形的样式,这两种属性在不同图形中差别比较大,我们都可以在官方文档中查到。
我们可以通过style.fill
属性,将上面的柱状图,改写成一个渐变的柱状图:
typescript
// 创建透明度渐变的辅助函数
function createAlphaGradient(baseColor, startAlpha = 1, endAlpha = 0) {
// 解析基础颜色(支持多种格式)
const parsedColor = echarts.color.parse(baseColor);
// 创建起始颜色(RGBA数组格式)
const startColor = [
parsedColor[0],
parsedColor[1],
parsedColor[2],
startAlpha,
];
// 创建结束颜色
const endColor = [
parsedColor[0],
parsedColor[1],
parsedColor[2],
endAlpha,
];
// 正确调用 stringify - 传入 RGBA 数组
const startColorStr = echarts.color.stringify(startColor, "rgba");
const endColorStr = echarts.color.stringify(endColor, "rgba");
// 返回线性渐变(从上到下)
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: startColorStr },
{ offset: 1, color: endColorStr },
]);
}
{
renderItem: function (params, api) {
// 省略其他代码
// 获取基础颜色
const baseColor = api.visual("color");
// 创建垂直渐变(从1到0.05透明度)
const gradientColor = createAlphaGradient(baseColor, 1, 0.05);
return {
style: {
fill: gradientColor,
},
};
},
}
上面代码通过visual获取了当前柱子的一个基准颜色baseColor,然后解析这个基准颜色的RGB通道值,再通过createAlphaGradient函数返回graphic.LinearGradient
一个渐变效果。

除了图形的属性,renderItem函数还可以返回enterFrom
、leaveTo
、enterAnimation
、leaveAnimation
等一系列进入和离开的动画属性,比如我们可以让柱子实现逐个增长的动画效果:
typescript
{
renderItem: function (params, api) {
// 省略其他代码
return {
type: "rect",
// 省略其他属性
enterAnimation: {
delay: params.dataIndex * 400,
},
enterFrom: {
shape: {
height: 0,
y: y0,
},
style: {}
},
}
}
}
这里添加了enterFrom
属性,这个属性用于实现入场动画;shape里面可以放各种图形的属性,style属性添加入场前的样式,例如在style中添加color、opacity等,我们看下效果:

我们可以访问这个页面查看效果
除了基础的元素,我们还可以将多元素组合后,返回一个group,这样就可以组合成更复杂的图形了;下面就是我们今天要实现的三个案例:折线装饰旋转元素、分隔渐变柱状图和立体渐变柱状图。
折线装饰旋转元素
我们想要在折线图上添加一些下面的装饰旋转元素:

首先我们发现这个装饰元素整体可以分成两个部分,一个大的圆点,后面拖着一个渐变的圆弧;大的圆点好实现,通过circle绘制即可,我们首先在group中添加一个circle:
typescript
{
renderItem: function (params, api) {
const xValue = api.value(0);
const yValue = api.value(1);
const [x, y] = api.coord([xValue, yValue]);
return {
type: "group",
x,
y,
children: [
{
type: "circle",
shape: {
cx: -15,
cy: 0,
r: 2,
},
style: {
fill: "rgba(255,255,255,0.6)",
},
},
]
}
}
}
接着我们来绘制圆环,经过在官方文档组件的一通查找后,发现了一个名为arc
的图形元素,我们通过stroke属性给它一个线性的渐变效果:
typescript
{
renderItem: function (params, api) {
return {
type: "group",
x,
y,
children: [
// 省略其他代码
{
type: "arc",
shape: {
cx: 0,
cy: 0,
r: 15,
r0: 10,
startAngle: 0,
endAngle: Math.PI * 2,
},
style: {
stroke: {
type: "linear",
colorStops: [
{
offset: 0,
color: "rgba(255,255,255,1)",
},
{
offset: 1,
color: "rgba(255,255,255,0)",
},
],
},
lineWidth: 2,
fill: "transparent",
},
},
]
}
}
}
我们看到arc元素的圆环渐变效果是在整体圆环上的从左到右渐变,而不是我们想要沿着整个圆环的渐变效果:

俗话说得好,既然一个圆环不行,那就给他来两个;我们把圆环分割成上下两个部分,每个部分分别使用线性渐变:
typescript
{
children: [
{
type: "arc",
shape: {
startAngle: 0,
endAngle: Math.PI,
},
style: {
colorStops: [
{
offset: 0,
color: "rgba(255,255,255,0.6)",
},
{
offset: 1,
color: "rgba(255,255,255,0.3)",
},
],
// 省略其他代码
},
},
{
type: "arc",
// 旋转
rotation: Math.PI,
shape: {
startAngle: 0,
endAngle: Math.PI,
},
style: {
colorStops: [
{
offset: 0,
color: "rgba(255,255,255,0.3)",
},
{
offset: 1,
color: "rgba(255,255,255,0)",
},
],
// 省略其他代码
},
},
]
}
这样就能达到我们想要的效果了:

图形上的元素已经基本绘制完成了,下面我们就来让group元素旋转起来;在renderItem
的返回属性中还有一个keyframeAnimation属性
可以用来配置动画
typescript
{
renderItem: function (params, api) {
return {
type: "group",
x,
y,
keyframeAnimation: {
// 动画时长,单位 ms
duration: 2000,
// 动画缓动
easing: 'linear'
// 是否循环
loop: true,
// 动画的关键帧
keyframes: [
{
percent: 0,
rotation: 0,
},
{
percent: 0.5,
rotation: -Math.PI,
},
{
percent: 1,
rotation: -Math.PI * 2,
},
],
},
// 省略其他代码
}
}
}
keyframes数组
每一项为一个关键帧,它的第一个必选属性是percent
,表示该关键帧的百分比,取值范围是0-1;其它属性为图形在这个关键帧的属性,例如位置x,y,样式style和shape等等;这样我们就实现了元素的旋转动画。
我们可以访问这个页面查看效果
分隔渐变柱状图
下面就是本文要实现的重点:分隔渐变柱状图,我们看下最后达到的效果:

回顾我们上面已经实现的渐变柱状图,我们发现分隔渐变柱状图,就是将一根柱子拆分成多个方块呈现出来,每个方块的颜色都在像着更浅的颜色变化;因此这个图的难度其实在于如何计算出当前index所对应小方块的颜色和方块所在y轴位置。
我们先把小方块的基础信息都定义出来,比如宽高以及格子间的间距等数据:
typescript
{
renderItem: function (params, api) {
const xValue = api.value(0); // 类目索引
const yValue = api.value(1); // 数值
const [x, y] = api.coord([xValue, yValue]); // 数据点坐标
const [x0, y0] = api.coord([xValue, 0]); // 基准线坐标
// 总体柱子的高度
const totalHeight = y0 - y;
// 每个格子间的间距
const gap = 3;
// 每个格子的宽度
const width = 20;
// 每个格子的高度
const height = 6;
// 格子的总数
const count = Math.round(totalHeight / (height + gap));
// 获取基础颜色
const baseColor = api.visual("color");
const rects = Array.from({ length: count }, (_, index) => {
const realHeight = y0 - (height + gap) * index;
return {
type: "rect",
shape: {
x: x,
y: realHeight,
width,
height,
},
style: {
fill: baseColor,
},
};
});
return {
type: "group",
x: 0,
y: 0,
children: rects,
};
},
}
这里我们把height+gap
看作是一整个格子的高度;先通过基准线yo减去数据y的坐标,获取整个柱子的高度totalHeight;然后通过totalHeight可以计算整个柱子所需的小方块数量count。
然后循环count,来生成每一个小方块;每一个小方块的y轴位置计算,从基准线开始向上计算,公式为realHeight = y0 - (height + gap) * index
;我们看下最后的效果:

我们发现由于api.coord([xValue, yValue])
获取的数据坐标的x轴是在中心,因此我们需要将x的位置减去二分之一的柱子宽度width;而下面第一个方块也超出了基准线y0的高度,我们为了让最后一个方块底部贴着基准线,我们将柱子整体往上偏移一个小方块height的高度:
typescript
// 省略其他代码
const rects = Array.from({ length: count }, (_, index) => {
const realHeight = y0 - (height + gap) * index;
return {
type: "rect",
shape: {
// x位置减去柱子一半的宽度
x: x - width / 2,
// y位置向上偏移height距离
y: realHeight - height,
},
};
});
这样修复柱子的x、y位置后,柱子就在整个空间的中间位置;下面我们再给每个小方块一个动画效果,让小方块刚开始宽度高度都为0,从中心开始,挨个的增长出来。
这里我们不设置width、height为0,而是通过scaleX和scaleY在X、Y轴上的缩放;这是因为通过缩放,我们可以控制缩放的中心点在小方块的中间:
typescript
const realHeight = y0 - (height + gap) * index;
return {
type: "rect",
scaleX: 1,
scaleY: 1,
opacity: 1,
originX: x0,
originY: realHeight - height / 2,
shape: {
x: x - width / 2,
y: realHeight - height,
width,
height,
},
enterFrom: {
scaleX: 0,
scaleY: 0,
style: { opacity: 0 },
},
enterAnimation: {
delay: 20 * index,
},
};
我们还是通过enterFrom属性
设置每个小方块进入时的状态,XY方向缩放都为0,透明度也为0;缩放中心点的设置上,originX设置为x0就是小方块的X轴中心;而我们上面Y轴向上偏移了height绘制了方块,因此originY设置为realHeight - height / 2
即为Y轴中心;这样我们缩放的动画也设置好了。
渐变颜色
每个小方块的形状和位置确定后,我们下面就需要给它们来"涂颜色"了;我们在上面获取到每个柱子的基础颜色baseColor
,一般是一个十六进制的颜色值,我们需要计算得到柱子底部的颜色,是一个rgba的色值,例如:rgba(255, 255, 255, 0.1)
;我们先实现两个色值转换的工具函数:
typescript
/**
* 十六进制颜色转rgb颜色
* @param {string} hex 十六进制颜色,例如#212c37
* @returns {number[]} rgb颜色
*/
export function hexToRgb(hex: string): number[] {
let str = hex.replace("#", "");
if (str.length === 8) {
// 长度八位,是212c37ff的格式,先把后面的透明度替换掉
str = str.substring(0, 6);
}
if (str.length % 3) {
return [];
}
//获取截取的字符长度
let count = str.length / 3;
//根据字符串的长度判断是否需要 进行幂次方
let power = 6 / str.length;
let r = parseInt("0x" + str.substring(0 * count, 1 * count)) ** power;
let g = parseInt("0x" + str.substring(1 * count, 2 * count)) ** power;
let b = parseInt("0x" + str.substring(2 * count)) ** power;
return [r, g, b];
}
/**
* 设置颜色的alpha值
* @param {string} colorStr 颜色字符串
* @param {number} alpha 亮度0-1
* @return {string} 新的rgba颜色字符串
*/
export function alphaBlend(colorStr: string, alpha: number): string {
const colorArr = hexToRgb(colorStr);
if (!colorArr.length) {
return "";
}
const [r, g, b] = colorArr;
return `rgba(${r},${g},${b},${alpha})`;
}
hexToRgb函数是一个色值转换函数,而alphaBlend则是我们需要用到的将baseColor
转为最终颜色的函数:
typescript
// 获取基础颜色
const baseColor = api.visual("color");
// 柱子底部的最终颜色
const endColor = alphaBlend(baseColor as string, 0);
起点和终点的颜色确定了,在循环count函数时,我们就可以根据这两个颜色来确定方块的颜色了;为了让每个小方块在相同高度有相同的渐变颜色,我们还需要用到api中的一个函数api.getHeight
,它的作用是获取整个echarts容器的高度。
typescript
const rects = Array.from({ length: count }, (_, index) => {
const realHeight = y0 - (height + gap) * index;
const fill = liftColor(
baseColor,
endColor,
realHeight / api.getHeight()
);
return {
type: "rect",
// 其他属性
style: {
fill,
},
};
});
这里我们使用一个liftColor函数
,它接受三个参数,baseColor和endColor作为起点和终点的颜色,第三个参数是在两个颜色中间的过渡比例,我们使用柱子当前高度realHeight除以容器的总高度作为比例,这样在相同的高度就可以保持一致;最后我们就能看到最后的实现效果如下:

我们可以访问这个页面查看效果
立体渐变柱状图
我们先来看下立体渐变柱状图的实现效果如下:

我们发现,这个立体柱状图其实是一个2.5D的立体,相比于普通一维的柱状图,它是由左右两个梯形和顶部的菱形拼接而成;而这三种形状官方文档中都是没有提供的,因此我们需要通过graphic.registerShape
来注册新的图形绘制函数:
typescript
// 左侧的图形
const leftShape = graphic.extendShape({
buildPath(ctx, shape) {
const { topBasicsYAxis, bottomYAxis, basicsXAxis } = shape
// 侧面宽度
const WIDTH = 15
// 斜角高度
const OBLIQUE_ANGLE_HEIGHT = 4
// 使用整数坐标避免浮点精度问题
const p1 = [Math.round(basicsXAxis - WIDTH), Math.round(topBasicsYAxis - OBLIQUE_ANGLE_HEIGHT)]
const p2 = [Math.round(basicsXAxis - WIDTH), Math.round(bottomYAxis)]
const p3 = [Math.round(basicsXAxis), Math.round(bottomYAxis)]
const p4 = [Math.round(basicsXAxis), Math.round(topBasicsYAxis)]
ctx.moveTo(p1[0], p1[1])
ctx.lineTo(p2[0], p2[1])
ctx.lineTo(p3[0], p3[1])
ctx.lineTo(p4[0], p4[1])
ctx.closePath()
},
})
// 右侧图形
const rightShape = graphic.extendShape({
buildPath(ctx, shape) {
const { topBasicsYAxis, bottomYAxis, basicsXAxis } = shape
// 侧面宽度
const WIDTH = 15
// 斜角高度
const OBLIQUE_ANGLE_HEIGHT = 4
const p1 = [Math.round(basicsXAxis), Math.round(topBasicsYAxis)]
const p2 = [Math.round(basicsXAxis), Math.round(bottomYAxis)]
const p3 = [Math.round(basicsXAxis + WIDTH), Math.round(bottomYAxis)]
const p4 = [Math.round(basicsXAxis + WIDTH), Math.round(topBasicsYAxis - OBLIQUE_ANGLE_HEIGHT)]
ctx.moveTo(p1[0], p1[1])
ctx.lineTo(p2[0], p2[1])
ctx.lineTo(p3[0], p3[1])
ctx.lineTo(p4[0], p4[1])
ctx.closePath()
},
})
// 顶部图形
const topShape = graphic.extendShape({
buildPath(ctx, shape) {
const { topBasicsYAxis, basicsXAxis } = shape
// 侧面宽度
const WIDTH = 15
// 斜角高度
const OBLIQUE_ANGLE_HEIGHT = 4
// 稍微扩大顶部形状以覆盖可能的间隙
const p1 = [Math.round(basicsXAxis), Math.round(topBasicsYAxis + 0.5)]
const p2 = [Math.round(basicsXAxis + WIDTH + 0.5), Math.round(topBasicsYAxis - OBLIQUE_ANGLE_HEIGHT)]
const p3 = [Math.round(basicsXAxis), Math.round(topBasicsYAxis - OBLIQUE_ANGLE_HEIGHT * 2)]
const p4 = [Math.round(basicsXAxis - WIDTH - 0.5), Math.round(topBasicsYAxis - OBLIQUE_ANGLE_HEIGHT)]
ctx.moveTo(p1[0], p1[1])
ctx.lineTo(p2[0], p2[1])
ctx.lineTo(p3[0], p3[1])
ctx.lineTo(p4[0], p4[1])
ctx.closePath()
},
})
graphic.registerShape("leftShape", leftShape)
graphic.registerShape("rightShape", rightShape)
graphic.registerShape("topShape", topShape)
这里ctx就是canvas的上下文,我们可以用它来绘制;shape是传入的图形数据,这里都是一些canvas的语法,不再赘述了。
下面就是我们熟悉的renderItem函数了,在type
中使用registerShape中注册的图形名称,传入shape的数据:
typescript
{
renderItem: function (params, api) {
const xValue = api.value(0) // 类目索引
const yValue = api.value(1) // 数值
const [x, y] = api.coord([xValue, yValue]) // 数据点坐标
const [x0, y0] = api.coord([xValue, 0]) // 基准线坐标
// 获取颜色
const baseColor = api.visual("color") as string
return {
type: "group",
children: [
{
type: "leftShape",
shape: {
topBasicsYAxis: y,
basicsXAxis: x,
bottomYAxis: y0,
},
style: {
fill: reateAlphaGradient(baseColor, 1, 0),
},
},
{
type: "rightShape",
shape: {
topBasicsYAxis: y,
basicsXAxis: x,
bottomYAxis: y0,
},
style: {
fill: createAlphaGradient(baseColor, 1, 0),
},
},
{
type: "topShape",
shape: {
topBasicsYAxis: y,
basicsXAxis: x,
bottomYAxis: y0,
},
style: {
fill: baseColor,
},
},
],
}
},
}
这样我们就得到了一个由三个图形拼接而成的渐变柱状图:

为了让图形更有立体的效果,我们让下面两个梯形的颜色透明度更低一点:
typescript
{
type: "leftShape",
style: {
fill: createAlphaGradient(baseColor, 0.7, 0),
},
}
{
type: "rightShape",
style: {
fill: createAlphaGradient(baseColor, 0.9, 0),
},
}
这样我们就实现了立体渐变柱状图的整体效果,我们可以访问这个页面查看效果
总结
本文详细介绍了renderItem
函数如何创建自定义的图表;首先解释renderItem
函数的基本用法和参数结构,包括params对象提供的上下文信息、api中的关键函数(value函数、coord函数、visual函数等);主要的难点就在于理解在函数中如何数据值转换为视觉属性,以及如何组合基本图形元素来构建复杂的自定义图表。
本文重点介绍了三个高级案例的实现:折线装饰旋转元素通过组合圆形和圆弧创建动态旋转效果;分隔渐变柱状图将单个柱子分解为多个渐变色块;立体渐变柱状图则通过注册自定义形状创建2.5D立体效果。
这三个案例显示了renderItem
强大的灵活性,让我们能够开发出超过Echarts内置的图表类型,创造出高度定制化的数据可视化效果。
本文所有代码敬请关注公众号【前端壹读】,后台回复关键词【Echarts高级开发】即可获取。