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

graphty-org / graphty-element / 20402143816

21 Dec 2025 12:26AM UTC coverage: 82.488% (+0.07%) from 82.423%
20402143816

push

github

apowers313
ci: fix vitest in ci

5163 of 6089 branches covered (84.79%)

Branch coverage included in aggregate %.

24804 of 30240 relevant lines covered (82.02%)

6475.79 hits per line

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

98.0
/src/algorithms/Algorithm.ts
1
import {set as deepSet} from "lodash";
3✔
2

3
import {AdHocData, type OptionsSchema as ZodOptionsSchema, SuggestedStylesConfig, SuggestedStylesProvider} from "../config";
4
import {Edge} from "../Edge";
5
import {Graph} from "../Graph";
6
import {type OptionsFromSchema, type OptionsSchema, resolveOptions} from "./types/OptionSchema";
3✔
7

8
/**
9
 * Type for algorithm class constructor
10
 * Uses any for options to allow flexibility with different algorithm option types
11
 */
12
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13
type AlgorithmClass = new (g: Graph, options?: any) => Algorithm;
14

15
/**
16
 * Interface for Algorithm class static members
17
 * Exported for use in type annotations when referencing algorithm classes
18
 */
19
export interface AlgorithmStatics {
20
    type: string;
21
    namespace: string;
22
    optionsSchema: OptionsSchema;
23
    suggestedStyles?: SuggestedStylesProvider;
24
    /** @deprecated Use getZodOptionsSchema() instead */
25
    getOptionsSchema(): OptionsSchema;
26
    /** @deprecated Use hasZodOptions() instead */
27
    hasOptions(): boolean;
28
    hasSuggestedStyles(): boolean;
29
    getSuggestedStyles(): SuggestedStylesConfig | null;
30
    /** NEW: Zod-based options schema for unified validation and UI metadata */
31
    zodOptionsSchema?: ZodOptionsSchema;
32
    /** Get the Zod-based options schema for this algorithm */
33
    getZodOptionsSchema(): ZodOptionsSchema;
34
    /** Check if this algorithm has a Zod-based options schema */
35
    hasZodOptions(): boolean;
36
}
37

38
const algorithmRegistry = new Map<string, AlgorithmClass>();
3✔
39

40
// algorithmResults layout:
41
// {
42
//     node: {
43
//         id: {
44
//             namespace: {
45
//                 algorithm: {
46
//                     result: unknown
47
//                 }
48
//             }
49
//         }
50
//     },
51
//     edge: {
52
//         id: {
53
//             namespace: {
54
//                 algorithm: {
55
//                     result: unknown
56
//                 }
57
//             }
58
//         }
59
//     },
60
//     graph: {
61
//         namespace: {
62
//             algorithm: {
63
//                 result: unknown
64
//             }
65
//         }
66
//     }
67
// }
68

69
/**
70
 * Base class for all graph algorithms
71
 *
72
 * @typeParam TOptions - The options type for this algorithm (defaults to empty object)
73
 *
74
 * @example
75
 * ```typescript
76
 * // Algorithm with options
77
 * interface PageRankOptions {
78
 *     dampingFactor: number;
79
 *     maxIterations: number;
80
 * }
81
 *
82
 * class PageRankAlgorithm extends Algorithm<PageRankOptions> {
83
 *     static optionsSchema: OptionsSchema = {
84
 *         dampingFactor: { type: 'number', default: 0.85, ... },
85
 *         maxIterations: { type: 'integer', default: 100, ... }
86
 *     };
87
 *
88
 *     async run(): Promise<void> {
89
 *         const { dampingFactor, maxIterations } = this.options;
90
 *         // ... use options
91
 *     }
92
 * }
93
 * ```
94
 */
95
export abstract class Algorithm<TOptions extends Record<string, unknown> = Record<string, unknown>> {
3✔
96
    static type: string;
97
    static namespace: string;
98
    static suggestedStyles?: SuggestedStylesProvider;
99

100
    /**
101
     * Options schema for this algorithm
102
     *
103
     * Subclasses should override this to define their configurable options.
104
     * An empty schema means the algorithm has no configurable options.
105
     *
106
     * @deprecated Use zodOptionsSchema instead for new implementations
107
     */
108
    static optionsSchema: OptionsSchema = {};
38✔
109

110
    /**
111
     * NEW: Zod-based options schema with rich metadata for UI generation.
112
     *
113
     * Override in subclasses to define algorithm-specific options.
114
     * This is the new unified system that provides both validation and UI metadata.
115
     */
116
    static zodOptionsSchema?: ZodOptionsSchema;
117

118
    protected graph: Graph;
119

120
    /**
121
     * Resolved options for this algorithm instance
122
     *
123
     * Options are resolved at construction time by:
124
     * 1. Starting with schema defaults
125
     * 2. Overriding with any provided options
126
     * 3. Validating all values against the schema
127
     *
128
     * Note: Named with underscore prefix to avoid conflicts with
129
     * existing algorithm implementations that have their own
130
     * options properties (will be removed in future refactoring).
131
     */
132
    protected _schemaOptions: TOptions;
133

134
    /**
135
     * Getter for schema options
136
     *
137
     * Algorithms that use the new schema-based options should access
138
     * options via this getter.
139
     */
140
    protected get schemaOptions(): TOptions {
38✔
141
        return this._schemaOptions;
39✔
142
    }
39✔
143

144
    /**
145
     * Creates a new algorithm instance
146
     *
147
     * @param g - The graph to run the algorithm on
148
     * @param options - Optional configuration options (uses schema defaults if not provided)
149
     */
150
    constructor(g: Graph, options?: Partial<TOptions>) {
38✔
151
        this.graph = g;
238✔
152
        this._schemaOptions = this.resolveOptions(options);
238✔
153
    }
238✔
154

155
    /**
156
     * Resolves and validates options against the schema
157
     *
158
     * @param options - User-provided options (partial)
159
     * @returns Fully resolved options with defaults applied
160
     */
161
    protected resolveOptions(options?: Partial<TOptions>): TOptions {
38✔
162
        // eslint-disable-next-line @typescript-eslint/no-deprecated -- Supporting backward compatibility
163
        const schema = (this.constructor as typeof Algorithm).optionsSchema;
238✔
164

165
        // If no schema defined, return empty object (backward compatible)
166
        if (Object.keys(schema).length === 0) {
238✔
167
            return {} as TOptions;
69✔
168
        }
69✔
169

170
        return resolveOptions(schema, options as Partial<OptionsFromSchema<typeof schema>>) as TOptions;
169✔
171
    }
238✔
172

173
    get type(): string {
38✔
174
        return (this.constructor as typeof Algorithm).type;
14,807✔
175
    }
14,807✔
176

177
    get namespace(): string {
38✔
178
        return (this.constructor as typeof Algorithm).namespace;
14,807✔
179
    }
14,807✔
180

181
    get results(): AdHocData {
38✔
182
        const algorithmResults = {} as AdHocData;
13✔
183

184
        // Node results
185
        for (const n of this.graph.getDataManager().nodes.values()) {
13✔
186
            deepSet(algorithmResults, `node.${n.id}`, n.algorithmResults);
554✔
187
        }
554✔
188

189
        // Edge results
190
        for (const e of this.graph.getDataManager().edges.values()) {
13✔
191
            const edgeKey = `${e.srcId}:${e.dstId}`;
1,790✔
192
            deepSet(algorithmResults, `edge.${edgeKey}`, e.algorithmResults);
1,790✔
193
        }
1,790✔
194

195
        // Graph results
196
        const dm = this.graph.getDataManager();
13✔
197
        if (dm.graphResults) {
13✔
198
            algorithmResults.graph = dm.graphResults;
11✔
199
        }
11✔
200

201
        return algorithmResults;
13✔
202
    }
13✔
203

204
    abstract run(g: Graph): Promise<void>;
205

206
    #createPath(resultName: string): string[] {
38✔
207
        const ret: string[] = [];
14,520✔
208

209
        ret.push("algorithmResults");
14,520✔
210
        ret.push(this.namespace);
14,520✔
211
        ret.push(this.type);
14,520✔
212
        ret.push(resultName);
14,520✔
213

214
        return ret;
14,520✔
215
    }
14,520✔
216

217
    addNodeResult(nodeId: number | string, resultName: string, result: unknown): void {
38✔
218
        const p = this.#createPath(resultName);
11,253✔
219
        const n = this.graph.getDataManager().nodes.get(nodeId);
11,253✔
220
        if (!n) {
11,253!
221
            throw new Error(`couldn't find nodeId '${nodeId}' while trying to run algorithm '${this.type}'`);
×
222
        }
×
223

224
        deepSet(n, p, result);
11,253✔
225
        // XXX: THIS IS WHERE I LEFT OFF
226
        // replace algorithmResults with graph.nodes; set result on each node.algorithmResult
227
    }
11,253✔
228

229
    addEdgeResult(edge: Edge, resultName: string, result: unknown): void {
38✔
230
        const p = this.#createPath(resultName);
3,267✔
231
        deepSet(edge, p, result);
3,267✔
232
    }
3,267✔
233

234
    addGraphResult(resultName: string, result: unknown): void {
38✔
235
        const dm = this.graph.getDataManager();
286✔
236
        dm.graphResults ??= {} as AdHocData;
286✔
237

238
        const path = [this.namespace, this.type, resultName];
286✔
239
        deepSet(dm.graphResults, path, result);
286✔
240
    }
286✔
241

242
    static register<T extends AlgorithmClass>(cls: T): T {
38✔
243
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
244
        const t: string = (cls as any).type;
4,889✔
245
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
246
        const ns: string = (cls as any).namespace;
4,889✔
247
        algorithmRegistry.set(`${ns}:${t}`, cls);
4,889✔
248
        return cls;
4,889✔
249
    }
4,889✔
250

251
    static get(g: Graph, namespace: string, type: string): Algorithm | null {
38✔
252
        const SourceClass = algorithmRegistry.get(`${namespace}:${type}`);
71✔
253
        if (SourceClass) {
71✔
254
            return new SourceClass(g);
69✔
255
        }
69✔
256

257
        return null;
2✔
258
    }
71✔
259

260
    static getClass(namespace: string, type: string): (AlgorithmClass & AlgorithmStatics) | null {
38✔
261
        return algorithmRegistry.get(`${namespace}:${type}`) as (AlgorithmClass & AlgorithmStatics) | null ?? null;
91✔
262
    }
91✔
263

264
    /**
265
     * Check if this algorithm has suggested styles
266
     */
267
    static hasSuggestedStyles(): boolean {
38✔
268
        return !!this.suggestedStyles;
312✔
269
    }
312✔
270

271
    /**
272
     * Get suggested styles for this algorithm
273
     */
274
    static getSuggestedStyles(): SuggestedStylesConfig | null {
38✔
275
        return this.suggestedStyles ? this.suggestedStyles() : null;
220✔
276
    }
220✔
277

278
    /**
279
     * Get the options schema for this algorithm
280
     *
281
     * @returns The options schema, or an empty object if no options defined
282
     * @deprecated Use getZodOptionsSchema() instead
283
     */
284
    static getOptionsSchema(): OptionsSchema {
38✔
285
        // eslint-disable-next-line @typescript-eslint/no-deprecated -- Implementation of deprecated method
286
        return this.optionsSchema;
50✔
287
    }
50✔
288

289
    /**
290
     * Check if this algorithm has configurable options
291
     *
292
     * @returns true if the algorithm has at least one option defined
293
     * @deprecated Use hasZodOptions() instead
294
     */
295
    static hasOptions(): boolean {
38✔
296
        // eslint-disable-next-line @typescript-eslint/no-deprecated -- Implementation of deprecated method
297
        return Object.keys(this.optionsSchema).length > 0;
25✔
298
    }
25✔
299

300
    /**
301
     * Get the Zod-based options schema for this algorithm.
302
     *
303
     * @returns The Zod options schema, or an empty object if no schema defined
304
     */
305
    static getZodOptionsSchema(): ZodOptionsSchema {
38✔
306
        return this.zodOptionsSchema ?? {};
233✔
307
    }
233✔
308

309
    /**
310
     * Check if this algorithm has a Zod-based options schema.
311
     *
312
     * @returns true if the algorithm has a Zod options schema defined
313
     */
314
    static hasZodOptions(): boolean {
38✔
315
        return this.zodOptionsSchema !== undefined &&
232✔
316
               Object.keys(this.zodOptionsSchema).length > 0;
131✔
317
    }
232✔
318

319
    /**
320
     * Get all registered algorithm names.
321
     * @param namespace - Optional namespace to filter by
322
     * @returns Array of algorithm names in "namespace:type" format
323
     */
324
    static getRegisteredAlgorithms(namespace?: string): string[] {
38✔
325
        const algorithms: string[] = [];
5✔
326
        for (const key of algorithmRegistry.keys()) {
5✔
327
            if (!namespace || key.startsWith(`${namespace}:`)) {
115✔
328
                algorithms.push(key);
92✔
329
            }
92✔
330
        }
115✔
331

332
        return algorithms.sort();
5✔
333
    }
5✔
334
}
38✔
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