Skip to content

Commit f54e9e1

Browse files
authored
feat: switch economy graph from layered to force-directed layout (#2108)
The layered algorithm compressed all nodes into a narrow horizontal band, requiring zoom to read. The force-directed (Fruchterman-Reingold) algorithm naturally fills available space - highly connected nodes like GBP gravitate to center with related nodes radiating outward. ELK's radial algorithm was tested but requires tree topology and collapsed non-tree nodes to identical positions. Force handles arbitrary graph topologies and produces organic, space-filling layouts. Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent a4903a4 commit f54e9e1

2 files changed

Lines changed: 26 additions & 14 deletions

File tree

frontend/src/features/manifests/components/manifest-graph.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {
4242
type ManifestNodeType,
4343
type ManifestGraph as ManifestGraphModel,
4444
} from '../lib/manifest-graph-model'
45-
import { NODE_TYPE_REGISTRY, getNodeThemes, getLayerPriority } from '../lib/node-type-registry'
45+
import { NODE_TYPE_REGISTRY, getNodeThemes } from '../lib/node-type-registry'
4646
import type { Manifest } from '@/api/gen/meridian/control_plane/v1/manifest_pb'
4747
import { useEventChain } from '../hooks/use-event-chain'
4848
import { EventChainPanel } from './event-chain-panel'
@@ -64,7 +64,7 @@ const EDGE_LEGEND: { label: string; color: string; dashed?: boolean }[] = [
6464
{ label: 'Converts to', color: 'var(--graph-valuation-rule)' },
6565
]
6666

67-
const LAYER_PRIORITY = getLayerPriority()
67+
6868

6969
// Trigger type display
7070
function getTriggerBadge(trigger: string): { label: string; variant: string } {
@@ -592,9 +592,6 @@ async function layoutManifestGraph(
592592
id: n.id,
593593
width: NODE_WIDTH,
594594
height: NODE_BASE_HEIGHT + NODE_PADDING,
595-
layoutOptions: {
596-
'elk.layered.layering.layerChoiceConstraint': LAYER_PRIORITY[n.type],
597-
},
598595
}))
599596

600597
const rfEdges = buildReactFlowEdges(filteredEdges)
@@ -619,9 +616,12 @@ async function layoutManifestGraph(
619616
}
620617
},
621618
{
622-
direction: 'DOWN',
623-
nodeNodeSpacing: '50',
624-
layerSpacing: '80',
619+
algorithm: 'force',
620+
nodeNodeSpacing: '80',
621+
extra: {
622+
'elk.force.iterations': '300',
623+
'elk.force.repulsion': '5.0',
624+
},
625625
},
626626
)
627627

frontend/src/lib/visualization/graph-layout.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface ELKLayoutOptions {
2121
direction?: string
2222
nodeNodeSpacing?: string
2323
layerSpacing?: string
24+
/** Additional ELK layout options passed through verbatim. */
25+
extra?: Record<string, string>
2426
}
2527

2628
export interface LayoutNode {
@@ -53,14 +55,24 @@ export async function layoutWithELK<T extends Record<string, unknown>>(
5355
targets: [e.target],
5456
}))
5557

58+
const algorithm = options?.algorithm ?? 'layered'
59+
const layoutOptions: Record<string, string> = {
60+
'elk.algorithm': algorithm,
61+
'elk.spacing.nodeNode': options?.nodeNodeSpacing ?? '60',
62+
}
63+
64+
if (algorithm === 'layered') {
65+
layoutOptions['elk.direction'] = options?.direction ?? 'DOWN'
66+
layoutOptions['elk.layered.spacing.nodeNodeBetweenLayers'] = options?.layerSpacing ?? '100'
67+
}
68+
69+
if (options?.extra) {
70+
Object.assign(layoutOptions, options.extra)
71+
}
72+
5673
const layout = await elk.layout({
5774
id: 'root',
58-
layoutOptions: {
59-
'elk.algorithm': options?.algorithm ?? 'layered',
60-
'elk.direction': options?.direction ?? 'DOWN',
61-
'elk.spacing.nodeNode': options?.nodeNodeSpacing ?? '60',
62-
'elk.layered.spacing.nodeNodeBetweenLayers': options?.layerSpacing ?? '100',
63-
},
75+
layoutOptions,
6476
children: elkNodes,
6577
edges: elkEdges,
6678
})

0 commit comments

Comments
 (0)