Skip to content

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

Merged
tlambert03 merged 3 commits into
pyapp-kit:mainfrom
matthiasschabel:fix/interpolation-aliasing
Aug 12, 2026
Merged

fix: preserve interpolation through construction and with_extremes()#150
tlambert03 merged 3 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.

Related (but not addressed here): with_extremes also drops identifier. I'm unclear whether an omitted bad/under/over should clear the existing one or leave it.

@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.72%. Comparing base (d1521b1) to head (c144c46).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #150      +/-   ##
==========================================
+ Coverage   95.70%   95.72%   +0.01%     
==========================================
  Files         168      168              
  Lines        2191     2197       +6     
==========================================
+ Hits         2097     2103       +6     
  Misses         94       94              

☔ 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 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
matthiasschabel and others added 2 commits August 10, 2026 10:42
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)
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 force-pushed the fix/interpolation-aliasing branch from 6f5cf96 to 85536bd Compare August 10, 2026 17:53
@matthiasschabel
matthiasschabel marked this pull request as ready for review August 10, 2026 18:24
@tlambert03

Copy link
Copy Markdown
Member

thanks!

@tlambert03
tlambert03 merged commit f0a4aec into pyapp-kit:main Aug 12, 2026
26 of 27 checks passed
@matthiasschabel
matthiasschabel deleted the fix/interpolation-aliasing branch August 13, 2026 18:37
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