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

graphty-org / graphty-element / 20390753610

20 Dec 2025 06:53AM UTC coverage: 82.423% (-1.2%) from 83.666%
20390753610

push

github

apowers313
Merge branch 'master' of https://github.com/graphty-org/graphty-element

5162 of 6088 branches covered (84.79%)

Branch coverage included in aggregate %.

24775 of 30233 relevant lines covered (81.95%)

6480.4 hits per line

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

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

3
import {AdHocData, 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
    getOptionsSchema(): OptionsSchema;
25
    hasOptions(): boolean;
26
    hasSuggestedStyles(): boolean;
27
    getSuggestedStyles(): SuggestedStylesConfig | null;
28
}
29

30
const algorithmRegistry = new Map<string, AlgorithmClass>();
3✔
31

32
// algorithmResults layout:
33
// {
34
//     node: {
35
//         id: {
36
//             namespace: {
37
//                 algorithm: {
38
//                     result: unknown
39
//                 }
40
//             }
41
//         }
42
//     },
43
//     edge: {
44
//         id: {
45
//             namespace: {
46
//                 algorithm: {
47
//                     result: unknown
48
//                 }
49
//             }
50
//         }
51
//     },
52
//     graph: {
53
//         namespace: {
54
//             algorithm: {
55
//                 result: unknown
56
//             }
57
//         }
58
//     }
59
// }
60

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

92
    /**
93
     * Options schema for this algorithm
94
     *
95
     * Subclasses should override this to define their configurable options.
96
     * An empty schema means the algorithm has no configurable options.
97
     */
98
    static optionsSchema: OptionsSchema = {};
38✔
99

100
    protected graph: Graph;
101

102
    /**
103
     * Resolved options for this algorithm instance
104
     *
105
     * Options are resolved at construction time by:
106
     * 1. Starting with schema defaults
107
     * 2. Overriding with any provided options
108
     * 3. Validating all values against the schema
109
     *
110
     * Note: Named with underscore prefix to avoid conflicts with
111
     * existing algorithm implementations that have their own
112
     * options properties (will be removed in future refactoring).
113
     */
114
    protected _schemaOptions: TOptions;
115

116
    /**
117
     * Getter for schema options
118
     *
119
     * Algorithms that use the new schema-based options should access
120
     * options via this getter.
121
     */
122
    protected get schemaOptions(): TOptions {
38✔
123
        return this._schemaOptions;
39✔
124
    }
39✔
125

126
    /**
127
     * Creates a new algorithm instance
128
     *
129
     * @param g - The graph to run the algorithm on
130
     * @param options - Optional configuration options (uses schema defaults if not provided)
131
     */
132
    constructor(g: Graph, options?: Partial<TOptions>) {
38✔
133
        this.graph = g;
238✔
134
        this._schemaOptions = this.resolveOptions(options);
238✔
135
    }
238✔
136

137
    /**
138
     * Resolves and validates options against the schema
139
     *
140
     * @param options - User-provided options (partial)
141
     * @returns Fully resolved options with defaults applied
142
     */
143
    protected resolveOptions(options?: Partial<TOptions>): TOptions {
38✔
144
        const schema = (this.constructor as typeof Algorithm).optionsSchema;
238✔
145

146
        // If no schema defined, return empty object (backward compatible)
147
        if (Object.keys(schema).length === 0) {
238✔
148
            return {} as TOptions;
69✔
149
        }
69✔
150

151
        return resolveOptions(schema, options as Partial<OptionsFromSchema<typeof schema>>) as TOptions;
169✔
152
    }
238✔
153

154
    get type(): string {
38✔
155
        return (this.constructor as typeof Algorithm).type;
14,807✔
156
    }
14,807✔
157

158
    get namespace(): string {
38✔
159
        return (this.constructor as typeof Algorithm).namespace;
14,807✔
160
    }
14,807✔
161

162
    get results(): AdHocData {
38✔
163
        const algorithmResults = {} as AdHocData;
13✔
164

165
        // Node results
166
        for (const n of this.graph.getDataManager().nodes.values()) {
13✔
167
            deepSet(algorithmResults, `node.${n.id}`, n.algorithmResults);
554✔
168
        }
554✔
169

170
        // Edge results
171
        for (const e of this.graph.getDataManager().edges.values()) {
13✔
172
            const edgeKey = `${e.srcId}:${e.dstId}`;
1,790✔
173
            deepSet(algorithmResults, `edge.${edgeKey}`, e.algorithmResults);
1,790✔
174
        }
1,790✔
175

176
        // Graph results
177
        const dm = this.graph.getDataManager();
13✔
178
        if (dm.graphResults) {
13✔
179
            algorithmResults.graph = dm.graphResults;
11✔
180
        }
11✔
181

182
        return algorithmResults;
13✔
183
    }
