D3和X6

D3

版本

d3已经更新到v7版本,中文文档只更新到v4版本,存在部分api不适用和过时问题

使用d3-darge插件布局,插件适配d3版本为v5,近年未更新

API

使用darge中setNodesetEdge绘制node和edge

使用d3中selectionzoom函数实现元素选择和缩放拖拽功能

使用darge的render方法绘制svg,selection的on事件对元素进行监听和方法执行

UI交互

使用element-ui的dropmenu实现下拉菜单功能,需功能和样式订制,代码修改

提示框使用css和d3animation实现

所有的弹出层样式(坐标)需要手动计算

增删改查

每次做出修改需要手动重绘页面,新添加node和edge需要携带完整配置,否则使用默认样式等

重绘后新添加元素手动监听各种事件,无法使用事件委托机制,或使用复杂

其他

拖拽元素、箭头等功能需要d3原生api进行大量算法编程,无现成api提供使用,写法繁琐复杂

流程分组、定制等逻辑需要自行开发,无法避开

X6

与G6的对比

使用场景 性能 定制化和学习
X6 偏重于图编辑,节点拖拽、锚点拖拽、编辑形状等 基于svg创建dom元素,节点或编辑元素过多会影响性能 基于svg(html),定制组件便利
G6 偏重于图可视化,分析数据(GIS)等 基于canvas,大量数据依然流畅渲染 深入了解canvas,不便于调试

具体功能实现

创建node和edge

  • d3
    createNode(id, node) {
      this.g.setNode(id, {
        ...node,
        rx: 5,
        ry: 5
      });
    },
    createEdge(source, target, edge) {
      this.g.setEdge(source, target, {
        ...edge,
        arrowhead: "vee",
        curve: d3.curveBasis
      });
    },

// style
	g {
    &.edgeLabel {
      user-select: none;
      cursor: pointer;
      .high-light {
        fill: #c4e3b6;
      }
    }
    &.edgePath {
      .path {
        stroke: #333;
        stroke-width: 2px;
      }
      .high-light {
        stroke: #c4e3b6;
      }
      marker path {
        fill: #a3a3a3;
        stroke: #5d5d5d;
        &.high-light {
          fill: #c4e3b6;
        }
      }
    }
    &.node {
      user-select: none;
      cursor: pointer;
      fill: #fff;
      stroke: #ddd;
      &.high-light {
        stroke: #c4e3b6;
        stroke-width: 2px;
      }
      text {
        fill: #4a4a4a;
        stroke: none;
        font-size: 16px;
        font-weight: 600;
      }
    }
  }

  • x6
		formatNode(id: string, node: Node.Metadata) {
      return {
        id,
        ...node,
        attrs: {
          body: {
            fill: "#5f95ff",
            stroke: "transparent",
            rx: 5,
            ry: 5
          },
          label: {
            fill: "#ffffff"
          }
        }
      };
    },
    formatEdge(source: string, target: string, edge: Edge.Metadata) {
      return {
        source,
        target,
        attrs: {
          line: {
            stroke: "#a2b1c3",
            strokeWidth: 2
          }
        },
        labels: [
          {
            markup: [
              {
                tagName: "rect",
                selector: "body"
              },
              {
                tagName: "text",
                selector: "label"
              }
            ],
            attrs: {
              label: {
                cursor: "pointer",
                text: edge.label,
                textAnchor: "middle",
                textVerticalAnchor: "middle",
                fontSize: 14,
                fill: "#8d939b"
              },
              body: {
                cursor: "pointer",
                ref: "label",
                refX: "-20%",
                refY: "-20%",
                refWidth: "140%",
                refHeight: "140%",
                fill: "#d3d3d3",
                stroke: "#a2b1c3",
                strokeWidth: 1
              }
            }
          }
        ]
      };
    },

