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

graphty-org / graphty-element / 20514590651

26 Dec 2025 02:37AM UTC coverage: 70.559% (-0.3%) from 70.836%
20514590651

push

github

apowers313
ci: fix npm ci

9591 of 13363 branches covered (71.77%)

Branch coverage included in aggregate %.

25136 of 35854 relevant lines covered (70.11%)

6233.71 hits per line

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

92.71
/src/algorithms/Algorithm.ts
1
import {set as deepSet} from "lodash";
15✔
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";
15✔
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>();
15✔
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
 * @template TOptions - The options type for this algorithm (defaults to empty object)
72
 * @example
73
 * ```typescript
74
 * // Algorithm with options
75
 * interface PageRankOptions {
76
 *     dampingFactor: number;
77
 *     maxIterations: number;
78
 * }
79
 *
80
 * class PageRankAlgorithm extends Algorithm<PageRankOptions> {
81
 *     static optionsSchema: OptionsSchema = {
82
 *         dampingFactor: { type: 'number', default: 0.85, ... },
83
 *         maxIterations: { type: 'integer', default: 100, ... }
84
 *     };
85
 *
86
 *     async run(): Promise<void> {
87
 *         const { dampingFactor, maxIterations } = this.options;
88
 *         // ... use options
89
 *     }
90
 * }
91
 * ```
92
 */
93
export abstract class Algorithm<TOptions extends Record<string, unknown> = Record<string, unknown>> {
15✔
94
    static type: string;
95
    static namespace: string;
96
    static suggestedStyles?: SuggestedStylesProvider;
97

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

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

115
    protected graph: Graph;
116

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

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

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

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

161
        // If no schema defined, return empty object (backward compatible)
162
        if (Object.keys(schema).length === 0) {
238✔
163
            return {} as TOptions;
69✔
164
        }
69✔
165

166
        return resolveOptions(schema, options as Partial<OptionsFromSchema<typeof schema>>) as TOptions;
169✔
167
    }
238✔
168

169
    /**
170
     * Gets the algorithm type
171
     * @returns The algorithm type identifier
172
     */
173
    get type(): string {
50✔
174
        return (this.constructor as typeof Algorithm).type;
14,807✔
175
    }
14,807✔
176

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

185
    /**
186
     * Gets all algorithm results for nodes, edges, and graph
187
     * @returns An object containing node, edge, and graph results
188
     */
189
    get results(): AdHocData {
50✔
190
        const algorithmResults = {} as AdHocData;
13✔
191

192
        // Node results
193
        for (const n of this.graph.getDataManager().nodes.values()) {
13✔
194
            deepSet(algorithmResults, `node.${n.id}`, n.algorithmResults);
554✔
195
        }
554✔
196

197
        // Edge results
198
        for (const e of this.graph.getDataManager().edges.values()) {
13✔
199
            const edgeKey = `${e.srcId}:${e.dstId}`;
1,790✔
200
            deepSet(algorithmResults, `edge.${edgeKey}`, e.algorithmResults);
1,790✔
201
        }
1,790✔
202

203
        // Graph results
204
        const dm = this.graph.getDataManager();
13✔
205
        if (dm.graphResults) {
13✔
206
            algorithmResults.graph = dm.graphResults;
11✔
207
        }
11✔
208

209
        return algorithmResults;
13✔
210
    }
13✔
211

212
    abstract run(g: Graph): Promise<void>;
213

214
    #createPath(resultName: string): string[] {
50✔
215
        const ret: string[] = [];
14,520✔
216

217
        ret.push("algorithmResults");
14,520✔
218
        ret.push(this.namespace);
14,520✔
219
        ret.push(this.type);
14,520✔
220
        ret.push(resultName);
14,520✔
221

222
        return ret;
14,520✔
223
    }
14,520✔
224

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

238
        deepSet(n, p, result);
11,253✔
239
        // XXX: THIS IS WHERE I LEFT OFF
240
        // replace algorithmResults with graph.nodes; set result on each node.algorithmResult
241
    }
11,253✔
242

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

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

263
        const path = [this.namespace, this.type, resultName];
286✔
264
        deepSet(dm.graphResults, path, result);
286✔
265
    }
286✔
266

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

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

294
        return null;
2✔
295
    }
71✔
296

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

307
    /**
308
     * Check if this algorithm has suggested styles
309
     * @returns true if suggested styles are defined
310
     */
311
    static hasSuggestedStyles(): boolean {
50✔
312
        return !!this.suggestedStyles;
312✔
313
    }
312✔
314

315
    /**
316
     * Get suggested styles for this algorithm
317
     * @returns The suggested styles configuration, or null if none defined
318
     */
319
    static getSuggestedStyles(): SuggestedStylesConfig | null {
50✔
320
        return this.suggestedStyles ? this.suggestedStyles() : null;
220!
321
    }
220✔
322

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

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

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

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

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

373
        return algorithms.sort();
5✔
374
    }
5✔
375

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

© 2025 Coveralls, Inc