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
102 changes: 102 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,108 @@ Fragment files in ``upcoming_changes/`` are assembled into this file by

.. towncrier release notes start

0.2.0 (2026-07-10)
==================

New Features
------------

- 2-D scalar images can now render on the **GPU via WebGPU** (``imshow(...,
gpu="auto"|True|False)``): the image uploads as an R8 texture and a WGSL fragment
shader applies the colormap LUT + contrast (clim) in one draw, replacing the
per-pixel JavaScript colormap loop. Large images (≳1 megapixel) take the GPU path
automatically; everything below the threshold, RGB images, ``gpu=False``, and any
device without WebGPU keep the identical Canvas2D path. ``plot.gpu_active`` reports
which path ran. Verified on an NVIDIA Pascal GPU.
- Arbitrarily large images can now display through **tile mode**
(``imshow(..., tile="auto")``): the figure shows a downsampled overview as its
base and, after each zoom/pan settles, samples a high-resolution detail tile of
just the visible region at panel resolution — deep zooms stay crisp without
ever shipping the full-resolution frame. ``Plot2D.enable_tile`` /
``update_tile_source`` swap the underlying frame while the zoom and
subselection persist (live-data contract), and a pluggable ``TileBackend``
(default: a fast vectorised numpy box-mean) lets out-of-core or GPU sources
own the sampling.
- Markers gained a ``clip_display`` option controlling whether they draw
outside the current axes view or are clipped to it.
- ``set_extent`` now updates the axes state (calibrated units / scale bar), so
applying a calibrated extent after figure creation renders labelled axes
instead of bare pixels.
- Regular ``pcolormesh`` meshes are detected and rasterized to an image for
display (fast IPF-style heatmaps) instead of drawing per-quad.


Bug Fixes
---------

- Fixed a display freeze under the Electron binary pixel transport: the routing
layer stripped the pixel key out of the slimmed geom JSON, so the renderer's
"unchanged → skip re-upload" caches (Canvas2D blit cache, WebGPU texture,
overlay-mask cache) fell back to a 4-sampled-byte fingerprint of the buffer —
two frames differing anywhere else collided and the display stayed frozen on
the old frame (seen as a stale overview after a movie scrub). The slimmed geom
now carries a small ``\x00bin:<checksum>`` content token under the pixel key,
binary buffers are additionally stamped with an arrival sequence as a fallback
key, and the overlay-mask draw path now reads the binary byte side-channel
(it previously only decoded base64, so masks never displayed over the binary
transport).
- Fixed the WebGPU 2-D image path sampling a vertically MIRRORED window when the
view was panned off-centre: the shader applied a global ``1 - v`` flip after
interpolating the ``[v0, v1]`` uv window, which sampled ``[1-v1, 1-v0]``
instead — correct only for a full or vertically-centred view. Symptoms: pan-y
moved the image the wrong way on GPU-rendered panels, and markers/widgets
(drawn by the shared Canvas2D overlay transform, which was always correct)
appeared detached from the image features they marked. The base and
detail-tile passes share the shader, so both are fixed. GPU-vs-CPU screenshot
parity tests (zoom, pan, markers, widgets, detail tile) now run on real WebGPU
in headless Chromium (``channel="chromium"`` + ``--enable-unsafe-webgpu``) and
skip on machines with no adapter.
- ``Plot2D.set_data`` no longer makes a float64 copy of every incoming frame.
The float64 cast now happens lazily in the ``.data`` property (the only reader),
so a frame stream — e.g. scrubbing an in-situ movie — keeps the source dtype and
skips a ~12 ms float64 copy of a 4k frame per tick. ``.data`` still returns a
read-only float64 copy, unchanged for callers.
- The Electron binary pixel transport now ships the RAW uint8 image bytes end to
end, instead of base64-encoding them in ``set_data`` only to base64-decode them
straight back in the routing layer. ``Plot2D.set_data`` stashes the raw bytes on
the Figure's ``_raw_pixels`` side-table and leaves a tiny content-checksum
change-token in ``image_b64``; ``_electron._route_change`` ships those bytes to a
PLOTBIN frame directly. This removes the ~20 ms base64 encode, the ~17 ms decode,
and the megabyte ``json.dumps`` of the pixel string from every scrub frame — a
2.2x faster ``set_data`` (≈98 ms → ≈44 ms on a 2048² frame) and ~25% less
bytes-on-wire. Non-Electron hosts (Jupyter / Pyodide / standalone / ``save_html``)
are unchanged: they have no PLOTBIN channel, so the token is resolved back to
inline base64 via ``Plot2D.resolve_pixel_tokens`` when the figure state is
serialised for them.
- Tile mode: a data update while zoomed in (``update_tile_source`` with a detail
tile shown) refreshes only the detail tile, leaving the overview base on the
old frame — zooming out then flashed the pre-update frame. The skipped
overview is now marked stale and re-sampled once on the next view settle
(riding the same push as the detail/clear), preserving the per-frame skip
optimisation while never exposing stale base pixels.
- Interactive zoom/pan on a 2-D image no longer re-serialises (and re-transmits)
the full image on every mouse tick. The wheel/pan/orbit handlers write only the
light *view* state back to the ``panel_<id>_json`` trait now, excluding the
cached geometry (pixels, colormap LUT) that ``_applyGeom`` splices into the panel
state for drawing. Previously the whole frame was ``JSON.stringify``-d per tick —
catastrophically so on the binary transport, where the pixel buffer is a
``Uint8Array`` that stringifies to a ``{"0":..,"1":..}`` object with one key per
byte — which stalled zoom on large images.
- Fixed a first-paint race under the Electron binary transport: binary
side-table bytes that arrived before ``render()`` attached its listeners were
stranded, leaving the first frame blank until the next update — they are now
spliced into the initial paint.


Maintenance
-----------

- ``anyplotlib.__version__`` is now exposed from the package metadata.
- Per-frame hot-path costs trimmed: the colormap LUT is cached instead of being
rebuilt every frame (~100 ms), and small-range data rescales in float32
(~60 ms → ~27 ms per 2048² frame).


0.1.0 (2026-06-24)
==================

Expand Down
6 changes: 3 additions & 3 deletions docs/_root/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
<meta charset="utf-8" />
<title>anyplotlib – redirecting…</title>
<!-- Redirect to the latest dev docs. -->
<meta http-equiv="refresh" content="0; url=dev/" />
<link rel="canonical" href="dev/" />
<meta http-equiv="refresh" content="0; url=v0.2.0/" />
<link rel="canonical" href="v0.2.0/" />
</head>
<body>
<p>
Redirecting to <a href="dev/">dev documentation</a>…
Redirecting to <a href="v0.2.0/">v0.2.0 documentation</a>…
</p>
</body>
</html>
Expand Down
5 changes: 5 additions & 0 deletions docs/_root/switcher.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"version": "dev",
"url": "https://cssfrancis.github.io/anyplotlib/dev/"
},
{
"name": "v0.2.0 (stable)",
"version": "v0.2.0",
"url": "https://cssfrancis.github.io/anyplotlib/v0.2.0/"
},
{
"name": "v0.1.0 (stable)",
"version": "v0.1.0",
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
project = "anyplotlib"
copyright = "2026, anyplotlib contributors"
author = "anyplotlib contributors"
release = "0.1.0"
release = "0.2.0"

# When built in CI the workflow sets DOCS_VERSION to the tag name (e.g.
# "v0.1.0") or "dev". Fall back to "dev" for local builds.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ exclude = [

[project]
name = "anyplotlib"
version = "0.1.0"
version = "0.2.0"
description = "A plotting library using python, javascript and anywidget for performant in browser plotting."
readme = "README.md"
license = { text = "MIT" }
Expand Down
8 changes: 7 additions & 1 deletion upcoming_changes/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ Each fragment is a plain ``.rst`` file named::
{PR_number}.{type}.rst

where ``{PR_number}`` is the GitHub pull-request number and ``{type}`` is one
of:
of the types below.

If a change has no natural PR number (e.g. work batched on a long-lived
feature branch), name the file ``+{slug}.{type}.rst`` — the leading ``+``
marks it as an "orphan" fragment so towncrier omits the issue link. Without
it, the slug is rendered as a broken PR link in the changelog (this bit the
0.2.0 notes).

================= ==============================================================
Type Use when …
Expand Down
11 changes: 0 additions & 11 deletions upcoming_changes/binary_token_cache_freeze.bugfix.rst

This file was deleted.

11 changes: 0 additions & 11 deletions upcoming_changes/gpu_pan_v_mirror.bugfix.rst

This file was deleted.

5 changes: 0 additions & 5 deletions upcoming_changes/lazy_data_cast.bugfix.rst

This file was deleted.

12 changes: 0 additions & 12 deletions upcoming_changes/raw_pixel_transport.bugfix.rst

This file was deleted.

6 changes: 0 additions & 6 deletions upcoming_changes/stale_overview_zoom_out.bugfix.rst

This file was deleted.

7 changes: 0 additions & 7 deletions upcoming_changes/webgpu_2d_image.new_feature.rst

This file was deleted.

8 changes: 0 additions & 8 deletions upcoming_changes/zoom_view_write.bugfix.rst

This file was deleted.

Loading