-
Notifications
You must be signed in to change notification settings - Fork 2
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.
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:
- Repository: https://github.com/antvis/X6
- Source Code: https://github.com/antvis/X6/tree/master/packages
- Examples: https://github.com/antvis/X6/tree/master/examples/x6-example-features/src/pages
TMI's DFD editor uses X6 with the following architecture:
| 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-historyis listed as a dependency but is not used in the source code. TMI implements custom undo/redo viaAppOperationStateManager.
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'
|
| 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 |
The central class for creating and managing a graph instance.
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,
});// 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', ... } } }],
});
},
},
});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(): thisViewport:
graph.zoom(factor?: number): this
graph.zoomTo(scale: number): this
graph.zoomToFit(options?: ZoomOptions): this
graph.centerContent(): this
graph.fitToContent(options?: FitOptions): voidCoordinate Transformation:
graph.clientToLocal(x, y): Point
graph.localToClient(x, y): Point
graph.pageToLocal(x, y): Point
graph.localToPage(x, y): PointSelection:
graph.getSelectedCells(): Cell[]
graph.select(cell: Cell | string): void
graph.unselect(cell: Cell | string): void
graph.resetSelection(): void
graph.isSelected(cell: Cell | string): booleanHistory:
graph.undo(): void
graph.redo(): void
graph.canUndo(): boolean
graph.canRedo(): boolean
graph.clearHistory(): voidClipboard:
graph.copy(cells: Cell[]): void
graph.cut(cells: Cell[]): void
graph.paste(): Cell[]
graph.isClipboardEmpty(): boolean
graph.cleanClipboard(): voidX6 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): thisCell 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
// 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 });
});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): thisinterface 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[]): thisimport { 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);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' },
],
};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 } },
});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',
}));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,
}));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
}));Note: Although
@antv/x6-plugin-historyis listed as a dependency inpackage.json, TMI does not import or use the X6 History plugin. Instead, TMI implements its own custom undo/redo history system viaAppOperationStateManagerand types defined inhistory.types.ts. This custom system provides operation-aware atomic batching, visual effect exclusion from history, and coordination with WebSocket-based collaborative editing.
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
InfraX6SelectionAdapterandSelectionService, which manage cell cloning, position calculation, and paste operations at a higher level than X6's built-in clipboard.
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.addTools([
{ name: 'vertices' },
{ name: 'source-arrowhead' },
{ name: 'target-arrowhead' },
{ name: 'button-remove', args: { distance: 0.5 } },
]);// Export to JSON
const data = graph.toJSON();
// Returns: { cells: [...] }
// Import from JSON
graph.fromJSON(data);
// or
graph.fromJSON({ cells: [...] });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.
-
Batch Operations: Use
graph.batchUpdate()for multiple changesgraph.batchUpdate(() => { // Multiple operations here });
-
Async Rendering: Enable for large graphs
new Graph({ async: true });
-
Frozen Graph: Pause rendering during bulk operations
graph.freeze(); // ... bulk operations graph.unfreeze();
- Always call
graph.dispose()when destroying the graph - Remove event listeners with
graph.off()when no longer needed - Clean up tools when nodes are removed
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);- Working-with-Data-Flow-Diagrams - User guide for DFD features
- Architecture-and-Design - System architecture overview
- Getting-Started-with-Development - Development setup
- Using TMI for Threat Modeling
- Accessing TMI
- Authentication
- Creating Your First Threat Model
- Understanding the User Interface
- Working with Data Flow Diagrams
- Managing Threats
- Collaborative Threat Modeling
- Using Notes and Documentation
- Timmy AI Assistant
- Metadata and Extensions
- Planning Your Deployment
- Terraform Deployment (AWS, OCI, GCP, Azure)
- Deploying TMI Server
- OCI Container Deployment
- Certificate Automation
- Deploying TMI Web Application
- Setting Up Authentication
- Database Setup
- Component Integration
- Post-Deployment
- Branding and Customization
- Monitoring and Health
- Cloud Logging
- Database Operations
- Security Operations
- Performance and Scaling
- Maintenance Tasks
- Getting Started with Development
- Architecture and Design
- API Integration
- Testing
- Contributing
- Extending TMI
- Dependency Upgrade Plans
- DFD Graphing Library Reference
- Migration Instructions