Coverage
The three commands and ten props no other example reaches — the leftovers of the prop surface, in one runnable place.
Why this page exists
Every other page here demonstrates a feature. This one covers the leftovers.
The component ships 38 props and 12 command types. Sweeping every example module for the commands it dispatches — both the {"type": "X"} dict form and the _cmd("X", …) helper form — and for every prop reached either as a constructor keyword or through an Input/Output/State, three commands and ten props came back untouched. A reader could find them in the component reference and nowhere else.
That is the gap this page closes. It is not a tutorial; it is the part of the surface that would otherwise be documented only as a table row.
The three commands
type | What it does here | How you see it |
|---|---|---|
addFiles | Registers one 1×1 PNG as raw BinaryFileData | The readout's registered file ids grows |
exportToCanvas | Exports the scene through a canvas element | The preview appears, tagged with the dispatched id |
updateLibrary | Merges a two-rectangle item into the library | The library panel opens with the item in it |
addFiles registers bytes, not a visible element. Excalidraw draws an image only when a scene element references the file id, so the file appearing in the readout while the canvas stays empty is correct, not a failure — see File uploads for the flow that pairs the two.
exportToCanvas returns a data: URL, where exportToSvg returns markup. Both land on lastExport carrying the id you dispatched, which is what lets you tell two in-flight exports apart. Export round-trip covers the correlation.
The ten props
| Prop | Default | What it does | Live? |
|---|---|---|---|
langCode | "en" | Excalidraw's UI language | Yes |
name | (unset) | Scene name, used by the export dialog | Yes |
width | "100%" | CSS width of the container | Yes |
detectScroll | True | Canvas handles wheel events | Yes |
handleKeyboardGlobally | True | Key handling on document, not the canvas | Yes |
hideExcalidrawLinks | True | Hides the vendor's links group | Yes |
docsLinkUrl | "https://excalidraw.2plot.dev" | Our own menu item's URL; "" renders none | Yes |
docsLinkLabel | "dash-excalidraw docs" | That item's visible label | Yes |
autoFocus | True | Focus the canvas on mount | Mount only |
libraryReturnUrl | (unset) | Where the Browse Library trip returns to | Mount only, in effect |
appState | — | Full serializable app state | Read-only |
sceneVersion | — | Monotonic scene counter | Read-only |
autoFocus is read by Excalidraw when the canvas mounts. A switch for it would be theatre — this page sets it to False and that takes effect on page load, nowhere else. Reload to change it.
appState and sceneVersion are written out by the component. Setting them from Python does nothing; the readout on the right is what they are for. sceneVersion is a single integer and far cheaper to compare than diffing elements, which is the reason it exists.
The main menu is composed here, not by Excalidraw
The wrapper renders its own MainMenu rather than letting Excalidraw render the default one. It mirrors the vendor's DefaultMainMenu item for item and in the same order — Load scene, Save to file, Export, Save as image, Find, Help, Reset canvas, then the links, then Theme and Canvas background.
It does that because there is no other supported way to control the links group: the vendor's Socials item is composed at a single site inside Excalidraw and no UIOptions key reaches it. Two consequences worth knowing:
- Each canvas decides for itself.
hideExcalidrawLinksis a render
condition on this canvas's own menu, so it works in both directions and two canvases on one page can disagree. It was one-way until R5 — a shared stylesheet that went in and never came out — and this page said so.
- Two switches are ours to keep in step. Most default items decide their
own visibility, but Export and Save as image are gated by Excalidraw where the menu is composed. Composing our own means replicating exactly those two, so UIOptions.canvasActions.export and .saveAsImage still work. A test in the JS harness reads the vendor's own composition and fails if a version bump changes the item set, rather than letting our copy quietly become a copy of an older menu.
| State | Vendor links group | Our docs item |
|---|---|---|
| Default | hidden | shown |
hideExcalidrawLinks=False | shown | shown |
docsLinkUrl="" | hidden | none |
docsLinkLabel exists so that re-pointing the URL does not leave a menu entry naming one destination and opening another. If you send docsLinkUrl to your own site, relabel it.
What still points at Excalidraw
Being honest about the residue: the menu is not the only place the vendor links out, and the rest is deliberately left alone.
- The Help dialog links to
docs.excalidraw.com,plus.excalidraw.com/blog,
the Excalidraw GitHub issues tracker and their YouTube channel.
- Two error dialogs link to the vendor's FAQ and to "open an issue".
That is Excalidraw's manual for Excalidraw's canvas, and a wrapper that rewrote it would be sending people to the wrong project for help with the thing that is actually failing. Out of scope on purpose.
Live demo
Source
# File: docs/coverage/coverage.py
"""Coverage: the three commands and ten props no other example page reaches.
Measured, not guessed — an AST sweep of every `docs/*/*.py` for command types
(both the `{"type": "X"}` dict literal and the `_cmd("X", ...)` helper form)
and for props reached via a constructor kwarg or an Input/Output/State
dependency. What was left over is what this page exists to exercise.
"""
import time
import uuid
import dash_mantine_components as dmc
from dash import Input, Output, State, callback, html, no_update
from dash_excalidraw import DashExcalidraw
from docs._shared import canvas_frame, code_block, json_panel, sync_canvas_theme, two_column
sync_canvas_theme("coverage-canvas")
# A real 1x1 PNG. Small enough to read inline, valid enough that Excalidraw
# accepts it as BinaryFileData rather than silently dropping the entry.
PNG_1PX = (
"data:image/png;base64,"
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8"
"z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
)
CODE = """
# The three commands no other page dispatches.
# addFiles — RAW BinaryFileData list. You supply `created` and `mimeType`;
# an entry missing either is ignored, so a bad payload is a silent no-op.
{'id': cmd_id, 'type': 'addFiles',
'payload': [{'id': file_id, 'mimeType': 'image/png',
'dataURL': PNG_1PX, 'created': int(time.time() * 1000)}]}
# exportToCanvas — async; the result arrives on `lastExport`, carrying the
# id you dispatched. A data: URL, not an SVG string.
{'id': cmd_id, 'type': 'exportToCanvas', 'payload': {'mimeType': 'image/png'}}
# updateLibrary — merge items into the user's library.
{'id': cmd_id, 'type': 'updateLibrary',
'payload': {'libraryItems': [item], 'merge': True, 'openLibraryMenu': True}}
"""
def _rect(x: int, y: int, colour: str, bg: str) -> dict:
now = int(time.time())
return {
"id": f"cov-{uuid.uuid4()}",
"type": "rectangle",
"x": x,
"y": y,
"width": 120,
"height": 80,
"angle": 0,
"strokeColor": colour,
"backgroundColor": bg,
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 1,
"opacity": 100,
"seed": now % 100000,
"version": 1,
"versionNonce": now % 100000,
"isDeleted": False,
"groupIds": [],
"frameId": None,
"boundElements": [],
"updated": 1,
"link": None,
"locked": False,
}
def _btn(btn_id: str, label: str, colour: str) -> dmc.Button:
return dmc.Button(label, id=btn_id, color=colour, size="sm", variant="light")
LANGS = ["en", "fr-FR", "de-DE", "zh-CN", "es-ES"]
controls = dmc.Paper(
withBorder=True,
p="md",
radius="md",
children=dmc.Stack(
gap="sm",
children=[
dmc.Text("Props no other page sets", size="sm", fw=600, c="dimmed"),
dmc.SimpleGrid(
cols={"base": 1, "sm": 2, "md": 3},
spacing="sm",
children=[
dmc.Select(
id="cov-lang",
label="langCode",
description="Excalidraw UI language",
data=LANGS,
value="en",
size="xs",
),
dmc.Select(
id="cov-name",
label="name",
description="Scene name in the export dialog",
data=["coverage-scene", "renamed-scene"],
value="coverage-scene",
size="xs",
),
dmc.Select(
id="cov-width",
label="width",
description="CSS width of the container",
data=["100%", "75%", "50%"],
value="100%",
size="xs",
),
dmc.Switch(
id="cov-detectscroll",
label="detectScroll",
description="Canvas handles wheel events",
checked=True,
size="sm",
),
dmc.Switch(
id="cov-keyboard",
label="handleKeyboardGlobally",
description="Key handling on document, not canvas",
checked=True,
size="sm",
),
dmc.Switch(
id="cov-hidelinks",
label="hideExcalidrawLinks",
description="Hides the GitHub/Discord/X menu group",
checked=True,
size="sm",
),
],
),
],
),
)
component = dmc.Stack(
gap="md",
children=[
dmc.Alert(
color="blue",
variant="light",
title="What this page is for",
children=(
"Every other example page demonstrates a feature. This one "
"exists to cover the leftovers: the three command types and "
"ten props that no other page reaches. If a prop is listed "
"in the component reference but you cannot find it exercised "
"anywhere, it is here."
),
),
code_block(CODE),
dmc.Group(
gap="sm",
children=[
_btn("cov-addfiles", "addFiles", "grape"),
_btn("cov-exportcanvas", "exportToCanvas", "teal"),
_btn("cov-updatelibrary", "updateLibrary", "indigo"),
],
),
controls,
two_column(
canvas_frame(
DashExcalidraw(
id="coverage-canvas",
height="520px",
width="100%",
name="coverage-scene",
langCode="en",
detectScroll=True,
handleKeyboardGlobally=True,
hideExcalidrawLinks=True,
# autoFocus is read by Excalidraw at MOUNT only, so a
# switch for it would be theatre — it is set here and
# takes effect on page load, nowhere else.
autoFocus=False,
# Only observable by taking the Browse Library round trip;
# it is where that trip returns to.
libraryReturnUrl="https://excalidraw.2plot.dev/coverage",
)
),
dmc.Stack(
gap="sm",
children=[
html.Div(id="cov-readout"),
html.Div(id="cov-export-out"),
],
),
),
],
)
@callback(
Output("coverage-canvas", "langCode"),
Output("coverage-canvas", "name"),
Output("coverage-canvas", "width"),
Output("coverage-canvas", "detectScroll"),
Output("coverage-canvas", "handleKeyboardGlobally"),
Output("coverage-canvas", "hideExcalidrawLinks"),
Input("cov-lang", "value"),
Input("cov-name", "value"),
Input("cov-width", "value"),
Input("cov-detectscroll", "checked"),
Input("cov-keyboard", "checked"),
Input("cov-hidelinks", "checked"),
)
def _drive_props(lang, name, width, detect, keyboard, hidelinks):
return lang, name, width, bool(detect), bool(keyboard), bool(hidelinks)
@callback(
Output("coverage-canvas", "command"),
Input("cov-addfiles", "n_clicks"),
Input("cov-exportcanvas", "n_clicks"),
Input("cov-updatelibrary", "n_clicks"),
prevent_initial_call=True,
)
def _dispatch(_add, _export, _library):
import dash
trigger = dash.ctx.triggered_id
cmd_id = str(uuid.uuid4())
if trigger == "cov-addfiles":
return {
"id": cmd_id,
"type": "addFiles",
"payload": [
{
"id": f"cov-file-{cmd_id[:8]}",
"mimeType": "image/png",
"dataURL": PNG_1PX,
"created": int(time.time() * 1000),
}
],
}
if trigger == "cov-exportcanvas":
return {
"id": cmd_id,
"type": "exportToCanvas",
"payload": {"mimeType": "image/png"},
}
if trigger == "cov-updatelibrary":
return {
"id": cmd_id,
"type": "updateLibrary",
"payload": {
"libraryItems": [
{
"id": f"cov-lib-{cmd_id[:8]}",
"status": "unpublished",
"created": int(time.time() * 1000),
"elements": [
_rect(100, 100, "#5f3dc4", "#d0bfff"),
_rect(240, 100, "#0b7285", "#99e9f2"),
],
}
],
"merge": True,
"openLibraryMenu": True,
},
}
return no_update
# Three INPUTS, not one input and two states, and the distinction is the whole
# reason this panel looked dead. `sceneVersion` tracks ELEMENTS: registering a
# file does not change it, and neither does panning or zooming. So with it as
# the only Input, clicking addFiles updated nothing — the command had worked
# and the panel simply never re-ran — and appState's zoom and scroll could
# never appear at all, because a State is only read when some Input fires.
@callback(
Output("cov-readout", "children"),
Input("coverage-canvas", "sceneVersion"),
Input("coverage-canvas", "files"),
Input("coverage-canvas", "appState"),
)
def _readout(scene_version, files, app_state):
state = app_state or {}
return json_panel(
"sceneVersion + appState (both read-only)",
{
"sceneVersion": scene_version,
"registered file ids": sorted((files or {}).keys()),
"appState.zoom": (state.get("zoom") or {}).get("value"),
"appState.scrollX": state.get("scrollX"),
"appState.scrollY": state.get("scrollY"),
"appState.viewBackgroundColor": state.get("viewBackgroundColor"),
"appState.activeTool": (state.get("activeTool") or {}).get("type"),
# The `name` prop lands here. Without this row the page offered a
# control for `name` and no way to observe it — its only other
# surface is the export dialog's filename, which you have to open
# a modal to see. A demo you cannot watch is indistinguishable
# from one that does not work.
"appState.name (the `name` prop)": state.get("name"),
},
height=200,
)
@callback(
Output("cov-export-out", "children"),
Input("coverage-canvas", "lastExport"),
prevent_initial_call=True,
)
def _show_export(export):
if not export:
return no_update
if export.get("error"):
return dmc.Alert(
f"{export['type']} (id {export['id']}) failed: {export['error']}",
color="red",
variant="light",
)
result = export.get("result") or ""
return dmc.Stack(
gap="xs",
children=[
dmc.Text(
f"lastExport carries id {export.get('id')} for {export.get('type')}",
size="sm",
fw=600,
),
dmc.Image(src=result, h=120, fit="contain")
if result.startswith("data:image")
else dmc.Code(result[:200], block=True),
],
)
:defaultExpanded: false :withExpandedButton: true
Source: /coverage
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:
- /coverage/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt