From 48b7b4bded42777548aee10706a838f167f79826 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 10 Jul 2026 23:33:41 +0000 Subject: [PATCH 1/2] chore: prepare release v0.2.0 --- CHANGELOG.rst | 73 +++++++++++++++++++ docs/_root/index.html | 6 +- docs/_root/switcher.json | 5 ++ docs/conf.py | 2 +- pyproject.toml | 2 +- .../binary_token_cache_freeze.bugfix.rst | 11 --- upcoming_changes/gpu_pan_v_mirror.bugfix.rst | 11 --- upcoming_changes/lazy_data_cast.bugfix.rst | 5 -- .../raw_pixel_transport.bugfix.rst | 12 --- .../stale_overview_zoom_out.bugfix.rst | 6 -- .../webgpu_2d_image.new_feature.rst | 7 -- upcoming_changes/zoom_view_write.bugfix.rst | 8 -- 12 files changed, 83 insertions(+), 65 deletions(-) delete mode 100644 upcoming_changes/binary_token_cache_freeze.bugfix.rst delete mode 100644 upcoming_changes/gpu_pan_v_mirror.bugfix.rst delete mode 100644 upcoming_changes/lazy_data_cast.bugfix.rst delete mode 100644 upcoming_changes/raw_pixel_transport.bugfix.rst delete mode 100644 upcoming_changes/stale_overview_zoom_out.bugfix.rst delete mode 100644 upcoming_changes/webgpu_2d_image.new_feature.rst delete mode 100644 upcoming_changes/zoom_view_write.bugfix.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6b38c69a..cee928fa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,79 @@ 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. (`#webgpu_2d_image `_) + + +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:`` 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). (`#binary_token_cache_freeze `_) +- 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. (`#gpu_pan_v_mirror `_) +- ``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. (`#lazy_data_cast `_) +- 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. (`#raw_pixel_transport `_) +- 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. (`#stale_overview_zoom_out `_) +- 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__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. (`#zoom_view_write `_) + + 0.1.0 (2026-06-24) ================== diff --git a/docs/_root/index.html b/docs/_root/index.html index c6277376..99ce7b4a 100644 --- a/docs/_root/index.html +++ b/docs/_root/index.html @@ -4,12 +4,12 @@ anyplotlib – redirecting… - - + +

- Redirecting to dev documentation… + Redirecting to v0.2.0 documentation

diff --git a/docs/_root/switcher.json b/docs/_root/switcher.json index 6039a4e8..0faa6ee4 100644 --- a/docs/_root/switcher.json +++ b/docs/_root/switcher.json @@ -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", diff --git a/docs/conf.py b/docs/conf.py index 526e8a43..cd7502c4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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. diff --git a/pyproject.toml b/pyproject.toml index 9ef7a1dd..fd97e3de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/upcoming_changes/binary_token_cache_freeze.bugfix.rst b/upcoming_changes/binary_token_cache_freeze.bugfix.rst deleted file mode 100644 index ae49fe19..00000000 --- a/upcoming_changes/binary_token_cache_freeze.bugfix.rst +++ /dev/null @@ -1,11 +0,0 @@ -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:`` 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). diff --git a/upcoming_changes/gpu_pan_v_mirror.bugfix.rst b/upcoming_changes/gpu_pan_v_mirror.bugfix.rst deleted file mode 100644 index 6df29827..00000000 --- a/upcoming_changes/gpu_pan_v_mirror.bugfix.rst +++ /dev/null @@ -1,11 +0,0 @@ -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. diff --git a/upcoming_changes/lazy_data_cast.bugfix.rst b/upcoming_changes/lazy_data_cast.bugfix.rst deleted file mode 100644 index 545bb523..00000000 --- a/upcoming_changes/lazy_data_cast.bugfix.rst +++ /dev/null @@ -1,5 +0,0 @@ -``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. diff --git a/upcoming_changes/raw_pixel_transport.bugfix.rst b/upcoming_changes/raw_pixel_transport.bugfix.rst deleted file mode 100644 index 02029326..00000000 --- a/upcoming_changes/raw_pixel_transport.bugfix.rst +++ /dev/null @@ -1,12 +0,0 @@ -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. diff --git a/upcoming_changes/stale_overview_zoom_out.bugfix.rst b/upcoming_changes/stale_overview_zoom_out.bugfix.rst deleted file mode 100644 index f1077a3d..00000000 --- a/upcoming_changes/stale_overview_zoom_out.bugfix.rst +++ /dev/null @@ -1,6 +0,0 @@ -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. diff --git a/upcoming_changes/webgpu_2d_image.new_feature.rst b/upcoming_changes/webgpu_2d_image.new_feature.rst deleted file mode 100644 index 7a04e1fe..00000000 --- a/upcoming_changes/webgpu_2d_image.new_feature.rst +++ /dev/null @@ -1,7 +0,0 @@ -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. diff --git a/upcoming_changes/zoom_view_write.bugfix.rst b/upcoming_changes/zoom_view_write.bugfix.rst deleted file mode 100644 index a84fa3cc..00000000 --- a/upcoming_changes/zoom_view_write.bugfix.rst +++ /dev/null @@ -1,8 +0,0 @@ -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__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. From d4d0e1b854cf611363a106cb37c871f57929fd80 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Fri, 10 Jul 2026 18:41:25 -0500 Subject: [PATCH 2/2] docs(release): clean up the 0.2.0 changelog - strip the bogus issue links: the 0.2.0 fragments were named without towncrier's '+' orphan prefix, so their FILENAMES rendered as PR links (#binary_token_cache_freeze etc.); document the '+{slug}' convention in the upcoming_changes README so it doesn't recur - add the release's missing user-facing entries: tile mode (imshow tile='auto', enable_tile/update_tile_source, pluggable TileBackend), marker clip_display, set_extent axes update, pcolormesh rasterization, the binary-transport first-paint race fix, and a Maintenance section (__version__, colormap-LUT cache + float32 rescale) --- CHANGELOG.rst | 43 +++++++++++++++++++++++++++++++------ upcoming_changes/README.rst | 8 ++++++- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cee928fa..005e8eaf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,7 +22,23 @@ New Features 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. (`#webgpu_2d_image `_) + 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 @@ -38,7 +54,7 @@ Bug Fixes 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). (`#binary_token_cache_freeze `_) + 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]`` @@ -49,12 +65,12 @@ Bug Fixes 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. (`#gpu_pan_v_mirror `_) + 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. (`#lazy_data_cast `_) + 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 @@ -66,13 +82,13 @@ Bug Fixes 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. (`#raw_pixel_transport `_) + 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. (`#stale_overview_zoom_out `_) + 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__json`` trait now, excluding the @@ -80,7 +96,20 @@ Bug Fixes 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. (`#zoom_view_write `_) + 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) diff --git a/upcoming_changes/README.rst b/upcoming_changes/README.rst index 118a12d9..46a63adb 100644 --- a/upcoming_changes/README.rst +++ b/upcoming_changes/README.rst @@ -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 …