-
跨平台方案
-
服务端渲染
-
服务端 SVG 渲染
- 5.3.0 里新引入了零依赖的服务端 SVG 字符串渲染方案:
// 服务端代码 const echarts = require('echarts');// 在 SSR 模式下第一个参数不需要再传入 DOM 对象 let chart = echarts.init(null, null, {renderer: 'svg', // 必须使用 SVG 模式ssr: true, // 开启 SSRwidth: 400, // 需要指明高和宽height: 300 });// 像正常使用一样 setOption chart.setOption({//... });// 输出字符串 const svgStr = chart.renderToSVGString();// 如果不再需要图表,调用 dispose 以释放内存 chart.dispose(); chart = null;
- 整体使用的代码结构跟在浏览器中使用一样,首先是init初始化一个图表实例,然后通过setOption设置图表的配置项。但是init传入的参数会跟在跟浏览器中使用有所不同:
首先因为在服务端会采用字符串拼接的方式来渲染得到 SVG,我们并不需要容器来展示渲染的内容,所以我们可以在init的时候第一个container参数传入null或者undefined。
然后我们在init的第三个参数中,我们需要通过显示指定ssr: true来告诉 ECharts 我们需要开启服务端渲染的模式,该模式下 ECharts 会关闭动画循环的模块以及事件交互的模块。
在服务端渲染中我们也必须要通过width和height显示的指定图表的高和宽,因此如果你的图表是需要根据容器大小自适应的话,可能需要思考一下服务端渲染是否适合你的场景了。一种可能的解决方案是,首屏获取到图表容器大小后,请求服务端渲染图表,然后在客户端渲染图表;当用户交互改变容器大小时,重新请求服务端渲染。
在浏览器中我们在setOption完之后 ECharts 就会自动进行渲染将结果绘制到页面中,后续也会在每一帧判断是否有动画需要进行重绘。Node.js 中我们在设置了ssr: true后则没有这个过程。取而代之我们使用了renderToSVGString,将当前的图表渲染到 SVG 字符串,进一步得再通过 HTTP Response 返回给前端或者缓存到本地。
HTTP Response 返回给前端(这里以 Express.js 为例):res.writeHead(200, {'Content-Type': 'application/xml' }); res.write(svgStr); // svgStr 是上面 chart.renderToSVGString() 得到的字符串 res.end();
- 或者保存到本地:
fs.writeFile('bar.svg', svgStr, 'utf-8');
- 服务端渲染中的动画效果
上面的例子中可以看到,就算是服务端渲染 ECharts 也可以提供动画效果,这个动画效果是通过在输出的 SVG 字符串中嵌入 CSS 动画实现的。并不需要额外的 JavaScript 再去控制动画。
但是,也因为 CSS 动画的局限性,我们没法在服务端渲染中实现一些更灵活的动画功能,诸如柱状图排序动画,标签动画,路径图的特效动画等。部分系列诸如饼图的动画效果也为服务端渲染做了特殊的优化。
如果你不希望有这个动画效果,可以在setOption的时候通过animation: false关闭动画。setOption({animation: false });
- 5.3.0 里新引入了零依赖的服务端 SVG 字符串渲染方案:
-
服务端 Canvas 渲染
- 如果你希望输出的是一张图片而非 SVG 字符串,或者你还在使用更老的版本,我们会推荐使用 node-canvas 来实现 ECharts 的服务渲染,node-canvas 是在 Node.js 上的一套 Canvas 实现,它提供了跟浏览器中 Canvas 几乎一致的接口。
下面是一个简单的例子var echarts = require('echarts'); const { createCanvas } = require('canvas');// 在 5.3.0 之前的版本中,你必须要通过该接口注册 canvas 实例创建方法。 // 从 5.3.0 开始就不需要了 echarts.setCanvasCreator(() => {return createCanvas(); });const canvas = createCanvas(800, 600); // ECharts 可以直接使用 node-canvas 创建的 Canvas 实例作为容器 let chart = echarts.init(canvas);// 像正常使用一样 setOption chart.setOption({//... });const buffer = renderChart().toBuffer('image/png');// 如果不再需要图表,调用 dispose 以释放内存 chart.dispose(); chart = null;// 通过 Response 输出 PNG 图片 res.writeHead(200, {'Content-Type': 'image/png' }); res.write(buffer); res.end();
- 图片的加载
- node-canvas 提供了图片加载的Image实现,如果你在图表中使用了到了图片,我们可以使用5.3.0新增的setPlatformAPI接口来适配。
echarts.setPlatformAPI({// 同老版本的 setCanvasCreatorcreateCanvas() {return createCanvas();},loadImage(src, onload, onerror) {const img = new Image();// 必须要绑定 this context.img.onload = onload.bind(img);img.onerror = onerror.bind(img);img.src = src;return img;} });
- 如果你的图片是需要远程获取的,我们建议你通过 http 请求先预取该图片得到base64之后再作为图片的 URL 传入,这样可以保证在 Response 输出的时候图片是加载完成的。
- node-canvas 提供了图片加载的Image实现,如果你在图表中使用了到了图片,我们可以使用5.3.0新增的setPlatformAPI接口来适配。
- 如果你希望输出的是一张图片而非 SVG 字符串,或者你还在使用更老的版本,我们会推荐使用 node-canvas 来实现 ECharts 的服务渲染,node-canvas 是在 Node.js 上的一套 Canvas 实现,它提供了跟浏览器中 Canvas 几乎一致的接口。
-
客户端二次渲染
-
客户端懒加载完整 ECharts
- 最新版本的 ECharts 服务端 SVG 渲染除了完成图表的渲染外,支持的功能包括:
图表初始动画(例如:柱状图初始化时的柱子上升动画)
高亮样式(例如:鼠标移动到柱状图柱子上时的高亮效果)
但仅使用服务端渲染无法支持的功能包括:
动态改变数据
点击图例切换系列是否显示
移动鼠标显示提示框
其他交互相关的功能
如果有相关需求,可以考虑先使用服务端渲染快速输出首屏图表,然后等待 echarts.js 加载完后,重新在客户端渲染同样的图表(称为 Hydration),这样就可以实现正常的交互效果和动态改变数据了。需要注意的是,在客户端渲染的时候,应开启 tooltip: { show: true } 之类的交互组件,并且用 animation: 0 关闭初始动画(初始动画应由服务端渲染结果的 SVG 动画完成)。
从用户体验的角度,几乎感受不到二次渲染的过程,整个切换效果是非常无缝衔接的。你也可以像上面的例子中一样,在加载 echarts.js 的过程中使用 pace-js 之类的库实现显示加载进度条的效果,来解决 ECharts 尚未完全加载完之前没有交互反馈的问题。
使用服务端渲染 SVG 加上客户端 ECharts 懒加载的方式,其优点是,能够在首屏快速展示图表,而懒加载完成后可以实现所有 ECharts 的功能和交互;而缺点是,懒加载完整的 ECharts 需要一定时间,在加载完成前无法实现除高亮之外的用户交互(在这种情况下,开发者可以通过显示“加载中”来解决无交互反馈带来的困惑)。这个方案也是目前比较推荐的对首屏加载时间敏感,对功能交互完整性要求高的方案。
- 最新版本的 ECharts 服务端 SVG 渲染除了完成图表的渲染外,支持的功能包括:
-
客户端轻量运行时
- 方案一给出了实现完整交互的方案,但是有些场景下,我们并不需要很复杂的交互,只是希望在服务端渲染的基础上,能够在客户端进行一些简单的交互,例如:点击图例切换系列是否显示。这种情况下,我们能否不在客户端加载至少需要几百 KB 的 ECharts 代码呢?
从 v5.5.0 版本起,如果图表只需要以下效果和交互,可以通过服务端 SVG 渲染 + 客户端轻量运行时来实现:
图表初始动画(实现原理:服务端渲染的 SVG 带有 CSS 动画)
高亮样式(实现原理:服务端渲染的 SVG 带有 CSS 动画)
动态改变数据(实现原理:轻量运行时请求服务器进行二次渲染)
点击图例切换系列是否显示(实现原理:轻量运行时请求服务器进行二次渲染)<div id="chart-container" style="width:800px;height:600px"></div><script src="https://cdn.jsdelivr.net/npm/echarts/ssr/client/dist/index.min.js"></script> <script> const ssrClient = window['echarts-ssr-client'];let isSeriesShown = {a: true,b: true };function updateChart(svgStr) {const container = document.getElementById('chart-container');container.innerHTML = svgStr;// 使用轻量运行时赋予图表交互能力ssrClient.hydrate(main, {on: {click: (params) => {if (params.ssrType === 'legend') {// 点击图例元素,请求服务器进行二次渲染isSeriesShown[params.seriesName] = !isSeriesShown[params.seriesName];fetch('...?series=' + JSON.stringify(isSeriesShown)).then(res => res.text()).then(svgStr => {updateChart(svgStr);});}}}}); }// 通过 AJAX 请求获取服务端渲染的 SVG 字符串 fetch('...').then(res => res.text()).then(svgStr => {updateChart(svgStr);}); </script>
- 服务器端根据客户端传来的每个系列是否显示的信息(isSeriesShown)进行二次渲染,返回新的 SVG 字符串。服务端代码同上文,不再赘述。使用服务端 SVG 渲染加上客户端轻量运行时的方式,其优点是,客户端不再需要加载几百 KB 的 ECharts 代码,只需要加载一个不到 4KB 的轻量运行时代码;并且从用户体验的角度牺牲很少(支持初始动画、鼠标高亮)。而缺点是,需要一定的开发成本来维护额外的状态信息,并且无法支持实时性要求高的交互(例如移动鼠标显示提示框)。总体来说,推荐在对代码体积有非常严格要求的环境使用。
- 方案一给出了实现完整交互的方案,但是有些场景下,我们并不需要很复杂的交互,只是希望在服务端渲染的基础上,能够在客户端进行一些简单的交互,例如:点击图例切换系列是否显示。这种情况下,我们能否不在客户端加载至少需要几百 KB 的 ECharts 代码呢?
-
使用轻量运行时
- 客户端轻量运行时通过将服务端渲染的 SVG 图表进行理解,从而赋予图表一定的交互能力。可以通过以下方式引入客户端轻量运行时:
<!-- 方法一:使用 CDN --> <script src="https://cdn.jsdelivr.net/npm/echarts/ssr/client/dist/index.min.js"></script> <!-- 方法二:使用 NPM --> <script src="node_modules/echarts/ssr/client/dist/index.js"></script>
- 客户端轻量运行时通过将服务端渲染的 SVG 图表进行理解,从而赋予图表一定的交互能力。可以通过以下方式引入客户端轻量运行时:
-
API
- 在全局变量 window['echarts-ssr-client'] 中提供了以下 API:
hydrate(dom: HTMLElement, options: ECSSRClientOptions)
dom:图表容器,其内部的内容在调用本方法前应已设为服务端渲染的 SVG 图表
options:配置项
ECSSRClientOptionson?: {mouseover?: (params: ECSSRClientEventParams) => void,mouseout?: (params: ECSSRClientEventParams) => void,click?: (params: ECSSRClientEventParams) => void }
- 和图表鼠标事件一样,这里的时间都是针对图表数据对象的(例如:柱状图的柱子、折线图的数据点等),而不是针对图表容器的。ECSSRClientEventParams
{type: 'mouseover' | 'mouseout' | 'click';ssrType: 'legend' | 'chart';seriesIndex?: number;dataIndex?: number;event: Event; }
- type:事件类型
ssrType:事件对象类型,legend 表示图例数据,chart 表示图表数据对象
seriesIndex:系列索引
dataIndex:数据索引
event:原生事件对象
- 在全局变量 window['echarts-ssr-client'] 中提供了以下 API:
-
-
附表
-
-
-
数据处理
-
动态的异步数据
-
异步加载
- 入门示例中的数据是在初始化后setOption中直接填入的,但是很多时候可能数据需要异步加载后再填入。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}]}); });
- 或者先设置完其它的样式,显示一个空的直角坐标轴,然后获取数据后填入数据。
var myChart = echarts.init(document.getElementById('main')); // 显示标题,图例和空的坐标轴 myChart.setOption({title: {text: '异步数据加载示例'},tooltip: {},legend: {data: ['销量']},xAxis: {data: []},yAxis: {},series: [{name: '销量',type: 'bar',data: []}] });// 异步加载数据 $.get('data.json').done(function(data) {// 填入数据myChart.setOption({xAxis: {data: data.categories},series: [{// 根据名字对应到相应的系列name: '销量',data: data.data}]}); });
- 如下:
- ECharts 中在更新数据的时候需要通过name属性对应到相应的系列,上面示例中如果name不存在也可以根据系列的顺序正常更新,但是更多时候推荐更新数据的时候加上系列的name数据。
- 入门示例中的数据是在初始化后setOption中直接填入的,但是很多时候可能数据需要异步加载后再填入。ECharts 中实现异步数据的更新非常简单,在图表初始化后不管任何时候只要通过 jQuery 等工具异步获取数据后通过 setOption 填入数据和配置项就行。
-
loading 动画
- 如果数据加载时间较长,一个空的坐标轴放在画布上也会让用户觉得是不是产生 bug 了,因此需要一个 loading 的动画来提示用户数据正在加载。ECharts 默认有提供了一个简单的加载动画。只需要调用 showLoading 方法显示。数据加载完成后再调用 hideLoading 方法隐藏加载动画。
myChart.showLoading(); $.get('data.json').done(function (data) {myChart.hideLoading();myChart.setOption(...); });
- 如果数据加载时间较长,一个空的坐标轴放在画布上也会让用户觉得是不是产生 bug 了,因此需要一个 loading 的动画来提示用户数据正在加载。ECharts 默认有提供了一个简单的加载动画。只需要调用 showLoading 方法显示。数据加载完成后再调用 hideLoading 方法隐藏加载动画。
-
数据的动态更新
- ECharts 由数据驱动,数据的改变驱动图表展现的改变,因此动态数据的实现也变得异常简单。
所有数据的更新都通过 setOption实现,你只需要定时获取数据,setOption 填入数据,而不用考虑数据到底产生了哪些变化,ECharts 会找到两组数据之间的差异然后通过合适的动画去表现数据的变化。
- ECharts 由数据驱动,数据的改变驱动图表展现的改变,因此动态数据的实现也变得异常简单。
-
-
-
标签
-
富文本标签
-
文本样式相关的配置项
- echarts 提供了丰富的文本标签配置项,包括:
字体基本样式设置:fontStyle、fontWeight、fontSize、fontFamily。
文字颜色:color。
文字描边:textBorderColor、textBorderWidth。
文字阴影:textShadowColor、textShadowBlur、textShadowOffsetX、textShadowOffsetY。
文本块或文本片段大小:lineHeight、width、height、padding。
文本块或文本片段的对齐:align、verticalAlign。
文本块或文本片段的边框、背景(颜色或图片):backgroundColor、borderColor、borderWidth、borderRadius。
文本块或文本片段的阴影:shadowColor、shadowBlur、shadowOffsetX、shadowOffsetY。
文本块的位置和旋转:position、distance、rotate。
可以在各处的 rich 属性中定义文本片段样式。例如 series-bar.label.rich
例如:labelOption = {// 在文本中,可以对部分文本采用 rich 中定义样式。// 这里需要在文本中使用标记符号:// `{styleName|text content text content}` 标记样式名。// 注意,换行仍是使用 '\n'。formatter: ['{a|这段文本采用样式a}','{b|这段文本采用样式b}这段用默认样式{x|这段用样式x}'].join('\n'),// 这里是文本块的样式设置:color: '#333',fontSize: 5,fontFamily: 'Arial',borderWidth: 3,backgroundColor: '#984455',padding: [3, 10, 10, 5],lineHeight: 20,// rich 里是文本片段的样式设置:rich: {a: {color: 'red',lineHeight: 10},b: {backgroundColor: {image: 'xxx/xxx.jpg'},height: 40},x: {fontSize: 18,fontFamily: 'Microsoft YaHei',borderColor: '#449933',borderRadius: 4}} };
- echarts 提供了丰富的文本标签配置项,包括:
-
文本、文本框、文本片段的基本样式和装饰
- 每个文本可以设置基本的字体样式:fontStyle、fontWeight、fontSize、fontFamily。
可以设置文字的颜色 color 和边框的颜色 textBorderColor、textBorderWidth。
文本框可以设置边框和背景的样式:borderColor、borderWidth、backgroundColor、padding。
文本片段也可以设置边框和背景的样式:borderColor、borderWidth、backgroundColor、padding。
例如:option = {series: [{type: 'scatter',symbolSize: 1,data: [{value: [0, 0],label: {show: true,formatter: ['Plain text','{textBorder|textBorderColor + textBorderWidth}','{textShadow|textShadowColor + textShadowBlur + textShadowOffsetX + textShadowOffsetY}','{bg|backgroundColor + borderRadius + padding}','{border|borderColor + borderWidth + borderRadius + padding}','{shadow|shadowColor + shadowBlur + shadowOffsetX + shadowOffsetY}'].join('\n'),backgroundColor: '#eee',borderColor: '#333',borderWidth: 2,borderRadius: 5,padding: 10,color: '#000',fontSize: 14,shadowBlur: 3,shadowColor: '#888',shadowOffsetX: 0,shadowOffsetY: 3,lineHeight: 30,rich: {textBorder: {fontSize: 20,textBorderColor: '#000',textBorderWidth: 3,color: '#fff'},textShadow: {fontSize: 16,textShadowBlur: 5,textShadowColor: '#000',textShadowOffsetX: 3,textShadowOffsetY: 3,color: '#fff'},bg: {backgroundColor: '#339911',color: '#fff',borderRadius: 15,padding: 5},border: {color: '#000',borderColor: '#449911',borderWidth: 1,borderRadius: 3,padding: 5},shadow: {backgroundColor: '#992233',padding: 5,color: '#fff',shadowBlur: 5,shadowColor: '#336699',shadowOffsetX: 6,shadowOffsetY: 6}}}}]}],xAxis: {show: false,min: -1,max: 1},yAxis: {show: false,min: -1,max: 1} };
- 每个文本可以设置基本的字体样式:fontStyle、fontWeight、fontSize、fontFamily。
-
标签的位置
- 对于折线图、柱状图、散点图等,均可以使用 label 来设置标签。标签的相对于图形元素的位置,一般使用 label.position、label.distance 来配置。
试试在下面例子中修改position和distance 属性: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} };
- 对于折线图、柱状图、散点图等,均可以使用 label 来设置标签。标签的相对于图形元素的位置,一般使用 label.position、label.distance 来配置。
-
标签的旋转
- 某些图中,为了能有足够长的空间来显示标签,需要对标签进行旋转。例如:
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]}] };
- 这种场景下,可以结合 align 和 verticalAlign 来调整标签位置。注意,逻辑是,先使用 align 和 verticalAlign 定位,再旋转。
- 某些图中,为了能有足够长的空间来显示标签,需要对标签进行旋转。例如:
-
文本片段的排版和对齐
- 关于排版方式,每个文本片段,可以想象成 CSS 中的 inline-block,在文档流中按行放置。
每个文本片段的内容盒尺寸(content box size),默认是根据文字大小决定的。但是,也可以设置 width、height 来强制指定,虽然一般不会这么做(参见下文)。文本片段的边框盒尺寸(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',则文字在文本片段盒中是居右的。
- 关于排版方式,每个文本片段,可以想象成 CSS 中的 inline-block,在文档流中按行放置。
- 特殊效果:图标、分割线、标题块、简单表格
- 看下面的例子:
option = {series: [{type: 'scatter',data: [{value: [0, 0],label: {formatter: ['{tc|Center Title}{titleBg|}',' Content text xxxxxxxx {sunny|} xxxxxxxx {cloudy|} ','{hr|}',' xxxxx {showers|} xxxxxxxx xxxxxxxxx '].join('\n'),rich: {titleBg: {align: 'right'}}}},{value: [0, 1],label: {formatter: ['{titleBg|Left Title}',' Content text xxxxxxxx {sunny|} xxxxxxxx {cloudy|} ','{hr|}',' xxxxx {showers|} xxxxxxxx xxxxxxxxx '].join('\n')}},{value: [0, 2],label: {formatter: ['{titleBg|Right Title}',' Content text xxxxxxxx {sunny|} xxxxxxxx {cloudy|} ','{hr|}',' xxxxx {showers|} xxxxxxxx xxxxxxxxx '].join('\n'),rich: {titleBg: {align: 'right'}}}}],symbolSize: 1,label: {show: true,backgroundColor: '#ddd',borderColor: '#555',borderWidth: 1,borderRadius: 5,color: '#000',fontSize: 14,rich: {titleBg: {backgroundColor: '#000',height: 30,borderRadius: [5, 5, 0, 0],padding: [0, 10, 0, 10],width: '100%',color: '#eee'},tc: {align: 'center',color: '#eee'},hr: {borderColor: '#777',width: '100%',borderWidth: 0.5,height: 0},sunny: {height: 30,align: 'left',backgroundColor: {image:'https://echarts.apache.org/examples/data/asset/img/weather/sunny_128.png'}},cloudy: {height: 30,align: 'left',backgroundColor: {image:'https://echarts.apache.org/examples/data/asset/img/weather/cloudy_128.png'}},showers: {height: 30,align: 'left',backgroundColor: {image:'https://echarts.apache.org/examples/data/asset/img/weather/showers_128.png'}}}}}],xAxis: {show: false,min: -1,max: 1},yAxis: {show: false,min: 0,max: 2,inverse: true} };
- 文本片段的 backgroundColor 可以指定为图片后,就可以在文本中使用图标了:
labelOption = {rich: {Sunny: {// 这样设定 backgroundColor 就可以是图片了。backgroundColor: {image: './data/asset/img/weather/sunny_128.png'},// 可以只指定图片的高度,从而图片的宽度根据图片的长宽比自动得到。height: 30}} };
- 分割线实际是用 border 实现的:
labelOption = {rich: {hr: {borderColor: '#777',// 这里把 width 设置为 '100%',表示分割线的长度充满文本块。// 注意,这里是文本块内容盒(content box)的 100%,而不包含 padding。// 虽然这和 CSS 相关的定义有所不同,但是在这类场景中更加方便。width: '100%',borderWidth: 0.5,height: 0}} };
- 标题块是使用
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'}} };
- 简单表格的设定,其实就是给不同行上纵向对应的文本片段设定同样的宽度就可以了。
- 看下面的例子:
-
-
-
动画
-
数据过渡动画
-
过渡动画的配置
- 因为数据添加和数据更新往往会需要不一样的动画效果,比如我们会期望数据更新动画的时长更短,因此 ECharts 区分了这两者的动画配置:
对于新添加的数据,我们会应用入场动画,通过animationDuration, animationEasing, animationDelay三个配置项分别配置动画的时长,缓动以及延时。
对于数据更新,我们会应用更新动画,通过animationDurationUpdate, animationEasingUpdate, animationDelayUpdate三个配置项分别配置动画的时长,缓动以及延时。
可以看到,更新动画配置是入场动画配置加上了Update的后缀。
所有这些配置都可以分别设置在option最顶层对所有系列和组件生效,也可以分别为每个系列配置。
如果我们想要关闭动画,可以直接设置option.animation为false。 - 动画时长
- animationDuration和animationDurationUpdate用于设置动画的时长,单位为ms,设置较长的动画时长可以让用户更清晰的看到过渡动画的效果,但是我们也需要小心过长的时间会让用户再等待的过程中失去耐心。
设置为0会关闭动画,在我们只想要单独关闭入场动画或者更新动画的时候可以通过单独将相应的配置设置为0来实现。
- animationDuration和animationDurationUpdate用于设置动画的时长,单位为ms,设置较长的动画时长可以让用户更清晰的看到过渡动画的效果,但是我们也需要小心过长的时间会让用户再等待的过程中失去耐心。
- 动画缓动
- animationEasing和animationEasingUpdate两个配置项用于设置动画的缓动函数,缓动函数是一个输入动画时间,输出动画进度的函数:
(t: number) => number;
- 在 ECharts 里内置了缓入
'cubicIn'
,缓出'cubicOut'
等常见的动画缓动函数,我们可以直接通过名字来声明使用这些缓动函数。
- animationEasing和animationEasingUpdate两个配置项用于设置动画的缓动函数,缓动函数是一个输入动画时间,输出动画进度的函数:
- 延时触发
- animationDelay和animationDelayUpdate用于设置动画延迟开始的时间,通常我们会使用回调函数将不同数据设置不同的延时来实现交错动画的效果:
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;} };
- animationDelay和animationDelayUpdate用于设置动画延迟开始的时间,通常我们会使用回调函数将不同数据设置不同的延时来实现交错动画的效果:
- 因为数据添加和数据更新往往会需要不一样的动画效果,比如我们会期望数据更新动画的时长更短,因此 ECharts 区分了这两者的动画配置:
-
动画的性能优化
- 在数据量特别大的时候,为图形应用动画可能会导致应用的卡顿,这个时候我们可以设置animation: false关闭动画。对于数据量会动态变化的图表,我们更推荐使用animationThreshold这个配置项,当画布中图形数量超过这个阈值的时候,ECharts 会自动关闭动画来提升绘制性能。这个配置往往是一个经验值,通常 ECharts 的性能足够实时渲染上千个图形的动画(我们默认值也是给了 2000),但是如果你的图表很复杂,或者你的用户环境比较恶劣,页面中又同时会运行很多其它复杂的代码,也可以适当的下调这个值保证整个应用的流畅性。
-
监听动画结束
- 有时候我们想要获取当前渲染的结果,如果没有使用动画,我们在setOption之后 ECharts 就会直接执行渲染,我们可以同步的通过getDataURL方法获取渲染得到的结果。
const chart = echarts.init(dom); chart.setOption({animation: false//... }); // 可以直接同步执行 const dataUrl = chart.getDataURL();
- 但是如果图表中有动画,马上执行getDataURL得到的是动画刚开始的画面,而非最终展示的结果。因此我们需要知道动画结束然后再执行getDataURL得到结果。
假如你确定动画的时长,一种比较简单粗暴的方式是根据动画时长来执行setTimeout延迟执行:chart.setOption({animationDuration: 1000//... }); setTimeout(() => {const dataUrl = chart.getDataURL(); }, 1000);
- 或者我们也可以使用 ECharts 提供的rendered事件来判断 ECharts 已经动画结束停止了渲染
chart.setOption({animationDuration: 1000//... });function onRendered() {const dataUrl = chart.getDataURL();// ...// 后续如果有交互,交互发生重绘也会触发该事件,因此使用完就需要移除chart.off('rendered', onRendered); } chart.on('rendered', onRendered);
- 有时候我们想要获取当前渲染的结果,如果没有使用动画,我们在setOption之后 ECharts 就会直接执行渲染,我们可以同步的通过getDataURL方法获取渲染得到的结果。
-
-
-
交互
-
拖拽的实现
-
实现基本的拖拽功能
- 在这个例子中,基础的图表是一个 折线图 (series-line)。参见如下配置:
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}] });
- 既然折线中原生的点没有拖拽功能,我们就为它加上拖拽功能:用 graphic 组件,在每个点上面,覆盖一个隐藏的可拖拽的圆点。
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)};}) });
- 上面的代码中,使用 convertToPixel 这个 API,进行了从 data 到“像素坐标”的转换,从而得到了每个圆点应该在的位置,从而能绘制这些圆点。myChart.convertToPixel('grid', dataItem) 这句话中,第一个参数 'grid' 表示 dataItem 在 grid 这个组件中(即直角坐标系)中进行转换。所谓“像素坐标”,就是以 echarts 容器 dom element 的左上角为零点的以像素为单位的坐标系中的坐标。
注意这件事需要在第一次 setOption 后再进行,也就是说,须在坐标系(grid)初始化后才能调用 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}]}); }
- 上面的代码中,使用了 convertFromPixel 这个 API。它是 convertToPixel 的逆向过程。myChart.convertFromPixel('grid', this.position) 表示把当前像素坐标转换成 grid 组件中直角坐标系的 dataItem 值。
最后,为了使 dom 尺寸改变时,图中的元素能自适应得变化,加上这些代码:window.addEventListener('resize', function() {// 对每个拖拽圆点重新计算位置,并用 setOption 更新。myChart.setOption({graphic: echarts.util.map(data, function(item, dataIndex) {return {position: myChart.convertToPixel('grid', item)};})}); });
- 在这个例子中,基础的图表是一个 折线图 (series-line)。参见如下配置:
-
添加 tooltip 组件
- 到此,拖拽的基本功能就完成了。但是想要更进一步得实时看到拖拽过程中,被拖拽的点的 data 值的变化状况,我们可以使用 tooltip 组件来实时显示这个值。但是,tooltip 有其默认的“显示”“隐藏”触发规则,在我们拖拽的场景中并不适用,所以我们还要手动定制 tooltip 的“显示”“隐藏”行为。
在上述代码中分别添加如下定义:myChart.setOption({// ...,tooltip: {// 表示不使用默认的“显示”“隐藏”触发规则。triggerOn: 'none',formatter: function(params) {return ('X: ' +params.data[0].toFixed(2) +'<br>Y: ' +params.data[1].toFixed(2));}} });
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'}); }
- 这里使用了 dispatchAction 来显示隐藏 tooltip。用到了 showTip、hideTip。
- 到此,拖拽的基本功能就完成了。但是想要更进一步得实时看到拖拽过程中,被拖拽的点的 data 值的变化状况,我们可以使用 tooltip 组件来实时显示这个值。但是,tooltip 有其默认的“显示”“隐藏”触发规则,在我们拖拽的场景中并不适用,所以我们还要手动定制 tooltip 的“显示”“隐藏”行为。
-
全部代码
- 总结一下,全部的代码如下:
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}]}); }
- 总结一下,全部的代码如下:
-
-
智能指针吸附
-
吸附原理
- 在鼠标或触摸事件发生时,ECharts 会根据鼠标或触摸的位置,判断是否和某个可交互元素相交。如果是,则认为该元素是交互对象(与优化前的逻辑一致);如果不是,则在一定范围内找到最接近鼠标或触摸位置的一个元素。
更具体地说,ECharts 会在鼠标或触摸位置的周围,依次循环不同角度和不同半径(在 opt.pointerSize 范围内),直到找到一个元素与其相交。如果找到了,则认为该元素是交互对象。
- 在鼠标或触摸事件发生时,ECharts 会根据鼠标或触摸的位置,判断是否和某个可交互元素相交。如果是,则认为该元素是交互对象(与优化前的逻辑一致);如果不是,则在一定范围内找到最接近鼠标或触摸位置的一个元素。
-
性能分析
- 在实际算法实现的时候,我们首先将鼠标或触摸位置与所有可交互元素的 AABB 包围盒判断相交性,从而快速剔除了大部分不相交的元素。然后,我们再对剩余的元素进行精确的路径相交判断。因此,从用户体验角度,不会带来可感知的性能损耗。
对于大规模数据的图表系列(也就是开启了 large: true 的柱状图、散点图等),不会开启吸附功能。
- 在实际算法实现的时候,我们首先将鼠标或触摸位置与所有可交互元素的 AABB 包围盒判断相交性,从而快速剔除了大部分不相交的元素。然后,我们再对剩余的元素进行精确的路径相交判断。因此,从用户体验角度,不会带来可感知的性能损耗。
-
-