# Canvas & Controls

> Background variants, zoom/pan controls, the minimap, and viewport control.

**Site index:** [https://flows.2plot.dev/llms.txt](https://flows.2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 13 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://flows.2plot.dev/sitemap.xml  


---



### 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):



```python
# 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/`](https://github.com/pip-install-python/dash-flows/tree/main/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.



```python
# 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 a `dmc.Select`.
- `backgroundColor` — CSS color for the pattern, wired to a `dmc.ColorInput`.
- `backgroundGap` / `backgroundSize` — spacing and scale of the pattern, driven by `dmc.NumberInput`.
- The callback rebuilds the whole `DashFlows` component 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.



```python
# 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.



```python
# 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*