修改node和edge

  • d3的增删改查需要自己去写逻辑(使用form表单提交),再去重绘svg,事件监听需要重新加在每一个node和edge上
  • x6可以使用封装好的tools,事件监听也是使用委托代理到graph上,自动更新视图
			// 鼠标移入节点显示
      this.g.on("node:mouseenter", ({ e, node, view }) => {
        node.addTools({
          name: "button-remove",
          args: {
            x: 0,
            y: 0,
            offset: { x: 10, y: 10 }
          }
        });
      });
      // 鼠标移出节点显示
      this.g.on("node:mouseleave", ({ node }) => {
        node.removeTools();
      });
      // 双击节点修改label
      this.g.on("node:dblclick", ({ node, e }) => {
        node.addTools({
          name: "node-editor",
          args: {
            event: e
          }
        });
      });
    }

x6在使用editor-tool时,setText有天坑,源码也会有些bug,需优化

// node
setText: (args: any) => {
  (
      CellEditor.NodeEditor.getDefaults() as {
      setText: (args: any) => void;
  }
  ).setText(args);
}

// edge
// 首先明确edge只存在一个label,有label时事件只作用于label上
if (edge.labels.length && e.target.nodeName === "path") {
  return;
}
// 再优化重写setText逻辑
setText: ({
  cell,
  value,
  index
}: {
  cell: Edge;
  value: string;
  index?: number;
}) => {
  if (index === -1) {
    if (value) {
      cell.appendLabel({
        position: {
          distance: 0.5
        },
        ...this.edgeLabelMarkUp(value)
      });
    }
  } else {
    if (value) {
      cell.prop(`labels/${index}/attrs/label/text`, value);
    } else if (typeof index === "number") {
      cell.removeLabelAt(index);
    }
  }
}

使用element UI插件

x6对react的ant-design框架提供了非常友好的支持,可以安装@antv/x6-react-components开箱即用,无需写适配逻辑和样式。在vue的elementUI中,使用了d3一样的做法,引入UI组件后进行样式、功能二次开发

    <el-dropdown ref="menuDown" trigger="click" placement="bottom">
      <div></div>
      <el-dropdown-menu class="x6-down-menu" slot="dropdown">
        <el-dropdown-item
          v-for="(item, index) in dropdown"
          :key="index"
          :icon="item.icon"
          :command="item.command"
          >{{ item.label }}</el-dropdown-item
        >
      </el-dropdown-menu>
    </el-dropdown>

使用ref定位到组件后,手动调用show()hide() 方法,再加上改变横竖坐标定位

// d3中计算坐标,鼠标坐标通过d3.mouse获取
			const svgGroup = this.svgGroup.node();
      const mouse = d3.mouse(svgGroup);
      const transform = d3.zoomTransform(svgGroup);
      tooltip
        .html(content)
        .style("left", mouse[0] * transform.k + transform.x + 15 + "px")
        .style("top", mouse[1] * transform.k + transform.y + 15 + "px");
        
        
// x6中计算坐标,鼠标坐标通过监听事件参数获取
			// 鼠标右键点击画布
      this.g.on("blank:contextmenu", ({ e, x, y }) => {
        this.showMenu(x, y);
      });
      showMenu(x: number, y: number) {
      console.log(x, y);
      console.log(this.g.scale());
      console.log(this.g.translate());
      const { tx, ty } = this.g.translate();
      const { sx, sy } = this.g.scale();
      (this.$refs.menuDown as any).show();
      const menu = (this.$refs.menuDown as any).popperElm;
      setTimeout(() => {
        menu.setAttribute("x-placement", "bottom-start");
        menu.style.left = x * sx + tx + 30 + "px";
        menu.style.top = y * sy + ty + 20 + "px";
      }, 0);
    },

小地图插件

// new Graph添加配置
const miniMap = this.$refs.miniMap as HTMLElement;
minimap: {
  enabled: true,
  container: miniMap,
  height: 150,
  width: 250
}

// 添加dom
<div class="mini-map" ref="miniMap"></div>

// style
.mini-map {
    position: absolute;
    bottom: 20px;
    right: 36px;
    .x6-widget-minimap {
      background-color: #d8e7cc;
      .x6-graph {
        box-shadow: 0 0 4px 0 #7ca190;
      }
    }
}

使用拖拽插件

