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