• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

graphty-org / graphty-element / 19792929756

30 Nov 2025 02:57AM UTC coverage: 86.308% (+3.9%) from 82.377%
19792929756

push

github

apowers313
docs: fix stories for chromatic

3676 of 4303 branches covered (85.43%)

Branch coverage included in aggregate %.

17 of 17 new or added lines in 2 files covered. (100.0%)

1093 existing lines in 30 files now uncovered.

17371 of 20083 relevant lines covered (86.5%)

7075.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.31
/src/managers/DataManager.ts
1
import jmespath from "jmespath";
3!
2

3
import type {AdHocData} from "../config";
4
import {DataSource} from "../data/DataSource";
3✔
5
import {Edge, EdgeMap} from "../Edge";
3✔
6
import type {LayoutEngine} from "../layout/LayoutEngine";
7
import {MeshCache} from "../meshes/MeshCache";
3✔
8
import {Node, NodeIdType} from "../Node";
3✔
9
import type {Styles} from "../Styles";
10
import type {EventManager} from "./EventManager";
11
import type {GraphContext} from "./GraphContext";
12
import type {Manager} from "./interfaces";
13

14
// Type guards for layout engines with optional removal methods
15
type LayoutEngineWithRemoveNode = LayoutEngine & {removeNode(node: Node): void};
16
type LayoutEngineWithRemoveEdge = LayoutEngine & {removeEdge(edge: Edge): void};
17

18
function hasRemoveNode(engine: LayoutEngine): engine is LayoutEngineWithRemoveNode {
4✔
19
    return "removeNode" in engine;
4✔
20
}
4✔
21

22
function hasRemoveEdge(engine: LayoutEngine): engine is LayoutEngineWithRemoveEdge {
2✔
23
    return "removeEdge" in engine;
2✔
24
}
2✔
25

26
/**
27
 * Manages all data operations for nodes and edges
28
 * Handles CRUD operations, caching, and data source loading
29
 */
