首页 > 今日焦点  >  正文
世界热头条丨可视化—AntV G6 紧凑树实现节点与边动态样式、超过X条展示更多等实用小功能
2023-03-29 16:01:19 来源:博客园

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

目录1. 树图的基本布局和使用2. 根据返回数据的属性不同,定制不一样的节点样式3. 节点 label 文案显示过长时,通过截断的方式,显示...4. 当一个父节点包含children叶子节点时,label后显示children的长度,格式为:node.label(children.length)5. 截断后的label ,通过鼠标悬浮,完全显示在 tooltip 中,定义并改写 tooltip 样式6. 根据节点展开收起状态 动态变更 label 显示位置,展开时在上,收起时在右7. 设置节点的icon样式和背景色(随机色可自行定制)8. 叶子节点超过 (xxx)条, 折叠叶子节点,显示展开更多9. 点击展开更多节点,渲染被折叠的叶子节点10. 定义鼠标点击事件,聚焦当前点击节点至画布中心点11. 定义鼠标移入移出事件12. 根据返回数据的属性不同,定制不一样的 边 样式13. 设置连线上关系文案样式14. 解决画布拖拽,出现黑色残影问题15. Demo 动图演示16. 完整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 显示... 时,显示提示 tipconst 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 = 0graph.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 显示... 时,显示提示 tipconst 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>

标签:

精彩放送