Skip to content

DFD Graphing Library Reference

Eric Fitzgerald edited this page Apr 8, 2026 · 2 revisions

DFD Graphing Library Reference

This document provides a developer reference for the AntV X6 graphing library (v2.x) used in TMI's Data Flow Diagram (DFD) editor.

Overview

TMI uses AntV X6 version 2.x as the underlying graph visualization library for the DFD editor. This reference covers the core API patterns, plugin configurations, and common usage scenarios.

Current Version: 2.19.2 (see package.json for exact version)

Key Resources:

TMI-Specific Implementation

TMI's DFD editor uses X6 with the following architecture:

Plugins Used

Plugin Purpose Configuration
@antv/x6-plugin-snapline Visual alignment guides when moving nodes Red snaplines (dfd-snapline-red), sharp mode
@antv/x6-plugin-transform Node resizing capabilities Resizing enabled, no rotation, no restrict
@antv/x6-plugin-export SVG/PNG export functionality Standard configuration
@antv/x6-plugin-clipboard Cut/copy/paste operations In-memory clipboard (useLocalStorage: false)
@antv/x6-plugin-selection Multi-select with rubberband Custom styling, no selection boxes, Shift for multi-select

Note: @antv/x6-plugin-history is listed as a dependency but is not used in the source code. TMI implements custom undo/redo via AppOperationStateManager.

Custom Shapes

TMI defines custom shapes for DFD elements in infra-x6-shape-definitions.ts:

Shape Name Base Class Visual
process Shape.Rect Rounded rectangle (rx/ry=10)
store Shape.Rect Cylinder/drum with custom SVG path and ellipse top
actor Shape.Rect Standard rectangle
security-boundary Shape.Rect Dashed border rounded rectangle (rx/ry=10)
text-box Shape.Rect Transparent background, no stroke
flow Shape.Edge Edge with classic arrowhead target marker (size 8); createEdge overrides to 'block'

Key Implementation Files

File Purpose
infra-x6-graph.adapter.ts Main graph adapter with configuration and plugin setup
infra-x6-shape-definitions.ts Custom shape definitions (process, store, actor, security-boundary, flow, text-box)
infra-x6-event-handlers.adapter.ts Domain event coordination via RxJS Subjects
infra-x6-event-logger.adapter.ts X6 event logging and diagnostics
infra-x6-selection.adapter.ts Selection and transform plugin initialization, visual effects
infra-x6-keyboard.adapter.ts Shift key handling for snap-to-grid toggle, cursor changes, and undo/redo shortcuts
infra-x6-embedding.adapter.ts Node embedding (parent-child nesting)
infra-x6-z-order.adapter.ts Z-order management for layering
infra-x6-label-editor.adapter.ts In-place label editing
infra-x6-tooltip.adapter.ts Tooltip system for graph elements
infra-x6-core-operations.service.ts Low-level X6 cell add/remove operations
styling-constants.ts Centralized styling values (DFD_STYLING and DFD_STYLING_HELPERS)
infra-node-configuration.service.ts Node type attributes and port configuration
infra-port-state.service.ts Port visibility management (show on hover, hide unconnected)
infra-visual-effects.service.ts Creation highlights, invalid embedding feedback, and animation effects
infra-dfd-validation.service.ts Connection validation rules and magnet validation
infra-edge-query.service.ts Edge querying and relationship lookups

Core Library API (@antv/x6)

Graph Class

The central class for creating and managing a graph instance.

Constructor Options

import { Graph } from '@antv/x6';

const graph = new Graph({
  container: HTMLElement,       // Required: DOM container
  width?: number,               // Canvas width (default: container width)
  height?: number,              // Canvas height (default: container height)
  background?: BackgroundOptions,
  grid?: GridOptions,
  interacting?: InteractingOptions,
  connecting?: ConnectingOptions,
  selecting?: SelectingOptions,
  panning?: PanningOptions,
  mousewheel?: MouseWheelOptions,
  history?: HistoryOptions,
  clipboard?: ClipboardOptions,
  embedding?: EmbeddingOptions,
  snapline?: SnaplineOptions,
  highlighting?: HighlightingOptions,
});

TMI Configuration Example

