1. 画布只重新渲染数据

  • graph.render = graph.draw+graph,fitview()+graph.fitCenter()
  • setData塞入新的数据
const updateGraph = (data) => {
  if (!graph) {
    console.warn("Graph is not initialized");
    return;
  }
  graph.clear();
  graph.setData(data);
  graph.render();
};

2. 画布修改大小

const resizeGraph = (width, height) => {
  if (!graph) return;
  graph.setSize(width, height);
};

3. 获取画布宽高

  1. 使用ref元素获取panelRef.value.$el.clientWidth/cilentHeight
  2. 如果这个div块是一会儿显示一会儿隐藏,为了1获取宽高生效,在隐藏的时候,css:display:none不行,读出来的一直都是0,应该用display:hidden

4. 自定义combo、node、edge的样式

1. 节点

  1. 节点/边设置
node: {
      style: (d) => {
        return setNodeStyle(d);
      },
    },
    edge: {
      type: "cubic-vertical",
      style: (d) => {
        return setEdgeStyle(data, d);
      },
    },
  1. 节点大小、标签、标签背景、字体大小、边框虚实等样式都是定义在style里,style和type是并列元素
  • 标签过长换行: labelMaxWidth是基于节点的宽度设置的百分比。
	labelWordWrap: true,
    labelMaxLines: 10,
    labelMaxWidth: "500%"
  • 完整配置
const setNodeStyle = (d) => {
  //flag:true-真实节点;false-虚拟节点
  let style = {
    size: NodeColor.find((item) => +item.category === +d.category)?.size || 16, //节点大小
    labelText: d.name,
    labelPlacement: "bottom",
    lineWidth: d.flag ? 0 : 1,
    lineDash: [5, 1],
    stroke: fillNodeColor(d), //节点边框颜色
    // labelOffsetX: 8,
    labelOffsetY: 4,
    labelTextAlign: "center",
    labelFontSize: 8,
    labelWordWrap: true,
    labelMaxLines: 10,
    labelMaxWidth: "500%",
    labelFontStyle: "italic",
    labelBackground: true,
    labelBackgroundFill:
      "linear-gradient(rgba(230,100,101,0.12), rgba(145,152,229,0.12))",
    labelBackgroundStroke: "rgba(230,100,101,0.4)",
    labelBackgroundRadius: 2,
    ports: [{ placement: "top" }, { placement: "bottom" }],
    fill: fillNodeColor(d), //节点颜色
  };
  return style;
}
  • 定义节点边框虚实:lineDash,边框颜色:stroke;边宽:lineWidth
  • 定义节点填充色:fill

2. combo

  1. combo设置
 combo: {
      type: "rect",
      style: (d) => {
        return setComboStyle(d);
      },
    },
  1. combo样式
const setComboStyle = (d) => {
  let style = {
    padding: 15,
    lineDash: [5, 5],
    lineWidth: 1,
    radius: 4,
    fill: "rgba(120,99,255,0.5)",
    zIndex: 10, //不让点击combo里的节点,而是点击combo弹框
    labelText: JSON.stringify(d.count) || "0", //必须是string类型,number类型会报错
    labelPlacement: "bottom",
    lineWidth: 1,
    lineDash: [5, 1],
    stroke: "rgba(120,99,255,0.5)", //节点边框颜色
    labelOffsetY: 10,
    labelFontSize: 12,
    labelFontStyle: "italic",
  };
  return style;
};

5. 动态控制树节点的显示和隐藏

1. 项目背景

当前树形结构包含多级层级关系,若一次性展示所有叶子节点会导致节点过小,难以看清详细信息。存在以下难点:

  1. Antv/G6有combo分组,可以展开收起,但是每次点击combo收起后无法重新渲染布局,显示出来的节点仍然很小。
  2. 如果设置combo默认收起,导致初始后combo节点位置为(0,0)并不对,点击展开后所有子节点也都重叠在了一起在(0,0)的位置,不会重新渲染到响应位置。
  3. Antv/g6有Graph.setElementVisibility(id, visibility, animation)设置节点隐藏,但是无法设置边的隐藏,其一边的数据结构只有source、target,没有id,其二,假设加了edgeId,需要根据隐藏的节点找出对应的边,对于数据量大的图,这不是一个好性能的方法。
    为此,我们采用以下交互设计:
  4. 初始状态下,每个父节点默认仅显示两个叶子节点
  5. 双击父节点时,将展开显示其所有叶子节点
  6. 再次双击该父节点,则收起所有叶子节点

2. 实现方式

  • 注意:初始默认节点隐藏,那么initGraph传入的data就应该是setElementHidden后的数据。

1. 初始化画布

const initGraph = (data) => {
  //如果之前有数据,先清空再重新渲染
  if (graph) {
    graph?.clear();
  }
  let container = document.getElementById("container");
  let width = container?.scrollWidth || 800;
  let height = container?.scrollHeight || 500;
  
  let graphData = setElementHidden(data);
  
  graph = new Graph({
    container: container,
    autoFit: "view",
    data: graphData,
    behaviors: ["drag-canvas", "zoom-canvas"],
    node: {
      style: (d) => {
        return setNodeStyle(d);
      },
    },
    edge: {
      type: "cubic-vertical",
      style: (d) => {
        return setEdgeStyle(data, d);
      },
    },
    layout: {
      type: "compact-box",
      direction: "TB",
      getVGap: function getVGap() {
        return 72;
      },
    },

2. 设置元素隐藏:我的要求是隐藏category是3的节点及其所有子节点,只保留两个;优势就是用到了Set,简化了代码

const setElementHidden = (data) => {
  let hiddenTypeNodes = new Set(
    data.nodes
      .filter((node) => node.index > 2 && +node.category === 3)
      .map((node) => node.id)
  );
  //hiddenTypeNodes作为source的对应的target节点
  let hiddenParamNodes = new Set(
    data.edges
      .filter((edge) => hiddenTypeNodes.has(edge.source))
      .map((edge) => edge.target)
  );

  let allHiddenNodes = new Set([...hiddenTypeNodes, ...hiddenParamNodes]);
  let filteredNodes = data.nodes.filter((node) => !allHiddenNodes.has(node.id));

  let filteredEdges = data.edges.filter((edge) => {
    // 默认过滤逻辑
    return !allHiddenNodes.has(edge.source) && !allHiddenNodes.has(edge.target);
  });

  _data.nodes = filteredNodes;
  _data.edges = filteredEdges;
  _data.combos = data.combos || [];
  return _data;
};

3. 设置部分元素可见:只双击category是2的节点能够展开/收起节点,再次双击展开这个节点对应的category=3的全部节点及其所有子节点

const setPartElementVisible = (data, taskId) => {
  // 找出未被选中的任务节点
  let _notSelectedTaskNodes = new Set(
    data.nodes
      .filter((node) => +node.category === 2 && node.id != taskId)
      .map((node) => node.id)
  );
  let _notSelectedTypeNodes = new Set(
    data.edges
      .filter((edge) => _notSelectedTaskNodes.has(edge.source))
      .map((edge) => edge.target)
  );

  let hiddenTypeNodes = new Set(
    data.nodes
      .filter(
        (node) =>
          _notSelectedTypeNodes.has(node.id) &&
          node.index > 2 &&
          +node.category === 3
      )
      .map((node) => node.id)
  );

  let hiddenParamNodes = new Set(
    data.edges
      .filter((edge) => hiddenTypeNodes.has(edge.source))
      .map((edge) => edge.target)
  );

  let allHiddenNodes = new Set([...hiddenTypeNodes, ...hiddenParamNodes]);

  let filteredNodes = data.nodes.filter((node) => !allHiddenNodes.has(node.id));
  let filteredEdges = data.edges.filter((edge) => {
    // 默认过滤逻辑
    return !allHiddenNodes.has(edge.source) && !allHiddenNodes.has(edge.target);
  });

  _data.nodes = filteredNodes;
  _data.edges = filteredEdges;
  _data.combos = data.combos || []; // 保留 combos(如果存在)
  return _data;
};

4. 双击事件重新渲染画布

  • preNode就是记住上次点击的节点,如果和上次一样,是要收起;如果和上次不一样,是要展开。
graph.on(NodeEvent.DBLCLICK, async (e) => {
    if (clickTimer.value) {
      clearTimeout(clickTimer.value);
      clickTimer.value = null;
    }
    let newData = {};
    let nodeDetail = await getNodeDetail(e?.target?.id);

    //任务节点:双击展开,再双击收起
    if (+nodeDetail.category === 2) {
      if (preNodeId.value == e?.target?.id) {
        newData = setElementHidden(data, e?.target?.id);
        preNodeId.value = "";
      } else {
        newData = setPartElementVisible(data, e?.target?.id);
        preNodeId.value = e?.target?.id;
      }

      updateGraph(newData);
    }
  });

5. 画布节点单击/双击阻止冒泡事件,不要互相影响

let clickTimer = ref();
 //点击节点,弹出详情页面
  graph.on(NodeEvent.CLICK, async (e) => {
    if (clickTimer.value) {
      clearTimeout(clickTimer.value);
      clickTimer.value = null;
    }

    clickTimer.value = setTimeout(async () => {
      //TODO:具体业务逻辑
    }, 200);
  });

  graph.on(NodeEvent.DBLCLICK, async (e) => {
    if (clickTimer.value) {
      clearTimeout(clickTimer.value);
      clickTimer.value = null;  //单击设置了clickTimer,如果有说明在单击事件,不做任何操作
    }
    //TODO:具体业务逻辑
    }
  });

6. contextMenu加图标

  • 官网给出的右键菜单只能显示文本,但是加图标能更好看些。
  • 图标是用deepseek生成的(目前就依赖这种了!)
  • 也可以使用unicode编码,用html字体实现,但我从iconfont上找到Unicode编码填入后无法加载
 plugins: [
      {
        type: "contextmenu",
        trigger: "contextmenu",
        enable: (e) => e.targetType === "node",
        getItems: (e) => {
          [
                  { name: "➕  新增子节点", value: "add" },
                  { name: "❌  删除节点", value: "delete" },
          ];
        },
        onClick: async (value, target, current) => {
          //TODO:具体业务逻辑
        },
      },
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