Skip to content
Merged
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
7 changes: 7 additions & 0 deletions cicd_utils/ridgeplot_examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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),
Expand Down
8 changes: 6 additions & 2 deletions cicd_utils/ridgeplot_examples/_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions cicd_utils/ridgeplot_examples/_basic_dark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import annotations

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:
fig = basic(template="plotly_dark")
return fig


if __name__ == "__main__":
fig = main()
fig.show()
Binary file added docs/_static/charts/basic_dark.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions docs/getting_started/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,20 @@ fig = ridgeplot(
```{raw} html
:file: ../_static/charts/lincoln_weather_red_blue.html
```

## Theming with Plotly templates

Since `ridgeplot` is built on top of Plotly, you can also theme your ridgeline plots using [Plotly's figure templates](https://plotly.com/python/templates/). Simply pass the name of a registered template (or any other valid template representation) to the {py:paramref}`~ridgeplot.ridgeplot.template` parameter. For instance, taking the basic example from the top of this page and applying the `"plotly_dark"` template:

```python
fig = ridgeplot(samples=my_samples, template="plotly_dark")
fig.show()
```

```{raw} html
:file: ../_static/charts/basic_dark.html
```

:::{note}
Unless a custom {py:paramref}`~ridgeplot.ridgeplot.colorscale` is specified, the default colorscale will also be inferred from the specified template.
:::
8 changes: 8 additions & 0 deletions docs/reference/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ 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`)

### Documentation

- Add a section on theming with Plotly templates to the getting started guide ({gh-pr}`389`)

### CI/CD

- Review the coverage configuration in light of `covdefaults`, adopting its `assert_never` exclusion and `skip_covered` report setting, and raising all package coverage gates to 100% ({gh-pr}`390`)
Expand Down
20 changes: 16 additions & 4 deletions src/ridgeplot/_color/colorscale.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
if TYPE_CHECKING:
from collections.abc import Collection

import plotly.graph_objects as go

from ridgeplot._types import Color, ColorScale


Expand Down Expand Up @@ -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)


Expand Down
11 changes: 9 additions & 2 deletions src/ridgeplot/_figure_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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"),
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -233,5 +239,6 @@ def create_ridgeplot(
xpad=xpad,
x_max=x_max,
x_min=x_min,
template=template,
)
return fig
23 changes: 21 additions & 2 deletions src/ridgeplot/_ridgeplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
ColorScale,
LabelsArray,
NormalisationOption,
PlotlyTemplate,
SampleWeights,
SampleWeightsArray,
ShallowDensities,
Expand Down Expand Up @@ -120,6 +121,7 @@ def ridgeplot(
line_width: float | None = None,
spacing: float = 0.5,
xpad: float = 0.05,
template: PlotlyTemplate | None = None,
Comment thread
tpvasconcelos marked this conversation as resolved.
# Deprecated parameters
coloralpha: float | None | MissingType = MISSING,
linewidth: float | MissingType = MISSING,
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
<plotly.graph_objs.layout.Template>` 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.,
``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
Expand Down Expand Up @@ -511,5 +529,6 @@ def ridgeplot(
line_width=line_width,
spacing=spacing,
xpad=xpad,
template=template,
)
return fig
31 changes: 31 additions & 0 deletions src/ridgeplot/_template.py
Original file line number Diff line number Diff line change
@@ -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
<plotly.graph_objs.layout.Template>` instance.

``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))
11 changes: 11 additions & 0 deletions src/ridgeplot/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 <plotly.graph_objs.layout.Template>`
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
<https://plotly.com/python/templates/>`_ for more details.
"""

# ========================================================
# --- Base nested Collection types (ragged arrays)
# ========================================================
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/artifacts/basic_dark.json

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions tests/unit/color/test_colorscale.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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()
# ==============================================================
Expand Down
1 change: 1 addition & 0 deletions tests/unit/test_figure_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
)
Loading
Loading