Graph apps need ways to arrange nodes and move the camera.
LivePanels provides headless layout helpers, graph fit commands, bounds calculation, and navigation queries. The application decides when users invoke them and how the UI presents the result.
Built-In Layout Strategies
Use LivePanels.Graph.layout/3 from the canvas LiveView:
socket = LivePanels.Graph.layout!(socket, :layered)Supported strategies:
| Strategy | Use when |
|---|---|
:layered | Directed graphs where downstream nodes should move by depth. |
:tree | Rooted or mostly rooted structures. |
:orthogonal | Graphs rendered with orthogonal connector paths. |
:components | Graphs with separate connected components. |
Layout commands move ordinary canvas items, so history, persistence, policy, and shared mode all see normal placement changes.
Layout Options
Pass options to tune spacing and axis:
socket =
LivePanels.Graph.layout!(socket, :components,
strategy: :layered,
axis: :horizontal,
component_axis: :vertical,
layer_gap: 380,
item_gap: 180,
component_gap: 260
)Use application controls for common layout actions:
<button phx-click="graph_layout" phx-value-strategy="layered">Layered</button>
<button phx-click="graph_layout" phx-value-strategy="tree">Tree</button>
<button phx-click="graph_layout" phx-value-strategy="orthogonal">Tracks</button>Handle the event:
def handle_event("graph_layout", %{"strategy" => strategy}, socket) do
strategy =
case strategy do
"layered" -> :layered
"tree" -> :tree
"orthogonal" -> :orthogonal
"components" -> :components
_ -> :layered
end
{:noreply, LivePanels.Graph.layout!(socket, strategy)}
endExternal Layout Engines
Use the adapter payload when an application wants an external layout engine:
state = LivePanels.Canvas.Read.state(socket)
adapter = LivePanels.Graph.Layout.port_aware_adapter(state)The adapter names runtime nodes, ports, connectors, components, and boundaries. After an external engine returns node coordinates, convert them to placement commands:
commands =
LivePanels.Graph.Layout.adapter_commands(state, [
%{id: "node-a", x: 100, y: 80},
%{id: "node-b", x: 480, y: 80}
])
socket =
Enum.reduce(commands, socket, fn {:place_item, item_id, placement}, acc ->
LivePanels.Canvas.Item.place!(acc, item_id, placement)
end)Keep external layout results on the command path. Do not directly rewrite item assigns.
Fit The Graph
Fit the whole graph:
<button {graph_fit_attrs()}>Fit graph</button>Fit the current graph selection:
<button {graph_fit_selection_attrs()}>Fit selection</button>Server equivalents:
socket = LivePanels.Graph.fit(socket, :graph)
socket = LivePanels.Graph.fit(socket, :selection)Fit changes the actor's viewport. It does not mutate node placement.
Bounds
Use LivePanels.Graph.Bounds when application UI needs graph extents:
state = LivePanels.Canvas.Read.state(socket)
bounds = LivePanels.Graph.Bounds.bounds(state)Use bounds for minimaps, overview panels, "empty graph" prompts, or app-level
navigation. Use LivePanels.Graph.fit/2 when the canvas viewport should move.
Navigation Queries
Use LivePanels.Graph.Query.navigation/4 to derive graph navigation targets:
navigation =
LivePanels.Graph.Query.navigation(
LivePanels.Canvas.Read.state(socket),
{:item, "node-a"},
:outgoing,
mode: :all
)
targets = navigation.targetsDirections:
:incoming;:outgoing;:connected.
Use navigation queries to build inspector actions such as "select downstream", "jump to parent", or "show connected nodes".
Viewport Anchors
For product-level graph navigation, combine graph queries with
LivePanels.Canvas.Viewport anchors:
state = LivePanels.Canvas.Read.state(socket)
socket =
case LivePanels.Canvas.Viewport.fit_to_items_anchor(state, "current-step", ["step-7"]) do
nil ->
socket
anchor ->
socket
|> LivePanels.Canvas.Viewport.put_anchor!(anchor)
|> LivePanels.Canvas.Viewport.goto_anchor!("current-step")
endUse graph fit for topology-driven views. Use viewport anchors for named product locations.
Layout And Shared Mode
Graph layout commands are canvas commands. In shared mode, applying a layout changes shared item placement for everyone.
For collaborative apps, decide whether layout buttons are editor-only, whether viewers can fit their own viewport, and whether automatic layout should be policy-gated.
Viewport fit is actor-scoped. Graph layout is shared state.