30
export class DataManager implements Manager {
3✔
31
    // Node and edge collections
32
    nodes = new Map<string | number, Node>();
3✔
33
    edges = new Map<string | number, Edge>();
3✔
34
    nodeCache = new Map<NodeIdType, Node>();
3✔
35
    edgeCache = new EdgeMap();
3✔
36

37
    // Graph-level algorithm results storage
38
    graphResults?: AdHocData;
39

40
    // Mesh cache for performance
41
    meshCache: MeshCache;
42

43
    // GraphContext for creating nodes and edges
44
    private graphContext: GraphContext | null = null;
3✔
45

46
    // State management flags
47
    private shouldStartLayout = false;
3✔
48
    private shouldZoomToFit = false;
3✔
49

50
    // Buffer for edges added before their nodes exist
51
    private bufferedEdges: {
3✔
52
        edge: Record<string | number, unknown>;
53
        srcIdPath?: string;
54
        dstIdPath?: string;
55
    }[] = [];
3✔
56

57
    constructor(
3✔
58
        private eventManager: EventManager,
588✔
59
        private styles: Styles,
588✔
60
    ) {
588✔
61
        this.meshCache = new MeshCache();
588✔
62
    }
588✔
63

64
    /**
65
     * Update the styles reference when styles change
66
     */
67
    updateStyles(styles: Styles): void {
3✔
68
        this.styles = styles;
326✔
69
        // Re-apply styles to all existing nodes and edges
70
        this.applyStylesToExistingNodes();
326✔
71
        this.applyStylesToExistingEdges();
326✔
72
    }
326✔
73

74
    /**
75
     * Apply styles to all existing nodes
76
     */
77
    applyStylesToExistingNodes(): void {
3✔
78
        for (const n of this.nodes.values()) {
418✔
79
            // First, recalculate and apply the base style from the new template
80
            // This handles static style values (color, shape, size, etc.)
81
            const newStyleId = this.styles.getStyleForNode(n.data, n.algorithmResults);
2,283✔
82
            n.updateStyle(newStyleId);
2,283✔
83

84
            // Then run calculated values immediately since node data (including algorithmResults) is already populated
85
            // This sets values in n.styleUpdates (same as changeManager.dataObjects.style)
86
            n.changeManager.loadCalculatedValues(this.styles.getCalculatedStylesForNode(n.data), true);
2,283✔
87
            // Call update() to merge calculated style updates with the base style
88
            // update() checks styleUpdates and creates a new styleId that includes calculated values
89
            n.update();
2,283✔
90
        }
2,283✔
91
    }
418✔
92

93
    /**
94
     * Apply styles to all existing edges
95
     */
96
    applyStylesToExistingEdges(): void {
3✔
97
        for (const e of this.edges.values()) {
418✔
98
            // Combine data and algorithmResults for selector matching and calculated style evaluation
99
            const combinedData = {... e.data, algorithmResults: e.algorithmResults};
3,337✔
100

101
            // First, recalculate and apply the base style from the new template
102
            // This handles static style values (color, width, arrow types, etc.)
103
            const newStyleId = this.styles.getStyleForEdge(combinedData);
3,337✔
104
            e.updateStyle(newStyleId);
3,337✔
105

106
            // Then run calculated values immediately since edge data (including algorithmResults) is already populated
107
            // This sets values in e.styleUpdates (same as changeManager.dataObjects.style)
108
            e.changeManager.loadCalculatedValues(this.styles.getCalculatedStylesForEdge(combinedData), true);
3,337✔
109
            // Call update() to merge calculated style updates with the base style
110
            // update() checks styleUpdates and creates a new styleId that includes calculated values
111
            e.update();
3,337✔
112
        }
3,337✔
113
    }
418✔
114

115
    /**
116
     * Set the GraphContext for creating nodes and edges
117
     */
118
    setGraphContext(context: GraphContext): void {
3✔
119
        this.graphContext = context;
572✔
120
    }
572✔
121

122
    /**
123
     * Set the layout engine reference for adding nodes/edges
124
     */
125
    private layoutEngine?: LayoutEngine;
126

127
    setLayoutEngine(engine: LayoutEngine | undefined): void {
3✔
128
        this.layoutEngine = engine;
1,306✔
129
    }
1,306✔
130

131
    async init(): Promise<void> {
3✔
132
        // DataManager doesn't need async initialization
133
        return Promise.resolve();
509✔
134
    }
509✔
135

136
    dispose(): void {
3✔
137
        // Clear all collections
138
        this.nodes.clear();
468✔
139
        this.edges.clear();
468✔
140
        this.nodeCache.clear();
468✔
141
        this.edgeCache.clear();
468✔
142

143
        // Clear graph-level results
144
        this.graphResults = undefined;
468✔
145

146
        // Clear mesh cache
147
        this.meshCache.clear();
468✔
148
    }
468✔
149

150
    // Node operations
151

152
    addNode(node: AdHocData, idPath?: string): void {
3✔
153
        this.addNodes([node], idPath);
280✔
154
    }
280✔
155

156
    addNodes(nodes: Record<string | number, unknown>[], idPath?: string): void {
3✔
157
        // create path to node ids
158
        const query = idPath ?? this.styles.config.data.knownFields.nodeIdPath;
1,093✔
159

160
        // create nodes
161
        for (const node of nodes) {
1,093✔
162
            const nodeId = jmespath.search(node, query) as NodeIdType;
4,750✔
163

164
            if (this.nodeCache.get(nodeId)) {
4,750✔
165
                continue;
96✔
166
            }
96✔
167

168
            const styleId = this.styles.getStyleForNode(node as AdHocData);
4,654✔
169
            if (!this.graphContext) {
4,750!
UNCOV
170
                throw new Error("GraphContext not set. Call setGraphContext before adding nodes.");
×
UNCOV
171
            }
✔
172

173
            const n = new Node(this.graphContext, nodeId, styleId, node as AdHocData, {
4,654✔
174
                pinOnDrag: this.graphContext.getConfig().pinOnDrag,
4,654✔
175
            });
4,654✔
176
            this.nodeCache.set(nodeId, n);
4,654✔
177
            this.nodes.set(nodeId, n);
4,654✔
178

179
            // Add to layout engine if it exists
180
            if (this.layoutEngine) {
4,654✔
181
                this.layoutEngine.addNode(n);
4,654✔
182
            }
4,654✔
183

184
            // Emit node added event
185
            this.eventManager.emitNodeEvent("node-add-before", {
4,654✔
186
                nodeId,
4,654✔
187
                metadata: node,
4,654✔
188
            });
4,654✔
189
        }
4,654✔
190

191
        // Notify that nodes were added
192
        if (nodes.length > 0) {
1,093✔
193
            // Request layout start and zoom to fit
194
            this.shouldStartLayout = true;
1,093✔
195
            this.shouldZoomToFit = true;
1,093✔
196

197
            // Process any buffered edges whose nodes now exist
198
            this.processBufferedEdges();
1,093✔
199

200
            // Emit event to notify graph that data has been added
201
            this.eventManager.emitDataAdded("nodes", nodes.length, true, true);
1,093✔
202
        }
1,093✔
203
    }
1,093✔
204

205
    /**
206
     * Process buffered edges whose nodes now exist
207
     * Called after nodes are added to retry edge creation
208
     */
209
    private processBufferedEdges(): void {
3✔
210
        if (this.bufferedEdges.length === 0) {
1,093✔
211
            return;
1,086✔
212
        }
1,086✔
213

214
        // Try to process all buffered edges
215
        const stillBuffered: typeof this.bufferedEdges = [];
7✔
216

217
        for (const {edge, srcIdPath, dstIdPath} of this.bufferedEdges) {
64✔
218
            // get paths
219
            const srcQuery = srcIdPath ?? this.styles.config.data.knownFields.edgeSrcIdPath;
23✔
220
            const dstQuery = dstIdPath ?? this.styles.config.data.knownFields.edgeDstIdPath;
23✔
221

222
            const srcNodeId = jmespath.search(edge, srcQuery) as NodeIdType;
23✔
223
            const dstNodeId = jmespath.search(edge, dstQuery) as NodeIdType;
23✔
224

225
            // Check if both nodes now exist
226
            const srcNode = this.nodeCache.get(srcNodeId);
23✔
227
            const dstNode = this.nodeCache.get(dstNodeId);
23✔
228

229
            if (!srcNode || !dstNode) {
23✔
230
                // Nodes still don't exist, keep in buffer
231
                stillBuffered.push({edge, srcIdPath, dstIdPath});
9✔
232
                continue;
9✔
233
            }
9✔
234

235
            // Check if edge already exists
236
            if (this.edgeCache.get(srcNodeId, dstNodeId)) {
22!
UNCOV
237
                continue;
×
UNCOV
238
            }
✔
239

240
            // Create the edge now that both nodes exist
241
            const style = this.styles.getStyleForEdge(edge as AdHocData);
14✔
242
            const opts = {};
14✔
243
            if (!this.graphContext) {
22!
UNCOV
244
                throw new Error("GraphContext not set. Call setGraphContext before adding edges.");
×
UNCOV
245
            }
✔
246

247
            const e = new Edge(this.graphContext, srcNodeId, dstNodeId, style, edge as AdHocData, opts);
14✔
248
            this.edgeCache.set(srcNodeId, dstNodeId, e);
14✔
249
            this.edges.set(e.id, e);
14✔
250

251
            // Add to layout engine if it exists
252
            if (this.layoutEngine) {
14✔
253
                this.layoutEngine.addEdge(e);
14✔
254
            }
14✔
255

256
            // Emit edge added event
257
            this.eventManager.emitEdgeEvent("edge-add-before", {
14✔
258
                srcNodeId,
14✔
259
                dstNodeId,
14✔
260
                metadata: edge,
14✔
261
            });
14✔
262
        }
14✔
263

264
        // Update buffer with edges that still couldn't be processed
265
        this.bufferedEdges = stillBuffered;
7✔
266
    }
1,093✔
267

268
    getNode(nodeId: NodeIdType): Node | undefined {
3✔
269
        return this.nodes.get(nodeId);
17✔
270
    }
17✔
271

272
    removeNode(nodeId: NodeIdType): boolean {
3✔
273
        const node = this.nodes.get(nodeId);
5✔
274
        if (!node) {
5✔
275
            return false;
1✔
276
        }
1✔
277

278
        // Remove from collections
279
        this.nodes.delete(nodeId);
4✔
280
        this.nodeCache.delete(nodeId);
4✔
281

282
        // Remove from layout engine
283
        if (this.layoutEngine && hasRemoveNode(this.layoutEngine)) {
5!
UNCOV
284
            this.layoutEngine.removeNode(node);
×
UNCOV
285
        }
✔
286

287
        // TODO: Remove connected edges
288

289
        return true;
4✔
290
    }
5✔
291

292
    // Edge operations
293

294
    addEdge(edge: AdHocData, srcIdPath?: string, dstIdPath?: string): void {
3✔
295
        this.addEdges([edge], srcIdPath, dstIdPath);
167✔
296
    }
167✔
297

298
    addEdges(edges: Record<string | number, unknown>[], srcIdPath?: string, dstIdPath?: string): void {
3✔
299
        // get paths
300
        const srcQuery = srcIdPath ?? this.styles.config.data.knownFields.edgeSrcIdPath;
11,776✔
301
        const dstQuery = dstIdPath ?? this.styles.config.data.knownFields.edgeDstIdPath;
11,776✔
302

303
        // create edges
304
        for (const edge of edges) {
11,776✔
305
            const srcNodeId = jmespath.search(edge, srcQuery) as NodeIdType;
19,530✔
306
            const dstNodeId = jmespath.search(edge, dstQuery) as NodeIdType;
19,530✔
307

308
            if (this.edgeCache.get(srcNodeId, dstNodeId)) {
19,530✔
309
                continue;
57✔
310
            }
57✔
311

312
            // Check if both nodes exist before creating edge
313
            const srcNode = this.nodeCache.get(srcNodeId);
19,473✔
314
            const dstNode = this.nodeCache.get(dstNodeId);
19,473✔
315

316
            if (!srcNode || !dstNode) {
19,530✔
317
                // Buffer this edge to be processed later when nodes exist
318
                this.bufferedEdges.push({edge, srcIdPath, dstIdPath});
11,317✔
319
                continue;
11,317✔
320
            }
11,317✔
321

322
            const style = this.styles.getStyleForEdge(edge as AdHocData);
8,156✔
323
            const opts = {};
8,156✔
324
            if (!this.graphContext) {
8,230!
325
                throw new Error("GraphContext not set. Call setGraphContext before adding edges.");
×
UNCOV
326
            }
✔
327

328
            const e = new Edge(this.graphContext, srcNodeId, dstNodeId, style, edge as AdHocData, opts);
8,156✔
329
            this.edgeCache.set(srcNodeId, dstNodeId, e);
8,156✔
330
            this.edges.set(e.id, e);
8,156✔
331

332
            // Add to layout engine if it exists
333
            if (this.layoutEngine) {
8,156✔
334
                this.layoutEngine.addEdge(e);
8,156✔
335
            }
8,156✔
336

337
            // Emit edge added event
338
            this.eventManager.emitEdgeEvent("edge-add-before", {
8,156✔
339
                srcNodeId,
8,156✔
340
                dstNodeId,
8,156✔
341
                metadata: edge,
8,156✔
342
            });
8,156✔
343
        }
8,156✔
344

345
        // Notify that edges were added
346
        if (edges.length > 0) {
11,776✔
347
            // Request layout start
348
            this.shouldStartLayout = true;
11,773✔
349
            // Emit event to notify graph that data has been added
350
            this.eventManager.emitDataAdded("edges", edges.length, true, false);
11,773✔
351
        }
11,773✔
352
    }
11,776✔
353

354
    getEdge(edgeId: string | number): Edge | undefined {
3✔
355
        return this.edges.get(edgeId);
14✔
356
    }
14✔
357

358
    getEdgeBetween(srcNodeId: NodeIdType, dstNodeId: NodeIdType): Edge | undefined {
3✔
UNCOV
359
        return this.edgeCache.get(srcNodeId, dstNodeId);
×
UNCOV
360
    }
×
361

362
    removeEdge(edgeId: string | number): boolean {
3✔
363
        const edge = this.edges.get(edgeId);
3✔
364
        if (!edge) {
3✔
365
            return false;
1✔
366
        }
1✔
367

368
        // Remove from collections
369
        this.edges.delete(edgeId);
2✔
370
        this.edgeCache.delete(edge.srcNode.id, edge.dstNode.id);
2✔
371

372
        // Remove from layout engine
373
        if (this.layoutEngine && hasRemoveEdge(this.layoutEngine)) {
3!
UNCOV
374
            this.layoutEngine.removeEdge(edge);
×
UNCOV
375
        }
✔
376

377
        return true;
2✔
378
    }
3✔
379

380
    // Data source operations
381

382
    async addDataFromSource(type: string, opts: object = {}): Promise<void> {
3✔
383
        const startTime = Date.now();
104✔
384
        let nodesLoaded = 0;
104✔
385
        let edgesLoaded = 0;
104✔
386
        let chunksProcessed = 0;
104✔
387

388
        try {
104✔
389
            const source = DataSource.get(type, opts);
104✔
390
            if (!source) {
104!
UNCOV
391
                throw new TypeError(`No data source named: ${type}`);
×
UNCOV
392
            }
×
393

394
            // Get file size for progress tracking (if available)
395
            const fileSize = (opts as {size?: number}).size;
104✔
396

397
            try {
104✔
398
                for await (const chunk of source.getData()) {
104✔
399
                    this.addNodes(chunk.nodes);
104✔
400
                    this.addEdges(chunk.edges);
104✔
401

402
                    nodesLoaded += chunk.nodes.length;
104✔
403
                    edgesLoaded += chunk.edges.length;
104✔
404
                    chunksProcessed++;
104✔
405

406
                    // Emit progress event
407
                    if (this.graphContext) {
104✔
408
                        this.eventManager.emitDataLoadingProgress(
104✔
409
                            type,
104✔
410
                            chunksProcessed * 64 * 1024, // Approximate bytes (chunk size)
104✔
411
                            fileSize,
104✔
412
                            nodesLoaded,
104✔
413
                            edgesLoaded,
104✔
414
                            chunksProcessed,
104✔
415
                        );
104✔
416
                    }
104✔
417
                }
104✔
418

419
                // Emit error summary if there were errors
420
                if (this.graphContext) {
104✔
421
                    const errorAggregator = source.getErrorAggregator();
104✔
422
                    if (errorAggregator.getErrorCount() > 0) {
104!
UNCOV
423
                        const summary = errorAggregator.getSummary();
×
UNCOV
424
                        this.eventManager.emitDataLoadingErrorSummary(
×
UNCOV
425
                            type,
×
UNCOV
426
                            summary.totalErrors,
×
UNCOV
427
                            summary.message,
×
UNCOV
428
                            errorAggregator.getDetailedReport(),
×
UNCOV
429
                            summary.primaryCategory,
×
UNCOV
430
                            summary.suggestion,
×
UNCOV
431
                        );
×
UNCOV
432
                    }
×
433
                }
104✔
434

435
                // Emit completion event
436
                if (this.graphContext) {
104✔
437
                    const duration = Date.now() - startTime;
104✔
438
                    const errorCount = source.getErrorAggregator().getErrorCount();
104✔
439
                    this.eventManager.emitDataLoadingComplete(
104✔
440
                        type,
104✔
441
                        nodesLoaded,
104✔
442
                        edgesLoaded,
104✔
443
                        duration,
104✔
444
                        errorCount,
104✔
445
                        0, // warnings
104✔
446
                        true,
104✔
447
                    );
104✔
448
                }
104✔
449

450
                // Keep existing data-loaded event for backward compatibility
451
                if (this.graphContext) {
104✔
452
                    this.eventManager.emitGraphDataLoaded(this.graphContext, chunksProcessed, type);
104✔
453
                }
104✔
454
            } catch (error) {
104!
455
                // Emit error event
UNCOV
456
                if (this.graphContext) {
×
UNCOV
457
                    this.eventManager.emitDataLoadingError(
×
UNCOV
458
                        error instanceof Error ? error : new Error(String(error)),
×
UNCOV
459
                        "parsing",
×
UNCOV
460
                        type,
×
UNCOV
461
                        {canContinue: false},
×
UNCOV
462
                    );
×
463

464
                    // Keep existing error event for backward compatibility
UNCOV
465
                    this.eventManager.emitGraphError(
×
UNCOV
466
                        this.graphContext,
×
UNCOV
467
                        error instanceof Error ? error : new Error(String(error)),
×
UNCOV
468
                        "data-loading",
×
UNCOV
469
                        {chunksLoaded: chunksProcessed, dataSourceType: type},
×
UNCOV
470
                    );
×
UNCOV
471
                }
×
472

UNCOV
473
                throw new Error(`Failed to load data from source '${type}' after ${chunksProcessed} chunks: ${error instanceof Error ? error.message : String(error)}`);
×
UNCOV
474
            }
×
475
        } catch (error) {
104!
476
            // Re-throw if already a processed error
UNCOV
477
            if (error instanceof Error && error.message.includes("Failed to load data")) {
×
UNCOV
478
                throw error;
×
UNCOV
479
            }
×
480

481
            // Otherwise wrap and throw
UNCOV
482
            throw new Error(`Error initializing data source '${type}': ${error instanceof Error ? error.message : String(error)}`);
×
UNCOV
483
        }
×
484
    }
104✔
485

486
    // Utility methods
487

488
    /**
489
     * Clear all data
490
     */
491
    clear(): void {
3✔
492
        // Remove all nodes and edges
UNCOV
493
        this.nodes.clear();
×
UNCOV
494
        this.edges.clear();
×
UNCOV
495
        this.nodeCache.clear();
×
UNCOV
496
        this.edgeCache.clear();
×
497

498
        // Clear graph-level results
UNCOV
499
        this.graphResults = undefined;
×
500

501
        // Clear mesh cache
UNCOV
502
        this.meshCache.clear();
×
503

504
        // TODO: Notify layout engine to clear
UNCOV
505
    }
×
506

507
    /**
508
     * Start label animations for all nodes
509
     * Called when layout has settled
510
     */
511
    startLabelAnimations(): void {
3✔
UNCOV
512
        for (const node of this.nodes.values()) {
×
UNCOV
513
            node.label?.startAnimation();
×
UNCOV
514
        }
×
UNCOV
515
    }
×
516

517
    /**
518
     * Get statistics about the data
519
     */
520
    getStats(): {
3✔
521
        nodeCount: number;
522
        edgeCount: number;
523
        cachedMeshes: number;
UNCOV
524
    } {
×
UNCOV
525
        return {
×
UNCOV
526
            nodeCount: this.nodes.size,
×
UNCOV
527
            edgeCount: this.edges.size,
×
UNCOV
528
            cachedMeshes: this.meshCache.size(),
×
UNCOV
529
        };
×
UNCOV
530
    }
×
531
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc