Skip to content

fix: preserve interpolation through construction and with_extremes() - #150

Draft
matthiasschabel wants to merge 2 commits into
pyapp-kit:mainfrom
matthiasschabel:fix/interpolation-aliasing
Draft

fix: preserve interpolation through construction and with_extremes()#150
matthiasschabel wants to merge 2 commits into
pyapp-kit:mainfrom
matthiasschabel:fix/interpolation-aliasing

Conversation

@matthiasschabel

@matthiasschabel matthiasschabel commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Two ways a colormap's interpolation mode goes missing.

The constructor writes into stops it does not own. The last line of Colormap.__init__ sets the requested mode on the ColorStops it was handed (src/cmap/_colormap.py:309), and that object usually belongs to the caller: _parse_colorstops returns a ColorStops argument unchanged (:1509), and the isinstance(value, Colormap) branch takes value.color_stops directly (:300).

base = Colormap(["red", "blue"], interpolation="nearest")
Colormap(base, interpolation="linear")   # discard the result

base.interpolation          # "nearest"
base.lut(4)                 # a smooth ramp: it renders linear now

The source keeps reporting "nearest" while rendering "linear", so the object contradicts itself and nothing raises.

with_extremes() drops the mode. It forwards name and category but not interpolation, and _norm_interp(None) is "linear", so one call converts a discrete colormap to a continuous one. An ordered class scheme like colorbrewer:Blues_9 starts blending between classes after with_extremes(bad=...).

The fixes are a shallow copy carrying its own mode, made only when the mode actually differs, and one added argument in with_extremes.

Shallow rather than rebuilt from scratch: a ColorStops can be backed by a user lut_func, and reconstructing would call that callable a second time over 256 values. Callables are part of the public ColormapLike API with no purity requirement, so a second call is neither free nor necessarily harmless.

A related issue probably requiring maintainer input: with_extremes also drops identifier. It's unclear whether an omitted bad/under/over should clear the existing one or leave it.

The last line of Colormap.__init__ wrote the requested interpolation
mode into the ColorStops it was handed, and that object often belongs
to someone else: _parse_colorstops returns a ColorStops argument
unchanged, and the Colormap branch takes value.color_stops directly.

Constructing a linear colormap from a nearest one therefore switched
the source to linear rendering while its own `interpolation` attribute
kept reporting "nearest".

The result now gets a shallow copy carrying its own mode, made only
when the mode differs. Shallow rather than rebuilt, because rebuilding
from `_lut_func` would call the user's callable a second time.

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 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.57%. Comparing base (8040ef7) to head (6f5cf96).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #150      +/-   ##
==========================================
+ Coverage   95.56%   95.57%   +0.01%     
==========================================
  Files         168      168              
  Lines        2186     2192       +6     
==========================================
+ Hits         2089     2095       +6     
  Misses         97       97              

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

with_extremes() forwards name and category but not interpolation, and
_norm_interp(None) is "linear", so it asserted "linear" over whatever
the source had. A single call turned a discrete colormap into a
continuous one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reviewed-By: Codex (gpt-5.6-sol, reasoning effort xhigh)
@matthiasschabel matthiasschabel changed the title fix: don't rewrite the interpolation of stops the caller owns fix: preserve interpolation through construction and with_extremes() Aug 9, 2026
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