Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/colormaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ to the [`cmap.Colormap`][] constructor; `cmap` refers to these objects collectiv

*(same as `['blue', (0.4, 'green'), (0.8, 'yellow'), 'red']`)*

A single color yields a constant colormap that returns that color for every
value of `x` (i.e. `f(x) = c`). Any explicit stop position on the single
color is ignored, since position is meaningless for a constant mapping.

- `Colormap(['violet'])` {{ cmap_expr: ['violet'] }}

### `numpy.ndarray`

A [`numpy.ndarray`][], in one of the following formats:
Expand Down
17 changes: 15 additions & 2 deletions src/cmap/_colormap.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ class Colormap:
specifying the position of the color in the gradient. When using color stops,
the stop position values should be in the range [0, 1]. If no scalar stop
positions are given, they will be linearly interpolated between any
neighboring stops (or 0-1 if there are no stops).
neighboring stops (or 0-1 if there are no stops). A single color (e.g.
`["red"]` or `{0.5: "red"}`) yields a constant colormap, where `f(x)`
returns that color for every value of `x`.
- a `dict` mapping scalar values to color-like values: e.g.
`{0.0: "red", 0.5: (0, 1, 0), 1.0: "#0000FF"}`.
- a matplotlib-style [segmentdata
Expand Down Expand Up @@ -1502,7 +1504,18 @@ def _parse_colorstops(
_clr_seq = list(val)

if len(_clr_seq) == 1:
_clr_seq = [None, _clr_seq[0]]
# A single color creates a constant colormap f(x) = c for all x in [0, 1].
# We duplicate the color at positions 0 and 1, discarding any explicit
# stop position the user may have provided (which is meaningless for a
# constant mapping).
item = _clr_seq[0]
if isinstance(item, (tuple, list)) and len(item) == 2:
item = item[1]
elif (isinstance(item, (tuple, list)) and len(item) == 5) or (
isinstance(item, np.ndarray) and item.shape == (5,)
):
item = list(item)[1:]
_clr_seq = [item, item]

_positions: list[float | None] = []
_colors: list[Color] = []
Expand Down
21 changes: 21 additions & 0 deletions tests/test_colormap.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,27 @@ def test_colormap_copy() -> None:
assert cmap1 != {"1234"}


def test_single_color_colormap() -> None:
"""A single-color Colormap should evaluate to that color for all x in [0, 1].

Regression test for #139: previously a single color injected an implicit
transparent stop at position 0 (creating a transparent-to-color gradient).
The new behavior is f(x) = c, represented as two stops at 0 and 1, both
with the same color.
"""
expected = ColorStops.parse([(0.0, "violet"), (1.0, "violet")])
# any of these single-color forms should produce the same constant colormap
for value in (
["violet"],
{0.5: "violet"},
[(0.5, "violet")], # explicit position is dropped — meaningless for f(x)=c
):
cmap = Colormap(value)
assert cmap.color_stops == expected
for x in (0.0, 0.25, 0.5, 0.75, 1.0):
assert cmap(x) == Color("violet")


def test_colormap_errors() -> None:
with pytest.raises(ValueError, match="Colormap 'bad_string' not found"):
Colormap("bad_string")
Expand Down
Loading