Canvas & Controls
Background variants, zoom/pan controls, the minimap, and viewport control.
Overview
Toggle the on-canvas chrome with showBackground, showControls, and showMiniMap, and pick a backgroundVariant of dots, lines, or cross. The viewport can also be driven programmatically from callbacks via viewportAction and fitView.
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/canvas/demo.py
"""Live, callback-free demo for the docs page. Rendered via `.. exec::docs.canvas.demo`."""
import dash_flows
nodes = [{'id': 'a', 'type': 'input', 'data': {'label': 'Source'}, 'position': {'x': 120, 'y': 40}},
{'id': 'b', 'type': 'default', 'data': {'label': 'Transform'}, 'position': {'x': 120, 'y': 180}},
{'id': 'c', 'type': 'output', 'data': {'label': 'Sink'}, 'position': {'x': 120, 'y': 320}}]
edges = [{'id': 'ab', 'source': 'a', 'target': 'b', 'animated': True},
{'id': 'bc', 'source': 'b', 'target': 'c'}]
component = dash_flows.DashFlows(
id="canvas-demo",
nodes=nodes,
edges=edges,
style={'border': '1px solid var(--mantine-color-default-border)',
'borderRadius': '8px',
'height': '440px'},
showControls=True,
showMiniMap=True,
showBackground=True,
backgroundVariant='cross',
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>.
Background Variants
Swap between the dots, lines, and cross background patterns and tweak their color, gap, and size — handy when picking a background that fits your theme.
# File: examples/04_background_variants.py
"""
Example 04: Background Variants
===============================
This example demonstrates the different background patterns available:
- dots: Grid of dots (default)
- lines: Horizontal and vertical lines
- cross: Crosshatch pattern
- Custom colors and sizes
"""
import dash
from dash import html, Input, Output, callback
import dash_flows
import dash_mantine_components as dmc
app = dash.Dash(__name__)
nodes = [
{"id": "1", "type": "default", "data": {"label": "Node A"}, "position": {"x": 100, "y": 100}},
{"id": "2", "type": "default", "data": {"label": "Node B"}, "position": {"x": 300, "y": 100}},
{"id": "3", "type": "default", "data": {"label": "Node C"}, "position": {"x": 200, "y": 250}},
]
edges = [
{"id": "e1", "source": "1", "target": "3"},
{"id": "e2", "source": "2", "target": "3"},
]
app.layout = dmc.MantineProvider([
html.H1("Background Variants Example"),
html.P("Use the controls below to change the background pattern."),
dmc.Group([
dmc.Select(
id="bg-variant",
label="Background Variant",
data=[
{"value": "dots", "label": "Dots"},
{"value": "lines", "label": "Lines"},
{"value": "cross", "label": "Cross"},
],
value="dots",
style={"width": 150},
),
dmc.ColorInput(
id="bg-color",
label="Pattern Color",
value="#e5e7eb",
style={"width": 150},
),
dmc.NumberInput(
id="bg-gap",
label="Gap Size",
value=20,
min=5,
max=50,
style={"width": 100},
),
dmc.NumberInput(
id="bg-size",
label="Pattern Size",
value=1,
min=0.5,
max=5,
step=0.5,
style={"width": 100},
),
], style={"marginBottom": 20}),
html.Div(id="flow-container"),
])
@callback(
Output("flow-container", "children"),
Input("bg-variant", "value"),
Input("bg-color", "value"),
Input("bg-gap", "value"),
Input("bg-size", "value"),
)
def update_background(variant, color, gap, size):
return dash_flows.DashFlows(
id="background-flow",
nodes=nodes,
edges=edges,
style={"height": "500px", "border": "1px solid #ddd"},
fitView=True,
showControls=True,
# Background configuration
backgroundVariant=variant,
backgroundGap=gap,
backgroundSize=size,
backgroundColor=color,
)
if __name__ == "__main__":
app.run(debug=True, port=8053)
:defaultExpanded: false :withExpandedButton: true
How it works
backgroundVariant— one of"dots","lines", or"cross"; controlled here by admc.Select.backgroundColor— CSS color for the pattern, wired to admc.ColorInput.backgroundGap/backgroundSize— spacing and scale of the pattern, driven bydmc.NumberInput.- The callback rebuilds the whole
DashFlowscomponent on every control change rather than patching individual props — simple and fine for background-only settings.
Controls and MiniMap
A larger 5x6 node grid shows off the zoom/pan control bar and the type-colored MiniMap, including position and pannable/zoomable toggles.
# File: examples/05_controls_and_minimap.py
"""
Example 05: Controls and MiniMap
================================
This example demonstrates:
- Zoom controls (zoom in, zoom out, fit view, lock interactivity)
- MiniMap with custom styling
- Control positioning options
"""
import dash
from dash import html, Input, Output, State, callback
import dash_flows
import dash_mantine_components as dmc
app = dash.Dash(__name__)
# Create a larger flow to demonstrate MiniMap utility
nodes = []
edges = []
# Create a grid of nodes
for row in range(5):
for col in range(6):
node_id = f"node-{row}-{col}"
nodes.append({
"id": node_id,
"type": "default",
"data": {"label": f"({row},{col})"},
"position": {"x": col * 200, "y": row * 150},
})
# Connect horizontally
if col > 0:
edges.append({
"id": f"h-{row}-{col}",
"source": f"node-{row}-{col-1}",
"target": node_id,
})
# Connect vertically
if row > 0:
edges.append({
"id": f"v-{row}-{col}",
"source": f"node-{row-1}-{col}",
"target": node_id,
})
app.layout = dmc.MantineProvider([
html.H1("Controls and MiniMap Example"),
html.P("A large flow with zoom controls and minimap navigation."),
dmc.Group([
dmc.Switch(id="show-controls", label="Show Controls", checked=True),
dmc.Switch(id="show-minimap", label="Show MiniMap", checked=True),
dmc.Switch(id="pannable-minimap", label="Pannable MiniMap", checked=True),
dmc.Switch(id="zoomable-minimap", label="Zoomable MiniMap", checked=True),
], style={"marginBottom": 20}),
dmc.Group([
dmc.Select(
id="controls-position",
label="Controls Position",
data=[
{"value": "top-left", "label": "Top Left"},
{"value": "top-right", "label": "Top Right"},
{"value": "bottom-left", "label": "Bottom Left"},
{"value": "bottom-right", "label": "Bottom Right"},
],
value="bottom-left",
style={"width": 150},
),
dmc.Select(
id="minimap-position",
label="MiniMap Position",
data=[
{"value": "top-left", "label": "Top Left"},
{"value": "top-right", "label": "Top Right"},
{"value": "bottom-left", "label": "Bottom Left"},
{"value": "bottom-right", "label": "Bottom Right"},
],
value="bottom-right",
style={"width": 150},
),
], style={"marginBottom": 20}),
html.Div(id="controls-flow-container"),
])
@callback(
Output("controls-flow-container", "children"),
Input("show-controls", "checked"),
Input("show-minimap", "checked"),
Input("pannable-minimap", "checked"),
Input("zoomable-minimap", "checked"),
Input("controls-position", "value"),
Input("minimap-position", "value"),
)
def update_flow(show_controls, show_minimap, pannable, zoomable, ctrl_pos, mm_pos):
return dash_flows.DashFlows(
id="controls-flow",
nodes=nodes,
edges=edges,
style={"height": "600px", "border": "1px solid #ddd"},
fitView=True,
# Controls configuration
showControls=show_controls,
controlsPosition=ctrl_pos,
controlsShowZoom=True,
controlsShowFitView=True,
controlsShowInteractive=True,
# MiniMap configuration
showMiniMap=show_minimap,
miniMapPosition=mm_pos,
miniMapPannable=pannable,
miniMapZoomable=zoomable,
)
if __name__ == "__main__":
app.run(debug=True, port=8054)
:defaultExpanded: false :withExpandedButton: true
How it works
showControls/controlsPosition— toggle and place the zoom-in/zoom-out/fit-view/lock control bar.controlsShowZoom,controlsShowFitView,controlsShowInteractive— enable individual buttons within the control bar.showMiniMap/miniMapPosition— toggle and place the minimap overlay.miniMapPannable/miniMapZoomable— let the minimap itself be dragged or scrolled to navigate large graphs.- With dozens of nodes, the minimap becomes the fastest way to orient yourself after zooming in.
Viewport Controls
Drive the camera from Dash callbacks: fit view, zoom in/out, reset zoom, and pan to named positions, plus switches to lock zooming, panning, or node dragging.
# File: examples/09_viewport_controls.py
"""
Example 09: Viewport Controls and Fit View
==========================================
This example demonstrates viewport manipulation:
- Zoom in/out programmatically
- Pan to specific positions
- Fit view to content
- Lock/unlock viewport
- Get/set viewport state
"""
import dash
from dash import html, Input, Output, State, callback, ctx
import dash_flows
import dash_mantine_components as dmc
import json
app = dash.Dash(__name__, suppress_callback_exceptions=True)
# Create nodes spread across the canvas
nodes = [
{"id": "1", "type": "default", "data": {"label": "Top Left"}, "position": {"x": 0, "y": 0}},
{"id": "2", "type": "default", "data": {"label": "Top Right"}, "position": {"x": 800, "y": 0}},
{"id": "3", "type": "default", "data": {"label": "Center"}, "position": {"x": 400, "y": 250}},
{"id": "4", "type": "default", "data": {"label": "Bottom Left"}, "position": {"x": 0, "y": 500}},
{"id": "5", "type": "default", "data": {"label": "Bottom Right"}, "position": {"x": 800, "y": 500}},
]
edges = [
{"id": "e1-3", "source": "1", "target": "3"},
{"id": "e2-3", "source": "2", "target": "3"},
{"id": "e3-4", "source": "3", "target": "4"},
{"id": "e3-5", "source": "3", "target": "5"},
]
app.layout = dmc.MantineProvider([
html.H1("Viewport Controls Example"),
html.P("Control the viewport programmatically."),
dmc.Group([
dmc.Button("Fit View", id="btn-fit-view", variant="filled"),
dmc.Button("Zoom In", id="btn-zoom-in", variant="outline"),
dmc.Button("Zoom Out", id="btn-zoom-out", variant="outline"),
dmc.Button("Reset Zoom", id="btn-reset-zoom", variant="outline"),
], style={"marginBottom": 10}),
dmc.Group([
dmc.Button("Pan to Top Left", id="btn-pan-tl", variant="light", size="sm"),
dmc.Button("Pan to Center", id="btn-pan-center", variant="light", size="sm"),
dmc.Button("Pan to Bottom Right", id="btn-pan-br", variant="light", size="sm"),
], style={"marginBottom": 10}),
dmc.Group([
dmc.Switch(id="lock-zoom", label="Lock Zoom", checked=False),
dmc.Switch(id="lock-pan", label="Lock Pan", checked=False),
dmc.Switch(id="lock-drag", label="Lock Node Drag", checked=False),
], style={"marginBottom": 20}),
html.Div(id="flow-viewport-container"),
dmc.Space(h=20),
dmc.Paper([
dmc.Text("Current Viewport State:", fw=600),
html.Pre(id="viewport-state", style={"fontSize": "11px"}),
], p="md", withBorder=True),
])
@callback(
Output("flow-viewport-container", "children"),
Input("lock-zoom", "checked"),
Input("lock-pan", "checked"),
Input("lock-drag", "checked"),
)
def update_flow_locks(lock_zoom, lock_pan, lock_drag):
return dash_flows.DashFlows(
id="viewport-flow",
nodes=nodes,
edges=edges,
style={"height": "400px", "border": "1px solid #ddd"},
fitView=True,
showControls=True,
# Viewport lock settings
zoomOnScroll=not lock_zoom,
zoomOnPinch=not lock_zoom,
zoomOnDoubleClick=not lock_zoom,
panOnDrag=not lock_pan,
panOnScroll=not lock_pan,
nodesDraggable=not lock_drag,
# Zoom limits
minZoom=0.1,
maxZoom=4,
)
# Clientside callback for viewport manipulation
app.clientside_callback(
"""
function(fitClicks, zoomInClicks, zoomOutClicks, resetClicks, panTLClicks, panCenterClicks, panBRClicks) {
const triggered = dash_clientside.callback_context.triggered;
if (!triggered || triggered.length === 0) {
return window.dash_clientside.no_update;
}
const btnId = triggered[0].prop_id.split('.')[0];
// These would need to be handled by the component itself
// This is a placeholder to show the concept
switch(btnId) {
case 'btn-fit-view':
return {'action': 'fitView', 'options': {padding: 0.2}};
case 'btn-zoom-in':
return {'action': 'zoomIn', 'options': {}};
case 'btn-zoom-out':
return {'action': 'zoomOut', 'options': {}};
case 'btn-reset-zoom':
return {'action': 'setZoom', 'zoom': 1, 'options': {}};
case 'btn-pan-tl':
return {'action': 'setCenter', 'x': 0, 'y': 0, 'options': {zoom: 1, duration: 500}};
case 'btn-pan-center':
return {'action': 'setCenter', 'x': 450, 'y': 300, 'options': {zoom: 1, duration: 500}};
case 'btn-pan-br':
return {'action': 'setCenter', 'x': 900, 'y': 550, 'options': {zoom: 1, duration: 500}};
default:
return window.dash_clientside.no_update;
}
}
""",
Output("viewport-flow", "viewportAction", allow_duplicate=True),
Input("btn-fit-view", "n_clicks"),
Input("btn-zoom-in", "n_clicks"),
Input("btn-zoom-out", "n_clicks"),
Input("btn-reset-zoom", "n_clicks"),
Input("btn-pan-tl", "n_clicks"),
Input("btn-pan-center", "n_clicks"),
Input("btn-pan-br", "n_clicks"),
prevent_initial_call=True,
)
@callback(
Output("viewport-state", "children"),
Input("viewport-flow", "viewport"),
)
def display_viewport(viewport):
if not viewport:
return "Viewport not available"
return json.dumps(viewport, indent=2)
if __name__ == "__main__":
app.run(debug=True, port=8068)
:defaultExpanded: false :withExpandedButton: true
How it works
viewportAction— an input prop you set from a callback to imperatively move the camera, e.g.{"action": "fitView", "options": {"padding": 0.2}}or{"action": "setCenter", "x": 450, "y": 300, "options": {"zoom": 1, "duration": 500}}.- The buttons here use a clientside callback (
clientside_callback) so the viewport reacts instantly without a server round-trip. zoomOnScroll,zoomOnPinch,zoomOnDoubleClick,panOnDrag,panOnScroll,nodesDraggable— booleans for locking specific interactions, toggled by the switches above.minZoom/maxZoom— clamp how far in/out the viewport can go.viewport— a read-only output prop reporting the current{x, y, zoom}, rendered live below the canvas.
Source: /canvas
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:
- /canvas/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt