Skip to content

Single-color colormaps add an extra value #139

Description

@aafkegros

Hi,

For my implementation it's easiest to describe single colors as a 1-valued colormap. Currently this is not possible in cmap as the colormap by default adds a black transparent 0-value:

>>> import cmap
>>> cmap.Colormap([[0.5,1.,1.,1.]]).as_dict()
{'name': 'custom colormap', 'identifier': 'custom_colormap', 'category': None, 'value': [(0.0, [0, 0, 0, 0]), (1.0, [0.5, 1.0, 1.0, 1.0])]}
>>> cmap.Colormap(['violet']).as_dict()
{'name': 'custom colormap', 'identifier': 'custom_colormap', 'category': None, 'value': [(0.0, [0, 0, 0, 0]), (1.0, [0.9333333333333333, 0.5098039215686274, 0.9333333333333333, 1])]}

Is this a feature (and if so should i be switching to Color for these cases), or is this unintended?

Thanks!

Activity

  1. tlambert03 commented on May 2, 2026

    @tlambert03
    Member

    hey @aafkegros

    I would say that single colors are intended to be represented by Color and not Colormap ... and it is intended (though, perhaps we should re-examine) that all colormaps have at least two control points. I'm definitely open to considering that the magic injection of a transparent control point in the case of a single color colormap was perhaps a bad idea.
    Can you elaborate a little bit on the case here? what are the details of your use case and implementation? what do you need the object to do for you? Why not use a single Color object? or, could you just explicitly use Colormap(['violet', 'violet']) instead?

    happy to consider changes, just need a bit more context

  2. aafkegros commented on May 3, 2026

    @aafkegros
    ContributorAuthor

    So this came up in the context of my blender package Microscopy Nodes, and is partially informed by the Blender shader UI/GUI. I already do a workaround for this to make it work, but it might be nice to have this cmap-native.

    Blender shader UI uses node trees of functions connected by i/o edges. In this i map pixel intensities to color values that then hand these colors to the shader, using a Color Ramp:

    Image

    For a single color i replace this with a ramp with a single stop, having multiple stops would make this less editable, as you now still have a single color picker to reassign (if you click on the colored box):

    Image

    Equivalent node tree to that would be a Color node, but the issue with this is that it loses the input into the node, as the Color is treated as a static input and not an operation on the input signal, which makes it much harder to edit and replace into a more complicated colorramp, as you then need to reconnect the input node:

    Image

    I think the core of this Blender design is that a Colormap is an operation onto an input signal, and a Color is static. This makes the interpretation of a single stop colormap different from a color:

    • a single stop colormap is f(x) = c
    • a color is c

    I think also outside of Blender this is a model that could work quite well, and to me the cmap docs read as if this is the same model:

    • from Color: The cmap.Color type represents an individual color.
    • from Colormap: The cmap.Colormap [...] is a mapping from a scalar value to a color.
  3. tlambert03 commented on May 3, 2026

    @tlambert03
    Member

    yes absolutely, the mapping is exactly the same: Colormap is a mapping from a scalar to a color, and Color is a single color.

    So, is it correct to interpret your issue here as a request/question to see whether Colormap([single_color]) should in fact just be a map/function that always returns f(x) = single_color, regardless of the input? (Rather than an implicit ramp from transparent to that color). Just clarifying, since I haven't seen that explicitly said yet... I assume it's the main request here and I think I agree with it. It would be a breaking change, but it's the less magic, more principled design... and I'm fine with it. is that the crux of the request?

  4. aafkegros commented on May 4, 2026

    @aafkegros
    ContributorAuthor

    Yes, i would indeed want this.
    I think most straightforwardly a Colormap(["violet"]) evaluates to {'name': 'custom colormap', 'identifier': 'custom_colormap', 'category': None, 'value': [(0.5, [0.9333333333333333, 0.5098039215686274, 0.9333333333333333, 1])]}
    For me having the stop at 0.5 would make the most sense/be most convenient, but this is also changeable

  5. tlambert03 commented on May 4, 2026

    @tlambert03
    Member

    Is there a reason you emphasize what that serialized state dictionary looks like, rather than the behavior of the mapping (ie: that f(x) returns a constant color regardless of x)?

    The most natural/simple way to accomplish this would be to just continue to make 2 stops (at 0 and 1), but just duplicate the single value rather than using transparent at 0. A single stop at 0.5 would not actually make a valid mapping (without special casing it elsewhere in the actual lookup logic).

    Why do you emphasize the way the dictionary looks, instead of the behavior of the colormap?

  6. aafkegros commented on May 4, 2026

    @aafkegros
    ContributorAuthor

    Ah, in my case it would be most convenient to actually have a single value representation, although I agree the behavior you describe could also work. I would probably still adapt my implementation to still get 1 value.

    In my implementation a colormap is an editable thing, which means I want to facilitate easy interaction. If I make a Colormap with one stop, there is only one stop to interactively edit in my final representation, making interaction easier. I also just find it more intuitive to have one stop for an iterable of length 1, rather than duplicating it.

    and in the current implementation, a 1-valued cmap would still evaluate correctly:

    >>> cmap.Colormap({0.3:"violet", 0.7:"white"}, over="black", under='black')(.1)
    Color('violet')
    >>> cmap.Colormap({0.3:"violet", 0.7:"white"}, over="black", under='black')(.999)
    Color('white')
    >>> cmap.Colormap({0.3:"violet", 0.7:"white"}, over="black", under='black')(0)
    Color('violet')
    

    as the domain is effectively [0, 1) and the range is extended from the defined colormap stops.
    where only outside [0, 1] it returns over|under

    >>> cmap.Colormap({0.3:"violet", 0.7:"white"}, over="black", under='black')(-.1)
    Color('black')
    >>> cmap.Colormap({0.3:"violet", 0.7:"white"}, over="black", under='black')(1.1)
    Color('black')
    

    With this behaviour, a single stop would still be a [0, 1) function f(x) = c but i also get how this is less explicit without a range given.

    Also in testing this I found that the value at 1 is a bit unexpected, returning the lower bound:

    >>> cmap.Colormap({0.3:"violet", 0.7:"white"}, over="black", under='black')(1)
    Color('violet')
    

    So in general i agree with the behaviour we discussed above, but I would still prefer a single-stop representation (e.g. at 0.5), rather than duplicating the value at 0 and 1, as that maps much more cleanly to an editable/UI representation in my case.

  7. tlambert03 commented on May 4, 2026

    @tlambert03
    Member

    In my implementation a colormap is an editable thing, which means I want to facilitate easy interaction

    ok, but a cmap.Colormap() is currently designed to be immutable (and I'd prefer not to change that). So, I think it might be valuable here for us to differentiate between mutating the actual cmap.Colormap instance, and whatever it is you need to do in your implementation to "swap" one colormap for another in your ui when the user changes a color? ... and whatever it is you need to do in your UI to show a colormap with [color, color] as a swatch of color.

    and in the current implementation, a 1-valued cmap would still evaluate correctly:

    I'm afraid I'm confused by your examples, and what they are showing. But I think we're in agreement that:

    Colormap([c]) should mean f(x) = c for x ∈ [0, 1]

    but I would still prefer a single-stop representation (e.g. at 0.5), rather than duplicating the value at 0 and 1, as that maps much more cleanly to an editable/UI representation in my case.

    ... so i think this is the only thing we need to decide on right? Can I ask why you prefer this representation? how exactly do you intend to use it? are you indeed hoping to have a single cmap.Colormap instance that you mutate as the user edits the color? Alternatively, are you hoping to avoid the small conditional of "if len(set(cmap.color_stops)) == 1" when presenting the color swatch?

  8. tlambert03 commented on May 4, 2026

    @tlambert03
    Member

    (feel free to point to the lines of code in microscopy nodes where you are actually using this if it helps to contextualize)

  9. aafkegros commented on May 4, 2026

    @aafkegros
    ContributorAuthor

    For the editability:

    I think I was a bit too caught up in my own implementation there. I’m not actually mutating the cmap.Colormap itself. Instead I generate a Blender-native color ramp object (here).

    Right now this means I explicitly detect and remove the transparent value from the colormap for single color (here). But this is not essential, I can just handle whatever cmap gives me.


    For my examples:
    What felt natural to me comes from how Blender behaves, where the boundary colors are implicitly extended to fill the [0, 1] range:

    • if the first stop is at 0.2, I expect [0, 0.2] to evaluate to that color
    • if the last stop is at 0.6, I expect [0.6, 1] to evaluate to that color

    So if that holds, a single stop at any position would imply a constant color over [0, 1].

    My examples were just trying to show that cmap already behaves like this in practice, since values outside the defined stops (but within [0, 1]) evaluate to the nearest boundary color. I acknowledge that this is less explicit than a stop at 0 and one at 1.


    More generally:

    The reason I focused on the single-stop representation is that it felt like the minimal form to me, and I was trying to make my expected behavior explicit in response to a perceived sense of unclarity after my first messages:

    "Just clarifying, since I haven't seen that explicitly said yet."

    That said, I agree that Colormap([c]) behaving as f(x) = c is the only important part. I can also treat {0: c, 1: c} as equivalent on my side and generate a single-stop ramp in my UI from that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions