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 ​
- Cache intermediate results: Store computed values for reuse
- Use efficient data structures: Maps over arrays for lookups
- Parallelize when possible: Web Workers for heavy computation
- 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;
}