Skip to content

Add Plot2D.set_display_window — re-window contrast without re-quantising - #61

Merged
CSSFrancis merged 2 commits into
mainfrom
feat/display-window
Aug 25, 2026
Merged

Add Plot2D.set_display_window — re-window contrast without re-quantising#61
CSSFrancis merged 2 commits into
mainfrom
feat/display-window

Conversation

@CSSFrancis

Copy link
Copy Markdown
Owner

Why

set_clim re-encodes the cached raw frame over the new range. That is the right
default — the codes then span exactly the visible band, so the contrast on screen
gets all 8 bits — but it means the pixels move, and the previous band is gone:
everything outside the new range is saturated to 0/255 in the codes that get
stored.

For a serialised figure that is the difference between having a contrast
control and not having one. build_standalone_html writes the codes plus
raw_min/raw_max, and the JS rebuilds its LUT from display_min/display_max
over that band (_buildLut32). So a saved page can re-window freely inside the
band it was encoded with, and not at all outside it. Encode with set_clim at the
display window and the band is the window: an identity LUT, nothing left to
move.

What

The non-destructive path already existed — inlined in set_clim's tile branch,
where re-quantising would re-encode a full-res frame on every drag tick. This
promotes it to a documented public method so a caller can choose the trade
deliberately:

p = ax.imshow(frame, vmin=lo, vmax=hi)   # quantise over a WIDE band
p.set_display_window(black, white)       # window inside it, no re-encode

Nothing is re-encoded and nothing travels but two floats. The cost is precision —
a window much narrower than the encoding band resolves in coarser steps — so the
docstring says to quantise over the range you want to be able to reach, and
set_clim gains a cross-reference so the pair is discoverable from either side.

No behaviour changes for existing callers.

Tests

anyplotlib/tests/test_plot2d/test_display_window.py, 10 cases pinning the
distinction from both sides:

  • set_display_window leaves image_b64 and the raw_* band untouched;
  • set_clim re-encodes and collapses raw_* onto the window;
  • a wide band keeps room to window in both directions, and set_clim first
    would have thrown that away;
  • set_clim in tile mode and set_display_window produce identical state, so
    the two cannot quietly diverge.

Full suite: 2122 passed, 58 skipped.

Context

Written for SpyDE's report export, where a reader opening a saved HTML report
could not adjust the contrast of a figure at all — the pixels arrive hard-clipped
to whatever window was chosen when the report was written.

Assisted-by: Claude Opus 5 (1M context)

…tising

`set_clim` re-encodes the cached raw frame over the new range. That is the
right default: the codes then span exactly the visible band, so the contrast
on screen gets all 8 bits. But it means the pixels move, and the previous
band is gone — everything outside the new range is saturated to 0/255 in the
codes that get stored.

For a figure that has been SERIALISED that is the difference between having a
contrast control and not having one. `build_standalone_html` writes the codes
plus `raw_min`/`raw_max`, and the JS rebuilds its LUT from `display_min`/
`display_max` over that band (`_buildLut32`) — so a saved page can re-window
freely inside the band it was encoded with, and not at all outside it. Encode
with `set_clim` at the display window and the band IS the window: the identity
LUT, nothing to move.

The non-destructive path already existed, inlined in `set_clim`'s tile branch,
where re-quantising would re-encode a full-res frame on every drag tick. This
promotes it to a documented public method so a caller can choose the trade
deliberately:

    p = ax.imshow(frame, vmin=lo, vmax=hi)   # quantise over a WIDE band
    p.set_display_window(black, white)       # window inside it, no re-encode

The cost is precision — a window much narrower than the encoding band resolves
in coarser steps — so the docstring says to quantise over the range you want to
be able to reach.

Tests pin the distinction from both sides, including that `set_clim` in tile
mode and `set_display_window` produce identical state, so the two cannot
quietly diverge.

Assisted-by: Claude Opus 5 (1M context)
The orphan '+' form is for a change with no PR number; this one has one now,
so towncrier can render the link.

Assisted-by: Claude Opus 5 (1M context)
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.88%. Comparing base (729cb16) to head (4ef7b68).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #61      +/-   ##
==========================================
+ Coverage   90.74%   90.88%   +0.14%     
==========================================
  Files          40       40              
  Lines        4526     4532       +6     
==========================================
+ Hits         4107     4119      +12     
+ Misses        419      413       -6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@CSSFrancis
CSSFrancis merged commit e538884 into main Aug 25, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants