echarts拓扑图一些功能实现

初始化成功后的 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 里面是不支持这种的,但是可以手动实现,前提及思路介绍:

  1. 你能获取到所有线路的最终点(两端的节点位置信息)(这些节点最终显示会被隐藏),当然正常显示的节点位置也需要(这个是你看到图上的节点-绿色和灰色的);总共合起来需要三个节点位置(这个为一条线路,两条线路需要5个节点位置以此类推)
  2. 代码实现的第一步:还是需要你把正常的能显示出来(就是 echarts 自带的那种关系图能正常显示)
  3. 第二步:你得喊后台把 多条直线 按以前线路分类给你放到一个地方; 然后把这些线路提取出来,把之前的 links 信息全部替换掉(这个操作是把正常 echarts 里面的线路全部删掉;然后重新赋值:你的现在处理好的直线线路)
  4. 做完第二步你会发现你的线路没法显示,这个是因为没有节点匹配到 echarts 无法划线;进行第三步处理
  5. 第三步: 节点处理;把你产生的所有线路循环生产两端节点( id 或者 name 必须唯一,不然echarts要报错,我这里直接以坐标信息加原本线路名字加随机数,这个名字或者id不需要给用户看);处理完节点后,把这些节点push进你的原有data里面;
  6. 重点:links是全体替换;data是push!!线路全部替换新的,节点是增量!!
  7. 如果这块看不懂可以问问我(我也好改改这块、或者弄个demo)。

14. 如果你的拓扑图实现了上面这一点;然后你会发现如果节点太近了而且两节点之间有多条线路,会导致平行直线它们会分的很开,跟你的节点大小不匹配了。效果如图:

在这里插入图片描述

解决方案:

  1. 第一步:跟你后台确认这个画布的x、y最大值,然后你自己定义四个隐形节点为四方 节点(固定echarts渲染图布大小【非你div渲染大小】) ;这样你的画布就固定了,所有节点显示都在你这个画布里面;此时你控制zoom放大缩小就行了;
  2. 第二步:定位到你需要显示的中心位置(一般是线路,线路两端节点求中间位置就行了,如果是节点那就直接节点就行了);
  3. 设置 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. 介绍一个节点与线路的连接算法

  1. 如果你的拓扑图有这个判断逻辑: 后台返回的数据红黄绿能完整不断线连接起来,后台返回 线路处于哪一级别(红线为0级,与红线两端节点相连的为第1级,再以此分散的为2级…至N级)。线路分为红黄绿三种线,红黄线必显示,红线两节点的绿线必显,其它绿线为红黄线断线的连接线选取(两点之间断线的只取一根绿线);如果有这方面需求可以看看;有相关涉及的可以看看逻辑
  2. 根据节点划分为n级节点数组
  3. 判断后一级的节点的target在当前级节点的source是否能找到;找得到意味着他们是相连的节点;找不到就是不相连的节点
  4. 不相连的节点,咱们需要把它变成相连的节点,这个时候就可以去 全是绿线的节点数组找 或者是全部节点里面找;很明显在绿线节点里面找会少去红黄色节点的循环次数
  5. 最后得到的就是所有相连的节点数组和 相连的节点线路了;
	  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: [],
     }],
   }
  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值