背景
最近在做一个工程相关的项目,需要用折线图展示传感器采集到的数据,并对采集到的数据可以圈定某一部分计算出平均值。现在将这个功能单独抽出来做一个示例项目,并进行记录。
示例项目最后展示效果:
示例项目传送门:echartsDemo: Echarts折线图自定义框选并高亮选中曲线,计算平均值 (gitee.com)
大致思路
- 监听折线图的mousedown、mousemove、mouseup事件;
- mousedown 记录鼠标按下的初始位置,并监听 mousemove、mouseup事件;
- mousemove 对选择框进行绘制;
- mouseup 记录鼠标松开的位置,并对数据源循环计算出在位置内的坐标点,以及对连续区间内的线进行高亮,取消move、up事件的监听。
用到的插件
- vue/cli 利用脚手架搭建一个vue项目
- vue2
- echarts 画折线图
数据格式
具体的完成数据在示例项目的 public/echartsData.txt 内。
less
x轴: ['2024-03-09 17:07:00', '2024-03-09 17:06:59',......, n]
y轴: [73.3, 72.9,......, n]
实现步骤
只粘贴出主要代码,文末会有示例项目的gitee链接。
1. 创建一个基本的echarts项目
xml
<template>
<div style="width: 1000px;height: 600px;" ref="echartsId" id="echartsId"></div>
</template>
<script>
export default {
methods: {
init() {
......
this.echartsInstance = echarts.init(document.querySelector('#echartsId'));
this.echartsInstance.setOption(this.option);
......
}
}
}
</script>
2. 画框的基本语法
画框三步骤:按下、移动、放开,吧唧就画了一个框。
如果直接利用折线图实例去监听(on)鼠标事件,只会在点击到图形处才生效。
但当前要实现的框选功能,需要在折线图内任何地方都可以触发,所以参考文档,可知需要用到 this.echartsInstance.getZr() 监听 zrender事件,才能监听到空白区域的鼠标事件。
直接在template中增加选择框
xml
<template>
<div id="app">
<div style="position: relative;">
<div style="width: 1000px;height: 600px;" ref="echartsId" id="echartsId"></div>
<div ref="boxSelect" class="box-select"></div>
</div>
</div>
</template>
<script>
......
mounted() {
this.selectionDiv = this.$refs.boxSelect
}
......
</script>
按下 mousedown
kotlin
init() {
......
this.echartsInstance.setOption(this.option);
this.boxSelectEvent()
}
// 该方法在实例化折线图后调用
boxSelectEvent() {
this.echartsGetZr = this.echartsInstance.getZr() // 为了监听折线图空白区域鼠标事件
setTimeout(() => {
this.echartsGetZr.on('mousedown', (e) => {
// 获取鼠标按下坐标点
const {offsetX, offsetY} = e;
this.selectionPoint = {offsetX, offsetY};
// 并设置选择框 初始位置、宽高
this.selectionDiv.style.left = offsetX + "px";
this.selectionDiv.style.top = offsetY + "px";
this.selectionDiv.style.width = "0px";
this.selectionDiv.style.height = "0px";
// 对move、up事件进行监听
this.echartsGetZr.on("mousemove", this.mousemove);
this.echartsGetZr.on("mouseup", this.mouseup);
})
}, 1000)
},
移动 mousemove
ini
mousemove(e) {
const x = e.offsetX
const y = e.offsetY
const width = Math.abs(this.selectionPoint.offsetX - x);
const height = Math.abs(this.selectionPoint.offsetY - y);
// 根据移动坐标 和 初始坐标,计算处宽高
this.selectionDiv.style.width = width + "px";
this.selectionDiv.style.height = height + "px";
this.selectionDiv.style.visibility = "visible";
// 如果移动的坐标 比 初始坐标小,需要切换选择框的位置
x < this.selectionPoint.offsetX && (this.selectionDiv.style.left = x + "px");
y < this.selectionPoint.offsetY && (this.selectionDiv.style.top = y + "px");
},
放开 mouseup
kotlin
mouseup() {
this.selectionDiv.style.visibility = "hidden";
// 取消监听事件
this.echartsGetZr.off("mousemove", this.mousemove);
this.echartsGetZr.off("mouseup", this.mouseup);
},
3. 如何知道Echarts某个点的像素坐标
主要是用到 convertToPixel 语法。
kotlin
this.echartsInstance.convertToPixel('grid', [x轴坐标, y轴坐标])
在该示例项目中,x轴为时间,y轴为数值。所以具体的代码如下
javascript
this.echartsInstance.convertToPixel('grid', [某一个时间点, Number(yValue))])
4. 计算坐标点是否在画的框内
需要先对绘制的选择框判断,是否宽高都大于0。然后得到选择框最后的左上角坐标和宽高,便于计算。
kotlin
mouseup(e) {
const {offsetX, offsetY} = e;
const width = Math.abs(this.selectionPoint.offsetX - offsetX);
const height = Math.abs(this.selectionPoint.offsetY - offsetY);
this.selectionDiv.style.visibility = "hidden";
this.echartsGetZr.off("mousemove", this.mousemove);
this.echartsGetZr.off("mouseup", this.mouseup);
if (width > 0 && height > 0) {
// 对鼠标放开坐标和初始坐标进行判断,看谁小,谁就是左上角的坐标
this.boxSelectOfPoint({
offsetX: offsetX < this.selectionPoint.offsetX ? offsetX : this.selectionPoint.offsetX,
offsetY: offsetY < this.selectionPoint.offsetY ? offsetY : this.selectionPoint.offsetY,
width,
height,
});
}
},
循环数据源,对坐标点进行像素转换,然后判断坐标是否在框选范围内。最后利用 echarts的visualMap语法,让选中的区域进行高亮显示。 配置项中使用 lt
(小于,less than),gt
(大于,greater than),lte
(小于等于 less than or equals),gte
(大于等于,greater than or equals)来表达边界
javascript
boxSelectOfPoint(result) {
const {offsetX, offsetY, width, height} = result;
// type dataItem = { x: string, y: string|number, index: number }
// dataList: Array<dataItem>
// 对这些数据进行收集,方便处理。 实际项目中比这个复杂很多,这里是简化过的
const dataList = []
this.dataSource.forEach((item, index) => {
const x = item.createTime
const y = item.value
// 得到一个转换的坐标
let p = this.echartsInstance.convertToPixel('grid', [x, Number(y)])
//
const flag = p[0] > offsetX && p[0] < offsetX + width && p[1] > offsetY && p[1] < offsetY + height;
if (flag) {
dataList.push({
x, y, index
})
}
})
if (dataList.length > 0) {
this.selectResult = dataList
this.echartsInstance.setOption({
// 在实际项目中是多条曲线的,如果让多条曲线高亮,则数组
visualMap: [
{
type: "piecewise",
show: false,
dimension: 0,
seriesIndex: 0, // 这里需要和之前的 series 对上
piecewise: true,
outOfRange: {
// color: ['#00000', 'red'][index]
color: ['#00000'][0] // 这里如果多条曲线的话 则需要一一对应非框选中的颜色,
},
pieces: [
{
gt: dataList[0].index, //
lt: dataList[dataList.length - 1].index,
color: "rgba(41,11,236,0.68)",//大于0小于12为蓝色
},
]
}
]
})
}
},
5. 给线段加上曲线高亮
代码上第4点中也有提到。主要是通过计算得到在范围内的坐标点,然后设置 visualMap 的配置项使某一段连续区间能够单独设置颜色。
php
this.echartsInstance.setOption({
// 在实际项目中是多条曲线的,如果让多条曲线高亮,则数组
visualMap: [
{
type: "piecewise",
show: false,
dimension: 0,
seriesIndex: 0, // 这里需要和之前的 series 对上
piecewise: true,
outOfRange: {
// color: ['#00000', 'red'][index]
color: ['#00000'][0] // 这里如果多条曲线的话 则需要一一对应非框选中的颜色,
},
pieces: [
{
gt: dataList[0].index, //
lt: dataList[dataList.length - 1].index,
color: "rgba(41,11,236,0.68)",//大于0小于12为蓝色
},
]
}
]
})
6. 计算平均值
啊哈,都得到选中数据了,计算平均值当然一口闷掉。
javascript
<template>
<div v-show="selectResult.length > 0" style="text-align: center;" v-html="selectResultStr()"></div>
</template>
getAvgValue(list) {
if (list.length > 1) {
return (list.reduce((acc, cur) => acc + cur, 0) / list.length).toFixed(2)
}
},
selectResultStr() {
if (this.selectResult.length === 0) return ''
return `<span style='margin-right: 30px;'>框选时间: <span style="font-size: 16px;font-weight: bold;">${this.selectResult[0].x} - ${this.selectResult[this.selectResult.length - 1].x}</span>
平均值: <span style="font-size: 16px;font-weight: bold;">${this.getAvgValue(this.selectResult.map(item => item.y))}</span></span>`
},
7. 选择框的交互优化
因为用的是鼠标左键进行点击选择,导致和echarts的拖拽事件冲突,交互不好。所以就使用鼠标右键进行选择,保留echarts默认功能。
kotlin
boxSelectEvent() {
this.echartsGetZr = this.echartsInstance.getZr() // 为了监听折线图空白区域鼠标事件
// 对右键默认事件阻止
this.echartsGetZr.on('contextmenu', () => {
event.preventDefault()
})
setTimeout(() => {
this.echartsGetZr.on('mousedown', (e) => {
// 获取鼠标按下坐标点
if (e.which === 3) {
const {offsetX, offsetY} = e;
this.selectionPoint = {offsetX, offsetY};
// 并设置选择框 初始位置、宽高
this.selectionDiv.style.left = offsetX + "px";
this.selectionDiv.style.top = offsetY + "px";
this.selectionDiv.style.width = "0px";
this.selectionDiv.style.height = "0px";
// 对move、up事件进行监听
this.echartsGetZr.on("mousemove", this.mousemove);
this.echartsGetZr.on("mouseup", this.mouseup);
}
})
}, 1000)
},
结语
echartsDemo: Echarts折线图自定义框选并高亮选中曲线,计算平均值 (gitee.com)
项目简单,请多指正。