Skip to content

Commit 66b5641

Browse files
Merge pull request #348 from tpvasconcelos/274-color_discrete_map
[274] Implement new `color_discrete_map` param
2 parents 4cc1f63 + 2fa71d4 commit 66b5641

19 files changed

Lines changed: 399 additions & 74 deletions

File tree

cicd_utils/cicd/test_helpers.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
from __future__ import annotations
22

33
import contextlib
4+
import copy
45
import pickle
56
import sys
67
from importlib.util import module_from_spec, spec_from_file_location
78
from pathlib import Path
89
from typing import TYPE_CHECKING, Any, TypeVar, cast
910
from unittest.mock import MagicMock, patch
1011

12+
import plotly.io
13+
import pytest_socket
14+
from plotly import graph_objects as go
15+
1116
if TYPE_CHECKING:
1217
from collections.abc import Iterator
1318
from importlib.abc import Loader
@@ -17,6 +22,24 @@
1722
from plotly.graph_objs import Figure
1823

1924

25+
_PLOTLY_SHOW_DEEPCOPY = copy.deepcopy(plotly.io.show)
26+
27+
28+
def plotly_show_browser(fig: go.Figure, renderer: str = "browser", **kwargs: Any) -> None:
29+
"""Display a Plotly figure in a new browser tab.
30+
31+
This temporarily enables network connections (if disabled by pytest-socket)
32+
and ensures the real (unpatched) `plotly.io.show()` is used. Useful for
33+
debugging test failures by viewing the actual rendered figure in a browser
34+
window.
35+
"""
36+
try:
37+
pytest_socket.enable_socket()
38+
_PLOTLY_SHOW_DEEPCOPY(fig=fig.to_dict(), renderer=renderer, **kwargs)
39+
finally:
40+
pytest_socket.disable_socket()
41+
42+
2043
@contextlib.contextmanager
2144
def patch_plotly_show() -> Iterator[None]:
2245
"""Patch the :func:`plotly.io.show()` function to skip any rendering steps

cicd_utils/ridgeplot_examples/_lincoln_weather.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Literal
3+
from typing import TYPE_CHECKING
44

55
if TYPE_CHECKING:
6-
from collections.abc import Collection
7-
86
import plotly.graph_objects as go
97

10-
from ridgeplot._color.interpolation import SolidColormode
11-
from ridgeplot._types import Color, ColorScale
12-
138

149
def main(
15-
colorscale: ColorScale | Collection[Color] | str | None = "Inferno",
16-
colormode: SolidColormode | Literal["fillgradient"] = "fillgradient",
10+
color_discrete_map: dict[str, str] | None = None,
1711
) -> go.Figure:
1812
import numpy as np
1913

@@ -33,9 +27,10 @@ def main(
3327

3428
fig = ridgeplot(
3529
samples=samples,
30+
labels=[["Min Temperature [F]", "Max Temperature [F]"]] * len(months),
3631
row_labels=months,
37-
colorscale=colorscale,
38-
colormode=colormode,
32+
colorscale="Inferno",
33+
color_discrete_map=color_discrete_map,
3934
bandwidth=4,
4035
kde_points=np.linspace(-40, 110, 400),
4136
spacing=0.3,

cicd_utils/ridgeplot_examples/_lincoln_weather_red_blue.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010

1111
def main() -> go.Figure:
1212
fig = lincoln_weather(
13-
colorscale=["orangered", "deepskyblue"],
14-
colormode="trace-index-row-wise",
13+
color_discrete_map={
14+
"Min Temperature [F]": "deepskyblue",
15+
"Max Temperature [F]": "orangered",
16+
}
1517
)
1618
return fig
1719

docs/getting_started/getting_started.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ samples = [
134134
# And finish by styling it up to your liking!
135135
fig = ridgeplot(
136136
samples=samples,
137+
labels=[["Min Temperature [F]", "Max Temperature [F]"]] * len(months),
137138
row_labels=months,
138139
colorscale="Inferno",
139140
bandwidth=4,
@@ -228,6 +229,7 @@ Finally, we can pass the {py:paramref}`~ridgeplot.ridgeplot.samples` list to the
228229
```python
229230
fig = ridgeplot(
230231
samples=samples,
232+
labels=[["Min Temperature [F]", "Max Temperature [F]"]] * len(months),
231233
row_labels=months,
232234
colorscale="Inferno",
233235
bandwidth=4,
@@ -237,8 +239,8 @@ fig = ridgeplot(
237239

238240
fig.update_layout(
239241
title="Minimum and maximum daily temperatures in Lincoln, NE (2016)",
240-
height=650,
241-
width=950,
242+
height=600,
243+
width=800,
242244
font_size=14,
243245
plot_bgcolor="rgb(245, 245, 245)",
244246
xaxis_gridcolor="white",
@@ -261,15 +263,24 @@ fig.show()
261263
We are currently investigating the best way to support all color options available in Plotly Express. If you have any suggestions or requests, or just want to track the progress, please check out {gh-issue}`226`.
262264
:::
263265

264-
The {py:func}`~ridgeplot.ridgeplot()` function offers flexible customisation options that help you control the automatic coloring of ridgeline traces. Take a look at {py:paramref}`~ridgeplot.ridgeplot.colorscale`, {py:paramref}`~ridgeplot.ridgeplot.colormode`, and {py:paramref}`~ridgeplot.ridgeplot.opacity` for more information.
266+
The {py:func}`~ridgeplot.ridgeplot()` function offers flexible customisation options that help you control the exact coloring of ridgeline traces. Take a look at {py:paramref}`~ridgeplot.ridgeplot.colorscale`, {py:paramref}`~ridgeplot.ridgeplot.colormode`, {py:paramref}`~ridgeplot.ridgeplot.color_discrete_map`, {py:paramref}`~ridgeplot.ridgeplot.opacity`, and {py:paramref}`~ridgeplot.ridgeplot.line_color` for a detailed description of the available options.
267+
268+
As a simple (but quite common) example, we'll try to adjust the output of the previous example to use different discrete colors for the minimum and maximum temperature traces. Specifically, we'll set all minimum temperature traces to a shade of blue and all maximum temperature traces to a shade of red. To achieve this, we just need to set the {py:paramref}`~ridgeplot.ridgeplot.color_discrete_map` parameter to a dictionary that maps the trace labels to the desired colors.
265269

266-
To demonstrate how these options can be used, we can try to adjust the output from the previous example to use different colors for the minimum and maximum temperature traces. For instance, setting all minimum temperature traces to a shade of blue and all maximum temperature traces to a shade of red. To achieve this, we just need to adjust the {py:paramref}`~ridgeplot.ridgeplot.colorscale` and {py:paramref}`~ridgeplot.ridgeplot.colormode` parameters in the call to the {py:func}`~ridgeplot.ridgeplot()` function. _i.e._,
270+
:::{note}
271+
Because the {py:paramref}`~ridgeplot.ridgeplot.color_discrete_map` parameter takes precedence over the {py:paramref}`~ridgeplot.ridgeplot.colorscale` and {py:paramref}`~ridgeplot.ridgeplot.colormode` parameters, we can keep them as they are in the previous example. However, since their behavior will be overridden by {py:paramref}`~ridgeplot.ridgeplot.color_discrete_map`, it is a good practice to remove them from the function call to avoid any confusion.
272+
:::
267273

268274
```python
269275
fig = ridgeplot(
270-
# Same options as before, with only the following changes:
271-
colorscale=["orangered", "deepskyblue"],
272-
colormode="trace-index-row-wise",
276+
# Same options as before, with the
277+
# addition of `color_discrete_map`
278+
# ...
279+
color_discrete_map={
280+
"Min Temperature [F]": "deepskyblue",
281+
"Max Temperature [F]": "orangered",
282+
}
283+
# ...
273284
)
274285
```
275286

docs/reference/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Unreleased changes
88
- Dropped support for Python 3.9, in accordance with the official Python support policy[^1] ({gh-pr}`345`)
99
- Bump project classification from Pre-Alpha to Alpha ({gh-pr}`336`)
1010

11+
### Features
12+
13+
- Implement a new `color_discrete_map` parameter to allow users to specify custom colors for each trace ({gh-pr}`348`)
14+
1115
### CI/CD
1216

1317
- Bump actions/github-script from 7 to 8 ({gh-pr}`338`)

src/ridgeplot/_figure_factory.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
ShallowTraceTypesArray,
2424
TraceType,
2525
TraceTypesArray,
26-
is_flat_str_collection,
2726
is_shallow_trace_types_array,
2827
is_trace_type,
2928
is_trace_types_array,
@@ -50,9 +49,9 @@ def normalise_trace_types(
5049
trace_types = cast("TraceTypesArray", [[trace_types] * len(row) for row in densities])
5150
elif is_shallow_trace_types_array(trace_types):
5251
trace_types = nest_shallow_collection(trace_types)
53-
trace_types = normalise_row_attrs(trace_types, l2_target=densities)
52+
trace_types = normalise_row_attrs(attrs=trace_types, l2_target=densities)
5453
elif is_trace_types_array(trace_types):
55-
trace_types = normalise_row_attrs(trace_types, l2_target=densities)
54+
trace_types = normalise_row_attrs(attrs=trace_types, l2_target=densities)
5655
else:
5756
raise TypeError(f"Invalid trace_type: {trace_types}")
5857
return trace_types
@@ -67,17 +66,15 @@ def normalise_trace_labels(
6766
ids = iter(range(1, n_traces + 1))
6867
trace_labels = [[f"Trace {next(ids)}" for _ in row] for row in densities]
6968
else:
70-
if is_flat_str_collection(trace_labels):
71-
trace_labels = nest_shallow_collection(trace_labels)
72-
trace_labels = normalise_row_attrs(trace_labels, l2_target=densities)
69+
trace_labels = normalise_row_attrs(attrs=trace_labels, l2_target=densities)
7370
return trace_labels
7471

7572

7673
def normalise_row_labels(trace_labels: LabelsArray) -> Collection[str]:
7774
return [",".join(ordered_dedup(row)) for row in trace_labels]
7875

7976

80-
def update_layout(
77+
def _update_layout(
8178
fig: go.Figure,
8279
row_labels: Collection[str] | Literal[False],
8380
tickvals: list[float],
@@ -123,12 +120,13 @@ def update_layout(
123120

124121
def create_ridgeplot(
125122
densities: Densities,
123+
trace_labels: LabelsArray | ShallowLabelsArray | None,
126124
trace_types: TraceTypesArray | ShallowTraceTypesArray | TraceType,
127125
row_labels: Collection[str] | None | Literal[False],
128126
colorscale: ColorScale | Collection[Color] | str | None,
129-
opacity: float | None,
130127
colormode: Literal["fillgradient"] | SolidColormode,
131-
trace_labels: LabelsArray | ShallowLabelsArray | None,
128+
color_discrete_map: dict[str, str] | None,
129+
opacity: float | None,
132130
line_color: Color | Literal["fill-color"],
133131
line_width: float | None,
134132
spacing: float,
@@ -159,6 +157,15 @@ def create_ridgeplot(
159157
elif row_labels is not False and len(row_labels) != n_rows:
160158
raise ValueError(f"Expected {n_rows} row_labels, got {len(row_labels)} instead.")
161159

160+
if color_discrete_map:
161+
missing_labels = {
162+
label for row in trace_labels for label in row if label not in color_discrete_map
163+
}
164+
if missing_labels:
165+
raise ValueError(
166+
f"The following labels are missing from 'color_discrete_map': {missing_labels}",
167+
)
168+
162169
# Force cast certain arguments to the expected types
163170
line_width = float(line_width) if line_width is not None else None
164171
spacing = float(spacing)
@@ -176,12 +183,18 @@ def create_ridgeplot(
176183
x_min=x_min,
177184
x_max=x_max,
178185
)
179-
solid_colors = compute_solid_colors(
180-
colorscale=colorscale,
181-
colormode=colormode if colormode != "fillgradient" else "mean-minmax",
182-
opacity=opacity,
183-
interpolation_ctx=interpolation_ctx,
184-
)
186+
if color_discrete_map:
187+
solid_colors = (
188+
(color_discrete_map[label] for label in row_trace_labels)
189+
for row_trace_labels in trace_labels
190+
)
191+
else:
192+
solid_colors = compute_solid_colors(
193+
colorscale=colorscale,
194+
colormode=colormode if colormode != "fillgradient" else "mean-minmax",
195+
opacity=opacity,
196+
interpolation_ctx=interpolation_ctx,
197+
)
185198

186199
tickvals: list[float] = []
187200
fig = go.Figure()
@@ -207,14 +220,14 @@ def create_ridgeplot(
207220
fig=fig,
208221
coloring_ctx=ColoringContext(
209222
colorscale=colorscale,
210-
colormode=colormode,
223+
fillgradient=colormode == "fillgradient" and not color_discrete_map,
211224
opacity=opacity,
212225
interpolation_ctx=interpolation_ctx,
213226
),
214227
)
215228
ith_trace += 1
216229

217-
fig = update_layout(
230+
fig = _update_layout(
218231
fig,
219232
row_labels=row_labels,
220233
tickvals=tickvals,

src/ridgeplot/_kde.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def normalize_sample_weights(
9494
return [[sample_weights] * len(row) for row in samples]
9595
if _is_shallow_sample_weights(sample_weights):
9696
sample_weights = nest_shallow_collection(sample_weights)
97-
sample_weights = normalise_row_attrs(sample_weights, l2_target=samples)
97+
sample_weights = normalise_row_attrs(attrs=sample_weights, l2_target=samples)
9898
return sample_weights
9999

100100

src/ridgeplot/_obj/traces/area.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class AreaTrace(RidgeplotTrace):
1717
_DEFAULT_LINE_WIDTH: ClassVar[float] = 1.5
1818

1919
def _get_coloring_kwargs(self, ctx: ColoringContext) -> dict[str, Any]:
20-
if ctx.colormode == "fillgradient":
20+
if ctx.fillgradient:
2121
if ctx.opacity is not None:
2222
# HACK: Plotly doesn't yet support setting the fill opacity
2323
# for traces with `fillgradient`. As a workaround, we

src/ridgeplot/_obj/traces/bar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class BarTrace(RidgeplotTrace):
1616
_DEFAULT_LINE_WIDTH: ClassVar[float] = 0.5
1717

1818
def _get_coloring_kwargs(self, ctx: ColoringContext) -> dict[str, Any]:
19-
if ctx.colormode == "fillgradient":
19+
if ctx.fillgradient:
2020
color_kwargs = dict(
2121
marker_line_color=self.line_color,
2222
marker_color=[

src/ridgeplot/_obj/traces/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
if TYPE_CHECKING:
1212
from plotly import graph_objects as go
1313

14-
from ridgeplot._color.interpolation import InterpolationContext, SolidColormode
14+
from ridgeplot._color.interpolation import InterpolationContext
1515
from ridgeplot._types import Color, ColorScale, DensityTrace
1616

1717

@@ -44,7 +44,7 @@
4444
@dataclass
4545
class ColoringContext:
4646
colorscale: ColorScale
47-
colormode: Literal["fillgradient"] | SolidColormode
47+
fillgradient: bool
4848
opacity: float | None
4949
interpolation_ctx: InterpolationContext
5050

0 commit comments

Comments
 (0)