echarts拓扑图一些功能实现
1. 更新echarts数据2. 给节点添加右键点击事件3. 字体大小跟着 滚动键 缩放4. 线路曲度设置的一些建议5. 线路宽度与颜色的设置6. 节点背景色修改7. 力引导布局设置(节点位置随机但美观,可设置节点拖拽)8. 用vue组件形式添加提示框(echarts 默认不支持vue模板编译)9. 连接线路设置箭头10. 设置自定义工具栏附上一份基础配置......
初始化成功后的 echarts 统一称为 myChart;配置项统一称为 option
1. 更新echarts数据
// 获取当前的 echarts 配置
const option = myChart.getOption()
// 你的对option的操作...
// option 为你的 echarts 的配置
myChart.setOption(option)
2. 重置 echarts 图表(宽高等)
myChart.resize()
3. 给节点添加右键点击事件
myChart.on('contextmenu', (params) => {
// ...
});
4. 字体大小跟着 滚动键 缩放
let fontSize = 12;
// https://echarts.apache.org/zh/option.html#series-graph.nodeScaleRatio
// 节点的缩放比例默认是0.6,echarts 配置项 里面有介绍
const nodeScaleRatio = 0.6;
myChart.getZr().on("mousewheel", function () {
const option = myChart.getOption();
const zoom = option.series[0].zoom;
fontSize = 10 * zoom * nodeScaleRatio;
option.series[0].label.fontSize = fontSize;
option.series[0].label.lineHeight = fontSize * 1.25;
option.series[0].edgeLabel.textStyle.fontSize = fontSize;
myChart.setOption(option)
// 经过四五天踩坑才找到的解决方法(不加这个会导致界面闪屏,且拓扑图中心位置乱变)
myChart.resize()
});
5. 线路曲度设置的一些建议
1). 设置线路曲度配置 总设置路径为 option.series[0].lineStyle.curveness
2). 单独设置线路曲度路径为 option.series[0].links[x].curveness
3). echarts 里面有自带的曲度设置,也就是说你可以设置全局的线路曲度;如果你想要某一类型、某一条线不需要曲度,直接设置这条线的曲度为0就行了;权限是每一条线路的配置权限高于全局(线单独配置>全局配置)
4).基本上全局配置在线路单独配置都支持!而且线 单独配置>全局配置
5). 自己设置线路曲线配置;需求:基础曲线设置为0(option.series[0].lineStyle.curveness = 0)
// 初始曲度
const initCurvature = 0.015
function handleLineCurveness() {
const obj = {},
// 判断传入两个名字不分顺序组合是否在 obj 中有
handleFn = (t, s) => [`${t}${s}`, `${s}${t}`][+(`${s}${t}` in obj)],
pushArr = (arr, param, cb) => (cb&&cb(arr, param)) || arr.push(param),
judgeCurvature = (length, index, link) => {
const curvature = this.curvature
if (length >= 2 && length < 11) link.lineStyle.curveness = (curvature * 2) * Math.ceil((index + 1) / 2);
else if (length > 10) link.lineStyle.curveness = curvature * Math.ceil((index + 1) / 2);
},
links = this.series[0].links;
links.forEach(item => {
let endKey = handleFn(item.target, item.source)
obj[endKey] = obj[endKey] || []
pushArr(obj[endKey], item, (arr, param) => { param.lhIndex = arr.length })
})
links.forEach(item => {
let endKey = handleFn(item.target, item.source)
// 这两个值的来源请看第五点 求节点之间线路长度与之平均数
// 这里是 线路的距离(item.lhDistance) 和 线路距离的平均值(this.lineAverage) 比较 ;如果没有这个需求可以删掉;
// 具体实现效果介绍:线路距离小于平均值的时候;曲度随着长度越短而越大
if (item.lhDistance < this.lineAverage) {
let percentage = (this.lineAverage - item.lhDistance) / this.lineAverage
this.curvature = (initCurvature * 4 + 0.005) * percentage + 0.02
}
else this.curvature = initCurvature
judgeCurvature(obj[endKey].length, item.lhIndex, item)
})
}
6. 求节点之间线路长度与之平均数
function changePosition() {
const { data, links } = this.series[0];
// 这里我这边是以id为拓扑图索引,如果是name的修改为name就行了
// _.keyBy 这个是lodash的方法;这里需求是:生成一个以data数组里面各项id为键而各项为值的对象;
// 官网地址介绍: https://www.lodashjs.com/docs/lodash.keyBy
const dataKey = _.keyBy(data, 'id');
const averageFn = () =>{
// data 以 id为键各项为值 的对象
const allDistance = [];
links.forEach(item=> {
const {x: sx, y: sy} = dataKey[item.source],{x: tx, y: ty} = dataKey[item.target];
const top = Math.abs(sx - tx);
const left = Math.abs(sy - ty);
// 计算出 两点之间的距离
const distance = Math.sqrt(Math.pow(top,2) + Math.pow(left, 2));
// 这里是判断是否有x,y节点数值的地方
if (_.isNaN(distance)) return
item.lhDistance = +distance.toFixed(2);
allDistance.push(distance);
})
// 平均值
return +(allDistance.reduce((pre, cur) => { return pre + cur }) / +allDistance.length).toFixed(2);
}
this.lineAverage = averageFn()
}
7. 线路宽度与颜色的设置
1). 具体位置: 宽度option.series[0].lineStyle.width 颜色option.series[0].lineStyle.color
const option = {
// ...
series: [{
lineStyle: {
width: 5,
color: 'red'
},
data: [
{id: '1'},
{id: '2'}
],
links: [{
target: '1',
source: '2',
lineStyle: {
width: 5,
color: 'red'
}
}]
}]
}
8. 节点背景色修改
option.series[0].itemStyle.color
itemStyle: {
// 修改拓扑图节点背景色 或者直接给入颜色字符串
color: (param) => {
const random = 1
return {
1: '#FF0036',
2: '#FFD200',
3: '#17DF75',
4: '#C7C7C7',
}[random] || '#045b9f'
},
// 文字块边框宽度。
borderWidth: 2,
// 修改拓扑图节点边框色
borderColor: 'rgb(229,229,229)',
}
9. 力引导布局设置(节点位置随机但美观,可设置节点拖拽)
option.series[0].layout = ‘force’
option.series[0].force
force: {
// 边的两个节点之间的距离
edgeLength: 120,
// 节点之间的斥力因子
repulsion: 1500,
// 节点受到的向中心的引力因子。该值越大节点越往中心点靠拢。
gravity: 0.1,
// 这个参数能减缓节点的移动速度。取值范围 0 到 1。
friction: 0.6
},
10. 用vue组件形式添加提示框(echarts 默认不支持vue模板编译)
1). 运用子绝父相(两个盒子,父级盒子里面有子级盒子,父级相对定位【relative】,子级绝对定位【position】)。把vue组件根据触发事件获取x,y位置。然后渲染提示框组件。边缘检测这些都不难;两个盒子的宽高都知道,在哪里召唤出提示框也知道。接下来就很简单了;具体实现看我
右键弹框,根据鼠标位置显示 echarts实现vue自定义组件tooltips
11. 连接线路设置箭头
官网地址
option.series[0].edgeSymbol && option.series[0].edgeSymbolSize
// 线路设置成箭头
edgeSymbol: ["arrow", "arrow"],
// 大小设置
edgeSymbolSize: [10, 10]
12. 设置自定义工具栏
feature: {
// 自定义跳转另一个页面(这里为全屏显示)
myFullScreen: {
show: true,
title: '全屏',
// 工具栏自定义图标
icon: `image://${require('@/assets/images/icon_o.png')}`,
// 设置点击事件
onclick: () => {
const url = window.location.href.replace(this.$route.path, '/xxx')
const other = window.open(url)
other.onload = () => {
// ...
}
}
},
// echarts 自带的 还原
restore: {},
// echarts 自带的 下载
saveAsImage: {}
}
13. 拓扑图线路实现多直线功能如图
正常 echarts 里面是不支持这种的,但是可以手动实现,前提及思路介绍:
- 你能获取到所有线路的最终点(两端的节点位置信息)(这些节点最终显示会被隐藏),当然正常显示的节点位置也需要(这个是你看到图上的节点-绿色和灰色的);总共合起来需要三个节点位置(这个为一条线路,两条线路需要5个节点位置以此类推)
- 代码实现的第一步:还是需要你把正常的能显示出来(就是 echarts 自带的那种关系图能正常显示)
- 第二步:你得喊后台把 多条直线 按以前线路分类给你放到一个地方; 然后把这些线路提取出来,把之前的 links 信息全部替换掉(这个操作是把正常 echarts 里面的线路全部删掉;然后重新赋值:你的现在处理好的直线线路)
- 做完第二步你会发现你的线路没法显示,这个是因为没有节点匹配到 echarts 无法划线;进行第三步处理
- 第三步: 节点处理;把你产生的所有线路循环生产两端节点( id 或者 name 必须唯一,不然echarts要报错,我这里直接以坐标信息加原本线路名字加随机数,这个名字或者id不需要给用户看);处理完节点后,把这些节点push进你的原有data里面;
- 重点:links是全体替换;data是push!!线路全部替换新的,节点是增量!!
- 如果这块看不懂可以问问我(我也好改改这块、或者弄个demo)。
14. 如果你的拓扑图实现了上面这一点;然后你会发现如果节点太近了而且两节点之间有多条线路,会导致平行直线它们会分的很开,跟你的节点大小不匹配了。效果如图:
解决方案:
- 第一步:跟你后台确认这个画布的x、y最大值,然后你自己定义四个隐形节点为四方 节点(固定echarts渲染图布大小【非你div渲染大小】) ;这样你的画布就固定了,所有节点显示都在你这个画布里面;此时你控制zoom放大缩小就行了;
- 第二步:定位到你需要显示的中心位置(一般是线路,线路两端节点求中间位置就行了,如果是节点那就直接节点就行了);
- 设置 zoom 空间高度(options.series[0].zoom)
addLimitArea() {
// 这个数组就是四个方位的节点位置了,然后map返回一个处理好的数组
return [[0, 0], [0, 3400], [2622, 0], [2622, 3400]].map(([x, y]) => ({
// 这个必须保持唯一
id: `${x}-=-${y}`,
// 这个必须保持唯一
name: `${x}-=-${y}`,
// 这个可要可不要,复制粘贴的
symbol: "circle",
// 这个是我加的判断是不需要的节点还是需要的节点(需要的节点:你看上面那个显示出来的节点;不需要的节点:隐藏的节点)
newDataFlag: true,
// x y 定位
x, y,
label: {
// 隐藏你节点的名字显示
show: false
},
symbolSize: 0 // 隐藏你的节点
}))
},
// 这个函数插入到你 echarts 重新渲染前就行了
getCenter(data) {
// 前两个为红线节点
// 定位中心为红线
const { x: x1, y: y1 } = data[0];
const { x: x2, y: y2 } = data[1];
this.series[0].center = [x1 - (x1 - x2) / 2, y1 - (y1 - y2) / 2]
}
15. 介绍一个节点与线路的连接算法
- 如果你的拓扑图有这个判断逻辑: 后台返回的数据红黄绿能完整不断线连接起来,后台返回 线路处于哪一级别(红线为0级,与红线两端节点相连的为第1级,再以此分散的为2级…至N级)。线路分为红黄绿三种线,红黄线必显示,红线两节点的绿线必显,其它绿线为红黄线断线的连接线选取(两点之间断线的只取一根绿线);如果有这方面需求可以看看;有相关涉及的可以看看逻辑
- 根据节点划分为n级节点数组
- 判断后一级的节点的target在当前级节点的source是否能找到;找得到意味着他们是相连的节点;找不到就是不相连的节点
- 不相连的节点,咱们需要把它变成相连的节点,这个时候就可以去 全是绿线的节点数组找 或者是全部节点里面找;很明显在绿线节点里面找会少去红黄色节点的循环次数
- 最后得到的就是所有相连的节点数组和 相连的节点线路了;
myChart.series[0] = this.handleEchartsEnd(charts);
function handleEchartsEnd(charts) {
let { data, links } = charts
// dataKeyValue: 节点的键值对(以id为键)
const dataKeyValue = _.keyBy(data, 'id');
// needLineList: 保留所有的红黄线;greenLineList: 所有的绿线;
const { needLineList, greenLineList } = this.handleLine(links)
this.a = 0
return this.connectLine(needLineList, greenLineList, dataKeyValue)
}
function handleLine(links) {
// needLineList: 保留所有的红黄线;greenLineList: 所有的绿线;
const needLineList = [], greenLineList = [];
links.forEach(item=>{
const dir = item.dir
if (!greenLineList[dir]) greenLineList[dir] = []
// 红色节点连接的线路都要显示
if (item.lineStyle.color === 'green' && +item.dir !== 1) return greenLineList[dir].push(item);
if (!needLineList[dir]) needLineList[dir] = []
needLineList[dir].push(item)
})
return { needLineList, greenLineList }
},
// 处理节点根据等级 dir 得到 具有重复性的 list 数组;后期需要去重
function handleData(line, keyValue) {
const list = [], repeatName = [];
line.forEach(item=> ['target', 'source'].forEach(val => {
const param = item[val], dir = item.dir;
if (!repeatName.includes(param + dir)) repeatName.push(param + dir) && list.push(keyValue[param])
})
)
return list
}
function connectLine(needLineList, greenLineList, dataKeyValue) {
for (let i = 0; i < needLineList.length; i++) {
const dirLineArr = needLineList[i],
nextDirLineArr = needLineList[i + 1]
// 第一级跟第二级比较 当第二级不存在则跳出循环
if (!nextDirLineArr) break;
// 当前级循环
for (const line of dirLineArr)
// 下一级循环
for (const nextLine of nextDirLineArr) {
// 是否是连上的线
nextLine['nextFlag'] = !!nextLine['nextFlag']
// 如果下级循环的 source 和 target 都在 当前级循环的 source 和 target 里面出现过,则此线路为相连的,否则为断线的
// 相连的线路置为 true;断线的默认 false
const nextArr = [nextLine.source, nextLine.target]
if (nextArr.includes(line.target) || nextArr.includes(line.source)) nextLine['nextFlag'] = true
}
// 必须等上面一级循环处理完数据后 主要是 nextFlag 判断依据,判断当前线路与下级线路是否匹配连接过
for (let k = 0; k < nextDirLineArr.length; k++) {
const nextLine = nextDirLineArr[k];
// 如果为 true 的时候则说明线路都是连接起的;否则需要添加绿线连接未连接的线路
if(nextLine.nextFlag) continue;
const nextArr = [nextLine.source, nextLine.target]
// 断线情况需要去绿线列表 的 当前级里面去找与下级出现的 target 或者 source 相同的 然后扔进 needLineList 里面
for (const greenLine of greenLineList[i])
if (greenLine.lineStyle.type !== "dashed" && (nextArr.includes(greenLine.target) || nextArr.includes(greenLine.source))) {
needLineList[i].push(greenLine);
// 当添加了超过100条绿线的时候判断出现死递归!
// 判断出现死递归的时候界面置空不卡顿;
if (++this.a > 100) return {data: [], links: []}
// 处理完一条 线路不连接的情况后 重新执行判断函数,看看还有不连接的情况不
return this.connectLine(needLineList, greenLineList, dataKeyValue)
}
}
}
const endLineList = lhArrayFlat(needLineList, 2);
return {
data: this.handleData(endLineList, dataKeyValue),
links: endLineList
}
},
附上一份基础配置
chartOptions: {
tooltip: {
formatter: (p) => {
let {
name
} = p;
return name
},
toolbox: {
show: true,
feature: {
// dataZoom: {
// yAxisIndex: 'none'
// },
// dataView: {readOnly: false},
// magicType: {type: ['line', 'bar']},
myFullScreen: {
show: true,
title: '全屏',
icon: `image://${require('@/assets/images/icon_o.png')}`,
onclick: () => {
const url = window.location.href.replace(this.$route.path, '/xxx')
const other = window.open(url)
other.onload = () => {
// ...
}
}
},
restore: {},
saveAsImage: {}
}
},
// animationDurationUpdate: 1500,
// animationEasingUpdate: "quinticInOut",
series: [{
type: "graph",
// 力引导模式
// layout: "force",
// 圆圈模式
// layout: 'circular',
// circular: {
// rotateLabel: true
// },
force: {
edgeLength: 120,
repulsion: 1500,
},
symbolSize: 40,
roam: true,
draggable: true,
label: {
show: true,
color: "#fff",
padding: 10,
width: 120,
textShadowBlur: 6,
textShadowColor: 'rgba(43,48,52, .7)',
textShadowOffsetX: 0,
textShadowOffsetY: 0
},
itemStyle: {
// 修改拓扑图节点背景色
color: (param) => {
const random = 1
return {
1: '#FF0036',
2: '#FFD200',
3: '#17DF75',
4: '#C7C7C7',
}[random] || '#045b9f'
},
borderWidth: 2,
borderMiterLimit: 100,
// 修改拓扑图节点边框色
borderColor: 'rgb(229,229,229)',
},
lineStyle: {
opacity: 0.9,
width: 3,
curveness: 0.04,
},
emphasis: {
// focus: 'adjacency',
lineStyle: {
width: 10
}
},
// 线路箭头处理
// edgeSymbol: ["arrow", "arrow"],
// edgeSymbolSize: [10, 10],
edgeLabel: {
fontSize: 12,
color: "#0AACF9",
textStyle: {}
},
// selectedMode:'single',
// select:{
// lineStyle:{
// color: 'red'
// },
// edgeLabel:{
// color:'red'
// }
// },
// 节点数据
data: [],
// 线路数据
links: [],
}],
}
更多推荐
所有评论(0)