Skip to content

Latest commit

 

History

98 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dash-excalidraw 2plot.ai

dash-excalidraw

dash-excalidraw — Excalidraw drawing canvas for Dash

The Excalidraw whiteboard as a first-class component for Plotly Dash 3 and 4.

Every prop JSON-serializable · events arrive as timestamped snapshot props · imperative actions via a command/lastExport round-trip · image externalization built in · no clientside_callback required for anything.

PyPI version Python Dash 3+ Excalidraw 0.18.1 License: MIT Discord YouTube

Documentation · Discord · YouTube · GitHub


dash-excalidraw running live at excalidraw.2plot.dev

Live at excalidraw.2plot.dev — every canvas on the docs site is a running Dash app.


Maintained by Pip Install Python LLC.


Overview

Excalidraw is a React application with a large imperative API. Wrapping it for Dash means answering one question honestly: what crosses the Python/JavaScript bridge?

Only JSON crosses. Functions, RegExps and class instances do not. Most wrappers give up at that point and hand you a clientside_callback for anything interesting. This one doesn't — it translates the whole surface into three JSON-safe patterns:

Upstream shape What Python sees Why
Callbacks (onPaste, onPointerUpdate, …) Snapshot props — lastPaste, lastPointerMove, … each carrying a timestamp A callback cannot be serialized. A record of it firing can. The timestamp lets you dedupe.
Imperative methods (updateScene, exportToSvg, …) command — {id, type, payload}, dispatched once per unique id One prop covers twelve methods, and re-renders can't re-fire a command.
Async results (exports) lastExport, keyed by the id you dispatched Exports resolve out of order under load. The id is how you correlate.
validateEmbeddable RegExp A list of glob strings, compiled to case-insensitive RegExps on the JS side A RegExp has no JSON representation. "*.youtube.com" does.

The result is that a Dash developer writes ordinary @callbacks and never touches JavaScript.

Installation

pip install dash-excalidraw

Nothing else is required — the Excalidraw bundle, its stylesheet and its UI font ship inside the wheel as a single self-contained JavaScript file. There is no CDN dependency at load time, no external_stylesheets entry to add, and no build step for consumers.

Quick Start

from dash import Dash, Input, Output, callback, html
from dash_excalidraw import DashExcalidraw

app = Dash(__name__)

app.layout = html.Div([
    DashExcalidraw(id="canvas", height="600px"),
    html.Pre(id="count"),
])


@callback(Output("count", "children"), Input("canvas", "elements"))
def show(elements):
    return f"{len(elements or [])} elements on the canvas"


if __name__ == "__main__":
    app.run(debug=True)

Driving the canvas from Python is the same idea in reverse — write a command:

import uuid
from dash import Input, Output, callback
from dash_excalidraw import DashExcalidraw


@callback(
    Output("canvas", "command"),
    Input("seed", "n_clicks"),
    prevent_initial_call=True,
)
def seed(_):
    return {
        # A NEW id every dispatch. The component dispatches once per unique
        # id, so re-sending the same one is a silent no-op.
        "id": str(uuid.uuid4()),
        "type": "updateScene",
        "payload": {
            "elements": [
                {"type": "rectangle", "x": 100, "y": 100,
                 "width": 200, "height": 120, "id": "r1"},
            ],
        },
    }

Documentation

Thirteen pages, each one a running Dash app you can draw on: basic usage, initialData, theming, view modes, UIOptions, events, command dispatch, export, persistence, library, collaboration, file uploads and AI scene generation.

Append /llms.txt to any page URL for the machine-readable Markdown of that page — the whole site is built to be readable by agents as well as people.

To run the docs site locally:

pip install -r requirements.txt
# markdown2dash pins gunicorn<22, against the CVE-driven gunicorn>=23 floor in
# requirements.txt. pip cannot resolve both, so it installs without its
# dependency graph — every one of its real dependencies is already pinned there.
pip install --no-deps markdown2dash==0.1.2
python run.py

The prop surface

38 props. Grouped by what they're for:

Sizing and seeding

Prop Type Notes
width, height str CSS values. height is the one you'll change.
initialData dict {elements, appState, files, libraryItems, scrollToContent}. Mount-only — see the gotcha below.

Output state — read-only from Python

Written by the component via setProps on every scene change.

Prop What it holds
elements The current element array
appState View background, zoom, scroll, grid/zen mode, theme, active tool
files The binary-file map (fileId → {dataURL, mimeType, …})
serializedData The canonical Excalidraw JSON envelope, as a string
externalizedSerializedData Same envelope with every inline data: URI stripped to null — persist this one
sceneVersion Cheap change detector; compare instead of diffing elements

Editor configuration — declarative

viewModeEnabled, zenModeEnabled, gridModeEnabled, isCollaborating, theme ("light"/"dark"), name, langCode, libraryReturnUrl, detectScroll, handleKeyboardGlobally, autoFocus, interceptLinkOpens, hideExcalidrawLinks, plus UIOptions and throttle controls (pointerMoveThrottleMs, scrollThrottleMs).

Event snapshots

lastPaste · lastPointerDown · lastPointerMove · lastPointerUp · lastScrollChange · lastLibraryChange · lastLinkOpen · lastFileAdded · lastExternalDrop · lastExport

Each is a dict carrying a timestamp. lastPointerMove and lastScrollChange are throttled — at 60 Hz they would otherwise be a callback storm.

