拖拽折线图的收尾自动闭合成面积图
目前有这样一个需求,在一个统计图中,会有多条可拖拽的折线图,当某条折线图的首尾相连时会自动填充内部变成面积图,分开又变回折线图,如下图
图1和图2都是4个端点,当第一个端点和最后一个端点通过拖拽相连后变回变成图2的效果,并自动填充其中颜色
一 分析
首先思考的几个问题:
- 如何拖拽折线图;
- 首个端点和最后一个端点如何正好重合(当数据过大,很难通过拖拽情况下恰好让两个端点的数值完全相同);
- 如何填充颜色;
以下是官方示例
二 准备工作
1 如何实现拖拽
从官方的示例中我们可以发现,它采用的是graphic
属性.根据官方手册我们可以发现,这个属性可以让我们在echarts
图上自由绘制图形区域,以及这些图形的鼠标按下,抬起,鼠标经过,拖拽等等操作回调,借助echartsInstance.convertToPixel
可进行坐标转换,将每个端点的坐标转换成像素坐标,为我们定点;
定点实现后后借助graphic
中的ondrag
回调函数便可知我们目前已拖拽的点的坐标,拿到的是像素坐标,再借助echartsInstance.convertFromPixel
将坐标转换成我们目前可用数值
2 如何实现首尾相连
如果单靠拖拽来实现首尾两个坐标点一致太难了,因为这中间会产生特别小的误差使其无法完全一致
可借助下面这个函数算出两个坐标点的距离,可在距离小于一定数值后让两个坐标点相连
function distant(a, b) {
let dx = Number(a[0]) - Number(b[0])
let dy = Number(a[1]) - Number(b[1])
return Math.pow(dx * dx + dy * dy, .5)
}
3 如何实现内容填充
根据官方手册series-line.areaStyle
的配置项可自定义填充颜色,因为存在首尾坐标在拖拽下合并或者分开两种情况随时变换,当我们给这个属性null
时会让填充失效,当给{}
则会使用默认的填充配置
4 多条折线图存在的注意事项
官方示例中均为单条折线,但是在多条线中,我们要考虑的问题便增加了,在拖拽的时候要注意我们拖拽的是第几条线,拖拽完毕还要将拖拽后的数值回填到定义的数据源中
5 限制拖拽端点数值
若限制端点拖拽到坐标外,可在ondrag
获取到当前数值的最大值最小值判定,然后return
即可
三 示例
本糖已经开出来有人看的还是有点懵,于是完整代码在下面了,复制到本地运行看看吧;
也可点击 在线示例 ,不过在线示例是放在本糖的服务器上,哪天本糖穷到没钱续费服务器就访问不到了,还是复制到本地看最保险
<!--
THIS EXAMPLE WAS DOWNLOADED FROM https://echarts.apache.org/examples/zh/editor.html?c=line-draggable&code=MYewdgzgLgBBCeBbARiANgZQJYC8CmMAvDAEwAMA3AFCiSwAmAhlI0TANpUwcAsZANDAC0ARjIBdflw5CAzAOEBWSdPZCA7ADYAdIsHkV3NZtm7BfQzJIltIwYolVx1EAAcoWcGwDe0j1DQ8AC4YX25uKDwADygQgHIAFQAneBgAESTGAHMsrDAsmCgACzwIAgAFEDyoCDipcJhAgDNYmDjgPDBIpLjpAF96wpB0D1cQsPCoJKwcvCSAeTB4sHA8OuluJpAkxGZukKaAVzBgDy8ACldGTMQIAEpQjfCkvChDpLAYc6eGuIANeIwADUP3CVxuEG0TBY7Ak2igIAAYlgonh6OcSA8QQ1fgAeZBJAB8AE1AdicdxwYxblDmIx2CJxPCkSi0Ri7qC7tQGn1-oMstN6OMngixm0ABwAUnWDVQUARiHiIhI0r50iiAEEolgIMKGog8iFRGQBE9dlEQupTQ0oPBXME2gA3RhoQ5rQbcRjaiAAGTyDu8MHAAC05iADi6yjBedwBtJ4FqdXrwgalsJ5B6YOaQpprZM7Q64s7Xe6nl6dX6wAGg2BQ0lwzAmpGCDHo4NoYxg8NFRwnhMbQX4hA0Fh6HMZTjNd6AJJgMcWmB5hpNLBoboAWRAY-Wq16PMz_fz9qHI7HPUz3ATM7n0RCS_CK7Xc0327aKyre_CcYah4ig7aeQQKOpYUlOOqzvOd4Xo2q4bluhbvmsTzfuEv6FP-cSAcBE4NFe4E3gu96bLBz7wTuH7IdIlhlNMpQhJwP6gqO8SMDhR6FiOH7QRAiDDMUIRTG60HXHgjAYLagTJhS2wzIacSdPQn77qCCAoOg2D4CEqmoJguB4NBHYhB2lHcOIVB9NQZRQAkWCIHgICHFA5xHCcZyfOcDwTAA9F5MAavQ9BwEUjD0CAADuMDAFgSTAIEEBfGFRRYMARQwDqMArLAjo6lgyCBA8CIwJ0jB5QQ9CZFk2jSIg8AAMLBUkUDaFZ8zuJ4YDnIeAqMK4SXAEZdLaLsrjOccpztV8WCRIgggdhB0SeaCLxvB8jwUn-x5tFFMWBGxDSuCAQFuSENX1dcTW0I6czWSA5Ssmg5xxAKo51Gl013NB3AQMFm1oQ0wCEZ94TAPAUGgg0SRaUgOkaQQvkkODbaI3k2VAaVAlJEJiPldkWQlZJhSY_piPgDjWQHGNblfPQUSzfAi3reE4CVNUGS43kWTnHNBGCOwxQ6totOFElkLwOIXKIyh0lgLxhxlLxV0U65E0eWtjNBeFCTDGuWAjdz84S4zUs4uAstlA5rQueNFwM-rSVjlrIy61zdLzVEhvrcbDQ4CEYhkKCFnIRysYSwMi4S2FeShWF2ghfQACiV1dH60CdHMj0vEB-CvYcrjQnglRHe1EunQ1TXgI9HZdiAiA53nzAF4dU3F9QVtU7n-eF83Fe26X53Na8rVuZ1Tzdb1yUDSwQ09aNysXFNeAzTA-sLWrEOvO8nx_TAB1F-AJ11WX2iXddWt3aiD1PYKr0L4gwc4oHPL330odUG3E3fZr2ujC7LBu73h9-70B1FcKAKUNTWw6oeW0m04ifzCjZVwbEaJYFKG7MGDQV4Liwf0V-78vD2zwI7HWetXYEQAWdRqUIQHMHAZAkeA5YGEMQZ-F-1BeT4M-MzKoXQ2Y5A5r_RgbtBC71th2dgWDxBsD7lQk-jVET1kQOfPAl9nqKREYdD2PkYAAFV66RGXnSaqgCqEtTahXQ8KC6K9gpNvZibRWJA0MoYlgAcnhmRDuwoAA
请注意,该图表不是 Apache ECharts 官方示例,而是由用户代码生成的。请注意鉴别其内容。
-->
<!DOCTYPE html>
<html lang="zh-CN" style="height: 100%">
<head>
<meta charset="utf-8">
<style>
body {
height: 100%;
}
.main {
height: 70%;
}
#container {
flex: 1;
height: 100%;
margin: 0
}
.tip {
padding: 10px 100px;
}
</style>
</head>
<body>
<div class="main">
<div id="container"></div>
<div class="tip">
<b>说明:</b>做了一个简单的示例,可以随意拖拽,拖拽后收尾相连开始判定可围成一个区域,首尾相连过程中会通过坐标计算两点距离,距离小于2时会自动吸附连接</br>
圈定的范围颜色是根据线的颜色变化</br>
首尾相连后可一起拖拽,快速拖拽可分离收尾连接,同时判定圈定范围失效</br>
注意多条线的拖拽逻辑是将所有可拖拽线的点压到一个数组里,处理拖拽后逻辑要根据原始数组下标确定是拖动了哪条线
</div>
</div>
<script type="text/javascript" src="https://fastly.jsdelivr.net/npm/echarts@5.3.0/dist/echarts.min.js"></script>
<!-- Uncomment this line if you want to dataTool extension
<script type="text/javascript" src="https://fastly.jsdelivr.net/npm/echarts@5.3.2/dist/extension/dataTool.min.js"></script>
-->
<!-- Uncomment this line if you want to use gl extension
<script type="text/javascript" src="https://fastly.jsdelivr.net/npm/echarts-gl@2/dist/echarts-gl.min.js"></script>
-->
<!-- Uncomment this line if you want to echarts-stat extension
<script type="text/javascript" src="https://fastly.jsdelivr.net/npm/echarts-stat@latest/dist/ecStat.min.js"></script>
-->
<!-- Uncomment this line if you want to use map
<script type="text/javascript" src="https://fastly.jsdelivr.net/npm/echarts@4.9.0/map/js/china.js"></script>
<script type="text/javascript" src="https://fastly.jsdelivr.net/npm/echarts@4.9.0/map/js/world.js"></script>
-->
<!-- Uncomment these two lines if you want to use bmap extension
<script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=YOUR_API_KEY"></script>
<script type="text/javascript" src="https://fastly.jsdelivr.net/npm/echarts@5.3.2/dist/extension/bmap.min.js"></script>
-->
<script type="text/javascript">
var dom = document.getElementById('container');
var myChart = echarts.init(dom, null, {
renderer: 'canvas',
useDirtyRect: false
});
var app = {};
var option;
const symbolSize = 20;
const data = [
[
[10, 56],
[14, 80],
[17, 63],
[22, 60],
],
[
[47, 156],
[34, 140],
[37, 100],
[40, 80],
[52, 90]
],
[
[10, 149],
[5, 109],
[20, 100],
[16, 145],
]
];
option = {
color: ['green', 'red','blue'],
title: {
text: '拖拽示例',
left: 'left'
},
legend: {
data: data.map((e,i)=>'图'+(i+1))
},
tooltip: {
triggerOn: 'none',
formatter: function (params) {
return (
'X: ' +
params.data[0].toFixed(2) +
'<br>Y: ' +
params.data[1].toFixed(2)
);
}
},
grid: {
top: '8%',
bottom: '12%'
},
xAxis: {
min: -90,
max: 90,
type: 'value',
axisLine: {onZero: false}
},
yAxis: {
min: -180,
max: 180,
type: 'value',
axisLine: {onZero: false}
},
dataZoom: [
// {
// type: 'slider',
// xAxisIndex: 0,
// filterMode: 'none'
// },
// {
// type: 'slider',
// yAxisIndex: 0,
// filterMode: 'none'
// },
{
type: 'inside',
xAxisIndex: 0,
start: 30,
filterMode: 'none',
},
{
type: 'inside',
yAxisIndex: 0,
start: 40,
filterMode: 'none'
}
],
series: data.map((item,index)=>{
return {
id: 'obj'+index,
name: '图'+(index+1),
type: 'line',
smooth: true,
areaStyle: null,
symbolSize: symbolSize,
data: data[index]
}
})
};
// Add shadow circles (which is not visible) to enable drag.
dragListener()
function dragListener() {
setTimeout(function () {
let graphicList = []
data.map((dataItem,dataIndex)=>{
graphicList=[...graphicList,...dataItem.map(function (item, index) {
return {
type: 'circle',
position: myChart.convertToPixel('grid', item),
shape: {
cx: 0,
cy: 0,
r: symbolSize / 2
},
invisible: true,
draggable: true,
ondrag: function (dx, dy) {
onPointDragging(dataIndex, index, [this.x, this.y]);
},
onmousemove: function () {
showTooltip(dataIndex,index);
},
onmouseout: function () {
hideTooltip(index);
},
z: 100
};
})]
})
myChart.setOption({
graphic:graphicList
});
}, 0);
}
function showTooltip(dataIndex,index) {
myChart.dispatchAction({
type: 'showTip',
seriesIndex: dataIndex,
dataIndex: index
});
}
function hideTooltip(dataIndex) {
myChart.dispatchAction({
type: 'hideTip'
});
}
function distant(a, b) {
let dx = Number(a[0]) - Number(b[0])
let dy = Number(a[1]) - Number(b[1])
return Math.pow(dx * dx + dy * dy, .5)
}
function onPointDragging(dataIndex, index, pos) {
let dataPos = myChart.convertFromPixel('grid', pos);
if (dataPos[0] > option.xAxis.max || dataPos[0] < option.xAxis.min || dataPos[1] > option.yAxis.max || dataPos[1] < option.yAxis.min) {
return dragListener()
}
data[dataIndex][index] = myChart.convertFromPixel('grid', pos);
if (distant(data[dataIndex][0], data[dataIndex][data[dataIndex].length - 1]) < 2) {
data[dataIndex][0] = data[dataIndex][data[dataIndex].length - 1];
}
// Update data
option.series[dataIndex].data = data[dataIndex]
option.series[dataIndex].areaStyle = distant(data[dataIndex][0], data[dataIndex][data[dataIndex].length - 1]) == 0 ? {} : null
myChart.setOption({
series: option.series
});
dragListener()
}
if (option && typeof option === 'object') {
myChart.setOption(option);
}
window.addEventListener('resize', dragListener);
myChart.on('dataZoom', dragListener);
window.addEventListener('resize', myChart.resize);
</script>
</body>
</html>