Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,29 @@ in the README).
to the internal engine object.

### Added
- **Export format parity and a unified export API (ENG-10447).**
`to_image(format=...)` and extension-inferred, atomic `write_image(path)`
on charts, facet grids, and the internal figure cover PNG, JPEG/JPG, WebP,
SVG, and PDF alongside interactive HTML; `to_png`/`to_svg`/`to_html`
remain as compatibility conveniences. All five image formats export
browser-free by default: JPEG uses a new pure-numpy baseline encoder
(4:4:4, quality 1-100), WebP a new bit-exact lossless VP8L encoder with
alpha, and PDF a new vector backend that converts XY's own SVG output
(vector text via Helvetica metrics, axial-shading gradients, embedded
rasters for density/heatmap layers — the documented hybrid-vector
policy). `engine=Engine.auto` deterministically selects native per
format and switches to Chromium only for `custom_css`;
`Engine.chromium` adds browser-fidelity JPEG/WebP (CDP screenshots) and
PDF (`printToPDF`). A shared background policy spans every format
("auto"/CSS color/"transparent", JPEG rejects transparent instead of
silently flattening). `xy.write_images(figures=..., files=...)` batches
mixed formats through one reused browser session with atomic per-file
writes. `xy.export_config()` declares formats/filename/dimensions/
scale/background/quality on the chart itself, governing both Python
defaults and the modebar's download menu, which now offers PNG, JPEG,
WebP, SVG, and CSV (client-safe subset) with the same filename and
background semantics — including in standalone HTML with no kernel and
in Reflex apps.
- **Declarative continuous colorbars.** `xy.colorbar()` derives the domain,
colormap, and default title from the last compatible heatmap, continuous
scatter, hexbin, contour, segment, or triangle-mesh mark, with explicit
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ Its Rust core and WebGL2 renderer keep work bounded by what the screen can show.
- **Interactive by default.** Pan, zoom, hover, select, and inspect exact source
rows without shipping the entire dataset as JSON.
- **One chart, many outputs.** Display in Jupyter, VS Code, Colab, and Marimo,
or export self-contained HTML, browser-free PNG, and SVG.
or export self-contained HTML plus browser-free PNG, JPEG, WebP, SVG, and
PDF through one `to_image`/`write_image` API.
- **Designed for applications.** Layer marks and style both chart chrome and
marks with CSS/Tailwind-friendly hooks, gradients, strokes, and curves.

Expand Down
32 changes: 31 additions & 1 deletion docs/api-reference/figure-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,35 @@ document string or PNG bytes; with a path, they also write the result.
browser CSS/WebGL fidelity. `custom_css` works for HTML and Chromium PNG;
native PNG rejects author CSS because it has no browser cascade.

## Unified Image Export

~~~python
chart.to_image(
format="png", # png | jpeg/jpg | webp | svg | pdf
*,
width=None,
height=None,
scale=None, # device-pixel-ratio for raster formats
background=None, # "auto" | CSS color | "transparent"
engine=xy.Engine.auto,
quality=None, # JPEG / Chromium-WebP, 1-100 (default 90)
optimize=False,
custom_css=None,
sandbox=True,
gl="software",
) -> bytes
chart.write_image(path, *, format=None, ...) -> bytes # same options
~~~

`write_image()` infers the format from the file extension (`.png`, `.jpg`,
`.jpeg`, `.webp`, `.svg`, `.pdf`; `.html` routes to `to_html()`), writes
atomically, and returns the written bytes. `Engine.auto` deterministically
selects the native path per format, switching to Chromium only when
`custom_css` is passed. Omitted width/height/scale/background/quality fall
back to the chart's `export_config()` defaults. Module-level batch export is
`xy.write_images(figures=..., files=...)` — mixed formats, one shared browser
session for Chromium-resolved files, atomic per-file writes.

## Data Readout and Mutation

~~~python
Expand Down Expand Up @@ -83,7 +112,8 @@ objects. `reflex_components()` is an alias retained for adapter code.
## FacetChart Methods

`FacetChart` provides `figure()`, `widget()`, `show()`, `to_html()`/`html()`,
`to_svg()`, `to_png()`, and `memory_report()`. Its widget methods return one
`to_svg()`, `to_png()`, `to_image()`, `write_image()`, and `memory_report()`.
Its widget methods return one
widget per panel, and its figure escape hatch returns an internal facet grid.
Grid dimensions come from `facet_chart()`, so the facet SVG/PNG methods do not
accept per-call width or height. Facets do not expose append, pick, or
Expand Down
167 changes: 135 additions & 32 deletions docs/guides/display-and-export.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
---
title: Display and Export
description: Display live charts and export standalone HTML, PNG, SVG, or image batches.
description: Display live charts and export PNG, JPEG, WebP, SVG, PDF, HTML, or image batches.
---

# Display and Export

The same composed chart can display as a live notebook widget or produce three
standalone output families.
The same composed chart can display as a live notebook widget or export
through one unified static API covering PNG, JPEG, WebP, SVG, PDF, and
standalone interactive HTML.

## Notebook Display

Expand All @@ -22,6 +23,87 @@ chart.widget()
See [Notebooks](/docs/xy/integrations/notebooks/) for callbacks, binary comms,
and supported hosts.

## Unified Image Export

`to_image()` returns bytes; `write_image()` writes a file atomically and
infers the format from the extension:

~~~python
data = chart.to_image("pdf", width=1200, height=800, scale=2)

chart.write_image("reports/revenue.webp") # format inferred from .webp
chart.write_image("reports/revenue.bin", format="png") # explicit override
~~~

### Format and engine matrix

| Format | Native (browser-free) | Chromium | Notes |
| --- | --- | --- | --- |
| `png` | yes (default) | yes | transparency supported |
| `jpeg` / `jpg` | yes (default) | yes | no alpha; flattens onto `background` (default white); `quality` 1-100 (default 90) |
| `webp` | yes (default) | yes | native output is **lossless** with alpha; Chromium output is lossy and honors `quality` |
| `svg` | yes (always) | — | vector, browser-free; SVG cannot be produced by a screenshotting browser |
| `pdf` | yes (default) | yes | native output keeps text/axes/marks as vectors; density/heatmap layers embed as bounded rasters (hybrid-vector policy). Chromium prints the page instead |
| `html` | yes | — | via `to_html()`; `write_image("chart.html")` routes there |

`engine="auto"` (the default) is deterministic: every format uses the native
path unless `custom_css` is passed, which forces Chromium because utility-class
CSS needs a real CSS engine. `engine=Engine.chromium` opts into browser CSS,
font, and WebGL fidelity for any format except SVG. Native exports never
install or launch a browser; when Chromium is requested but not found, the
error names `XY_BROWSER` and the supported browsers.

### Background policy

`background` accepts `"auto"` (each renderer's default backdrop: opaque white
for raster/browser output, transparent for SVG), any CSS color, or
`"transparent"`:

~~~python
chart.to_image("png", background="transparent") # alpha-0 backdrop
chart.to_image("webp", background="#0f172a") # explicit backdrop
chart.to_image("jpeg") # flattened onto white
~~~

JPEG has no alpha channel, so `background="transparent"` is rejected there
rather than silently flattened. An explicit color (or `"transparent"`)
**replaces** the chart's theme backgrounds — both `theme(background=...)` and
`theme(plot_background=...)` — painting one backdrop consistently in every
format (raster canvas, SVG/PDF rect, browser page); `"auto"` keeps the theme
paints untouched.

`scale` is the device-pixel-ratio for raster formats and is ignored by
SVG/PDF, which are resolution-independent. A 300×200 chart at `scale=2`
produces a 600×400 raster.

## Declarative Export Defaults

`xy.export_config` describes export behavior as part of the chart — no I/O
happens at build time. It governs the modebar's download menu and provides
defaults for the Python export calls:

~~~python
xy.chart(
xy.line("date", "revenue", data=frame),
xy.export_config(
formats=["png", "webp", "svg", "csv"], # menu availability + order
filename="revenue",
width=1200,
height=800,
scale=2,
background="auto",
),
)
~~~

The browser modebar shows the client-safe subset (`png`, `jpeg`, `webp`,
`svg`, `csv`) with the same filename, scale, background, and quality semantics
as the Python exporters; `pdf`/`html` entries affect Python-side defaults
only. `formats=[]` hides the download menu entirely. Standalone HTML exports
keep the full download menu working without any Python kernel attached, and
Reflex charts inherit the same spec-driven configuration. Explicit arguments
to `to_image()`/`write_image()` always override the declarative defaults.

## Standalone HTML

~~~python
Expand All @@ -38,7 +120,10 @@ exported document. Standalone HTML uses inline scripts and styles by design;
read [Serving, CSP, and offline use](/docs/xy/guides/serving-csp-and-offline-use/)
before placing it inside a stricter application policy.

## PNG
## Compatibility Conveniences

`to_png()`, `to_svg()`, and `to_html()` remain supported with their existing
signatures:

~~~python
from xy import Engine
Expand All @@ -49,50 +134,68 @@ chart.to_png(
engine=Engine.chromium,
custom_css=".xy { font-family: Inter, sans-serif; }",
)
svg = chart.to_svg(width=1200, height=630)
~~~

The default engine is XY's browser-free native rasterizer. Set
`optimize=True` to spend more time producing a smaller native PNG. Use
`Engine.chromium` when browser fonts, injected CSS, or WebGL fidelity matters.
XY searches for Chrome, Chromium, Edge, or `chrome-headless-shell`; set
`XY_BROWSER` to select an executable explicitly.
The default PNG engine is XY's browser-free native rasterizer; set
`optimize=True` to spend more time producing a smaller native PNG. SVG export
is browser-free and screen-bounded: long lines are decimated before vector
generation, while density and heatmap representations embed compact raster
data where appropriate. For Chromium exports, XY searches for Chrome,
Chromium, Edge, or `chrome-headless-shell`; set `XY_BROWSER` to select an
executable explicitly. The browser sandbox is enabled by default; disable it
only for trusted input in an environment where the caller accepts that risk.

`custom_css` is Chromium-only. The browser sandbox is enabled by default;
disable it only for trusted input in an environment where the caller accepts
that risk.
## Batch Export

## SVG
Use one batch call instead of exporting in a loop — formats can be mixed, and
every Chromium-resolved file in the batch shares a single browser session:

~~~python
svg = chart.to_svg(width=1200, height=630)
chart.to_svg("chart.svg")
import xy

xy.write_images(
figures=[overview, detail],
files=["overview.svg", "detail.pdf"],
)
~~~

SVG export is browser-free and screen-bounded. Long lines are decimated before
vector generation, while density and heatmap representations embed compact
raster data where appropriate.
Per-file formats come from the extensions (`formats=` overrides them), and
writes are atomic per file. With the native engine the same call loops the
millisecond-fast browser-free renderers.

## Batch PNG Export
## Facets

Use one batch call instead of repeatedly starting Chromium:
Facet grids support the same format matrix as single charts:

~~~python
from xy import Engine
from xy.export import write_images

write_images(
[first.figure(), second.figure()],
["first.png", "second.png"],
engine=Engine.chromium,
)
grid = xy.facet_chart(xy.scatter("x", "y"), data=frame, by="region")
grid.write_image("regions.pdf") # vector panels, composed natively
grid.to_image("webp", background="transparent")
~~~

Chromium batches reuse one browser session. With the default native engine,
the same function loops over the fast browser-free rasterizer.
Native raster output composes the browser-free panel renders (the grid title
strip is omitted there — the native rasterizer has no free-standing text
path); SVG/PDF compose the vector panels, title included; Chromium renders
the full HTML grid.

## Migrating from Plotly

| Plotly | XY |
| --- | --- |
| `fig.to_image(format="png", scale=2)` | `chart.to_image("png", scale=2)` |
| `fig.write_image("out.webp")` | `chart.write_image("out.webp")` |
| `fig.write_html("out.html")` | `chart.to_html("out.html")` |
| `pio.write_images(figs, files)` | `xy.write_images(figures=..., files=...)` |
| Kaleido/Chrome required for static export | browser-free by default; `Engine.chromium` opt-in |
| EPS | not supported (dropped by modern Plotly/Kaleido as well) |

## Deterministic Dimensions

Interactive chart `width` and `height` accept positive pixel integers.
Ordinary charts also accept percentages such as `width="100%"`; the parent
must define a height when using `height="100%"`. Static raster and facet output
should use explicit dimensions for deterministic results.
must define a height when using `height="100%"`. Static raster and facet
output should use explicit dimensions for deterministic results; fluid
(`"100%"`) charts fall back to 800×500 at export time. Exports are
deterministic byte-for-byte for identical figures and options — no
timestamps, transient hover chrome, or nondeterministic ids are embedded.
Loading
Loading