Apache ECharts 5.3.0 特性介绍

Apache ECharts 5.3.0 在动画表达力、渲染性能、服务端渲染上做了大幅度的增强,同时也新增了多坐标轴刻度自动对齐、tooltip 数值格式化、地图投影等社区中期盼已久的特性。

关键帧动画

在之前 ECharts 的动画集中在图形添加、更新以及移除的过渡动画上,过渡动画往往只有开始状态和结束状态。为了表达更复杂的动画效果,我们 5.3.0 中为自定义系列图形组件引入了全新的关键帧动画。

下面是一个简单的通过关键帧动画实现的呼吸动画的效果。

option = {
  graphic: {
    type: 'circle',
    shape: { r: 100 },
    left: 'center',
    top: 'center',
    keyframeAnimation: [
      {
        duration: 3000,
        loop: true,
        keyframes: [
          {
            percent: 0.5,
            easing: 'sinusoidalInOut',
            scaleX: 0.1,
            scaleY: 0.1
          },
          {
            percent: 1,
            easing: 'sinusoidalInOut',
            scaleX: 1,
            scaleY: 1
          }
        ]
      }
    ]
  }
};
live

在关键帧动画中,你可以配置动画时长、缓动、是否循环、每个关键帧的位置、缓动以及图形属性等。而且每个图形可以同时设置多个不同配置的关键帧动画。灵活的配置让我们可以实现非常复杂的动画效果,下面列举几个可以应用关键帧动画的场景。

自定义加载动画

ECharts 默认内置了一个加载动画,可以调用showLoading显示。开发者经常会提需求需要更多的加载动画效果。现在有了关键帧动画后,我们可以通过图形(graphic)组件配合关键帧动画实现任何想要的加载动画效果。

比如文本描边动画:

option = {
  graphic: {
    elements: [
      {
        type: 'text',
        left: 'center',
        top: 'center',
        style: {
          text: 'Apache ECharts',
          fontSize: 40,
          fontWeight: 'bold',
          lineDash: [0, 200],
          lineDashOffset: 0,
          fill: 'transparent',
          stroke: '#000',
          lineWidth: 1
        },
        keyframeAnimation: {
          duration: 3000,
          loop: true,
          keyframes: [
            {
              percent: 0.7,
              style: {
                fill: 'transparent',
                lineDashOffset: 200,
                lineDash: [200, 0]
              }
            },
            {
              // Stop for a while.
              percent: 0.8,
              style: {
                fill: 'transparent'
              }
            },
            {
              percent: 1,
              style: {
                fill: 'black'
              }
            }
          ]
        }
      }
    ]
  }
};
live

或者柱状图形状的加载动画:

const columns = [];
for (let i = 0; i < 7; i++) {
  columns.push({
    type: 'rect',
    x: i * 20,
    shape: {
      x: 0,
      y: -40,
      width: 10,
      height: 80
    },
    style: {
      fill: '#5470c6'
    },
    keyframeAnimation: {
      duration: 1000,
      delay: i * 200,
      loop: true,
      keyframes: [
        {
          percent: 0.5,
          scaleY: 0.1,
          easing: 'cubicIn'
        },
        {
          percent: 1,
          scaleY: 1,
          easing: 'cubicOut'
        }
      ]
    }
  });
}
option = {
  graphic: {
    elements: [
      {
        type: 'group',
        left: 'center',
        top: 'center',
        children: columns
      }
    ]
  }
};
live

扩展更丰富的散点图动画特效

带有特效动画的散点图一直以来是 ECharts 的特色功能。开发者可以使用 effectScatter 系列来实现带有涟漪特效的动态散点图,这种特效动画除了让作品更有趣,也起到了高亮提示用户的效果。跟加载动画一样,开发者也常常提出需要更多动画效果的需求。现在我们可以在自定义系列中通过使用关键帧动画来实现更复杂的特效。

比如下面例子在 SVG 地图上给自定义系列绘制的图钉加上了跳动的动画效果,同时配上了涟漪动画。