addon/stencil
// stencil配置
// 一定要配置stencilGraphWidth和stencilGraphHeight,否则初次渲染不出来
// getDragNode配置拖拽中节点,getDropNode配置放下后节点
const stencil = new Addon.Stencil({
  title: "新建节点",
  target: this.g,
  search: (cell, key) => {
    return (
      cell.shape.indexOf(key) !== -1 ||
      (cell.attr("text/text") as String).indexOf(key) !== -1
    );
  },
  placeholder: "搜索节点",
  notFoundText: "404",
  stencilGraphWidth: 300,
  stencilGraphHeight: 100,
  groups: [
    {
      name: "node",
      collapsable: false
    }
  ],
  // getDragNode: (node) => {
  //   return node.clone();
  // },
  getDropNode: (node) => {
    const shape = node.shape;
    const nodeInfo = { label: node.attrs!.text.text };
    if (shape === "rect") {
      Object.assign(nodeInfo, {
        width: 80,
        height: 48,
        shape: "rect"
      });
    } else if (shape === "circle") {
      Object.assign(nodeInfo, {
        width: 80,
        height: 80,
        shape: "circle"
      });
    }

    return this.g.createNode(this.formatNode(nodeInfo));
  }
});
(this.$refs.stencil as HTMLElement).appendChild(stencil.container);

const rect = new Shape.Rect({
  width: 70,
  height: 40,
  label: "rect",
  attrs: {
    body: {
      fill: "#5f95ff",
      stroke: "transparent",
      rx: 5,
      ry: 5
    },
    label: {
      fill: "#ffffff"
    }
  }
});

const circle = new Shape.Circle({
  width: 80,
  height: 80,
  label: "circle",
  attrs: {
    body: {
      fill: "#5f95ff",
      stroke: "transparent"
    },
    label: {
      fill: "#ffffff"
    }
  }
});
stencil.load([rect, circle], "node");

// 需要添加dom和样式
<div class="stencil" ref="stencil"></div>

// style
.stencil {
    flex: 1;
    height: 100%;
    position: relative;
    margin-right: 20px;
}

监听node:added事件,在新添加节点上添加链接桩、可移动等属性

this.g.on("node:added", ({ node }) => {
	node.setData({ disableMove: true });
	this.setNodePort(node);
});

链接桩

要自定义链接桩位置(top\bottom\left\right等),只能使用group方式来创建制定position,attrs无法通过groups复用。属于可以优化但没做处理的功能,其实用到地方还很多

const ports = {
  groups: {
    top: {
      position: "top",
      attrs: {
        circle: {
          r: 5,
          magnet: true,
          stroke: "#31d0c6",
          strokeWidth: 2,
          fill: "#fff"
        }
      }
    },
    left: {
      position: "left",
      attrs: {
        circle: {
          r: 5,
          magnet: true,
          stroke: "#31d0c6",
          strokeWidth: 2,
          fill: "#fff"
        }
      }
    },
    right: {
      position: "right",
      attrs: {
        circle: {
          r: 5,
          magnet: true,
          stroke: "#31d0c6",
          strokeWidth: 2,
          fill: "#fff"
        }
      }
    },
    bottom: {
      position: "bottom",
      attrs: {
        circle: {
          r: 5,
          magnet: true,
          stroke: "#31d0c6",
          strokeWidth: 2,
          fill: "#fff"
        }
      }
    }
  },
  items: [
    {
      id: node.id + "port-top",
      group: "top"
    },
    {
      id: node.id + "port-bottom",
      group: "bottom"
    },
    {
      id: node.id + "port-left",
      group: "left"
    },
    {
      id: node.id + "port-right",
      group: "right"
    }
  ]
};
node.prop("ports", ports);

链接桩配置

connecting: {
  snap: {
  	radius: 30
  },
  allowBlank: false,
  allowMulti: false,
  allowLoop: false,
  allowEdge: false,
  highlight: true
},
// 对应connecting中highlight配置
highlighting: {
  magnetAvailable: {
    name: "stroke",
    args: {
    	padding: 4,
    	attrs: {
      "stroke-width": 2,
      stroke: "#FE854F"
      }
    }
  }
},
posted @ 2023-03-15 09:11  大禹不治水  阅读(280)  评论(0编辑  收藏  举报