NetiPlot

Custom Layouts

Replace the default hierarchical layout with any algorithm via the layouter prop.

Signature — NetiPlotLayouter

type NetiPlotLayouter = (
  data: {
    nodeMap: Map<string, NetiPlotNode>
    edgeMap: Map<string, NetiPlotEdge>
    shapes?: NetiPlotShapeDefinition[]
  },
  options: NetiPlotLayoutOptions,
  screen: NetiPlotScreen,
  onStopped?: () => void,
) => NetiPlotLayouterResult

Set node.destination = { x, y } to animate nodes to their target positions. Set node.x and node.y directly for immediate placement.

Call onStopped?.() when the layout is complete — this triggers a fit-to-view if layoutOptions.fitOnUpdate is enabled.

Synchronous layout

import type { NetiPlotLayouter } from '@jonmodell/netiplot'

const circleLayout: NetiPlotLayouter = (data, options, screen, onStopped) => {
  const nodes = Array.from(data.nodeMap.values())
  const cx = (screen.width ?? 600) / 2
  const cy = (screen.height ?? 400) / 2
  const radius = Math.min(cx, cy) * 0.7

  nodes.forEach((node, i) => {
    const angle = (i / nodes.length) * Math.PI * 2
    node.destination = {
      x: cx + Math.cos(angle) * radius,
      y: cy + Math.sin(angle) * radius,
    }
  })

  onStopped?.()
}

Async / force layout

Return { stop } for layouts that run over multiple frames (e.g. force simulations):

const forceLayout: NetiPlotLayouter = (data, options, screen, onStopped) => {
  const simulation = d3.forceSimulation(/* ... */)

  simulation.on('end', () => onStopped?.())

  return {
    stop: () => simulation.stop(),
  }
}

Controlling when layout re-runs — ShouldRunLayouter

By default the layout re-runs whenever the graph changes. Override this with shouldRunLayouter:

// Only re-layout when the number of nodes changes
const shouldRunLayouter: ShouldRunLayouter = (prev, next) =>
  prev.graph.nodes.length !== next.graph.nodes.length

NetiPlotScreen

interface NetiPlotScreen {
  width: number | undefined
  height: number | undefined
  ratio: number
  boundingRect: DOMRect | undefined | null
}