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

graphty-org / graphty-element / 16289845933

15 Jul 2025 09:43AM UTC coverage: 69.908% (-0.3%) from 70.161%
16289845933

push

github

apowers313
fix: fix semantic release, refactor edge, node, and richtextlabel to use their config objects

736 of 935 branches covered (78.72%)

Branch coverage included in aggregate %.

2 of 126 new or added lines in 3 files covered. (1.59%)

5 existing lines in 3 files now uncovered.

4742 of 6901 relevant lines covered (68.71%)

293.48 hits per line

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

28.37
/src/Node.ts
1
import {
2
    AbstractMesh,
3
    SixDofDragBehavior,
4
} from "@babylonjs/core";
5
import jmespath from "jmespath";
2!
6
import _ from "lodash";
2!
7

8
import {CalculatedValue} from "./CalculatedValue";
9
import {ChangeManager} from "./ChangeManager";
2✔
10
import {AdHocData, NodeStyle, NodeStyleConfig} from "./config";
2✔
11
import type {Graph} from "./Graph";
12
import type {GraphContext} from "./managers/GraphContext";
13
import {NodeMesh} from "./meshes/NodeMesh";
2✔
14
import {RichTextLabel, type RichTextLabelOptions} from "./meshes/RichTextLabel";
2✔
15
import {NodeBehavior} from "./NodeBehavior";
2✔
16
import {NodeStyleId, Styles} from "./Styles";
2✔
17

18
export type NodeIdType = string | number;
19

20
interface NodeOpts {
21
    pinOnDrag?: boolean;
22
}
23

24
export class Node {
2✔
25
    parentGraph: Graph | GraphContext;
26
    opts: NodeOpts;
27
    id: NodeIdType;
28
    data: AdHocData<string | number>;
29
    algorithmResults: AdHocData;
30
    styleUpdates: AdHocData;
31
    mesh: AbstractMesh;
32
    label?: RichTextLabel;
33
    meshDragBehavior!: SixDofDragBehavior;
34
    dragging = false;
2✔
35
    styleId: NodeStyleId;
36
    pinOnDrag!: boolean;
37
    size!: number;
38
    changeManager: ChangeManager;
39

40
    /**
41
     * Helper to check if we're using GraphContext
42
     */
43
    private get context(): GraphContext {
2✔
44
        // Check if parentGraph has GraphContext methods
45
        if ("getStyles" in this.parentGraph) {
2✔
46
            return this.parentGraph;
2✔
47
        }
2!
48

49
        // Otherwise, it's a Graph instance which implements GraphContext
50
        return this.parentGraph;
×
51
    }
2✔
52

53
    constructor(graph: Graph | GraphContext, nodeId: NodeIdType, styleId: NodeStyleId, data: AdHocData<string | number>, opts: NodeOpts = {}) {
2✔
54
        this.parentGraph = graph;
1,222✔
55
        this.id = nodeId;
1,222✔
56
        this.opts = opts;
1,222✔
57
        this.changeManager = new ChangeManager();
1,222✔
58
        this.changeManager.loadCalculatedValues(this.context.getStyleManager().getStyles().getCalculatedStylesForNode(data));
1,222✔
59
        this.data = this.changeManager.watch("data", data);
1,222✔
60
        this.algorithmResults = this.changeManager.watch("algorithmResults", {} as unknown as AdHocData);
1,222✔
61
        this.styleUpdates = this.changeManager.addData("style", {} as unknown as AdHocData, NodeStyle);
1,222✔
62

63
        // copy nodeMeshOpts
64
        this.styleId = styleId;
1,222✔
65

66
        // create graph node
67
        // Note: Node is added to layout engine by DataManager, not here
68

69
        // create mesh
70
        const o = Styles.getStyleForNodeStyleId(styleId);
1,222✔
71
        this.size = o.shape?.size ?? 0;
1,222!
72

73
        this.mesh = NodeMesh.create(
1,222✔
74
            this.context.getMeshCache(),
1,222✔
75
            {styleId: String(styleId), is2D: this.context.is2D(), size: this.size},
1,222✔
76
            {shape: o.shape, texture: o.texture, effect: o.effect},
1,222✔
77
            this.context.getScene(),
1,222✔
78
        );
1,222✔
79

80
        // create label
81
        if (o.label?.enabled) {
1,222!
82
            this.label = this.createLabel(o);
×
83
        }
×
84

85
        NodeBehavior.addDefaultBehaviors(this, this.opts);
1,222✔
86
    }
1,222✔
87

88
    addCalculatedStyle(cv: CalculatedValue): void {
2✔
89
        this.changeManager.addCalculatedValue(cv);
×
90
    }
×
91

92
    update(): void {
2✔
93
        const newStyleKeys = Object.keys(this.styleUpdates);
2,583✔
94
        if (newStyleKeys.length > 0) {
2,583!
95
            let style = Styles.getStyleForNodeStyleId(this.styleId);
×
96
            style = _.defaultsDeep(this.styleUpdates, style);
×
97
            const styleId = Styles.getNodeIdForStyle(style);
×
98
            this.updateStyle(styleId);
×
99
            for (const key of newStyleKeys) {
×
100
                // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
101
                delete this.styleUpdates[key];
×
102
            }
×
103
        }
×
104

105
        if (this.dragging) {
2,583!
106
            return;
×
107
        }
×
108

109
        const pos = this.context.getLayoutManager().layoutEngine?.getNodePosition(this);
2,583✔
110
        if (pos) {
2,583✔
111
            this.mesh.position.x = pos.x;
2,583✔
112
            this.mesh.position.y = pos.y;
2,583✔
113
            this.mesh.position.z = pos.z ?? 0;
2,583!
114
        }
2,583✔
115
    }
2,583✔
116

117
    updateStyle(styleId: NodeStyleId): void {
2✔
118
        if (styleId === this.styleId) {
×
119
            return;
×
120
        }
×
121

122
        this.styleId = styleId;
×
123
        this.mesh.dispose();
×
124

125
        const o = Styles.getStyleForNodeStyleId(styleId);
×
126
        this.size = o.shape?.size ?? 0;
×
127

128
        this.mesh = NodeMesh.create(
×
129
            this.context.getMeshCache(),
×
130
            {styleId: String(styleId), is2D: this.context.is2D(), size: this.size},
×
131
            {shape: o.shape, texture: o.texture, effect: o.effect},
×
132
            this.context.getScene(),
×
133
        );
×
134

135
        // recreate label if needed
136
        if (o.label?.enabled) {
×
137
            this.label?.dispose();
×
138
            this.label = this.createLabel(o);
×
139
        } else if (this.label) {
×
140
            this.label.dispose();
×
141
            this.label = undefined;
×
142
        }
×
143

144
        NodeBehavior.addDefaultBehaviors(this, this.opts);
×
145
    }
×
146

147
    pin(): void {
2✔
148
        this.context.getLayoutManager().layoutEngine?.pin(this);
×
149
    }
×
150

151
    unpin(): void {
2✔
152
        this.context.getLayoutManager().layoutEngine?.unpin(this);
×
153
    }
×
154

155
    private createLabel(styleConfig: NodeStyleConfig): RichTextLabel {
2✔
156
        const labelText = this.extractLabelText(styleConfig.label);
×
157
        const labelOptions = this.createLabelOptions(labelText, styleConfig);
×
158
        return new RichTextLabel(this.mesh.getScene(), labelOptions);
×
159
    }
×
160

161
    private extractLabelText(labelConfig?: Record<string, unknown>): string {
2✔
162
        if (!labelConfig) {
×
163
            return this.id.toString();
×
164
        }
×
165

166
        // Check if text is directly provided
167
        if (labelConfig.text !== undefined && labelConfig.text !== null) {
×
168
            // Only convert to string if it's a primitive type
169
            if (typeof labelConfig.text === "string" || typeof labelConfig.text === "number" || typeof labelConfig.text === "boolean") {
×
170
                return String(labelConfig.text);
×
171
            }
×
172
        } else if (labelConfig.textPath && typeof labelConfig.textPath === "string") {
×
173
            try {
×
174
                const result = jmespath.search(this.data, labelConfig.textPath);
×
175
                if (result !== null && result !== undefined) {
×
176
                    return String(result);
×
177
                }
×
178
            } catch {
×
179
                // Ignore jmespath errors
180
            }
×
181
        }
×
182

183
        return this.id.toString();
×
184
    }
×
185

186
    private createLabelOptions(labelText: string, styleConfig: NodeStyleConfig): RichTextLabelOptions {
2✔
187
        const labelStyle = styleConfig.label ?? {};
×
188

189
        // Get attach position and offset
190
        const attachPosition = this.getAttachPosition(labelStyle.location ?? "top");
×
191
        const attachOffset = labelStyle.attachOffset ?? this.getDefaultAttachOffset(labelStyle.location ?? "top");
×
192

193
        // Transform backgroundColor to string if it's an advanced color style
NEW
194
        let backgroundColor: string | undefined = undefined;
×
NEW
195
        if (labelStyle.backgroundColor) {
×
NEW
196
            if (typeof labelStyle.backgroundColor === "string") {
×
NEW
197
                ({backgroundColor} = labelStyle);
×
NEW
198
            } else if (labelStyle.backgroundColor.colorType === "solid") {
×
NEW
199
                ({value: backgroundColor} = labelStyle.backgroundColor);
×
NEW
200
            } else if (labelStyle.backgroundColor.colorType === "gradient") {
×
201
                // For gradients, use the first color as a fallback
NEW
202
                [backgroundColor] = labelStyle.backgroundColor.colors;
×
NEW
203
            }
×
NEW
204
        }
×
205

206
        // Filter out undefined values from backgroundGradientColors
NEW
207
        let backgroundGradientColors: string[] | undefined = undefined;
×
NEW
208
        if (labelStyle.backgroundGradientColors) {
×
NEW
209
            backgroundGradientColors = labelStyle.backgroundGradientColors.filter((color): color is string => color !== undefined);
×
NEW
210
            if (backgroundGradientColors.length === 0) {
×
NEW
211
                backgroundGradientColors = undefined;
×
NEW
212
            }
×
NEW
213
        }
×
214

215
        // Transform borders to ensure colors are strings
NEW
216
        let borders: {width: number, color: string, spacing: number}[] | undefined = undefined;
×
NEW
217
        if (labelStyle.borders && labelStyle.borders.length > 0) {
×
NEW
218
            const validBorders = labelStyle.borders
×
NEW
219
                .filter((border): border is typeof border & {color: string} => border.color !== undefined)
×
NEW
220
                .map((border) => ({
×
NEW
221
                    width: border.width,
×
NEW
222
                    color: border.color,
×
NEW
223
                    spacing: border.spacing,
×
NEW
224
                }));
×
225
            // Only set borders if we have valid borders, otherwise leave it undefined
226
            // so the default empty array is used
NEW
227
            if (validBorders.length > 0) {
×
NEW
228
                borders = validBorders;
×
NEW
229
            }
×
NEW
230
        }
×
231

232
        // Create label options by spreading the entire labelStyle object
233
        const labelOptions: RichTextLabelOptions = {
×
NEW
234
            ... labelStyle,
×
235
            // Override with computed values
236
            text: labelText,
×
237
            attachTo: this.mesh,
×
238
            attachPosition,
×
239
            attachOffset,
×
NEW
240
            backgroundColor,
×
NEW
241
            backgroundGradientColors,
×
NEW
242
            ... (borders !== undefined && {borders}),
×
UNCOV
243
        };
×
244

245
        // Handle special case for transparent background
NEW
246
        if (labelOptions.backgroundColor === "transparent") {
×
247
            labelOptions.backgroundColor = undefined;
×
248
        }
×
249

250
        // Remove properties that shouldn't be passed to RichTextLabel
251
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
NEW
252
        const {location, textPath, enabled, ... finalLabelOptions} = labelOptions as RichTextLabelOptions & {location?: string, textPath?: string, enabled?: boolean};
×
253

NEW
254
        return finalLabelOptions;
×
UNCOV
255
    }
×
256

257
    private getAttachPosition(location: string): "top" | "top-left" | "top-right" | "left" | "center" | "right" | "bottom" | "bottom-left" | "bottom-right" {
2✔
258
        switch (location) {
×
259
            case "floating":
×
260
            case "automatic":
×
261
                return "top";
×
262
            case "top":
×
263
            case "top-left":
×
264
            case "top-right":
×
265
            case "left":
×
266
            case "center":
×
267
            case "right":
×
268
            case "bottom":
×
269
            case "bottom-left":
×
270
            case "bottom-right":
×
271
                return location;
×
272
            default:
×
273
                return "top";
×
274
        }
×
275
    }
×
276

277
    private getDefaultAttachOffset(location: string): number {
2✔
278
        // Return larger offsets for left/right positions to prevent overlap
279
        switch (location) {
×
280
            case "left":
×
281
            case "right":
×
282
                return 1.0; // Larger offset for horizontal positions
×
283
            case "center":
×
284
                return 0; // No offset for center
×
285
            default:
×
286
                return 0.5; // Standard offset for top/bottom positions
×
287
        }
×
288
    }
×
289
}
2✔
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