// From infra-x6-graph.adapter.ts (uses DFD_STYLING constants)
this._graph = new Graph({
  container,
  width: container.clientWidth,
  height: container.clientHeight,
  grid: {
    size: DFD_STYLING.GRID.SIZE,           // 10
    visible: DFD_STYLING.GRID.VISIBLE,     // true
    type: 'dot',
    args: [
      { color: DFD_STYLING.GRID.PRIMARY_COLOR, thickness: 1 },   // '#666666'
      { color: DFD_STYLING.GRID.SECONDARY_COLOR, thickness: 1, factor: 4 }, // '#888888'
    ],
  },
  panning: {
    enabled: true,
    eventTypes: ['leftMouseDown'],
    modifiers: ['shift'],
  },
  mousewheel: {
    enabled: true,
    modifiers: ['shift'],
    factor: DFD_STYLING.VIEWPORT.ZOOM_FACTOR,      // 1.1
    maxScale: DFD_STYLING.VIEWPORT.MAX_MANUAL_ZOOM, // 3.0
    minScale: 0.2,
  },
  highlighting: {
    default: DFD_STYLING_HELPERS.getDefaultHighlighter(),
    embedding: DFD_STYLING_HELPERS.getEmbeddingHighlighter(),
    magnetAvailable: DFD_STYLING_HELPERS.getMagnetAvailableHighlighter(),
    magnetAdsorbed: DFD_STYLING_HELPERS.getMagnetAdsorbedHighlighter(),
  },
  interacting: {
    nodeMovable: true,
    edgeMovable: true,
    edgeLabelMovable: true,
    arrowheadMovable: true,
    vertexMovable: true,
    vertexAddable: true,
    vertexDeletable: true,
    magnetConnectable: true,
  },
  embedding: {
    enabled: true,
    findParent: 'bbox',
    validate: ({ child, parent }) => {
      // Delegates to InfraEmbeddingService for business rules
      return this._embeddingService.validateEmbedding(parent, child).isValid;
    },
  },
  connecting: {
    allowNode: false,
    allowPort: true,
    allowBlank: false,
    allowLoop: true,
    allowMulti: true,
    allowEdge: false,
    snap: { radius: 20 },
    highlight: true,
    validateMagnet: ({ magnet }) => boolean,
    validateConnection: ({ sourceView, targetView, sourceMagnet, targetMagnet }) => boolean,
    createEdge: () => {
      return this._graph.createEdge({
        shape: 'flow',               // CANONICAL_EDGE_SHAPE
        connector: 'smooth',         // DFD_STYLING.EDGES.CONNECTOR
        router: 'normal',            // DFD_STYLING.EDGES.ROUTER
        attrs: { line: { stroke, strokeWidth, targetMarker: 'block' } },
        labels: [{ position: 0.5, attrs: { text: { text: 'Flow', ... } } }],
      });
    },
  },
});

Key Methods

Cell Management:

graph.addNode(metadata: Node.Metadata): Node
graph.addNodes(nodes: Node.Metadata[]): Node[]
graph.removeNode(nodeId: string): Node | null
graph.addEdge(metadata: Edge.Metadata): Edge
graph.addEdges(edges: Edge.Metadata[]): Edge[]
graph.removeEdge(edgeId: string): Edge | null
graph.getNodes(): Node[]
graph.getEdges(): Edge[]
graph.getCellById(id: string): Cell | null
graph.clearCells(): this

Viewport:

graph.zoom(factor?: number): this
graph.zoomTo(scale: number): this
graph.zoomToFit(options?: ZoomOptions): this
graph.centerContent(): this
graph.fitToContent(options?: FitOptions): void

Coordinate Transformation:

graph.clientToLocal(x, y): Point
graph.localToClient(x, y): Point
graph.pageToLocal(x, y): Point
graph.localToPage(x, y): Point

Selection:

graph.getSelectedCells(): Cell[]
graph.select(cell: Cell | string): void
graph.unselect(cell: Cell | string): void
graph.resetSelection(): void
graph.isSelected(cell: Cell | string): boolean

History:

graph.undo(): void
graph.redo(): void
graph.canUndo(): boolean
graph.canRedo(): boolean
graph.clearHistory(): void

Clipboard:

graph.copy(cells: Cell[]): void
graph.cut(cells: Cell[]): void
graph.paste(): Cell[]
graph.isClipboardEmpty(): boolean
graph.cleanClipboard(): void

Event Handling

X6 uses an event emitter pattern:

graph.on(eventName: string, handler: Function): this
graph.once(eventName: string, handler: Function): this
graph.off(eventName?: string, handler?: Function): this

Common Events

Cell Events:

  • cell:added, cell:removed, cell:changed:*
  • cell:click, cell:dblclick, cell:contextmenu
  • cell:mouseenter, cell:mouseleave

Node Events:

  • node:added, node:removed, node:changed, node:move, node:moved, node:resize, node:resized
  • node:change:position, node:change:size, node:change:parent
  • node:embedding, node:embedded, node:unembedded
  • node:mouseenter, node:mouseleave, node:mousedown, node:mouseup
  • node:click, node:dblclick, node:contextmenu
  • node:port:mouseenter, node:port:mouseleave

Edge Events:

  • edge:added, edge:removed, edge:changed, edge:connecting, edge:connected, edge:disconnected
  • edge:change:source, edge:change:target, edge:change:vertices
  • edge:click, edge:dblclick, edge:contextmenu, edge:mouseup

Selection Events:

  • selection:changed

Graph Events:

  • blank:click, blank:dblclick, blank:contextmenu
  • blank:mousedown, blank:mouseup
  • graph:cleared, graph:error
  • scale, translate, resize
  • render:start, render:done
  • batch:start, batch:stop

TMI Event Handling Example

// From infra-x6-event-handlers.adapter.ts
// Uses RxJS Subjects to bridge X6 events to Angular observables
graph.on('node:added', ({ node }) => {
  this.logger.info('Node added to graph', { nodeId: node.id, shape: node.shape });
  this.emitEvent('nodeAdded', { node });
});

graph.on('selection:changed', ({ added, removed }) => {
  this.logger.info('Selection changed - event handler', {
    addedCount: added.length,
    removedCount: removed.length,
    totalSelected: graph.getSelectedCells().length,
  });
  this.emitEvent('selectionChanged', { selected: added, deselected: removed });
});

Cell Classes

Node Class

interface Node.Metadata {
  id?: string;
  x?: number;
  y?: number;
  width?: number;
  height?: number;
  label?: string;
  shape?: string;
  markup?: Markup;
  attrs?: CellAttrs;
  ports?: PortConfiguration;
  zIndex?: number;
  data?: any;
}

Key Methods:

node.getPosition(): { x: number, y: number }
node.setPosition(x: number, y: number): this
node.getSize(): { width: number, height: number }
node.setSize(width: number, height: number): this
node.getPorts(): Port[]
node.addPort(port: PortMetadata): this
node.removePort(portId: string): this

Edge Class

interface Edge.Metadata {
  id?: string;
  source: TerminalData;  // { cell: string, port?: string } or { x, y }
  target: TerminalData;
  labels?: Label[];
  vertices?: Point[];
  router?: RouterConfig;
  connector?: ConnectorConfig;
  attrs?: CellAttrs;
}

Key Methods:

edge.getSource(): TerminalData
edge.setSource(source: TerminalData): this
edge.getTarget(): TerminalData
edge.setTarget(target: TerminalData): this
edge.getVertices(): Point[]
edge.setVertices(vertices: Point[]): this
edge.getLabels(): Label[]
edge.setLabels(labels: Label[]): this

Shape Registration

import { Graph, Shape } from '@antv/x6';

// Define custom shape
Shape.Rect.define({
  shape: 'custom-rect',
  markup: [
    { tagName: 'rect', selector: 'body' },
    { tagName: 'text', selector: 'text' },
  ],
  attrs: {
    body: {
      fill: '#ffffff',
      stroke: '#000000',
      strokeWidth: 2,
    },
    text: {
      refX: '50%',
      refY: '50%',
      textAnchor: 'middle',
      textVerticalAnchor: 'middle',
    },
  },
});

// Register with Graph
Graph.registerNode('custom-rect', CustomRectClass, true);

Port Configuration

Ports are configured in infra-node-configuration.service.ts. Text-box shapes have no ports; all other node types share the same port layout:

// From infra-node-configuration.service.ts
const ports = {
  groups: {
    top: {
      position: 'top',
      attrs: {
        circle: {
          r: 5,
          magnet: 'active',
          'port-group': 'top',
          stroke: '#000',
          strokeWidth: 2,
          fill: '#fff',
          style: {
            visibility: 'hidden',  // Ports are hidden by default, shown on hover
          },
        },
      },
    },
    // right, bottom, left groups follow same pattern with matching 'port-group' values
  },
  items: [
    { group: 'top', id: 'top' },
    { group: 'right', id: 'right' },
    { group: 'bottom', id: 'bottom' },
    { group: 'left', id: 'left' },
  ],
};

Edge Routing and Connectors

Routers:

  • 'normal' - Direct line with vertices
  • 'orth' - Orthogonal segments
  • 'manhattan' - Orthogonal, avoiding crossings
  • 'metro' - Like manhattan with bend radius options
  • 'er' - Entity-relationship style

Connectors:

  • 'normal' - Straight line
  • 'smooth' - Bezier curve
  • 'rounded' - Polyline with rounded corners
  • 'jumpover' - Line jumps over intersections
const edge = graph.addEdge({
  source: { cell: nodeId1, port: 'right' },
  target: { cell: nodeId2, port: 'left' },
  router: { name: 'manhattan' },
  connector: { name: 'rounded', args: { radius: 8 } },
});

Plugin Configuration

Snapline Plugin

import { Snapline } from '@antv/x6-plugin-snapline';

// TMI uses 'dfd-snapline-red' CSS class for red-colored snaplines
graph.use(new Snapline({
  enabled: true,
  sharp: true,
  className: 'dfd-snapline-red',
}));

Transform Plugin

import { Transform } from '@antv/x6-plugin-transform';

// TMI configures Transform in both infra-x6-graph.adapter.ts and
// infra-x6-selection.adapter.ts (selection adapter is authoritative)
graph.use(new Transform({
  resizing: {
    enabled: true,
    minWidth: 40,
    minHeight: 30,
    maxWidth: Number.MAX_SAFE_INTEGER,
    maxHeight: Number.MAX_SAFE_INTEGER,
    orthogonal: false,
    restrict: false,
    preserveAspectRatio: false,
  },
  rotating: false,
}));

Selection Plugin

import { Selection } from '@antv/x6-plugin-selection';

// From infra-x6-selection.adapter.ts
graph.use(new Selection({
  enabled: true,
  multiple: true,
  rubberband: true,
  modifiers: null,                       // Allow rubberband without modifiers
  movable: true,
  multipleSelectionModifiers: ['shift'],  // Shift for multi-selection
  showNodeSelectionBox: false,
  showEdgeSelectionBox: false,
  pointerEvents: 'none',
  strict: false,                         // Partial overlap enables edge selection in rubberband
}));

History Management

Note: Although @antv/x6-plugin-history is listed as a dependency in package.json, TMI does not import or use the X6 History plugin. Instead, TMI implements its own custom undo/redo history system via AppOperationStateManager and types defined in history.types.ts. This custom system provides operation-aware atomic batching, visual effect exclusion from history, and coordination with WebSocket-based collaborative editing.

Clipboard Plugin

import { Clipboard } from '@antv/x6-plugin-clipboard';

// From infra-x6-graph.adapter.ts
graph.use(new Clipboard({
  enabled: true,
  useLocalStorage: false,  // Use in-memory clipboard only
}));

Note: TMI's copy/paste logic is handled by InfraX6SelectionAdapter and SelectionService, which manage cell cloning, position calculation, and paste operations at a higher level than X6's built-in clipboard.

Node Tools

X6 provides built-in tools for node/edge interaction:

// Add tools to a node
node.addTools([
  {
    name: 'button-remove',
    args: { x: '100%', y: 0, offset: { x: -10, y: 10 } },
  },
  {
    name: 'boundary',
    args: {
      padding: 5,
      attrs: {
        fill: 'none',
        stroke: '#fe854f',
        'stroke-width': 2,
        'stroke-dasharray': '5,5',
      },
    },
  },
]);

// Remove tools
node.removeTools();

Edge Tools

edge.addTools([
  { name: 'vertices' },
  { name: 'source-arrowhead' },
  { name: 'target-arrowhead' },
  { name: 'button-remove', args: { distance: 0.5 } },
]);

Serialization

// Export to JSON
const data = graph.toJSON();
// Returns: { cells: [...] }

// Import from JSON
graph.fromJSON(data);
// or
graph.fromJSON({ cells: [...] });

X6 Native Format

TMI uses X6's native toJSON() format for all cell persistence with zero transformation overhead. Key characteristics:

Node Labels: Set via attrs.text.text, not a top-level label property

{
  id: 'node-1',
  shape: 'process',
  x: 100, y: 100, width: 120, height: 60,
  attrs: {
    body: { fill: '#ffffff', stroke: '#000000' },
    text: { text: 'Process Name', fontSize: 14 }  // Label here
  }
}

Edge Labels: Set via labels array with positioning control

{
  id: 'edge-1',
  shape: 'flow',
  source: { cell: 'node-1', port: 'right' },
  target: { cell: 'node-2', port: 'left' },
  attrs: {
    line: { stroke: '#808080', strokeWidth: 1 }
  },
  labels: [{
    attrs: { text: { text: 'Data Flow', fontSize: 12 } },
    position: 0.5
  }]
}

Styling: All visual properties use the X6 attrs object structure, not convenience style properties. This enables direct serialization/deserialization with X6 without transformation.

Best Practices

Performance

  1. Batch Operations: Use graph.batchUpdate() for multiple changes

    graph.batchUpdate(() => {
      // Multiple operations here
    });
  2. Async Rendering: Enable for large graphs

    new Graph({ async: true });
  3. Frozen Graph: Pause rendering during bulk operations

    graph.freeze();
    // ... bulk operations
    graph.unfreeze();

Memory Management

  1. Always call graph.dispose() when destroying the graph
  2. Remove event listeners with graph.off() when no longer needed
  3. Clean up tools when nodes are removed

Coordinate Systems

X6 uses multiple coordinate systems:

  • Client: Browser viewport coordinates
  • Local: Graph canvas coordinates (affected by zoom/pan)
  • Page: Document coordinates

Use transformation methods:

const localPoint = graph.clientToLocal(clientX, clientY);
const clientPoint = graph.localToClient(localX, localY);

Related Pages

External References

Clone this wiki locally