• 可视化—AntV G6 紧凑树实现节点与边动态样式、超过X条展示更多等实用小功能


    通过一段时间的使用和学习,对G6有了更一步的经验,这篇博文主要从以下几个小功能着手介绍,文章最后会给出完整的demo代码。

    1. 树图的基本布局和使用

    树图的布局,使用的模板是官网所提供的 紧凑树模板,在此基础上,进行一些定制化的改造,官网紧凑树案例

    属性说明:

    graph = new G6.TreeGraph({
        container,
        width: document.documentElement.clientWidth,
        height:document.documentElement.clientHeight,
        // .....
        layout: {
          type: 'compactBox',    // 布局类型
          direction: 'LR',       // 树图布局方向, 从左向右
          getHeight: function getHeight() { // 高度
            return 16
          },
          getWidth: function getWidth() {  // 宽度
            return 16
          },
          getVGap: function getVGap() {   // 节点之间 垂直间距
            return 25
          },
          getHGap: function getHGap() {  // 节点之间 水平间距
            return 150
          }
        }
    })
    
    

    2. 根据返回数据的属性不同,定制不一样的节点样式

    大部分在实际项目中,数据都是由后端返回,可能会存在多种类型的数据,需要进行不同的处理和展示,那么此时只在 graph 初始化时,定义defaultNode显然是不够用的,G6支持动态改变节点样式。需要在 graph 实例化之后:

    // 以下函数均在下方有实现代码:
    graph.node((node)=> {
      return {
        label: node.label || formatLabel(node),   
        icon: formatIcon(node),
        size: node.size || 40,
        labelCfg: { position: setLabelPos(node) },  // label 显示位置
        style: {
          fill: getNodeColor(),
          stroke: getNodeColor()
        }
      }
    })
    

    3. 节点 label 文案显示过长时,通过截断的方式,显示...

    首先判断当前节点有无子节点,后续会进行label的拼接,显示叶子节点的数量,在进行结束,此书由于数据的原因,案例中数据labelid使用同一个字段,后续同学们可以自己按实际情况进行调整:我截取的长度是 15

    // label 过长截断 显示...
    const formatLabel = (node) => {
      const hasChildren = node.childrenBak?.length || node.children?.length
      const ellipsis = node.id.length > 15 ? '...' : ''
      return `${node.id.slice(0, 15)}${ellipsis}${hasChildren ? ' (' + hasChildren + ')' : ''}`
    }
    

    4. 当一个父节点包含children叶子节点时,label后显示children的长度,格式为:node.label(children.length)

    这个小功能,与上一个label 截断,统一都是处理label的,因此放在同一个函数中返回,其主要实现代码为 这一行:

     return `${node.id.slice(0, 15)}${ellipsis}${hasChildren ? ' (' + hasChildren + ')' : ''}`
    

    5. 截断后的label ,通过鼠标悬浮,完全显示在 tooltip 中,定义并改写 tooltip 样式

    这个小功能设计的改动比较多,同学们在看的时候,不要看错了哈

    第一步:首先定义一个函数,返回tooltip,官网的案例中并不是返回的函数,而是直接返回了一个对象,这个在实际使用过程中会存在问题,就是新增的数据无法使用到这个插件,因此通过函数调用的方式 ,可以解决该现象:

    // label 显示... 时,显示提示 tip
    const treeTooltip = ()=> {
      return new G6.Tooltip({
        offsetX: 10,  // 鼠标偏移量
        offsetY: 20,
        shouldBegin(e: any) {
          return e.item?.get('model')?.label?.includes('...')   // label中有...才显示,表示被截断
        },
        getContent(e: any) {
          let outDiv = document.createElement('div')   // 设置tip容器和样式
          outDiv.innerHTML = `
          

    ${e.item.getModel().id}

    `
    return outDiv }, itemTypes: ['node'] // 表示触发的元素类型 }) }

    第二步:在new 实例化的options中添加 插件使用

    graph = new G6.TreeGraph({
        container,
        width: document.documentElement.clientWidth,
        height:document.documentElement.clientHeight,
        plugins: [treeTooltip()],
        // .....
    })
    

    第三步:完整以上代码后,基本可以看出tip的提示框,但是由于是改写原有的样式,因此还需要 改一下 style, 由于画布操作较多,因此canvas画布,修改鼠标样式,全文只有这里提到了style修改。就写在一起了,实际并不影响 tooltip 功能

    
    

    6. 根据节点展开收起状态 动态变更 label 显示位置,展开时在上,收起时在右

    此功能的设计是为了优化,当节点label过长并展开时,父子之间水平间距不够时会出现文案互相重叠等问题,做了一个小优化,

    首先判断节点是否是展开状态,以及是否有叶子节点,节点有children并展开时在上,其余情况都显示在右边,这是初始化时的代码:

    // 根据节点展开收起状态 动态变更 label 显示位置,展开时在上,收起时在右
    const setLabelPos = (node) => {
      return !node.collapsed && node.children?.length ? 'top' : 'right'
    }
    

    因为树图可以监听节点的展开收起状态,因此在切换的时候,也需要进行 label定位的问题:在new 实例化的options中添加 modes

    graph = new G6.TreeGraph({
        container,
        width: document.documentElement.clientWidth,
        height:document.documentElement.clientHeight,
        plugins: [treeTooltip()],
        // .....
        modes: {
           default: [
              {
                type: 'collapse-expand',
                onChange: function onChange(item: any, collapsed) {
                  const data = item?.get('model')
                  data.collapsed = collapsed
                  const model = {
                    id: data.id,
                    labelCfg: { position: !collapsed ? 'top' : 'right' }
                  }
                  item.update(model)
                  item.refresh()
                  return true
                }
              },
              'drag-canvas',
              'zoom-canvas'
            ]
         }
    }) 
    

    7. 设置节点的icon样式和背景色(随机色可自行定制)

    废话不多话,处理节点的icon图表,图表可以使用图片,也可以使用文字,此处就用文字了,截取的label前两个字符串,并设置背景颜色(随机色可自行定制)

    // 叶子节点 图标处理  截取ID 前两个字符串
    const formatIcon = (node) => {
      node.icon = {
        text: node.id.slice(0,2),
        fill: '#fff',
        stroke: '#fff',
        textBaseline: 'middle',
        fontSize: 20,
        width: 25,
        height: 25,
        show: true
      }
    }
    
    // 叶子节点 背景颜色随机填充
    const getNodeColor = () =>  {
      const colors = ["#8470FF",  "#A020F0", "#C0FF3E", "#FF4500", "#66d6d1"];
      return colors[Math.floor(Math.random() * colors.length)];
    }
    

    8. 叶子节点超过 (xxx)条, 折叠叶子节点,显示展开更多

    当叶子节点很多时,并不想全部展开,而是先只展示一部分,其他节点折叠在 展开按钮中,实现思路,定义一个属性,接受原有全部children, 然后进行截取,在push一个 展开按钮,实现:

    第一步:定义一个childrenBak属性接受 children数据

    //  子还有子,因此需要递归
    const splitChild = (node) => {
      node.childrenBak = node.children ? [...node.children] : []
      let result: any = []
    
      if(node.children){
        result = node.children.slice(0, 5)
        if (node.children.length > 5) {
          result.push({ id: `Expand-${node.id}`, label: ' 展开更多...' })
        }
        node.children = result
        node.children.forEach(child =>{
          splitChild(child)
        })  
      }
    }
    

    9. 点击展开更多节点,渲染被折叠的叶子节点

    第二步:(接功能小8继续)点击展开更多时,显示被折叠的剩余节点,,定义node:click事件

    思路:找到展开更多 , 找到展开更多节点的 父节点, 更新父节点的 children

    graph.on('node:click', (evt) => {
        const { item } = evt
        const node = item?.get('model')
        if (node.id.includes('Expand')) {  // id 中包含Expand 表示是展开更多 
          const parentNode = graph.getNeighbors(item, 'source')[0].get('model')  // 找到展开更多节点的 父节点
          graph.updateChildren(parentNode.childrenBak, parentNode.id)   //  使用上一步声明的childrenBak 更新父节点的 children
        } 
      })
    

    10. 定义鼠标点击事件,聚焦当前点击节点至画布中心点

    小9 说了点击事件,那么点击事件中,还有一个小的优化点,就是将当前点击的节点,移动至画布中心,并赋予高亮选中样式

      const animateCfg = { duration: 200, easing: 'easeCubic' }
      graph.on('node:click', (evt) => {
        const { item } = evt
        const node = item?.get('model')
        // if (node.id.includes('Expand')) {    //  功能点 9 代码
        //   const parentNode = graph.getNeighbors(item, 'source')[0].get('model')
        //   console.log(parentNode,parentNode.childrenBak);
        //   graph.updateChildren(parentNode.childrenBak, parentNode.id)
        // } 
        setTimeout(() => {
          if (!node.id.includes('Expand')) {
            graph.focusItem(item, true, animateCfg)
            graph.getNodes().forEach((node) => {   
              graph.clearItemStates(node)  // 先清空其他节点的 高亮样式
            })
            graph.setItemState(item, 'selected', true)   // selected 需要在实例化处进行定义
          }
        }, 500)
      })
    

    selected 表示的是 nodeStateStyles,也就是节点状态样式

    graph = new G6.TreeGraph({
        container,
        width: document.documentElement.clientWidth,
        height:document.documentElement.clientHeight,
        plugins: [treeTooltip()],
        // .....
        nodeStateStyles: {
          active: {   // 这个用在了 鼠标悬浮,可以自行定义
            fill: 'l(0) 0:#FF4500 1:#32CD32',
            stroke: 'l(0) 0:#FF4500 1:#32CD32',
            lineWidth: 5
          },
          selected: {  // 这个用在了 鼠标选中,可以自行定义
            fill: 'l(0) 0:#FF4500 1:#32CD32',
            stroke: 'l(0) 0:#FF4500 1:#32CD32',
            lineWidth: 5
          }
        }
    }) 
    

    11. 定义鼠标移入移出事件

      graph.on('node:mouseenter', (evt) => {
        const { item } = evt
        graph.setItemState(item, 'active', true)   // active 与 selected 都是节点状态样式
      })
    
      graph.on('node:mouseleave', (evt) => {
        const { item } = evt
        graph.setItemState(item, 'active', false)
      })
    

    12. 根据返回数据的属性不同,定制不一样的 边 样式

    关于节点的差不多介绍完了,关于连线,内容就比较少了,动态定义连线样式,及连线上的文字样式:

    可以根据 link的不同属性自定义 连线颜色和label颜色,因为是测试数据,因此就用一个自增长的数判断奇偶性来进行区分,以便明白其中定制化的方法

    let selfGrowthNum = 0
    graph.edge((edge)=> {
        // let {source, target } = edge   // 解构连线的 起始节点
        selfGrowthNum++
        return {
          style: {
            opacity: 0.5,
            stroke: selfGrowthNum % 2 ? '#ADD8E6' : "#FFDEAD",
            lineWidth: 2
          },
          labelCfg: {
            position: 'end',
            style: {
              fontSize: 16,
              fill: selfGrowthNum % 2 ? '#ADD8E6' : "#FFDEAD",
            }
          },
          label:  selfGrowthNum % 2 ? 'even' : "odd"
        }
      })
    

    13. 设置连线上关系文案样式

    上述代码基本完成了 连线的样式和文案的样式,但此时,线是贯穿文字的,看着比较乱,因此还需要修改连线样式 defaultEdge

    graph = new G6.TreeGraph({
        container,
        width: document.documentElement.clientWidth,
        height:document.documentElement.clientHeight,
        // .....
        defaultEdge: {
          type: 'cubic-horizontal',
          style: {    // 如果不定制化,这个就是默认样式
            opacity: 0.5,
            stroke: '#ccc',
            lineWidth: 2
          },
          labelCfg: {
            position: 'end',   // 文字显示在线段的哪个位置,
            refX: -15,
            style: {
              fontSize: 16,
              background: {
                fill: '#ffffff',  // 给文字添加背景色,解决文字被横穿的问题
                padding: [2, 2, 2, 2]
              }
            }
          }
        }
    })
    

    14. 解决画布拖拽,出现黑色残影问题

    G6 4.x 依赖的渲染引擎 @antv/g@4.x 版本支持了局部渲染,带了性能提升的同时,也带来了图形更新时可能存在渲染残影的问题。比如拖拽节点时,节点的文本会留下轨迹。由于目前 @antv/g 正在进行大版本的升级(到 5.x),可能不考虑在 4.x 彻底修复这个问题。当我们遇到这个问题的时候,可以通过关闭局部渲染的方法解决,但是这样可能导致性能有所降低。

    graph.get('canvas').set('localRefresh', false)。
    

    15. Demo 动图演示

    16. 完整Demo案例

    
    
    <script lang="ts" setup>
    import G6 from "@antv/g6";
    import { onMounted } from "vue";
    
    let graph: any = null;
    
    // 树图初始数据
    const treeData = {
      id: "Modeling Methods",
      color: "",
      children: [
        {
          id: "Classification",
          children: [
            { id: "Logistic regression" },
            { id: "Linear discriminant analysis" },
            { id: "Rules" },
            { id: "Decision trees" },
            { id: "Naive Bayes" },
            { id: "Knearest neighbor" },
            { id: "Probabilistic neural network" },
            { id: "Support vector machine" },
          ],
        },
        {
          id: "Methods",
          children: [
            { id: "Classifier selection" },
            { id: "Models diversity" },
            { id: "Classifier fusion" },
          ],
        },
      ],
    };
    onMounted(() => {
      splitChild(treeData);
      drawTreeGraph();
    });
    
    function drawTreeGraph() {
      if (graph) graph.destroy();
      const container = document.getElementById("container") as HTMLElement;
    
      graph = new G6.TreeGraph({
        container,
        width: document.documentElement.clientWidth - 300,
        height: document.documentElement.clientHeight,
        fitView: false,
        fitViewPadding: [10, 50, 10, 50],
        animate: true,
        plugins: [treeTooltip()],
        defaultNode: {
          type: "circle",
          size: 40,
          collapsed: false,
          style: {
            fill: "#fff",
            lineWidth: 2,
            cursor: "pointer",
          },
          labelCfg: {
            position: "right",
            offset: 10,
            style: {
              fill: "#333",
              fontSize: 20,
              stroke: "#fff",
              background: {
                fill: "#ffffff",
                padding: [2, 2, 2, 2],
              },
            },
          },
          anchorPoints: [
            [0, 0.5],
            [1, 0.5],
          ],
          icon: {
            show: true,
            width: 25,
            height: 25,
          },
        },
        defaultEdge: {
          type: "cubic-horizontal",
          labelCfg: {
            position: "end",
            refX: -15,
            style: {
              fontSize: 16,
              background: {
                fill: "#ffffff",
                padding: [2, 2, 2, 2],
              },
            },
          },
        },
        modes: {
          default: [
            {
              type: "collapse-expand",
              onChange: function onChange(item: any, collapsed) {
                const data = item?.get("model");
                data.collapsed = collapsed;
                const model = {
                  id: data.id,
                  labelCfg: { position: !collapsed ? "top" : "right" },
                };
                item.update(model);
                item.refresh();
                return true;
              },
            },
            "drag-canvas",
            "zoom-canvas",
          ],
        },
        layout: {
          type: "compactBox",
          direction: "LR",
          getHeight: function getHeight() {
            return 30;
          },
          getWidth: function getWidth() {
            return 16;
          },
          getVGap: function getVGap() {
            return 30;
          },
          getHGap: function getHGap() {
            return 150;
          },
        },
        nodeStateStyles: {
          active: {
            fill: "l(0) 0:#FF4500 1:#32CD32",
            stroke: "l(0) 0:#FF4500 1:#32CD32",
            lineWidth: 5,
          },
          selected: {
            fill: "l(0) 0:#FF4500 1:#32CD32",
            stroke: "l(0) 0:#FF4500 1:#32CD32",
            lineWidth: 5,
          },
        },
      });
    
      graph.node((node: { label: any; size: any }) => {
        return {
          label: node.label || formatLabel(node),
          icon: formatIcon(node),
          size: node.size || 40,
          labelCfg: { position: setLabelPos(node) },
          style: {
            fill: getNodeColor(),
            stroke: getNodeColor(),
          },
        };
      });
      let selfGrowthNum = 0;
      graph.edge((edge: any) => {
        // let {source, target } = edge   // 也可以根据 link的属性不同自定义 连线颜色和label颜色,因为是测试数据,因此就用一个自增长的数判断奇偶性来进行区分,以便明白其中定制化的方法
        selfGrowthNum++;
        return {
          style: {
            opacity: 0.5,
            stroke: selfGrowthNum % 2 ? "#ADD8E6" : "#FFDEAD",
            lineWidth: 2,
          },
          labelCfg: {
            position: "end",
            style: {
              fontSize: 16,
              fill: selfGrowthNum % 2 ? "#ADD8E6" : "#FFDEAD",
            },
          },
          label: selfGrowthNum % 2 ? "even" : "odd",
        };
      });
    
      graph.on("node:mouseenter", (evt: { item: any }) => {
        const { item } = evt;
        graph.setItemState(item, "active", true);
      });
    
      graph.on("node:mouseleave", (evt: { item: any }) => {
        const { item } = evt;
        graph.setItemState(item, "active", false);
      });
    
      const animateCfg = { duration: 200, easing: "easeCubic" };
      graph.on("node:click", (evt: { item: any }) => {
        const { item } = evt;
        const node = item?.get("model");
        if (node.id.includes("expand")) {
          const parentNode = graph.getNeighbors(item, "source")[0].get("model");
          console.log(parentNode, parentNode.childrenBak);
          graph.updateChildren(parentNode.childrenBak, parentNode.id);
        }
        setTimeout(() => {
          if (!node.id.includes("expand")) {
            graph.focusItem(item, true, animateCfg);
            graph.getNodes().forEach((node: any) => {
              graph.clearItemStates(node);
            });
            graph.setItemState(item, "selected", true);
          }
        }, 500);
      });
    
      graph.on("canvas:click", () => {
        graph.getNodes().forEach((node: any) => {
          graph.clearItemStates(node);
        });
      });
    
      graph.data(treeData);
      graph.render();
      graph.zoom(0.9);
      graph.fitCenter();
      graph.get("canvas").set("localRefresh", false);
    }
    // label 过长截断 显示...
    const formatLabel = (node) => {
      const hasChildren = node.childrenBak?.length || node.children?.length;
      const ellipsis = node.id.length > 15 ? "..." : "";
      return `${node.id.slice(0, 15)}${ellipsis}${
        hasChildren ? " (" + hasChildren + ")" : ""
      }`;
    };
    
    // 叶子节点 图标处理  截取ID 前两个字符串
    const formatIcon = (node) => {
      node.icon = {
        text: node.id.slice(0, 2),
        fill: "#fff",
        stroke: "#fff",
        textBaseline: "middle",
        fontSize: 20,
        width: 25,
        height: 25,
        show: true,
      };
    };
    
    // 叶子节点 背景颜色随机填充
    const getNodeColor = () => {
      const colors = ["#8470FF", "#A020F0", "#C0FF3E", "#FF4500", "#66d6d1"];
      return colors[Math.floor(Math.random() * colors.length)];
    };
    
    // 根据节点展开收起状态 动态变更 label 显示位置,展开时在上,收起时在右
    const setLabelPos = (node: { collapsed: any; children: string | any[] }) => {
      return !node.collapsed && node.children?.length ? "top" : "right";
    };
    // label 显示... 时,显示提示 tip
    const treeTooltip = () => {
      return new G6.Tooltip({
        offsetX: 10,
        offsetY: 20,
        shouldBegin(e: any) {
          return e.item?.get("model")?.label?.includes("...");
        },
        getContent(e: any) {
          let outDiv = document.createElement("p");
          outDiv.innerHTML = ` ${e.item.getModel().id} `;
          return outDiv;
        },
        itemTypes: ["node"],
      });
    };
    
    // 叶子节点超过 5(xxx)条, 折叠叶子节点,显示展开更多
    const splitChild = (node: any) => {
      node.childrenBak = node.children ? [...node.children] : [];
      let result: any = [];
    
      if (node.children) {
        result = node.children.slice(0, 5);
        if (node.children.length > 5) {
          result.push({ id: `expand-${node.id}`, label: " 展开更多..." });
        }
        node.children = result;
        node.children.forEach((child: any) => {
          splitChild(child);
        });
      }
    };
    script>
    <style scoped>
    #container >>> .g6-component-tooltip {
        background:#333;
        color:#fff;
        padding: 0 8px;
    }
    
    canvas {
      cursor: pointer !important;
    }
    style>
    
    
  • 相关阅读:
    Java集合(Collection List Set Map)
    MySQL主从数据库(主读从写)
    Python学习记录 异常处理
    网件r7000梅林系统5g不稳定 5g信号经常掉线解决方法
    【Linux】(五)GateWay远程开发方式-实验室服务器使用GateWay远程开发
    掌握测评补单技术对Shopee、Lazada店铺有什么好处?
    深入探讨:Spring与MyBatis中的连接池与缓存机制
    基于JAVA中学网站设计与实现演示录像2020计算机毕业设计源码+系统+数据库+lw文档+部署
    Parse [5/10/2020 7:05:04 PM] with format [yyyy-MM-dd] error!
    推荐系统中的特征工程
  • 原文地址:https://www.cnblogs.com/echoyya/p/17189750.html