Add Graph Connections

Copy Markdown View Source

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
end

Ports 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.