Skip to content

Custom Algorithms ​

Guide to creating custom graph algorithms.

Overview ​

Graphty's algorithm system is extensible. Create custom algorithms to compute metrics, detect patterns, or analyze graph structure.

Algorithm Interface ​

All algorithms extend the abstract Algorithm class:

typescript
abstract class Algorithm {
    static namespace: string;
    static type: string;

    abstract run(graph: Graph, options?: object): AlgorithmResult;
}

interface AlgorithmResult {
    nodeResults: Map<string, any>;
    edgeResults?: Map<string, any>;
    suggestedStyles?: StyleSchema;
}

Creating a Custom Algorithm ​

Basic Example ​

typescript
import { Algorithm, Graph, AlgorithmResult } from "@graphty/graphty-element";

class MyAlgorithm extends Algorithm {
    static namespace = "custom";
    static type = "my-algo";

    run(graph: Graph, options?: object): AlgorithmResult {
        const nodeResults = new Map<string, any>();

        // Compute something for each node
        for (const node of graph.getNodes()) {
            const value = this.computeValue(node, graph);
            nodeResults.set(node.id, value);
        }

        return { nodeResults };
    }

    private computeValue(node: Node, graph: Graph): number {
        // Your algorithm logic here
        return 42;
    }
}

// Register the algorithm
Algorithm.register(MyAlgorithm);

Using Your Algorithm ​

typescript
await graph.runAlgorithm("custom", "my-algo");

// Access results
const node = graph.getNode("node1");
const result = node.algorithmResults["custom:my-algo"];

Complete Example: In-Degree/Out-Degree ​

typescript
import { Algorithm, Graph, AlgorithmResult, Node } from "@graphty/graphty-element";

class InOutDegreeAlgorithm extends Algorithm {
    static namespace = "custom";
    static type = "in-out-degree";

    run(graph: Graph, options?: object): AlgorithmResult {
        const nodeResults = new Map<string, { in: number; out: number }>();

        // Initialize counts
        for (const node of graph.getNodes()) {
            nodeResults.set(node.id, { in: 0, out: 0 });
        }

        // Count edges
        for (const edge of graph.getEdges()) {
            const sourceId = edge.source as string;
            const targetId = edge.target as string;

            // Increment out-degree for source
            const sourceResult = nodeResults.get(sourceId);
            if (sourceResult) {
                sourceResult.out++;
            }

            // Increment in-degree for target
            const targetResult = nodeResults.get(targetId);
            if (targetResult) {
                targetResult.in++;
            }
        }

        return {
            nodeResults,
            suggestedStyles: this.getSuggestedStyles(nodeResults),
        };
    }

    private getSuggestedStyles(results: Map<string, { in: number; out: number }>): StyleSchema {
        // Find max for normalization
        let maxTotal = 0;
        for (const { in: inDeg, out: outDeg } of results.values()) {
            maxTotal = Math.max(maxTotal, inDeg + outDeg);
        }

        return {
            layers: [
                {
                    selector: "*",
                    styles: {
                        node: {
                            size: (node: Node) => {
                                const result = node.algorithmResults["custom:in-out-degree"];
                                if (!result) return 1;
                                const total = result.in + result.out;
                                return 0.5 + (total / maxTotal) * 2;
                            },
                        },
                    },
                },
            ],
        };
    }
}

Algorithm.register(InOutDegreeAlgorithm);

Algorithm with Options ​

Accept configuration options:

typescript
interface ClusteringOptions {
    threshold: number;
    maxIterations: number;
}

class ClusteringAlgorithm extends Algorithm {
    static namespace = "custom";
    static type = "clustering";

    run(graph: Graph, options: Partial<ClusteringOptions> = {}): AlgorithmResult {
        const config: ClusteringOptions = {
            threshold: 0.5,
            maxIterations: 100,
            ...options,
        };

        const nodeResults = new Map<string, number>();

        // Use config in algorithm
        let iterations = 0;
        while (iterations < config.maxIterations) {
            // Clustering logic...
            iterations++;
        }

        return { nodeResults };
    }
}

Algorithm.register(ClusteringAlgorithm);

Usage:

typescript
await graph.runAlgorithm("custom", "clustering", {
    threshold: 0.7,
    maxIterations: 50,
});

Suggested Styles ​

Provide visualization suggestions with your algorithm:

typescript
class ImportanceAlgorithm extends Algorithm {
    static namespace = "custom";
    static type = "importance";

    run(graph: Graph): AlgorithmResult {
        const nodeResults = new Map<string, number>();

        // Calculate importance scores (0-1)
        for (const node of graph.getNodes()) {
            const score = this.calculateImportance(node, graph);
            nodeResults.set(node.id, score);
        }

        return {
            nodeResults,
            suggestedStyles: {
                layers: [
                    {
                        selector: "*",
                        styles: {
                            node: {
                                color: (node: Node) => {
                                    const score = node.algorithmResults["custom:importance"] || 0;
                                    // Green to red gradient
                                    const r = Math.floor(255 * score);
                                    const g = Math.floor(255 * (1 - score));
                                    return `rgb(${r}, ${g}, 0)`;
                                },
                                size: (node: Node) => {
                                    const score = node.algorithmResults["custom:importance"] || 0;
                                    return 0.5 + score * 2;
                                },
                            },
                        },
                    },
                ],
            },
        };
    }
}

Apply suggested styles:

typescript
await graph.runAlgorithm("custom", "importance");
graph.applySuggestedStyles("custom:importance");

Using @graphty/algorithms ​

Leverage the algorithms package for complex computations:

typescript
import { Algorithm, Graph, AlgorithmResult } from "@graphty/graphty-element";
import { shortestPath } from "@graphty/algorithms";

class CentralityFromShortestPaths extends Algorithm {
    static namespace = "custom";
    static type = "custom-centrality";

    run(graph: Graph): AlgorithmResult {
        const nodeResults = new Map<string, number>();

        // Convert to format expected by @graphty/algorithms
        const algorithmGraph = this.convertGraph(graph);

        // Use library functions
        for (const node of graph.getNodes()) {
            let totalDistance = 0;
            for (const other of graph.getNodes()) {
                if (node.id !== other.id) {
                    const path = shortestPath(algorithmGraph, node.id, other.id);
                    totalDistance += path?.length || Infinity;
                }
            }
            // Closeness centrality (inverse of average distance)
            const centrality = (graph.getNodes().length - 1) / totalDistance;
            nodeResults.set(node.id, centrality);
        }

        return { nodeResults };
    }

    private convertGraph(graph: Graph) {
        // Convert to @graphty/algorithms format
        // ...
    }
}

Algorithm.register(CentralityFromShortestPaths);

Edge Results ​

Return results for edges as well as nodes:

typescript
class EdgeWeightAnalysis extends Algorithm {
    static namespace = "custom";
    static type = "edge-analysis";

    run(graph: Graph): AlgorithmResult {
        const nodeResults = new Map<string, number>();
        const edgeResults = new Map<string, { normalized: number }>();

        // Find weight range
        let minWeight = Infinity;
        let maxWeight = -Infinity;

        for (const edge of graph.getEdges()) {
            const weight = edge.weight || 1;
            minWeight = Math.min(minWeight, weight);
            maxWeight = Math.max(maxWeight, weight);
        }

        // Normalize weights
        const range = maxWeight - minWeight || 1;

        for (const edge of graph.getEdges()) {
            const weight = edge.weight || 1;
            const normalized = (weight - minWeight) / range;
            edgeResults.set(edge.id, { normalized });
        }

        return { nodeResults, edgeResults };
    }
}

Async Algorithms ​

For long-running algorithms, use async/await:

typescript
class AsyncAlgorithm extends Algorithm {
    static namespace = "custom";
    static type = "async-algo";

    async run(graph: Graph): Promise<AlgorithmResult> {
        const nodeResults = new Map<string, number>();

        // Simulate long computation
        for (const node of graph.getNodes()) {
            await this.heavyComputation(node);
            nodeResults.set(node.id, 1);
        }

        return { nodeResults };
    }

    private async heavyComputation(node: Node): Promise<void> {
        // Expensive operation
        await new Promise((resolve) => setTimeout(resolve, 10));
    }
}

Performance Tips ​

  1. Cache intermediate results: Store computed values for reuse
  2. Use efficient data structures: Maps over arrays for lookups
  3. Parallelize when possible: Web Workers for heavy computation
  4. Early termination: Stop when result is "good enough"
typescript
// Use adjacency list for faster neighbor lookups
private buildAdjacencyList(graph: Graph): Map<string, string[]> {
  const adj = new Map<string, string[]>();

  for (const node of graph.getNodes()) {
    adj.set(node.id, []);
  }

  for (const edge of graph.getEdges()) {
    adj.get(edge.source as string)?.push(edge.target as string);
  }

  return adj;
}