@moxa/graph 3.0.0-beta.1 → 3.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +602 -114
- package/assets/icon-sprite.svg +1 -1
- package/components/edge-label/index.d.ts +4 -4
- package/components/edge-label/index.d.ts.map +1 -1
- package/components/edge-polyline/index.d.ts.map +1 -1
- package/components/node-device/models/index.d.ts +1 -0
- package/components/node-device/models/index.d.ts.map +1 -1
- package/components/node-label/index.d.ts +1 -1
- package/components/node-label/index.d.ts.map +1 -1
- package/components/shared/transforms/edge-transform.d.ts.map +1 -1
- package/components/shared/transforms/node-transform.d.ts.map +1 -1
- package/components/shared/utils/node-utils/config.d.ts +27 -0
- package/components/shared/utils/node-utils/config.d.ts.map +1 -0
- package/components/shared/utils/node-utils/icon-style.d.ts.map +1 -1
- package/components/shared/utils/node-utils/index.d.ts +5 -5
- package/components/shared/utils/node-utils/index.d.ts.map +1 -1
- package/components/shared/utils/node-utils/offset.d.ts +2 -6
- package/components/shared/utils/node-utils/offset.d.ts.map +1 -1
- package/components/shared/utils/node-utils/text-style.d.ts.map +1 -1
- package/components/shared/utils/node-utils/title-style.d.ts.map +1 -1
- package/core/model/index.d.ts +1 -0
- package/core/model/index.d.ts.map +1 -1
- package/core/model/label.model.d.ts +2 -0
- package/core/model/label.model.d.ts.map +1 -1
- package/core/model/plugin.model.d.ts +1 -1
- package/core/model/text.model.d.ts +5 -0
- package/core/model/text.model.d.ts.map +1 -0
- package/index.cjs +57 -57
- package/index.js +3643 -3580
- package/layouts/grid/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/plugins/context-menu/index.d.ts +11 -1
- package/plugins/context-menu/index.d.ts.map +1 -1
- package/plugins/minimap/index.d.ts +3 -2
- package/plugins/minimap/index.d.ts.map +1 -1
- package/shared/transforms/plugin-transform.d.ts +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
- [Install](#install)
|
|
6
6
|
- [Basic Usage](#basic-usage)
|
|
7
|
+
- [Components](#components)
|
|
8
|
+
- [Behaviors](#behaviors)
|
|
9
|
+
- [Layouts](#layouts)
|
|
10
|
+
- [Plugins](#plugins)
|
|
7
11
|
- [Graph Control](#graph-control)
|
|
8
12
|
- [Event Handling](#event-handling)
|
|
9
13
|
- [Developer Guidelines](#developer-guidelines)
|
|
@@ -41,45 +45,34 @@ pnpm add @moxa/graph
|
|
|
41
45
|
import { Graph } from '@moxa/graph';
|
|
42
46
|
|
|
43
47
|
const graph = new Graph({
|
|
44
|
-
renderer: 'svg',
|
|
45
48
|
container: 'container',
|
|
46
49
|
width: 500,
|
|
47
50
|
height: 500,
|
|
51
|
+
renderer: 'canvas', // 'canvas', 'svg', or 'webgl'
|
|
48
52
|
data: {
|
|
49
53
|
nodes: [
|
|
50
54
|
{
|
|
51
55
|
id: 'node1',
|
|
52
|
-
|
|
53
|
-
type: '
|
|
54
|
-
|
|
56
|
+
data: {
|
|
57
|
+
type: 'node-device',
|
|
58
|
+
x: 100,
|
|
59
|
+
y: 50,
|
|
55
60
|
},
|
|
56
61
|
},
|
|
57
62
|
{
|
|
58
63
|
id: 'node2',
|
|
59
|
-
|
|
60
|
-
type: '
|
|
61
|
-
|
|
64
|
+
data: {
|
|
65
|
+
type: 'node-device',
|
|
66
|
+
x: 100,
|
|
67
|
+
y: 250,
|
|
62
68
|
},
|
|
63
69
|
},
|
|
64
70
|
{
|
|
65
71
|
id: 'node3',
|
|
66
|
-
|
|
67
|
-
type: '
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: 'node4',
|
|
73
|
-
config: {
|
|
74
|
-
type: 'circle-node',
|
|
75
|
-
point: { x: 300, y: 250 },
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: 'node5',
|
|
80
|
-
config: {
|
|
81
|
-
type: 'circle-node',
|
|
82
|
-
point: { x: 200, y: 150 },
|
|
72
|
+
data: {
|
|
73
|
+
type: 'node-device',
|
|
74
|
+
x: 300,
|
|
75
|
+
y: 50,
|
|
83
76
|
},
|
|
84
77
|
},
|
|
85
78
|
],
|
|
@@ -88,91 +81,228 @@ const graph = new Graph({
|
|
|
88
81
|
id: 'edge1',
|
|
89
82
|
source: 'node1',
|
|
90
83
|
target: 'node2',
|
|
84
|
+
data: {
|
|
85
|
+
type: 'edge-line',
|
|
86
|
+
},
|
|
91
87
|
},
|
|
92
88
|
{
|
|
93
89
|
id: 'edge2',
|
|
94
|
-
source: '
|
|
95
|
-
target: 'node5',
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
id: 'edge3',
|
|
99
|
-
source: 'node5',
|
|
90
|
+
source: 'node2',
|
|
100
91
|
target: 'node3',
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
source: 'node3',
|
|
105
|
-
target: 'node4',
|
|
92
|
+
data: {
|
|
93
|
+
type: 'edge-quadratic',
|
|
94
|
+
},
|
|
106
95
|
},
|
|
107
96
|
],
|
|
108
97
|
},
|
|
109
98
|
});
|
|
99
|
+
|
|
100
|
+
// Render the graph
|
|
101
|
+
graph.render();
|
|
110
102
|
```
|
|
111
103
|
|
|
104
|
+
## Components
|
|
105
|
+
|
|
106
|
+
### Node Components
|
|
107
|
+
- **node-device**: Device node with icon and label support
|
|
108
|
+
- **node-icon**: Icon-only node component
|
|
109
|
+
- **node-label**: Label-only node component
|
|
110
|
+
|
|
111
|
+
### Edge Components
|
|
112
|
+
- **edge-line**: Straight line edges
|
|
113
|
+
- **edge-polyline**: Multi-segment polyline edges
|
|
114
|
+
- **edge-quadratic**: Curved quadratic edges
|
|
115
|
+
- **edge-arrow**: Arrow markers for edges
|
|
116
|
+
- **edge-label**: Labels for edges
|
|
117
|
+
|
|
118
|
+
### Group Components
|
|
119
|
+
- **group-device**: Device group with expand/collapse functionality
|
|
120
|
+
|
|
121
|
+
## Behaviors
|
|
122
|
+
|
|
123
|
+
Interactive behaviors for user interaction:
|
|
124
|
+
|
|
125
|
+
- **brush-select**: Rectangle selection
|
|
126
|
+
- **click-select**: Click to select elements
|
|
127
|
+
- **collapse-expand**: Group collapse/expand
|
|
128
|
+
- **create-edge**: Interactive edge creation
|
|
129
|
+
- **drag-canvas**: Canvas dragging
|
|
130
|
+
- **drag-element**: Element dragging
|
|
131
|
+
- **fix-element-size**: Fixed element sizing
|
|
132
|
+
- **focus-element**: Element focusing
|
|
133
|
+
- **hover-activate**: Hover activation
|
|
134
|
+
- **scroll-canvas**: Canvas scrolling
|
|
135
|
+
- **select-all**: Select all elements
|
|
136
|
+
- **zoom-canvas**: Canvas zooming
|
|
137
|
+
|
|
138
|
+
## Layouts
|
|
139
|
+
|
|
140
|
+
Automatic layout algorithms:
|
|
141
|
+
|
|
142
|
+
- **align**: Alignment layout
|
|
143
|
+
- **force**: Force-directed layout
|
|
144
|
+
- **grid**: Grid layout
|
|
145
|
+
- **ring**: Circular ring layout
|
|
146
|
+
- **tree**: Hierarchical tree layout
|
|
147
|
+
|
|
148
|
+
## Plugins
|
|
149
|
+
|
|
150
|
+
Extensible plugin system:
|
|
151
|
+
|
|
152
|
+
- **context-menu**: Right-click context menu
|
|
153
|
+
- **element-toolbar**: Element-specific toolbar
|
|
154
|
+
- **fixed-toolbar**: Fixed position toolbar
|
|
155
|
+
- **graph-background**: Customizable graph background
|
|
156
|
+
- **history**: Undo/redo functionality
|
|
157
|
+
- **minimap**: Mini navigation map
|
|
158
|
+
- **snapline**: Element alignment guides
|
|
159
|
+
- **tooltip**: Element tooltips
|
|
160
|
+
|
|
112
161
|
## Graph Control
|
|
113
162
|
|
|
114
163
|
> You can control the presentation and behavior of the graph by calling the `Graph` methods
|
|
115
164
|
|
|
116
165
|
```typescript
|
|
117
|
-
//
|
|
118
|
-
graph.
|
|
119
|
-
graph.
|
|
120
|
-
graph.
|
|
166
|
+
// Data Management
|
|
167
|
+
graph.addNode({ id: 'node1', data: { type: 'node-device' } });
|
|
168
|
+
graph.updateNode('node1', { data: { x: 100, y: 100 } });
|
|
169
|
+
graph.removeNode('node1');
|
|
170
|
+
|
|
171
|
+
graph.addEdge({ id: 'edge1', source: 'node1', target: 'node2' });
|
|
172
|
+
graph.updateEdge('edge1', { data: { type: 'edge-line' } });
|
|
173
|
+
graph.removeEdge('edge1');
|
|
174
|
+
|
|
175
|
+
graph.addGroup({ id: 'group1', data: { type: 'group-device' } });
|
|
176
|
+
graph.updateGroup('group1', { data: { collapsed: true } });
|
|
177
|
+
graph.removeGroup('group1');
|
|
121
178
|
|
|
122
179
|
// View Control
|
|
123
|
-
graph.zoom();
|
|
180
|
+
graph.zoom(1.5);
|
|
181
|
+
graph.zoomTo(2.0, { x: 100, y: 100 });
|
|
124
182
|
graph.fitView();
|
|
125
|
-
graph.
|
|
183
|
+
graph.fitCenter();
|
|
184
|
+
graph.focusItem('node1');
|
|
185
|
+
|
|
186
|
+
// Layout
|
|
187
|
+
graph.setLayout({ type: 'force', options: {} });
|
|
188
|
+
graph.layout();
|
|
126
189
|
|
|
127
|
-
//
|
|
128
|
-
graph.
|
|
190
|
+
// Behaviors
|
|
191
|
+
graph.setBehavior(['drag-canvas', 'zoom-canvas']);
|
|
129
192
|
|
|
130
|
-
//
|
|
131
|
-
graph.
|
|
193
|
+
// Plugins
|
|
194
|
+
graph.setPlugin('minimap', { size: [200, 150] });
|
|
132
195
|
|
|
133
|
-
//
|
|
134
|
-
graph.
|
|
196
|
+
// Themes
|
|
197
|
+
graph.setTheme('dark');
|
|
198
|
+
graph.setThemeTokens({ primaryColor: '#1890ff' });
|
|
135
199
|
|
|
136
|
-
//
|
|
137
|
-
graph.
|
|
138
|
-
graph.
|
|
139
|
-
graph.removePlugin({ ... });
|
|
200
|
+
// State Management
|
|
201
|
+
graph.setElementState('node1', 'selected', true);
|
|
202
|
+
graph.clearElementState('node1', 'selected');
|
|
140
203
|
```
|
|
141
204
|
|
|
142
205
|
## Event Handling
|
|
143
206
|
|
|
144
207
|
```typescript
|
|
145
208
|
// Listen Events
|
|
146
|
-
graph.
|
|
147
|
-
|
|
148
|
-
|
|
209
|
+
graph.on('node:click', (event) => {
|
|
210
|
+
console.log('Node clicked:', event.itemId);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
graph.on('edge:click', (event) => {
|
|
214
|
+
console.log('Edge clicked:', event.itemId);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
graph.on('canvas:click', (event) => {
|
|
218
|
+
console.log('Canvas clicked:', event.canvas);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Remove event listeners
|
|
222
|
+
graph.off('node:click', listener);
|
|
223
|
+
|
|
224
|
+
// One-time event listeners
|
|
225
|
+
graph.once('afterrender', () => {
|
|
226
|
+
console.log('Graph rendered');
|
|
227
|
+
});
|
|
149
228
|
```
|
|
150
229
|
|
|
151
230
|
## Developer Guidelines
|
|
152
231
|
|
|
153
232
|
This section provides guidelines for developers who want to contribute to or extend the Moxa Graph library.
|
|
154
233
|
|
|
155
|
-
### Project Structure
|
|
234
|
+
### Project Structure & Folder Responsibilities
|
|
156
235
|
|
|
157
236
|
```
|
|
158
237
|
libs/graph/
|
|
159
|
-
├── src/
|
|
160
|
-
│ ├──
|
|
161
|
-
│
|
|
162
|
-
│
|
|
163
|
-
│
|
|
164
|
-
│
|
|
165
|
-
│
|
|
166
|
-
│
|
|
167
|
-
│
|
|
168
|
-
│
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
│ └── stories/ # Storybook examples
|
|
238
|
+
├── src/ # Source code
|
|
239
|
+
│ ├── core/ # 🔧 Core functionality (Graph API, models, utilities)
|
|
240
|
+
│ ├── components/ # 🎨 Visual components (nodes, edges, groups)
|
|
241
|
+
│ ├── behaviors/ # 🖱️ Interactive behaviors (12 behaviors)
|
|
242
|
+
│ ├── layouts/ # 📐 Layout algorithms (5 layouts)
|
|
243
|
+
│ ├── plugins/ # 🔌 Plugin system (8 plugins)
|
|
244
|
+
│ ├── shared/ # 🛠️ Shared utilities and types
|
|
245
|
+
│ ├── assets/ # 📦 Static assets (icons, images)
|
|
246
|
+
│ ├── styles/ # 🎨 Global styles
|
|
247
|
+
│ └── stories/ # 📚 API documentation
|
|
248
|
+
├── tests/ # 🧪 Test utilities
|
|
249
|
+
├── e2e/ # 🎭 End-to-end testing
|
|
250
|
+
├── .storybook/ # 📖 Storybook configuration
|
|
251
|
+
└── package.json # 📦 Package configuration
|
|
174
252
|
```
|
|
175
253
|
|
|
254
|
+
#### Detailed Folder Responsibilities
|
|
255
|
+
|
|
256
|
+
##### Core (`src/core/`)
|
|
257
|
+
**Purpose**: Essential graph functionality and APIs
|
|
258
|
+
- **`graph/`**: Main Graph class implementing the public API, lifecycle management, and core operations
|
|
259
|
+
- **`model/`**: TypeScript type definitions, interfaces, and data structures used throughout the library
|
|
260
|
+
- **`utils/`**: Core utility functions for graph manipulation, theme management, and element operations
|
|
261
|
+
|
|
262
|
+
##### Components (`src/components/`)
|
|
263
|
+
**Purpose**: Visual rendering components for graph elements
|
|
264
|
+
- **Node Components**: Different node types (device, icon, label) with specialized rendering logic
|
|
265
|
+
- **Edge Components**: Various edge types (line, polyline, quadratic) with arrow and label support
|
|
266
|
+
- **Group Components**: Container components for grouping and organizing elements
|
|
267
|
+
- **Shared**: Common utilities and transformations used across components
|
|
268
|
+
|
|
269
|
+
##### Behaviors (`src/behaviors/`)
|
|
270
|
+
**Purpose**: User interaction handling and graph manipulation
|
|
271
|
+
- Each behavior handles specific user interactions (clicking, dragging, selecting)
|
|
272
|
+
- Behaviors are modular and can be enabled/disabled independently
|
|
273
|
+
- Includes both mouse and keyboard interaction handling
|
|
274
|
+
|
|
275
|
+
##### Layouts (`src/layouts/`)
|
|
276
|
+
**Purpose**: Automatic positioning algorithms for graph elements
|
|
277
|
+
- **Force**: Physics-based layout for natural node arrangements
|
|
278
|
+
- **Tree**: Hierarchical layouts for tree-structured data
|
|
279
|
+
- **Grid**: Regular grid arrangements for systematic positioning
|
|
280
|
+
- **Ring**: Circular arrangements for cyclical data
|
|
281
|
+
- **Align**: Tools for manual element alignment
|
|
282
|
+
|
|
283
|
+
##### Plugins (`src/plugins/`)
|
|
284
|
+
**Purpose**: Extensible features that enhance graph functionality
|
|
285
|
+
- Each plugin is self-contained and optional
|
|
286
|
+
- Plugins can add UI elements (toolbars, tooltips, minimap)
|
|
287
|
+
- Includes data management features (history, context menus)
|
|
288
|
+
|
|
289
|
+
##### Shared (`src/shared/`)
|
|
290
|
+
**Purpose**: Common utilities and infrastructure
|
|
291
|
+
- **Types**: Shared TypeScript definitions used across modules
|
|
292
|
+
- **Utils**: Generic utility functions and shared components
|
|
293
|
+
- **Transforms**: Data transformation pipeline for processing graph configurations
|
|
294
|
+
- **Constants**: Application-wide constants and configuration values
|
|
295
|
+
|
|
296
|
+
##### Testing Infrastructure
|
|
297
|
+
- **`tests/`**: Reusable test utilities and helper functions
|
|
298
|
+
- **`e2e/`**: End-to-end visual regression testing with Playwright
|
|
299
|
+
- **Each component includes its own test suite with visual snapshots
|
|
300
|
+
|
|
301
|
+
##### Documentation & Development
|
|
302
|
+
- **`.storybook/`**: Interactive documentation and component playground
|
|
303
|
+
- **`stories/`**: Written documentation and API guides
|
|
304
|
+
- **Each component includes comprehensive Storybook stories for demonstration and testing
|
|
305
|
+
|
|
176
306
|
### Development Workflow
|
|
177
307
|
|
|
178
308
|
1. **Setup Development Environment**
|
|
@@ -194,92 +324,443 @@ libs/graph/
|
|
|
194
324
|
# Build the library
|
|
195
325
|
pnpm build:graph
|
|
196
326
|
|
|
197
|
-
# Run tests
|
|
327
|
+
# Run unit tests
|
|
198
328
|
pnpm test:graph
|
|
329
|
+
|
|
330
|
+
# Run e2e tests
|
|
331
|
+
pnpm e2e:graph
|
|
332
|
+
|
|
333
|
+
# Start Storybook for development
|
|
334
|
+
pnpm storybook:graph
|
|
199
335
|
```
|
|
200
336
|
|
|
201
337
|
### Extending the Library
|
|
202
338
|
|
|
203
|
-
#### Creating Custom
|
|
339
|
+
#### Creating Custom Components
|
|
204
340
|
|
|
205
|
-
Custom
|
|
341
|
+
Custom components are created using the G6 5.x API and registered with specific types:
|
|
206
342
|
|
|
207
343
|
```typescript
|
|
208
|
-
import {
|
|
344
|
+
import { ExtensionController } from '@antv/g6';
|
|
209
345
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
346
|
+
// Custom Node Component
|
|
347
|
+
const customNode = (context) => {
|
|
348
|
+
const { model, theme } = context;
|
|
349
|
+
const { data } = model;
|
|
213
350
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// Implement custom rendering logic
|
|
217
|
-
const shape = this.createShape('circle', {
|
|
351
|
+
return {
|
|
352
|
+
circle: {
|
|
218
353
|
r: 20,
|
|
219
|
-
fill:
|
|
220
|
-
|
|
354
|
+
fill: data.customProperty === 'special' ? 'red' : 'blue',
|
|
355
|
+
stroke: theme.nodeStroke,
|
|
356
|
+
lineWidth: 2,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
};
|
|
221
360
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
361
|
+
// Register the custom component
|
|
362
|
+
ExtensionController.register('node', 'custom-node', customNode);
|
|
225
363
|
|
|
226
|
-
//
|
|
227
|
-
|
|
364
|
+
// Use in graph data
|
|
365
|
+
const nodeData = {
|
|
366
|
+
id: 'node1',
|
|
367
|
+
data: {
|
|
368
|
+
type: 'custom-node',
|
|
369
|
+
customProperty: 'special',
|
|
370
|
+
},
|
|
371
|
+
};
|
|
228
372
|
```
|
|
229
373
|
|
|
230
374
|
#### Creating Custom Layouts
|
|
231
375
|
|
|
232
376
|
```typescript
|
|
233
|
-
import {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
const radius = 200;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
377
|
+
import { ExtensionController } from '@antv/g6';
|
|
378
|
+
|
|
379
|
+
// Custom Layout Implementation
|
|
380
|
+
const customLayout = (graph, options) => {
|
|
381
|
+
return {
|
|
382
|
+
id: 'custom-layout',
|
|
383
|
+
async execute() {
|
|
384
|
+
const { nodes } = graph.getData();
|
|
385
|
+
const { radius = 200 } = options;
|
|
386
|
+
|
|
387
|
+
nodes.forEach((node, index) => {
|
|
388
|
+
const angle = (index / nodes.length) * Math.PI * 2;
|
|
389
|
+
node.data.x = Math.cos(angle) * radius;
|
|
390
|
+
node.data.y = Math.sin(angle) * radius;
|
|
246
391
|
});
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
392
|
+
|
|
393
|
+
return { nodes, edges: graph.getData().edges };
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Register the layout
|
|
399
|
+
ExtensionController.register('layout', 'custom-layout', customLayout);
|
|
250
400
|
|
|
251
401
|
// Apply custom layout
|
|
252
|
-
graph.
|
|
253
|
-
|
|
402
|
+
graph.setLayout({
|
|
403
|
+
type: 'custom-layout',
|
|
404
|
+
options: { radius: 300 },
|
|
254
405
|
});
|
|
255
406
|
```
|
|
256
407
|
|
|
257
408
|
#### Creating Plugins
|
|
258
409
|
|
|
259
410
|
```typescript
|
|
260
|
-
import { BasePlugin
|
|
411
|
+
import { BasePlugin } from '@antv/g6';
|
|
261
412
|
|
|
262
413
|
class CustomPlugin extends BasePlugin {
|
|
414
|
+
constructor(options) {
|
|
415
|
+
super(options);
|
|
416
|
+
}
|
|
417
|
+
|
|
263
418
|
init() {
|
|
264
419
|
// Setup plugin
|
|
265
|
-
this.graph.
|
|
420
|
+
this.graph.on('node:click', this.handleNodeClick);
|
|
266
421
|
}
|
|
267
422
|
|
|
268
|
-
handleNodeClick = (
|
|
423
|
+
handleNodeClick = (event) => {
|
|
269
424
|
// Implement custom behavior
|
|
270
|
-
console.log('Node clicked:',
|
|
425
|
+
console.log('Node clicked:', event.itemId);
|
|
271
426
|
};
|
|
272
427
|
|
|
273
428
|
destroy() {
|
|
274
429
|
// Clean up
|
|
275
|
-
this.graph.
|
|
430
|
+
this.graph.off('node:click', this.handleNodeClick);
|
|
431
|
+
super.destroy();
|
|
276
432
|
}
|
|
277
433
|
}
|
|
278
434
|
|
|
279
435
|
// Add plugin to graph
|
|
280
|
-
graph.
|
|
436
|
+
graph.setPlugin('custom-plugin', new CustomPlugin({
|
|
437
|
+
// plugin options
|
|
438
|
+
}));
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Development Workflow
|
|
442
|
+
|
|
443
|
+
This section describes the complete development workflow for contributing to the Moxa Graph library, including component implementation, Storybook documentation, and visual testing.
|
|
444
|
+
|
|
445
|
+
### Directory Structure & Responsibilities
|
|
446
|
+
|
|
447
|
+
Each component follows a standardized directory structure:
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
{component-name}/
|
|
451
|
+
├── index.ts # Component export
|
|
452
|
+
├── models/ # TypeScript type definitions
|
|
453
|
+
│ └── index.ts
|
|
454
|
+
├── utils/ # Utility functions
|
|
455
|
+
│ └── index.ts
|
|
456
|
+
├── stories/ # Storybook documentation
|
|
457
|
+
│ ├── {story-name}.stories.ts # Storybook configuration
|
|
458
|
+
│ ├── {story-name}.component.ts # Angular wrapper component
|
|
459
|
+
│ └── data/ # Test data definitions
|
|
460
|
+
│ └── {story-name}-data.ts
|
|
461
|
+
├── tests/ # Visual regression tests
|
|
462
|
+
│ ├── {test-name}.spec.ts
|
|
463
|
+
│ └── __snapshots__/ # Visual test snapshots
|
|
464
|
+
│ └── {test-name}.png
|
|
465
|
+
└── transforms/ # Data transformation helpers (optional)
|
|
466
|
+
└── index.ts
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
#### Directory Responsibilities
|
|
470
|
+
|
|
471
|
+
- **`index.ts`**: Main component export, registers the component with G6
|
|
472
|
+
- **`models/`**: TypeScript interfaces and type definitions for component configuration
|
|
473
|
+
- **`utils/`**: Helper functions for styling, positioning, state management
|
|
474
|
+
- **`stories/`**: Complete Storybook documentation with interactive examples
|
|
475
|
+
- **`data/`**: Reusable test data configurations for different scenarios
|
|
476
|
+
- **`tests/`**: Playwright visual regression tests ensuring UI consistency
|
|
477
|
+
- **`transforms/`**: Data transformation utilities (for complex components)
|
|
478
|
+
|
|
479
|
+
### Development Process
|
|
480
|
+
|
|
481
|
+
#### 1. Component Implementation
|
|
482
|
+
|
|
483
|
+
Start by implementing the core component logic:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
// components/my-component/index.ts
|
|
487
|
+
import { ExtensionController } from '@antv/g6';
|
|
488
|
+
|
|
489
|
+
const myComponent = (context) => {
|
|
490
|
+
const { model, theme } = context;
|
|
491
|
+
const { data } = model;
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
// Component rendering logic
|
|
495
|
+
rect: {
|
|
496
|
+
width: data.width || 100,
|
|
497
|
+
height: data.height || 50,
|
|
498
|
+
fill: theme.primaryColor,
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Register component
|
|
504
|
+
ExtensionController.register('node', 'my-component', myComponent);
|
|
505
|
+
|
|
506
|
+
export { myComponent };
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
#### 2. Type Definitions
|
|
510
|
+
|
|
511
|
+
Define TypeScript interfaces in `models/`:
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
// components/my-component/models/index.ts
|
|
515
|
+
export interface MyComponentConfig {
|
|
516
|
+
width?: number;
|
|
517
|
+
height?: number;
|
|
518
|
+
customProperty?: string;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export interface MyComponentData extends NodeData {
|
|
522
|
+
type: 'my-component';
|
|
523
|
+
config: MyComponentConfig;
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
#### 3. Storybook Documentation
|
|
528
|
+
|
|
529
|
+
Create comprehensive Storybook stories:
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
// components/my-component/stories/my-component.stories.ts
|
|
533
|
+
import { Meta, StoryObj } from '@storybook/angular';
|
|
534
|
+
import { MY_COMPONENT_DATA } from './data/my-component-data';
|
|
535
|
+
import { MyComponentComponent } from './my-component.component';
|
|
536
|
+
|
|
537
|
+
export default {
|
|
538
|
+
title: 'Components/My Component',
|
|
539
|
+
component: MyComponentComponent,
|
|
540
|
+
parameters: {
|
|
541
|
+
docs: {
|
|
542
|
+
description: {
|
|
543
|
+
component: `
|
|
544
|
+
# My Component
|
|
545
|
+
|
|
546
|
+
This component demonstrates custom node rendering capabilities.
|
|
547
|
+
|
|
548
|
+
## Features
|
|
549
|
+
- Configurable dimensions
|
|
550
|
+
- Theme integration
|
|
551
|
+
- State management
|
|
552
|
+
|
|
553
|
+
## Usage
|
|
554
|
+
\`\`\`typescript
|
|
555
|
+
const graph = new Graph({
|
|
556
|
+
container: 'graph',
|
|
557
|
+
data: MY_COMPONENT_DATA,
|
|
558
|
+
});
|
|
559
|
+
\`\`\`
|
|
560
|
+
`,
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
args: {
|
|
565
|
+
graphData: MY_COMPONENT_DATA,
|
|
566
|
+
},
|
|
567
|
+
} as Meta;
|
|
568
|
+
|
|
569
|
+
type Story = StoryObj<MyComponentComponent>;
|
|
570
|
+
|
|
571
|
+
export const Basic: Story = {};
|
|
572
|
+
export const Advanced: Story = {
|
|
573
|
+
args: {
|
|
574
|
+
graphData: ADVANCED_MY_COMPONENT_DATA,
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
#### 4. Angular Wrapper Component
|
|
580
|
+
|
|
581
|
+
Create an Angular component for Storybook:
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
// components/my-component/stories/my-component.component.ts
|
|
585
|
+
import { Component, input, computed } from '@angular/core';
|
|
586
|
+
import { Graph, GraphConfig, GraphData } from '@moxa/graph';
|
|
587
|
+
import { StoryWrapperComponent, JsonViewerComponent } from '@shared/utils/components';
|
|
588
|
+
|
|
589
|
+
@Component({
|
|
590
|
+
selector: 'moxa-vizion-my-component',
|
|
591
|
+
imports: [JsonViewerComponent, StoryWrapperComponent],
|
|
592
|
+
template: `
|
|
593
|
+
<story-wrapper>
|
|
594
|
+
<json-viewer [data]="data()">
|
|
595
|
+
<div container [id]="id"></div>
|
|
596
|
+
</json-viewer>
|
|
597
|
+
</story-wrapper>
|
|
598
|
+
`,
|
|
599
|
+
})
|
|
600
|
+
export class MyComponentComponent {
|
|
601
|
+
graph!: Graph;
|
|
602
|
+
id: string = Math.random().toString(36).substring(2);
|
|
603
|
+
|
|
604
|
+
graphData = input<GraphData>();
|
|
605
|
+
|
|
606
|
+
data = computed<GraphConfig>(() => ({
|
|
607
|
+
container: this.id,
|
|
608
|
+
data: this.graphData()!,
|
|
609
|
+
}));
|
|
610
|
+
|
|
611
|
+
ngAfterViewInit(): void {
|
|
612
|
+
this.graph = new Graph(this.data());
|
|
613
|
+
this.graph.render();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
ngOnDestroy(): void {
|
|
617
|
+
this.graph.destroy();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
281
620
|
```
|
|
282
621
|
|
|
622
|
+
#### 5. Test Data
|
|
623
|
+
|
|
624
|
+
Define comprehensive test data:
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
// components/my-component/stories/data/my-component-data.ts
|
|
628
|
+
import { GraphData } from '@moxa/graph';
|
|
629
|
+
|
|
630
|
+
export const MY_COMPONENT_DATA: GraphData = {
|
|
631
|
+
nodes: [
|
|
632
|
+
{
|
|
633
|
+
id: 'node1',
|
|
634
|
+
data: {
|
|
635
|
+
type: 'my-component',
|
|
636
|
+
x: 100,
|
|
637
|
+
y: 100,
|
|
638
|
+
width: 120,
|
|
639
|
+
height: 60,
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
// More test scenarios...
|
|
643
|
+
],
|
|
644
|
+
};
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
#### 6. Visual Testing
|
|
648
|
+
|
|
649
|
+
Implement Playwright visual regression tests:
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
// components/my-component/stories/tests/my-component-basic.spec.ts
|
|
653
|
+
import { test } from '@playwright/test';
|
|
654
|
+
import {
|
|
655
|
+
getGraphContainer,
|
|
656
|
+
getStorybookUrl,
|
|
657
|
+
verifySnapshot,
|
|
658
|
+
} from '@tests/helpers';
|
|
659
|
+
|
|
660
|
+
const STORY_ID = 'components-my-component--basic';
|
|
661
|
+
const SNAPSHOT_NAME = 'my-component-basic.png';
|
|
662
|
+
|
|
663
|
+
test.describe('My Component Visual Tests', () => {
|
|
664
|
+
test.beforeEach(async ({ page }) => {
|
|
665
|
+
await page.setViewportSize({ width: 600, height: 500 });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test('should display basic component correctly', async ({
|
|
669
|
+
page,
|
|
670
|
+
baseURL,
|
|
671
|
+
}) => {
|
|
672
|
+
await page.goto(getStorybookUrl(baseURL, STORY_ID));
|
|
673
|
+
await page.waitForSelector('div.container', { timeout: 10000 });
|
|
674
|
+
|
|
675
|
+
const graphContainer = getGraphContainer(page);
|
|
676
|
+
await page.waitForTimeout(1000); // Wait for graph rendering
|
|
677
|
+
|
|
678
|
+
await verifySnapshot(graphContainer, SNAPSHOT_NAME);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Testing Strategy
|
|
684
|
+
|
|
685
|
+
#### Visual Regression Testing
|
|
686
|
+
|
|
687
|
+
The library uses Playwright for comprehensive visual regression testing:
|
|
688
|
+
|
|
689
|
+
1. **Snapshot Generation**: First test run generates baseline snapshots
|
|
690
|
+
2. **Automatic Comparison**: Subsequent runs compare against baselines
|
|
691
|
+
3. **Pixel-Perfect Accuracy**: Detects even minor visual changes
|
|
692
|
+
4. **Cross-Browser Support**: Ensures consistency across different browsers
|
|
693
|
+
|
|
694
|
+
#### Test Configuration
|
|
695
|
+
|
|
696
|
+
Playwright is configured for optimal visual testing:
|
|
697
|
+
|
|
698
|
+
```typescript
|
|
699
|
+
// playwright.config.ts highlights
|
|
700
|
+
export default defineConfig({
|
|
701
|
+
testMatch: ['**/e2e/**/*.spec.ts', '**/src/**/tests/*.spec.ts'],
|
|
702
|
+
snapshotPathTemplate: `{testFileDir}/__snapshots__/{arg}{ext}`,
|
|
703
|
+
use: {
|
|
704
|
+
viewport: { width: 1280, height: 720 },
|
|
705
|
+
deviceScaleFactor: 1, // Fixed DPR for consistency
|
|
706
|
+
// Disable animations for consistent screenshots
|
|
707
|
+
launchOptions: {
|
|
708
|
+
args: ['--disable-gpu', '--disable-web-security'],
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
expect: {
|
|
712
|
+
toMatchSnapshot: {
|
|
713
|
+
maxDiffPixelRatio: 0.01,
|
|
714
|
+
threshold: 0.1,
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### Development Commands
|
|
721
|
+
|
|
722
|
+
```bash
|
|
723
|
+
# Start Storybook for development
|
|
724
|
+
pnpm storybook:graph
|
|
725
|
+
|
|
726
|
+
# Run all tests
|
|
727
|
+
pnpm test:graph
|
|
728
|
+
|
|
729
|
+
# Run visual regression tests
|
|
730
|
+
pnpm e2e:graph
|
|
731
|
+
|
|
732
|
+
# Update visual snapshots (when intentional changes are made)
|
|
733
|
+
pnpm e2e:graph --update-snapshots
|
|
734
|
+
|
|
735
|
+
# Build the library
|
|
736
|
+
pnpm build:graph
|
|
737
|
+
|
|
738
|
+
# Generate documentation
|
|
739
|
+
pnpm docs:graph
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### Quality Assurance
|
|
743
|
+
|
|
744
|
+
#### Pre-commit Checklist
|
|
745
|
+
|
|
746
|
+
Before submitting code, ensure:
|
|
747
|
+
|
|
748
|
+
1. ✅ **Component Registration**: Component is properly registered with G6
|
|
749
|
+
2. ✅ **Type Safety**: All TypeScript interfaces are defined
|
|
750
|
+
3. ✅ **Storybook Stories**: Complete documentation with examples
|
|
751
|
+
4. ✅ **Test Data**: Comprehensive test scenarios
|
|
752
|
+
5. ✅ **Visual Tests**: All visual regression tests pass
|
|
753
|
+
6. ✅ **Documentation**: Component behavior is clearly documented
|
|
754
|
+
7. ✅ **Performance**: Component renders efficiently for large datasets
|
|
755
|
+
|
|
756
|
+
#### Code Review Process
|
|
757
|
+
|
|
758
|
+
1. **Functionality Review**: Verify component logic and API design
|
|
759
|
+
2. **Documentation Review**: Ensure Storybook stories are comprehensive
|
|
760
|
+
3. **Visual Review**: Check all visual test snapshots for accuracy
|
|
761
|
+
4. **Performance Review**: Validate rendering performance
|
|
762
|
+
5. **Integration Review**: Confirm component works with existing ecosystem
|
|
763
|
+
|
|
283
764
|
### Best Practices
|
|
284
765
|
|
|
285
766
|
1. **Performance Considerations**
|
|
@@ -293,10 +774,17 @@ graph.addPlugin('custom', new CustomPlugin());
|
|
|
293
774
|
- Support keyboard navigation for critical operations
|
|
294
775
|
|
|
295
776
|
3. **Testing**
|
|
296
|
-
- Write
|
|
297
|
-
- Create
|
|
777
|
+
- Write comprehensive visual regression tests for all component states
|
|
778
|
+
- Create multiple test scenarios covering edge cases
|
|
779
|
+
- Maintain up-to-date snapshots when making intentional visual changes
|
|
298
780
|
- Test across different browsers and device sizes
|
|
299
781
|
|
|
782
|
+
4. **Documentation**
|
|
783
|
+
- Provide clear usage examples in Storybook
|
|
784
|
+
- Document all configuration options
|
|
785
|
+
- Include interactive demos showing component capabilities
|
|
786
|
+
- Explain integration patterns with other components
|
|
787
|
+
|
|
300
788
|
### Troubleshooting
|
|
301
789
|
|
|
302
790
|
Common issues and their solutions:
|