记录使用ECharts实现复杂图表交互功能的一些dem

用ECharts玩出花——疑难case 前言

本文将总结我在这次需求中遇到的一些ECharts疑难case,主要是tooltipmarkPoint的深度使用等。文章偏实践,有深度EChart使用经验会比较容易get,建议遇到有不熟悉的API时,仔细查阅EChart文档再回到本文。

本文所有的图表截图均在github-demo实现

大纲 tooltip tooltip属性的作用 如何自定义tooltip 如何解决tooltip展示被遮挡的问题 markPoint markPoint属性的作用 如何自定义markPoint图标 如何按条件正确的位置上显示markPoint图标 如何主动高亮markPoint 结合图表事件 如何实现markPoint与非markPoint显示的tooltip不同 如何实现点击某个节点出现指定markPoint icon 如何实现图表可视区域变化自适应 其他较简单case 柱状图的case 饼图的case 一些问题 一、tooltip 1.1 tooltip属性的作用

通常用来展示某个节点的详细数据,如下图(分别来自ECharts官方示例的折线图、柱状图、饼图)。

1.2 如何自定义tooltip

可以看到官方提供的tooltip比较简单,想要实现自定义数据、带 “特有样式” 、带 “交互” 功能的tooltip怎么办?比如下图功能:

首先我们需要借助属性formatter:提示框浮层内容格式器,支持字符串模板回调函数两种形式

自定义数据
formatter足够实现

带“特有样式”

label带“特有样式”,可以通过rich属性配置,文章后半部分会体现,但tooltip是没有的

常见解决方案拼接html片段,如下图;很不优雅,但仅仅一个样式可能还凑合?

formatter: function(params) { let html = `<div style="color: red;">${params.xx}<div>` return html }

带“交互”
比如说点击事件、调接口,hover出提示,form表单带校验...,用上述拼html片段的方案如果硬写原则上说是可以实现的,但是肯定很不优雅,有无更好的解决方案?

1、思路一

拼接html太蠢了,一定要借助ECharts的tooltip嘛?

有没有可能先拿到鼠标在图表上的x、y坐标,在图表之外用借助antd这种组件库写react组件实现功能,再把react组件定位到鼠标的x、y坐标上?你看,antd的气泡卡片组件很像了嘛!

先把tooltip隐藏,使用echartsInstance. on 监听鼠标事件,可以拿到x、y坐标以及该点的数据,用样式定位组件,把数据传入组件。

看起来一切没有问题,实践之后发现了以下几个致命问题

问题一:怎么控制自定义组件的展示与否

正常逻辑:在鼠标移入某个节点的时候展示,移出某个节点的时候消失;这个可以通过监听鼠标移入移出事件echartsInstance.on('mouseover')、echartsInstance.on('mouseout')实现

实践结果:鼠标移动到节点,自定义组件能正常展示,但是当鼠标从节点移动到自定义组件上时,自定义组件直接消失了 原因你应该可以猜到是:当鼠标从节点移动到自定义组件上的时候就已经触发移出事件echartsInstance.on('mouseout')了,因为自定义组件是ECharts之外的部分

解决方法:增加自定义tooltip组件的移入移出事件,用一个变量标识tooltip是否正在显示,并且对鼠标的移入移出加setTimeout,保证先触发tooltip的移入事件,再触发echartsInstance.on('mouseout')事件,并在此判断如果tooltip正在显示的时候就不隐藏tooltip组件

问题二:移动到自定义组件时,该节点的高亮会失效

上面解决了组件的展示问题,但是选中节点的高亮还有问题,如下图
还是同样的原因,但是当你鼠标移动到组件上的时候就已经离开ECharts了;需要用echartsInstance.dispatchAction主动高亮,只要在显示tooltip组件的时候都需要高亮,隐藏的时候就取消高亮

理论上这个方案是可以实现的,我这里没用方案一,具体代码实现就不展开

2、思路二

有没有办法在tooltip里面直接写react组件?

我们知道formatter不能直接返回组件,可以返回一个html片段。那怎样的html片段可以渲染出组件呢,所以想到用ReactDOM.render去渲染jsx并挂载到formatter返回的html上,如下代码所示

formatter(params) { setTimeout(() => { const root = document.getElementById(`tool-tip`); if (root) { ReactDOM.render( <EChartToolTips data={params} />, root ); } }, 0); return `<div id="tool-tip"></div>`; }

tooltip是ECharts内置实现的,配上tooltip.enterable设置为true时,当鼠标移动到tooltip组件上时,是不会触发移出事件echartsInstance.on('mouseout')的;节点高亮也不会消失。

3、饼图、柱状图 tooltip 问题

自定义tooltip,折线图实现下来基本没啥问题,但在饼图、折线图中暴露了问题:
鼠标放在节点上,从节点移动到tooltip组件上的时候,组件一直跟着鼠标位置在动,导致不能顺利的移动到react组件。

仔细观察发现原因是ECharts默认的tooltip位置会与鼠标位置x、y都有一段距离,如下图,红点是鼠标位置,而tooltip位置总在鼠标位置的一定距离,如下图(分别来自ECharts官方示例的折线图、柱状图)。

为什么折线图没有问题,饼图、柱状图却有问题呢?
折线图的节点“很小”,选中节点之后,移动到tooltip的过程中不会有其他行为;而柱状图和饼图,整个“面积”都是“节点范围”,也就是说当你在饼图、柱状图位置上移动的时候,tooltip的位置也跟着移动,导致不能顺利的移动到tooltip上。

解决思路:自定义tooltip的位置,将tooltip正好放在鼠标的旁边,这样从节点异动到tooltip组件就可以无缝衔接

解决方案使用到的属性同下面的1.3被遮挡问题,1.3一起讲代码实现

1.3 如何解决tooltip展示被遮挡的问题 1、问题暴露

如图下图所示

ECharts本身其实已经处理了tooltip位置自适应的问题(但我发现有时候也会出现不准被遮挡的情况,概率虽然不大),自定义了tooltip组件后,位置自适应不准问题更加明显;

自定义tooltip组件其实又分为以下两种场景

场景一:自定义tooltip组件,tooltip组件的宽高是固定的,不会发生变化 场景二:自定义tooltip组件,组件在不同情况下,大小不一样,也就说tooltip的宽高可能变化

尝试解决方案:按理来说两种场景都可以借助属性tooltip.extraCssText将tooltip的宽高定义为react组件实际的宽高,场景二根据条件给tooltip.extraCssText设置不同的宽高,这样在渲染的时候tooltip就会以定义的宽高来自适应位置

实践结果:理想很美好,现实很残酷;本身tooltip的自适应可能就不准,自定义tooltip之后更不准;tooltip.extraCssText属性也没有使的被遮挡的概率减少

2、解决思路

有没有支持控制tooltip位置的属性呢?让tooltip一直保持在鼠标的右上方,如果在右边要溢出就适量右移一定位置

tooltip. position 这个属性是可以支持的,参数如下

x坐标

ECharts当前容器的宽度size.viewSize[0] - 鼠标在ECharts上的x坐标point[0] < tooltip组件的宽度 否:直接返回鼠标在ECharts上的x坐标point[0]即可 是:说明tooltip的x坐标需要往右移动,否则会溢出被隐藏 向右移动的距离是 鼠标在ECharts上的x坐标point[0] + tooltip组件的宽度 - ECharts当前容器的宽度size.viewSize[0] ,所以x坐标为:鼠标在ECharts上的x坐标point[0] - 向右移动的距离,其实就是ECharts当前容器的宽度size.viewSize[0] - tooltip组件的宽度

y坐标: 固定在上方,计算就很简单了:鼠标在ECharts上的y坐标point[1] -tooltip组件的高度

代码实现

因为我这里的业务场景,tooltip有三种不同的情况,有三种不同的宽高

position(point, params, dom, rect, size) { if (isViewNote) { if (size.viewSize[0] - point[0] < toolTipFromStyle.width) { return [ point[0] - (point[0] + toolTipFromStyle.width - size.viewSize[0]), point[1] - toolTipFromStyle.height, ]; } else { return [point[0], point[1] - toolTipFromStyle.height]; } } else { if (size.viewSize[0] - point[0] < toolTipDataStyle.width) { return [ point[0] - (toolTipDataStyle.width + point[0] - size.viewSize[0]), point[1] - toolTipDataStyle.height - toolTipDataStyle.itemHeight * detail.trends.length, ]; } else { return [ point[0], params.componentSubType === 'pie' || params.componentSubType === 'bar' ? point[1] - toolTipDataStyle.height : point[1] - toolTipDataStyle.height - toolTipDataStyle.itemHeight * detail.trends.length, ]; } } }, 二、markPoint 2.1 markPoint属性的作用

用来标注某个特殊的节点,看以下ECharts官方示例,比如说标注最大值节点、最小值节点(如下图一),比如说有某个值的节点(如图二)

2.2 如何自定义markPoint图标

需求背景:折线图的节点上有a字段时,展示A图标;没有a字段的点击该节点出现B的图标

可以看到下面的markPoint形态都不一样,不同场景下的图标不一样,不同颜色折线图上的图标颜色也不一样。

markPoint.symbol支持自定义图标

在官网试一试,可以看到如果是矢量路径,ECharts对于矢量路径本身已经处理了图标自适应折线图颜色,但是仅仅使用图标比较简单的情况下是可以的,如果图标太复杂,矢量路径会丢失部分节点,如下图

跟UI沟通后,确实替换成简单的图标达不到想要的效果,所以最后使用image://路径根据颜色、状态去判断是否展示图标、展示什么样的图标;用symbolSizesymbolOffset属性去调整不同图标的大小。

配置在哪个symbol

如果只有一种类型的图标(也就是说图标跟节点数据无关,无论什么情况图标都长一样),markPoint.symbol配置即可 如果只有有多种类型的图标,遍历markPoint.data,在markPoint.data.symbol根据data状态展示不同图标

2.3 如何按条件在正确的位置上显示正确的markPoint

图标类型的问题在2.2中解决了,但是想要图标展示在正确位置上(在哪个节点上)应该怎么实现呢?不同的图实现方式有些不一样。

从官网可以看到我们可以通过配置markPoint.data来控制哪些节点展示图标,有以下几种方式

type属性:特殊的标注类型,用于标注最大值最小值等。只适用于特殊场景 coord属性:坐标系中的坐标 x、y属性:相对容器的屏幕的坐标,单位像素

1、折线图、柱状图如何确定markPoint位置 利用coord属性,遍历data,x坐标直接取dataIndex即可,y坐标(高度)即该节点的值

markPoint: { data: item.data.map((dataItem: any, dataIndex: number) => ({ coord: [dataIndex, dataItem.trendValue], })), }

2、饼图如何确定markPoint位置

饼图继续使用coord: [dataIndex, dataItem.trendValue],图标压根都不显示了。思考一下,确实饼图没有坐标系,无论将coord设置成什么值都显示不出来,尝试设置x、y坐标,图标可以显示出来,但是x、y值应该怎么算出来让人头疼。

搜索到issue,最终是先让饼图渲染,渲染后拿到labelLine开始位置的x、y坐标,赋值给饼图的markPoint.data的x,y坐标,再渲染

myChart.setOption(Object.assign(getBarEChartOption(), options)); const xYAxis = myChart._chartsMap[Object.keys(myChart?._chartsMap)[0]]?.group?._children?.map((item) => item?.textGuideLineConfig?.anchor) || []; const options2 = Object.assign( getPieEChartOption(xYAxis), options, ); myChart.setOption(options2); 2.4 如何主动高亮markPoint

不要试图通过 API 高亮markPoint,而是按照条件加载不同的symbol来表示markPoint高亮

echartsInstance.dispatchAction这个API提供了控制tooltip显示的API等,通过配置如下图所示参数,确定高亮哪个节点。

但是是无法高亮markPoint的!!我在这配置了很久,最后发现需要换一个思路:去替换 markPoint.data.symbol 的图片,用数据条件判断是否高亮

上述是主动高亮,还有markPointhover高亮,通过配置markPoint.data.emphasis.itemStyle一般都能满足需求,如下右图是hover高亮。

emphasis: { itemStyle: { borderColor: '#fff', borderWidth: 2, shadowBlur: 4, shadowColor: chartColors[0], }, },

三、结合图表事件 3.1 如何实现markPoint与非markPoint显示的tooltip不同

需求背景:有A字段(有markPoint)如下左图,无A字段(无markPoint)如下右图

我们已经自定义了tooltip为react组件,可以传props即可,通过echartsInstance.on可以监听鼠标移入移出,params.componentType可以区分选中的是否是markPointvisibleNote变化后,echartsInstance.setOption重新渲染,这样tooltip的react组件,就能拿到最新的状态

useEffect(() => { myChart.on('mouseover', function (params) { if (params.componentType === 'markPoint') { setIsPrize(true); } }); myChart.on('mouseout', function (params) { setIsPrize(false); }); }, []);

注意:此场景下还必须设置 tooltip.trigger item

折线图多线条如果配置为axis,上述监听到的params是一个数组 你是无法知道鼠标具体划入了哪个节点的;设置**tooltip.trigger** 为axis通常是想看到同一个x轴上的多个y轴的点,可以自己通过数据拼装(也就是自己去拿一下该节点x坐标上所有的数据,二维数组好拿)实现

默认值 可选值 效果
‘item’ ‘item’,’axis’ 触发类型,默认数据触发
为’item’时只会显示该点的数据 为’axis’时显示该列下所有坐标轴所对应的数据 如下所示

3.2 如何实现点击某个节点出现指定markPoint icon

echartsInstance.on监听鼠标点击事件,通过params拿到的值拼装然后重新调用,activeNode变化后,echartsInstance.setOption重新渲染,这样tooltip的react组件,就能拿到最新的数据;然后上述降到的symbol里面根据数据按条件加载不同的图标。

