使用G6实现树图|2022.3

G6介绍

AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单 方便、专业可靠、无限可能的数据可视化最佳实践。

G6 是一个简单、易用、完备的图可视化引擎,它在高定制能力的基础上,提供了一系列设计优雅、便于使用的图可视化解决方案。能帮助开发者搭建属于自己的图可视化、图分析、或图编辑器应用。

G6官网

创建树图实例TreeGraph

TreeGraph 是 G6 专门为树图场景打造的图,所有的 G6 节点实例操作以及事件,行为监听都在 TreeGraph 实例上进行。TreeGraph 的初始化通过 new 进行实例化,实例化时需要传入需要的参数。

js 复制代码
const graph = new G6.TreeGraph({
  container: 'container', //图的 DOM 容器,可以传入该 DOM 的 id 或者直接传入容器的 HTML 节点对象。
  width: 500, //单位为 'px'
  height: 500,
  defaultNode: {
    type: "card-node",//自定义节点名
    size: [100, 40],
  },
  defaultEdge: {
     type: "cubic-horizontal",//内置边
     style: {
     stroke: "gray",
  },
  modes: {
  	default: ["zoom-canvas",{type: "drag-canvas",allowDragOnItem:true}],
  	edit: ['click-select']
  }, 
  layout: {
    type: 'dendrogram',
    direction: 'LR', // H / V / LR / RL / TB / BT
    indent:20, //列间间距
    nodeSep: 50,
    rankSep: 100,
    radial: true,
  },
});
graph.data(data);//添加数据
graph.render();//渲染
graph.fitView();//让画布内容适应视口
graph.on("node:click", (e) => {
//当点击node时的回调函数
});

自定义节点

内置节点内置边不满足需求时,可以通过 G6.registerNode(nodeName, options, extendedNodeName) 方法自定义节点,使用registerEdge(edgeName, options, extendedEdgeName)自定义边,Combo使用G6.registerCombo(comboName, options, extendedComboName)

js 复制代码
G6.registerNode(
  'nodeName',
  {
    /**
     * 绘制节点和边,包括节点和边上的文本,返回图形的 keyShape
     * @param  {Object} cfg 节点的配置项
     * @param  {G.Group} group 图形分组,节点中的图形对象的容器
     * @return {G.Shape} 绘制的图形,通过 node.get('keyShape') 可以获取到
     */
    draw(cfg, group) {},
    /**
     * 绘制完成以后的操作,用户可继承现有的节点或边,在 afterDraw() 方法中扩展图形或添加动画。
     */
    afterDraw(cfg, group) {},
    /**
     * 更新节点或边,包括节点或边上的文本,重写后在更新时将会调用该方法替代 draw,起到性能优化的作用
     * @override
     * @param  {Node} node 节点
     */
    update(cfg, node) {},
    /**
     * 更新完以后的操作,如扩展图形或添加动画。,一般同 afterDraw 配合使用
     * @param  {Node} node 节点
     */
    afterUpdate(cfg, node) {},
    /**
     * 用于响应外部对元素状态的改变。当外部调用 graph.setItemState(item, state, value) 时,该函数作出相关响应,主要是交互状态,业务状态请在 draw 方法中实现
     * 单图形的节点仅考虑 selected、active 状态,有其他状态需求的用户自己复写这个方法
     * @param  {String} name 状态名称
     * @param  {Object} value 状态值
     * @param  {Node} node 节点
     */
    setState(name, value, node) {
   }},
    /**
     * 获取锚点(相关边的连入点)
     * @param  {Object} cfg 节点的配置项
     * @return {Array|null} 锚点(相关边的连入点)的数组,如果为 null,则没有锚点
     */
    getAnchorPoints(cfg) {},
  },
  'extendedNodeName',//可以继承内置节点进行扩展
);

图中的元素由多个图形(Shape)组成,在draw中,通过group.addShape()来添加图形。可选择的图形有:circle:圆、rect:矩形、ellipse:椭圆、polygon:多边形、image:图片、marker:标记、path:路径、text:文本、dom(svg):DOM(图渲染方式 renderer 为 'svg' 时可用)。

用x、y等属性控制位置((0, 0) 是该节点的中心),width、height等调整大小。

js 复制代码
group.addShape('rect', {
  attrs: {
    x: 150,
    y: 150,
    width: 150,
    height: 150,
    stroke: 'black',
    radius: [2, 4],
  },
  // must be assigned in G6 3.3 and later versions. it can be any value you want
  name: 'rect-shape',
});

对于过长的内容,文本会超出外框:

可以通过使用 JS 来计算文本长度,也可以通过使用 \n 来进行换行或添加省略符号。G6 提供了 Util.getLetterWidth 与 Util.getTextSize 辅助计算文本长度,但仅适用于默认字体。

js 复制代码
//计算节点宽度,改变外框大小
const getMaxWidth = (strList = [], fontSize = 14) => {
  let maxWidth = 100;
  const pattern = new RegExp("[\u4E00-\u9FA5]+");
  if (strList.length) {
    strList.map((str) => {
    let currentWidth = 0;
    str.split("").forEach((letter, i) => {
      if (pattern.test(letter)) {
         currentWidth += fontSize;
      } else {
         currentWidth += G6.Util.getLetterWidth(letter, fontSize);
      }
    });
    if (currentWidth > maxWidth) {
      maxWidth = currentWidth;
    }
  });
	}
   return maxWidth;
};
//超出时添加省略符号
const getMaxWidth = (str , fontSize = 14) => {
  const maxWidth=100
  const ellipsis = '...';
  const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0]
  let currentWidth = 0;
  let res = str;
  const pattern = new RegExp('[\u4E00-\u9FA5]+'); 
  str.split('').forEach((letter, i) => {
    if (currentWidth > maxWidth - ellipsisLength) return;
    if (pattern.test(letter)) {
       currentWidth += fontSize;
    } else {
       currentWidth += G6.Util.getLetterWidth(letter, fontSize);
    }
    if (currentWidth > maxWidth - ellipsisLength) {
      res = `${str.substr(0, i)}${ellipsis}`;
    }
  });
  return res;
};

modes

modes用于管理图的交互,比如拖拽画布、放大缩小、节点点击、hover等行为

可以设置多个模式,在特定的情况下使用graph.setMode('edit')来切换使用的模式,同样的,可以使用graph.addBehaviors('drag-canvas', 'default')和graph.removeBehaviors('drag-canvas', 'edit')来增加或移除Behavior

G6的内置交互可以处理较为常见的情况:内置Behavior

对于内置交互不能满足条件的情况,也可以使用自定义交互 registerBehavior来定制化开发:自定义交互 Behavior

js 复制代码
G6.registerBehavior('activate-node', {
  getDefaultCfg() {
    return {
      multiple: true
    };
  },//定义自定义 Behavior 时的默认参数,会与用户传入的参数进行合并。
  getEvents() {
    return {
      'node:click': 'onNodeClick',
      'canvas:click': 'onCanvasClick'
    };
  }
  onNodeClick(e) {
    const graph = this.graph;
    const item = e.item;
    //通过 hasState 方法判断元素的某种状态是否是激活态,从而判断是否应该激活另一个状态
    if (item.hasState('active')) {
      graph.setItemState(item, 'active', false);
      return;
    }
    // this 上即可取到配置,如果不允许多个 'active',先取消其他节点的 'active' 状态
    if (!this.multiple) {
      this.removeNodesState();
    }
    // 置点击的节点状态 'active' 为 true
    graph.setItemState(item, 'active', true);
  },
  onCanvasClick(e) {
    // shouldUpdate 可以由用户复写,返回 true 时取消所有节点的 'active' 状态,即将 'active' 状态置为 false
    if (this.shouldUpdate(e)) {
      removeNodesState();
    }
  },
  removeNodesState() {
    graph.findAllByState('node', 'active').forEach(node => {
        graph.setItemState(node, 'active', false);
      });
  }
});

Behavior 默认包含 shouldBegin,shouldUpdate,shouldEnd 三个回调,代表是否开始行为,是否更新元素,是否进行结束行为,当返回值为 false 时阻止默认行为。

getEvents返回该 Behavior 所需监听事件的对象其中标识了事件和回调函数的对应关系,可用事件:交互事件,在回调函数中自行处理逻辑;

如果需要改变样式,可以通过graph.setItemState(item, state, value)改变节点的状态,不同的状态对应不同的样式,而各状态所对应的样式可以在三个地方配置:

  1. 在实例化 Graph 时,通过 nodeStateStyles 和 edgeStateStyles 对象定义(对于自定义节点不生效,参考第三条);
  2. 在节点/边数据中,在 stateStyles 对象中定义状态(默认为styles),使用这种方式可以为不同的节点/边分别配置不同的 state 样式;
  3. 在自定义节点/边时,在registerNode的setState中定义不同状态的样式时的更改。
js 复制代码
//内置节点
nodeStateStyles: {
  // 二值状态 hover 为 true 时的样式
  hover: {
    // keyShape 的状态样式
    fill: '#d3adf7',
    // name 为 node-label 的子图形在该状态值下的样式
    'node-label': {
      fontSize: 15
    },
  },
}
//自定义节点
setState(name, value, node) {
  const group = item.getContainer();
  const shape = group.get('children')[0]; // 需要更改的图形,顺序根据 draw 时确定
  if (name === 'active') {
    if (value) {
       shape.attr('fill', 'red');
    } else {
       shape.attr('fill', 'white');
    }
}},

注意:后设置的状态(通过 graph.setItemState)优先级高于前者,以此来控制各状态样式冲突时的显示

layout

布局配置项,使用 type 字段指定使用的布局方式,树图的type 可取以下值:

CompactBox 紧凑树布局 Dendrogram 生态树布局 Indented 缩进树布局 Mindmap 脑图树布局

注:树图不支持自定义布局

绑定事件

graph.on(eventName, handler) 来进行交互事件的监听

graph.emit(eventName, params) 手动触发某个事件

graph.off(eventName, handler) 为图解除指定的事件监听

graph.off(eventName) 为图解除某事件的所有监听

graph.off() 为图解除所有监听

数据更新

树图的数据一般是嵌套结构,边的数据隐含在嵌套结构中,并不会特意指定 edge 。此布局要求数据中一个节点需要有 id 和 children 两个数据项

js 复制代码
const data = {
  id: 'root',
  children: [
    {
      id: 'subTree1',
      children: [...]
    },
    {
      id: 'subTree2',
      children: [...]
    }
  ]
};

树图的子树更新可以使用addChild(childData, parentNode/parentId)、updateChild(childData, parentId)、updateChildren(childData, parentId)、removeChild(id)

js 复制代码
const data = {
  id: 'sub1',
  children: [
    {
      id: 'subTree1',
      children: [...]
    },
    {
      id: 'subTree2',
      children: [...]
    }
  ]
};

treeGraph.addChild(data, 'root')

更新子树的数据并不会触发父节点的更新,因此如果需要同时更改父节点的设置,可以graph.refreshItem(item)刷新父节点或graph.setItemState更改父节点的对应状态

例:需要让图中只有叶子结点有操作菜单

注:将会直接使用 data 对象作为新增节点/边的数据模型,G6 内部可能会对其增加或修改一些必要的字段。若不希望原始参数被修改,建议在使用深拷贝后的 data。

插件Plugins

使用方法:

js 复制代码
// 实例化 Grid 插件
const grid = new G6.Grid();
const minimap = new G6.Minimap();
const graph = new G6.Graph({
  //... 其他配置项
  plugins: [grid, minimap], // 配置 Grid 插件和 Minimap 插件
});

此外,类似menu、legend的功能可以通过自定义组件完成,需要注意的是使用不同的坐标系:clientX,clientY原点是浏览器的左上角,canvasX、canvasY、pointX、pointY的原点为画布左上角,同时图的缩放、平移其实是整个 pointX/pointY 坐标系的缩放和平移。

js 复制代码
//悬浮 DOM 挂载在 body 上
document.body.appendChild(floatDOM);
graph.on('canvas:click', event => {
  floatDOM.style.left = event.clientX;
  floatDOM.style.top = event.clientY;
})
//悬浮 DOM 挂载在 Container DOM 上
container.appendChild(floatDOM);
graph.on('canvas:click', event => {
  floatDOM.style.marginLeft = event.canvasX;
  floatDOM.style.marginTop = event.canvasY;
});
相关推荐
a栋栋栋2 小时前
apifox
java·前端·javascript
lauo5 小时前
【智体OS】官方上新发布“空钥登陆”--方便访客使用智体操作系统OS和智体应用
前端·javascript·分布式·机器人·开源
哥谭居民00015 小时前
普通的树形数据primevue的treetable组件的treetable[ ]
前端·javascript·算法
sorryhc5 小时前
基于H5请求劫持能力如何设计一款异常监听SDK?
前端·javascript·架构
粥里有勺糖5 小时前
视野修炼-技术周刊第115期 | 现代的 Nodejs 能力
前端·javascript·github
徐小黑ACG5 小时前
JavaScript 基础
开发语言·javascript
郑大乾6666 小时前
vuex - 第一天
javascript·vue.js·node.js
阿卡基YUAN6 小时前
JavaScript 箭头函数
前端·javascript
湛海不过深蓝6 小时前
【js】记录预览pdf文件
开发语言·javascript·pdf
兮动人7 小时前
vue之axios基本使用
前端·javascript·vue.js