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;
}