---
title: "自定义数据处理"
description: "自定义数据处理"
language: "zh"
canonical: "https://g6.antv.antgroup.com/zh/manual/transform/custom-transform/"
version: "5.1.1"
---

## 概述

自定义数据处理允许用户在业务实现过程中，把额外的数据处理封装起来，在渲染前或者布局后对数据进行进一步处理。用户通过自定义数据处理，实现部分数据处理解耦，更好地进行管理、编排代码以及提高代码可维护性。

## 开始前

用户在进行自定义数据处理前，需要仔细阅读并掌握数据处理器的 [实现原理和执行时机](/zh/manual/transform/overview/#实现原理) 。

## 代码示例

接下来将讲述两个可能的业务场景，并通过自定义数据处理来实现：

### 不展示游离节点

- **需求**

  游离节点，即没有连线的节点，画布渲染时不展示游离节点

- **实现**

  ```typescript
  import type \{ DrawData, DrawContext \} from '@antv/g6';
  import \{ Graph, BaseTransform, register, ExtensionCategory \} from '@antv/g6';

  class HideFreeNode extends BaseTransform \{
    public beforeDraw(input: DrawData, context: DrawContext): DrawData \{
      const \{ model \} = this.context;
      const \{ add, update, remove \} = input;

      add.nodes.forEach((nodeData, nodeId) => \{
        // 获取节点的相关连线
        const edges = model.getRelatedEdgesData(nodeId);
        // 没有任何连线的的节点则从add里面移除，添加到remove里面
        if (!edges.length) \{
          add.nodes.delete(nodeId);
          remove.nodes.set(nodeId, nodeData);
        \}
      \});

      return input;
    \}
  \}
  ```

  

```ts
import { BaseTransform, ExtensionCategory, Graph, register } from '@antv/g6';

class HideFreeNode extends BaseTransform {
  beforeDraw(input, context) {
    const { model } = this.context;
    const { add, update, remove } = input;

    add.nodes.forEach((nodeData, nodeId) => {
      // 获取节点的相关连线
      const edges = model.getRelatedEdgesData(nodeId);
      // 没有任何连线的的节点则从add里面移除，添加到remove里面
      if (!edges.length) {
        add.nodes.delete(nodeId);
        remove.nodes.set(nodeId, nodeData);
      }
    });

    return input;
  }
}

register(ExtensionCategory.TRANSFORM, 'hide-free-node', HideFreeNode);

const data = {
  nodes: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }],
  edges: [
    { source: '1', target: '2' },
    { source: '2', target: '3' },
    { source: '3', target: '5' },
  ],
};

const graph = new Graph({
  container: 'container',
  width: 600,
  height: 400,
  autoFit: 'center',
  data,
  node: {
    style: {
      labelText: (d) => d.id,
    },
  },
  layout: {
    type: 'antv-dagre',
    rankdir: 'LR',
  },
  transforms: ['hide-free-node'],
});

graph.render();
```



- **说明**

  示例中总共有6个节点，id为1-6，id为4的节点没有连线，因此被移除了。

  通过 `getRelatedEdgesData` 获取节点的相关连线，没有则把该节点放到 `remove.nodes` 里面去，并从 `add.nodes` 里面移除。

### 环形布局径向label

- **需求**

  使用 [环形布局](/zh/manual/layout/CircularLayout/) 时，节点 label 的也需要像内置数据处理器 [PlaceRadialLabels](/zh/manual/transform/PlaceRadialLabels/) 一样实现径向展示（但 PlaceRadialLabels 只支持径向布局，环形布局不是径向布局）

- **实现**

  ```typescript
  import type \{ RuntimeContext, DrawContext, Point, TransformArray, Vector2, Vector3 \} from '@antv/g6';
  import \{ Graph, BaseTransform, register, ExtensionCategory, BaseTransformOptions \} from '@antv/g6';

  // 目前circular布局没有暴露方法可以获取布局中心，这里简单处理先固定一个，配置circular布局时center与这里保持一致即可
  const circularCenter = [300, 300];

  // 下面的函数 G6 没有暴露出来，先自行声明
  function subtract(a: Vector2 | Vector3, b: Vector2 | Vector3): Vector2 | Vector3 \{
    return a.map((v, i) => v - b[i]) as Vector2 | Vector3;
  \}
  function rad(a: Vector2 | Vector3): number \{
    const [x, y] = a;
    if (!x && !y) return 0;
    return Math.atan2(y, x);
  \}
  function rad2deg(rad: number): number \{
    return rad * (180 / Math.PI);
  \}

  interface CircularRadialLabelsOptions extends BaseTransformOptions \{
    offset?: number; // 偏移量
  \}

  class CircularRadialLabels extends BaseTransform&lt;CircularRadialLabelsOptions&gt; \{
    static defaultOptions = \{
      offset: 5,
    \};
    constructor(context: RuntimeContext, options: CircularRadialLabelsOptions) \{
      super(context, Object.assign(\{\}, CircularRadialLabels.defaultOptions, options));
    \}
    get center(): Point \{
      return circularCenter;
    \}
    public afterLayout() \{
      const \{ graph, model \} = this.context;
      const data = model.getData();
      data.nodes?.forEach((datum) => \{
        const radian = rad(subtract([datum.style.x, datum.style.y], this.center));
        const isLeft = Math.abs(radian) > Math.PI / 2;
        const isLeaf = !datum.children || datum.children.length === 0;
        const nodeId = datum.id;
        const node = this.context.element?.getElement(nodeId);
        if (!node || !node.isVisible()) return;

        const nodeHalfWidth = graph.getElementRenderStyle(nodeId).size / 2;
        const offset = (isLeaf ? 1 : -1) * (nodeHalfWidth + this.options.offset);

        const labelTransform: TransformArray = [
          ['translate', offset * Math.cos(radian), offset * Math.sin(radian)],
          ['rotate', isLeft ? rad2deg(radian) + 180 : rad2deg(radian)],
        ];

        model.updateNodeData([
          \{
            id: datum.id,
            style: \{
              labelTextAlign: isLeft === isLeaf ? 'right' : 'left',
              labelTextBaseline: 'middle',
              labelTransform,
            \},
          \},
        ]);
      \});

      graph.draw();
    \}
  \}
  ```

  

```ts
import { BaseTransform, ExtensionCategory, Graph, register } from '@antv/g6';

// 目前circular布局没有暴露方法可以获取布局中心，这里简单处理先固定一个
const circularCenter = [300, 300];

// 下面的函数 G6 没有暴露出来，先自行声明
function subtract(a, b) {
  return a.map((v, i) => v - b[i]);
}
function rad(a) {
  const [x, y] = a;
  if (!x && !y) return 0;
  return Math.atan2(y, x);
}
function rad2deg(rad) {
  return rad * (180 / Math.PI);
}

class CircularRadialLabels extends BaseTransform {
  static defaultOptions = {
    offset: 5,
  };
  constructor(context, options) {
    super(context, Object.assign({}, CircularRadialLabels.defaultOptions, options));
  }
  get center() {
    return circularCenter;
  }
  afterLayout() {
    const { graph, model } = this.context;
    const data = model.getData();
    data.nodes?.forEach((datum) => {
      const radian = rad(subtract([datum.style.x, datum.style.y], this.center));
      const isLeft = Math.abs(radian) > Math.PI / 2;
      const isLeaf = !datum.children || datum.children.length === 0;
      const nodeId = datum.id;
      const node = this.context.element?.getElement(nodeId);
      if (!node || !node.isVisible()) return;

      const nodeHalfWidth = graph.getElementRenderStyle(nodeId).size / 2;
      const offset = (isLeaf ? 1 : -1) * (nodeHalfWidth + this.options.offset);

      const labelTransform = [
        ['translate', offset * Math.cos(radian), offset * Math.sin(radian)],
        ['rotate', isLeft ? rad2deg(radian) + 180 : rad2deg(radian)],
      ];

      model.updateNodeData([
        {
          id: datum.id,
          style: {
            labelTextAlign: isLeft === isLeaf ? 'right' : 'left',
            labelTextBaseline: 'middle',
            labelTransform,
          },
        },
      ]);
    });

    graph.draw();
  }
}

register(ExtensionCategory.TRANSFORM, 'circular-radial-labels', CircularRadialLabels);

const data = {
  nodes: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }, { id: '6' }],
  edges: [
    { source: '1', target: '2' },
    { source: '2', target: '3' },
    { source: '3', target: '4' },
    { source: '4', target: '5' },
    { source: '5', target: '6' },
    { source: '6', target: '1' },
  ],
};

const graph = new Graph({
  container: 'container',
  width: 600,
  height: 400,
  autoFit: 'center',
  data,
  node: {
    style: {
      labelText: (d) => 'label' + d.id,
      size: 30,
    },
  },
  layout: {
    type: 'circular',
    width: 200,
    center: circularCenter,
    preLayout: false, // 不能是渲染前布局，否则不生效
  },
  transforms: ['circular-radial-labels'],
});

graph.render();
```



- **说明**
  上面的实现基本是参考内置数据处理器 [PlaceRadialLabels](/zh/manual/transform/PlaceRadialLabels/) 来实现的，区别是这里的实现是通过拿到布局中心来计算偏移和旋转，具体可参考 PlaceRadialLabels 的 [源码](https://github.com/antvis/G6/blob/v5/packages/g6/src/transforms/place-radial-labels.ts)
