antv g6实现系统拓扑图

发布于:2024-06-21 ⋅ 阅读:(200) ⋅ 点赞:(0)

1 背景

为例描述各个服务、redis、mysql等之间的联系及其健康状态,构建系统拓扑图,考虑 g6 更适合处理大量数据之间的关系,所以我们采用g6来绘制前端的图形。

g6提供的支持:

  • 节点/边类型多样,同样支持自定义
  • 对于节点的样式可以直接配置化处理
  • 丰富的事件体系,包括对节点/边/画布,以及时机事件的监听
  • 多种布局算法
  • 节点/边的数据,都是可以配置化的json对象

在线工具:g6示例

2 功能列表

节点:

  • 添加节点:除了id、style、type外,还包括一些业务需要的数据
  • 删除节点:除了删除该节点相对于画布的id外,还包括与之相关的业务数据
  • 节点状态:比如错误节点需要标红;非活跃节点需要标灰

边:

  • 添加边:除了id、style、type外,还包括一些业务需要的数据
  • 删除变:除了删除该边相对于画布的id外,还包括与之相关的业务数据
  • 修改边:主要是修改边所代表的业务信息,如果没有业务信息的话,这条边应该被删除

画布:

  • 用户自定义布局,比如需要保存用户拖拽节点后的节点位置坐标信息
  • dagre层次布局
  • 工具栏
  • 图例
  • 小地图
  • 触摸板放大缩小
  • 节点搜索

3 节点

3.1 渲染节点

渲染节点,包括自定义节点类型和样式。

自定义节点,该节点由rect和image组成,类似于矩形里面有icon:

// 其实可以不用自定义节点,可以使用circle类型的icon字段。但是这种方式,点击节点的时候,里面的icon会存在闪缩的情况
// https://g6.antv.antgroup.com/manual/middle/elements/nodes/built-in/circle#%E5%9B%BE%E6%A0%87-icon
G6.registerNode(
  'drag-inner-image-node',
  {
    afterDraw(cfg, group) {
      const size = cfg?.size as number[];
      const width = size[0] - 20;
      const height = size[1] - 20;
      const imageShape = group?.addShape('image', {
        attrs: {
          x: -width / 2,
          y: -height / 2,
          width,
          height,
          img: cfg?.img,
          cursor: 'move',
        },
        // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
        name: 'image-shape',
      });
      // 启用拖拽
      imageShape?.set('draggable', true);
    },
  },
  'circle',
);

节点样式:

const DefaultNodeSelectedStyle = {
  lineWidth: 8,
  'text-shape': {
    // 点击后的文本样式,保持点击前一致
    fontWeight: 400,
  },
};

export const NodeStyleMap = {
  default: {
    // 正常节点 - 样式设置
    style: {
      fill: GlobalLightBlueColor,
      stroke: GlobalBlueColor,
      lineWidth: 1,
    },
    // 状态样式,比如 selected点击状态
    stateStyles: {
      selected: {
        stroke: GlobalBlueColor,
        fill: GlobalLightBlueColor,
        shadowColor: GlobalBlueColor,
        ...DefaultNodeSelectedStyle,
      },
    },
  },
  error: {
    // 异常节点
    style: {
      stroke: GlobalRedColor,
      fill: GlobalLightRedColor,
      lineWidth: 1,
    },
    stateStyles: {
      selected: {
        stroke: GlobalRedColor,
        fill: GlobalLightRedColor,
        shadowColor: GlobalRedColor,
        ...DefaultNodeSelectedStyle,
      },
    },
  },
};

获取节点的渲染数据:

export const formatNodes = (nodes: MttkArchitectureNode[] = []) => {
  return nodes?.map((node) => {
    const { component, has_error, coordinates } = node;
    // 业务逻辑
    const middlewareType = getMiddlewareType(component) as MttkComponentType;

    const { id, label, wholeLabelName } = getNodeId(node);

    // 样式和icon
    const nodeStyle = NodeStyleMap[has_error ? 'error' : 'default'];
    const img = has_error ? ErrorIconImageMap[middlewareType] : IconImageMap[middlewareType];

    return {
      ...node,
      img,
      middlewareType,
      label,
      wholeLabelName, // 仅前端展示使用
      ...nodeStyle,
      id, // 仅前端展示使用
      x: coordinates?.x, // 节点的位置坐标
      y: coordinates?.y, // 节点的位置坐标
    };
  });
};

3.2 删除节点

3.3 添加节点

4 边

4.1 渲染边

边的样式:

const DefaultEdgeSelectedStyle = {
  lineWidth: 4,
  shadowBlur: 10, // 阴影的模糊级别,数值越大越模糊
};

export const EdgeStyleMap = {
  default: {
    // 正常边 - 样式设置
    style: {
      stroke: GlobalBlueColor,
      lineWidth: 1,
      lineDash: [0], // 如果[0]表示直线,需要覆盖一下创建边之后的虚线样式
    },
    // 状态样式,比如 selected点击状态
    stateStyles: {
      selected: {
        stroke: GlobalBlueColor,
        shadowColor: GlobalBlueColor,
        ...DefaultEdgeSelectedStyle,
      },
    },
  },
  error: {
    // 异常边
    style: {
      stroke: GlobalRedColor,
      lineWidth: 1,
    },
    stateStyles: {
      selected: {
        stroke: GlobalRedColor,
        shadowColor: GlobalRedColor,
        ...DefaultEdgeSelectedStyle,
      },
    },
  },
};

边的渲染数据:

export const formatEdges = (edges: MttkArchitectureEdge[] = [], nodes: MttkArchitectureNode[] = []) => {
  return edges?.map((edge) => {
    const { has_error } = edge;

    const edgeStyle = EdgeStyleMap[has_error ? 'error' : 'default'];

    const { id, fromId, toId } = getEdgeId(nodes, edge);

    return {
      ...edge,
      source: fromId,
      target: toId,
      ...edgeStyle,
      id,
      from: fromId, // 前端直接替换掉get接口返回的随机数id
      to: toId, // 前端直接替换掉get接口返回的随机数id
    };
  });
};

4.2 删除边

4.3 添加边

5 画布全局配置


export const LayoutMap = {
  [LayoutType.LR]: {
    // 从左到右
    type: 'dagre',
    ranksep: 70,
    controlPoints: true, // 是否保留布局连线的控制点
    rankdir: 'LR', // 可选,默认为图的中心
    nodesep: 10, // 可选
  },
  [LayoutType.TB]: {
    // 从上到下
    // type: 'dagre',
    // ranksep: 70,
    // controlPoints: true,
    rankdir: 'TB',
  },
};

export const DefaultOptions = {
  layout: LayoutMap.LR,
  defaultNode: {
    type: 'drag-inner-image-node',
    size: [50, 50],
    style: { cursor: 'move' },
    label: 'node-label',
    labelCfg: {
      position: 'bottom',
      offset: 2,
      style: {
        fill: '#666',
        fontSize: 14,
        cursor: 'move',
      },
    },
  },
  defaultEdge: {
    type: 'polyline',
    style: {
      radius: 20, // 拐弯处的圆角弧度
      offset: 20, // 拐弯处距离节点最小距离
      endArrow: true,
      lineAppendWidth: 20, // 提升边的击中范围
    },
  },
  modes: {
    default: [
      'drag-canvas',
      'drag-node',
      {
        type: 'create-edge',
        trigger: 'click', // 'click' by default. options: 'drag', 'click'
        key: 'shift', // undefined by default, options: 'shift', 'control', 'ctrl', 'meta', 'alt'
        edgeConfig: {
          // 有该交互创建出的边的配置项,可以配置边的类型、样式等
          style: {
            radius: 20, // 拐弯处的圆角弧度
            offset: 20, // 拐弯处距离节点最小距离
            endArrow: true,
            lineAppendWidth: 20, // 提升边的击中范围
            ...EdgeStyleMap.default.style,
            lineDash: [5], // 设置线的虚线样式, 如果[0]表示直线
          },
        },
        shouldEnd: (e: any, self: any) => {
          const { item: toItem } = e;
          const { source: fromId, graph } = self;
          const toId = toItem._cfg.id;

          // 不允许创建自环边
          if (toId === fromId) {
            return false;
          }

          // 不允许创建已经存在的边
          const edges = graph.getEdges();
          if (
            edges.some((ed: any) => {
              const { source, target } = ed.getModel();
              return fromId === source && toId === target;
            })
          ) {
            return false;
          }

          return true;
        },
      },
      {
        type: 'click-select',
        // 不允许节点被该交互选中。如果为true的话,会存在重复点击当前节点闪烁的情况,
        // 因为 已选中 > 再次点击,会默认给当前节点 selected status设置为false,我们再手动改为true的时候,就会存在闪烁
        selectNode: false,
        multiple: false, // 不允许多选
      },
    ],
  },
  fitView: true, // 图是否自适应画布
};

6 图例

g6自带的图例不是很好自定义ui,虽然可以进行与节点/边数据联动的功能,所以考虑直接react实现。

// interface Props {
//   extendLegend?: React.ReactNode; // 扩展图例,比如错误的信息
// }
export const GraphNodeTypeConfigs = [
  {
    icon: IconImageMap[MttkComponentType.SERVICE],
    description: 'Service',
    key: MttkComponentType.SERVICE,
  },
  {
    icon: IconImageMap[MttkComponentType.MYSQL],
    description: 'MySQL',
    key: MttkComponentType.MYSQL,
  },
  {
    icon: IconImageMap[MttkComponentType.KAFKA],
    description: 'Kafka',
    key: MttkComponentType.KAFKA,
  },
  {
    icon: IconImageMap[MttkComponentType.REDIS],
    description: 'Redis',
    key: MttkComponentType.REDIS,
  },
  {
    icon: IconImageMap[MttkComponentType.UNKNOWN],
    description: 'Unknown',
    key: MttkComponentType.UNKNOWN,
  },
];

export function LegendRow() {
  return (
    <>
      {GraphNodeTypeConfigs.map(({ icon, description }) => (
        <Row justify="start" align="middle" wrap={false} style={{ marginRight: 8 }}>
          <img src={icon} style={{ width: 18, height: 18, marginRight: 4 }} />
          {description}
        </Row>
      ))}
    </>
  );
}

7 工具栏

跟图例一样,考虑不太好自定义ui,所以直接react实现。

import { ZoomInOutlined, ZoomOutOutlined, FullscreenExitOutlined } from '@ant-design/icons';
import { Col, Row, Button } from 'antd';

interface Props {
  onZoomIn: () => void; // 放大
  onZoomOut: () => void; // 缩小
  onFixCenter: () => void; // 回到中间
}

export function Toolbar(props: Props) {
  const { onZoomIn, onZoomOut, onFixCenter } = props;
  return (
    <Col style={{ width: 30 }}>
      <Row justify="center">
        <Button type="link" style={{ padding: 0 }} onClick={onZoomIn}>
          <ZoomInOutlined />
        </Button>
      </Row>
      <Row justify="center">
        <Button type="link" style={{ padding: 0 }} onClick={onZoomOut}>
          <ZoomOutOutlined />
        </Button>
      </Row>
      <Row justify="center">
        <Button type="link" style={{ padding: 0 }} onClick={onFixCenter}>
          <FullscreenExitOutlined />
        </Button>
      </Row>
    </Col>
  );
}


网站公告

今日签到

点亮在社区的每一天
去签到