chartRef.on('click', function (params: any) { if (!(!isAddNote && params.componentType !== EChartComponentTypeEnum.MARKPOINT)) { setIsSelectNote(true); setActiveNode({ minutes: params.data.minutes, noteType: params.data.noteType, noteStr: params.data.note?.noteStr, id: params.data.note?.id, value: params.data.value, name: params.name, color: params.data?.color || params.color, backgroundColor: params.data.backgroundColor, }); } });

注意: 单根线折线图、柱状图、饼图用一个唯一标识即可,多条线折线图如果没有唯一标识,需要拿多个值作为唯一标识,比如这里拿了代表x坐标的事件,以及折线类型

3.3 如何实现图表可视区域变化自适应

通过echartsInstance.getWidth拿到ECharts 实例容器的宽度,容器需要变化时调用echartsInstance.resize传入变化后的width即可

useUpdateEffect(() => { if (isLiveNowDrawerOpen !== undefined) { const chartWidth = chartRefs.current[0].current.getWidth(); [...Array(6).keys()].forEach(index => { chartRefs.current[index].current.resize({ width: isLiveNowDrawerOpen ? chartWidth - 300 : chartWidth + 300 }); }); } }, [isLiveNowDrawerOpen]); 四、其他case 4.1 柱状图 1、问题一

当tooltip.trigger设置为item时,柱状图数值太小,或者为0时,鼠标上去展示不出tooltip,可以设置series.barMinHeight,设置最小高度,并且给一个默认的灰色

2、问题二

柱状图想要y轴想要展示百分比,如上图

如果你什么都不处理,ECharts的自适应,会存在Y轴刻度大于100%的情况; 不要尝试直接修改yAxis,比如固定成0% 25% 50% 75% 100%,会存在值与刻度对不上的情况,因为值还在以数值为标准展示Y轴 可以直接把data.value换算成百分比的值,然后Y轴直接展示{value}%

yAxis: [ { min: 0, max: 100, interval: 20, axisLabel: { formatter: '{value}%', }, }, ], value: parseFloat(((dataItem.value / total) * 100).toFixed(2)) // 保留两位有效小数 4.2 饼图 1、一个demo

label可自定义样式

const options = { color: chartColors, legend: { top: '5%', left: 'center', selectedMode: false, }, title: { text: total, left: 'center', top: '50%', textStyle: { fontSize: 20, color: '#1C1F23', }, }, graphic: { type: 'text', left: 'center', top: '45%', style: { text: 'Total', textAlign: 'center', fontSize: 12, fill: 'rgba(28, 31, 35, 0.6)', }, }, series: [ { type: 'pie', top: '5%', radius: ['30%', '55%'], itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2, }, startAngle: 270, data: handledData.map((dataItem, dataIndex) => ({ value: dataItem.value, name: dataItem.name, label: { position: 'outside', formatter: ['{icon|}{value|{c}}{gap|}', '{tag|{b} {d}%}'].join('\n'), rich: { icon: { width: 14, height: 14, backgroundColor: { image: require(`../assets/img/prize-${chartColors[dataIndex].replace('#', '')}.png`), }, }, value: { height: 14, color: chartColors[dataIndex], fontWeight: 700, fontSize: 16, lineHeight: 20, }, gap: { height: 12, }, tag: { height: 10, backgroundColor: 'rgba(46, 50, 56, 0.05)', borderRadius: 18, padding: 8, fontWeight: 600, fontSize: 12, color: 'rgba(28, 31, 35, 0.8)', margin: [8, 0, 0, 0], }, }, }, labelLine: { show: true, length: 16, length2: 20, lineStyle: { color: chartColors[dataIndex], }, maxSurfaceAngle: 60, }, })), markPoint: { label: { show: false, }, animation: false, data: handledData.map((dataItem, dataIndex) => ({ textValue: `${dataItem.percentValue}%`, x: xYAxis[dataIndex]?.x, y: xYAxis[dataIndex]?.y, symbol: dataItem.value % 100 === 0 ? getMarkPointSymbol(chartColors[dataIndex]) : 'none', symbolSize: [36, 32], symbolOffset: [0, '-40%'], emphasis: { itemStyle: { borderColor: '#fff', borderWidth: 2, shadowBlur: 4, shadowColor: "#fff", }, }, })), }, }, ], }; 五、一些问题 5.1 在form表单输入时,不能刷新渲染表单(轮循环数据),会导致输入清空,需要控制输入时,停止轮训

版权声明:

1、该文章(资料)来源于互联网公开信息,我方只是对该内容做点评,所分享的下载地址为原作者公开地址。
2、网站不提供资料下载,如需下载请到原作者页面进行下载。