13✔
184

185
    abstract run(g: Graph): Promise<void>;
186

187
    #createPath(resultName: string): string[] {
38✔
188
        const ret: string[] = [];
14,520✔
189

190
        ret.push("algorithmResults");
14,520✔
191
        ret.push(this.namespace);
14,520✔
192
        ret.push(this.type);
14,520✔
193
        ret.push(resultName);
14,520✔
194

195
        return ret;
14,520✔
196
    }
14,520✔
197

198
    addNodeResult(nodeId: number | string, resultName: string, result: unknown): void {
38✔
199
        const p = this.#createPath(resultName);
11,253✔
200
        const n = this.graph.getDataManager().nodes.get(nodeId);
11,253✔
201
        if (!n) {
11,253!
202
            throw new Error(`couldn't find nodeId '${nodeId}' while trying to run algorithm '${this.type}'`);
×
203
        }
×
204

205
        deepSet(n, p, result);
11,253✔
206
        // XXX: THIS IS WHERE I LEFT OFF
207
        // replace algorithmResults with graph.nodes; set result on each node.algorithmResult
208
    }
11,253✔
209

210
    addEdgeResult(edge: Edge, resultName: string, result: unknown): void {
38✔
211
        const p = this.#createPath(resultName);
3,267✔
212
        deepSet(edge, p, result);
3,267✔
213
    }
3,267✔
214

215
    addGraphResult(resultName: string, result: unknown): void {
38✔
216
        const dm = this.graph.getDataManager();
286✔
217
        dm.graphResults ??= {} as AdHocData;
286✔
218

219
        const path = [this.namespace, this.type, resultName];
286✔
220
        deepSet(dm.graphResults, path, result);
286✔
221
    }
286✔
222

223
    static register<T extends AlgorithmClass>(cls: T): T {
38✔
224
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
225
        const t: string = (cls as any).type;
4,889✔
226
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
227
        const ns: string = (cls as any).namespace;
4,889✔
228
        algorithmRegistry.set(`${ns}:${t}`, cls);
4,889✔
229
        return cls;
4,889✔
230
    }
4,889✔
231

232
    static get(g: Graph, namespace: string, type: string): Algorithm | null {
38✔
233
        const SourceClass = algorithmRegistry.get(`${namespace}:${type}`);
71✔
234
        if (SourceClass) {
71✔
235
            return new SourceClass(g);
69✔
236
        }
69✔
237

238
        return null;
2✔
239
    }
71✔
240

241
    static getClass(namespace: string, type: string): (AlgorithmClass & AlgorithmStatics) | null {
38✔
242
        return algorithmRegistry.get(`${namespace}:${type}`) as (AlgorithmClass & AlgorithmStatics) | null ?? null;
87✔
243
    }
87✔
244

245
    /**
246
     * Check if this algorithm has suggested styles
247
     */
248
    static hasSuggestedStyles(): boolean {
38✔
249
        return !!this.suggestedStyles;
243✔
250
    }
243✔
251

252
    /**
253
     * Get suggested styles for this algorithm
254
     */
255
    static getSuggestedStyles(): SuggestedStylesConfig | null {
38✔
256
        return this.suggestedStyles ? this.suggestedStyles() : null;
220✔
257
    }
220✔
258

259
    /**
260
     * Get the options schema for this algorithm
261
     *
262
     * @returns The options schema, or an empty object if no options defined
263
     */
264
    static getOptionsSchema(): OptionsSchema {
38✔
265
        return this.optionsSchema;
212✔
266
    }
212✔
267

268
    /**
269
     * Check if this algorithm has configurable options
270
     *
271
     * @returns true if the algorithm has at least one option defined
272
     */
273
    static hasOptions(): boolean {
38✔
274
        return Object.keys(this.optionsSchema).length > 0;
186✔
275
    }
186✔
276

277
    /**
278
     * Get all registered algorithm names.
279
     * @param namespace - Optional namespace to filter by
280
     * @returns Array of algorithm names in "namespace:type" format
281
     */
282
    static getRegisteredAlgorithms(namespace?: string): string[] {
38✔
283
        const algorithms: string[] = [];
5✔
284
        for (const key of algorithmRegistry.keys()) {
5✔
285
            if (!namespace || key.startsWith(`${namespace}:`)) {
115✔
286
                algorithms.push(key);
92✔
287
            }
92✔
288
        }
115✔
289

290
        return algorithms.sort();
5✔
291
    }
5✔
292
}
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