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

graphty-org / graphty-monorepo / 20690039805

04 Jan 2026 08:00AM UTC coverage: 77.552% (-0.4%) from 77.925%
20690039805

push

github

apowers313
chore: merge master into graphty algorithms and fix errors

13487 of 17880 branches covered (75.43%)

Branch coverage included in aggregate %.

107 of 108 new or added lines in 23 files covered. (99.07%)

245 existing lines in 9 files now uncovered.

41791 of 53399 relevant lines covered (78.26%)

142735.49 hits per line

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

92.11
/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 = {};
51✔
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 {
51✔
144
        return this._schemaOptions;
60✔
145
    }
60✔
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>) {
51✔
153
        this.graph = g;
289✔
154
        this._schemaOptions = this.resolveOptions(options);
289✔
155
    }
289✔
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 {
51✔
163
        // eslint-disable-next-line @typescript-eslint/no-deprecated -- Supporting backward compatibility
164
        const schema = (this.constructor as typeof Algorithm).optionsSchema;
289✔
165

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

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

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

182
    /**
183
     * Gets the algorithm namespace
184
     * @returns The algorithm namespace identifier
185
     */
186
    get namespace(): string {
51✔
187
        return (this.constructor as typeof Algorithm).namespace;
18,749✔
188
    }
18,749✔
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 {
51✔
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[] {
51✔
220
        const ret: string[] = [];
18,359✔
221

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

227
        return ret;
18,359✔
228
    }
18,359✔
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 {
51✔
237
        const p = this.#createPath(resultName);
14,996✔
238
        const n = this.graph.getDataManager().nodes.get(nodeId);
14,996✔
239
        if (!n) {
14,996!
240
            throw new Error(`couldn't find nodeId '${nodeId}' while trying to run algorithm '${this.type}'`);
×
241
        }
×
242

243
        deepSet(n, p, result);
14,996✔
244
        // XXX: THIS IS WHERE I LEFT OFF
245
        // replace algorithmResults with graph.nodes; set result on each node.algorithmResult
246
    }
14,996✔
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 {
51✔
255
        const p = this.#createPath(resultName);
3,363✔
256
        deepSet(edge, p, result);
3,363✔
257
    }
3,363✔
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 {
51✔
265
        const dm = this.graph.getDataManager();
389✔
266
        dm.graphResults ??= {} as AdHocData;
389✔
267

268
        const path = [this.namespace, this.type, resultName];
389✔
269
        deepSet(dm.graphResults, path, result);
389✔
270
    }
389✔
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 {
51✔
278
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
279
        const t: string = (cls as any).type;
5,456✔
280
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
281
        const ns: string = (cls as any).namespace;
5,456✔
282
        algorithmRegistry.set(`${ns}:${t}`, cls);
5,456✔
283
        return cls;
5,456✔
284
    }
5,456✔
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
     * @param options - Optional algorithm-specific options to pass to constructor
292
     * @returns A new instance of the algorithm, or null if not found
293
     */
294
    static get(g: Graph, namespace: string, type: string, options?: Record<string, unknown>): Algorithm | null {
51✔
295
        const SourceClass = algorithmRegistry.get(`${namespace}:${type}`);
73✔
296
        if (SourceClass) {
73✔
297
            return new SourceClass(g, options);
71✔
298
        }
71!
299

300
        return null;
2✔
301
    }
73✔
302

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

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

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

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

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

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

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

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

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

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