# Echarts应用

# 一、常用图表类型

  • 设置柱状图的方式,是将 seriestype 设为 'bar'
  • 设置折线图的方式,是将 seriestype 设为 'category'
  • 设置饼图的方式,是将 seriestype 设为 'pie'
  • 设置散点图的方式,是将 seriestype 设为 'scatter'

# 二、图表数据异步加载

# 1.数据异步

很多时候可能数据需要异步加载后再填入。ECharts 中实现异步数据的更新非常简单,在图表初始化后不管任何时候只要通过 jQuery 等工具异步获取数据后通过 setOption 填入数据和配置项就行。

var myChart = echarts.init(document.getElementById('main'));

$.get('data.json').done(function(data) {
  // data 的结构:
  // {
  //     categories: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"],
  //     values: [5, 20, 36, 10, 10, 20]
  // }
  myChart.setOption({
    title: {
      text: '异步数据加载示例'
    },
    tooltip: {},
    legend: {},
    xAxis: {
      data: data.categories
    },
    yAxis: {},
    series: [
      {
        name: '销量',
        type: 'bar',
        data: data.values
      }
    ]
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

也可以先设置图表样式显示空图标,等获取到数据后再展示

var myChart = echarts.init(document.getElementById('main'));

$.get('data.json').done(function(data) {
  // data 的结构:
  // {
  //     categories: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"],
  //     values: [5, 20, 36, 10, 10, 20]
  // }
  myChart.setOption({
    title: {
      text: '异步数据加载示例'
    },
    tooltip: {},
    legend: {},
    xAxis: {
      data: data.categories
    },
    yAxis: {},
    series: [
      {
        name: '销量',
        type: 'bar',
        data: data.values
      }
    ]
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 2.loading 动画

如果数据加载时间较长,一个空的坐标轴放在画布上也会让用户觉得是不是产生 bug 了,因此需要一个 loading 的动画来提示用户数据正在加载。

ECharts 默认有提供了一个简单的加载动画。只需要调用 showLoading (opens new window) 方法显示。数据加载完成后再调用 hideLoading (opens new window) 方法隐藏加载动画。

myChart.showLoading();
$.get('data.json').done(function (data) {
    myChart.hideLoading();
    myChart.setOption(...);
});
1
2
3
4
5

# 3.数据的动态更新

ECharts 由数据驱动,数据的改变驱动图表展现的改变,因此动态数据的实现也变得异常简单。

所有数据的更新都通过 setOption (opens new window)实现,你只需要定时获取数据,setOption (opens new window) 填入数据,而不用考虑数据到底产生了哪些变化,ECharts 会找到两组数据之间的差异然后通过合适的动画去表现数据的变化。

chart.setOption(option, { 
    notMerge: ...,
    lazyUpdate: ...,
    silent: ... 
});
1
2
3
4
5
  • notMerge 可选。是否不跟之前设置的 option 进行合并。默认为 false。即表示合并。合并的规则,详见 组件合并模式。如果为 true,表示所有组件都会被删除,然后根据新 option 创建所有新组件。
  • replaceMerge 可选。用户可以在这里指定一个或多个组件,如:xAxis, series,这些指定的组件会进行 replaceMerge。如果用户想删除部分组件,也可使用 replaceMerge。详见 组件合并模式
  • lazyUpdate 可选。在设置完 option 后是否不立即更新图表,默认为 false,即同步立即更新。如果为 true,则会在下一个 animation frame 中,才更新图表。
  • silent 可选。阻止调用 setOption 时抛出事件,默认为 false,即抛出事件。

# 三、富文本标签

Apache EChartsTM 中的文本标签从v3.7 开始支持富文本模式,能够:

  • 定制文本块整体的样式(如背景、边框、阴影等)、位置、旋转等。
  • 对文本块中个别片段定义样式(如颜色、字体、高宽、背景、阴影等)、对齐方式等。
  • 在文本中使用图片做小图标或者背景。
  • 特定组合以上的规则,可以做出简单表格、分割线等效果。

开始下面的介绍之前,先说明一下下面会使用的两个名词的含义:

  • 文本块(Text Block):文本标签块整体。
  • 文本片段(Text fragment):文本标签块中的部分文本。

# 1.文本样式相关的配置项

echarts 提供了丰富的文本标签配置项,包括:

  • 字体基本样式设置:fontStylefontWeightfontSizefontFamily
  • 文字颜色:color
  • 文字描边:textBorderColortextBorderWidth
  • 文字阴影:textShadowColortextShadowBlurtextShadowOffsetXtextShadowOffsetY
  • 文本块或文本片段大小:lineHeightwidthheightpadding
  • 文本块或文本片段的对齐:alignverticalAlign
  • 文本块或文本片段的边框、背景(颜色或图片):backgroundColorborderColorborderWidthborderRadius
  • 文本块或文本片段的阴影:shadowColorshadowBlurshadowOffsetXshadowOffsetY
  • 文本块的位置和旋转:positiondistancerotate

可以在各处的 rich 属性中定义文本片段样式。例如 series-bar.label.rich (opens new window)

注意:如果不定义 rich,不能指定文字块的 widthheight

# 2.文本、文本框、文本片段的基本样式和装饰

每个文本可以设置基本的字体样式:fontStylefontWeightfontSizefontFamily

可以设置文字的颜色 color 和边框的颜色 textBorderColortextBorderWidth

文本框可以设置边框和背景的样式:borderColorborderWidthbackgroundColorpadding

文本片段也可以设置边框和背景的样式:borderColorborderWidthbackgroundColorpadding

# 3.标签的位置

对于折线图、柱状图、散点图等,均可以使用 label 来设置标签。标签的相对于图形元素的位置,一般使用 label.position (opens new window)label.distance (opens new window) 来配置。

option = {
  series: [
    {
      type: 'scatter',
      symbolSize: 160,
      symbol: 'roundRect',
      data: [[1, 1]],
      label: {
        // 修改 position 和 distance 的值试试
        // 支持:'left', 'right', 'top', 'bottom', 'inside', 'insideTop', 'insideLeft', 'insideRight', 'insideBottom', 'insideTopLeft', 'insideTopRight', 'insideBottomLeft', 'insideBottomRight'
        position: 'top',
        distance: 10,

        show: true,
        formatter: ['Label Text'].join('\n'),
        backgroundColor: '#eee',
        borderColor: '#555',
        borderWidth: 2,
        borderRadius: 5,
        padding: 10,
        fontSize: 18,
        shadowBlur: 3,
        shadowColor: '#888',
        shadowOffsetX: 0,
        shadowOffsetY: 3,
        textBorderColor: '#000',
        textBorderWidth: 3,
        color: '#fff'
      }
    }
  ],
  xAxis: {
    max: 2
  },
  yAxis: {
    max: 2
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

注意:position 在不同的图中可取值有所不同。distance 并不是在每个图中都支持。详情请参见 option 文档 (opens new window)

# 4.标签的旋转

const labelOption = {
  show: true,
  rotate: 90,
  formatter: '{c}  {name|{a}}',
  fontSize: 16,
  rich: {
    name: {}
  }
};

option = {
  xAxis: [
    {
      type: 'category',
      data: ['2012', '2013', '2014', '2015', '2016']
    }
  ],
  yAxis: [
    {
      type: 'value'
    }
  ],
  series: [
    {
      name: 'Forest',
      type: 'bar',
      barGap: 0,
      label: labelOption,
      emphasis: {
        focus: 'series'
      },
      data: [320, 332, 301, 334, 390]
    },
    {
      name: 'Steppe',
      type: 'bar',
      label: labelOption,
      emphasis: {
        focus: 'series'
      },
      data: [220, 182, 191, 234, 290]
    }
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 5.文本片段的排版和对齐

关于排版方式,每个文本片段,可以想象成 CSS 中的 inline-block,在文档流中按行放置。

每个文本片段的内容盒尺寸(content box size),默认是根据文字大小决定的。但是,也可以设置 widthheight 来强制指定,虽然一般不会这么做(参见下文)。文本片段的边框盒尺寸(border box size),由上述本身尺寸,加上文本片段的 padding 来得到。

只有 '\n' 是换行符,能导致换行。

一行内,会有多个文本片段。每行的实际高度,由 lineHeight 最大的文本片段决定。文本片段的 lineHeight 可直接在 rich 中指定,也可以在 rich 的父层级中统一指定而采用到 rich 的所有项中,如果都不指定,则取文本片段的边框盒尺寸(border box size)。

在一行的 lineHeight 被决定后,一行内,文本片段的竖直位置,由文本片段的 verticalAlign 来指定(这里和 CSS 中的规则稍有不同):

  • 'bottom':文本片段的盒的底边贴住行底。
  • 'top':文本片段的盒的顶边贴住行顶。
  • 'middle':居行中。

文本块的宽度,可以直接由文本块的 width 指定,否则,由最长的行决定。宽度决定后,在一行中进行文本片段的放置。文本片段的 align 决定了文本片段在行中的水平位置:

  • 首先,从左向右连续紧靠放置 align'left' 的文本片段盒。
  • 然后,从右向左连续紧靠放置 align'right' 的文本片段盒。
  • 最后,剩余的没处理的文本片段盒,紧贴着,在中间剩余的区域中居中放置。

关于文字在文本片段盒中的位置:

  • 如果 align'center',则文字在文本片段盒中是居中的。
  • 如果 align'left',则文字在文本片段盒中是居左的。
  • 如果 align'right',则文字在文本片段盒中是居右的。

# 6.特殊效果:图标、分割线、标题块、简单表格

文本片段的 backgroundColor 可以指定为图片后,就可以在文本中使用图标了:

labelOption = {
  rich: {
    Sunny: {
      // 这样设定 backgroundColor 就可以是图片了。
      backgroundColor: {
        image: './data/asset/img/weather/sunny_128.png'
      },
      // 可以只指定图片的高度,从而图片的宽度根据图片的长宽比自动得到。
      height: 30
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12

分割线实际是用 border 实现的:

labelOption = {
  rich: {
    hr: {
      borderColor: '#777',
      // 这里把 width 设置为 '100%',表示分割线的长度充满文本块。
      // 注意,这里是文本块内容盒(content box)的 100%,而不包含 padding。
      // 虽然这和 CSS 相关的定义有所不同,但是在这类场景中更加方便。
      width: '100%',
      borderWidth: 0.5,
      height: 0
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13

标题块是使用 backgroundColor 实现的:

labelOption = {
  // 标题文字居左
  formatter: '{titleBg|Left Title}',
  rich: {
    titleBg: {
      backgroundColor: '#000',
      height: 30,
      borderRadius: [5, 5, 0, 0],
      padding: [0, 10, 0, 10],
      width: '100%',
      color: '#eee'
    }
  }
};

// 标题文字居中。
// 这个实现有些 tricky,但是,能够不引入更复杂的排版规则而实现这个效果。
labelOption = {
  formatter: '{tc|Center Title}{titleBg|}',
  rich: {
    titleBg: {
      align: 'right',
      backgroundColor: '#000',
      height: 30,
      borderRadius: [5, 5, 0, 0],
      padding: [0, 10, 0, 10],
      width: '100%',
      color: '#eee'
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 四、基础的过渡动画

Apache EChartsTM 中使用了平移,缩放,变形等形式的过渡动画让数据的添加更新删除,以及用户的交互变得更加顺滑。通常情况下开发者不需要操心该如何去使用动画,只需要按自己的需求使用setOption更新数据,ECharts 就会找出跟上一次数据之间的区别,然后自动应用最合适的过渡动画。

# 1.过渡动画的配置

因为数据添加和数据更新往往会需要不一样的动画效果,比如我们会期望数据更新动画的时长更短,因此 ECharts 区分了这两者的动画配置:

  • 对于新添加的数据,我们会应用入场动画,通过animationDuration, animationEasing, animationDelay三个配置项分别配置动画的时长,缓动以及延时。
  • 对于数据更新,我们会应用更新动画,通过animationDurationUpdate, animationEasingUpdate, animationDelayUpdate三个配置项分别配置动画的时长,缓动以及延时。

可以看到,更新动画配置是入场动画配置加上了Update的后缀。

ECharts 中每次 setOption 的更新,数据会跟上一次更新的数据做对比,然后根据对比结果分别为数据执行三种状态:添加,更新以及移除。这个比对是根据数据的name来决定的,例如上一次更新数据有三个name'A', 'B', 'C'的数据,而新更新的数据变为了'B', 'C', 'D'的数据,则数据'B', 'C'会被执行更新,数据'A'会被移除,而数据'D'会被添加。如果是第一次更新因为没有旧数据,所以所有数据都会被执行添加。根据这三种状态 ECharts 会分别应用相应的入场动画,更新动画以及移除动画。

所有这些配置都可以分别设置在option最顶层对所有系列和组件生效,也可以分别为每个系列配置。

如果我们想要关闭动画,可以直接设置option.animationfalse

# 动画时长

animationDurationanimationDurationUpdate用于设置动画的时长,单位为ms,设置较长的动画时长可以让用户更清晰的看到过渡动画的效果,但是我们也需要小心过长的时间会让用户再等待的过程中失去耐心。

设置为0会关闭动画,在我们只想要单独关闭入场动画或者更新动画的时候可以通过单独将相应的配置设置为0来实现。

# 动画缓动

animationEasinganimationEasingUpdate两个配置项用于设置动画的缓动函数,缓动函数是一个输入动画时间,输出动画进度的函数:

(t: number) => number;
1

ECharts 里内置了缓入'cubicIn',缓出'cubicOut'等常见的动画缓动函数,我们可以直接通过名字来声明使用这些缓动函数。

内置缓动函数:

img

# 延时触发

animationDelayanimationDelayUpdate用于设置动画延迟开始的时间,通常我们会使用回调函数将不同数据设置不同的延时来实现交错动画的效果

var xAxisData = [];
var data1 = [];
var data2 = [];
for (var i = 0; i < 100; i++) {
  xAxisData.push('A' + i);
  data1.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5);
  data2.push((Math.cos(i / 5) * (i / 5 - 10) + i / 6) * 5);
}
option = {
  legend: {
    data: ['bar', 'bar2']
  },
  xAxis: {
    data: xAxisData,
    splitLine: {
      show: false
    }
  },
  yAxis: {},
  series: [
    {
      name: 'bar',
      type: 'bar',
      data: data1,
      emphasis: {
        focus: 'series'
      },
      animationDelay: function(idx) {
        return idx * 10;
      }
    },
    {
      name: 'bar2',
      type: 'bar',
      data: data2,
      emphasis: {
        focus: 'series'
      },
      animationDelay: function(idx) {
        return idx * 10 + 100;
      }
    }
  ],
  animationEasing: 'elasticOut',
  animationDelayUpdate: function(idx) {
    return idx * 5;
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 2.动画的性能优化

在数据量特别大的时候,为图形应用动画可能会导致应用的卡顿,这个时候我们可以设置animation: false关闭动画。

对于数据量会动态变化的图表,我们更推荐使用animationThreshold这个配置项,当画布中图形数量超过这个阈值的时候,ECharts 会自动关闭动画来提升绘制性能。这个配置往往是一个经验值,通常 ECharts 的性能足够实时渲染上千个图形的动画(我们默认值也是给了 2000),但是如果你的图表很复杂,或者你的用户环境比较恶劣,页面中又同时会运行很多其它复杂的代码,也可以适当的下调这个值保证整个应用的流畅性。

# 3.监听动画结束

有时候我们想要获取当前渲染的结果,如果没有使用动画,我们在setOption之后 ECharts 就会直接执行渲染,我们可以同步的通过getDataURL方法获取渲染得到的结果。

const chart = echarts.init(dom);
chart.setOption({
  animation: false
  //...
});
// 可以直接同步执行
const dataUrl = chart.getDataURL();
1
2
3
4
5
6
7

但是如果图表中有动画,马上执行getDataURL得到的是动画刚开始的画面,而非最终展示的结果。因此我们需要知道动画结束然后再执行getDataURL得到结果。

假如你确定动画的时长,一种比较简单粗暴的方式是根据动画时长来执行setTimeout延迟执行:

chart.setOption({
  animationDuration: 1000
  //...
});
setTimeout(() => {
  const dataUrl = chart.getDataURL();
}, 1000);
1
2
3
4
5
6
7

或者我们也可以使用 ECharts 提供的rendered事件来判断 ECharts 已经动画结束停止了渲染

chart.setOption({
  animationDuration: 1000
  //...
});

function onRendered() {
  const dataUrl = chart.getDataURL();
  // ...
  // 后续如果有交互,交互发生重绘也会触发该事件,因此使用完就需要移除
  chart.off('rendered', onRendered);
}
chart.on('rendered', onRendered);
1
2
3
4
5
6
7
8
9
10
11
12

# 五、交互

# 1.拖拽的实现

这个例子主要做到了这样一件事,用鼠标可以拖拽曲线的点,从而改变曲线的形状。例子很简单,但是有了这个基础我们还可以做更多的事情,比如在图中进行可视化得编辑。所以我们从这个简单的例子开始。

echarts 本身没有提供封装好的“拖拽改变图表”这样比较业务定制的功能。但是这个功能开发者可以通过 API 扩展实现。

# 实现基本的拖拽功能

在这个例子中,基础的图表是一个 折线图 (series-line) (opens new window)。参见如下配置:

var symbolSize = 20;

// 这个 data 变量在这里单独声明,在后面也会用到。
var data = [
  [15, 0],
  [-50, 10],
  [-56.5, 20],
  [-46.5, 30],
  [-22.1, 40]
];

myChart.setOption({
  xAxis: {
    min: -100,
    max: 80,
    type: 'value',
    axisLine: { onZero: false }
  },
  yAxis: {
    min: -30,
    max: 60,
    type: 'value',
    axisLine: { onZero: false }
  },
  series: [
    {
      id: 'a',
      type: 'line',
      smooth: true,
      symbolSize: symbolSize, // 为了方便拖拽,把 symbolSize 尺寸设大了。
      data: data
    }
  ]
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

既然折线中原生的点没有拖拽功能,我们就为它加上拖拽功能:用 graphic (opens new window) 组件,在每个点上面,覆盖一个隐藏的可拖拽的圆点。

myChart.setOption({
  // 声明一个 graphic component,里面有若干个 type 为 'circle' 的 graphic elements。
  // 这里使用了 echarts.util.map 这个帮助方法,其行为和 Array.prototype.map 一样,但是兼容 es5 以下的环境。
  // 用 map 方法遍历 data 的每项,为每项生成一个圆点。
  graphic: echarts.util.map(data, function(dataItem, dataIndex) {
    return {
      // 'circle' 表示这个 graphic element 的类型是圆点。
      type: 'circle',

      shape: {
        // 圆点的半径。
        r: symbolSize / 2
      },
      // 用 transform 的方式对圆点进行定位。position: [x, y] 表示将圆点平移到 [x, y] 位置。
      // 这里使用了 convertToPixel 这个 API 来得到每个圆点的位置,下面介绍。
      position: myChart.convertToPixel('grid', dataItem),

      // 这个属性让圆点不可见(但是不影响他响应鼠标事件)。
      invisible: true,
      // 这个属性让圆点可以被拖拽。
      draggable: true,
      // 把 z 值设得比较大,表示这个圆点在最上方,能覆盖住已有的折线图的圆点。
      z: 100,
      // 此圆点的拖拽的响应事件,在拖拽过程中会不断被触发。下面介绍详情。
      // 这里使用了 echarts.util.curry 这个帮助方法,意思是生成一个与 onPointDragging
      // 功能一样的新的函数,只不过第一个参数永远为此时传入的 dataIndex 的值。
      ondrag: echarts.util.curry(onPointDragging, dataIndex)
    };
  })
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

上面的代码中,使用 convertToPixel (opens new window) 这个 API,进行了从 data 到“像素坐标”的转换,从而得到了每个圆点应该在的位置,从而能绘制这些圆点。myChart.convertToPixel('grid', dataItem) 这句话中,第一个参数 'grid' 表示 dataItemgrid (opens new window) 这个组件中(即直角坐标系)中进行转换。所谓“像素坐标”,就是以 echarts 容器 dom element 的左上角为零点的以像素为单位的坐标系中的坐标。

注意这件事需要在第一次 setOption 后再进行,也就是说,须在坐标系(grid (opens new window))初始化后才能调用 myChart.convertToPixel('grid', dataItem)

有了这段代码后,就有了诸个能拖拽的点。接下来要为每个点,加上拖拽响应的事件:

// 拖拽某个圆点的过程中会不断调用此函数。
// 此函数中会根据拖拽后的新位置,改变 data 中的值,并用新的 data 值,重绘折线图,从而使折线图同步于被拖拽的隐藏圆点。
function onPointDragging(dataIndex) {
  // 这里的 data 就是本文最初的代码块中声明的 data,在这里会被更新。
  // 这里的 this 就是被拖拽的圆点。this.position 就是圆点当前的位置。
  data[dataIndex] = myChart.convertFromPixel('grid', this.position);
  // 用更新后的 data,重绘折线图。
  myChart.setOption({
    series: [
      {
        id: 'a',
        data: data
      }
    ]
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面的代码中,使用了 convertFromPixel (opens new window) 这个 API。它是 convertToPixel (opens new window) 的逆向过程。myChart.convertFromPixel('grid', this.position) 表示把当前像素坐标转换成 grid (opens new window) 组件中直角坐标系的 dataItem 值。

最后,为了使 dom 尺寸改变时,图中的元素能自适应得变化,加上这些代码:

window.addEventListener('resize', function() {
  // 对每个拖拽圆点重新计算位置,并用 setOption 更新。
  myChart.setOption({
    graphic: echarts.util.map(data, function(item, dataIndex) {
      return {
        position: myChart.convertToPixel('grid', item)
      };
    })
  });
});
1
2
3
4
5
6
7
8
9
10

# 添加 tooltip 组件

到此,拖拽的基本功能就完成了。但是想要更进一步得实时看到拖拽过程中,被拖拽的点的 data 值的变化状况,我们可以使用 tooltip (opens new window) 组件来实时显示这个值。但是,tooltip 有其默认的“显示”“隐藏”触发规则,在我们拖拽的场景中并不适用,所以我们还要手动定制 tooltip 的“显示”“隐藏”行为。

在上述代码中分别添加如下定义:

myChart.setOption({
  // ...,
  tooltip: {
    // 表示不使用默认的“显示”“隐藏”触发规则。
    triggerOn: 'none',
    formatter: function(params) {
      return (
        'X: ' +
        params.data[0].toFixed(2) +
        '<br>Y: ' +
        params.data[1].toFixed(2)
      );
    }
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
myChart.setOption({
  graphic: data.map(function(item, dataIndex) {
    return {
      type: 'circle',
      // ...,
      // 在 mouseover 的时候显示,在 mouseout 的时候隐藏。
      onmousemove: echarts.util.curry(showTooltip, dataIndex),
      onmouseout: echarts.util.curry(hideTooltip, dataIndex)
    };
  })
});

function showTooltip(dataIndex) {
  myChart.dispatchAction({
    type: 'showTip',
    seriesIndex: 0,
    dataIndex: dataIndex
  });
}

function hideTooltip(dataIndex) {
  myChart.dispatchAction({
    type: 'hideTip'
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

这里使用了 dispatchAction (opens new window) 来显示隐藏 tooltip。用到了 showTip (opens new window)hideTip (opens new window)

# 全部代码

总结一下,全部的代码如下:

import echarts from 'echarts';

var symbolSize = 20;
var data = [
  [15, 0],
  [-50, 10],
  [-56.5, 20],
  [-46.5, 30],
  [-22.1, 40]
];
var myChart = echarts.init(document.getElementById('main'));
myChart.setOption({
  tooltip: {
    triggerOn: 'none',
    formatter: function(params) {
      return (
        'X: ' +
        params.data[0].toFixed(2) +
        '<br />Y: ' +
        params.data[1].toFixed(2)
      );
    }
  },
  xAxis: { min: -100, max: 80, type: 'value', axisLine: { onZero: false } },
  yAxis: { min: -30, max: 60, type: 'value', axisLine: { onZero: false } },
  series: [
    { id: 'a', type: 'line', smooth: true, symbolSize: symbolSize, data: data }
  ]
});
myChart.setOption({
  graphic: echarts.util.map(data, function(item, dataIndex) {
    return {
      type: 'circle',
      position: myChart.convertToPixel('grid', item),
      shape: { r: symbolSize / 2 },
      invisible: true,
      draggable: true,
      ondrag: echarts.util.curry(onPointDragging, dataIndex),
      onmousemove: echarts.util.curry(showTooltip, dataIndex),
      onmouseout: echarts.util.curry(hideTooltip, dataIndex),
      z: 100
    };
  })
});
window.addEventListener('resize', function() {
  myChart.setOption({
    graphic: echarts.util.map(data, function(item, dataIndex) {
      return { position: myChart.convertToPixel('grid', item) };
    })
  });
});
function showTooltip(dataIndex) {
  myChart.dispatchAction({
    type: 'showTip',
    seriesIndex: 0,
    dataIndex: dataIndex
  });
}
function hideTooltip(dataIndex) {
  myChart.dispatchAction({ type: 'hideTip' });
}
function onPointDragging(dataIndex, dx, dy) {
  data[dataIndex] = myChart.convertFromPixel('grid', this.position);
  myChart.setOption({
    series: [
      {
        id: 'a',
        data: data
      }
    ]
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

有了这些基础,就可以定制更多的功能了。可以加 dataZoom (opens new window) 组件,可以制作一个直角坐标系上的绘图板等等。可以发挥想象力。

# 2.智能指针吸附

在图表中,部分可交互元素可能比较小,有时候用户不易准确地进行点击等操作,移动端尤其如此。因此,在 Apache EChartsTM 5.4.0 中,我们引入了“智能指针吸附”的概念。

从该版本起,在默认情况下,ECharts 对移动端的图表开启指针吸附,对非移动端的图表关闭。

如果需要对所有平台都开启,则可以通过在 init (opens new window) 的时候将 opt.useCoarsePointer 设置为 true 来实现;设为 false 则对所有平台都关闭。

# 吸附原理

在鼠标或触摸事件发生时,ECharts 会根据鼠标或触摸的位置,判断是否和某个可交互元素相交。如果是,则认为该元素是交互对象(与优化前的逻辑一致);如果不是,则在一定范围内找到最接近鼠标或触摸位置的一个元素。

默认的范围是 44px(参见 W3C 标准 (opens new window)),开发者可在 init (opens new window) 的时候,通过 opt.pointerSize 设置该值。

更具体地说,ECharts 会在鼠标或触摸位置的周围,依次循环不同角度和不同半径(在 opt.pointerSize 范围内),直到找到一个元素与其相交。如果找到了,则认为该元素是交互对象。

img

也就是说,如果有元素在鼠标或触摸位置的 opt.pointerSize 半径范围内,则最靠近的可交互元素会被认为是交互对象。

# 性能分析

在实际算法实现的时候,我们首先将鼠标或触摸位置与所有可交互元素的 AABB 包围盒判断相交性,从而快速剔除了大部分不相交的元素。然后,我们再对剩余的元素进行精确的路径相交判断。因此,从用户体验角度,不会带来可感知的性能损耗。

对于大规模数据的图表系列(也就是开启了 large: true 的柱状图、散点图等),不会开启吸附功能。

# 六、渲染器

一般来说,Canvas 更适合绘制图形元素数量较多(这一般是由数据量大导致)的图表(如热力图、地理坐标系或平行坐标系上的大规模线图或散点图等),也利于实现某些视觉 特效 (opens new window)。但是,在不少场景中,SVG 具有重要的优势:它的内存占用更低(这对移动端尤其重要)、并且用户使用浏览器内置的缩放功能时不会模糊。

选择哪种渲染器,我们可以根据软硬件环境、数据量、功能需求综合考虑。

  • 在软硬件环境较好,数据量不大的场景下,两种渲染器都可以适用,并不需要太多纠结。
  • 在环境较差,出现性能问题需要优化的场景下,可以通过试验来确定使用哪种渲染器。比如有这些经验:
    • 在需要创建很多 ECharts 实例且浏览器易崩溃的情况下(可能是因为 Canvas 数量多导致内存占用超出手机承受能力),可以使用 SVG 渲染器来进行改善。大略的说,如果图表运行在低端安卓机,或者我们在使用一些特定图表如 水球图 (opens new window) 等,SVG 渲染器可能效果更好。
    • 数据量较大(经验判断 > 1k)、较多交互时,建议选择 Canvas 渲染器。

我们强烈欢迎开发者们反馈 (opens new window)给我们使用的体验和场景,帮助我们更好的做优化。

注:当前某些特殊的渲染依然需要依赖 Canvas:如炫光尾迹特效 (opens new window)带有混合效果的热力图 (opens new window)等。

我们在 v5.3.0 (opens new window) 中使用虚拟 DOM 技术对 SVG渲染器进行了重构,从而使其渲染性能提升了 2~10 倍,在某些特殊场景中甚至能有数十倍的提升!参见 #836 (opens new window)

如果是用如下的方式完整引入echarts,代码中已经包含了 SVG 渲染器和 Canvas 渲染器

import * as echarts from 'echarts';
1

如果你是按照 在项目中引入 Apache ECharts (opens new window) 一文中的介绍使用按需引入,则需要手动引入需要的渲染器

import * as echarts from 'echarts/core';
// 可以根据需要选用只用到的渲染器
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';

echarts.use([SVGRenderer, CanvasRenderer]);
1
2
3
4
5

然后,我们就可以在代码中,初始化图表实例时,传入参数 (opens new window) 选择渲染器类型:

// 使用 Canvas 渲染器(默认)
var chart = echarts.init(containerDom, null, { renderer: 'canvas' });
// 等价于:
var chart = echarts.init(containerDom);

// 使用 SVG 渲染器
var chart = echarts.init(containerDom, null, { renderer: 'svg' });
1
2
3
4
5
6
7
Last Updated: 3/21/2023, 8:58:05 PM