@moxa/graph
v3.0.0-beta.9
Published
- [Install](#install) - [Basic Usage](#basic-usage) - [Components](#components) - [Behaviors](#behaviors) - [Layouts](#layouts) - [Plugins](#plugins) - [Graph Control](#graph-control) - [Event Handling](#event-handling) - [Developer Guidelines](#developer
Keywords
Readme
Moxa Graph Library
👉 Documentation
Install
Use npm
npm i @moxa/graphUse pnpm
pnpm add @moxa/graphBasic Usage
Graph will be generated on the dom element with the specified
id
Create a DOM
<div id="container"></div>Initialize Graph
You need to provide a json config object to initialize graph instance The config include all graph settings, please refer to
MxGraphConfig
import { Graph } from '@moxa/graph';
const graph = new Graph({
container: 'container',
width: 500,
height: 500,
renderer: 'canvas', // 'canvas', 'svg', or 'webgl'
data: {
nodes: [
{
id: 'node1',
data: {
type: 'node-device',
x: 100,
y: 50,
},
},
{
id: 'node2',
data: {
type: 'node-device',
x: 100,
y: 250,
},
},
{
id: 'node3',
data: {
type: 'node-device',
x: 300,
y: 50,
},
},
],
edges: [
{
id: 'edge1',
source: 'node1',
target: 'node2',
data: {
type: 'edge-line',
},
},
{
id: 'edge2',
source: 'node2',
target: 'node3',
data: {
type: 'edge-quadratic',
},
},
],
},
});
// Render the graph
graph.render();Components
Node Components
- node-device: Device node with icon and label support
- node-icon: Icon-only node component
- node-label: Label-only node component
Edge Components
- edge-line: Straight line edges
- edge-polyline: Multi-segment polyline edges
- edge-quadratic: Curved quadratic edges
- edge-arrow: Arrow markers for edges
- edge-label: Labels for edges
Group Components
- group-device: Device group with expand/collapse functionality
Behaviors
Interactive behaviors for user interaction:
- brush-select: Rectangle selection
- click-select: Click to select elements
- collapse-expand: Group collapse/expand
- create-edge: Interactive edge creation
- drag-canvas: Canvas dragging
- drag-element: Element dragging
- fix-element-size: Fixed element sizing
- focus-element: Element focusing
- hover-activate: Hover activation
- scroll-canvas: Canvas scrolling
- select-all: Select all elements
- zoom-canvas: Canvas zooming
Layouts
Automatic layout algorithms:
- align: Alignment layout
- force: Force-directed layout
- grid: Grid layout
- ring: Circular ring layout
- tree: Hierarchical tree layout
Plugins
Extensible plugin system:
- context-menu: Right-click context menu
- element-toolbar: Element-specific toolbar
- fixed-toolbar: Fixed position toolbar
- graph-background: Customizable graph background
- history: Undo/redo functionality
- minimap: Mini navigation map
- snapline: Element alignment guides
- tooltip: Element tooltips
Graph Control
You can control the presentation and behavior of the graph by calling the
Graphmethods
// Data Management
graph.addNode({ id: 'node1', data: { type: 'node-device' } });
graph.updateNode('node1', { data: { x: 100, y: 100 } });
graph.removeNode('node1');
graph.addEdge({ id: 'edge1', source: 'node1', target: 'node2' });
graph.updateEdge('edge1', { data: { type: 'edge-line' } });
graph.removeEdge('edge1');
graph.addGroup({ id: 'group1', data: { type: 'group-device' } });
graph.updateGroup('group1', { data: { collapsed: true } });
graph.removeGroup('group1');
// View Control
graph.zoom(1.5);
graph.zoomTo(2.0, { x: 100, y: 100 });
graph.fitView();
graph.fitCenter();
graph.focusItem('node1');
// Layout
graph.setLayout({ type: 'force', options: {} });
graph.layout();
// Behaviors
graph.setBehavior(['drag-canvas', 'zoom-canvas']);
// Plugins
graph.setPlugin('minimap', { size: [200, 150] });
// Themes
graph.setTheme('dark');
graph.setThemeTokens({ primaryColor: '#1890ff' });
// State Management
graph.setElementState('node1', 'selected', true);
graph.clearElementState('node1', 'selected');Event Handling
// Listen Events
graph.on('node:click', (event) => {
console.log('Node clicked:', event.itemId);
});
graph.on('edge:click', (event) => {
console.log('Edge clicked:', event.itemId);
});
graph.on('canvas:click', (event) => {
console.log('Canvas clicked:', event.canvas);
});
// Remove event listeners
graph.off('node:click', listener);
// One-time event listeners
graph.once('afterrender', () => {
console.log('Graph rendered');
});Developer Guidelines
This section provides guidelines for developers who want to contribute to or extend the Moxa Graph library.
Project Structure & Folder Responsibilities
libs/graph/
├── src/ # Source code
│ ├── core/ # 🔧 Core functionality (Graph API, models, utilities)
│ ├── components/ # 🎨 Visual components (nodes, edges, groups)
│ ├── behaviors/ # 🖱️ Interactive behaviors (12 behaviors)
│ ├── layouts/ # 📐 Layout algorithms (5 layouts)
│ ├── plugins/ # 🔌 Plugin system (8 plugins)
│ ├── shared/ # 🛠️ Shared utilities and types
│ ├── assets/ # 📦 Static assets (icons, images)
│ ├── styles/ # 🎨 Global styles
│ └── stories/ # 📚 API documentation
├── tests/ # 🧪 Test utilities
├── e2e/ # 🎭 End-to-end testing
├── .storybook/ # 📖 Storybook configuration
└── package.json # 📦 Package configurationDetailed Folder Responsibilities
Core (src/core/)
Purpose: Essential graph functionality and APIs
graph/: Main Graph class implementing the public API, lifecycle management, and core operationsmodel/: TypeScript type definitions, interfaces, and data structures used throughout the libraryutils/: Core utility functions for graph manipulation, theme management, and element operations
Components (src/components/)
Purpose: Visual rendering components for graph elements
- Node Components: Different node types (device, icon, label) with specialized rendering logic
- Edge Components: Various edge types (line, polyline, quadratic) with arrow and label support
- Group Components: Container components for grouping and organizing elements
- Shared: Common utilities and transformations used across components
Behaviors (src/behaviors/)
Purpose: User interaction handling and graph manipulation
- Each behavior handles specific user interactions (clicking, dragging, selecting)
- Behaviors are modular and can be enabled/disabled independently
- Includes both mouse and keyboard interaction handling
Layouts (src/layouts/)
Purpose: Automatic positioning algorithms for graph elements
- Force: Physics-based layout for natural node arrangements
- Tree: Hierarchical layouts for tree-structured data
- Grid: Regular grid arrangements for systematic positioning
- Ring: Circular arrangements for cyclical data
- Align: Tools for manual element alignment
Plugins (src/plugins/)
Purpose: Extensible features that enhance graph functionality
- Each plugin is self-contained and optional
- Plugins can add UI elements (toolbars, tooltips, minimap)
- Includes data management features (history, context menus)
Shared (src/shared/)
Purpose: Common utilities and infrastructure
- Types: Shared TypeScript definitions used across modules
- Utils: Generic utility functions and shared components
- Transforms: Data transformation pipeline for processing graph configurations
- Constants: Application-wide constants and configuration values
Testing Infrastructure
tests/: Reusable test utilities and helper functionse2e/: End-to-end visual regression testing with Playwright- **Each component includes its own test suite with visual snapshots
Documentation & Development
.storybook/: Interactive documentation and component playgroundstories/: Written documentation and API guides- **Each component includes comprehensive Storybook stories for demonstration and testing
Development Workflow
Setup Development Environment
# Clone the repository git clone <repository-url> # Install dependencies pnpm install # Start development server pnpm devBuilding the Library
# Build the library pnpm build:graph # Run unit tests pnpm test:graph # Run e2e tests pnpm e2e:graph # Start Storybook for development pnpm storybook:graph
Extending the Library
Creating Custom Components
Custom components are created using the G6 5.x API and registered with specific types:
import { ExtensionController } from '@antv/g6';
// Custom Node Component
const customNode = (context) => {
const { model, theme } = context;
const { data } = model;
return {
circle: {
r: 20,
fill: data.customProperty === 'special' ? 'red' : 'blue',
stroke: theme.nodeStroke,
lineWidth: 2,
},
};
};
// Register the custom component
ExtensionController.register('node', 'custom-node', customNode);
// Use in graph data
const nodeData = {
id: 'node1',
data: {
type: 'custom-node',
customProperty: 'special',
},
};Creating Custom Layouts
import { ExtensionController } from '@antv/g6';
// Custom Layout Implementation
const customLayout = (graph, options) => {
return {
id: 'custom-layout',
async execute() {
const { nodes } = graph.getData();
const { radius = 200 } = options;
nodes.forEach((node, index) => {
const angle = (index / nodes.length) * Math.PI * 2;
node.data.x = Math.cos(angle) * radius;
node.data.y = Math.sin(angle) * radius;
});
return { nodes, edges: graph.getData().edges };
},
};
};
// Register the layout
ExtensionController.register('layout', 'custom-layout', customLayout);
// Apply custom layout
graph.setLayout({
type: 'custom-layout',
options: { radius: 300 },
});Creating Plugins
import { BasePlugin } from '@antv/g6';
class CustomPlugin extends BasePlugin {
constructor(options) {
super(options);
}
init() {
// Setup plugin
this.graph.on('node:click', this.handleNodeClick);
}
handleNodeClick = (event) => {
// Implement custom behavior
console.log('Node clicked:', event.itemId);
};
destroy() {
// Clean up
this.graph.off('node:click', this.handleNodeClick);
super.destroy();
}
}
// Add plugin to graph
graph.setPlugin(
'custom-plugin',
new CustomPlugin({
// plugin options
}),
);Development Workflow
This section describes the complete development workflow for contributing to the Moxa Graph library, including component implementation, Storybook documentation, and visual testing.
Directory Structure & Responsibilities
Each component follows a standardized directory structure:
{component-name}/
├── index.ts # Component export
├── models/ # TypeScript type definitions
│ └── index.ts
├── utils/ # Utility functions
│ └── index.ts
├── stories/ # Storybook documentation
│ ├── {story-name}.stories.ts # Storybook configuration
│ ├── {story-name}.component.ts # Angular wrapper component
│ └── data/ # Test data definitions
│ └── {story-name}-data.ts
├── tests/ # Visual regression tests
│ ├── {test-name}.spec.ts
│ └── __snapshots__/ # Visual test snapshots
│ └── {test-name}.png
└── transforms/ # Data transformation helpers (optional)
└── index.tsDirectory Responsibilities
index.ts: Main component export, registers the component with G6models/: TypeScript interfaces and type definitions for component configurationutils/: Helper functions for styling, positioning, state managementstories/: Complete Storybook documentation with interactive examplesdata/: Reusable test data configurations for different scenarios
tests/: Playwright visual regression tests ensuring UI consistencytransforms/: Data transformation utilities (for complex components)
Development Process
1. Component Implementation
Start by implementing the core component logic:
// components/my-component/index.ts
import { ExtensionController } from '@antv/g6';
const myComponent = (context) => {
const { model, theme } = context;
const { data } = model;
return {
// Component rendering logic
rect: {
width: data.width || 100,
height: data.height || 50,
fill: theme.primaryColor,
},
};
};
// Register component
ExtensionController.register('node', 'my-component', myComponent);
export { myComponent };2. Type Definitions
Define TypeScript interfaces in models/:
// components/my-component/models/index.ts
export interface MyComponentConfig {
width?: number;
height?: number;
customProperty?: string;
}
export interface MyComponentData extends NodeData {
type: 'my-component';
config: MyComponentConfig;
}3. Storybook Documentation
Create comprehensive Storybook stories:
// components/my-component/stories/my-component.stories.ts
import { Meta, StoryObj } from '@storybook/angular';
import { MY_COMPONENT_DATA } from './data/my-component-data';
import { MyComponentComponent } from './my-component.component';
export default {
title: 'Components/My Component',
component: MyComponentComponent,
parameters: {
docs: {
description: {
component: `
# My Component
This component demonstrates custom node rendering capabilities.
## Features
- Configurable dimensions
- Theme integration
- State management
## Usage
\`\`\`typescript
const graph = new Graph({
container: 'graph',
data: MY_COMPONENT_DATA,
});
\`\`\`
`,
},
},
},
args: {
graphData: MY_COMPONENT_DATA,
},
} as Meta;
type Story = StoryObj<MyComponentComponent>;
export const Basic: Story = {};
export const Advanced: Story = {
args: {
graphData: ADVANCED_MY_COMPONENT_DATA,
},
};4. Angular Wrapper Component
Create an Angular component for Storybook:
// components/my-component/stories/my-component.component.ts
import { Component, input, computed } from '@angular/core';
import { Graph, GraphConfig, GraphData } from '@moxa/graph';
import { StoryWrapperComponent, JsonViewerComponent } from '@shared/utils/components';
@Component({
selector: 'moxa-vizion-my-component',
imports: [JsonViewerComponent, StoryWrapperComponent],
template: `
<story-wrapper>
<json-viewer [data]="data()">
<div container [id]="id"></div>
</json-viewer>
</story-wrapper>
`,
})
export class MyComponentComponent {
graph!: Graph;
id: string = Math.random().toString(36).substring(2);
graphData = input<GraphData>();
data = computed<GraphConfig>(() => ({
container: this.id,
data: this.graphData()!,
}));
ngAfterViewInit(): void {
this.graph = new Graph(this.data());
this.graph.render();
}
ngOnDestroy(): void {
this.graph.destroy();
}
}5. Test Data
Define comprehensive test data:
// components/my-component/stories/data/my-component-data.ts
import { GraphData } from '@moxa/graph';
export const MY_COMPONENT_DATA: GraphData = {
nodes: [
{
id: 'node1',
data: {
type: 'my-component',
x: 100,
y: 100,
width: 120,
height: 60,
},
},
// More test scenarios...
],
};6. Visual Testing
Implement Playwright visual regression tests:
// components/my-component/stories/tests/my-component-basic.spec.ts
import { test } from '@playwright/test';
import { getGraphContainer, getStorybookUrl, verifySnapshot } from '@tests/helpers';
const STORY_ID = 'components-my-component--basic';
const SNAPSHOT_NAME = 'my-component-basic.png';
test.describe('My Component Visual Tests', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize({ width: 600, height: 500 });
});
test('should display basic component correctly', async ({ page, baseURL }) => {
await page.goto(getStorybookUrl(baseURL, STORY_ID));
await page.waitForSelector('div.container', { timeout: 10000 });
const graphContainer = getGraphContainer(page);
await page.waitForTimeout(1000); // Wait for graph rendering
await verifySnapshot(graphContainer, SNAPSHOT_NAME);
});
});Testing Strategy
Visual Regression Testing
The library uses Playwright for comprehensive visual regression testing:
- Snapshot Generation: First test run generates baseline snapshots
- Automatic Comparison: Subsequent runs compare against baselines
- Pixel-Perfect Accuracy: Detects even minor visual changes
- Cross-Browser Support: Ensures consistency across different browsers
Test Configuration
Playwright is configured for optimal visual testing:
// playwright.config.ts highlights
export default defineConfig({
testMatch: ['**/e2e/**/*.spec.ts', '**/src/**/tests/*.spec.ts'],
snapshotPathTemplate: `{testFileDir}/__snapshots__/{arg}{ext}`,
use: {
viewport: { width: 1280, height: 720 },
deviceScaleFactor: 1, // Fixed DPR for consistency
// Disable animations for consistent screenshots
launchOptions: {
args: ['--disable-gpu', '--disable-web-security'],
},
},
expect: {
toMatchSnapshot: {
maxDiffPixelRatio: 0.01,
threshold: 0.1,
},
},
});Development Commands
# Start Storybook for development
pnpm storybook:graph
# Run all tests
pnpm test:graph
# Run visual regression tests
pnpm e2e:graph
# Update visual snapshots (when intentional changes are made)
pnpm e2e:graph --update-snapshots
# Build the library
pnpm build:graph
# Generate documentation
pnpm docs:graphQuality Assurance
Pre-commit Checklist
Before submitting code, ensure:
- ✅ Component Registration: Component is properly registered with G6
- ✅ Type Safety: All TypeScript interfaces are defined
- ✅ Storybook Stories: Complete documentation with examples
- ✅ Test Data: Comprehensive test scenarios
- ✅ Visual Tests: All visual regression tests pass
- ✅ Documentation: Component behavior is clearly documented
- ✅ Performance: Component renders efficiently for large datasets
Code Review Process
- Functionality Review: Verify component logic and API design
- Documentation Review: Ensure Storybook stories are comprehensive
- Visual Review: Check all visual test snapshots for accuracy
- Performance Review: Validate rendering performance
- Integration Review: Confirm component works with existing ecosystem
Best Practices
Performance Considerations
- Use throttling and debouncing for event handlers on large graphs
- Implement pagination or virtualization for very large datasets
- Consider using WebGL renderer for graphs with thousands of elements
Accessibility
- Ensure proper contrast ratios in your themes
- Add ARIA attributes to important interactive elements
- Support keyboard navigation for critical operations
Testing
- Write comprehensive visual regression tests for all component states
- Create multiple test scenarios covering edge cases
- Maintain up-to-date snapshots when making intentional visual changes
- Test across different browsers and device sizes
Documentation
- Provide clear usage examples in Storybook
- Document all configuration options
- Include interactive demos showing component capabilities
- Explain integration patterns with other components
Troubleshooting
Common issues and their solutions:
Graph not rendering properly
- Check if container has proper dimensions
- Verify data format follows required schema
- Check browser console for errors
Performance issues with large graphs
- Try different renderers (SVG vs Canvas vs WebGL)
- Simplify node/edge renderers
- Use appropriate layout algorithms
Events not firing
- Verify event listeners are properly attached
- Check if event propagation is being stopped
- Ensure targets exist when attaching listeners
