native v2 drawing/editing toolbar with full dash-leaflet-style API parity.

Draw & Edit

native v2 drawing/editing toolbar with full dash-leaflet-style API parity.


Overview

Mirrors dash-leaflet's EditControl prop shape (draw, edit, drawToolbar, editToolbar, action) so callbacks compose the same way: bump n_clicks to dispatch from Python; read action to react to any change. The contextual sub-toolbar (Finish / Delete last point / Cancel during draw; Save / Cancel during edit; Clear all during remove) appears alongside the icon strip while a tool/mode is active. The Edit section appears only when shapes exist.

Live demo

The shape

dl2.EditControl pattern (dash-leaflet API parity)

# File: docs/edit-control/example.py  (region: map)

dl2.Map(
    id="ec-map",
    center=TORONTO.center,
    zoom=12,
    style={"height": "62vh"},
    children=[
        dl2.TileLayer(
            id="ec-tile", **TILES.kwargs("light")
        ),
        dl2.EditControl(
            id="ec",
            position="topleft",
            shapeOptions={
                "color": "#2f9e44",
                "weight": 3,
                "fillOpacity": 0.2,
            },
        ),
    ],
),

Source

# File: docs/edit-control/example.py

"""
Edit Control — native v2 drawing/editing toolbar with full dash-leaflet-style API parity.

Mirrors dash-leaflet's EditControl prop shape (`draw`, `edit`, `drawToolbar`, `editToolbar`,
`action`) so callbacks compose the same way: bump n_clicks to dispatch from Python; read
`action` to react to any change. The contextual sub-toolbar (Finish / Delete last point /
Cancel during draw; Save / Cancel during edit; Clear all during remove) appears alongside
the icon strip while a tool/mode is active. The Edit section appears only when shapes exist.
"""

import json

import dash
import dash_leaflet2 as dl2
import dash_mantine_components as dmc
from dash import Input, Output, State, callback, ctx
from dl2_tiles import TRANSIT, register_theme_swap
from dl2_locations import TORONTO
from dl2_shared import info_panel

# Basemap pair for this page — dl2_tiles owns the light/dark wiring.
TILES = TRANSIT

TILE_URL = TILES.url("light")
ATTR = TILES.attribution()



def _btn(label, _id, color="gray"):
    return dmc.Button(label, id=_id, size="xs", variant="light", color=color)


component = dmc.Stack(
    [
        dmc.Grid(
            [
                dmc.GridCol(
                    dmc.Paper(
                        # region map
                        dl2.Map(
                            id="ec-map",
                            center=TORONTO.center,
                            zoom=12,
                            style={"height": "62vh"},
                            children=[
                                dl2.TileLayer(
                                    id="ec-tile", **TILES.kwargs("light")
                                ),
                                dl2.EditControl(
                                    id="ec",
                                    position="topleft",
                                    shapeOptions={
                                        "color": "#2f9e44",
                                        "weight": 3,
                                        "fillOpacity": 0.2,
                                    },
                                ),
                            ],
                        ),
                        # endregion
                        shadow="sm",
                        radius="md",
                        withBorder=True,
                        style={"overflow": "hidden"},
                    ),
                    span=8,
                ),
                dmc.GridCol(
                    dmc.Stack(
                        [
                            info_panel(
                                "Counter / last action",
                                dmc.Code(
                                    id="ec-status",
                                    block=True,
                                    style={
                                        "minHeight": "80px",
                                        "whiteSpace": "pre-wrap",
                                    },
                                ),
                            ),
                            info_panel(
                                "action (single Input for 'anything changed')",
                                dmc.Code(id="ec-action", block=True),
                            ),
                            info_panel(
                                "Python → control",
                                dmc.Stack(
                                    [
                                        dmc.Text(
                                            "Start a draw tool from Python:",
                                            size="sm",
                                            c="dimmed",
                                        ),
                                        dmc.Group(
                                            [
                                                _btn(
                                                    "Marker", "ec-draw-marker", "green"
                                                ),
                                                _btn(
                                                    "Polyline",
                                                    "ec-draw-polyline",
                                                    "green",
                                                ),
                                                _btn(
                                                    "Polygon",
                                                    "ec-draw-polygon",
                                                    "green",
                                                ),
                                                _btn(
                                                    "Rectangle",
                                                    "ec-draw-rectangle",
                                                    "green",
                                                ),
                                            ]
                                        ),
                                        dmc.Text(
                                            "Dispatch an action on the active tool:",
                                            size="sm",
                                            c="dimmed",
                                            mt="xs",
                                        ),
                                        dmc.Group(
                                            [
                                                _btn(
                                                    "Finish", "ec-act-finish", "green"
                                                ),
                                                _btn(
                                                    "Delete last point",
                                                    "ec-act-del",
                                                    "yellow",
                                                ),
                                                _btn("Cancel", "ec-act-cancel", "gray"),
                                            ]
                                        ),
                                        dmc.Text(
                                            "Edit / Remove:",
                                            size="sm",
                                            c="dimmed",
                                            mt="xs",
                                        ),
                                        dmc.Group(
                                            [
                                                _btn(
                                                    "Enter edit mode", "ec-edit", "blue"
                                                ),
                                                _btn("Clear all", "ec-clear", "red"),
                                            ]
                                        ),
                                        dmc.Divider(my="xs"),
                                        dmc.Group(
                                            [
                                                dmc.Switch(
                                                    id="ec-disable-rect",
                                                    checked=False,
                                                    size="sm",
                                                    label="Disable rectangle tool (draw= prop)",
                                                ),
                                            ]
                                        ),
                                        # See /edit-control-measurement for the measurementSystem prop, a
                                        # popover-driven color picker, click-to-recolor in edit mode, and
                                        # per-shape area / radius tooltips (showMeasurementTooltips=True).
                                    ],
                                    gap="xs",
                                ),
                            ),
                        ],
                        gap="md",
                    ),
                    span=4,
                ),
            ]
        ),
        info_panel(
            "GeoJSON FeatureCollection",
            dmc.Code(
                id="ec-geojson",
                block=True,
                style={
                    "maxHeight": "260px",
                    "overflow": "auto",
                    "fontSize": "11px",
                    "whiteSpace": "pre",
                },
            ),
        ),
    ],
    gap="md",
)


# --- Readouts ----------------------------------------------------------------
@callback(
    Output("ec-status", "children"),
    Output("ec-geojson", "children"),
    Output("ec-action", "children"),
    Input("ec", "geojson"),
    Input("ec", "n_drawn"),
    Input("ec", "lastAction"),
    Input("ec", "action"),
)
def show_state(geo, n, last, action):
    if not geo:
        return "Pick a tool (top-left) or use the Python buttons →", "—", "—"
    feats = (geo or {}).get("features", [])
    status = f"n_drawn: {n or 0}\nfeatures: {len(feats)}\nlastAction: {last}"
    return status, json.dumps(geo, indent=2), json.dumps(action or {}, indent=1)


# --- Python -> control: drawToolbar ------------------------------------------
@callback(
    Output("ec", "drawToolbar"),
    Input("ec-draw-marker", "n_clicks"),
    Input("ec-draw-polyline", "n_clicks"),
    Input("ec-draw-polygon", "n_clicks"),
    Input("ec-draw-rectangle", "n_clicks"),
    Input("ec-act-finish", "n_clicks"),
    Input("ec-act-del", "n_clicks"),
    Input("ec-act-cancel", "n_clicks"),
    prevent_initial_call=True,
)
def drive_draw(m, l, p, r, fin, dl, cn):
    t = ctx.triggered_id
    n = sum(x or 0 for x in (m, l, p, r, fin, dl, cn))  # always-increasing tick
    if t == "ec-draw-marker":
        return {"mode": "marker", "n_clicks": n}
    if t == "ec-draw-polyline":
        return {"mode": "polyline", "n_clicks": n}
    if t == "ec-draw-polygon":
        return {"mode": "polygon", "n_clicks": n}
    if t == "ec-draw-rectangle":
        return {"mode": "rectangle", "n_clicks": n}
    if t == "ec-act-finish":
        return {"action": "finish", "n_clicks": n}
    if t == "ec-act-del":
        return {"action": "delete last point", "n_clicks": n}
    if t == "ec-act-cancel":
        return {"action": "cancel", "n_clicks": n}
    return dash.no_update


# --- Python -> control: editToolbar ------------------------------------------
@callback(
    Output("ec", "editToolbar"),
    Input("ec-edit", "n_clicks"),
    Input("ec-clear", "n_clicks"),
    prevent_initial_call=True,
)
def drive_edit(e, c):
    t = ctx.triggered_id
    n = (e or 0) + (c or 0)
    if t == "ec-edit":
        return {"mode": "edit", "n_clicks": n}
    if t == "ec-clear":
        return {"mode": "remove", "action": "clear all", "n_clicks": n}
    return dash.no_update


# --- draw= prop gating: disable rectangle tool when the switch is on ---------
@callback(Output("ec", "draw"), Input("ec-disable-rect", "checked"))
def gate(disabled):
    return {"rectangle": not bool(disabled)}


# Light/dark basemap, driven off the color-scheme store.
register_theme_swap("ec-tile", TILES)

Source: /edit-control

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: