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

graphty-org / graphty-monorepo / 20661584252

02 Jan 2026 03:50PM UTC coverage: 77.924% (+7.3%) from 70.62%
20661584252

push

github

apowers313
ci: fix flakey performance test

13438 of 17822 branches covered (75.4%)

Branch coverage included in aggregate %.

41247 of 52355 relevant lines covered (78.78%)

145534.85 hits per line

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

92.67
/graphty-element/src/algorithms/Algorithm.ts
1
import { set as deepSet } from "lodash";
15✔
2

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

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

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

43
const algorithmRegistry = new Map<string, AlgorithmClass>();
15✔
44

45
// algorithmResults layout:
46
// {
47
//     node: {
48
//         id: {
49
//             namespace: {
50
//                 algorithm: {
51
//                     result: unknown
52
//                 }
53
//             }
54
//         }
55
//     },
56
//     edge: {
57
//         id: {
58
//             namespace: {
59
//                 algorithm: {
60
//                     result: unknown
61
//                 }
62
//             }
63
//         }
64
//     },
65
//     graph: {
66
//         namespace: {
67
//             algorithm: {
68
//                 result: unknown
69
//             }
70
//         }
71
//     }
72
// }
73

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

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

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

120
    protected graph: Graph;
121

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

136
    /**
137
     * Getter for schema options
138
     *
139
     * Algorithms that use the new schema-based options should access
140
     * options via this getter.
141
     * @returns The resolved schema options
142
     */
143
    protected get schemaOptions(): TOptions {
50✔
144
        return this._schemaOptions;
39✔
145
    }
39✔
146

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

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

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

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

174
    /**
175
     * Gets the algorithm type
176
     * @returns The algorithm type identifier
177
     */
178
    get type(): string {
50✔
179
        return (this.constructor as typeof Algorithm).type;
14,807✔
180
    }
14,807✔
181

182
    /**
183
     * Gets the algorithm namespace
184
     * @returns The algorithm namespace identifier
185
     */
186
    get namespace(): string {
50✔
187
        return (this.constructor as typeof Algorithm).namespace;
14,807✔
188
    }
14,807✔
189

190
    /**
191
     * Gets all algorithm results for nodes, edges, and graph
192
     * @returns An object containing node, edge, and graph results
193
     */
194
    get results(): AdHocData {
50✔
195
        const algorithmResults = {} as AdHocData;
13✔
196

197
        // Node results
198
        for (const n of this.graph.getDataManager().nodes.values()) {
13✔
199
            deepSet(algorithmResults, `node.${n.id}`, n.algorithmResults);
554✔
200
        }
554✔
201

202
        // Edge results
203
        for (const e of this.graph.getDataManager().edges.values()) {
13✔
204
            const edgeKey = `${e.srcId}:${e.dstId}`;
1,790✔
205
            deepSet(algorithmResults, `edge.${edgeKey}`, e.algorithmResults);
1,790✔
206
        }
1,790✔
207

208
        // Graph results
209
        const dm = this.graph.getDataManager();
13✔
210
        if (dm.graphResults) {
13✔
211
            algorithmResults.graph = dm.graphResults;
11✔
212
        }
11✔
213

214
        return algorithmResults;
13✔
215
    }
13✔
216

217
    abstract run(g: Graph): Promise<void>;
218

219
    #createPath(resultName: string): string[] {
50✔
220
        const ret: string[] = [];
14,520✔
221

222
        ret.push("algorithmResults");
14,520✔
223
        ret.push(this.namespace);
14,520✔
224
        ret.push(this.type);
14,520✔
225
        ret.push(resultName);
14,520✔
226

227
        return ret;
14,520✔
228
    }
14,520✔
229

230
    /**
231
     * Adds a result value for a specific node
232
     * @param nodeId - The ID of the node to add the result to
233
     * @param resultName - The name of the result field
234
     * @param result - The result value to store
235
     */
236
    addNodeResult(nodeId: number | string, resultName: string, result: unknown): void {
50✔
237
        const p = this.#createPath(resultName);
11,253✔
238
        const n = this.graph.getDataManager().nodes.get(nodeId);
11,253✔
239
        if (!n) {
11,253!
240
            throw new Error(`couldn't find nodeId '${nodeId}' while trying to run algorithm '${this.type}'`);
×
241
        }
×
242

243
        deepSet(n, p, result);
11,253✔
244
        // XXX: THIS IS WHERE I LEFT OFF
245
        // replace algorithmResults with graph.nodes; set result on each node.algorithmResult
246
    }
11,253✔
247

248
    /**
249
     * Adds a result value for a specific edge
250
     * @param edge - The edge to add the result to
251
     * @param resultName - The name of the result field
252
     * @param result - The result value to store
253
     */
254
    addEdgeResult(edge: Edge, resultName: string, result: unknown): void {
50✔
255
        const p = this.#createPath(resultName);
3,267✔
256
        deepSet(edge, p, result);
3,267✔
257
    }
3,267✔
258

259
    /**
260
     * Adds a result value for the graph
261
     * @param resultName - The name of the result field
262
     * @param result - The result value to store
263
     */
264
    addGraphResult(resultName: string, result: unknown): void {
50✔
265
        const dm = this.graph.getDataManager();
286✔
266
        dm.graphResults ??= {} as AdHocData;
286✔
267

268
        const path = [this.namespace, this.type, resultName];
286✔
269
        deepSet(dm.graphResults, path, result);
286✔
270
    }
286✔
271

272
    /**
273
     * Registers an algorithm class in the global registry
274
     * @param cls - The algorithm class to register
275
     * @returns The registered algorithm class
276
     */
277
    static register<T extends AlgorithmClass>(cls: T): T {
50✔
278
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
279
        const t: string = (cls as any).type;
5,441✔
280
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
281
        const ns: string = (cls as any).namespace;
5,441✔
282
        algorithmRegistry.set(`${ns}:${t}`, cls);
5,441✔
283
        return cls;
5,441✔
284
    }
5,441✔
285

286
    /**
287
     * Gets an algorithm instance from the registry
288
     * @param g - The graph to run the algorithm on
289
     * @param namespace - The algorithm namespace
290
     * @param type - The algorithm type
291
     * @returns A new instance of the algorithm, or null if not found
292
     */
293
    static get(g: Graph, namespace: string, type: string): Algorithm | null {
50✔
294
        const SourceClass = algorithmRegistry.get(`${namespace}:${type}`);
71✔
295
        if (SourceClass) {
71✔
296
            return new SourceClass(g);
69✔
297
        }
69!
298

299
        return null;
2✔
300
    }
71✔
301

302
    /**
303
     * Gets an algorithm class from the registry
304
     * @param namespace - The algorithm namespace
305
     * @param type - The algorithm type
306
     * @returns The algorithm class, or null if not found
307
     */
308
    static getClass(namespace: string, type: string): (AlgorithmClass & AlgorithmStatics) | null {
50✔
309
        return (algorithmRegistry.get(`${namespace}:${type}`) as (AlgorithmClass & AlgorithmStatics) | null) ?? null;
91!
310
    }
91✔
311

312
    /**
313
     * Check if this algorithm has suggested styles
314
     * @returns true if suggested styles are defined
315
     */
316
    static hasSuggestedStyles(): boolean {
50✔
317
        return !!this.suggestedStyles;
312✔
318
    }
312✔
319

320
    /**
321
     * Get suggested styles for this algorithm
322
     * @returns The suggested styles configuration, or null if none defined
323
     */
324
    static getSuggestedStyles(): SuggestedStylesConfig | null {
50✔
325
        return this.suggestedStyles ? this.suggestedStyles() : null;
220!
326
    }
220✔
327

328
    /**
329
     * Get the options schema for this algorithm
330
     * @returns The options schema, or an empty object if no options defined
331
     * @deprecated Use getZodOptionsSchema() instead
332
     */
333
    static getOptionsSchema(): OptionsSchema {
50✔
334
        // eslint-disable-next-line @typescript-eslint/no-deprecated -- Implementation of deprecated method
335
        return this.optionsSchema;
50✔
336
    }
50✔
337

338
    /**
339
     * Check if this algorithm has configurable options
340
     * @returns true if the algorithm has at least one option defined
341
     * @deprecated Use hasZodOptions() instead
342
     */
343
    static hasOptions(): boolean {
50✔
344
        // eslint-disable-next-line @typescript-eslint/no-deprecated -- Implementation of deprecated method
345
        return Object.keys(this.optionsSchema).length > 0;
25✔
346
    }
25✔
347

348
    /**
349
     * Get the Zod-based options schema for this algorithm.
350
     * @returns The Zod options schema, or an empty object if no schema defined
351
     */
352
    static getZodOptionsSchema(): ZodOptionsSchema {
50✔
353
        return this.zodOptionsSchema ?? {};
233✔
354
    }
233✔
355

356
    /**
357
     * Check if this algorithm has a Zod-based options schema.
358
     * @returns true if the algorithm has a Zod options schema defined
359
     */
360
    static hasZodOptions(): boolean {
50✔
361
        return this.zodOptionsSchema !== undefined && Object.keys(this.zodOptionsSchema).length > 0;
232✔
362
    }
232✔
363

364
    /**
365
     * Get all registered algorithm names.
366
     * @param namespace - Optional namespace to filter by
367
     * @returns Array of algorithm names in "namespace:type" format
368
     */
369
    static getRegisteredAlgorithms(namespace?: string): string[] {
50✔
370
        const algorithms: string[] = [];
5✔
371
        for (const key of algorithmRegistry.keys()) {
5✔
372
            if (!namespace || key.startsWith(`${namespace}:`)) {
115✔
373
                algorithms.push(key);
92✔
374
            }
92✔
375
        }
115✔
376

377
        return algorithms.sort();
5✔
378
    }
5✔
379

380
    /**
381
     * Get all registered algorithm types.
382
     * This method is provided for API consistency with DataSource.
383
     * @returns Array of algorithm keys in "namespace:type" format
384
     * @since 1.5.0
385
     * @example
386
     * ```typescript
387
     * const types = Algorithm.getRegisteredTypes();
388
     * console.log('Available algorithms:', types);
389
     * // ['graphty:betweenness', 'graphty:closeness', 'graphty:degree', ...]
390
     * ```
391
     */
392
    static getRegisteredTypes(): string[] {
50✔
393
        return this.getRegisteredAlgorithms();
×
394
    }
×
395
}
50✔
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