初始化成功后的 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.015function 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.curvatureif (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.lineAveragethis.curvature = (initCurvature * 4 + 0.005) * percentage + 0.02}else this.curvature = initCurvaturejudgeCurvature(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.keyByconst 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)) returnitem.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 = 1return {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 = 0return this.connectLine(needLineList, greenLineList, dataKeyValue)}function handleLine(links) {// needLineList: 保留所有的红黄线;greenLineList: 所有的绿线;const needLineList = [], greenLineList = [];links.forEach(item=>{const dir = item.dirif (!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;断线的默认 falseconst 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 = 1return {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: [],}],}