前言
接上一篇文章 《「干货」用 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 的版本问题。具体实现如下:
<ve-line ... :extend="chartExtend"></ve-line>
...
// 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 }<br>`;
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 = `<img src="${iconUrl}" width="11" height="11" style="display: inline-block; margin-right: 4px; margin-left: -1px;">${dataNames[idx]}<br>`;
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 轴上
},
...
]
具体的实现代码如下:
<ve-histogram :data="chartData" :extend="chartExtend"></ve-histogram>
...
// 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 的自定义图标不显示了
。
我查了很久,还是没找到有用的信息。怀疑是个渲染的 bug,所以给 Echarts 提了 issue,希望能得到解决吧。也欢迎大家在留言区中,共同探讨相关的问题,感谢!
PS:欢迎关注我的公众号 “超哥前端小栈”,交流更多的想法与技术。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。