作者简介:
Jaked
8年前端工作经验,
主要分享:职业发展方面、前端技术、面试技巧等。
公众号:超哥前端小栈
掘金:https://juejin.im/user/5a5d4522518825732b19d364/posts
接上一篇文章 《「干货」用 Vue + Echarts 打造你的专属可视化界面(上)》,今天着重介绍 标记
的用法,来实现下图中的效果。
所用的 Echarts 的版本号为:v4.3。v-charts 的版本号为:v1.19.0。
标记的用法有很多,今天要介绍的场景有:折线图、柱状图、折线图 + 柱状图。
折线图标记 —— symbol
上图中,折线的拐点处,一些 “小圆点”,被替换成了小图标。
要实现这样的效果,需要先理一下原始的需求:
每种标记,代表一种活动类型。
有一些活动会发生在某些时间点,或时间段内,需要在活动发生的日期上标注出该活动的类型。
当同一天有多个活动发生时,采用复合图标,并当展示 tooltip 时,显示当日的每一个活动的信息。
tooltip的布局为:首先显示当前日期,中段展示各个活动的图标以及活动名称,最后展示指标名称和对应的数值。
没有活动的日期,拐点处与 tooltip 照常显示原先的样式。
所以,它的完整效果,应该是这样的:
要实现这样的效果,需要思考以下几点:
如何通过日期的定向匹配,将活动的图标以“打点”的形式,定在折线拐点处。
图标的大小,要怎么设置?它需要区别于正常的拐点标识。
tooltip 的样式要如何改写?还需要兼容没有活动的日期样式。
思路解析:
首先,为了做日期的定向匹配,需要设计的数据结构如下:
data: [ { id: '1, 1, 3, 2', date: '2019-10-10', name: 'test-name1, test-name2, test-name3, test-name4' }, ...]
接下来的这个 核心 属性:symbol
是关键,它其实就是折线上的 拐点
。
symbol 支持的标记类型有:circle、rect、roundRect、triangle、diamond、pin、arrow、none。默认情况下为 emptyCircle,也就是空心的圆。
它还支持链接的格式:'image://http://xxx.xxx.xxx/a/b.png'
。此外,如果需要每个数据的图形不一样,可以设置为如下格式的回调函数:
(value: Array|number, params: Object) => string
其中第一个参数 value 为 data 中的数据值。第二个参数 params 是其它的数据项参数。
这些正是我们需要的。踩坑亲测:上述回调函数,只有在最新版的 V4.3
中才能正常使用,否则会报错。这也是为何,我在一开始就先强调了 Echarts 的版本问题。具体实现如下:
...// mock 包含标注的数据结构dataList: [ { id: '1, 1, 3, 2', date: '2019-10-10', name: 'test-name1, test-name2, test-name3, test-name4' }, ... { id: '1', date: '2019-10-17', name: 'test-name1' }],...setChartExtend () { this.chartExtend = { series: (v) => { Array.from(v).forEach((e, idx) => { e.symbol = (value, params) => { return getSymbolIcon(params.name, dataList); }; e.symbolSize = (value, params) => { return getSymbolSize(params.name, dataList); }; }); return v; }, ... };},getSymbolIcon (date, dataList) { const defaultSymbol = 'circle'; if (!dataList || dataList.length === 0) { return defaultSymbol; } // 通过日期匹配,找到对应的标注对象 const dataItem = dataList.find(item => item.date === date); const iconUrl = getSymbolUrl(dataItem.id); return iconUrl ? iconUrl : defaultSymbol;},getSymbolSize (date, dataList) { if (!dataList || dataList.length === 0) { return 4; } // 通过日期匹配,找到对应的标注对象 const dataItem = dataList.find(item => item.date === date); return dataItem ? 15 : 4;},getSymbolUrl (id) { // 这里需要额外先做一层准备工作:将图标按 id 对应图标进行命名,然后传到自家的cdn上 // 命名可以像这样:symbol-icon-1.jpg、symbol-icon-2.jpg 等等 // 这里拿到标注的id,拼上链接返回即可 // 形如:image://http://xxx.xxx.com/symbol-icon-1.jpg // 遇到多个 id 的情况,可以多加一个复合图标来处理,id 可以定为 0}
最后的一个问题,如何改写 tooltip 的样式问题,以做好兼容呢?
之前说到,tooltip 的布局分为三块:日期、标注信息、具体数值。那么我们就以此,来重新绘制 tooltip。
tooltip 支持 formatter 回调函数,它的返回值类型是 Sting。
// 回调函数格式(params: Object|Array, ticket: string, callback: (ticket: string, html: string)) => string
日期的信息,可以通过 params[0].axisValue 来获取。
获取标注信息的方法,与上述获取图标的思路类似,只是这里需要展示具体的标注类型和名称。
具体的数值,可以通过 params 中的 marker,seriesName,value 等属性获得。具体实现如下:
setChartExtend () { this.chartExtend = { series: (v) => { ... }, tooltip: { formatter: (params) => { return getTooltipResult(params, dataList); } } };},getTooltipResult (params, dataList) { const dateResult = params[0].axisValue; // 获取原版 tooltip 的渲染结构 const originalResultObj = getOriginalTooltipResult(params); if (!dataList || dataList.length === 0) { return dateResult + originalResultObj.strResult; } const dataItem = dataList.find(item => item.date === date); if (dataItem) { return dateResult + getSymbolResult(dataItem, originalResultObj.strResult); } return dateResult + originalResultObj.strResult;},getOriginalTooltipResult (params) { let result = ''; params.forEach((param, idx) => { // value 会因为 seriesType 的不同,类型也会有不同 let value = Object.prototype.toString.call(param.value) === '[object Array]' ? param.value[1] : param.value; const str = `${param.marker}${param.seriesName}: ${ value }
`; result += str; }); return { strResult: result };},getSymbolResult (dataItem, originalResult) { // 将 dataItem 的 id 转为数组的形式,循环渲染输出图标与名称的组合 const dataIds = dataItem.id.split(','); const dataNames = dataItem.name.split(','); dataIds.forEach ((id, idx) => { // 通过 id 换取 图标的链接 const iconUrl = ...; // 仿照 param.marker 的 style 写法,渲染图标样式 const str = `${dataNames[idx]}
`; result += str; }); return result + originalResult;}
或许有同学会问 getOriginalTooltipResult 方法返回的值,里面只有一个 strResult,为何要设计为对象?
其实是为了方便扩展。例如,可以在日期的后面跟上下方具体数据的值的总计。那就需要通过 getOriginalTooltipResult 方法里的 params 循环,计算出 total,在配合上样式,生成一个 strTotal。
getOriginalTooltipResult (params) { ... return { strTotal: strTotal, strResult: result };}
此外,在实际的业务中,还可能会出现某些线的数值是百分比。那就需要再对 getOriginalTooltipResult 方法做扩展,比如传入一个 options 对象:
getOriginalTooltipResult = (params, options = { isLinePercent: false, isShowTotal: false }) { ...}
至此,折线图标记的渲染,就能完美地呈现了。
柱状图标记 —— markPoint
很尴尬的一点是:柱状图没有 symbol 属性。也就意味着上面的折线图的那一套,在柱状图中玩不转了。
没办法,只能从头查文档,继续找资料。经过一番“摸爬滚打”,终于发现了 markPoint 这个属性。在 markPoint 这个对象里面,可以设置 symbol,这样的话,那么之前搞出来的那一套就没有白费呀?!?
需要注意的是,markPoint 的默认 symbol 为 'pin',就是一个气泡的图标。另外,想要让 markPoint 的标记出现,就必须设置它的 data 属性。我们需要设置 data 里的这样几个属性:
data: [ { symbol: '...', // 设置标记的图标链接 symbolSize: 15, // 设置标记的大小 coord: [index, 0], // x 轴的第 index 个上,打标记 symbolOffset: [0, 0] // 将标记定位在 x 轴上 }, ...]
具体的实现代码如下:
...// mock 包含标注的数据结构dataList: [ { id: '1, 1, 3, 2', date: '2019-10-10', name: 'test-name1, test-name2, test-name3, test-name4' }, ... { id: '1', date: '2019-10-17', name: 'test-name1' }],...setChartExtend () { this.chartExtend = { series: (v) => { Array.from(v).forEach((e, idx) => { e.markPoint = { data: getMarkPointData(this.chartData.rows, dataList) }; }); }, ... };},getMarkPointData (rows, dataList) { const results = []; rows.forEach((row, index) => { // 通过日期匹配,找到对应的标注对象 const dataItem = dataList.find(item => item.date === row.date); if (dataItem) { results.push({ symbol: getSymbolUrl(dataItem.id), symbolSize: 15, coord: [index, 0], symbolOffset: [0, 0] }); } }); return results;},getSymbolUrl (id) { // 这里需要额外先做一层准备工作:将图标按 id 对应图标进行命名,然后传到自家的cdn上 // 命名可以像这样:symbol-icon-1.jpg、symbol-icon-2.jpg 等等 // 这里拿到标注的id,拼上链接返回即可 // 形如:image://http://xxx.xxx.com/symbol-icon-1.jpg // 遇到多个 id 的情况,可以多加一个复合图标来处理,id 可以定为 0}
因为 markPoint 在设置 data 时,取不到日期的数据,所以就需要用到 chartData 中的 rows 了。
在 rows 的循环中,如果匹配到当天需要打标记,则往结果数组中存入刚才预设的数据结构,最终返回给 markPoint 的 data,渲染展现。效果如下:
tooltip 的实现方法,不受图表的类型影响,是可以通用的,故此处不再赘述。
另外,有的时候,会遇到需要处理柱状图是否堆叠的效果。这会影响 symbolOffset 的定位,为了美观,可以这样处理:对于非堆叠的项,将之往右偏移 50%,将标记居中展示,即 symbolOffset : ['50%', 0]
。
折线图 + 柱状图
最后来个 组合拳,折线图与柱状图的复合型图表结构,像下面这样:
从代码实现上,我们当然可以给每一根符合条件的柱子,和折线的拐点,都打上标记。但在界面设计上,为了美观,我们选择只给折线的拐点打上标记,并且忽略了虚线的拐点。
这样的设计初衷是:标记,只是为了给人提个醒,是为了告诉查阅者,这一天因为发生了某些特殊事件,而导致数据发生了较为明显的变化。所以,由此得到的结论是:每天只要出现一个标记就够了。
具体的实现,其实很简单,只需要在渲染时判断 series 的 type 即可:
setChartExtend () { this.chartExtend = { series: (v) => { Array.from(v).forEach((e, idx) => { if (e.type === 'bar') { // 设置柱状图 markPoint 的方法 ... } if (e.type === 'line') { // 设置折线图 symbol、symbolSize 的方法 ... } }); }, ... };}
总结
Echarts 可以实现的效果有很多,本篇涉及其中 “标记” 的渲染。在折线图的拐点处,用 symbol 做了匹配化的处理。在柱状图中,因为没有直接的 symbol,转而使用 markPoint 来实现,采用将标记定位在某个维度上的做法。效果都挺不错的。
不过,在后续的使用中,发现了另一个尴尬的情况:在折线图中,当点击图例中的某一项,使其数据隐藏,然后再次点击重新渲染后,发现 symbol 的自定义图标不显示了。
作者简介:
Jaked
8年前端工作经验,
主要分享:职业发展方面、前端技术、面试技巧等。
公众号:超哥前端小栈
掘金:https://juejin.im/user/5a5d4522518825732b19d364/posts
本文已经获得Jaked老师授权转发,其他人若有兴趣转载,请直接联系作者授权。