Commands

Twelve command.type values:

Category Types
Scene updateScene · resetScene · scrollToContent
Files addFiles · replaceFiles
Tools & UI setActiveTool · setToast · toggleSidebar · updateLibrary
Export (async → lastExport) exportToSvg · exportToBlob · exportToCanvas

Images at scale

Excalidraw stores every pasted or dropped image as a base64 dataURL inside the scene. Leaving them inline bloats serializedData by orders of magnitude — a handful of screenshots turns a 40 KB scene into a 12 MB one, and you pay that on every callback.

The wrapper supports the full upload-and-swap pattern:

  1. lastFileAdded fires once per new inline file — {fileId, mimeType, dataURL, size}. Push the bytes to your storage layer from an ordinary callback.
  2. command: replaceFiles with {fileId: {dataURL: "https://…"}} overwrites Excalidraw's copy in place. The old base64 string is unreferenced and garbage-collected.
  3. externalizedSerializedData is the envelope with every remaining data: URI stripped. External URLs that step 2 installed survive. Persist this.

dash_excalidraw.helpers ships decode_data_url, strip_inline_files and restore_inline_files — all pure Python, no dependencies.

The component never makes the network call. It emits the bytes and waits to be told where they landed, which keeps the wrapper credential-free and backend-agnostic. Working reference: /file-uploads.

AI scene generation, and what it costs

The docs site's /ai-agent page turns a natural-language prompt into a scene by asking Claude or Gemini for Excalidraw JSON and dispatching it with command: updateScene. The technique is the interesting part and it generalizes: the model returns data, not code, so nothing it produces is executed — a malformed response draws nothing, it cannot do anything.

Two costs worth stating plainly before you copy the pattern:

  • Tokens. A scene of any complexity is a few thousand output tokens. The system prompt is stable across requests, so Claude calls use prompt caching — that is most of the saving. Costs are per-provider and change; check current pricing before wiring it to a public form.
  • Exposure. An unauthenticated LLM endpoint on a public host is somebody else's free API credit. Note where the guard has to go: on the docs site the page declares an auth tier, but that tier governs who can read the page. It does not govern who can make it bill — page tiers are path-based and every Dash callback posts to one shared route, and the tier machinery deliberately fails open when auth isn't configured so a misconfigured deploy still serves its docs. The spend check therefore lives inside the callback itself, fails closed in production, and is the thing worth copying. A page-level gate alone would have looked right and protected nothing.

The page runs the model call synchronously inside a Dash callback for clarity. For production, move it to background=True or a task queue.

Dash compatibility

Dash 3.0.3+ and the whole 4.x line
Python 3.9+
React 18.3.1 (provided by Dash — not bundled)
Excalidraw 0.18.1, pinned exactly and bundled

The wheel depends on Dash and nothing else. The documentation site's requirements are separate and never reach a pip install dash-excalidraw.

Excalidraw 0.18 notes

Upgraded from 0.17.6. Two changes are worth knowing if you used the earlier release:

commitToHistory became captureUpdate, and the default's meaning changed. 0.17 left undo history untouched when you passed nothing. 0.18 defaults to EVENTUALLY, which folds a programmatic scene push into the next captured action — so a user's first Ctrl+Z after your updateScene would also roll back their own previous edit. Nothing errors and nothing warns. This wrapper defaults to IMMEDIATELY, so a Python-dispatched push is one discrete, individually undoable step. Override per dispatch with payload["captureUpdate"] = "IMMEDIATELY" | "NEVER" | "EVENTUALLY"; peer-driven scenes in a collaborative app want "NEVER". A legacy commitToHistory is translated and warned about once.

collaborators moved to top-level SceneData. Either spelling is accepted from Python and normalised.

0.18 also brings elbow arrows, flowchart shortcuts (Cmd+Arrow), scene search, image cropping, element linking and the command palette.

Common gotchas

  • initialData is mount-only. Excalidraw owns the scene after mount. Setting the prop from a callback does nothing — use command: updateScene or resetScene.
  • Commands need unique ids. Dispatching the same {id, type} twice is a no-op by design. Use uuid.uuid4().
  • lastExport carries the id you dispatched. Don't assume the newest lastExport answers your newest command; async exports arrive out of order.
  • validateEmbeddable takes glob strings, not regex — "*.youtube.com".

Development

# Python side
python -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'

# JS side (only needed when changing src/ts)
nvm use                 # .nvmrc pins node 20.11.1
npm install
npm run build           # webpack bundle + regenerated Python wrappers
npm run watch           # webpack --watch during iteration

# Test
pytest tests -q

The built bundle dash_excalidraw/dash_excalidraw.js is committed on purpose: the release workflow gates on its commit timestamp, and tracking it keeps pip install git+… working without a Node toolchain. Rebuild and commit it only in release-prep commits.

See REBUILD.md for the architecture and the reasoning behind the prop surface.

Community & support

More from Pip Install Python LLC

Part of the 2plot network — component documentation sites, each one a running Dash app: leaflet · email · flexlayout · llms · boilerplate

License

MIT — see LICENSE.

Excalidraw itself is MIT-licensed by the Excalidraw team. Third-party notices for everything bundled into the JavaScript artifact ship alongside it in dash_excalidraw/dash_excalidraw.js.LICENSE.txt.

About

Porting over the excalidraw canvas as to make it easily available within the dash framework.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages