From 04f93a4d36bad16f02b5dee01bb0fbedd6c9adde Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Fri, 24 Jul 2026 20:07:26 +0200 Subject: [PATCH 1/5] Add a new template parameter to ridgeplot() Allow users to specify a Plotly figure template (by name, as a go.layout.Template object, or as a dict) directly through the new `template` parameter. When no explicit `colorscale` is specified, the default colorscale is now also inferred from the specified template. --- src/ridgeplot/_color/colorscale.py | 20 ++++++++-- src/ridgeplot/_figure_factory.py | 11 +++++- src/ridgeplot/_ridgeplot.py | 23 ++++++++++- src/ridgeplot/_template.py | 31 +++++++++++++++ src/ridgeplot/_types.py | 11 ++++++ tests/unit/color/test_colorscale.py | 17 ++++++++ tests/unit/test_figure_factory.py | 1 + tests/unit/test_ridgeplot.py | 60 +++++++++++++++++++++++++++++ tests/unit/test_template.py | 44 +++++++++++++++++++++ 9 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 src/ridgeplot/_template.py create mode 100644 tests/unit/test_template.py diff --git a/src/ridgeplot/_color/colorscale.py b/src/ridgeplot/_color/colorscale.py index 44f93063..4b935a53 100644 --- a/src/ridgeplot/_color/colorscale.py +++ b/src/ridgeplot/_color/colorscale.py @@ -14,6 +14,8 @@ if TYPE_CHECKING: from collections.abc import Collection + import plotly.graph_objects as go + from ridgeplot._types import Color, ColorScale @@ -43,19 +45,29 @@ def validate_coerce(self, v: Any) -> ColorScale: return cast("ColorScale", coerced) -def infer_default_colorscale() -> ColorScale | Collection[Color] | str: +def infer_default_colorscale( + template: go.layout.Template | None = None, +) -> ColorScale | Collection[Color] | str: + if template is None: + template = default_plotly_template() return validate_coerce_colorscale( - default_plotly_template().layout.colorscale.sequential or px.colors.sequential.Viridis + template.layout.colorscale.sequential or px.colors.sequential.Viridis ) def validate_coerce_colorscale( colorscale: ColorScale | Collection[Color] | str | None, + template: go.layout.Template | None = None, ) -> ColorScale: """Convert mixed colorscale representations to the canonical - :data:`ColorScale` format.""" + :data:`ColorScale` format. + + If ``colorscale`` is None, the default colorscale is inferred from the + given ``template`` (or from the current default Plotly template, if no + template is specified). + """ if colorscale is None: - colorscale = infer_default_colorscale() + colorscale = infer_default_colorscale(template=template) return ColorscaleValidator().validate_coerce(colorscale) diff --git a/src/ridgeplot/_figure_factory.py b/src/ridgeplot/_figure_factory.py index 85f2eaa8..16011810 100644 --- a/src/ridgeplot/_figure_factory.py +++ b/src/ridgeplot/_figure_factory.py @@ -14,6 +14,7 @@ ) from ridgeplot._obj.traces import get_trace_cls from ridgeplot._obj.traces.base import ColoringContext +from ridgeplot._template import validate_coerce_template from ridgeplot._types import ( Color, ColorScale, @@ -37,7 +38,7 @@ if TYPE_CHECKING: from collections.abc import Collection - from ridgeplot._types import Densities + from ridgeplot._types import Densities, PlotlyTemplate def normalise_trace_types( @@ -80,8 +81,11 @@ def _update_layout( xpad: float, x_max: float, x_min: float, + template: go.layout.Template | None, ) -> go.Figure: """Update figure's layout.""" + if template is not None: + fig.update_layout(template=template) fig.update_layout( legend=dict(traceorder="normal"), ) @@ -130,6 +134,7 @@ def create_ridgeplot( line_width: float | None, spacing: float, xpad: float, + template: PlotlyTemplate | None, ) -> go.Figure: # ============================================================== # --- Get clean and validated input arguments @@ -169,7 +174,8 @@ def create_ridgeplot( line_width = float(line_width) if line_width is not None else None spacing = float(spacing) xpad = float(xpad) - colorscale = validate_coerce_colorscale(colorscale) + template = validate_coerce_template(template) + colorscale = validate_coerce_colorscale(colorscale, template=template) # ============================================================== # --- Build the figure @@ -233,5 +239,6 @@ def create_ridgeplot( xpad=xpad, x_max=x_max, x_min=x_min, + template=template, ) return fig diff --git a/src/ridgeplot/_ridgeplot.py b/src/ridgeplot/_ridgeplot.py index 95049cae..cb7d33c7 100644 --- a/src/ridgeplot/_ridgeplot.py +++ b/src/ridgeplot/_ridgeplot.py @@ -31,6 +31,7 @@ ColorScale, LabelsArray, NormalisationOption, + PlotlyTemplate, SampleWeights, SampleWeightsArray, ShallowDensities, @@ -120,6 +121,7 @@ def ridgeplot( line_width: float | None = None, spacing: float = 0.5, xpad: float = 0.05, + template: PlotlyTemplate | None = None, # Deprecated parameters coloralpha: float | None | MissingType = MISSING, linewidth: float | MissingType = MISSING, @@ -140,6 +142,8 @@ def ridgeplot( https://www.statsmodels.org/stable/_modules/statsmodels/nonparametric/bandwidths.html .. _Plotly's built-in color-scales: https://plotly.com/python/builtin-colorscales/ + .. _Plotly's theming and templates guide: + https://plotly.com/python/templates/ .. _ragged: https://en.wikipedia.org/wiki/Jagged_array @@ -290,8 +294,8 @@ def ridgeplot( ``["rgb(255, 0, 0)", "blue", "hsl(120, 100%, 50%)"]``). The list will ultimately be converted into a :data:`~ridgeplot._types.ColorScale` object, assuming the colors provided are evenly spaced. If not specified - (default), the color scale will be inferred from current Plotly - template. + (default), the color scale will be inferred from the current Plotly + template (see the :paramref:`.template` parameter). colormode : "fillgradient" or SolidColormode This parameter controls the logic used for the coloring of each @@ -395,6 +399,20 @@ def ridgeplot( units of the range between the minimum and maximum x-values from all distributions. + template : PlotlyTemplate or None + The Plotly template to use in this figure. This can be the name of a + registered template (e.g., ``"plotly_dark"``), a + :class:`plotly.graph_objects.layout.Template + ` object, or a dictionary with a + template's properties. See `Plotly's theming and templates guide`_ + for more details. If not specified (default), the plot will be + rendered using Plotly's current default template (i.e., + ``plotly.io.templates.default``). Note that, unless a custom + :paramref:`.colorscale` is specified, the default colorscale will + also be inferred from this template. + + .. versionadded:: 0.7.0 + coloralpha : float .. deprecated:: 0.2.0 @@ -511,5 +529,6 @@ def ridgeplot( line_width=line_width, spacing=spacing, xpad=xpad, + template=template, ) return fig diff --git a/src/ridgeplot/_template.py b/src/ridgeplot/_template.py new file mode 100644 index 00000000..ef160c2f --- /dev/null +++ b/src/ridgeplot/_template.py @@ -0,0 +1,31 @@ +"""Plotly figure template utilities.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from _plotly_utils.basevalidators import BaseTemplateValidator + +if TYPE_CHECKING: + import plotly.graph_objects as go + + from ridgeplot._types import PlotlyTemplate + + +def validate_coerce_template(template: PlotlyTemplate | None) -> go.layout.Template | None: + """Convert mixed template representations into a + :class:`plotly.graph_objects.layout.Template + ` object. + + ``None`` is passed through as-is, meaning that no template has been + specified and that Plotly's current default template should be used. + """ + if template is None: + return None + validator = BaseTemplateValidator( + plotly_name="template", + parent_name="layout", + data_class_str="Template", + data_docs="", + ) + return cast("go.layout.Template", validator.validate_coerce(template)) diff --git a/src/ridgeplot/_types.py b/src/ridgeplot/_types.py index dcee9404..2c5b5493 100644 --- a/src/ridgeplot/_types.py +++ b/src/ridgeplot/_types.py @@ -7,6 +7,7 @@ from typing import Literal, TypeAlias import numpy as np +import plotly.graph_objects as go from typing_extensions import Any, TypeIs, TypeVar # Snippet used to generate and store the image artifacts: @@ -70,6 +71,16 @@ available for the ridgeplot. See :paramref:`ridgeplot.ridgeplot.norm` for more details.""" +PlotlyTemplate: TypeAlias = go.layout.Template | dict[str, Any] | str +"""A Plotly figure template can be represented by a +:class:`plotly.graph_objects.layout.Template ` +object, a dictionary with a template's properties, or the name of a registered +template (e.g., ``"plotly_dark"``). + +See `Plotly's theming and templates guide +`_ for more details. +""" + # ======================================================== # --- Base nested Collection types (ragged arrays) # ======================================================== diff --git a/tests/unit/color/test_colorscale.py b/tests/unit/color/test_colorscale.py index c585ec5a..57b02787 100644 --- a/tests/unit/color/test_colorscale.py +++ b/tests/unit/color/test_colorscale.py @@ -3,6 +3,9 @@ import re from typing import TYPE_CHECKING +import plotly.express as px +import plotly.graph_objects as go +import plotly.io as pio import pytest from ridgeplot._color.colorscale import ( @@ -26,6 +29,20 @@ def test_infer_default_colorscale() -> None: assert infer_default_colorscale() == validate_coerce_colorscale("plasma") +def test_infer_default_colorscale_from_template() -> None: + template = pio.templates["ggplot2"] + assert infer_default_colorscale(template=template) == validate_coerce_colorscale( + template.layout.colorscale.sequential + ) + + +def test_infer_default_colorscale_fallback_to_viridis() -> None: + template = go.layout.Template() + assert infer_default_colorscale(template=template) == validate_coerce_colorscale( + px.colors.sequential.Viridis + ) + + # ============================================================== # --- validate_coerce_colorscale() # ============================================================== diff --git a/tests/unit/test_figure_factory.py b/tests/unit/test_figure_factory.py index c24fa3da..3a4d7911 100644 --- a/tests/unit/test_figure_factory.py +++ b/tests/unit/test_figure_factory.py @@ -134,4 +134,5 @@ def test_densities_must_be_4d(self, densities: Densities) -> None: line_width=..., # type: ignore[reportArgumentType] spacing=..., # type: ignore[reportArgumentType] xpad=..., # type: ignore[reportArgumentType] + template=..., # type: ignore[reportArgumentType] ) diff --git a/tests/unit/test_ridgeplot.py b/tests/unit/test_ridgeplot.py index c6c7df3a..e9f587a1 100644 --- a/tests/unit/test_ridgeplot.py +++ b/tests/unit/test_ridgeplot.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any import plotly.express as px +import plotly.io as pio import pytest from ridgeplot import ridgeplot @@ -171,6 +172,65 @@ def test_colorscale_invalid(invalid_colorscale: ColorScale | Collection[Color] | ridgeplot(samples=[[[1, 2, 3], [4, 5, 6]]], colorscale=invalid_colorscale) +# ============================================================== +# --- param: template +# ============================================================== + + +def test_template_is_applied_to_the_figure() -> None: + fig = ridgeplot(samples=[[[1, 2, 3], [4, 5, 6]]], template="plotly_dark") + assert fig.layout.template == pio.templates["plotly_dark"] + + +def test_template_equivalent_representations() -> None: + samples = [[[1, 2, 3], [4, 5, 6]]] + template = pio.templates["seaborn"] + assert ( + ridgeplot(samples=samples, template="seaborn") == + ridgeplot(samples=samples, template=template) == + ridgeplot(samples=samples, template=template.to_plotly_json()) + ) # fmt: skip + + +def test_template_default() -> None: + # Not specifying a template should be equivalent to + # specifying Plotly's current default template + samples = [[[1, 2, 3], [4, 5, 6]]] + assert ( + ridgeplot(samples=samples) == + ridgeplot(samples=samples, template=pio.templates.default or "plotly") + ) # fmt: skip + + +def test_template_sets_default_colorscale() -> None: + # The default colorscale should be inferred from the + # template specified via the `template` argument + samples = [[[1, 2, 3], [4, 5, 6]]] + template = pio.templates["ggplot2"] + assert ( + ridgeplot(samples=samples, template="ggplot2") == + ridgeplot( + samples=samples, + template="ggplot2", + colorscale=template.layout.colorscale.sequential, + ) + ) # fmt: skip + + +def test_template_explicit_colorscale_takes_precedence() -> None: + fig = ridgeplot( + samples=[[[1, 2, 3], [4, 5, 6]]], + template="ggplot2", + colorscale=( + (0.0, "rgb(10, 10, 10)"), + (1.0, "rgb(20, 20, 20)"), + ), + colormode="trace-index", + ) + assert fig.data[1].fillcolor == "rgb(20, 20, 20)" + assert fig.data[3].fillcolor == "rgb(10, 10, 10)" + + # ============================================================== # --- param: color_discrete_map # ============================================================== diff --git a/tests/unit/test_template.py b/tests/unit/test_template.py new file mode 100644 index 00000000..78d49317 --- /dev/null +++ b/tests/unit/test_template.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import plotly.graph_objects as go +import plotly.io as pio +import pytest + +from ridgeplot._template import validate_coerce_template + + +def test_none_passthrough() -> None: + assert validate_coerce_template(None) is None + + +def test_registered_template_name() -> None: + assert validate_coerce_template("plotly_dark") == pio.templates["plotly_dark"] + + +# Merging templates causes Plotly to internally instantiate all +# registered trace types, including the deprecated Scattermapbox +@pytest.mark.filterwarnings(r"ignore:\*scattermapbox\* is deprecated.*:DeprecationWarning") +def test_merged_template_names() -> None: + assert validate_coerce_template("plotly_white+ggplot2") == pio.templates["plotly_white+ggplot2"] + + +def test_template_object() -> None: + template = pio.templates["seaborn"] + assert validate_coerce_template(template) == template + + +def test_template_dict() -> None: + template = pio.templates["seaborn"] + coerced = validate_coerce_template(template.to_plotly_json()) + assert isinstance(coerced, go.layout.Template) + assert coerced == template + + +def test_invalid_template_name() -> None: + with pytest.raises(ValueError, match=r"Invalid value .* received for the 'template' property"): + validate_coerce_template("this-is-not-a-registered-template") + + +def test_invalid_template_type() -> None: + with pytest.raises(ValueError, match=r"Invalid value .* received for the 'template' property"): + validate_coerce_template(42) # pyright: ignore[reportArgumentType] From 990917f61da253f6f57583009b60aa32ff9c6f5b Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Fri, 24 Jul 2026 20:08:18 +0200 Subject: [PATCH 2/5] Add changelog entry for the new template parameter --- docs/reference/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/changelog.md b/docs/reference/changelog.md index e7bf832f..5bad8f07 100644 --- a/docs/reference/changelog.md +++ b/docs/reference/changelog.md @@ -5,6 +5,10 @@ This document outlines the list of changes to ridgeplot between each release. Fo Unreleased changes ------------------ +### Features + +- Implement a new `template` parameter to allow users to specify a Plotly figure template ({gh-pr}`389`) + ### CI/CD - Bump actions/download-artifact from 7 to 8 ({gh-pr}`368`) From c1e9759a09142b0b94518ed72b581ebed8af0e85 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Sat, 25 Jul 2026 04:08:40 +0200 Subject: [PATCH 3/5] Say 'Template instance' instead of 'object' in docstrings, matching Plotly's phrasing --- src/ridgeplot/_ridgeplot.py | 2 +- src/ridgeplot/_template.py | 2 +- src/ridgeplot/_types.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ridgeplot/_ridgeplot.py b/src/ridgeplot/_ridgeplot.py index cb7d33c7..6bbbc185 100644 --- a/src/ridgeplot/_ridgeplot.py +++ b/src/ridgeplot/_ridgeplot.py @@ -403,7 +403,7 @@ def ridgeplot( The Plotly template to use in this figure. This can be the name of a registered template (e.g., ``"plotly_dark"``), a :class:`plotly.graph_objects.layout.Template - ` object, or a dictionary with a + ` instance, or a dictionary with a template's properties. See `Plotly's theming and templates guide`_ for more details. If not specified (default), the plot will be rendered using Plotly's current default template (i.e., diff --git a/src/ridgeplot/_template.py b/src/ridgeplot/_template.py index ef160c2f..209573fd 100644 --- a/src/ridgeplot/_template.py +++ b/src/ridgeplot/_template.py @@ -15,7 +15,7 @@ def validate_coerce_template(template: PlotlyTemplate | None) -> go.layout.Template | None: """Convert mixed template representations into a :class:`plotly.graph_objects.layout.Template - ` object. + ` instance. ``None`` is passed through as-is, meaning that no template has been specified and that Plotly's current default template should be used. diff --git a/src/ridgeplot/_types.py b/src/ridgeplot/_types.py index 2c5b5493..21056649 100644 --- a/src/ridgeplot/_types.py +++ b/src/ridgeplot/_types.py @@ -74,7 +74,7 @@ PlotlyTemplate: TypeAlias = go.layout.Template | dict[str, Any] | str """A Plotly figure template can be represented by a :class:`plotly.graph_objects.layout.Template ` -object, a dictionary with a template's properties, or the name of a registered +instance, a dictionary with a template's properties, or the name of a registered template (e.g., ``"plotly_dark"``). See `Plotly's theming and templates guide From 31b9e48a1e62a8c6308e382e8c4d447dcca829be Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Sat, 25 Jul 2026 05:07:09 +0200 Subject: [PATCH 4/5] Add a section on theming with Plotly templates to the getting started guide --- cicd_utils/ridgeplot_examples/__init__.py | 7 ++++++ cicd_utils/ridgeplot_examples/_basic_dark.py | 24 +++++++++++++++++++ docs/_static/charts/basic_dark.webp | Bin 0 -> 26282 bytes docs/getting_started/getting_started.md | 17 +++++++++++++ docs/reference/changelog.md | 4 ++++ tests/e2e/artifacts/basic_dark.json | 1 + 6 files changed, 53 insertions(+) create mode 100644 cicd_utils/ridgeplot_examples/_basic_dark.py create mode 100644 docs/_static/charts/basic_dark.webp create mode 100644 tests/e2e/artifacts/basic_dark.json diff --git a/cicd_utils/ridgeplot_examples/__init__.py b/cicd_utils/ridgeplot_examples/__init__.py index 66c27f6f..61619926 100644 --- a/cicd_utils/ridgeplot_examples/__init__.py +++ b/cicd_utils/ridgeplot_examples/__init__.py @@ -19,6 +19,12 @@ def load_basic() -> go.Figure: return main() +def load_basic_dark() -> go.Figure: + from ._basic_dark import main + + return main() + + def load_basic_hist() -> go.Figure: from ._basic_hist import main @@ -45,6 +51,7 @@ def load_probly() -> go.Figure: ALL_EXAMPLES: list[Example] = [ Example("basic", load_basic), + Example("basic_dark", load_basic_dark), Example("basic_hist", load_basic_hist), Example("lincoln_weather", load_lincoln_weather), Example("lincoln_weather_red_blue", load_lincoln_weather_red_blue), diff --git a/cicd_utils/ridgeplot_examples/_basic_dark.py b/cicd_utils/ridgeplot_examples/_basic_dark.py new file mode 100644 index 00000000..bb13249f --- /dev/null +++ b/cicd_utils/ridgeplot_examples/_basic_dark.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import plotly.graph_objects as go + + +def main() -> go.Figure: + import numpy as np + + from ridgeplot import ridgeplot + + rng = np.random.default_rng(42) + my_samples = [rng.normal(n, size=900) for n in range(6, 0, -2)] + fig = ridgeplot(samples=my_samples, template="plotly_dark") + fig.update_layout(height=400, width=800) + + return fig + + +if __name__ == "__main__": + fig = main() + fig.show() diff --git a/docs/_static/charts/basic_dark.webp b/docs/_static/charts/basic_dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..cd95986ab4618f97a1b13e6d00bdfd4c268d9d17 GIT binary patch literal 26282 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 O)NNe;wcr20 Date: Sat, 25 Jul 2026 05:12:51 +0200 Subject: [PATCH 5/5] Reuse the basic example in basic_dark to keep them in sync --- cicd_utils/ridgeplot_examples/_basic.py | 8 ++++++-- cicd_utils/ridgeplot_examples/_basic_dark.py | 12 +++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/cicd_utils/ridgeplot_examples/_basic.py b/cicd_utils/ridgeplot_examples/_basic.py index cbac4b35..e54216ca 100644 --- a/cicd_utils/ridgeplot_examples/_basic.py +++ b/cicd_utils/ridgeplot_examples/_basic.py @@ -5,15 +5,19 @@ if TYPE_CHECKING: import plotly.graph_objects as go + from ridgeplot._types import PlotlyTemplate -def main() -> go.Figure: + +def main( + template: PlotlyTemplate | None = None, +) -> go.Figure: import numpy as np from ridgeplot import ridgeplot rng = np.random.default_rng(42) my_samples = [rng.normal(n, size=900) for n in range(6, 0, -2)] - fig = ridgeplot(samples=my_samples) + fig = ridgeplot(samples=my_samples, template=template) fig.update_layout(height=400, width=800) return fig diff --git a/cicd_utils/ridgeplot_examples/_basic_dark.py b/cicd_utils/ridgeplot_examples/_basic_dark.py index bb13249f..4a4d281a 100644 --- a/cicd_utils/ridgeplot_examples/_basic_dark.py +++ b/cicd_utils/ridgeplot_examples/_basic_dark.py @@ -2,20 +2,14 @@ from typing import TYPE_CHECKING +from ridgeplot_examples._basic import main as basic + if TYPE_CHECKING: import plotly.graph_objects as go def main() -> go.Figure: - import numpy as np - - from ridgeplot import ridgeplot - - rng = np.random.default_rng(42) - my_samples = [rng.normal(n, size=900) for n in range(6, 0, -2)] - fig = ridgeplot(samples=my_samples, template="plotly_dark") - fig.update_layout(height=400, width=800) - + fig = basic(template="plotly_dark") return fig