Layout & Handles
Automatic ELK layouts, smart handles, helper lines, and animated layout transitions.
Overview
Arrange graphs automatically with ELK by passing layoutOptions, and animate the transition with animateLayout. smartHandles auto-routes edges to the closest node side, while helperLines shows alignment guides as you drag.
Live demo
Drag the nodes, pan, and zoom — the canvas below is a real, running DashFlows component (no callbacks, just the rendered graph):
# File: docs/layout/demo.py
"""Live, callback-free demo for the docs page. Rendered via `.. exec::docs.layout.demo`."""
import dash_flows
nodes = [{'id': 'root', 'type': 'input', 'data': {'label': 'Root'}, 'position': {'x': 200, 'y': 20}},
{'id': 'l1', 'type': 'default', 'data': {'label': 'Branch 1'}, 'position': {'x': 60, 'y': 160}},
{'id': 'l2', 'type': 'default', 'data': {'label': 'Branch 2'}, 'position': {'x': 340, 'y': 160}},
{'id': 'l3', 'type': 'output', 'data': {'label': 'Leaf A'}, 'position': {'x': 20, 'y': 300}},
{'id': 'l4', 'type': 'output', 'data': {'label': 'Leaf B'}, 'position': {'x': 160, 'y': 300}},
{'id': 'l5', 'type': 'output', 'data': {'label': 'Leaf C'}, 'position': {'x': 340, 'y': 300}}]
edges = [{'id': 'r1', 'source': 'root', 'target': 'l1'},
{'id': 'r2', 'source': 'root', 'target': 'l2'},
{'id': '13', 'source': 'l1', 'target': 'l3'},
{'id': '14', 'source': 'l1', 'target': 'l4'},
{'id': '25', 'source': 'l2', 'target': 'l5'}]
component = dash_flows.DashFlows(
id="layout-demo",
nodes=nodes,
edges=edges,
style={'border': '1px solid var(--mantine-color-default-border)',
'borderRadius': '8px',
'height': '440px'},
showControls=True,
showMiniMap=True,
smartHandles=True,
fitView=True,
colorMode='system',
)
:defaultExpanded: false :withExpandedButton: true
Examples
Each example below is a complete, runnable Dash app from the examples/ folder. Run any of them with python examples/<file>.
ELK Layouts
Automatic graph layout with the ELK engine (layered, tree, radial, force, and stress). Pick an algorithm, click Apply, and watch the whole graph rearrange — useful whenever nodes are added dynamically and you don't want to hand-place positions.
# File: examples/12_elk_layouts.py
"""
Example 12: ELK Automatic Layouts
=================================
This example demonstrates automatic graph layouts using ELK:
- Layered (hierarchical) layout
- Force-directed layout
- Radial layout
- Different layout directions
"""
import dash
from dash import html, Input, Output, State, callback
import dash_flows
import dash_mantine_components as dmc
import json
app = dash.Dash(__name__)
# Create a more complex graph to show layout differences
initial_nodes = [
{"id": "1", "type": "input", "data": {"label": "Start"}, "position": {"x": 0, "y": 0}},
{"id": "2", "type": "default", "data": {"label": "Step A"}, "position": {"x": 0, "y": 0}},
{"id": "3", "type": "default", "data": {"label": "Step B"}, "position": {"x": 0, "y": 0}},
{"id": "4", "type": "default", "data": {"label": "Step C"}, "position": {"x": 0, "y": 0}},
{"id": "5", "type": "default", "data": {"label": "Step D"}, "position": {"x": 0, "y": 0}},
{"id": "6", "type": "default", "data": {"label": "Step E"}, "position": {"x": 0, "y": 0}},
{"id": "7", "type": "default", "data": {"label": "Step F"}, "position": {"x": 0, "y": 0}},
{"id": "8", "type": "output", "data": {"label": "End"}, "position": {"x": 0, "y": 0}},
]
initial_edges = [
{"id": "e1-2", "source": "1", "target": "2"},
{"id": "e1-3", "source": "1", "target": "3"},
{"id": "e2-4", "source": "2", "target": "4"},
{"id": "e2-5", "source": "2", "target": "5"},
{"id": "e3-5", "source": "3", "target": "5"},
{"id": "e3-6", "source": "3", "target": "6"},
{"id": "e4-7", "source": "4", "target": "7"},
{"id": "e5-7", "source": "5", "target": "7"},
{"id": "e6-7", "source": "6", "target": "7"},
{"id": "e7-8", "source": "7", "target": "8"},
]
layout_presets = {
"layered-down": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.spacing.nodeNode": 50,
"elk.layered.spacing.nodeNodeBetweenLayers": 80,
},
"layered-right": {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.spacing.nodeNode": 50,
"elk.layered.spacing.nodeNodeBetweenLayers": 120,
},
"layered-up": {
"elk.algorithm": "layered",
"elk.direction": "UP",
"elk.spacing.nodeNode": 50,
},
"layered-left": {
"elk.algorithm": "layered",
"elk.direction": "LEFT",
"elk.spacing.nodeNode": 50,
},
"force": {
"elk.algorithm": "org.eclipse.elk.force",
"elk.force.iterations": 300,
"elk.spacing.nodeNode": 80,
},
"radial": {
"elk.algorithm": "org.eclipse.elk.radial",
"elk.radial.radius": 150,
},
"stress": {
"elk.algorithm": "org.eclipse.elk.stress",
"elk.spacing.nodeNode": 100,
},
}
app.layout = dmc.MantineProvider([
html.H1("ELK Automatic Layouts Example"),
html.P("Apply different automatic layout algorithms to arrange nodes."),
dmc.Group([
dmc.Select(
id="layout-select",
label="Select Layout Algorithm",
data=[
{"value": "layered-down", "label": "Layered (Top to Bottom)"},
{"value": "layered-right", "label": "Layered (Left to Right)"},
{"value": "layered-up", "label": "Layered (Bottom to Top)"},
{"value": "layered-left", "label": "Layered (Right to Left)"},
{"value": "force", "label": "Force-Directed"},
{"value": "radial", "label": "Radial"},
{"value": "stress", "label": "Stress"},
],
value="layered-down",
style={"width": 250},
),
dmc.Button("Apply Layout", id="apply-layout-btn", variant="filled"),
dmc.Button("Reset Positions", id="reset-btn", variant="outline"),
], style={"marginBottom": 20}),
dash_flows.DashFlows(
id="elk-flow",
nodes=initial_nodes,
edges=initial_edges,
style={"height": "500px", "border": "1px solid #ddd"},
fitView=True,
showControls=True,
showMiniMap=True,
layoutOptions=json.dumps(layout_presets["layered-down"]),
),
dmc.Space(h=20),
dmc.Paper([
dmc.Text("Current Layout Options:", fw=600),
html.Pre(id="layout-options-display", style={"fontSize": "11px"}),
], p="md", withBorder=True),
dmc.Space(h=20),
dmc.Alert([
dmc.Text("About ELK Layouts:", fw=600),
dmc.List([
dmc.ListItem([dmc.Text("Layered", fw=600), ": Best for hierarchical/directed graphs. Supports multiple directions."]),
dmc.ListItem([dmc.Text("Force-Directed", fw=600), ": Simulates physical forces for organic layouts."]),
dmc.ListItem([dmc.Text("Radial", fw=600), ": Arranges nodes in concentric circles from a root."]),
dmc.ListItem([dmc.Text("Stress", fw=600), ": Minimizes stress in edge lengths for balanced layouts."]),
], size="sm"),
], color="gray", variant="light"),
])
@callback(
Output("elk-flow", "layoutOptions"),
Output("layout-options-display", "children"),
Input("apply-layout-btn", "n_clicks"),
State("layout-select", "value"),
prevent_initial_call=True,
)
def apply_layout(n_clicks, layout_type):
options = layout_presets.get(layout_type, layout_presets["layered-down"])
return json.dumps(options), json.dumps(options, indent=2)
@callback(
Output("elk-flow", "nodes"),
Input("reset-btn", "n_clicks"),
prevent_initial_call=True,
)
def reset_positions(n_clicks):
# Reset to initial scattered positions
return initial_nodes
if __name__ == "__main__":
app.run(debug=True, port=8063)
:defaultExpanded: false :withExpandedButton: true
How it works
layoutOptions— a JSON string of ELK options (elk.algorithm,elk.direction, spacing keys); setting it from a callback re-runs the layout engine and repositions every node.elk.algorithmvalues shown here:layered(withelk.directionofDOWN/RIGHT/UP/LEFT),org.eclipse.elk.force,org.eclipse.elk.radial, andorg.eclipse.elk.stress.- All nodes start at
{"x": 0, "y": 0}— ELK computes real positions on first render sincelayoutOptionsis set in the initial component props. Output("ex12-elk-flow", "nodes")on the Reset button — pushes the nodes back to their original (unlaid-out) positions so you can re-apply a layout and see the change.- The full example additionally renders the active
layoutOptionsJSON in aPreblock for inspection, alongside a short reference on when to use each algorithm.
Smart Handles
Auto-route edges to the nearest node side by rendering handles on all four sides, instead of forcing every connection through fixed top/bottom handles. Toggle it off to see the difference on a graph laid out in a loose, non-linear grid.
# File: examples/24_smart_handles.py
"""
Example 24: Smart Handle Positioning
=====================================
Demonstrates the smartHandles feature which automatically positions edge
connection points to the closest side of each node, preventing edges from
wrapping around nodes unnecessarily.
Also shows manual handle positioning via sourcePosition/targetPosition in node data.
"""
import dash
from dash import html, dcc, callback, Input, Output, State
import dash_flows
app = dash.Dash(__name__)
# --- Nodes arranged in various positions to show smart routing ---
smart_nodes = [
# Top-left cluster
{
"id": "source-1",
"type": "input",
"data": {"label": "Data Source", "sublabel": "API"},
"position": {"x": 50, "y": 50},
},
# Top-right
{
"id": "process-1",
"type": "default",
"data": {"label": "Validate", "sublabel": "Schema Check"},
"position": {"x": 400, "y": 30},
},
# Center
{
"id": "process-2",
"type": "default",
"data": {"label": "Transform", "sublabel": "Map & Filter"},
"position": {"x": 250, "y": 200},
},
# Bottom-left
{
"id": "process-3",
"type": "default",
"data": {"label": "Aggregate", "sublabel": "Group By"},
"position": {"x": 50, "y": 350},
},
# Far right - mid
{
"id": "process-4",
"type": "default",
"data": {"label": "Enrich", "sublabel": "Join External"},
"position": {"x": 500, "y": 220},
},
# Bottom-right
{
"id": "output-1",
"type": "output",
"data": {"label": "Dashboard", "sublabel": "Visualization"},
"position": {"x": 400, "y": 400},
},
# Bottom center
{
"id": "output-2",
"type": "output",
"data": {"label": "Database", "sublabel": "PostgreSQL"},
"position": {"x": 200, "y": 430},
},
]
smart_edges = [
# Horizontal: left -> right (should use right/left handles)
{"id": "e1", "source": "source-1", "target": "process-1", "type": "smoothstep"},
# Diagonal: top-left -> center (should pick best side)
{"id": "e2", "source": "source-1", "target": "process-2", "type": "smoothstep"},
# Right -> far right (horizontal, should use right/left)
{"id": "e3", "source": "process-1", "target": "process-4", "type": "smoothstep"},
# Center -> bottom-left (should use left/bottom or bottom/top)
{"id": "e4", "source": "process-2", "target": "process-3", "type": "smoothstep"},
# Center -> far right (horizontal)
{"id": "e5", "source": "process-2", "target": "process-4", "type": "smoothstep"},
# Center -> bottom-right output
{"id": "e6", "source": "process-2", "target": "output-1", "type": "smoothstep"},
# Far right -> bottom-right output
{"id": "e7", "source": "process-4", "target": "output-1", "type": "smoothstep"},
# Bottom-left -> bottom-center output
{"id": "e8", "source": "process-3", "target": "output-2", "type": "smoothstep"},
]
# --- Manual handle positioning example ---
manual_nodes = [
# Horizontal flow: left to right
{
"id": "h-start",
"type": "input",
"data": {
"label": "Start",
"sourcePosition": "right", # Manual: source handle on right side
},
"position": {"x": 0, "y": 100},
},
{
"id": "h-middle",
"type": "default",
"data": {
"label": "Process",
"sourcePosition": "right",
"targetPosition": "left", # Manual: target handle on left side
},
"position": {"x": 250, "y": 100},
},
{
"id": "h-end",
"type": "output",
"data": {
"label": "End",
"targetPosition": "left",
},
"position": {"x": 500, "y": 100},
},
]
manual_edges = [
{"id": "he1", "source": "h-start", "target": "h-middle", "type": "smoothstep"},
{"id": "he2", "source": "h-middle", "target": "h-end", "type": "smoothstep"},
]
app.layout = html.Div([
html.H1("Smart Handle Positioning"),
html.P(
"The smartHandles prop automatically routes edges to the closest side of each node, "
"eliminating unnecessary wrapping and overlapping."
),
# Toggle
html.Div([
html.H3("Automatic Smart Handles"),
html.P(
"Toggle smartHandles to see the difference. Without it, all edges connect "
"from bottom (source) to top (target), causing messy routing on non-vertical layouts."
),
dcc.Checklist(
id="smart-toggle",
options=[{"label": "Enable smartHandles", "value": "on"}],
value=["on"],
style={"fontSize": "14px", "marginBottom": "12px"},
),
html.Div(id="smart-status", style={"fontSize": "12px", "color": "#666", "marginBottom": "8px"}),
dash_flows.DashFlows(
id="smart-flow",
nodes=smart_nodes,
edges=smart_edges,
smartHandles=True,
style={"height": "550px", "border": "1px solid #ddd"},
fitView=True,
fitViewOptions={"padding": 0.15},
showControls=True,
showMiniMap=True,
colorScheme="ocean",
),
], style={"marginBottom": "40px"}),
html.Hr(),
# Manual positioning example
html.Div([
html.H3("Manual Handle Positioning"),
html.P(
"You can also manually set sourcePosition and targetPosition in node data "
"for precise control. This example creates a left-to-right horizontal flow."
),
html.Pre(
'{"label": "Process", "sourcePosition": "right", "targetPosition": "left"}',
style={
"background": "#f5f5f5",
"padding": "8px 12px",
"borderRadius": "4px",
"fontSize": "12px",
},
),
dash_flows.DashFlows(
id="manual-flow",
nodes=manual_nodes,
edges=manual_edges,
style={"height": "250px", "border": "1px solid #ddd"},
fitView=True,
fitViewOptions={"padding": 0.3},
showControls=True,
showBackground=True,
),
]),
])
@callback(
Output("smart-flow", "smartHandles"),
Output("smart-status", "children"),
Input("smart-toggle", "value"),
)
def toggle_smart_handles(value):
enabled = "on" in (value or [])
status = "smartHandles=True (edges route to closest side)" if enabled else "smartHandles=False (default top/bottom handles)"
return enabled, status
if __name__ == "__main__":
app.run(debug=True, port=8024)
:defaultExpanded: false :withExpandedButton: true
How it works
smartHandles=True— renders connection handles on all four sides of every node and picks whichever pair is closest for each edge, avoiding backtracking loops.- With
smartHandlesoff, all edges fall back to the default single top (target) / bottom (source) handle pair, which looks messy on diagonally arranged nodes. Output("ex24-smart-flow", "smartHandles")— flips the prop live from adcc.Checklist, so you can compare routing with and without it on the same graph.data.sourcePosition/data.targetPosition— the full example also shows manual handle placement (e.g."right"/"left") for a strict left-to-right flow when you want precise control instead of automatic routing.colorScheme="ocean"— swaps in the built-in ocean palette for this demo.
Helper Lines
Alignment guides that appear as you drag a node near another node's edge, snapping it into place — the same kind of guide you'd get in a design tool like Figma.
# File: examples/27_helper_lines.py
"""
Example 27: Helper Lines (Alignment Guides)
=============================================
Demonstrates helper lines that appear as visual guides when dragging a node
near another node's edge. Helps users align nodes precisely.
"""
import dash
from dash import html, dcc, callback, Input, Output
import dash_flows
app = dash.Dash(__name__)
nodes = [
{
"id": "a",
"type": "input",
"data": {"label": "Start"},
"position": {"x": 50, "y": 50},
},
{
"id": "b",
"type": "default",
"data": {"label": "Process A"},
"position": {"x": 300, "y": 50},
},
{
"id": "c",
"type": "default",
"data": {"label": "Process B"},
"position": {"x": 300, "y": 200},
},
{
"id": "d",
"type": "default",
"data": {"label": "Process C"},
"position": {"x": 550, "y": 125},
},
{
"id": "e",
"type": "output",
"data": {"label": "End"},
"position": {"x": 550, "y": 300},
},
]
edges = [
{"id": "e-ab", "source": "a", "target": "b", "type": "smoothstep"},
{"id": "e-ac", "source": "a", "target": "c", "type": "smoothstep"},
{"id": "e-bd", "source": "b", "target": "d", "type": "smoothstep"},
{"id": "e-cd", "source": "c", "target": "d", "type": "smoothstep"},
{"id": "e-de", "source": "d", "target": "e", "type": "smoothstep"},
]
app.layout = html.Div([
html.H1("Helper Lines (Alignment Guides)"),
html.P(
"Drag any node and blue alignment guides will appear when it aligns "
"with another node's edge. The node will snap to the alignment."
),
html.Div([
dcc.Checklist(
id="helper-toggle",
options=[{"label": " Enable helper lines", "value": "on"}],
value=["on"],
style={"fontSize": "14px", "marginBottom": "8px"},
),
html.Label("Snap threshold (px): ", style={"fontSize": "14px"}),
dcc.Slider(
id="threshold-slider",
min=1, max=20, step=1, value=5,
marks={1: "1", 5: "5", 10: "10", 20: "20"},
),
], style={"marginBottom": "12px", "maxWidth": "400px"}),
dash_flows.DashFlows(
id="helper-flow",
nodes=nodes,
edges=edges,
helperLines=True,
helperLineThreshold=5,
smartHandles=True,
style={"height": "550px", "border": "1px solid #ddd"},
fitView=True,
fitViewOptions={"padding": 0.2},
showControls=True,
showBackground=True,
backgroundVariant="dots",
),
])
@callback(
Output("helper-flow", "helperLines"),
Output("helper-flow", "helperLineThreshold"),
Input("helper-toggle", "value"),
Input("threshold-slider", "value"),
)
def update_helper_settings(toggle, threshold):
enabled = "on" in (toggle or [])
return enabled, threshold or 5
if __name__ == "__main__":
app.run(debug=True, port=8027)
:defaultExpanded: false :withExpandedButton: true
How it works
helperLines=True— enables the alignment-guide overlay; drag any node near another node's top/bottom/left/right edge or center line to see a blue guide and snap.helperLineThreshold— the snap distance in pixels; wired here to adcc.Slider(1–20px) so you can feel the difference between a tight and loose snap radius live.smartHandles=True— combined with helper lines so edges keep routing cleanly even as you nudge nodes into alignment.Output("ex27-helper-flow", "helperLines")/Output("ex27-helper-flow", "helperLineThreshold")— both driven by the same callback, so the checkbox and slider update the running flow instantly.
Animated Layout
Smoothly interpolate node positions when a new ELK layout is applied, instead of nodes jumping straight to their new spot. Click between the layout buttons to see the pipeline graph animate.
# File: examples/31_animated_layout.py
"""
Example 31: Animated Layout Transitions
========================================
Demonstrates smooth animated transitions when switching between
different ELK layout algorithms. Nodes interpolate from their current
positions to the new layout positions using an ease-out cubic curve.
"""
import json
import dash
from dash import html, dcc, callback, Input, Output, State
import dash_flows
app = dash.Dash(__name__)
# Create a pipeline-like graph
nodes = [
{"id": "src-1", "type": "input", "data": {"label": "Source A"}, "position": {"x": 0, "y": 0}},
{"id": "src-2", "type": "input", "data": {"label": "Source B"}, "position": {"x": 0, "y": 100}},
{"id": "proc-1", "type": "default", "data": {"label": "Validate"}, "position": {"x": 200, "y": 0}},
{"id": "proc-2", "type": "default", "data": {"label": "Transform"}, "position": {"x": 200, "y": 100}},
{"id": "proc-3", "type": "default", "data": {"label": "Enrich"}, "position": {"x": 200, "y": 200}},
{"id": "merge", "type": "default", "data": {"label": "Merge"}, "position": {"x": 400, "y": 100}},
{"id": "filter", "type": "default", "data": {"label": "Filter"}, "position": {"x": 600, "y": 50}},
{"id": "agg", "type": "default", "data": {"label": "Aggregate"}, "position": {"x": 600, "y": 150}},
{"id": "out-1", "type": "output", "data": {"label": "Dashboard"}, "position": {"x": 800, "y": 50}},
{"id": "out-2", "type": "output", "data": {"label": "Report"}, "position": {"x": 800, "y": 150}},
]
edges = [
{"id": "e1", "source": "src-1", "target": "proc-1"},
{"id": "e2", "source": "src-1", "target": "proc-2"},
{"id": "e3", "source": "src-2", "target": "proc-2"},
{"id": "e4", "source": "src-2", "target": "proc-3"},
{"id": "e5", "source": "proc-1", "target": "merge"},
{"id": "e6", "source": "proc-2", "target": "merge"},
{"id": "e7", "source": "proc-3", "target": "merge"},
{"id": "e8", "source": "merge", "target": "filter"},
{"id": "e9", "source": "merge", "target": "agg"},
{"id": "e10", "source": "filter", "target": "out-1"},
{"id": "e11", "source": "agg", "target": "out-2"},
]
# Layout presets
layouts = {
"layered-lr": {
"label": "← → Layered (LR)",
"options": json.dumps({
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.spacing.nodeNode": "50",
"elk.layered.spacing.nodeNodeBetweenLayers": "80",
}),
},
"layered-tb": {
"label": "↓ Layered (TB)",
"options": json.dumps({
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.spacing.nodeNode": "50",
"elk.layered.spacing.nodeNodeBetweenLayers": "80",
}),
},
"force": {
"label": "⚛ Force",
"options": json.dumps({
"elk.algorithm": "force",
"elk.spacing.nodeNode": "80",
"elk.force.iterations": "300",
}),
},
"radial": {
"label": "◎ Radial",
"options": json.dumps({
"elk.algorithm": "radial",
"elk.spacing.nodeNode": "60",
}),
},
}
_btn_base = {
"padding": "8px 16px", "border": "1px solid #ddd",
"borderRadius": "6px", "cursor": "pointer",
"fontSize": "13px", "fontWeight": "500", "transition": "all 0.15s",
}
_btn_active = {**_btn_base, "background": "#3b82f6", "color": "white", "borderColor": "#3b82f6"}
_btn_idle = {**_btn_base, "background": "#fff", "color": "#374151"}
app.layout = html.Div([
html.H2("Animated Layout Transitions"),
html.P(
"Click a layout to animate all nodes to their new positions. "
"The viewport auto-fits after each transition.",
style={"color": "#666"},
),
html.Div([
html.Button(info["label"], id=f"btn-{key}", style=_btn_idle)
for key, info in layouts.items()
] + [
html.Span(id="active-layout-label", style={
"padding": "8px 14px", "background": "#f0f0f0",
"borderRadius": "6px", "fontSize": "12px", "color": "#555",
}),
], style={"display": "flex", "gap": "8px", "alignItems": "center", "marginBottom": "12px"}),
dcc.Store(id="active-layout-store", data=""),
dash_flows.DashFlows(
id="animated-layout-flow",
nodes=nodes,
edges=edges,
fitView=True,
animateLayout=True,
animateLayoutDuration=500,
showControls=True,
showMiniMap=True,
showBackground=True,
style={"height": "550px"},
),
], style={"padding": "20px"})
@callback(
Output("animated-layout-flow", "layoutOptions"),
Output("animated-layout-flow", "viewportAction"),
Output("active-layout-store", "data"),
Output("active-layout-label", "children"),
[Input(f"btn-{key}", "n_clicks") for key in layouts],
prevent_initial_call=True,
)
def apply_layout(*_clicks):
from dash import ctx
triggered = ctx.triggered_id
if not triggered:
return dash.no_update, dash.no_update, dash.no_update, dash.no_update
# e.g. "btn-layered-lr" → "layered-lr"
key = triggered[len("btn-"):]
if key not in layouts:
return dash.no_update, dash.no_update, dash.no_update, dash.no_update
# Fit view after layout animation completes
viewport_action = {"action": "fitView", "duration": 300, "delay": 550}
return (
layouts[key]["options"],
viewport_action,
key,
f"Layout: {layouts[key]['label']}",
)
# Highlight the active layout button
for key in layouts:
@callback(
Output(f"btn-{key}", "style"),
Input("active-layout-store", "data"),
)
def update_btn_style(active, _key=key):
return _btn_active if active == _key else _btn_idle
if __name__ == "__main__":
app.run(debug=True, port=8031)
:defaultExpanded: false :withExpandedButton: true
How it works
animateLayout=True— turns on eased position transitions wheneverlayoutOptionschanges, instead of an instant re-layout.animateLayoutDuration=500— the transition length in milliseconds; nodes interpolate along an ease-out cubic curve.layoutOptions— swapped between four ELK presets (layeredRIGHT,layeredDOWN,force,radial) by the layout buttons.viewportAction={"action": "fitView", "duration": 300, "delay": 550}— re-fits the viewport just after the layout animation finishes, so the newly arranged graph stays centered.dcc.Store+ctx.triggered_id— tracks which layout button was last clicked so its style can be highlighted as "active" via a second callback.
Source: /layout
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /layout/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt