Echarts 中如何绘制自定义图像、动画

3,421 阅读15分钟

如上述的两种圆柱形、立方体类型的图形,需要我们自己定义绘制的形状。

这时就需要通过 series: { type: 'custom' } 进行设置。如果你仅仅是想在图表中添加一些修饰图案、特殊文案、或者水印可以使用 graphic 来进行设置,具体细节请点击这里

series-custom

自定义系列可以自定义系列中的图形元素渲染。从而能扩展出不同的图表。同时,echarts 会统一管理图形的创建删除、动画、与其他组件(如 dataZoomvisualMap)的联动,使开发者不必纠结这些细节。

开发者自定义渲染逻辑(renderItem 函数)

系列需要开发者自己提供图形渲染的逻辑。这个渲染逻辑一般命名为 renderItem例如:(图2的代码)

// 表示立方体左侧面和右侧面水平方向的宽度。
const RECT_WIDTH = 12;
// 表示立方体顶部、底部矩形的大小计算因子,
// 通过 RECT_WIDTH * FACTOR * 2 我们可以计算出立方体顶部矩形的高度。
const FACTOR = 0.5;

//...
series: {
  type: 'custom',
  name: '省级中心示范村建设数量',
  animation: true,
  renderItem: function (params: any, api: any): any {
    // api.value(0) - 表示返回 dataItem 在 data 中数组中的索引下标;api.value(1) - 表示返回 dataItem[1]
    // api.coord()  - 返回对应 dataItem 的 x、y 坐标值(pixel)
    // 注意: echarts 坐标系默认是从 div 元素的左上角作为原点。
    // 朝左边 x 值越大,朝下边 y 值越大。
    const [x, y] = api.coord([api.value(0), api.value(1)]);

    // api.size() - 返回对应 dataItem 的 width、height。
    const [width, height] = api.size([api.value(0), api.value(1)]);

    // 返回 grid 的大小和位置,grid 包含在坐标系中。
    const { y: gridY, height: gridH } = params.coordSys;
    return {
      type: 'group',
      // 当没有数据的时候什么都不渲染
      children: api.value(1) > 0 ? [
        {
            type: 'LeftShape',
            shape: {
                // 图形向左偏移半个立方体的宽度,保持和刻度线居中
                x: x - RECT_WIDTH,
                y: y,
                // 立方体左侧面的宽度
                width: RECT_WIDTH,
                // 立方体的高度,减去RECT_WIDTH * FACTOR 是为了不让立方体绘制到 X 轴下方
                // 应该和 X 轴对齐。
                height: h - RECT_WIDTH * FACTOR,
            },
            style: {
                fill: colorLeft,
            },
        },
        {
            type: 'RightShape',
            shape: {
                x: x,
                y: y,
                // 立方体左侧面的宽度
                width: RECT_WIDTH,
                // 立方体的高度
                height: h - RECT_WIDTH * FACTOR,
            },
            style: {
                fill: colorRight,
            },
        },
        {
            type: 'TopShape',
            shape: {
                x: x - RECT_WIDTH,
                y: y,
                // 立方体顶部矩形的宽度
                width: RECT_WIDTH * 2,
                // 立方体的高度
                height: h - RECT_WIDTH * FACTOR,
            },
            style: {
                fill: '#38E3FF',
            },
        },
        /* 绘制 bar 的背景 */
        {
            type: 'LeftShape',
            shape: {
                x: x - RECT_WIDTH,
                y: gridY,
                // 立方体的宽度
                width: RECT_WIDTH,
                // 背景的高度应该始终和 grid 高度一样,
                // 减去 RECT_WIDTH * FACTOR 是为了不让背景绘制到 X 轴下方
                height: gridH - RECT_WIDTH * FACTOR,
            },
            style: {
            fill: colorLeft,
                opacity: 0.2,
            },
        },
        {
            type: 'RightShape',
            shape: {
                x: x,
                y: gridY,
                width: RECT_WIDTH,
                height: gridH - RECT_WIDTH * FACTOR,
            },
            style: {
                fill: colorRight,
                opacity: 0.2,
            },
        },
        {
            type: 'TopShape',
            shape: {
                x: x - RECT_WIDTH,
                y: gridY,
                width: RECT_WIDTH * 2,
                height: gridH - RECT_WIDTH * FACTOR,
            },
            style: {
                fill: '#38E3FF',
                opacity: 0.2,
            },
        },
    ]} : [],
  },
},

对于 data 中的每个数据项(为方便描述,这里称为 dataItem)调用此 renderItem 函数。

renderItem(params, api)

  • params:包含了当前数据信息和坐标系的信息。
  • api:是一些开发者可调用的方法集合。
  • renderItem: 函数须返回根据此 dataItem 绘制出的图形元素的定义信息。

一般来说,renderItem 函数的主要逻辑,是将 dataItem 里的值映射到坐标系上的图形元素。这一般需要用到 renderItem.arguments.api 中的两个函数:

  • api.value(...),意思是取出 dataItem 中的数值。例如 api.value(0) 表示取出当前 dataItem 中第一个维度的数值。
  • api.coord(...),意思是进行坐标转换计算。例如 var point = api.coord([api.value(0), api.value(1)]) 表示 dataItem 中的数值转换成坐标系上的点。

有时候还需要用到 api.size(...) 函数,表示得到坐标系上该 dataItem 的对应的图像尺寸。

返回值中样式的设置可以使用 api.style(...) 函数,他能得到 series.itemStyle 中定义的样式信息,以及视觉映射的样式信息。

也可以用这种方式覆盖这些样式信息:api.style({ fill: 'green', stroke: 'yellow' })

renderItem.arguments 介绍

params.dataIndex

返回该 dataItem 在数据中对应的索引下标

params.seriesName

返回系列名称

params.dataInsideLength

返回数据的长度。

params.encode

返回 series.encode

params.coordSys

返回 grid 的坐标和尺寸: {x: number, y: number, width: numberm, height: number }

api.value()

返回对应的 X、Y 轴的值。 如果 X 轴是类目轴,则返回的是该数据项在数据集合中的索引下标。 如果 Y 轴是数值轴,则返回的是该数据项的数据值。

api.coord()

返回数据项在坐标系中的坐标

// x、y 分别表示 XY 的坐标,单位为 pixel。
// 注意,这里的参数必须传值,否则得不到想要的结果
const [x, y] = api.coord([api.value(0), api.value(1)]);

api.size()

返回数据项对应图形的尺寸

// w、h 分别表示图像的宽高,单位为 pixel。
// 注意,这里的参数必须传值,否则得不到想要的结果
const [w, h] = api.coord([api.value(0), api.value(1)]);

renderItem.return 介绍

图形元素。每个图形元素是一个 object。详细信息参见:graphic

如果什么都不渲染,可以不返回任何东西。

下面我们按照绘制图像的类型来进行介绍,例如图一和图二都是组合类型(group)。

组合类型

type

如果是一个组合类型,则值应该为 group、并通过 children 属性来绘制单个图形。

x、y

图像的 x、y 坐标(像素)位置。

rotation

图像的旋转。number 类型

scaleX、scaleY

图像在 x、y 方向上的缩放。

originX、originY

图像旋转和缩放原点的 x、y 像素位置。

transition

可以通过'all'指定所有属性都开启过渡动画,也可以指定单个或一组属性。可以设置为 string、Array 类型。

Transform 相关的属性:'x''y''scaleX''scaleY''rotation''originX''originY'。例如:

{
    type: 'rect',
    x: 100,
    y: 200,
    transition: ['x', 'y']
}

还可以是这三个属性 'shape''style''extra'。表示这三个属性中所有的子属性都开启过渡动画。例如:

{
    type: 'rect',
    shape: { // ... },
    // 表示 shape 中所有属性都开启过渡动画。
    transition: 'shape',
}

在自定义系列中,当 transition 没有指定时,'x''y' 会默认开启过渡动画。如果想禁用这种默认,可设定为空数组:transition: []

enterFrom

配置图形的入场属性用于入场动画。例如:

{
    type: 'circle',
    x: 100,
    // enterFrom 其实是指定入场动画初始化时的样式。可以设置 x、y、shape、style 等
    enterFrom: {
        // 淡入
        style: { opacity: 0 },
        // 从左飞入
        x: 0
    }
}

leaveTo

配置图形的退场属性用于退场动画。例如:

{
    type: 'circle',
    x: 100,
    // leaveTo 其实是指定离场动画结束时的样式。可以设置 x、y、shape、style 等
    leaveTo: {
        // 淡出
        style: { opacity: 0 },
        // 向右飞出
        x: 200
    }
}

enterAnimation

入场动画配置。

enterAnimation:{
  // 动画时长,单位 ms
  duration: 100,
  // 可以查看 https://echarts.apache.org/examples/zh/editor.html?c=line-easing
  easing: 'linear',
    // 动画延迟时长,单位 ms
  delay: 100,
}

updateAnimation、leaveAnimation

更新属性的动画配置。

z2

用于决定图形元素的覆盖关系。number 类型

width

用于描述此 group 的宽。width 属性只有组合类型中有,单类型中没有(可以在 shape 中设置 width)

这个宽只用于给子节点定位。

即便当宽度为零的时候,子节点也可以使用 left: 'center' 相对于父节点水平居中。

height

用于描述此 group 的高。height 属性只有组合类型中有,单类型中没有(可以在 shape 中设置 height)

这个高只用于给子节点定位。

即便当高度为零的时候,子节点也可以使用 top: 'middle' 相对于父节点垂直居中。

children

子节点列表,其中项都是一个图形元素定义。

单个类型

type

setOption 首次设定图形元素时必须指定。 可取值:imagetextrectgrouplinepolygencirclearc 等。

我们也可以通过 graphic.registerShape('名称', 自定义的shape类)来注册一个自定义的图形类型。后面在介绍。

组合类型中所有的属性在这里都有,单类型中没有 children、width、height 属性。介绍几个不一样的属性:

shape

{
  // 图形元素的左上角在父节点坐标系(以父节点左上角为原点)中的横坐标值。
  x: 100,
  // 图形元素的左上角在父节点坐标系(以父节点左上角为原点)中的纵坐标值。
  y: 100,
  // 图形元素的宽度。
  width: 100,
  // 图形元素的高度。
  height: 100,
  // 圆角
  r: [8],
  // 可以是一个属性名,或者一组属性名。 被指定的属性,在其指发生变化时,会开启过渡动画。 
  // 只可以指定本 shape 下的属性。
  transition: ['x', 'y'],
}

注意:不建议设置 shape 中的 x、y 属性。应该采用外部 x、y 属性。这样在设置 enterFrom 动画时更方便设置。

style

{
  fill: 填充色
  stroke:描边色
  lineWidth:线的宽度
  linecap: 笔触的类型,round、butt、square
  lineJoin: 设置线条转折点的样式。默认 miter
  shadowBlur:阴影模糊半径
  shadowOffsetX: 阴影 X 方向偏移。
  shadowOffsetY: 阴影 Y 方向偏移。
  shadowColor: 阴影颜色。
  opacity: 透明度。
}

注意:如果我们希望 shape、style 中设置的属性在入场和离场时有动画效果,那么就需要在 enterFrom、或 leaveTo 动画中设置 shape、style 的属性。

{
  type: 'rect',
  x: 100,
  y: 100,
  shape: {
    width: 30,
    height: 100,
  },
  // 开启 shape 中所有的属性动画,如果忽略该属性,动画也会照常执行。
  transition: ['shape'],
  // 设置图像入场时的位置和尺寸,
  enterFrom: {
    x: 0,
    y: 0,
    shape: {
      width: 30,
      height: 0
    },
    style: {
      opacity: 0,
    }
  }
}

自定义图形类型

如果自定义图形类型,我们需要先通过 graphic.extendShape(options) 方法定义一个类。然后再使用 graphic.registerShape('xxx', xxx)把先前自定的图形类注册到 Echarts 中,这样方可使用。

注册一个面(图2中立方体的左侧面)


const LeftRectShape = graphic.extendShape({
    // shape 是一个固定值。
    shape: {
        x: 0,
        y: 0,
        width: 0,
        height: 0,
    },
    // ctx 其实就是 canvas.getContext('2d')。可以使用 ctx 绘制 arc、rect、bezierCurveTo 等形状
    // 没有 transform 等这样的 api。
    // shape 是绘制的图形的尺寸和位置信息。
    buildPath: function (ctx, shape) {
        // 注意:shape 其实 dataTime 调用 renderItem() 方法是返回的图形定义。
        const { width: w, height: h, x, y } = shape;

        // 绘制一个平面
        ctx.moveTo(x, y); // 左上角
        ctx.lineTo(x, y + h); // 左下角
        ctx.lineTo(x + w, y + h + w * FACTOR); // 右下角
        ctx.lineTo(x + w, y + w * FACTOR); // 右上角
        ctx.closePath();

    },
});

graphic.registerShape('LeftRectShape', LeftRectShape);

注册一个椭圆(图1中圆柱体的顶部面)

// 绘制一个椭圆
const EllipseShape = graphic.extendShape({
  shape: {
    x: 0,
    y: 0,
    width: 0,
    height: 0,
  },
  buildPath: function (ctx, shape) {
    /**
     * 绘制椭圆形,这是一个固定的公式:
     * x、y 表示椭圆的圆心坐标
     * w、y 表示椭圆的X、Y方向的直径。
     * kappa 理解为椭圆的绘制因子,固定值。
     */
    const { x, y, width: w, height: h } = shape;
    const kappa = 0.5522848;
    const ox = (w / 2) * kappa;
    const oy = (h / 2) * kappa;
    const xe = x + w;
    const ye = y + h;
    const xm = x + w / 2;
    const ym = y + h / 2;

    ctx.moveTo(x, ym);
    ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
    ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
    ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
    ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
    ctx.closePath();
  },
});

// 注册
graphic.registerShape('EllipseShape', EllipseShape);

案例一(图2)

// 我们将立方体的顶部矩形定义为一个矩形,矩形的宽度和高度比值设定为 0.5 (可自己设定合适的值)。 
const FACTOR = 0.5;

useEffect(() => {
  if (!echartsInstanceRef.current) {
    echartsInstanceRef.current = echartsInit(dvRef.current);

    // 立方体左侧面的图形类
    const LeftRectShape = graphic.extendShape({
      // shape 是一个固定值。
      shape: {
        x: 0,
        y: 0,
        width: 0,
        height: 0,
      },
      // ctx 其实就是 canvas.getContext('2d')。可以使用 ctx 绘制 arc、rect、bezierCurveTo 等形状
      // 没有 transform 等这样的 api。
      // shape 是绘制的图形的尺寸和位置信息。
      buildPath: function (ctx, shape) {
        // 注意:shape 其实 dataTime 调用 renderItem() 方法是返回的图形定义。
        const { width: w, height: h, x, y } = shape;
        
        // 绘制一个平面
        ctx.moveTo(x, y); // 左上角
        ctx.lineTo(x, y + h); // 左下角
        ctx.lineTo(x + w, y + h + w * FACTOR); // 右下角
        ctx.lineTo(x + w, y + w * FACTOR); // 右上角
        ctx.closePath();
      },
    });
    // 立方体右侧面的图形类
    const RightRectShape = graphic.extendShape({
      shape: {
        x: 0,
        y: 0,
        width: 0,
        height: 0,
      },
      buildPath: function (ctx, shape) {
        const { width: w, height: h, x, y } = shape;

        ctx.moveTo(x, y + w * FACTOR);
        ctx.lineTo(x, y + w * FACTOR + h);
        ctx.lineTo(x + w, y + h);
        ctx.lineTo(x + w, y);
        ctx.closePath();
      },
    });

    const TopRectShape = graphic.extendShape({
      shape: {
        x: 0,
        y: 0,
        width: 0,
        height: 0,
      },
      buildPath: function (ctx, shape) {
        const { width: w, x, y } = shape;

        ctx.moveTo(x, y);
        ctx.lineTo(x + w / 2, y + (w / 2) * FACTOR);
        ctx.lineTo(x + w, y);
        ctx.lineTo(x + w / 2, y - (w / 2) * FACTOR);
        ctx.closePath();
      },
    });

    graphic.registerShape('LeftRectShape', LeftRectShape);
    graphic.registerShape('RightRectShape', RightRectShape);
    graphic.registerShape('TopRectShape', TopRectShape);
  }

  // 左侧填充色
  const colorLeft = new graphic.LinearGradient(0, 0, 0, 1, [
    { color: '#2C8CAE', offset: 0 },
    { color: '#1AE1DE', offset: 0 },
  ]);
  // 右侧填充色
  const colorRight = new graphic.LinearGradient(0, 0, 0, 1, [
    { color: '#3398BC', offset: 0 },
    { color: '#60FFFD', offset: 0 },
  ]);

  const options: EChartsOption = {
    tooltip: {
      show: true,
      trigger: 'axis',
      axisPointer: {
        type: 'shadow',
      },
    },
    legend: {
      show: true,
      orient: 'horizontal',
      icon: 'circle',
      textStyle: {
        color: '#fff',
      },
    },
    grid: {
      left: 20,
      right: 0,
      bottom: 0,
      top: 70,
      containLabel: true,
    },
    xAxis: [
      {
        show: true,
        type: 'category',
        boundaryGap: true,
        axisLabel: {
          color: '#fff',
          fontSize: 10,
          rotate: 30,
          margin: 10,
        },
        axisTick: { show: false },
        splitLine: { show: false },
        axisLine: {
          show: true,
          lineStyle: {
            width: 1,
            color: '#37B6F2',
            opacity: 0.32,
          },
        },
      },
    ],
    yAxis: [
      {
        show: true,
        alignTicks: true,
        type: 'value',
        name: '单位(个)',
        nameTextStyle: {
          color: '#ddd',
          fontSize: 10,
          align: 'center',
        },
        nameGap: 20,
        nameLocation: 'end',
        axisLabel: {
          inside: false,
          color: '#fff',
          fontSize: 10,
        },
        axisLine: {
          show: false,
          lineStyle: {
            width: 1,
            color: '#37B6F2',
            opacity: 0.32,
          },
        },
        splitLine: { show: false },
      },
      {
        show: true,
        type: 'value',
        max: 100,
        name: '单位(%)',
        nameTextStyle: {
          color: '#ddd',
          fontSize: 10,
          align: 'center',
        },
        nameGap: 20,
        nameLocation: 'end',
        axisLabel: {
          inside: false,
          color: '#fff',
          fontSize: 10,
        },
        axisLine: { show: false },
        splitLine: { show: false },
      },
    ],
    series: [
      {
        type: 'custom',
        color: '#0BF6FF',
        name: props.source[0][1],
        // name: '省级中心村建设数量(单位:个)',
        yAxisIndex: 0,
        xAxisIndex: 0,
        animation: true,
        renderItem: function (params: any, api: any): any {
            const [x, y] = api.coord([api.value(0), api.value(1)]);
            const [w, h] = api.size([api.value(0), api.value(1)]);

            const { y: gridY, height: gridH } = params.coordSys;

            // w 表示整个图像的宽度,理论上我们将立方体分为 LeftRectShape 和 RightRectShape,
            // 所以每个 rect 的宽度就是 w/2,
            const rectWidth = w / 2 * 0.5;
            return {
              type: 'group',
              children: api.value(1) > 0 ? [
                {
                  type: 'LeftRectShape',
                  x: x - rectWidth,
                  y: y,
                  shape: {
                    // 立方体左侧面的宽度
                    width: rectWidth,
                    // 立方体的高度
                    height: h - rectWidth * FACTOR,
                  },
                  enterFrom: {
                    // 淡入
                    style: { opacity: 0 },
                    // y: 0,
                    shape: {
                      height: 0,
                    },
                    y: gridH + gridY,
                  },
                  style: {
                    fill: colorLeft,
                  },
                },
                {
                  type: 'RightRectShape',
                  x: x,
                  y: y,
                  shape: {
                    // 立方体左侧面的宽度
                    width: rectWidth,
                    // 立方体的高度
                    height: h - rectWidth * FACTOR,
                  },
                  enterFrom: {
                    // 淡入
                    style: { opacity: 0 },
                    // y: 0,
                    shape: {
                      height: 0,
                    },
                    y: gridH + gridY,
                  },
                  style: {
                    fill: colorRight,
                  },
                },
                {
                  type: 'TopRectShape',
                  x: x - rectWidth,
                  y: y,
                  shape: {
                    // 立方体顶部矩形的宽度
                    width: rectWidth * 2,
                    // 立方体的高度
                    height: h - rectWidth * FACTOR,
                  },
                  enterFrom: {
                    // 淡入
                    style: { opacity: 0 },
                    // y: 0,
                    shape: {
                      height: 0,
                    },
                    y: gridH + gridY,
                  },
                  style: {
                    fill: '#38E3FF',
                  },
                },

                /* 绘制 bar 的背景 */
                {
                  type: 'LeftRectShape',
                  shape: {
                    x: x - rectWidth,
                    y: gridY,
                    // 立方体的宽度
                    width: rectWidth,
                    // 立方体的高度
                    height: gridH - rectWidth * FACTOR,
                  },
                  enterFrom: {
                    // 淡入
                    style: { opacity: 0 },
                  },
                  style: {
                    fill: colorLeft,
                    opacity: 0.2,
                  },
                },
                {
                  type: 'RightRectShape',
                  shape: {
                    x: x,
                    y: gridY,
                    // 立方体的宽度
                    width: rectWidth,
                    // 立方体的高度
                    height: gridH - rectWidth * FACTOR,
                  },
                  enterFrom: {
                    // 淡入
                    style: { opacity: 0, },
                  },
                  style: {
                    fill: colorRight,
                    opacity: 0.2,
                  },
                },
                {
                  type: 'TopRectShape',
                  shape: {
                    x: x - rectWidth,
                    y: gridY,
                    // 立方体顶部矩形的宽度
                    width: rectWidth * 2,
                    // 立方体的高度
                    height: gridH - rectWidth * FACTOR,
                  },
                  enterFrom: {
                    // 淡入
                    style: { opacity: 0 },
                  },
                  style: {
                    fill: '#38E3FF',
                    opacity: 0.2,
                  },
                },
              ] : [],
            };
        },
        tooltip: {
          show: true,
          valueFormatter: (value: any) => `${value}个`,
        },
      },
      {
        type: 'line',
        name: props.source[0][2],
        color: '#229AFF',
        // name: '覆盖率(单位:%)',
        yAxisIndex: 1,
        xAxisIndex: 0,
        encode: {
          x: '城市名称',
          y: '覆盖率',
        },
        smooth: true,
        lineStyle: {
          color: '#229AFF',
        },
        tooltip: {
          valueFormatter: (value: any) => `${value.toFixed(2)}%`,
        },
      },
    ],
    dataset: {
      source: props.source,
    },
  };

  echartsInstanceRef.current.setOption(options);
}, [props.source]);

案例二(图3)

// 绘制一个椭圆
const EllipseShape = graphic.extendShape({
  shape: {
    x: 0,
    y: 0,
    width: 0,
    height: 0,
  },
  buildPath: function (ctx, shape) {
    /**
     * 绘制椭圆形,这是一个固定的公式:
     * x、y 表示椭圆的圆心坐标
     * w、y 表示椭圆的 X、Y 方向的直径。
     * kappa 理解为椭圆的绘制因子,固定值。
     */
    const { x, y, width: w, height: h } = shape;
    const kappa = 0.5522848;
    const ox = (w / 2) * kappa;
    const oy = (h / 2) * kappa;
    const xe = x + w;
    const ye = y + h;
    const xm = x + w / 2;
    const ym = y + h / 2;

    ctx.moveTo(x, ym);
    ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
    ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
    ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
    ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
    ctx.closePath();
  },
});
graphic.registerShape('EllipseShape', EllipseShape);

series: {
  type: 'custom',
  name: props.source[0][1],
  animation: true,
  renderItem: function (params: any, api: any): any {
    // 注意,x 的坐标默认和 X 轴的刻度线时对齐的。
    const [x, y] = api.coord([api.value(0), api.value(1)]);
    const [w, h] = api.size([api.value(0), api.value(1)]);

    const { y: gridY, height: gridH } = params.coordSys;

    // w 表示图形的宽度,这个宽度是 echarts 自己算出来的;
    // 我们应该根据 w 来计算,否则当数量量变化时无法实现图像的自适应。
    // 0.4 表示每个图形之间就都有 0.6w 的间隙了,更美观。
    const ellipseRadiusX = w / 2 * 0.4;
    // 椭圆的 Y 轴半径是 X 轴半径的一般
    const ellipseRadiusY = ellipseRadiusX * 0.5;

    return {
      type: 'group',
      children: api.value(1) > 0 [
        {
          // 绘制矩形
          type: 'rect',
          x: x - ellipseRadiusX,
          y: y,
          shape: {
            // 圆柱体的宽度
            width: ellipseRadiusX * 2,
            // 圆柱体的高度
            height: h - ellipseRadiusY * 2,
          },
          style: {
            fill: colorSteps,
          },
          enterFrom: {
            style: { opacity: 0 },
            // 高度为 0
            shape: { height: 0 },
            // 入场动画,图像的 y 在X 轴线位置
            y: gridY + gridH,
          }
        },
        {
          // 圆柱形顶部的椭圆绘制
          type: 'EllipseShape',
          x: x - ellipseRadiusX,
          y: y - ellipseRadiusY,
          shape: {
            width: ellipseRadiusX * 2,
            height: ellipseRadiusY * 2,
          },
          style: {
            fill: '#21F3FF',
          },
          // 顶部椭圆的动画是从下往上的,和圆柱体的高度递增动画保持一致。
          enterFrom: {
            style: { opacity: 0 },
            y: gridY + gridH,
          }
        },
        {
          // 圆柱形底部的椭圆绘制
          type: 'EllipseShape',
          x: x - ellipseRadiusX,
          // 与 X 轴线保持一定的距离(图像向上偏移 10 pixels)。
          // 注意,椭圆圆心坐标为(x,y + radiusY),所以这里是 ellipseRadiusY * 2 - 10。
          y: gridY + gridH - ellipseRadiusY * 2 - 10,
          shape: {
            width: ellipseRadiusX * 2,
            height: ellipseRadiusY * 2,
          },
          style: {
            fill: '#1F61EA',
            opacity: 0.8,
          },
          // 圆柱体的底部椭圆的只需要设置一个透明度渐变就行。他不需要移动的
          enterFrom: {
            style: { opacity: 0 },
          }
        },
      ] : [],
    };
  },
},

案例三(图三)

series: {
  type: 'custom',
  name: props.source[0][1],
  animation: true,
  renderItem: function (params: any, api: any): any {
    const [x, y] = api.coord([api.value(0), api.value(1)]);
    const [w, h] = api.size([api.value(0), api.value(1)]);

    const { y: gridY, height: gridH } = params.coordSys;

    const H = gridY + gridH;

    const ellipseRadiusX = w * 0.5 * 0.25;
    const ellipseRadiusY = ellipseRadiusX * 0.5;

    return {
      type: 'group',
      children: api.value(1) > 0 [
        {
          // 绘制矩形
          type: 'rect',
          // 要保证圆柱体的中心和X轴刻度线保持对齐
          x: x - ellipseRadiusX,
          y: y,
          shape: {
            // 圆柱体的宽度,
            width: ellipseRadiusX * 2,
            // 圆柱体的高度
            height: h - ellipseRadiusY,
          },
          style: {
            fill: new graphic.LinearGradient(0, 0, 0, 1, [
              { color: 'rgba(18, 229, 237, 0.9)', offset: 0 },
              { color: 'rgba(18, 229, 237, 0)', offset: 1 },
            ]),
          },
          // 动画
          enterFrom: {
            style: { opacity: 0 },
            y: H - ellipseRadiusY * 2 * 2.5,
            shape: { height: 0 },
          }
        },
        {
          // 圆柱形底部的椭圆环(外)
          type: 'EllipseShape',
          x: x - ellipseRadiusX * 2.5,
          // 顶部椭圆外环的边应该与 X 轴线对齐,减去 ellipseRadiusY * 2 * 2.5
          // 是因为椭圆的圆心坐标是(x, y + radiusY),所以才要乘以 2
          y: H - ellipseRadiusY * 2 * 2.5,
          // 2.5 表示缩放因子
          shape: {
            width: ellipseRadiusX * 2 * 2.5,
            height: ellipseRadiusY * 2 * 2.5,
          },
          style: {
            stroke: new graphic.LinearGradient(0, 0, 0, 1, [
              { color: '#38A0D6', offset: 1 },
              { color: '#6DCDE6', offset: 0 },
            ]),
          },
          enterFrom: {
            style: { opacity: 0 },
          }
        },
        {
          // 圆柱形底部的椭圆环(内)
          type: 'EllipseShape',
          x: x - ellipseRadiusX * 1.8,
          y: H - ellipseRadiusY * 2 * 2.5,
          shape: {
            width: ellipseRadiusX * 2 * 1.8,
            height: ellipseRadiusY * 2 * 1.8,
          },
          style: {
            stroke: new graphic.LinearGradient(0, 0, 0, 1, [
              { color: '#38A0D6', offset: 1 },
              { color: '#6DCDE6', offset: 0 },
            ]),
          },
          enterFrom: {
            style: { opacity: 0 },
          }
        },
        {
          // 圆柱形顶部的椭圆绘制
          type: 'EllipseShape',
          x: x - ellipseRadiusX,
          y: y - ellipseRadiusY,
          shape: {
            width: ellipseRadiusX * 2,
            height: ellipseRadiusY * 2,
          },
          style: {
            fill: new graphic.LinearGradient(0, 0, 0, 1, [
              { color: '#00F0FF', offset: 0 },
              { color: '#7BF7FF', offset: 1 },
            ]),
          },
          enterFrom: {
            style: { opacity: 0 },
            y: H - ellipseRadiusY * 2 * 2.5,
          }
        },
        {
          // 圆柱形底部的椭圆绘制
          type: 'EllipseShape',
          x: x - ellipseRadiusX,
          y: H - ellipseRadiusY * 2 * 2.2,
          shape: {
            width: ellipseRadiusX * 2,
            height: ellipseRadiusY * 2,
          },
          style: {
            fill: new graphic.LinearGradient(0, 0, 0, 1, [
              { color: '#38A0D6', offset: 1 },
              { color: '#6DCDE6', offset: 0 },
            ]),
          },
          enterFrom: {
            style: { opacity: 0 },
          }
        },
      ] : [],
    };
},