Skip to content

fix: preserve colormap state through reversed(), with_extremes(), pickle, and as_dict() - #155

Open
matthiasschabel wants to merge 2 commits into
pyapp-kit:mainfrom
matthiasschabel:fix/state-preservation
Open

fix: preserve colormap state through reversed(), with_extremes(), pickle, and as_dict()#155
matthiasschabel wants to merge 2 commits into
pyapp-kit:mainfrom
matthiasschabel:fix/state-preservation

Conversation

@matthiasschabel

@matthiasschabel matthiasschabel commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Depends on #151. Follow-up to the four preservation questions from that PR:

None of the four channels preserved the extreme colors, and two of them dropped the interpolation
mode as well. Verified on main before this change:

  • reversed() passes only stops, name, and category, so a nearest colormap comes back `linear with no extremes.
  • with_extremes() clears anything not repeated in the call.
  • __reduce__ carries only color_stops, so pickle.loads(pickle.dumps(cm)) == cm is already False for any colormap with bad set.
  • as_dict() has no keys for interpolation or the extremes, and it is what both the pydantic serializer and _json_encode emit.

Following matplotlib 3.11 where it has a position:

  • reversed() swaps under/over and neg_inf/pos_inf, which name the ends they extend, and preserves the rest.
  • with_extremes() preserves anything not passed. Clearing a single color now means constructing a new Colormap, the same limitation matplotlib has. This is the one backward-incompatible change here.
  • __reduce__ carries the full constructor state.
  • as_dict() gains optional keys, written only when they hold non-default state, so existing payloads are unchanged.

Two changes that are more than preservation:

  • Colormap("x_r") now swaps the catalog record's under/over, so it agrees with Colormap("x").reversed(). napari:HiLo is the only affected entry. An explicit under=/over= argument still lands on the end the caller named.
  • The pydantic serializer emitted a catalog colormap's qualified name unconditionally, so Colormap("viridis", under="red") came back as plain viridis, and Colormap("viridis_r") came back unreversed because info is looked up under the stripped name. It now emits the name only when the name alone rebuilds the same as_dict().

Left unchanged:

  • Pickle passes color_stops as the object rather than as_dict()'s samples, so a colormap backed by a lut function keeps the function.
  • A round trip through the dict form does not restore info, which is set only from a string or another Colormap, so an unpickled catalog colormap serializes as a dict rather than as its name.
  • identifier follows the name: reversed() and shifted() rename, so it re-derives; with_extremes() does not, so it is preserved. One consequence: `cm.reversed().reversed() restores the name but not an explicitly supplied identifier - not 100% sure this is the correct behavior.

matthiasschabel and others added 2 commits August 13, 2026 09:33
cmap colors three exceptional classes: under, over, and bad. Floating point
data has more. Negative and positive infinity are indistinguishable from
ordinary out-of-range values, and NaN is indistinguishable from a masked entry.

Adds neg_inf, pos_inf, nan, and masked. Each falls back to the color its class
uses now: neg_inf to under, pos_inf to over, nan and masked to bad. bad is kept
as the joint fallback for both of its children, so code that sets it is
unaffected and either child may be set alone.

Routing appends four fallback-resolved rows to a call-local copy of the
over/under LUT, so a class with no color of its own lands on exactly the row it
lands on now. Colormap.lut() is unchanged.

The infinity masks are taken before the input is scaled by N: that multiply
overflows large finite values to infinity (float16 65504 does it), and those
are out of range rather than infinite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reviewed-By: Codex (gpt-5.6-sol, reasoning effort xhigh)
…kle, and as_dict()

None of the four channels carried the extreme colors, and reversed() and
with_extremes() dropped the interpolation mode as well. Following matplotlib
where it has a position: reversed() swaps the directional colors and keeps the
rest, with_extremes() keeps anything not passed, __reduce__ carries the
constructor state alongside the stops, and as_dict() gains optional keys that
are written only when set, so existing payloads are unchanged.

The pydantic serializer emitted a catalog colormap's qualified name
unconditionally, which discarded any added extremes and turned "viridis_r"
back into plain viridis. It now emits the name only when the name alone
rebuilds the same colormap.

Colormap("x_r") swaps the catalog record's under and over, so it agrees with
Colormap("x").reversed().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reviewed-By: Codex (gpt-5.6-sol, reasoning effort xhigh)
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.95%. Comparing base (f0a4aec) to head (4302481).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #155      +/-   ##
==========================================
+ Coverage   95.72%   95.95%   +0.23%     
==========================================
  Files         168      168              
  Lines        2197     2250      +53     
==========================================
+ Hits         2103     2159      +56     
+ Misses         94       91       -3     

☔ 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.

@matthiasschabel
matthiasschabel marked this pull request as ready for review August 14, 2026 19:39
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.

1 participant