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

graphty-org / graphty-element / 16242819426

12 Jul 2025 10:52PM UTC coverage: 72.536% (-1.2%) from 73.747%
16242819426

push

github

apowers313
docs: add a chromatic-specific test for node styles

523 of 721 branches covered (72.54%)

Branch coverage included in aggregate %.

3996 of 5509 relevant lines covered (72.54%)

1084.38 hits per line

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

34.5
/src/layout/D3GraphLayoutEngine.ts
1
import {
1✔
2
    Edge as D3Edge,
3
    forceCenter,
1✔
4
    forceLink,
1✔
5
    forceManyBody,
1✔
6
    forceSimulation,
1✔
7
    InputEdge as D3InputEdge,
8
    Node as D3Node,
9
} from "d3-force-3d";
1✔
10
import {z} from "zod/v4";
1✔
11

12
import type {Edge} from "../Edge";
13
import type {Node, NodeIdType} from "../Node";
14
import {EdgePosition, LayoutEngine, Position} from "./LayoutEngine";
1✔
15

16
interface D3InputNode extends Partial<D3Node> {
17
    id: NodeIdType;
18
}
19

20
function isD3Node(n: unknown): n is D3Node {
×
21
    if (typeof n === "object" &&
×
22
        n !== null &&
×
23
        "index" in n &&
×
24
        typeof n.index === "number" &&
×
25
        "x" in n &&
×
26
        typeof n.x === "number" &&
×
27
        "y" in n &&
×
28
        typeof n.y === "number" &&
×
29
        "z" in n &&
×
30
        typeof n.z === "number" &&
×
31
        "vx" in n &&
×
32
        typeof n.vx === "number" &&
×
33
        "vy" in n &&
×
34
        typeof n.vy === "number" &&
×
35
        "vz" in n &&
×
36
        typeof n.vz === "number") {
×
37
        return true;
×
38
    }
×
39

40
    return false;
×
41
}
×
42

43
export const D3LayoutConfig = z.strictObject({
1✔
44
    alphaMin: z.number().positive().default(0.1),
1✔
45
    alphaTarget: z.number().min(0).default(0),
1✔
46
    alphaDecay: z.number().positive().default(0.0228),
1✔
47
    velocityDecay: z.number().positive().default(0.4),
1✔
48
});
1✔
49

50
export type D3LayoutOptions = Partial<z.infer<typeof D3LayoutConfig>>;
51

52
function isD3Edge(e: unknown): e is D3Edge {
×
53
    if (typeof e === "object" &&
×
54
        e !== null &&
×
55
        Object.hasOwn(e, "index") &&
×
56
        "index" in e &&
×
57
        typeof e.index === "number" &&
×
58
        "source" in e &&
×
59
        isD3Node(e.source) &&
×
60
        "target" in e &&
×
61
        isD3Node(e.target)) {
×
62
        return true;
×
63
    }
×
64

65
    return false;
×
66
}
×
67

68
export class D3GraphEngine extends LayoutEngine {
1✔
69
    static type = "d3";
1✔
70
    static maxDimensions = 3;
1✔
71
    d3ForceLayout: ReturnType<typeof forceSimulation>;
72
    d3AlphaMin: number;
73
    d3AlphaTarget: number;
74
    d3AlphaDecay: number;
75
    d3VelocityDecay: number;
76
    nodeMapping = new Map<Node, D3Node>();
1✔
77
    edgeMapping = new Map<Edge, D3Edge>();
1✔
78
    newNodeMap = new Map<Node, D3InputNode>();
1✔
79
    newEdgeMap = new Map<Edge, D3InputEdge>();
1✔
80
    reheat = false;
1✔
81

82
    get graphNeedsRefresh(): boolean {
1✔
83
        return !!this.newNodeMap.size || !!this.newEdgeMap.size;
×
84
    }
×
85

86
    constructor(anyOpts: D3LayoutOptions = {}) {
1✔
87
        super();
1✔
88

89
        const opts = D3LayoutConfig.parse(anyOpts);
1✔
90
        this.d3AlphaMin = opts.alphaMin;
1✔
91
        this.d3AlphaTarget = opts.alphaTarget;
1✔
92
        this.d3AlphaDecay = opts.alphaDecay;
1✔
93
        this.d3VelocityDecay = opts.velocityDecay;
1✔
94

95
        // https://github.com/vasturiano/d3-force-3d?tab=readme-ov-file#links
96
        const fl = forceLink();
1✔
97
        fl.strength(0.9);
1✔
98
        this.d3ForceLayout = forceSimulation()
1✔
99
            .numDimensions(3)
1✔
100
            .alpha(1)
1✔
101
            .force("link", fl)
1✔
102
            .force("charge", forceManyBody())
1✔
103
            .force("center", forceCenter())
1✔
104
            .force("dagRadial", null)
1✔
105
            .stop();
1✔
106
        this.d3ForceLayout.force("link").id((d) => (d as D3InputNode).id);
1✔
107
    }
1✔
108

109
    // eslint-disable-next-line @typescript-eslint/no-empty-function
110
    async init(): Promise<void> {}
1✔
111

112
    refresh(): void {
1✔
113
        if (this.graphNeedsRefresh || this.reheat) {
×
114
            // update nodes
115
            let nodeList: (D3Node | D3InputNode)[] = [... this.nodeMapping.values()];
×
116
            nodeList = nodeList.concat([... this.newNodeMap.values()]);
×
117
            this.d3ForceLayout
×
118
                .alpha(1) // re-heat the simulation
×
119
                .nodes(nodeList)
×
120
                .stop();
×
121

122
            // copy over new nodes
123
            for (const entry of this.newNodeMap.entries()) {
×
124
                const n = entry[0];
×
125
                const d3node = entry[1];
×
126
                if (!isD3Node(d3node)) {
×
127
                    throw new Error("Internal error: Node is not settled as a complete D3 Node");
×
128
                }
×
129

130
                this.nodeMapping.set(n, d3node);
×
131
            }
×
132
            this.newNodeMap.clear();
×
133

134
            // update edges
135
            let linkList: (D3Edge | D3InputEdge)[] = [... this.edgeMapping.values()];
×
136
            linkList = linkList.concat([... this.newEdgeMap.values()]);
×
137
            this.d3ForceLayout
×
138
                .force("link")
×
139
                .links(linkList);
×
140

141
            // copy over new edges
142
            for (const entry of this.newEdgeMap.entries()) {
×
143
                const e = entry[0];
×
144
                const d3edge = entry[1];
×
145
                if (!isD3Edge(d3edge)) {
×
146
                    throw new Error("Internal error: Edge is not settled as a complete D3 Edge");
×
147
                }
×
148

149
                this.edgeMapping.set(e, d3edge);
×
150
            }
×
151
            this.newEdgeMap.clear();
×
152
        }
×
153
    }
×
154

155
    step(): void {
1✔
156
        this.refresh();
×
157
        this.d3ForceLayout.tick();
×
158
    }
×
159

160
    get isSettled(): boolean {
1✔
161
        // console.log(`this.d3ForceLayout.alpha() ${this.d3ForceLayout.alpha()}`);
162
        return this.d3ForceLayout.alpha() < this.d3AlphaMin;
×
163
    }
×
164

165
    addNode(n: Node): void {
1✔
166
        this.newNodeMap.set(n, {id: n.id});
77✔
167
    }
77✔
168

169
    addEdge(e: Edge): void {
1✔
170
        this.newEdgeMap.set(e, {
254✔
171
            source: e.srcId,
254✔
172
            target: e.dstId,
254✔
173
        });
254✔
174
    }
254✔
175

176
    get nodes(): Iterable<Node> {
1✔
177
        return this.nodeMapping.keys();
×
178
    }
×
179

180
    get edges(): Iterable<Edge> {
1✔
181
        return this.edgeMapping.keys();
×
182
    }
×
183

184
    getNodePosition(n: Node): Position {
1✔
185
        const d3node = this._getMappedNode(n);
×
186
        // if (d3node.x === undefined || d3node.y === undefined || d3node.z === undefined) {
187
        //     throw new Error("Internal error: Node not initialized in D3GraphEngine");
188
        // }
189

190
        return {
×
191
            x: d3node.x,
×
192
            y: d3node.y,
×
193
            z: d3node.z,
×
194
        };
×
195
    }
×
196

197
    setNodePosition(n: Node, newPos: Position): void {
1✔
198
        const d3node = this._getMappedNode(n);
×
199
        d3node.x = newPos.x;
×
200
        d3node.y = newPos.y;
×
201
        d3node.z = newPos.z ?? 0;
×
202
        this.reheat = true;
×
203
    }
×
204

205
    getEdgePosition(e: Edge): EdgePosition {
1✔
206
        const d3edge = this._getMappedEdge(e);
×
207

208
        return {
×
209
            src: {
×
210
                x: d3edge.source.x,
×
211
                y: d3edge.source.y,
×
212
                z: d3edge.source.z,
×
213
            },
×
214
            dst: {
×
215
                x: d3edge.target.x,
×
216
                y: d3edge.target.y,
×
217
                z: d3edge.target.z,
×
218
            },
×
219
        };
×
220
    }
×
221

222
    pin(n: Node): void {
1✔
223
        const d3node = this._getMappedNode(n);
×
224

225
        d3node.fx = d3node.x;
×
226
        d3node.fy = d3node.y;
×
227
        d3node.fz = d3node.z;
×
228
        this.reheat = true; // TODO: is this necessary?
×
229
    }
×
230

231
    unpin(n: Node): void {
1✔
232
        const d3node = this._getMappedNode(n);
×
233

234
        d3node.fx = undefined;
×
235
        d3node.fy = undefined;
×
236
        d3node.fz = undefined;
×
237
        this.reheat = true; // TODO: is this necessary?
×
238
    }
×
239

240
    private _getMappedNode(n: Node): D3Node {
1✔
241
        this.refresh(); // ensure consistent state
×
242

243
        const d3node = this.nodeMapping.get(n);
×
244
        if (!d3node) {
×
245
            throw new Error("Internal error: Node not found in D3GraphEngine");
×
246
        }
×
247

248
        return d3node;
×
249
    }
×
250

251
    private _getMappedEdge(e: Edge): D3Edge {
1✔
252
        this.refresh(); // ensure consistent state
×
253

254
        const d3edge = this.edgeMapping.get(e);
×
255
        if (!d3edge) {
×
256
            throw new Error("Internal error: Edge not found in D3GraphEngine");
×
257
        }
×
258

259
        return d3edge;
×
260
    }
×
261
}
1✔
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

© 2025 Coveralls, Inc