fetch(
  'https://fastly.jsdelivr.net/gh/apache/echarts-website@asf-site/examples/data/asset/geo/Map_of_Iceland.svg'
)
  .then(response => response.text())
  .then(svg => {
    echarts.registerMap('iceland_svg', { svg: svg });
    option = {
      geo: {
        map: 'iceland_svg',
        left: 0,
        right: 0
      },
      series: {
        type: 'custom',
        coordinateSystem: 'geo',
        geoIndex: 0,
        zlevel: 1,
        data: [
          [488, 459, 100],
          [770, 757, 30],
          [1180, 743, 80],
          [894, 1188, 61],
          [1372, 477, 70],
          [1378, 935, 81]
        ],
        renderItem(params, api) {
          const coord = api.coord([
            api.value(0, params.dataIndex),
            api.value(1, params.dataIndex)
          ]);

          const circles = [];
          for (let i = 0; i < 5; i++) {
            circles.push({
              type: 'circle',
              shape: {
                cx: 0,
                cy: 0,
                r: 30
              },
              style: {
                stroke: 'red',
                fill: 'none',
                lineWidth: 2
              },
              // Ripple animation
              keyframeAnimation: {
                duration: 4000,
                loop: true,
                delay: (-i / 4) * 4000,
                keyframes: [
                  {
                    percent: 0,
                    scaleX: 0,
                    scaleY: 0,
                    style: {
                      opacity: 1
                    }
                  },
                  {
                    percent: 1,
                    scaleX: 1,
                    scaleY: 0.4,
                    style: {
                      opacity: 0
                    }
                  }
                ]
              }
            });
          }
          return {
            type: 'group',
            x: coord[0],
            y: coord[1],
            children: [
              ...circles,
              {
                type: 'path',
                shape: {
                  d:
                    'M16 0c-5.523 0-10 4.477-10 10 0 10 10 22 10 22s10-12 10-22c0-5.523-4.477-10-10-10zM16 16c-3.314 0-6-2.686-6-6s2.686-6 6-6 6 2.686 6 6-2.686 6-6 6z',
                  x: -10,
                  y: -35,
                  width: 20,
                  height: 40
                },
                style: {
                  fill: 'red'
                },
                // Jump animation.
                keyframeAnimation: {
                  duration: 1000,
                  loop: true,
                  delay: Math.random() * 1000,
                  keyframes: [
                    {
                      y: -10,
                      percent: 0.5,
                      easing: 'cubicOut'
                    },
                    {
                      y: 0,
                      percent: 1,
                      easing: 'bounceOut'
                    }
                  ]
                }
              }
            ]
          };
        }
      }
    };

    myChart.setOption(option);
  });
live

加载 Lottie 动画

为了充分发掘出新的关键帧动画的能力,ECharts 团队的沈毅写了一个 Lottie 动画的解析库,可以将 Lottie 动画文件解析成 ECharts 的图形格式进行渲染。结合 Lottie 的表达力我们可以进一步的在我们的项目中绘制出细腻的动画:

图形组件过渡动画

我们在 5.0 里为自定义系列中返回的图形提供了更灵活的过渡动画配置。可以通过transition, enterFrom, leaveTo三个配置项来配置每个图形哪些属性会拥有过渡动画,当图形创建和被移除的时候该执行怎么样的动画。例如:

function renderItem() {
  //...
  return {
    //...
    x: 100,
    // 'style', 'x', 'y' 会被动画
    transition: ['style', 'x', 'y'],
    enterFrom: {
      style: {
        // 淡入
        opacity: 0
      },
      //从左侧飞入
      x: 0
    },
    leaveTo: {
      // 淡出
      opacity: 0
    },
    // 向右侧飞出
    x: 200
  };
}

在 5.3.0 中我们把这些过渡动画的配置扩展到了图形(graphic)组件中,并且做了更多的增强:

如果你不想一一写出每个要动画的属性,现在你可以直接配置transition: 'all'为所有属性都加上动画过渡。

