• 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.1
/src/algorithms/MinCutAlgorithm.ts
1
/**
2
 * @fileoverview Minimum Cut Algorithm wrapper
3
 *
4
 * This algorithm finds the minimum cut that separates a source from a sink
5
 * or the global minimum cut of a graph. Uses the max-flow min-cut theorem.
6
 */
7

8
import {Graph as AlgorithmGraph, kargerMinCut, minSTCut, stoerWagner} from "@graphty/algorithms";
3✔
9
import {z} from "zod/v4";
3✔
10

11
import {defineOptions, type OptionsSchema as ZodOptionsSchema, type SuggestedStylesConfig} from "../config";
3✔
12
import {Algorithm} from "./Algorithm";
3✔
13
import {type OptionsSchema} from "./types/OptionSchema";
14

15
/**
16
 * Zod-based options schema for Min Cut algorithm
17
 */
18
export const minCutOptionsSchema = defineOptions({
3✔
19
    source: {
3✔
20
        schema: z.union([z.string(), z.number()]).nullable().default(null),
3✔
21
        meta: {
3✔
22
            label: "Source Node",
3✔
23
            description: "Source node for s-t cut (uses global min cut if not set)",
3✔
24
        },
3✔
25
    },
3✔
26
    sink: {
3✔
27
        schema: z.union([z.string(), z.number()]).nullable().default(null),
3✔
28
        meta: {
3✔
29
            label: "Sink Node",
3✔
30
            description: "Sink node for s-t cut (uses global min cut if not set)",
3✔
31
        },
3✔
32
    },
3✔
33
    useGlobalMinCut: {
3✔
34
        schema: z.boolean().default(false),
3✔
35
        meta: {
3✔
36
            label: "Use Global Min Cut",
3✔
37
            description: "Use global minimum cut algorithm instead of s-t cut",
3✔
38
        },
3✔
39
    },
3✔
40
    useKarger: {
3✔
41
        schema: z.boolean().default(false),
3✔
42
        meta: {
3✔
43
            label: "Use Karger Algorithm",
3✔
44
            description: "Use Karger's randomized algorithm instead of Stoer-Wagner for global min cut",
3✔
45
            advanced: true,
3✔
46
        },
3✔
47
    },
3✔
48
    kargerIterations: {
3✔
49
        schema: z.number().int().min(1).max(10000).default(100),
3✔
50
        meta: {
3✔
51
            label: "Karger Iterations",
3✔
52
            description: "Number of iterations for Karger's algorithm (higher = better accuracy but slower)",
3✔
53
            advanced: true,
3✔
54
        },
3✔
55
    },
3✔
56
});
3✔
57

58
/**
59
 * Options for Min Cut algorithm
60
 */
61
export interface MinCutOptions extends Record<string, unknown> {
62
    /** Source node for s-t cut (optional - uses global min cut if not provided) */
63
    source: string | number | null;
64
    /** Sink node for s-t cut (optional - uses global min cut if not provided) */
65
    sink: string | number | null;
66
    /** Whether to use Stoer-Wagner global minimum cut instead of s-t cut */
67
    useGlobalMinCut: boolean;
68
    /** Whether to use Karger's randomized algorithm instead of Stoer-Wagner for global min cut */
69
    useKarger: boolean;
70
    /** Number of iterations for Karger's algorithm (higher = better accuracy) */
71
    kargerIterations: number;
72
}
73

74
export class MinCutAlgorithm extends Algorithm<MinCutOptions> {
3✔
75
    static namespace = "graphty";
13✔
76
    static type = "min-cut";
13✔
77

78
    static zodOptionsSchema: ZodOptionsSchema = minCutOptionsSchema;
13✔
79

80
    /**
81
     * Options schema for Min Cut algorithm
82
     */
83
    static optionsSchema: OptionsSchema = {
13✔
84
        source: {
13✔
85
            type: "nodeId",
13✔
86
            default: null,
13✔
87
            label: "Source Node",
13✔
88
            description: "Source node for s-t cut (uses global min cut if not set)",
13✔
89
            required: false,
13✔
90
        },
13✔
91
        sink: {
13✔
92
            type: "nodeId",
13✔
93
            default: null,
13✔
94
            label: "Sink Node",
13✔
95
            description: "Sink node for s-t cut (uses global min cut if not set)",
13✔
96
            required: false,
13✔
97
        },
13✔
98
        useGlobalMinCut: {
13✔
99
            type: "boolean",
13✔
100
            default: false,
13✔
101
            label: "Use Global Min Cut",
13✔
102
            description: "Use global minimum cut algorithm instead of s-t cut",
13✔
103
            required: false,
13✔
104
        },
13✔
105
        useKarger: {
13✔
106
            type: "boolean",
13✔
107
            default: false,
13✔
108
            label: "Use Karger Algorithm",
13✔
109
            description: "Use Karger's randomized algorithm instead of Stoer-Wagner for global min cut",
13✔
110
            advanced: true,
13✔
111
        },
13✔
112
        kargerIterations: {
13✔
113
            type: "integer",
13✔
114
            default: 100,
13✔
115
            label: "Karger Iterations",
13✔
116
            description: "Number of iterations for Karger's algorithm (higher = better accuracy but slower)",
13✔
117
            min: 1,
13✔
118
            max: 10000,
13✔
119
            advanced: true,
13✔
120
        },
13✔
121
    };
13✔
122

123
    /**
124
     * Legacy options set via configure() for backward compatibility
125
     */
126
    private legacyOptions: {source?: string, sink?: string, useGlobalMinCut?: boolean} | null = null;
13✔
127

128
    static suggestedStyles = (): SuggestedStylesConfig => ({
108✔
129
        layers: [
13✔
130
            {
13✔
131
                edge: {
13✔
132
                    selector: "",
13✔
133
                    style: {enabled: true},
13✔
134
                    calculatedStyle: {
13✔
135
                        inputs: ["algorithmResults.graphty.min-cut.inCut"],
13✔
136
                        output: "style.line.color",
13✔
137
                        expr: "{ return StyleHelpers.color.binary.orangeWarning(arguments[0]) }",
13✔
138
                    },
13✔
139
                },
13✔
140
                metadata: {
13✔
141
                    name: "Min Cut - Cut Edges",
13✔
142
                    description: "Highlights edges in the minimum cut (orange) - colorblind-safe",
13✔
143
                },
13✔
144
            },
13✔
145
            {
13✔
146
                edge: {
13✔
147
                    selector: "algorithmResults.graphty.\"min-cut\".inCut == `false`",
13✔
148
                    style: {
13✔
149
                        enabled: true,
13✔
150
                        line: {
13✔
151
                            opacity: 0.4,
13✔
152
                        },
13✔
153
                    },
13✔
154
                },
13✔
155
                metadata: {
13✔
156
                    name: "Min Cut - Non-Cut Edges",
13✔
157
                    description: "Dims edges not in the minimum cut",
13✔
158
                },
13✔
159
            },
13✔
160
            {
13✔
161
                node: {
13✔
162
                    selector: "",
13✔
163
                    style: {enabled: true},
13✔
164
                    calculatedStyle: {
13✔
165
                        inputs: ["algorithmResults.graphty.min-cut.partition"],
13✔
166
                        output: "style.texture.color",
13✔
167
                        expr: "{ return StyleHelpers.color.categorical.okabeIto(Number(arguments[0] ?? 1) - 1) }",
13✔
168
                    },
13✔
169
                },
13✔
170
                metadata: {
13✔
171
                    name: "Min Cut - Partition Colors",
13✔
172
                    description: "Colors nodes by partition - colorblind-safe",
13✔
173
                },
13✔
174
            },
13✔
175
        ],
13✔
176
        description: "Visualizes minimum cut edges and the resulting graph partition",
13✔
177
        category: "path",
13✔
178
    });
13✔
179

180
    /**
181
     * Configure the algorithm with source, sink, and useGlobalMinCut options
182
     *
183
     * @deprecated Use constructor options instead. This method is kept for backward compatibility.
184
     */
185
    configure(options: {source?: string, sink?: string, useGlobalMinCut?: boolean}): this {
7✔
186
        this.legacyOptions = options;
1✔
187
        return this;
1✔
188
    }
1✔
189

190
    // eslint-disable-next-line @typescript-eslint/require-await
191
    async run(): Promise<void> {
7✔
192
        const g = this.graph;
6✔
193
        const edges = Array.from(g.getDataManager().edges.values());
6✔
194
        const nodes = Array.from(g.getDataManager().nodes.values());
6✔
195

196
        if (edges.length === 0 || nodes.length === 0) {
6✔
197
            return;
1✔
198
        }
1✔
199

200
        // Build weighted graph from edges - Map format for stoerWagner/kargerMinCut
201
        const weightedGraphMap = new Map<string, Map<string, number>>();
5✔
202
        // Also build AlgorithmGraph for minSTCut (which requires Graph type)
203
        const weightedGraph = new AlgorithmGraph({directed: false});
5✔
204

205
        // Initialize nodes
206
        for (const node of nodes) {
6✔
207
            weightedGraphMap.set(String(node.id), new Map());
34✔
208
            weightedGraph.addNode(String(node.id));
34✔
209
        }
34✔
210

211
        // Add edges with weights
212
        for (const edge of edges) {
6✔
213
            const srcId = String(edge.srcId);
39✔
214
            const dstId = String(edge.dstId);
39✔
215

216
            // Get weight from edge data
217
            const edgeData = edge.data as Record<string, unknown> | undefined;
39✔
218
            const edgeObject = edge as unknown as Record<string, unknown>;
39✔
219
            const rawWeight = edgeData?.value ?? edgeObject.value ?? 1;
39!
220
            const weight: number = typeof rawWeight === "number" ? rawWeight : 1;
39!
221

222
            // Add to Map (for stoerWagner/kargerMinCut)
223
            const srcNeighbors = weightedGraphMap.get(srcId);
39✔
224
            if (srcNeighbors) {
39✔
225
                srcNeighbors.set(dstId, weight);
39✔
226
            }
39✔
227

228
            // Add reverse edge for undirected graph (Map)
229
            const dstNeighbors = weightedGraphMap.get(dstId);
39✔
230
            if (dstNeighbors) {
39✔
231
                dstNeighbors.set(srcId, weight);
39✔
232
            }
39✔
233

234
            // Add to AlgorithmGraph (for minSTCut)
235
            weightedGraph.addEdge(srcId, dstId, weight);
39✔
236
        }
39✔
237

238
        // Get options from legacy or schema options
239
        // Legacy configure() takes precedence for backward compatibility
240
        const useGlobalMinCut = this.legacyOptions?.useGlobalMinCut ?? this._schemaOptions.useGlobalMinCut;
6✔
241
        const sourceOption = this.legacyOptions?.source ?? this._schemaOptions.source;
6✔
242
        const sinkOption = this.legacyOptions?.sink ?? this._schemaOptions.sink;
6✔
243
        const {useKarger, kargerIterations} = this._schemaOptions;
6✔
244

245
        // Determine which algorithm to use
246
        let partition1: Set<string>;
6✔
247
        let partition2: Set<string>;
6✔
248
        let cutEdges: {from: string, to: string, weight: number}[];
6✔
249
        let cutValue: number;
6✔
250

251
        if (useGlobalMinCut || (sourceOption === null && sinkOption === null)) {
6✔
252
            // Use global minimum cut algorithm
253
            if (useKarger) {
3!
254
                // Use Karger's randomized algorithm (accepts Map)
255
                const result = kargerMinCut(weightedGraphMap, kargerIterations);
×
256
                ({partition1, partition2, cutEdges, cutValue} = result);
×
257
            } else {
3✔
258
                // Use Stoer-Wagner for global minimum cut (accepts Map)
259
                const result = stoerWagner(weightedGraphMap);
3✔
260
                ({partition1, partition2, cutEdges, cutValue} = result);
3✔
261
            }
3✔
262
        } else {
4✔
263
            // Use min s-t cut via max flow (requires AlgorithmGraph)
264
            const source = sourceOption !== null ? String(sourceOption) : String(Array.from(g.getDataManager().nodes.keys())[0]);
2!
265
            const sink = sinkOption !== null ? String(sinkOption) : String(Array.from(g.getDataManager().nodes.keys()).pop());
2!
266

267
            const result = minSTCut(weightedGraph, source, sink);
2✔
268
            ({partition1, partition2, cutEdges, cutValue} = result);
2✔
269
        }
2✔
270

271
        // Create set of cut edge keys for fast lookup
272
        const cutEdgeKeys = new Set<string>();
5✔
273
        for (const edge of cutEdges) {
6✔
274
            cutEdgeKeys.add(`${edge.from}:${edge.to}`);
6✔
275
            cutEdgeKeys.add(`${edge.to}:${edge.from}`);
6✔
276
        }
6✔
277

278
        // Store edge results
279
        for (const edge of edges) {
6✔
280
            const edgeKey = `${edge.srcId}:${edge.dstId}`;
39✔
281
            const inCut = cutEdgeKeys.has(edgeKey);
39✔
282

283
            // Find weight if in cut
284
            let weight = 0;
39✔
285
            if (inCut) {
39✔
286
                const cutEdge = cutEdges.find(
6✔
287
                    (ce) =>
6✔
288
                        (ce.from === String(edge.srcId) && ce.to === String(edge.dstId)) ||
7✔
289
                        (ce.to === String(edge.srcId) && ce.from === String(edge.dstId)),
4✔
290
                );
6✔
291
                weight = cutEdge?.weight ?? 0;
6!
292
            }
6✔
293

294
            this.addEdgeResult(edge, "inCut", inCut);
39✔
295
            this.addEdgeResult(edge, "cutWeight", weight);
39✔
296
        }
39✔
297

298
        // Store node results
299
        for (const node of nodes) {
6✔
300
            const nodeId = String(node.id);
34✔
301
            const isInPartition1 = partition1.has(nodeId);
34✔
302
            const partition = isInPartition1 ? "1" : "2";
34✔
303

304
            this.addNodeResult(node.id, "partition", partition);
34✔
305
            this.addNodeResult(node.id, "isInPartition1", isInPartition1);
34✔
306
            this.addNodeResult(node.id, "isInPartition2", partition2.has(nodeId));
34✔
307
        }
34✔
308

309
        // Store graph-level results
310
        this.addGraphResult("cutValue", cutValue);
5✔
311
        this.addGraphResult("cutEdgeCount", cutEdges.length);
5✔
312
        this.addGraphResult("partition1Size", partition1.size);
5✔
313
        this.addGraphResult("partition2Size", partition2.size);
5✔
314
        this.addGraphResult("partition1", Array.from(partition1));
5✔
315
        this.addGraphResult("partition2", Array.from(partition2));
5✔
316
    }
6✔
317
}
7✔
318

319
// Auto-register this algorithm when the module is imported
320
Algorithm.register(MinCutAlgorithm);
3✔
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