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

graphty-org / graphty-element / 15948993626

28 Jun 2025 09:54PM UTC coverage: 73.423% (-4.5%) from 77.956%
15948993626

push

github

apowers313
build: add package linting and remove unused packages

254 of 356 branches covered (71.35%)

Branch coverage included in aggregate %.

1992 of 2703 relevant lines covered (73.7%)

462.07 hits per line

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

34.17
/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
    d3ForceLayout: ReturnType<typeof forceSimulation>;
71
    d3AlphaMin: number;
72
    d3AlphaTarget: number;
73
    d3AlphaDecay: number;
74
    d3VelocityDecay: number;
75
    nodeMapping = new Map<Node, D3Node>();
1✔
76
    edgeMapping = new Map<Edge, D3Edge>();
1✔
77
    newNodeMap = new Map<Node, D3InputNode>();
1✔
78
    newEdgeMap = new Map<Edge, D3InputEdge>();
1✔
79
    reheat = false;
1✔
80

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

247
        return d3node;
×
248
    }
×
249

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

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

258
        return d3edge;
×
259
    }
×
260
}
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