Skip to content

Commit 12cc5e8

Browse files
authored
Expose Colormap.num_colors and document usage for cycling through qualitative colormaps (#55)
* Add num_colors property to Colormap * Add docs * Address PR feedback
1 parent d085c10 commit 12cc5e8

File tree

2 files changed

+143
-4
lines changed

2 files changed

+143
-4
lines changed

docs/colormaps.md

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,143 @@ A [`numpy.ndarray`][], in one of the following formats:
151151

152152
## Usage
153153

154-
### Useful properties
154+
### Using qualitative colormaps
155+
156+
Consider a ColorBrewer qualitative colormap from the default catalog with eight color stops
157+
158+
```python
159+
c = Colormap("colorbrewer:set1_8")
160+
c.color_stops
161+
```
162+
163+
As with all colormaps, the color stop positions are in [0, 1]
164+
165+
```python
166+
ColorStops(
167+
(0.0, Color((0.8941, 0.102, 0.1098))),
168+
(0.14285714285714285, Color((0.2157, 0.4941, 0.7216))),
169+
(0.2857142857142857, Color((0.302, 0.6863, 0.2902))),
170+
(0.42857142857142855, Color((0.5961, 0.3059, 0.6392))),
171+
(0.5714285714285714, Color((1.0, 0.498, 0.0))),
172+
(0.7142857142857142, Color((1.0, 1.0, 0.2))),
173+
(0.8571428571428571, Color((0.651, 0.3373, 0.1569))),
174+
(1.0, Color((0.9686, 0.5059, 0.749)))
175+
)
176+
```
177+
178+
so a floating point value in [0, 1] can be used to map to a color
179+
180+
```python
181+
c(0.2)
182+
```
183+
184+
which will use nearest neighbor interpolation by default to return the second color exactly
185+
186+
```python
187+
Color((0.2157, 0.4941, 0.7216))
188+
```
189+
190+
even though the position is in between the second and third color stops.
191+
192+
However, qualitative colormaps are often used to map integer valued or categorical values to colors.
193+
The behavior of calling a `Colormap` depends on the type of the input.
194+
Calling a `Colormap` with an integer
195+
196+
```python
197+
c(1)
198+
```
199+
200+
indexes directly into the colormap's LUT
201+
202+
```python
203+
Color((0.2157, 0.4941, 0.7216))
204+
```
205+
206+
which is often a more natural operation.
207+
208+
#### Cycling through colors
209+
210+
When using qualitative colormaps to map integer values, sometimes the input domain
211+
may be larger than the number of colors in the colormap.
212+
213+
By default, values less than zero
214+
215+
```python
216+
c(-1)
217+
```
218+
219+
map to the first color
220+
221+
```python
222+
Color((0.8941, 0.102, 0.1098))
223+
```
224+
225+
and values greater than the number of colors
226+
227+
```python
228+
c(9)
229+
```
230+
231+
map to the last color
232+
233+
```python
234+
Color((0.9686, 0.5059, 0.749))
235+
```
236+
237+
This behavior can be customized by providing the `under` and `over` colors when initializing a `Colormap`.
238+
Instead, sometimes it is preferable for the mapping to cycle through the color stops.
239+
240+
There is currently no built-in way to do this when calling the `Colormap`, but it can be done by using the
241+
modulo operator on the input value with `Colormap.num_colors`
242+
243+
```python
244+
c(9 % c.num_colors)
245+
```
246+
247+
which now maps to the first color
248+
249+
250+
```python
251+
Color((0.8941, 0.102, 0.1098))
252+
```
253+
254+
This also works well when using an array as input
255+
256+
```python
257+
c(np.arange(16 % c.num_colors))
258+
```
259+
260+
which returns the cycled RGBA color values in an array output
261+
262+
```python
263+
array([[0.89411765, 0.10196078, 0.10980392, 1. ],
264+
[0.21568627, 0.49411765, 0.72156863, 1. ],
265+
[0.30196078, 0.68627451, 0.29019608, 1. ],
266+
[0.59607843, 0.30588235, 0.63921569, 1. ],
267+
[1. , 0.49803922, 0. , 1. ],
268+
[1. , 1. , 0.2 , 1. ],
269+
[0.65098039, 0.3372549 , 0.15686275, 1. ],
270+
[0.96862745, 0.50588235, 0.74901961, 1. ],
271+
[0.89411765, 0.10196078, 0.10980392, 1. ],
272+
[0.21568627, 0.49411765, 0.72156863, 1. ],
273+
[0.30196078, 0.68627451, 0.29019608, 1. ],
274+
[0.59607843, 0.30588235, 0.63921569, 1. ],
275+
[1. , 0.49803922, 0. , 1. ],
276+
[1. , 1. , 0.2 , 1. ],
277+
[0.65098039, 0.3372549 , 0.15686275, 1. ],
278+
[0.96862745, 0.50588235, 0.74901961, 1. ]])
279+
```
280+
281+
The behavior of calling a `Colormap` with an array depends on its `dtype`.
282+
With a floating point `dtype`, it expects the values to be [0, 1], so the
283+
equivalent call to the above is
284+
285+
```python
286+
c(np.linspace(0, 2, 16, endpoint=False) % 1)
287+
```
288+
289+
which returns the same output array values.
155290

156-
... TODO
157291

158292
## Immutability
159293

src/cmap/_colormap.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ def iter_colors(self, N: Iterable[float] | int | None = None) -> Iterator[Color]
473473
Color objects.
474474
"""
475475
if N is None:
476-
N = len(self.color_stops)
476+
N = self.num_colors
477477
nums = np.linspace(0, 1, N) if isinstance(N, int) else np.asarray(N)
478478
for c in self(nums, N=len(nums)):
479479
yield Color(c)
@@ -572,6 +572,11 @@ def to_css(
572572
max_stops=max_stops, angle=angle, radial=radial, as_hex=as_hex
573573
)
574574

575+
@property
576+
def num_colors(self) -> int:
577+
"""The number of colors in this colormap."""
578+
return len(self.color_stops)
579+
575580
def __setattr__(self, _name: str, _value: Any) -> None:
576581
if getattr(self, "_initialized", False):
577582
raise AttributeError("Colormap is immutable")
@@ -599,7 +604,7 @@ def __eq__(self, other: object) -> bool:
599604
# -------------------------- reprs ----------------------------------
600605

601606
def __repr__(self) -> str:
602-
return f"Colormap(name={self.name!r}, <{len(self.color_stops)} colors>)"
607+
return f"Colormap(name={self.name!r}, <{self.num_colors} colors>)"
603608

604609
def _repr_png_(
605610
self, *, width: int = 512, height: int = 48, img: np.ndarray | None = None

0 commit comments

Comments
 (0)