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

graphty-org / graphty-element / 20514590651

26 Dec 2025 02:37AM UTC coverage: 70.559% (-0.3%) from 70.836%
20514590651

push

github

apowers313
ci: fix npm ci

9591 of 13363 branches covered (71.77%)

Branch coverage included in aggregate %.

25136 of 35854 relevant lines covered (70.11%)

6233.71 hits per line

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

87.0
/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,360✔
170
        const {lastMove} = this.ngraphLayout;
8,360✔
171
        const nodeCount = this.nodeMapping.size;
8,360✔
172
        const ratio = nodeCount > 0 ? lastMove / nodeCount : 0;
8,360!
173

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

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

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

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

191
        this._settled = ngraphSettled ||
8,360✔
192
                       avgMovement <= customThreshold ||
8,291✔
193
                       this._stepCount >= maxSteps;
8,088✔
194
    }
8,360✔
195

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

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

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

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

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

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

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

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

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

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

308
    private _getMappedNode(n: Node): NGraphNode {
14✔
309
        const ngraphNode = this.nodeMapping.get(n);
130,469✔
310
        if (!ngraphNode) {
130,469!
311
            throw new Error("Internal error: Node not found in NGraphEngine");
×
312
        }
×
313

314
        return ngraphNode;
130,469✔
315
    }
130,469✔
316

317
    private _getMappedEdge(e: Edge): NGraphLink {
14✔
318
        const ngraphNode = this.edgeMapping.get(e);
303,329✔
319
        if (!ngraphNode) {
303,329!
320
            throw new Error("Internal error: Edge not found in NGraphEngine");
×
321
        }
×
322

323
        return ngraphNode;
303,329✔
324
    }
303,329✔
325
}
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