feat: colors for +inf, -inf, NaN values - #151
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #151 +/- ##
==========================================
+ Coverage 95.72% 95.78% +0.05%
==========================================
Files 168 168
Lines 2197 2228 +31
==========================================
+ Hits 2103 2134 +31
Misses 94 94 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@matthiasschabel I'm ok with this if it enables you to do things that regularly pop up in your work. My only objection would be the cognitive burden on the more casual user who doesn't particularly care to discriminate between "over" and "positive infinity". So, as long as a user not providing these values has behavior that just degrades to the old behavior, and as long as they are documented well (they are), then I'm fine with this. 👍 I think i'm most concerned with now having A couple questions. (some of these might be broken before this PR, but now is as good a time as any to address them). Do all of these over/under/inf/neg-inf values get preserved in:
(it occurs to me that some of that might not be tested) |
Thanks for the close read and for being agreeable! I do think this proposal addresses legitimate use cases and closes the circle on handling special/exceptional values in cmap. The implementation should be purely additive and takes care to degrade in the predictable/expected backward-compatible path, so transparent to current users and hopefully the cognitive overhead is minimal. When new classes are unused the routing appends fallback-resolved rows to a call-local copy of the LUT, so a class with no color of its own lands on exactly the row it lands on today.
Here's my argument in favor (in addition to the completeness argument):
None of those four channels preserve the existing
I think we should conform to matplotlib wherever it has a position:
Would you prefer that these get broken up into more granular, separate PRs or lumped in with this one? |
|
Thank you for the thorough response! Yeah I thought that might be the case (that it was broken before this PR). And I agree that we should mirror mpl where there is prior art. I suppose we should go ahead and split that fix out into a new PR. And it needn't hold this one up either. I would like to get a quick opinion from @jni on the API addition, but then this is all good by me. Thanks again! |
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)
0587c89 to
9380611
Compare
|
Hello, and thanks for flagging me! I'm flattered. 😂 I don't have a strong opinion about the extra values. I can see that they are in some sense "interesting" to preserve (+inf being something that could never get captured into the colormap regardless of how you set the contrast limits). On the other hand, I have gotten by this far without needing to make this distinction, and so have millions of matplotlib users, it seems. So, the extra maintenance and cognitive burden of the added values is worth weighing against the marginal added utility. Some things that would demonstrate that utility as far as I'm concerned:
The maintenance burden is quite small but we've all seen tiny maintenance costs accumulate over time to create a large maintenance cost, so I do think it's worth being cautious here. None of this is to outright block this PR, just to pause and assess a bit. |
matplotlib already supports differentiating NaN from masked in line plots: https://matplotlib.org/stable/gallery/lines_bars_and_markers/masked_demo.html I, obviously, also felt it was important enough that I supported it in the MATLAB scientific visualization code that I am hoping to replace with napari. But I think the strongest argument is that the existing support is partial yet incomplete in that it doesn't cover the full range of cases that are possible (masked being the one where there is some conceptual overlap with things like label arrays since a mask is really just a binary label array). But, from the underlying numerics, there are only -inf/under range/in range/over range/+inf and nan representations for a floating point value - that is complete. |
I think that example shows the opposite: masked and NaN values are treated the same way by the plotting machinery. There's no API (afaict?) for doing different things in the two cases. |
I think that's the key when it comes to motivating this contribution.
I totally get the motivation. What I'm asking is whether this distinction is standard practice in your field (and if so, whether you can demonstrate this with some references to some paper figures, for example), or whether you are pushing the cutting edge of data visualisation (in and outside your field). The latter is valuable and commendable, but may not belong in a foundational library as much as in downstream libraries. (Again, that's a "may", I'm not strongly opposed here. It's just a different conversation.) |
It's not really pushing the envelope, certainly not in remote sensing (one of my former lives). Here's an example of routinely processed satellite radiometry data : https://images.remss.com/amsr/amsr2_image_view_v08.2.html?&time=day&sat=amsr2&year=2026&month=8&day=24&pass=ascending&product=vapor where they composite a land mask (to show terrain elevation) with values derived from satellite measurements, where the missing data is indicated in black, land is masked, ice is white, and water vapor is color mapped in a certain range (OOB values are often given distinct colors, but not here). Maybe I'm just not familiar enough with how napari works, but it seems like this would be difficult to accomplish in the current state? There's a bit of a chicken-and-egg problem in the sense that, if something is not supported or difficult to accomplish, people will generally just not bother, even if it's the right thing to do... Here's a paper trying to address various sources of measurement/modeling error in pharmacokinetic modeling (Figures 3/4) : https://www.sciencedirect.com/science/article/pii/S0730725X1400321X#f0015 (
I see what you're saying here, both regarding potential complexity and the question of where this belongs. I'm just not clear if moving mask handling into, e.g. napari would actually simplify things. |
Keep the proposal scoped to negative infinity, NaN, and positive infinity in addition to the existing under and over behavior. Masked arrays continue to use the existing bad color, including when they hide an infinity or NaN.
|
Since adding |


Closes #144.
Adds three optional colors so exceptional float values can be told apart:
neg_inf,pos_inf,nan. Today-infis indistinguishable from any other under-rangevalue, and NaN is indistinguishable from
bad. Log transformed signal data andsaturated logistic regression both produce infinities worth marking rather than blending
into the ends of the scale.
Each new color falls back to the one its class uses now, so nothing changes for an existing
colormap:
neg_infunder, then the first ramp colorpos_infover, then the last ramp colornanbad, then transparentbadstays the fallback fornanrather than being replaced by it, socode that sets it is unaffected and either child can be set alone.
Two things the diff does not show:
xa *= N. That multiply overflows large finite valuesto infinity (
float1665504 does at N=256), and those are out of range, not infinite.There is a test for it, because classifying after the multiply looks right and is not.
to_naparinow prefersnan_coloroverbad_colorfor napari'snan_color, since it isthe one converter target that represents the class. matplotlib's
badcovers NaN and maskedtogether, so
badis still what goes there.Depends on #150, which this branches from. Only the last commit is mine; the first two are
#150's.