Plotly figures in Dash are rendered by JavaScript in the browser — the Python server never holds the chart as pixels. dash-capture bridges this gap by triggering the capture directly in the running browser, with no server-side headless browser (Chrome, Playwright, webshot2) required. The result is delivered to Python for post-processing, custom rendering, and download.
pip install dash-captureOptional extras for built-in decoration renderers:
pip install 'dash-capture[pil]' # bordered / titled / watermarkedfrom dash_capture import capture_graph
# Returns an html.Div — place it next to your dcc.Graph
capture_graph("my-graph", trigger="Export")The default renderer is a zero-dependency passthrough: the wizard shows just Generate + Download. Clicking the trigger opens a modal with the live preview and a PNG / JPEG / SVG download.
from dash_capture import capture_graph, plotly_strategy
capture_graph(
"my-graph",
trigger="Export",
strategy=plotly_strategy(
strip_title=True,
strip_legend=True,
strip_margin=True,
format="png", # or "jpeg", "webp", "svg"
),
)Strip patches remove chart decorations before capture without touching the live chart.
from dash_capture import capture_element
capture_element("my-data-table", trigger="Capture table")capture_element defaults to html2canvas_strategy() and works with any Dash component that has an id.
from dash_capture import capture_graph
from dash_capture.pil import titled, bordered, watermarked
# Title bar above the captured chart
capture_graph("my-graph", renderer=titled)
# Colored border
capture_graph("my-graph", renderer=bordered)
# Diagonal watermark
capture_graph("my-graph", renderer=watermarked)The wizard auto-generates form fields (text input, color picker, dropdown) from each renderer's type hints, so users can edit title, color, width, etc. before downloading.
Define a function that takes _target (file-like) and _snapshot_img (callable returning raw PNG bytes). Type-hinted parameters become auto-generated form fields in the wizard.
from dash_capture import capture_graph, renderer
@renderer
def my_renderer(_target, _snapshot_img, title: str = "", dpi: int = 150):
png = _snapshot_img()
# post-process: add a watermark, corporate frame, etc.
_target.write(png)
capture_graph("my-graph", renderer=my_renderer)The @renderer decorator validates the magic parameter names at definition time. A typo like _snaphot_img raises ValueError with a "did you mean ...?" hint instead of silently failing at runtime.
from dash import Input
from dash_capture import capture_binding, plotly_strategy
binding = capture_binding(
"my-graph",
strategy=plotly_strategy(strip_title=True),
trigger=Input("my-btn", "n_clicks"),
)
# Place binding.store in the layout
# React to binding.store_id to get the base64 PNG| Strategy | Method | Use case |
|---|---|---|
plotly_strategy() |
Plotly.toImage() |
Plotly charts — exact resolution |
html2canvas_strategy() |
html2canvas | Any DOM element (tables, divs) |
canvas_strategy() |
canvas.toDataURL() |
Raw <canvas> elements |
plotly_strategy() accepts strip flags (strip_title, strip_legend, strip_annotations, strip_axis_titles, strip_colorbar, strip_margin) and format. For per-export width / height / scale, declare capture_width: int / capture_height: int / capture_scale: float parameters on your renderer — they get plumbed into Plotly.toImage() automatically.
The same capture_width / capture_height / capture_scale magic params work with any strategy that consumes them (plotly_strategy, html2canvas_strategy, dygraph_strategy, …). Three ways to provide values:
| How | Where the value comes from | Use case |
|---|---|---|
Omit (no field_specs, no capture_resolver) |
The element's current size in the browser | Fast, sensible default — works without any config. |
fixed(value) in field_specs |
Inlined as a JS constant | Pin a specific export size at import time. |
capture_resolver=fn |
Computed server-side from form fields | Drive sizes from user input, with snapshot caching keyed by the resolved options. |
from dash_fn_form import fixed
from dash_capture import capture_graph
# (1) Omit — capture at the live element's current size:
capture_graph(graph, renderer=my_renderer)
# (2) fixed — pin an export size:
capture_graph(graph, renderer=my_renderer,
field_specs={"capture_width": fixed(1200), "capture_height": fixed(600)})
# (3) capture_resolver — drive from form fields:
capture_graph(graph, renderer=my_renderer,
capture_resolver=lambda width, height, **_:
{"capture_width": width, "capture_height": height})FromPlotly reads a value from the running Plotly figure to pre-populate auto-generated form fields:
from dash import dcc
from dash_capture import capture_graph, FromPlotly, renderer
graph = dcc.Graph(id="my-graph", figure=fig)
@renderer
def export(_target, _snapshot_img, title: str = "", sources: str = ""):
_target.write(_snapshot_img())
capture_graph(
graph,
renderer=export,
field_specs={"title": FromPlotly("layout.title.text", graph)},
)MIT