Graph support lets canvas items expose ports and connect those ports with validated connectors.
Use it when a connector changes the canvas itself: workflow transitions, dataflow pipes, dependency lines, relationships, references, or diagram connections.
LivePanels owns the runtime facts: declared ports, connector state, endpoint validation, connector selection, routing projection, history, persistence, and shared coordination. Your application owns node markup, port visuals, connector styling, labels, toolbars, and product meaning.
Use The Graph Toolkit
Import the graph toolkit on graph-capable canvases:
defmodule MyAppWeb.FlowLive do
use MyAppWeb, :live_view
use LivePanels.Canvas,
pubsub: MyApp.PubSub
use LivePanels.Toolkit.Graph
def render(assigns) do
~H"""
<.canvas_runtime {canvas_runtime_attrs(assigns)}>
<:item
id="transform-1"
type="transform"
at={{120, 80}}
size={{560, 400}}
ports={[
%{name: "input", direction: :input, type: :data, max_connections: 1},
%{name: "output", direction: :output, type: :data}
]}
>
<.transform_node />
</:item>
</.canvas_runtime>
"""
end
endPorts belong to item attrs. They are not just buttons in the DOM. That lets the server validate endpoint existence, direction, type, and connection limits.
Declare Port Geometry
Ports need world-space endpoints. Declare their anchor relative to the item frame:
%{
name: "output",
direction: :output,
type: :data,
anchor: %{edge: :right, offset: 0.5, inset: 0}
}Or use explicit offsets from the item origin:
%{
name: "preview",
direction: :output,
type: :image,
anchor: %{x: {:px_from_end, 24}, y: 32}
}The runtime resolves anchors from item placement. The browser does not become the canonical source of port coordinates.
Render Ports
Render app-owned port controls in the :item slot:
<.canvas_runtime {canvas_runtime_attrs(assigns)}>
<:item :let={context}>
<article class="node-card">
<header {drag_handle_attrs()}>{context.item.label}</header>
<button
:for={port <- context.ports}
class={"node-port node-port--#{port.direction}"}
{graph_port_anchor_attrs(port)}
{if port.direction == :output,
do: begin_connector_attrs(context.item, port),
else: complete_connector_attrs(context.item, port)}
>
{port.label}
</button>
{context.content}
</article>
</:item>
</.canvas_runtime>graph_port_anchor_attrs/2 gives the browser the port metadata. The begin and
complete attrs send graph commands when a user connects ports.
You can render ports as buttons, dots, handles, rows, or icons. The runtime only needs the attrs.
Connect From Server Code
Use LivePanels.Graph.connect/2 when server code creates a connector:
socket =
LivePanels.Graph.connect!(socket,
from: {"extract-1", "output"},
to: {"transform-1", "input"}
)Remove or reconnect connectors through the same owner module:
socket = LivePanels.Graph.disconnect!(socket, connector_id)
socket =
LivePanels.Graph.reconnect!(socket, connector_id,
endpoint: :to,
target: {"load-1", "input"}
)These calls validate graph rules before changing canvas state.
LivePanels permits cycles by default because not every graph is a DAG. If your
application needs DAG semantics, reject the connector command in policy with
LivePanels.Graph.Query.creates_cycle?/3.
Render Connectors
LivePanels.Toolkit.Graph registers the graph extension. With canvas_runtime/1,
connector items render automatically in the world SVG item projection. Do not
add a host connector layer:
<.canvas_runtime {canvas_runtime_attrs(assigns)}>
<:item :let={context}>
<.node context={context} />
</:item>
</.canvas_runtime>Style connectors with the runtime classes and data attrs:
.graph-canvas .livepanels-connector-path {
stroke: currentColor;
stroke-width: 3px;
}
.graph-canvas
[data-livepanels-connector-selection="selected"]
.livepanels-connector-path {
stroke-width: 4px;
}Store route, label, role, marker, and emphasis on connector items:
socket =
LivePanels.Graph.edit!(socket, connector_id,
route: :orthogonal,
label: "rows",
emphasis: :active
)Select And Fit Graphs
Use graph attrs for graph selection controls:
<button {graph_select_all_attrs()}>Select graph</button>
<button {graph_fit_attrs()}>Fit graph</button>
<button {graph_fit_selection_attrs()}>Fit selection</button>Use server helpers when the application drives graph navigation:
socket = LivePanels.Graph.select_all!(socket)
socket = LivePanels.Graph.fit(socket, :graph)
socket = LivePanels.Graph.fit(socket, :selection)Graph selection includes graph connectors as well as items. Ordinary item selection helpers continue to work for item-only selections.
Layout Graph Nodes
Use graph layout when node placement should follow graph structure:
socket = LivePanels.Graph.layout!(socket, :layered)Graph layout applies ordinary canvas placement commands. That means history, persistence, policy, and shared coordination still see item placement changes the normal way.
Persistence, History, Clipboard, And Shared Mode
Connector items live in canvas state. Because of that:
- persistent layouts can include graph connectors;
- undo and redo can reverse graph mutations;
- clipboard can copy selected items and their internal connector items together;
- shared canvases can broadcast accepted graph changes;
- moving an item can refresh connected connector projection.
The application should not keep a separate canonical graph store for runtime connectors. If your product has domain relationships, store those in your app and mirror the runtime connector commands when the canvas should show or edit them.
What LivePanels Owns
LivePanels owns port declarations, connector validation, connector state, selection, projection helpers, graph commands, history/persistence participation, and shared coordination.
Your application owns node design, port design, SVG connector styling, labels, menus, semantic meaning, and any domain records behind the graph.