Kaleido
Cross-platform Python library for fast static image export (PNG, SVG, PDF) of Plotly figures via headless Chrome.
Repository Health
Technical Analysis
Kaleido is the static image export engine behind Plotly’s Python library. It drives a headless Chrome instance over the DevTools Protocol to render a Plotly figure exactly as it would appear in a browser, then rasterizes or vectorizes it to PNG, JPEG, SVG, PDF, WebP, or EPS. Before Kaleido existed, static export in Plotly.py depended on Orca, a separate binary that had to be installed and managed outside of pip — Kaleido replaced that with a pure-Python, pip-installable dependency.
Version 1.0 rewrote the project around an async-first Kaleido browser class built on top of the choreographer CDP driver, with a synchronous API layer (write_fig_sync, a background sync server) for callers who don’t want to manage an event loop themselves. It batches many figures across a configurable number of Chrome tabs/processes, exposes both one-shot convenience functions and a long-lived context manager for high-throughput export, and no longer bundles Chrome itself — it locates an existing installation or fetches one on demand via kaleido_get_chrome.
What You Get
- An async
Kaleidoclass (achoreographer.Browsersubclass) that manages a pool of Chrome tabs/processes for concurrent figure rendering - Synchronous wrapper functions (
write_fig_sync,calc_fig_sync) plus an optional persistent sync server so blocking callers avoid asyncio boilerplate - Support for PNG, JPEG, WebP, SVG, PDF, and EPS output formats with scale/width/height layout options
- A
kaleido_get_chromeCLI command and Python API (get_chrome,get_chrome_sync) to fetch a compatible Chrome build when one isn’t already installed - Batch export helpers (
write_fig_from_object) that accept an iterable of figure/path/opts dicts for exporting many charts in one call - A custom
PlotlyJSONEncoderregistration path so figures serialize correctly without extra configuration in Plotly.py
Common Use Cases
- Exporting Plotly charts to PNG/SVG/PDF from a Python script or notebook for reports and documents
- Generating chart images in batch pipelines (dashboards, scheduled reports) without a running browser session per call
- Producing static image assets for static site generators or emailed reports where an interactive chart isn’t usable
- CI/test pipelines that snapshot-render figures to images for visual regression checks
- Any web charting library beyond Plotly.js that needs a headless, scriptable way to rasterize its output
Under The Hood
Architecture
Kaleido’s core is a Kaleido class (src/py/kaleido/kaleido.py) that subclasses choreographer.Browser, giving it a pool of Chrome DevTools Protocol connections it dispatches work across. Individual render/export operations are handled by _KaleidoTab (kaleido/_kaleido_tab/_tab.py), which owns a single browser tab’s lifecycle and talks to injected page JavaScript to convert a figure into image bytes. PageGenerator (_page_generator.py) selects and serves the versioned HTML/JS page template a tab loads (supporting multiple bundled Plotly.js versions), while _utils/fig_tools.py and _utils/path_tools.py normalize input figures and output paths. A separate _sync_server.py layer wraps the async API in a background event loop so write_fig_sync/calc_fig_sync and the module-level start_sync_server/stop_sync_server singleton can serve synchronous callers without them managing asyncio directly. This split — async browser core, thin sync facade, pluggable page templates — lets the same rendering engine serve both notebook-style one-shot calls and high-throughput batch export.
Tech Stack
The package targets Python 3.8+ and depends on choreographer (the Chrome DevTools Protocol driver that replaced the bundled Orca binary), logistro for logging, orjson for fast figure serialization, and packaging for version comparisons. It vendors a JS page bundle (kaleido/vendor/) built from a small legacy src/js project (browserify + fast-isnumeric/is-plain-obj/semver) rather than pulling Plotly.js at runtime. The build uses setuptools with setuptools-git-versioning for tag-derived versions, and development tooling runs on uv (uv.lock committed) with poethepoet (poe) task definitions for test/lint shortcuts.
Code Quality
The tests/ directory holds roughly 15 files (~2,000+ lines) covering utils, the page generator, the sync server, the public API surface, path tools, JSON encoding, and large-figure handling, run under pytest with pytest-asyncio, pytest-xdist (parallel), and pytest-order. Linting is strict: ruff is configured with select = ["ALL"] and an explicit, justified ignore list, and both mypy and pyright are wired in for static typing despite the runtime code itself favoring lightweight type hints over heavy annotation. Three GitHub Actions workflows (test.yml, ruff.yml, publish_testpypi.yml) gate merges and releases, so the quality bar is enforced in CI, not just documented.
API Design
The public API is deliberately small and layered: write_fig/write_fig_sync/calc_fig/calc_fig_sync cover the common one-shot case with no object to construct, while the Kaleido async context manager (async with Kaleido(n=4) as k: ...) is there for callers who need to reuse a browser pool across many figures. Options are passed as a single opts dict (scale, format, width, height) rather than a long positional signature, and the same kopts dict threads through the convenience wrappers into the Kaleido constructor so behavior is consistent whichever entry point is used. The README includes an explicit v0-to-v1 migration section, and errors are surfaced through named exception types (KaleidoError, JavascriptError, ChromeNotFoundError, BrowserClosedError) re-exported from a dedicated errors module rather than generic exceptions, making failure modes easy to catch selectively.