与此同时我们还新增了enterAnimationupdateAnimationleaveAnimation分别配置每个图形入场、更新、出场动画的时长(duration)、延迟(delay)和缓动(easing)。除此之外,渐变色现在也支持动画了。

全新的 SVG 渲染器

在 5.3.0 中我们重构了我们的 SVG 渲染器,新的 SVG 渲染器能够带来 2x ~ 10x 的性能提升,在某些特殊场景中甚至能有数十倍的提升。

之前的 SVG 渲染器我们直接从渲染队列更新到 DOM。但是因为 zrender 的图形属性跟 DOM 并不是一一对应的,因此中间需要实现非常复杂的 Diff 逻辑,容易出错而且在某些场景下性能并不能做到最好。在这个版本我们重构成先全量渲染到 VDOM,然后再将 VDOM patch 到 DOM 完成渲染。全量渲染可以避免复杂的 Diff 逻辑带来的潜在 Bug。而 VDOM 和 DOM 的一一对应可以保证在 patch 的时候保证更新是最少的,从而带来巨大的性能提升。

这个例子 可以给大家带来比较直观的性能提升的感受。新的版本在 SVG 模式下拖动的交互上比之前版本流畅非常多。

5.2.2 (Before) 5.3.0 (After)
before after

除了性能的提升,我们还可以使用中间全量渲染得到的 VDom 做更多的事情,比如下面会介绍的服务端渲染。

零依赖的服务端渲染

在之前的版本 ECharts 也可以实现服务端的渲染,但是必须得依赖 node-canvas,如果是使用 SVG 模式则需要依赖 JSDOM 来模拟 DOM 环境。这些依赖一是带来了额外的体积和使用要求,二是也会有更多的性能损耗。

这次新的 SVG 渲染器可以让我们从中间的 VDOM 渲染得到字符串,带来了完全零依赖的服务端渲染,输出更精简并且带有 CSS 动画的 SVG 字符串。

const echarts = require('echarts');

// 在 SSR 模式下第一个参数不需要再传入 DOM 对象
const chart = echarts.init(null, null, {
  renderer: 'svg', // 必须使用 SVG 模式
  ssr: true, // 开启 SSR
  width: 400, // 需要指明高和宽
  height: 300
});

// 像正常使用一样 setOption
chart.setOption({
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      data: [120, 200, 150, 80, 70, 110, 130],
      type: 'bar'
    }
  ]
});

// 输出字符串
const svgStr = chart.renderToSVGString();

我们在 CodeSandbox 中搭建一个最简单的 NodeJS 服务器然后使用 ECharts 服务端渲染的效果:

在此基础上,我们优化了输出的 SVG 字符串,使其在诸如 PowerPoint 等更多的平台上有更好的显示效果。

自定义地图投影

地图一直是 ECharts 中使用非常广泛的组件。一般地图组件会使用存储了经纬度的 GeoJSON 格式的数据。而 ECharts 则计算出合适的显示区域然后把经纬度线性映射到这个区域。这是一种最简单的地图投影方式。但是简单的线性投影并无法满足某些复杂的地图场景,例如使用 Albers 投影解决线性投影中面积失真的问题,或者在世界地图中让太平洋显示在中间等等。

因此在 5.3.0 里中我们引入了自定义的地图投影,可以通过projectunproject两个方法告诉 ECharts 如何投影坐标,以及如何根据投影后坐标计算经纬度。下面是简单的使用墨卡托投影的例子:

series = {
  type: 'map',
  projection: {
    project: point => [
      (point[0] / 180) * Math.PI,
      -Math.log(Math.tan((Math.PI / 2 + (point[1] / 180) * Math.PI) / 2))
    ],
    unproject: point => [
      (point[0] * 180) / Math.PI,
      ((2 * 180) / Math.PI) * Math.atan(Math.exp(point[1])) - 90
    ]
  }
};

除了我们自己实现投影公式,我们也可以使用 d3-geo 等第三方库提供的现成的投影实现:

const projection = d3.geoConicEqualArea();
// ...
series = {
  type: 'map',
  projection: {
    project: point => projection(point),
    unproject: point => projection.invert(point)
  }
};

配合在 5.2 里新增的全局过渡动画特性,我们可以实现不同投影效果之间的动画过渡:

地图投影动画

除了地图的投影之外,我们在这个版本对于地图还做了下面两个增强:

  • 对 GeoJSON 数据提供了'LineString''MultiLineString'的支持。
  • 将默认标签位置的计算从包围盒中心改为最大区域的重心坐标,计算结果更加准确。

多坐标轴的刻度对齐

多坐标轴的刻度对齐是社区中提了很久的一个需求,我们在网上也可以看到很多开发者写的如何在 ECharts 中实现坐标轴对齐的文章,通常都会比较麻烦而且会有比较多的局限性。

在 5.3.0 中我们终于引入了数值轴坐标轴刻度对齐的功能。可以在需要对齐刻度的坐标轴中配置alignTicks: true。该坐标轴就会根据第一个坐标轴的刻度划分去调整自己的刻度,实现自动对齐。

option = {
  tooltip: {
    trigger: 'axis'
  },
  legend: {},
  xAxis: [
    {
      type: 'category',
      data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
      axisPointer: {
        type: 'shadow'
      }
    }
  ],
  yAxis: [
    {
      type: 'value',
      name: 'Precipitation',
      alignTicks: true,
      axisLabel: {
        formatter: '{value} ml'
      }
    },
    {
      type: 'value',
      name: 'Temperature',
      axisLabel: {
        formatter: '{value} °C'
      }
    }
  ],
  series: [
    {
      name: 'Evaporation',
      type: 'bar',
      // prettier-ignore
      data: [2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20.0, 6.4, 3.3]
    },
    {
      name: 'Precipitation',
      type: 'bar',
      // prettier-ignore
      data: [2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3]
    },
    {
      name: 'Temperature',
      type: 'line',
      yAxisIndex: 1,
      data: [2.0, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3, 23.4, 23.0, 16.5, 12.0, 6.2]
    }
  ]
};
live

支持高亮和选中状态的关闭

ECharts 中高亮状态可以在鼠标移到图形上的时候给用户提供反馈,但是在图表中有海量图形的时候,高亮的动画也可能带来交互上的性能问题。特别在 tooltip 或者图例组件联动触发的高亮会同时高亮多个图形。

因此在这个版本中我们新增了emphasis.disabled配置项。如果不需要高亮的反馈,又对交互性能非常在意的话,可以通过这个配置项来关闭高亮状态。

与此同时,对于选中状态,我们也新增了select.disabled。该配置项可以用于细粒度配置部分数据不可选。

支持整个系列的选中

在 5.3.0 中我们支持将selectedMode配置为'series'以实现系列所有数据的选中。

tooltip 中的数值格式化

tooltip 可以在用户移到图形上的时候通过提示框显示更详细的相关信息,ECharts 也提供了formatter回调函数可以让开发者更灵活的自定义提示框的内容。

但是我们发现大部分时候开发者只是需要格式化提示框中的数字部分,例如固定精度,加上$前缀等等,而之前为了格式化数字开发者只能通过formatter重写整个提示框的内容。特别是在 5.0 后 ECharts 的提示框结构更复杂,样式更美观了,重写变得成本很大而且很难达到默认的效果。

因此在这个版本我们为 tooltip 新增了valueFormatter配置项用于数值部分的格式化。

还是刚才那个坐标轴对齐的例子,我们可以为提示框中的数值部分加上 °C 和 ml 的后缀。

option = {
  tooltip: {
    trigger: 'axis'
  },
  legend: {},
  xAxis: [
    {
      type: 'category',
      data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
      axisPointer: {
        type: 'shadow'
      }
    }
  ],
  yAxis: [
    {
      type: 'value',
      name: 'Precipitation',
      alignTicks: true,
      axisLabel: {
        formatter: '{value} ml'
      }
    },
    {
      type: 'value',
      name: 'Temperature',
      axisLabel: {
        formatter: '{value} °C'
      }
    }
  ],
  series: [
    {
      name: 'Evaporation',
      type: 'bar',
      tooltip: {
        valueFormatter: value => value + ' ml'
      },
      // prettier-ignore
      data: [2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20.0, 6.4, 3.3]
    },
    {
      name: 'Precipitation',
      type: 'bar',
      tooltip: {
        valueFormatter: value => value + ' ml'
      },
      // prettier-ignore
      data: [2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3]
    },
    {
      name: 'Temperature',
      type: 'line',
      yAxisIndex: 1,
      tooltip: {
        valueFormatter: value => value + ' °C'
      },
      data: [2.0, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3, 23.4, 23.0, 16.5, 12.0, 6.2]
    }
  ]
};
live

每个系列都可以根据自己的数值格式配置自己的valueFormatter

更灵活的扇区圆角

在 5.0 中我们为扇区新增了圆角的配置,可以让饼图,旭日图变得更有趣。之前圆角的配置只支持内半径和外半径分开配置,这次我们更进一步,支持扇区的四个角都配置成不同的圆角大小,带来更灵活的呈现。

option = {
  tooltip: {
    trigger: 'item'
  },
  legend: {
    top: '5%',
    left: 'center'
  },
  series: [
    {
      name: 'Access From',
      type: 'pie',
      radius: ['30%', '70%'],
      roseType: 'angle',
      itemStyle: {
        borderRadius: [20, 5, 5, 10],
        borderColor: '#fff',
        borderWidth: 2
      },
      label: {
        show: false
      },
      data: [
        { value: 800, name: 'Search Engine' },
        { value: 735, name: 'Direct' },
        { value: 580, name: 'Email' },
        { value: 484, name: 'Union Ads' },
        { value: 400, name: 'Video Ads' }
      ]
    }
  ]
};
live

饼图的复杂标签优化

饼图一直是 ECharts 中标签呈现最复杂的图表之一,我们从 5.0 开始就一直在饼图的标签布局、显示上做了很多的优化。

这次我们针对使用了换行,背景色,富文本等格式比较复杂的饼图标签做了深度的优化。在宽度的自适应、超出容器、引导线的计算上比之前有了更好的效果:

5.2.2 (Before) 5.3.0 (After)
before after
before after

柱状图 large 模式优化

在数据量很多(> 2k)的时候,我们支持柱状图通过开启 large 模式来加速渲染,提升交互性能,但是之前 large 模式下对柱状图布局比较简单,不支持多系列堆叠后的布局。在 5.3.0 中我们对 large 模式的布局进行了优化,跟普通模式保持了一致性。我们可以在更多的场景中通过开启 large 来优化柱状图的性能。

除此之外,优化后的柱状图布局也修复了在对数轴这样的非线性轴上堆叠效果不正确的 bug。

非兼容改动

registerMap 和 getMap 方法需要在引入地图组件后才能使用

为了减少最小打包的体积,我们从核心模块中移除了地图数据管理的方法getMapregisterMap

如果你是按需引入 ECharts 组件的话,需要保证先引入了GeoComponent或者MapChart之后,才能使用registerMap注册地图数据。

import * as echarts from 'echarts/core';
import { MapChart } from 'echarts/charts';

echarts.use([MapChart]);

// 必须在使用 use 方法注册了 MapChart 后才能使用 registerMap 注册地图
echarts.registerMap('world', worldJSON);

如果你是使用import * as echarts from 'echarts'全量引入,这次改动不会对你产生任何影响。

折线图移除默认高亮加粗的效果

我们在 5.0 里对折线图引入了默认高亮加粗的效果,但是社区反馈这个在很多场景效果并不好,所以在这个版本我们将这个效果从默认开启改为默认关闭,如果需要使用高亮加粗,则可以显示配置:

series = {
  type: 'line',
  //...
  emphasis: {
    lineStyle: {
      width: 'bolder'
    }
  }
};

本文贡献者 在 GitHub 上编辑本页

pissang pissangOvilia Oviliaplainheart plainheart