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

graphty-org / graphty-monorepo / 20661584252

02 Jan 2026 03:50PM UTC coverage: 77.924% (+7.3%) from 70.62%
20661584252

push

github

apowers313
ci: fix flakey performance test

13438 of 17822 branches covered (75.4%)

Branch coverage included in aggregate %.

41247 of 52355 relevant lines covered (78.78%)

145534.85 hits per line

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

87.5
/graphty-element/src/layout/NGraphLayoutEngine.ts
1
import ngraphCreateLayout, { Layout as NGraphLayout } from "ngraph.forcelayout";
14!
2
import createGraph, { Graph as NGraph, Link as NGraphLink, Node as NGraphNode } from "ngraph.graph";
14✔
3
import random from "ngraph.random";
14!
4
import { z } from "zod/v4";
14✔
5

6
import { defineOptions, type OptionsSchema } from "../config";
14✔
7
import type { Edge } from "../Edge";
8
import type { Node } from "../Node";
9
import { EdgePosition, LayoutEngine, Position } from "./LayoutEngine";
14✔
10

11
/**
12
 * Zod-based options schema for NGraph Force Layout
13
 */
14
export const ngraphLayoutOptionsSchema = defineOptions({
14✔
15
    dim: {
14✔
16
        schema: z.number().int().min(2).max(3).default(3),
14✔
17
        meta: {
14✔
18
            label: "Dimensions",
14✔
19
            description: "Layout dimensionality (2D or 3D)",
14✔
20
        },
14✔
21
    },
14✔
22
    springLength: {
14✔
23
        schema: z.number().positive().default(30),
14✔
24
        meta: {
14✔
25
            label: "Spring Length",
14✔
26
            description: "Ideal spring length between connected nodes",
14✔
27
        },
14✔
28
    },
14✔
29
    springCoefficient: {
14✔
30
        schema: z.number().positive().default(0.0008),
14✔
31
        meta: {
14✔
32
            label: "Spring Coefficient",
14✔
33
            description: "Spring stiffness coefficient",
14✔
34
            step: 0.0001,
14✔
35
            advanced: true,
14✔
36
        },
14✔
37
    },
14✔
38
    gravity: {
14✔
39
        schema: z.number().default(-1.2),
14✔
40
        meta: {
14✔
41
            label: "Gravity",
14✔
42
            description: "Gravity strength (negative for repulsion)",
14✔
43
            step: 0.1,
14✔
44
        },
14✔
45
    },
14✔
46
    theta: {
14✔
47
        schema: z.number().positive().default(0.8),
14✔
48
        meta: {
14✔
49
            label: "Theta",
14✔
50
            description: "Barnes-Hut approximation parameter",
14✔
51
            step: 0.1,
14✔
52
            advanced: true,
14✔
53
        },
14✔
54
    },
14✔
55
    dragCoefficient: {
14✔
56
        schema: z.number().positive().default(0.02),
14✔
57
        meta: {
14✔
58
            label: "Drag Coefficient",
14✔
59
            description: "Velocity damping coefficient",
14✔
60
            step: 0.01,
14✔
61
            advanced: true,
14✔
62
        },
14✔
63
    },
14✔
64
    timeStep: {
14✔
65
        schema: z.number().positive().default(20),
14✔
66
        meta: {
14✔
67
            label: "Time Step",
14✔
68
            description: "Simulation time step size",
14✔
69
            advanced: true,
14✔
70
        },
14✔
71
    },
14✔
72
    seed: {
14✔
73
        schema: z.number().int().positive().nullable().default(null),
14✔
74
        meta: {
14✔
75
            label: "Random Seed",
14✔
76
            description: "Seed for reproducible layout",
14✔
77
            advanced: true,
14✔
78
        },
14✔
79
    },
14✔
80
});
14✔
81

82
/**
83
 * NGraph force-directed layout engine using ngraph.forcelayout
84
 */
85
export class NGraphEngine extends LayoutEngine {
14✔
86
    static type = "ngraph";
14✔
87
    static maxDimensions = 3;
14✔
88
    static zodOptionsSchema: OptionsSchema = ngraphLayoutOptionsSchema;
14✔
89
    ngraph: NGraph;
90
    ngraphLayout: NGraphLayout<NGraph>;
91

92
    /**
93
     * Get dimension-specific options for NGraph layout
94
     * @param dimension - The desired dimension (2 or 3)
95
     * @returns Options object with dim parameter
96
     */
97
    static getOptionsForDimension(dimension: 2 | 3): object {
14✔
98
        return { dim: dimension };
3,583✔
99
    }
3,583✔
100
    nodeMapping = new Map<Node, NGraphNode>();
14✔
101
    edgeMapping = new Map<Edge, NGraphLink>();
14✔
102
    _settled = true;
14✔
103
    _stepCount = 0;
14✔
104
    _lastMoves: number[] = [];
14✔
105

106
    /**
107
     * Create an NGraph layout engine
108
     * @param config - Configuration options for the NGraph simulation
109
     */
110
    constructor(config: object = {}) {
14✔
111
        super();
1,879✔
112
        this.ngraph = createGraph();
1,879✔
113

114
        // Cast config to a more specific type for property access
115
        const typedConfig = config as Record<string, unknown>;
1,879✔
116

117
        // Build ngraph configuration from provided config
118
        const ngraphConfig: Record<string, unknown> = {
1,879✔
119
            dimensions: typedConfig.dim !== undefined ? typedConfig.dim : 3,
1,879!
120
        };
1,879✔
121

122
        // Map from layout config to ngraph parameters
123
        if (typedConfig.springLength !== undefined) {
1,879!
124
            ngraphConfig.springLength = typedConfig.springLength;
×
125
        }
×
126

127
        if (typedConfig.springCoefficient !== undefined) {
1,879!
128
            ngraphConfig.springCoefficient = typedConfig.springCoefficient;
×
129
        }
×
130

131
        if (typedConfig.gravity !== undefined) {
1,879!
132
            ngraphConfig.gravity = typedConfig.gravity;
×
133
        }
×
134

135
        if (typedConfig.theta !== undefined) {
1,879!
136
            ngraphConfig.theta = typedConfig.theta;
×
137
        }
×
138

139
        if (typedConfig.dragCoefficient !== undefined) {
1,879!
140
            ngraphConfig.dragCoefficient = typedConfig.dragCoefficient;
×
141
        }
×
142

143
        if (typedConfig.timeStep !== undefined) {
1,879!
144
            ngraphConfig.timeStep = typedConfig.timeStep;
×
145
        }
×
146

147
        // Add random number generator with seed if provided
148
        if (typedConfig.seed !== undefined && typeof typedConfig.seed === "number") {
1,879✔
149
            ngraphConfig.random = random(typedConfig.seed);
72✔
150
        }
72✔
151

152
        this.ngraphLayout = ngraphCreateLayout(this.ngraph, ngraphConfig);
1,879✔
153
    }
1,879✔
154

155
    /**
156
     * Initialize the layout engine
157
     *
158
     * NGraph layout is initialized in the constructor and doesn't require
159
     * additional async initialization.
160
     */
161
    async init(): Promise<void> {
14✔
162
        // No-op - NGraph layout is ready after construction
163
    }
1,879✔
164

165
    /**
166
     * Advance the NGraph simulation by one step
167
     */
168
    step(): void {
14✔
169
        const ngraphSettled = this.ngraphLayout.step();
8,375✔
170
        const { lastMove } = this.ngraphLayout;
8,375✔
171
        const nodeCount = this.nodeMapping.size;
8,375✔
172
        const ratio = nodeCount > 0 ? lastMove / nodeCount : 0;
8,375!
173

174
        this._stepCount++;
8,375✔
175

176
        // Keep track of last 10 moves for averaging
177
        this._lastMoves.push(ratio);
8,375✔
178
        if (this._lastMoves.length > 10) {
8,375✔
179
            this._lastMoves.shift();
5,752✔
180
        }
5,752✔
181

182
        // Calculate average movement over last 10 steps
183
        const avgMovement =
8,375✔
184
            this._lastMoves.length > 0 ? this._lastMoves.reduce((a, b) => a + b, 0) / this._lastMoves.length : 0;
8,375!
185

186
        // Use a more forgiving threshold or force settling after many steps
187
        const customThreshold = 0.05; // More forgiving than ngraph's 0.01
8,375✔
188
        const maxSteps = 1000; // Force settling after 1000 steps
8,375✔
189

190
        this._settled = ngraphSettled || avgMovement <= customThreshold || this._stepCount >= maxSteps;
8,375✔
191
    }
8,375✔
192

193
    /**
194
     * Check if the simulation has settled
195
     * @returns True if the simulation has settled
196
     */
197
    get isSettled(): boolean {
14✔
198
        return this._settled;
36,141✔
199
    }
36,141✔
200

201
    /**
202
     * Add a node to the NGraph simulation
203
     * @param n - The node to add
204
     */
205
    addNode(n: Node): void {
14✔
206
        const ngraphNode: NGraphNode = this.ngraph.addNode(n.id, { parentNode: n });
4,124✔
207
        this.nodeMapping.set(n, ngraphNode);
4,124✔
208
        this._settled = false;
4,124✔
209
        this._stepCount = 0;
4,124✔
210
        this._lastMoves = [];
4,124✔
211
    }
4,124✔
212

213
    /**
214
     * Add an edge to the NGraph simulation
215
     * @param e - The edge to add
216
     */
217
    addEdge(e: Edge): void {
14✔
218
        const ngraphEdge = this.ngraph.addLink(e.srcId, e.dstId, { parentEdge: this });
4,909✔
219
        this.edgeMapping.set(e, ngraphEdge);
4,909✔
220
        this._settled = false;
4,909✔
221
        this._stepCount = 0;
4,909✔
222
        this._lastMoves = [];
4,909✔
223
    }
4,909✔
224

225
    /**
226
     * Get the current position of a node
227
     * @param n - The node to get position for
228
     * @returns The node's position coordinates
229
     */
230
    getNodePosition(n: Node): Position {
14✔
231
        const ngraphNode = this._getMappedNode(n);
132,059✔
232
        return this.ngraphLayout.getNodePosition(ngraphNode.id);
132,059✔
233
    }
132,059✔
234

235
    /**
236
     * Set a node's position in the simulation
237
     * @param n - The node to set position for
238
     * @param newPos - The new position coordinates
239
     */
240
    setNodePosition(n: Node, newPos: Position): void {
14✔
241
        const ngraphNode = this._getMappedNode(n);
6✔
242
        const currPos = this.ngraphLayout.getNodePosition(ngraphNode.id);
6✔
243
        currPos.x = newPos.x;
6✔
244
        currPos.y = newPos.y;
6✔
245
        currPos.z = newPos.z;
6✔
246
    }
6✔
247

248
    /**
249
     * Get the position of an edge based on its endpoint positions
250
     * @param e - The edge to get position for
251
     * @returns The edge's source and destination positions
252
     */
253
    getEdgePosition(e: Edge): EdgePosition {
14✔
254
        const ngraphEdge = this._getMappedEdge(e);
307,244✔
255
        const pos = this.ngraphLayout.getLinkPosition(ngraphEdge.id);
307,244✔
256
        return {
307,244✔
257
            src: {
307,244✔
258
                x: pos.from.x,
307,244✔
259
                y: pos.from.y,
307,244✔
260
                z: pos.from.z,
307,244✔
261
            },
307,244✔
262
            dst: {
307,244✔
263
                x: pos.to.x,
307,244✔
264
                y: pos.to.y,
307,244✔
265
                z: pos.to.z,
307,244✔
266
            },
307,244✔
267
        };
307,244✔
268
    }
307,244✔
269

270
    /**
271
     * Get all nodes in the simulation
272
     * @returns Iterable of nodes
273
     */
274
    get nodes(): Iterable<Node> {
14✔
275
        // ...is this cheating?
276
        return this.nodeMapping.keys();
8,417✔
277
    }
8,417✔
278

279
    /**
280
     * Get all edges in the simulation
281
     * @returns Iterable of edges
282
     */
283
    get edges(): Iterable<Edge> {
14✔
284
        return this.edgeMapping.keys();
39,931✔
285
    }
39,931✔
286

287
    /**
288
     * Pin a node to its current position
289
     * @param n - The node to pin
290
     */
291
    pin(n: Node): void {
14✔
292
        const ngraphNode = this._getMappedNode(n);
13✔
293
        this.ngraphLayout.pinNode(ngraphNode, true);
13✔
294
    }
13✔
295

296
    /**
297
     * Unpin a node to allow it to move freely
298
     * @param n - The node to unpin
299
     */
300
    unpin(n: Node): void {
14✔
301
        const ngraphNode = this._getMappedNode(n);
1✔
302
        this.ngraphLayout.pinNode(ngraphNode, false);
1✔
303
    }
1✔
304

305
    private _getMappedNode(n: Node): NGraphNode {
14✔
306
        const ngraphNode = this.nodeMapping.get(n);
132,079✔
307
        if (!ngraphNode) {
132,079!
308
            throw new Error("Internal error: Node not found in NGraphEngine");
×
309
        }
×
310

311
        return ngraphNode;
132,079✔
312
    }
132,079✔
313

314
    private _getMappedEdge(e: Edge): NGraphLink {
14✔
315
        const ngraphNode = this.edgeMapping.get(e);
307,244✔
316
        if (!ngraphNode) {
307,244!
317
            throw new Error("Internal error: Edge not found in NGraphEngine");
×
318
        }
×
319

320
        return ngraphNode;
307,244✔
321
    }
307,244✔
322
}
14✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc