From ce491580d1e52daa6cf4e71918a4a49bee72d934 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Sat, 30 Nov 2024 17:45:22 +0000 Subject: [PATCH 01/21] Implement histogram binning and bar trace types --- docs/conf.py | 29 ++- src/ridgeplot/_color/interpolation.py | 267 ++++++------------------- src/ridgeplot/_figure_factory.py | 222 +++++++------------- src/ridgeplot/_hist.py | 49 +++++ src/ridgeplot/_kde.py | 17 +- src/ridgeplot/_obj/__init__.py | 26 +++ src/ridgeplot/_obj/_area.py | 85 ++++++++ src/ridgeplot/_obj/_bar.py | 53 +++++ src/ridgeplot/_obj/_base.py | 75 +++++++ src/ridgeplot/_ridgeplot.py | 171 ++++++++++------ src/ridgeplot/_types.py | 68 ++++++- tests/unit/color/test_interpolation.py | 185 ++++++++--------- tests/unit/test_figure_factory.py | 1 + tox.ini | 2 +- 14 files changed, 708 insertions(+), 542 deletions(-) create mode 100644 src/ridgeplot/_hist.py create mode 100644 src/ridgeplot/_obj/__init__.py create mode 100644 src/ridgeplot/_obj/_area.py create mode 100644 src/ridgeplot/_obj/_bar.py create mode 100644 src/ridgeplot/_obj/_base.py diff --git a/docs/conf.py b/docs/conf.py index 88abd329..e89c607c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,6 +3,7 @@ import sys from contextlib import contextmanager from datetime import datetime +from importlib import import_module from pathlib import Path from typing import TYPE_CHECKING @@ -263,18 +264,9 @@ # ------- ._color.interpolation ---------------- "ridgeplot._color.interpolation.ColorscaleInterpolants", "ridgeplot._color.interpolation.SolidColormode", - # ------- ._figure_factory --------------------- - "ridgeplot._figure_factory.TraceType", - "ridgeplot._figure_factory.TraceTypesArray", - "ridgeplot._figure_factory.ShallowTraceTypesArray", - "ridgeplot._figure_factory.LabelsArray", - "ridgeplot._figure_factory.ShallowLabelsArray", # ------- ._kde -------------------------------- "ridgeplot._kde.KDEPoints", "ridgeplot._kde.KDEBandwidth", - "ridgeplot._kde.SampleWeights", - "ridgeplot._kde.SampleWeightsArray", - "ridgeplot._kde.ShallowSampleWeightsArray", # ------- ._missing ---------------------------- "ridgeplot._missing.MISSING", "ridgeplot._missing.MissingType", @@ -298,13 +290,32 @@ "ridgeplot._types.SamplesRow", "ridgeplot._types.Samples", "ridgeplot._types.ShallowSamples", + "ridgeplot._types.TraceType", + "ridgeplot._types.TraceTypesArray", + "ridgeplot._types.ShallowTraceTypesArray", + "ridgeplot._types.LabelsArray", + "ridgeplot._types.ShallowLabelsArray", + "ridgeplot._types.SampleWeights", + "ridgeplot._types.SampleWeightsArray", + "ridgeplot._types.ShallowSampleWeightsArray", } +for fq in _TYPE_ALIASES_FULLY_QUALIFIED: + module_name, _, type_name = fq.rpartition(".") + try: + import_module(module_name) + except ImportError as e: + raise AssertionError(f"Type alias {fq!r} is not importable: {e}") from e + _TYPE_ALIASES = {fq.split(".")[-1]: fq for fq in _TYPE_ALIASES_FULLY_QUALIFIED} autodoc_type_aliases = { **{a: a for a in _TYPE_ALIASES.values()}, **{fq: fq for fq in _TYPE_ALIASES.values()}, } napoleon_type_aliases = {a: f":data:`~{fq}`" for a, fq in _TYPE_ALIASES.items()} +EXTRA_NAPOLEON_ALIASES = { + "Collection[Color]": r":data:`~collections.abc.Collection`\[:data:`~ridgeplot._types.Color`\]", +} +napoleon_type_aliases.update(EXTRA_NAPOLEON_ALIASES) # -- sphinx_remove_toctrees ------------------------------------------------------------------------ diff --git a/src/ridgeplot/_color/interpolation.py b/src/ridgeplot/_color/interpolation.py index f3241711..f9376903 100644 --- a/src/ridgeplot/_color/interpolation.py +++ b/src/ridgeplot/_color/interpolation.py @@ -1,28 +1,25 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Literal, Protocol, TypedDict, overload +from typing import TYPE_CHECKING, Literal, Protocol -import plotly.graph_objs as go - -from ridgeplot._color.colorscale import validate_and_coerce_colorscale from ridgeplot._color.utils import apply_alpha, round_color, to_rgb, unpack_rgb -from ridgeplot._types import CollectionL2, Color, ColorScale +from ridgeplot._types import CollectionL2, ColorScale from ridgeplot._utils import get_xy_extrema, normalise_min_max from ridgeplot._vendor.more_itertools import zip_strict if TYPE_CHECKING: - from collections.abc import Collection, Generator + from collections.abc import Generator from ridgeplot._types import Densities, Numeric # ============================================================== -# --- Common interpolation utilities +# --- Interpolation utilities # ============================================================== -def _interpolate_color(colorscale: ColorScale, p: float) -> str: +def interpolate_color(colorscale: ColorScale, p: float) -> str: """Get a color from a colorscale at a given interpolation point ``p``. This function always returns a color in the RGB format, even if the input @@ -59,6 +56,51 @@ def _interpolate_color(colorscale: ColorScale, p: float) -> str: return round_color(rgb, 5) +def slice_colorscale( + colorscale: ColorScale, + p_lower: float, + p_upper: float, +) -> ColorScale: + """Slice a continuous colorscale between two intermediate points. + + Parameters + ---------- + colorscale + The continuous colorscale to slice. + p_lower + The lower bound of the slicing interval. Must be >= 0 and < p_upper. + p_upper + The upper bound of the slicing interval. Must be <= 1 and > p_lower. + + Returns + ------- + ColorScale + The sliced colorscale. + + Raises + ------ + ValueError + If ``p_lower`` is >= ``p_upper``, or if either ``p_lower`` or ``p_upper`` + are outside the range [0, 1]. + """ + if p_lower >= p_upper: + raise ValueError("p_lower should be less than p_upper.") + if p_lower < 0 or p_upper > 1: + raise ValueError("p_lower should be >= 0 and p_upper should be <= 1.") + if p_lower == 0 and p_upper == 1: + return colorscale + + return ( + (0.0, interpolate_color(colorscale, p=p_lower)), + *[ + (normalise_min_max(v, min_=p_lower, max_=p_upper), c) + for v, c in colorscale + if p_lower < v < p_upper + ], + (1.0, interpolate_color(colorscale, p=p_upper)), + ) + + # ============================================================== # --- Solid color modes # ============================================================== @@ -79,6 +121,8 @@ def _interpolate_color(colorscale: ColorScale, p: float) -> str: @dataclass class InterpolationContext: + """Context information needed by the interpolation functions.""" + densities: Densities n_rows: int n_traces: int @@ -87,7 +131,7 @@ class InterpolationContext: @classmethod def from_densities(cls, densities: Densities) -> InterpolationContext: - x_min, x_max, _, _ = map(float, get_xy_extrema(densities=densities)) + x_min, x_max, _, _ = get_xy_extrema(densities=densities) return cls( densities=densities, n_rows=len(densities), @@ -160,8 +204,8 @@ def _interpolate_mean_means(ctx: InterpolationContext) -> ColorscaleInterpolants x, y = zip(*trace) means_row.append(sum(_mul(x, y)) / sum(y)) means.append(means_row) - min_mean = min([min(row) for row in means]) - max_mean = max([max(row) for row in means]) + min_mean = min(min(row) for row in means) + max_mean = max(max(row) for row in means) return [ [normalise_min_max(mean, min_=min_mean, max_=max_mean) for mean in row] for row in means ] @@ -185,14 +229,16 @@ def _interpolate_mean_means(ctx: InterpolationContext) -> ColorscaleInterpolants } -def _compute_solid_colors( +def compute_solid_colors( colorscale: ColorScale, colormode: SolidColormode, opacity: float | None, interpolation_ctx: InterpolationContext, ) -> Generator[Generator[str]]: - def _get_fill_color(p: float) -> str: - fill_color = _interpolate_color(colorscale, p=p) + """Compute the solid colors for all traces in the plot.""" + + def get_fill_color(p: float) -> str: + fill_color = interpolate_color(colorscale, p=p) if opacity is not None: # Sometimes the interpolation logic can drop the alpha channel fill_color = apply_alpha(fill_color, alpha=float(opacity)) @@ -200,195 +246,4 @@ def _get_fill_color(p: float) -> str: interpolate_func = SOLID_COLORMODE_MAPS[colormode] interpolants = interpolate_func(ctx=interpolation_ctx) - return ((_get_fill_color(p) for p in row) for row in interpolants) - - -class SolidColorsDict(TypedDict): - line_color: Color - fillcolor: str - - -def _compute_solid_trace_colors( - colorscale: ColorScale, - colormode: SolidColormode, - line_color: Color | Literal["fill-color"], - opacity: float | None, - interpolation_ctx: InterpolationContext, -) -> Generator[Generator[SolidColorsDict]]: - return ( - ( - dict( - line_color=fill_color if line_color == "fill-color" else line_color, - fillcolor=fill_color, - ) - for fill_color in row - ) - for row in _compute_solid_colors( - colorscale=colorscale, - colormode=colormode, - opacity=opacity, - interpolation_ctx=interpolation_ctx, - ) - ) - - -# ============================================================== -# --- `fillgradient` color mode -# ============================================================== - - -def _slice_colorscale( - colorscale: ColorScale, - p_lower: float, - p_upper: float, -) -> ColorScale: - """Slice a continuous colorscale between two intermediate points. - - Parameters - ---------- - colorscale - The continuous colorscale to slice. - p_lower - The lower bound of the slicing interval. Must be >= 0 and < p_upper. - p_upper - The upper bound of the slicing interval. Must be <= 1 and > p_lower. - - Returns - ------- - ColorScale - The sliced colorscale. - - Raises - ------ - ValueError - If ``p_lower`` is >= ``p_upper``, or if either ``p_lower`` or ``p_upper`` - are outside the range [0, 1]. - """ - if p_lower >= p_upper: - raise ValueError("p_lower should be less than p_upper.") - if p_lower < 0 or p_upper > 1: - raise ValueError("p_lower should be >= 0 and p_upper should be <= 1.") - if p_lower == 0 and p_upper == 1: - return colorscale - - return ( - (0.0, _interpolate_color(colorscale, p=p_lower)), - *[ - (normalise_min_max(v, min_=p_lower, max_=p_upper), c) - for v, c in colorscale - if p_lower < v < p_upper - ], - (1.0, _interpolate_color(colorscale, p=p_upper)), - ) - - -class FillgradientColorsDict(TypedDict): - line_color: str - fillgradient: go.scatter.Fillgradient - - -def _compute_fillgradient_trace_colors( - colorscale: ColorScale, - line_color: Color | Literal["fill-color"], - opacity: float | None, - interpolation_ctx: InterpolationContext, -) -> Generator[Generator[FillgradientColorsDict]]: - solid_line_colors: Generator[Generator[Color]] - if line_color == "fill-color": - solid_line_colors = _compute_solid_colors( - colorscale=colorscale, - colormode="mean-minmax", - opacity=opacity, - interpolation_ctx=interpolation_ctx, - ) - else: - solid_line_colors = ((line_color for _ in row) for row in interpolation_ctx.densities) - if opacity is not None: - # HACK: Plotly doesn't yet support setting the fill opacity - # for traces with `fillgradient`. As a workaround, we - # can override the color-scale's color values and add - # the corresponding alpha channel to all colors. - colorscale = [(v, apply_alpha(c, float(opacity))) for v, c in colorscale] - return ( - ( - dict( - line_color=line_color, - fillgradient=go.scatter.Fillgradient( - colorscale=_slice_colorscale( - colorscale=colorscale, - p_lower=normalise_min_max( - min(next(zip(*trace))), - min_=interpolation_ctx.x_min, - max_=interpolation_ctx.x_max, - ), - p_upper=normalise_min_max( - max(next(zip(*trace))), - min_=interpolation_ctx.x_min, - max_=interpolation_ctx.x_max, - ), - ), - type="horizontal", - ), - ) - for line_color, trace in zip_strict(line_colors_row, densities_row) - ) - for line_colors_row, densities_row in zip_strict( - solid_line_colors, interpolation_ctx.densities - ) - ) - - -# ============================================================== -# --- Main public function -# ============================================================== - - -@overload -def compute_trace_colors( - colorscale: ColorScale | Collection[Color] | str | None, - colormode: Literal["fillgradient"], - line_color: Color | Literal["fill-color"], - opacity: float | None, - interpolation_ctx: InterpolationContext, -) -> Generator[Generator[FillgradientColorsDict]]: ... - - -@overload -def compute_trace_colors( - colorscale: ColorScale | Collection[Color] | str | None, - colormode: SolidColormode, - line_color: Color | Literal["fill-color"], - opacity: float | None, - interpolation_ctx: InterpolationContext, -) -> Generator[Generator[SolidColorsDict]]: ... - - -def compute_trace_colors( - colorscale: ColorScale | Collection[Color] | str | None, - colormode: Literal["fillgradient"] | SolidColormode, - line_color: Color | Literal["fill-color"], - opacity: float | None, - interpolation_ctx: InterpolationContext, -) -> Generator[Generator[FillgradientColorsDict | SolidColorsDict]]: - colorscale = validate_and_coerce_colorscale(colorscale) - - valid_colormodes = ("fillgradient", *SOLID_COLORMODE_MAPS) - if colormode not in valid_colormodes: - raise ValueError( - f"The colormode argument should be one of {valid_colormodes}, got {colormode} instead." - ) - - if colormode == "fillgradient": - return _compute_fillgradient_trace_colors( - colorscale=colorscale, - line_color=line_color, - opacity=opacity, - interpolation_ctx=interpolation_ctx, - ) - return _compute_solid_trace_colors( - colorscale=colorscale, - colormode=colormode, - line_color=line_color, - opacity=opacity, - interpolation_ctx=interpolation_ctx, - ) + return ((get_fill_color(p) for p in row) for row in interpolants) diff --git a/src/ridgeplot/_figure_factory.py b/src/ridgeplot/_figure_factory.py index 0793d2a1..5adba2da 100644 --- a/src/ridgeplot/_figure_factory.py +++ b/src/ridgeplot/_figure_factory.py @@ -1,21 +1,25 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Literal, cast from plotly import graph_objects as go +from ridgeplot._color.colorscale import validate_and_coerce_colorscale from ridgeplot._color.interpolation import ( InterpolationContext, SolidColormode, - compute_trace_colors, + compute_solid_colors, ) +from ridgeplot._obj import get_trace_cls +from ridgeplot._obj._base import ColoringContext from ridgeplot._types import ( - CollectionL1, - CollectionL2, Color, ColorScale, - DensityTrace, + LabelsArray, + ShallowLabelsArray, + ShallowTraceTypesArray, + TraceType, + TraceTypesArray, is_flat_str_collection, nest_shallow_collection, ) @@ -30,48 +34,20 @@ if TYPE_CHECKING: from collections.abc import Collection - from ridgeplot._types import Densities, Numeric + from ridgeplot._types import Densities -LabelsArray = CollectionL2[str] -"""A :data:`LabelsArray` represents the labels of traces in a ridgeplot. - -Example -------- - ->>> labels_array: LabelsArray = [ -... ["trace 1", "trace 2", "trace 3"], -... ["trace 4", "trace 5"], -... ] -""" - -ShallowLabelsArray = CollectionL1[str] -"""Shallow type for :data:`LabelsArray`. - -Example -------- - ->>> labels_array: ShallowLabelsArray = ["trace 1", "trace 2", "trace 3"] -""" - -_D3HF = ".7" -"""Default (d3-format) format for floats in hover labels. - -After trying to read through the plotly.py source code, I couldn't find a -simple way to replicate the default hover format using the d3-format syntax -in Plotly's 'hovertemplate' parameter. The closest I got was by using the -string below, but it's not quite the same... (see '.7~r' as well) -""" - -_DEFAULT_HOVERTEMPLATE = ( - f"(%{{x:{_D3HF}}}, %{{customdata[0]:{_D3HF}}})" - "
" - "%{fullData.name}" -) # fmt: skip -"""Default ``hovertemplate`` for density traces. - -See :func:`draw_density_trace`. -""" +def normalise_trace_types( + densities: Densities, + trace_types: TraceTypesArray | ShallowTraceTypesArray | TraceType, +) -> TraceTypesArray: + if isinstance(trace_types, str): + trace_types = [[trace_types] * len(row) for row in densities] + else: + if is_flat_str_collection(trace_types): + trace_types = nest_shallow_collection(cast(ShallowTraceTypesArray, trace_types)) + trace_types = normalise_row_attrs(trace_types, l2_target=densities) + return trace_types def normalise_trace_labels( @@ -93,76 +69,6 @@ def normalise_y_labels(trace_labels: LabelsArray) -> LabelsArray: return [ordered_dedup(row) for row in trace_labels] -@dataclass -class RidgeplotTrace: - trace: DensityTrace - label: str - color: dict[str, Any] - - -@dataclass -class RidgeplotRow: - traces: list[RidgeplotTrace] - y_shifted: float - - -def draw_base( - fig: go.Figure, - x: Collection[Numeric], - y_shifted: float, -) -> go.Figure: - """Draw the base for a density trace. - - Adds an invisible trace at constant y that will serve as the fill-limit - for the corresponding density trace. - """ - fig.add_trace( - go.Scatter( - x=x, - y=[y_shifted] * len(x), - # make trace 'invisible' - # Note: visible=False does not work with fill="tonexty" - line=dict(color="rgba(0,0,0,0)", width=0), - showlegend=False, - hoverinfo="skip", - ) - ) - return fig - - -def draw_density_trace( - fig: go.Figure, - x: Collection[Numeric], - y: Collection[Numeric], - y_shifted: float, - label: str, - color: dict[str, Any], - line_width: float, -) -> go.Figure: - """Draw a density trace. - - Adds a density 'trace' to the Figure. The ``fill="tonexty"`` option - fills the trace until the previously drawn trace (see - :meth:`draw_base`). This is why the base trace must be drawn first. - """ - fig = draw_base(fig, x=x, y_shifted=y_shifted) - fig.add_trace( - go.Scatter( - x=x, - y=[y_i + y_shifted for y_i in y], - **color, - name=label, - fill="tonexty", - mode="lines", - line=dict(width=line_width), - # Hover information - customdata=[[y_i] for y_i in y], - hovertemplate=_DEFAULT_HOVERTEMPLATE, - ), - ) - return fig - - def update_layout( fig: go.Figure, y_labels: LabelsArray, @@ -192,17 +98,20 @@ def update_layout( showticklabels=True, **axes_common, ) + # TODO: Review default layout for bar traces... + fig.update_layout(barmode="stack", bargap=0, bargroupgap=0) return fig def create_ridgeplot( densities: Densities, + trace_types: TraceTypesArray | ShallowTraceTypesArray | TraceType, colorscale: ColorScale | Collection[Color] | str | None, opacity: float | None, colormode: Literal["fillgradient"] | SolidColormode, trace_labels: LabelsArray | ShallowLabelsArray | None, line_color: Color | Literal["fill-color"], - line_width: float, + line_width: float | None, spacing: float, show_yticklabels: bool, xpad: float, @@ -218,6 +127,10 @@ def create_ridgeplot( n_traces = sum(len(row) for row in densities) x_min, x_max, _, y_max = map(float, get_xy_extrema(densities=densities)) + trace_types = normalise_trace_types( + densities=densities, + trace_types=trace_types, + ) trace_labels = normalise_trace_labels( densities=densities, trace_labels=trace_labels, @@ -226,58 +139,65 @@ def create_ridgeplot( y_labels = normalise_y_labels(trace_labels) # Force cast certain arguments to the expected types - line_width = float(line_width) + line_width = float(line_width) if line_width is not None else None spacing = float(spacing) show_yticklabels = bool(show_yticklabels) xpad = float(xpad) + colorscale = validate_and_coerce_colorscale(colorscale) # ============================================================== # --- Build the figure # ============================================================== - colors = compute_trace_colors( + interpolation_ctx = InterpolationContext( + densities=densities, + n_rows=n_rows, + n_traces=n_traces, + x_min=x_min, + x_max=x_max, + ) + solid_colors = compute_solid_colors( colorscale=colorscale, - colormode=colormode, - line_color=line_color, + colormode=colormode if colormode != "fillgradient" else "mean-minmax", opacity=opacity, - interpolation_ctx=InterpolationContext( - densities=densities, - n_rows=n_rows, - n_traces=n_traces, - x_min=x_min, - x_max=x_max, - ), + interpolation_ctx=interpolation_ctx, ) - rows: list[RidgeplotRow] = [ - RidgeplotRow( - traces=[ - RidgeplotTrace(trace=trace, label=label, color=color) - for trace, label, color in zip_strict(traces, labels, colors) - ], - y_shifted=float(-ith_row * y_max * spacing), - ) - for ith_row, (traces, labels, colors) in enumerate( - zip_strict(densities, trace_labels, colors) - ) - ] + tickvals: list[float] = [] fig = go.Figure() - for row in rows: - for trace in row.traces: - x, y = zip(*trace.trace) - fig = draw_density_trace( - fig, - x=x, - y=y, - y_shifted=row.y_shifted, - label=trace.label, - color=trace.color, + ith_trace = 0 + for ith_row, (row_traces, row_trace_types, row_labels, row_colors) in enumerate( + zip_strict(densities, trace_types, trace_labels, solid_colors) + ): + y_shifted = float(-ith_row * y_max * spacing) + tickvals.append(y_shifted) + for trace, trace_type, label, color in zip_strict( + row_traces, row_trace_types, row_labels, row_colors + ): + trace_drawer = get_trace_cls(trace_type)( + trace=trace, + label=label, + solid_color=color, + zorder=ith_trace, + y_shifted=y_shifted, + line_color=line_color, line_width=line_width, ) + fig = trace_drawer.draw( + fig=fig, + coloring_ctx=ColoringContext( + colorscale=colorscale, + colormode=colormode, + opacity=opacity, + interpolation_ctx=interpolation_ctx, + ), + ) + ith_trace += 1 + fig = update_layout( fig, y_labels=y_labels, - tickvals=[row.y_shifted for row in rows], + tickvals=tickvals, show_yticklabels=show_yticklabels, xpad=xpad, x_max=x_max, diff --git a/src/ridgeplot/_hist.py b/src/ridgeplot/_hist.py new file mode 100644 index 00000000..ac919ddd --- /dev/null +++ b/src/ridgeplot/_hist.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from ridgeplot._kde import normalize_sample_weights +from ridgeplot._vendor.more_itertools import zip_strict + +if TYPE_CHECKING: + + from ridgeplot._types import ( + Densities, + Float, + Samples, + SamplesTrace, + SampleWeights, + SampleWeightsArray, + ShallowSampleWeightsArray, + XYCoordinate, + ) + + +def bin_trace_samples( + trace_samples: SamplesTrace, + nbins: int, + weights: SampleWeights, +) -> list[XYCoordinate[Float]]: + hist, bins = np.histogram( + np.asarray(trace_samples, dtype=float), + bins=nbins, + weights=np.asarray(weights, dtype=float) if weights is not None else None, + ) + return list(zip(bins[:-1], hist)) + + +def bin_samples( + samples: Samples, + nbins: int, + sample_weights: SampleWeightsArray | ShallowSampleWeightsArray | SampleWeights = None, +) -> Densities: + normalised_weights = normalize_sample_weights(sample_weights=sample_weights, samples=samples) + return [ + [ + bin_trace_samples(trace_samples, nbins=nbins, weights=weights) + for trace_samples, weights in zip_strict(samples_row, weights_row) + ] + for samples_row, weights_row in zip_strict(samples, normalised_weights) + ] diff --git a/src/ridgeplot/_kde.py b/src/ridgeplot/_kde.py index ceda645b..f48adbd3 100644 --- a/src/ridgeplot/_kde.py +++ b/src/ridgeplot/_kde.py @@ -3,7 +3,7 @@ import sys from collections.abc import Collection from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast +from typing import TYPE_CHECKING, Any, Callable, Union, cast import numpy as np import statsmodels.api as sm @@ -16,9 +16,11 @@ from ridgeplot._types import ( CollectionL1, - CollectionL2, Float, Numeric, + SampleWeights, + SampleWeightsArray, + ShallowSampleWeightsArray, is_flat_numeric_collection, nest_shallow_collection, ) @@ -37,17 +39,6 @@ KDEBandwidth = Union[str, float, Callable[[CollectionL1[Numeric], StatsmodelsKernel], float]] """The :paramref:`ridgeplot.ridgeplot.bandwidth` parameter.""" -SampleWeights = Optional[CollectionL1[Numeric]] -"""An array of KDE weights corresponding to each sample.""" - -SampleWeightsArray = CollectionL2[SampleWeights] -"""A :data:`SampleWeightsArray` represents the weights of the datapoints in a -:data:`Samples` array. The shape of the :data:`SampleWeightsArray` array should -match the shape of the corresponding :data:`Samples` array.""" - -ShallowSampleWeightsArray = CollectionL1[SampleWeights] -"""Shallow type for :data:`SampleWeightsArray`.""" - def _is_sample_weights(obj: Any) -> TypeIs[SampleWeights]: """Type guard for :data:`SampleWeights`. diff --git a/src/ridgeplot/_obj/__init__.py b/src/ridgeplot/_obj/__init__.py new file mode 100644 index 00000000..2e6f26f0 --- /dev/null +++ b/src/ridgeplot/_obj/__init__.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ridgeplot._obj._area import AreaTrace +from ridgeplot._obj._bar import BarTrace + +if TYPE_CHECKING: + from ridgeplot._obj._base import RidgeplotTrace + from ridgeplot._types import TraceType + + +TRACE_TYPES: dict[TraceType, type[RidgeplotTrace]] = { + "area": AreaTrace, + "bar": BarTrace, +} +"""Mapping of trace types to trace classes.""" + + +def get_trace_cls(trace_type: TraceType) -> type[RidgeplotTrace]: + """Get a trace class by its type.""" + try: + return TRACE_TYPES[trace_type] + except KeyError as err: + types = ", ".join(repr(t) for t in TRACE_TYPES) + raise ValueError(f"Unknown trace type {trace_type!r}. Available types: {types}.") from err diff --git a/src/ridgeplot/_obj/_area.py b/src/ridgeplot/_obj/_area.py new file mode 100644 index 00000000..a5ef685f --- /dev/null +++ b/src/ridgeplot/_obj/_area.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from typing import Any, ClassVar + +from plotly import graph_objects as go + +from ridgeplot._color.interpolation import slice_colorscale +from ridgeplot._color.utils import apply_alpha +from ridgeplot._obj._base import DEFAULT_HOVERTEMPLATE, ColoringContext, RidgeplotTrace +from ridgeplot._utils import normalise_min_max + + +class AreaTrace(RidgeplotTrace): + _DEFAULT_LINE_WIDTH: ClassVar[float] = 1.5 + + def _get_coloring_kwargs(self, ctx: ColoringContext) -> dict[str, Any]: + if ctx.colormode == "fillgradient": + if ctx.opacity is not None: + # HACK: Plotly doesn't yet support setting the fill opacity + # for traces with `fillgradient`. As a workaround, we + # can override the color-scale's color values and add + # the corresponding alpha channel to all colors. + ctx.colorscale = [ + (v, apply_alpha(c, float(ctx.opacity))) for v, c in ctx.colorscale + ] + color_kwargs = dict( + line_color=self.line_color, + fillgradient=go.scatter.Fillgradient( + colorscale=slice_colorscale( + colorscale=ctx.colorscale, + p_lower=normalise_min_max( + min(self.x), + min_=ctx.interpolation_ctx.x_min, + max_=ctx.interpolation_ctx.x_max, + ), + p_upper=normalise_min_max( + max(self.x), + min_=ctx.interpolation_ctx.x_min, + max_=ctx.interpolation_ctx.x_max, + ), + ), + type="horizontal", + ), + ) + else: + color_kwargs = dict( + line_color=self.line_color, + fillcolor=self.solid_color, + ) + return color_kwargs + + def draw(self, fig: go.Figure, coloring_ctx: ColoringContext) -> go.Figure: + # Draw an invisible trace at constance y=y_shifted so that we + # can set fill="tonexty" below and get a filled area plot. + fig.add_trace( + go.Scatter( + x=self.x, + y=[self.y_shifted] * len(self.x), + # make trace 'invisible' + # Note: visible=False does not work with fill="tonexty" + line=dict(color="rgba(0,0,0,0)", width=0), + # Hide this invisible helper trace from the legend and hoverinfo + showlegend=False, + hoverinfo="skip", + # z-order (higher z-order means the trace is drawn on top) + zorder=self.zorder, + ) + ) + fig.add_trace( + go.Scatter( + x=self.x, + y=[y_i + self.y_shifted for y_i in self.y], + name=self.label, + fill="tonexty", + mode="lines", + line=dict(width=self.line_width if self.line_width is not None else 1.5), + **self._get_coloring_kwargs(ctx=coloring_ctx), + # Hover information + customdata=[[y_i] for y_i in self.y], + hovertemplate=DEFAULT_HOVERTEMPLATE, + # z-order (higher z-order means the trace is drawn on top) + zorder=self.zorder, + ), + ) + return fig diff --git a/src/ridgeplot/_obj/_bar.py b/src/ridgeplot/_obj/_bar.py new file mode 100644 index 00000000..78c50d06 --- /dev/null +++ b/src/ridgeplot/_obj/_bar.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import Any, ClassVar + +from plotly import graph_objects as go + +from ridgeplot._color.interpolation import interpolate_color +from ridgeplot._obj._base import DEFAULT_HOVERTEMPLATE, ColoringContext, RidgeplotTrace +from ridgeplot._utils import normalise_min_max + + +class BarTrace(RidgeplotTrace): + _DEFAULT_LINE_WIDTH: ClassVar[float] = 0.5 + + def _get_coloring_kwargs(self, ctx: ColoringContext) -> dict[str, Any]: + if ctx.colormode == "fillgradient": + color_kwargs = dict( + marker_line_color=self.line_color, + marker_color=[ + interpolate_color( + colorscale=ctx.colorscale, + p=normalise_min_max( + x_i, min_=ctx.interpolation_ctx.x_min, max_=ctx.interpolation_ctx.x_max + ), + ) + for x_i in self.x + ], + ) + else: + color_kwargs = dict( + marker_line_color=self.line_color, + marker_color=self.solid_color, + ) + return color_kwargs + + def draw(self, fig: go.Figure, coloring_ctx: ColoringContext) -> go.Figure: + fig.add_trace( + go.Bar( + x=self.x, + y=self.y, + name=self.label, + base=self.y_shifted, + marker_line_width=self.line_width if self.line_width is not None else 0.5, + # width=1, # TODO: do we need to specify the bar width? + **self._get_coloring_kwargs(ctx=coloring_ctx), + # Hover information + customdata=[[y_i] for y_i in self.y], + hovertemplate=DEFAULT_HOVERTEMPLATE, + # z-order (higher z-order means the trace is drawn on top) + zorder=self.zorder, + ), + ) + return fig diff --git a/src/ridgeplot/_obj/_base.py b/src/ridgeplot/_obj/_base.py new file mode 100644 index 00000000..bda55cbf --- /dev/null +++ b/src/ridgeplot/_obj/_base.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import TYPE_CHECKING, ClassVar, Literal + +from ridgeplot._vendor.more_itertools import zip_strict + +if TYPE_CHECKING: + from plotly import graph_objects as go + + from ridgeplot._color.interpolation import InterpolationContext, SolidColormode + from ridgeplot._types import Color, ColorScale, DensityTrace + + +_D3HF = ".7" +"""Default (d3-format) format for floats in hover labels. + +After trying to read through the plotly.py source code, I couldn't find a +simple way to replicate the default hover format using the d3-format syntax +in Plotly's 'hovertemplate' parameter. The closest I got was by using the +string below, but it's not quite the same... (see '.7~r' as well) +""" + +DEFAULT_HOVERTEMPLATE = ( + f"(%{{x:{_D3HF}}}, %{{customdata[0]:{_D3HF}}})" + "
" + "%{fullData.name}" +) # fmt: skip +"""Default ``hovertemplate`` for density traces. + +The default hover template that should be used for all density traces. It +displays the x and y values of the hovered point, as well as the trace's name. +When using this as ``hovertemplate=DEFAULT_HOVERTEMPLATE``, it is expected that +the trace's ``customdata`` is set to a list of lists, where each inner list +contains a single element that is the y-value of the corresponding x-value +(e.g. ``customdata=[[y_i] for y_i in y]``). The ``name`` attribute of the trace +should also be set to the desired label for the trace (e.g. ``name=self.label``). +""" + + +@dataclass +class ColoringContext: + colorscale: ColorScale + colormode: Literal["fillgradient"] | SolidColormode + opacity: float | None + interpolation_ctx: InterpolationContext + + +class RidgeplotTrace(ABC): + _DEFAULT_LINE_WIDTH: ClassVar[float] = 2.0 + + def __init__( + self, + trace: DensityTrace, + label: str, + solid_color: str, + zorder: int, + # Constant over the trace's row + y_shifted: float, + # Constant over the entire plot + line_color: Color | Literal["fill-color"], + line_width: float | None, + ): + self.x, self.y = zip_strict(*trace) + self.label = label + self.solid_color = solid_color + self.zorder = zorder + self.y_shifted = y_shifted + self.line_color: Color = self.solid_color if line_color == "fill-color" else line_color + self.line_width: float = line_width if line_width is not None else self._DEFAULT_LINE_WIDTH + + @abstractmethod + def draw(self, fig: go.Figure, coloring_ctx: ColoringContext) -> go.Figure: + raise NotImplementedError diff --git a/src/ridgeplot/_ridgeplot.py b/src/ridgeplot/_ridgeplot.py index de7b00c6..fbad3231 100644 --- a/src/ridgeplot/_ridgeplot.py +++ b/src/ridgeplot/_ridgeplot.py @@ -1,22 +1,13 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, Literal, cast +from typing import TYPE_CHECKING, cast -from ridgeplot._figure_factory import ( - LabelsArray, - ShallowLabelsArray, - create_ridgeplot, -) -from ridgeplot._missing import MISSING, MissingType +from ridgeplot._figure_factory import create_ridgeplot +from ridgeplot._missing import MISSING from ridgeplot._types import ( - Color, - ColorScale, Densities, - NormalisationOption, Samples, - ShallowDensities, - ShallowSamples, is_shallow_densities, is_shallow_samples, nest_shallow_collection, @@ -25,6 +16,7 @@ if TYPE_CHECKING: from collections.abc import Collection + from typing import Literal import plotly.graph_objects as go @@ -32,39 +24,68 @@ from ridgeplot._kde import ( KDEBandwidth, KDEPoints, + ) + from ridgeplot._missing import MissingType + from ridgeplot._types import ( + Color, + ColorScale, + LabelsArray, + NormalisationOption, SampleWeights, SampleWeightsArray, + ShallowDensities, + ShallowLabelsArray, + ShallowSamples, ShallowSampleWeightsArray, + ShallowTraceTypesArray, + TraceType, + TraceTypesArray, ) def _coerce_to_densities( samples: Samples | ShallowSamples | None, densities: Densities | ShallowDensities | None, + # KDE parameters kernel: str, bandwidth: KDEBandwidth, kde_points: KDEPoints, + # Histogram parameters + nbins: int | None, + # Common parameters for density estimation sample_weights: SampleWeightsArray | ShallowSampleWeightsArray | SampleWeights, ) -> Densities: # Importing statsmodels, scipy, and numpy can be slow, # so we're hiding the kde import here to only incur # this cost if the user actually needs this it... + from ridgeplot._hist import bin_samples from ridgeplot._kde import estimate_densities + # Input validation has_samples = samples is not None has_densities = densities is not None if has_samples and has_densities: raise ValueError("You may not specify both `samples` and `densities` arguments!") if not has_samples and not has_densities: raise ValueError("You must specify either `samples` or `densities`") + + # Exit early if densities are already provided if has_densities: if is_shallow_densities(densities): densities = nest_shallow_collection(densities) - densities = cast(Densities, densities) + return cast(Densities, densities) + + # Transform samples into densities via KDE or histogram binning + if is_shallow_samples(samples): + samples = nest_shallow_collection(samples) + samples = cast(Samples, samples) + if nbins is not None: + densities = bin_samples( + samples=samples, + nbins=nbins, + sample_weights=sample_weights, + ) else: - if is_shallow_samples(samples): - samples = nest_shallow_collection(samples) - samples = cast(Samples, samples) densities = estimate_densities( samples=samples, points=kde_points, @@ -78,29 +99,40 @@ def _coerce_to_densities( def ridgeplot( samples: Samples | ShallowSamples | None = None, densities: Densities | ShallowDensities | None = None, + trace_type: TraceTypesArray | ShallowTraceTypesArray | TraceType = "area", + labels: LabelsArray | ShallowLabelsArray | None = None, + # KDE parameters kernel: str = "gau", bandwidth: KDEBandwidth = "normal_reference", kde_points: KDEPoints = 500, + # Histogram parameters + nbins: int | None = None, + # Common parameters for density estimation sample_weights: SampleWeightsArray | ShallowSampleWeightsArray | SampleWeights = None, + norm: NormalisationOption | None = None, + # Coloring and styling parameters colorscale: ColorScale | Collection[Color] | str | None = None, colormode: Literal["fillgradient"] | SolidColormode = "fillgradient", opacity: float | None = None, - labels: LabelsArray | ShallowLabelsArray | None = None, - norm: NormalisationOption | None = None, line_color: Color | Literal["fill-color"] = "black", - line_width: float = 1.5, + line_width: float | None = None, spacing: float = 0.5, show_yticklabels: bool = True, xpad: float = 0.05, - # Deprecated arguments + # Deprecated parameters coloralpha: float | None | MissingType = MISSING, linewidth: float | MissingType = MISSING, ) -> go.Figure: r"""Return an interactive ridgeline (Plotly) |~go.Figure|. .. note:: - You must pass either :paramref:`samples` or :paramref:`densities` to - this function, but not both. See descriptions below for more details. + You must specify either :paramref:`samples` or :paramref:`densities` to + this function, but not both. When specifying :paramref:`samples`, the + function will estimate the densities using either Kernel Density + Estimation (KDE) or histogram binning. When specifying + :paramref:`densities`, the function will skip the density estimation + step and use the provided densities directly. See the parameter + descriptions below for more details. .. _bandwidths.py: https://www.statsmodels.org/stable/_modules/statsmodels/nonparametric/bandwidths.html @@ -112,12 +144,19 @@ def ridgeplot( Parameters ---------- samples : Samples or ShallowSamples - If ``samples`` data is specified, Kernel Density Estimation (KDE) will - be computed. See :paramref:`kernel`, :paramref:`bandwidth`, - :paramref:`kde_points`, and :paramref:`sample_weights` for more details - and KDE configuration options. The ``samples`` argument should be an - array of shape :math:`(R, T_r, S_t)`. Note that we support irregular - (`ragged`_) arrays, where: + If ``samples`` data is specified, either Kernel Density Estimation (KDE) + or histogram binning will be performed to estimate the underlying + densities. + + See :paramref:`kernel`, :paramref:`bandwidth`, and + :paramref:`kde_points` for more details on the different KDE parameters. + See :paramref:`nbins` for more details on histogram binning. The + :paramref:`sample_weights` parameter can be used for both KDE and + histogram binning. + + The ``samples`` argument should be an array of shape + :math:`(R, T_r, S_t)`. Note that we support irregular (`ragged`_) + arrays, where: - :math:`R` is the number of rows in the plot - :math:`T_r` is the number of traces per row, where each row @@ -125,17 +164,17 @@ def ridgeplot( - :math:`S_t` is the number of samples per trace, where each trace :math:`t \in T_r` can also have a different number of samples. - The KDE will be performed over the sample values (:math:`S_t`) for all - traces. After the KDE, the resulting array will be a (4D) - :paramref:`densities` array with shape :math:`(R, T_r, P_t, 2)` - (see below for more details). + The density estimation step will be performed over the sample values + (:math:`S_t`) for all traces. The resulting array will be a (4D) + :paramref:`densities` array of shape :math:`(R, T_r, P_t, 2)` + (see :paramref:`densities` below for more details). densities : Densities or ShallowDensities - If a ``densities`` array is specified, the KDE step will be skipped and - all associated arguments ignored. Each density array should have shape - :math:`(R, T_r, P_t, 2)` (4D). Just like the :paramref:`samples` - argument, we also support irregular (`ragged`_) ``densities`` arrays, - where: + If a ``densities`` array is specified, the density estimation step will + be skipped and all associated arguments ignored. Each density array + should have shape :math:`(R, T_r, P_t, 2)` (4D). Just like the + :paramref:`samples` argument, we also support irregular (`ragged`_) + ``densities`` arrays, where: - :math:`R` is the number of rows in the plot - :math:`T_r` is the number of traces per row, where each row @@ -144,6 +183,20 @@ def ridgeplot( :math:`t \in T_r` can also have a different number of points. - :math:`2` is the number of coordinates per point (x and y) + See :paramref:`samples` above for more details. + + trace_type : TraceTypesArray or ShallowTraceTypesArray or TraceType + The type of trace to display. The default is ``"area"``. Choices are + ``"area"`` or ``"bar"``. If a single value is passed, it will be used + for all traces. If a list of values is passed, it should have the same + shape as the samples array. + + labels : LabelsArray or ShallowLabelsArray or None + A list of string labels for each trace. If not specified (default), the + labels will be automatically generated as ``"Trace {n}"``, where ``n`` + is the trace's index. If instead a list of labels is specified, it + should have the same shape as the samples array. + kernel : str The Kernel to be used during Kernel Density Estimation. The default is a Gaussian Kernel (``"gau"``). Choices are: @@ -182,11 +235,27 @@ def ridgeplot( set of samples. Optionally, you can also pass a custom 1D numerical array, which will be used for all traces. + nbins : int or None + The number of bins to use when applying histogram binning. If not + specified (default), KDE will be used instead of histogram binning. + sample_weights : SampleWeightsArray or ShallowSampleWeightsArray or SampleWeights or None An (optional) array of KDE weights corresponding to each sample. The weights should have the same shape as the samples array. If not specified (default), all samples will be weighted equally. + norm : NormalisationOption or None + The normalisation option to use when normalising the densities. If not + specified (default), no normalisation will be applied and the densities + will be used *as is*. The following normalisation options are available: + + - ``"probability"`` - normalise the densities by dividing each trace by + its sum. + - ``"percent"`` - same as ``"probability"``, but the normalised values + are multiplied by 100. + + .. versionadded:: 0.2.0 + colorscale : ColorScale or Collection[Color] or str or None A continuous color scale used to color the different traces in the ridgeline plot. It can be represented by a string name (e.g., @@ -256,24 +325,6 @@ def ridgeplot( .. versionadded:: 0.2.0 Replaces the deprecated :paramref:`coloralpha` argument. - labels : LabelsArray or ShallowLabelsArray or None - A list of string labels for each trace. If not specified (default), the - labels will be automatically generated as ``"Trace {n}"``, where ``n`` - is the trace's index. If instead a list of labels is specified, it - should have the same shape as the samples array. - - norm : NormalisationOption or None - The normalisation option to use when normalising the densities. If not - specified (default), no normalisation will be applied and the densities - will be used *as is*. The following normalisation options are available: - - - ``"probability"`` - normalise the densities by dividing each trace by - its sum. - - ``"percent"`` - same as ``"probability"``, but the normalised values - are multiplied by 100. - - .. versionadded:: 0.2.0 - line_color : Color or "fill-color" The color of the traces' lines. Any valid CSS color is allowed (default: ``"black"``). If the value is set to "fill-color", the line @@ -284,8 +335,10 @@ def ridgeplot( .. versionadded:: 0.2.0 - line_width : float - The traces' line width (in px). + line_width : float or None + The traces' line width (in px). If not specified (default), area plots + will have a line width of 1.5 px, and bar plots will have a line width + of 0.5 px. .. versionadded:: 0.2.0 Replaces the deprecated :paramref:`linewidth` argument. @@ -338,6 +391,7 @@ def ridgeplot( kernel=kernel, bandwidth=bandwidth, kde_points=kde_points, + nbins=nbins, sample_weights=sample_weights, ) del samples, kernel, bandwidth, kde_points @@ -360,7 +414,7 @@ def ridgeplot( opacity = coloralpha if linewidth is not MISSING: - if line_width != 1.5: + if line_width is not None: raise ValueError( "You may not specify both the 'linewidth' and 'line_width' arguments! " "HINT: Use the new 'line_width' argument instead of the deprecated 'linewidth'." @@ -389,6 +443,7 @@ def ridgeplot( fig = create_ridgeplot( densities=densities, trace_labels=labels, + trace_types=trace_type, colorscale=colorscale, opacity=opacity, colormode=colormode, diff --git a/src/ridgeplot/_types.py b/src/ridgeplot/_types.py index 7ed7de36..b4173d7e 100644 --- a/src/ridgeplot/_types.py +++ b/src/ridgeplot/_types.py @@ -2,7 +2,7 @@ import sys from collections.abc import Collection -from typing import Any, Literal, TypeVar, Union +from typing import Any, Literal, Optional, TypeVar, Union import numpy as np @@ -502,6 +502,72 @@ def is_shallow_samples(obj: Any) -> TypeIs[ShallowSamples]: return isinstance(obj, Collection) and all(map(is_trace_samples, obj)) +# ======================================================== +# --- Other array types +# ======================================================== + + +# Trace types --- + +TraceType = Literal["area", "bar"] +"""The type of trace to draw in a ridgeplot. See +:paramref:`ridgeplot.ridgeplot.trace_type` for more information.""" + +TraceTypesArray = CollectionL2[TraceType] +"""A :data:`TraceTypesArray` represents the types of traces in a ridgeplot. + +Example +------- +>>> trace_types_array: TraceTypesArray = [ +... ["area", "bar", "area"], +... ["bar", "area"], +... ] +""" + +ShallowTraceTypesArray = CollectionL1[TraceType] +"""Shallow type for :data:`TraceTypesArray`. + +Example +------- +>>> trace_types_array: ShallowTraceTypesArray = ["area", "bar", "area"] +""" + +# Labels --- + +LabelsArray = CollectionL2[str] +"""A :data:`LabelsArray` represents the labels of traces in a ridgeplot. + +Example +------- + +>>> labels_array: LabelsArray = [ +... ["trace 1", "trace 2", "trace 3"], +... ["trace 4", "trace 5"], +... ] +""" + +ShallowLabelsArray = CollectionL1[str] +"""Shallow type for :data:`LabelsArray`. + +Example +------- + +>>> labels_array: ShallowLabelsArray = ["trace 1", "trace 2", "trace 3"] +""" + +# Sample weights --- + +SampleWeights = Optional[CollectionL1[Numeric]] +"""An array of KDE weights corresponding to each sample.""" + +SampleWeightsArray = CollectionL2[SampleWeights] +"""A :data:`SampleWeightsArray` represents the weights of the datapoints in a +:data:`Samples` array. The shape of the :data:`SampleWeightsArray` array should +match the shape of the corresponding :data:`Samples` array.""" + +ShallowSampleWeightsArray = CollectionL1[SampleWeights] +"""Shallow type for :data:`SampleWeightsArray`.""" + # ======================================================== # --- More type guards and other utilities # ======================================================== diff --git a/tests/unit/color/test_interpolation.py b/tests/unit/color/test_interpolation.py index 3bddf200..5b6b40b6 100644 --- a/tests/unit/color/test_interpolation.py +++ b/tests/unit/color/test_interpolation.py @@ -10,11 +10,10 @@ ColorscaleInterpolants, InterpolationContext, SolidColormode, - _interpolate_color, _interpolate_mean_means, _interpolate_mean_minmax, - _slice_colorscale, - compute_trace_colors, + interpolate_color, + slice_colorscale, ) from ridgeplot._color.utils import to_rgb @@ -23,36 +22,106 @@ # ============================================================== -# --- _interpolate_color() +# --- interpolate_color() # ============================================================== def test_interpolate_color_p_in_scale(viridis_colorscale: ColorScale) -> None: viridis_colorscale = list(viridis_colorscale) - assert _interpolate_color(colorscale=viridis_colorscale, p=0) == to_rgb( - viridis_colorscale[0][1] - ) - assert _interpolate_color(colorscale=viridis_colorscale, p=1) == to_rgb( + assert interpolate_color(colorscale=viridis_colorscale, p=0) == to_rgb(viridis_colorscale[0][1]) + assert interpolate_color(colorscale=viridis_colorscale, p=1) == to_rgb( viridis_colorscale[-1][1] ) # Test that the alpha channels are also properly handled here cs = ((0, "rgba(0, 0, 0, 0)"), (1, "rgba(255, 255, 255, 1)")) - assert _interpolate_color(colorscale=cs, p=0) == cs[0][1] - assert _interpolate_color(colorscale=cs, p=1) == cs[-1][1] + assert interpolate_color(colorscale=cs, p=0) == cs[0][1] + assert interpolate_color(colorscale=cs, p=1) == cs[-1][1] def test_interpolate_color_p_not_in_scale(viridis_colorscale: ColorScale) -> None: # Hard-coded test case for the Viridis colorscale - assert _interpolate_color(colorscale=viridis_colorscale, p=0.5) == "rgb(34.5, 144.0, 139.5)" + assert interpolate_color(colorscale=viridis_colorscale, p=0.5) == "rgb(34.5, 144.0, 139.5)" # Test that the alpha channels are also properly handled here cs = ((0, "rgba(0, 0, 0, 0)"), (1, "rgba(255, 255, 255, 1)")) - assert _interpolate_color(colorscale=cs, p=0.5) == "rgba(127.5, 127.5, 127.5, 0.5)" + assert interpolate_color(colorscale=cs, p=0.5) == "rgba(127.5, 127.5, 127.5, 0.5)" @pytest.mark.parametrize("p", [-10.0, -1.3, 1.9, 100.0]) def test_interpolate_color_fails_for_p_out_of_bounds(p: float) -> None: with pytest.raises(ValueError, match="should be a float value between 0 and 1"): - _interpolate_color(colorscale=..., p=p) # type: ignore[arg-type] + interpolate_color(colorscale=..., p=p) # type: ignore[arg-type] + + +# ============================================================== +# --- slice_colorscale() +# ============================================================== + + +def test_slice_colorscale_lower_less_than_upper() -> None: + with pytest.raises(ValueError, match="p_lower should be less than p_upper"): + slice_colorscale(colorscale=[(0, "...")], p_lower=1, p_upper=0) + + +def test_slice_colorscale_lower_than_0() -> None: + with pytest.raises(ValueError, match="p_lower should be >= 0"): + slice_colorscale(colorscale=[(0, "...")], p_lower=-1, p_upper=0) + + +def test_slice_colorscale_upper_than_1() -> None: + with pytest.raises(ValueError, match="p_upper should be <= 1"): + slice_colorscale(colorscale=[(0, "...")], p_lower=0, p_upper=1.1) + + +def test_slice_colorscale_unchanged() -> None: + cs = ((0, "rgb(0, 0, 0)"), (1, "rgb(255, 255, 255)")) + assert slice_colorscale(colorscale=cs, p_lower=0, p_upper=1) == cs + + +def test_slice_colorscale() -> None: + cs = ( + (0, "rgb(0, 0, 0)"), + (0.5, "rgb(127.5, 127.5, 127.5)"), + (1, "rgb(255, 255, 255)"), + ) + assert slice_colorscale(colorscale=cs, p_lower=0.25, p_upper=0.75) == ( + (0.0, "rgb(63.75, 63.75, 63.75)"), + (0.5, "rgb(127.5, 127.5, 127.5)"), + (1.0, "rgb(191.25, 191.25, 191.25)"), + ) + + +def test_slice_colorscale_no_intermediate_values() -> None: + cs = ((0, "rgb(0, 0, 0)"), (1, "rgb(255, 255, 255)")) + assert slice_colorscale(colorscale=cs, p_lower=0.25, p_upper=0.75) == ( + (0.0, "rgb(63.75, 63.75, 63.75)"), + (1.0, "rgb(191.25, 191.25, 191.25)"), + ) + + +def test_slice_colorscale_alpha() -> None: + cs = ( + (0, "rgba(0, 0, 0, 0)"), + (0.5, "rgba(127.5, 127.5, 127.5, 0.5)"), + (1, "rgba(255, 255, 255, 1)"), + ) + assert slice_colorscale(colorscale=cs, p_lower=0.25, p_upper=0.75) == ( + (0.0, "rgba(63.75, 63.75, 63.75, 0.25)"), + (0.5, "rgba(127.5, 127.5, 127.5, 0.5)"), + (1.0, "rgba(191.25, 191.25, 191.25, 0.75)"), + ) + + +def test_slice_colorscale_mixed_alpha_channels() -> None: + cs = ( + (0, "rgba(0, 0, 0, 0)"), + (0.5, "rgba(127.5, 127.5, 127.5, 1)"), + (1, "rgba(255, 255, 255, 0)"), + ) + assert slice_colorscale(colorscale=cs, p_lower=0.25, p_upper=0.75) == ( + (0.0, "rgba(63.75, 63.75, 63.75, 0.5)"), + (0.5, "rgba(127.5, 127.5, 127.5, 1)"), + (1.0, "rgba(191.25, 191.25, 191.25, 0.5)"), + ) # ============================================================== @@ -130,93 +199,3 @@ def test_index_based_colormodes( interpolate_func = SOLID_COLORMODE_MAPS[colormode] interpolants = interpolate_func(ctx=InterpolationContext.from_densities(densities)) assert interpolants == expected - - -# ============================================================== -# --- _slice_colorscale() -# ============================================================== - - -def test_slice_colorscale_lower_less_than_upper() -> None: - with pytest.raises(ValueError, match="p_lower should be less than p_upper"): - _slice_colorscale(colorscale=[(0, "...")], p_lower=1, p_upper=0) - - -def test_slice_colorscale_lower_than_0() -> None: - with pytest.raises(ValueError, match="p_lower should be >= 0"): - _slice_colorscale(colorscale=[(0, "...")], p_lower=-1, p_upper=0) - - -def test_slice_colorscale_upper_than_1() -> None: - with pytest.raises(ValueError, match="p_upper should be <= 1"): - _slice_colorscale(colorscale=[(0, "...")], p_lower=0, p_upper=1.1) - - -def test_slice_colorscale_unchanged() -> None: - cs = ((0, "rgb(0, 0, 0)"), (1, "rgb(255, 255, 255)")) - assert _slice_colorscale(colorscale=cs, p_lower=0, p_upper=1) == cs - - -def test_slice_colorscale() -> None: - cs = ( - (0, "rgb(0, 0, 0)"), - (0.5, "rgb(127.5, 127.5, 127.5)"), - (1, "rgb(255, 255, 255)"), - ) - assert _slice_colorscale(colorscale=cs, p_lower=0.25, p_upper=0.75) == ( - (0.0, "rgb(63.75, 63.75, 63.75)"), - (0.5, "rgb(127.5, 127.5, 127.5)"), - (1.0, "rgb(191.25, 191.25, 191.25)"), - ) - - -def test_slice_colorscale_no_intermediate_values() -> None: - cs = ((0, "rgb(0, 0, 0)"), (1, "rgb(255, 255, 255)")) - assert _slice_colorscale(colorscale=cs, p_lower=0.25, p_upper=0.75) == ( - (0.0, "rgb(63.75, 63.75, 63.75)"), - (1.0, "rgb(191.25, 191.25, 191.25)"), - ) - - -def test_slice_colorscale_alpha() -> None: - cs = ( - (0, "rgba(0, 0, 0, 0)"), - (0.5, "rgba(127.5, 127.5, 127.5, 0.5)"), - (1, "rgba(255, 255, 255, 1)"), - ) - assert _slice_colorscale(colorscale=cs, p_lower=0.25, p_upper=0.75) == ( - (0.0, "rgba(63.75, 63.75, 63.75, 0.25)"), - (0.5, "rgba(127.5, 127.5, 127.5, 0.5)"), - (1.0, "rgba(191.25, 191.25, 191.25, 0.75)"), - ) - - -def test_slice_colorscale_mixed_alpha_channels() -> None: - cs = ( - (0, "rgba(0, 0, 0, 0)"), - (0.5, "rgba(127.5, 127.5, 127.5, 1)"), - (1, "rgba(255, 255, 255, 0)"), - ) - assert _slice_colorscale(colorscale=cs, p_lower=0.25, p_upper=0.75) == ( - (0.0, "rgba(63.75, 63.75, 63.75, 0.5)"), - (0.5, "rgba(127.5, 127.5, 127.5, 1)"), - (1.0, "rgba(191.25, 191.25, 191.25, 0.5)"), - ) - - -# ============================================================== -# --- compute_trace_colors() -# ============================================================== - - -def test_colormode_invalid() -> None: - with pytest.raises( - ValueError, match="The colormode argument should be one of .* got INVALID instead" - ): - compute_trace_colors( - colorscale="Viridis", - colormode="INVALID", # type: ignore[call-overload] - line_color="black", - opacity=None, - interpolation_ctx=InterpolationContext.from_densities([[[(0, 0)]]]), - ) diff --git a/tests/unit/test_figure_factory.py b/tests/unit/test_figure_factory.py index 008ccf3f..001209c6 100644 --- a/tests/unit/test_figure_factory.py +++ b/tests/unit/test_figure_factory.py @@ -26,6 +26,7 @@ def test_densities_must_be_4d(self, densities: Densities) -> None: with pytest.raises(ValueError, match="Expected a 4D array of densities"): create_ridgeplot( densities=densities, + trace_types=..., # type: ignore[arg-type] colorscale=..., # type: ignore[arg-type] opacity=..., # type: ignore[arg-type] colormode=..., # type: ignore[arg-type] diff --git a/tox.ini b/tox.ini index 76f11217..e9f581fc 100644 --- a/tox.ini +++ b/tox.ini @@ -73,7 +73,7 @@ description = run type checks with mypy deps = -r requirements/mypy.txt setenv = {[testenv]setenv} - _MYPY_DFLT_ARGS=--config-file=mypy.ini --strict --enable-incomplete-feature=NewGenericSyntax + _MYPY_DFLT_ARGS=--config-file=mypy.ini --strict commands = safe: mypy {env:_MYPY_DFLT_ARGS} --no-incremental --cache-dir=/dev/null {posargs:} incremental: mypy {env:_MYPY_DFLT_ARGS} --incremental {posargs:} From 334832f7789bae1a2cebb30b22b0415b283d3936 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Sat, 30 Nov 2024 18:05:04 +0000 Subject: [PATCH 02/21] Add histogram example to getting started docs --- cicd_utils/ridgeplot_examples/__init__.py | 7 +++++ cicd_utils/ridgeplot_examples/_basic.py | 4 +-- cicd_utils/ridgeplot_examples/_basic_hist.py | 24 ++++++++++++++++++ docs/_static/charts/basic.html | 2 +- docs/_static/charts/basic.jpeg | Bin 40654 -> 36471 bytes docs/_static/charts/basic.webp | Bin 57498 -> 50946 bytes docs/_static/charts/basic_hist.html | 1 + docs/_static/charts/basic_hist.jpeg | Bin 0 -> 39816 bytes docs/_static/charts/basic_hist.webp | Bin 0 -> 36328 bytes docs/_static/charts/lincoln_weather.html | 2 +- .../charts/lincoln_weather_red_blue.html | 2 +- docs/_static/charts/probly.html | 2 +- docs/conf.py | 4 +-- docs/getting_started/getting_started.md | 14 +++++++++- docs/index.md | 2 +- 15 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 cicd_utils/ridgeplot_examples/_basic_hist.py create mode 100644 docs/_static/charts/basic_hist.html create mode 100644 docs/_static/charts/basic_hist.jpeg create mode 100644 docs/_static/charts/basic_hist.webp diff --git a/cicd_utils/ridgeplot_examples/__init__.py b/cicd_utils/ridgeplot_examples/__init__.py index b46bb7ba..b0faf677 100644 --- a/cicd_utils/ridgeplot_examples/__init__.py +++ b/cicd_utils/ridgeplot_examples/__init__.py @@ -24,6 +24,12 @@ def load_basic() -> go.Figure: return main() +def load_basic_hist() -> go.Figure: + from ._basic_hist import main + + return main() + + def load_lincoln_weather() -> go.Figure: from ._lincoln_weather import main @@ -44,6 +50,7 @@ def load_probly() -> go.Figure: ALL_EXAMPLES: list[tuple[str, Callable[[], go.Figure]]] = [ ("basic", load_basic), + ("basic_hist", load_basic_hist), ("lincoln_weather", load_lincoln_weather), ("lincoln_weather_red_blue", load_lincoln_weather_red_blue), ("probly", load_probly), diff --git a/cicd_utils/ridgeplot_examples/_basic.py b/cicd_utils/ridgeplot_examples/_basic.py index 885ad5a5..8192f1f6 100644 --- a/cicd_utils/ridgeplot_examples/_basic.py +++ b/cicd_utils/ridgeplot_examples/_basic.py @@ -12,9 +12,9 @@ def main() -> go.Figure: from ridgeplot import ridgeplot rng = np.random.default_rng(42) - my_samples = [rng.normal(n / 1.2, size=600) for n in range(7, 0, -1)] + my_samples = [rng.normal(n / 1.2, size=600) for n in range(6, 0, -1)] fig = ridgeplot(samples=my_samples) - fig.update_layout(height=400, width=800) + fig.update_layout(height=350, width=800) return fig diff --git a/cicd_utils/ridgeplot_examples/_basic_hist.py b/cicd_utils/ridgeplot_examples/_basic_hist.py new file mode 100644 index 00000000..9222438d --- /dev/null +++ b/cicd_utils/ridgeplot_examples/_basic_hist.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 / 1.2, size=600) for n in range(7, 0, -1)] + fig = ridgeplot(samples=my_samples, nbins=20, trace_type="bar") + fig.update_layout(height=350, width=800) + + return fig + + +if __name__ == "__main__": + fig = main() + fig.show() diff --git a/docs/_static/charts/basic.html b/docs/_static/charts/basic.html index 8e43d4db..a3a124c7 100644 --- a/docs/_static/charts/basic.html +++ b/docs/_static/charts/basic.html @@ -1 +1 @@ -
+
diff --git a/docs/_static/charts/basic.jpeg b/docs/_static/charts/basic.jpeg index 29ffc7617e7d35518f8cf227ce035c4a6e565948..42b90ebfcc1f17502194b8b6bb0430acdbd71b07 100644 GIT binary patch literal 36471 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

(Qq>jW1dXBvr-iP zN7G-K!G8q*Vzei6MpA;MtfPo>RPEDphVRzq?midSr^2;&C_bd}U=q8tF)2APuZR#H z`&4K0xCKJzK{Z+FJU+p3BB!6KD&Lg>2YG9n6edyW(&zQ-REz5BW$H?(cs`BC`$<2v zj65w$Z0f}KY--HP!073su?mZzhPBgDy6CLPw6zTb-Z#BcQ%!ljNx`H?Z^cPeS+t03 zS^+IStd(ic8yzs@-XvT+?YLIiCCX}VB9vAm`cnj-IIi0A06BMiH%rdcBqSZCVAg4% zKwLDD*ce@5U9hp18985F1V@>SJU5#YBnq$0-d!I3PViP=UIKST3+$rAK39|$xo`Tj zx;fLs!Z6PAf-+i;eK3_Ri@Z(yLCp2_%wDLkYOSN*RbMHjg1KJ(iafdagdN2P>C?)% z1lol`I&%!kl^%2K8ha@oin!W)kDE*tk10j0jKqF?*!v_Vj z=qn_4mn!?d-g>ld4;TGmj9PT`a&314>qJr+&uQcsWq!y^QI27-LCz3GtVc_9pb~$c zxrlsrsVGvFuWJDxKg3r>{rg4HdQ`=zbd0?FyKxWs=-&u||3~>P-OP#+3TMh)4MQh* z;iEZc*E=^dd0}jFF@p+Erk#%usd%4KB~0#*n=!20VxUP>aq6y2JPv`YmM>x}!?>Bj z^<|sN5gw>sEeLqufS;PX!bU!UpEO4?k}V5dr7telqVz2`EJm!!frwSm zp9X!k&{#lq3wCoBz-n$P2TQe>r!#>8%!?0Ck7%carv@dn?58QQ?nbh?v(r4dBFvKl z4c)pmA>vLA3l%eUQ;vgPmKW7vJ{ek>j>?JQUL96Ds9T?o*RY^rjRv^-i~c?aeS2gj zYNbM6GuR_)kp=}3ygTDkzgUIBVmirmC*&Rv#6vf{;KF=V&IJF?>F z{8ftBWxmvnPhytF1c~coT*jkAu81v}LEnqq&NGb>EvIMdPC%8)kY~V+Qfx3hy-yLY z=H=4pEm6bvz7fTc_yallZ))EO)Nf!X2sx)R33>HHKMYgEKFFJB_L5sn#PRGN`-%6R zu+%c~7+wH|%<;cmd#azN%cOaj%x~)PnZg;~ zDsixt_c_ncC%x&DPR&gSGQ|B#*1 ztVEoGzlvq$GvA?(%WJ_=^HHV{`$O~SNMztMOa9FeLu_WJ%_O0LOkahjmF3AA&P)r< z61YT+0QGtx>jiQAq0^BI93=@MVIcL`(l9IaR4d(xkZ6nAuH&x5h^#%&K*e%|i4KLq zVCE;Q7I2Z04xsyxk3F-$uT8r=Pfc4YWDCb!C>>VjohKwxr9W+!U>Z=Xl9RMNXA-X% z%&ZylGO!8S*aSsls9P{26W#ST`;?rmiREB&fSM@2e>W@#l<>EydfbP?i$IB9zs0U_ zDKFg-;Ac`qh?0A5t`+zmRU zAr%%OuBct@X!NljPgU_5l=n6yoTEsgsdnLoo7oBOSYzC-7I}eY`c#{$UpSv8jmxVgg(rZga^1S0lpYK@7%nhUpO#< zTpDq?>PWfI_`W#0Qn7u^aPDj#^h5NYr`^lx%HC-c-GRQI4#gZdX;A`^60Z={Jw`co zcC%A(GWe>whiM=oaaVg#OszYNteeyd&2K#w-&&9gJ{$>C7ytf+0^|v<5Uq10HM@0WnBxc+JE??^MRJzT(yJI;2 z`e-@l;mbXf3-*}MN7bA!%h3%}K^N~M#jp2kG?BA?Cvassc{}u8sMgHGE^g={gW&Sk z1>$A%&Wza^^v?pTAeW*!*kO)^}<9 zCl-gEFLR~n|J2ALw3EB|duWEUFe>NVC+>VYZH-W`Zw+Y$dC3ph0`m8-9<-$g{w%gw z^i-GMlQ?_a>nI}g=Cl%G<`QTH%YYe^|sGbMG{5B!xk|vYyu{ zc9mu+1Es^>0;v{&n67PZQ;RMn^tCR8#<32}(~jd_Gj!w03X|(0j0u+#8wd4@=gyaf zv!k0e-H?G7;?`@y%ffpa&7(D6@JR5m1bg;4BAJ(mv;q&y-CE-0dRMdCeUzmChEV z-9oX4Z97WF?Xt?s%>SBI=;tDG1Z;nQ>)x+5{u1B%HL3N_HG+O^_?>`A zX0TvMFM{n|25_nfBzZ4PT+5{Ao!r+Ok7~mKmj-)xT!UidauS>+YA4_6-(=U$=qh8r zH`(t)KOk<9Ba?#9zQ7il7^OO5IiKfgd$3H?%6etRy9D+qRovHR^*UL6F*L9Y7*wGm z-yt=C7VVWPUM2gQF4)7a@1%fDz%j83t@ zHO=c<9Qpq00bRM9u}YQ;c%LKO%rs>XI1I&C1qRg+I+W3VxMo64y2)Im zcjJs(wSH|15xplLRX5t!#=4ZfE7{F55zmqxOJ+h}$?Gw|SjG=Pg0;_Gz?N+z-X1Jt zUl-XWcZQ_ALj`2qZ>ALn<&}%ef1-G&jqbud>xu?Dh^^&fIoVsd_>k@G9{RSyS;C77 z{XY5+KfAr2bMAe7|LN)Qrwgl>@`v`D-=rG}j^q4wmFJfTqXb8X`FA3$Dkfu?za*kG zP1t!mA|wYg1!KRy+tHSjkK~k8$ihQcoFD#S)oIIi7li$`V|<#tKD5Y18`qcM(A`?P zUC@7uu~^RQLq8tRzzolMW5CW{iYsWLAk^0em<7}Ajjnxy8{PRKcPxDX#H9|T<5{y&engO6fc?)7E*o7fU}Ac zFJzX|joxdzB6GwyMD zF-L>}MyhkD>Vvv031pfHism5`+i})u%PH|Q)8>+8StszS z@v4dYB>>pKGKyuJBC$n_rO3Vd?*!fm#_CC%hqx4>8lTVrxW8+oJXF&?TC~)pMJ@yn zz{cECX0bc2O@dqNa0sh#f)gT180y?&?~ZMa6nySX5mJgGltn?9ySdxjTS;d>;!^j@R%3^^-6vwuR6{F+xpocK*7vO}<6!El;Z9j!gL zUO557S0Hu~%jAYc)Vro^-mqG!J`&!or|SVrY$bNBZa_U}BE1K3alwnoDYc%MTPwP? zoiW{-T8?Y^L)OD0#5GA;u?zoSb=Mxx^xnrOQACJSndDN)J(reJoFkVuA)<20R5QnA zla5?+J?UZY#+XYunOu`jGaH?9632!TW3#!ID3>HLZ0O9^=~>Tnp7Wf)p4aQyKiluM z@9Xn-w#E6WiYt6_Jh*fIOHdQ$K&q!q4kC++}^k?J$AcdulEK=2GZW# zUG?6D91GT}xMJS5X)6kada$EBH|BQD!?mj%YXu&x&|S^Po4naEUPOuL+NqV|H#M7_ zr52QU#(2t?oYJJJzYHyBZ97N>HdVe^PyaZBRt-;Jiwrc;_3JM3m(FK$pSIQgzQ%L~+VugtIv zGs%q|w^^3Jl7rZ0ZS2g5!;AvfzJ_Do!_jEwwxyruPVwd8;laOrsxf+;V@~C$_fe;# zyB5|dbK^f>yv^bWS{3L`<(`b@!;XRVKbA&t4pTWAEoIz+(n|gzJg@Tj8I)WwzPg-# zhh>-ia_0y>%KSm3+r};%EA%T-YCUxQ4N8oaliXQKQRep}{WqE&II5g3S;x*oPga-N z+jX~_d_%Zk!5-_{lTlgv?RGpbEiZ5MKTOjESZX`iABp5zTrE=FoyB#N6q-D$qo(O( zrv;Xm=Z+>Lk)wPCJk{ztc|4}~<0pVM6f?43OXX~7Ygk4kEZ%y42i`s8Mj|;%XBX4J zGM@mb@rnfqq27CC7mEJwz7;kI+z78@tiCHq`VK3}@FpeD-U$TKnZoZK4vQ?!9$91u z;yysjx?f-evRAK3VS8sFJr*di*7i(#68Ko335J~x^6*UK4wxhrMLHy=TA6}V^(L)t z?bR5Io$1FRHI?`--4!6tY3;pB-q!WT zZktqt()SZ4^E+`v=+*6+I`cqaXrl;jTXI14IhX6PlNESmwO=oz&+O_UShR< zSDn{Q`H_%Lb$C=~^9DPYyqKbUrY39;6aS=R;dJJ^{Frr_7teheCEE9Nd)gv`HWPq} zdIdFi@n*r5n|;k?(7PYnQ4d=|>MJeUI*faXVx_m4nLsy8ILtEpwNDFiJY#lJ>X?(F z>a}qH>c#ZZAgU&Y8Ar)7D+bz5(+@l<~e?^zVQ_U*cprD+N( zga?$5OdzUTCjOiF{~Zsblt&a--OP_P=oGgFfma?|o*!FMgK>*oZxhX2Iw3NVobcIp zd}|5Bw>=ETcgDYQd}@%v_XXehf>lN0w-f(Ko{>C|Wa%w=!;nlVon33ukhU-~36L*> zBQ#=WRZL2ja?XwdHi><87xv}iz?WGl1AqbX#w6N7%%Q^0n_o_i+y1EZlG^Ysi?~a} zXYI@|E)}v>#%-gtH2(sVdxrlgvxPr}b?2Q7AipS=E%G(m*WnKHYq)S{!fP~#u00Eqz=_33EJ5X!HliU(jodTSZJmrDxx~XX$9uC_ma<$APn5V;96J zU6h=?7w?o<*u~e|%TofVRY>J|SN~8`&Q{y1msy^$1DePUI-3!HSF;U&3k5Hy-N$sn zTNPZPtCi`9M{Gl@^vM)e?Qku!bnje2C5LakgO(^Nc%rPS#^VcYzXjAyl4DFqOSR>! zFN?Y$CRXXJ`J6furPtVA(ta(DZj6>Wz;eyEH3aA7_;ncdCtmZ|TkVc=?#-qeD1(08 z7f3F|8to7c5F)+?d32tXye~SE!T4e0D~0G2-kmM60|3N|BzMUldlCL^9?;ynwT7@0 z_iLu2#O++)BDylhktAo*UE{H21ZD@N{Ot5S&2qwNjCYo;kt1?nw#zey>K?ft0$y`n zZasoF{S#OJMKqv4pImxqtm5viFl;Ck;f8dVTmArtR9kuVbN`GnNE0q&(m{J$kL7JM z)IFoIYi?kl>Y-VRGp4U+@X{zQE8>}nd)s*_bU}*S93-lGC+Y`@z0&M=07XSJ| z;>MNGt=&P<=WyE)a&&vwuB8_h=p?A-A%BGdgk4nG&(r>IiXuLC!sdRXx3Upqj*UTD zBX#@j@A@0BITZ&%qR41zKai?*((9c)Ipoe%IC5GOey1p^U3S0=giy>6&z;g90sw%W zv0rIEdX?Q=Fidc1oCI-t4T%OqZ}rcX+Gb3tHTTz3rLuI@2r6qh31iH($#4lFWwB#_ z+898-9SPR>wbe@F3P4y`SwNh*9*`mq%FNb5ErM;(@*m;hd!b%y0O88}CA8M6;~i&6 zL3(k%=1w&}9oll!H%=ue_=-jr?UT86lj(kO^Bp8JFCRmwc7+TLC<|+LTo^>Pxts?8 zS_F?QGdzz@NO@(YE~3dPAy_OuE^T!rPWtSo5nVe6AXF8syPFssyERoIhbd?6hI89= zCMeX>I3ItMBQiJfD83Fz&>DR>K_4#mtn6imShtbWs9IVO9Z+JMs?(09-mLKv7XYIqa}!<1A$S38EWuv;b>&FWJqly4~#?d&r)QI`Cyj ztmNPdgph9(Xyl7i%ds~7N%TrVT%!TTIXllj-N8j2Et*ql4>k$PPYXOUH)Om&>u#<5 zc;4!(QNYu1L8e@#!FE612u&rRRGq7T1aN{y3Or~5t n{Sh6k-n@gMq7J(-BW&{Qi7_d+UCWL;^}vgZ|Nr0>^NI00D}+Vn diff --git a/docs/_static/charts/basic.webp b/docs/_static/charts/basic.webp index 7401abf1b7e4c11b8535956ca2d4a3066bd71b05..9581732e886878351a5e54a646c42fa05429e50f 100644 GIT binary patch literal 50946 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%0DyJnl9x) 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 LNc^+u|9|a2sg6)J diff --git a/docs/_static/charts/basic_hist.html b/docs/_static/charts/basic_hist.html new file mode 100644 index 00000000..2518bda9 --- /dev/null +++ b/docs/_static/charts/basic_hist.html @@ -0,0 +1 @@ +

diff --git a/docs/_static/charts/basic_hist.jpeg b/docs/_static/charts/basic_hist.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..9823042712dad77a7fd2ad07e4e3ef26d53bb8ba GIT binary patch literal 39816 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 literal 0 HcmV?d00001 diff --git a/docs/_static/charts/basic_hist.webp b/docs/_static/charts/basic_hist.webp new file mode 100644 index 0000000000000000000000000000000000000000..c90de7c87a4254bc7fd8aafd5943bbdf9fd3a727 GIT binary patch literal 36328 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
+
diff --git a/docs/_static/charts/lincoln_weather_red_blue.html b/docs/_static/charts/lincoln_weather_red_blue.html index 8949d609..1db6aee5 100644 --- a/docs/_static/charts/lincoln_weather_red_blue.html +++ b/docs/_static/charts/lincoln_weather_red_blue.html @@ -1 +1 @@ -
+
diff --git a/docs/_static/charts/probly.html b/docs/_static/charts/probly.html index 0381eed4..5aa18a96 100644 --- a/docs/_static/charts/probly.html +++ b/docs/_static/charts/probly.html @@ -1 +1 @@ -
+
diff --git a/docs/conf.py b/docs/conf.py index e89c607c..f78817d4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -205,9 +205,9 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "packaging": ("https://packaging.pypa.io/en/latest", None), - "numpy": ("https://docs.scipy.org/doc/numpy/", None), + "numpy": ("https://numpy.org/doc/stable/", None), "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), - "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), "statsmodels": ("https://www.statsmodels.org/stable/", None), "plotly": ("https://plotly.com/python-api-reference/", None), } diff --git a/docs/getting_started/getting_started.md b/docs/getting_started/getting_started.md index 899b9479..2accca34 100644 --- a/docs/getting_started/getting_started.md +++ b/docs/getting_started/getting_started.md @@ -10,7 +10,7 @@ This basic example shows how you can quickly get started with a simple call to t import numpy as np from ridgeplot import ridgeplot -my_samples = [np.random.normal(n / 1.2, size=600) for n in range(7, 0, -1)] +my_samples = [np.random.normal(n / 1.2, size=600) for n in range(6, 0, -1)] fig = ridgeplot(samples=my_samples) fig.show() ``` @@ -19,6 +19,18 @@ fig.show() :file: ../_static/charts/basic.html ``` +By default, the `ridgeplot` function will estimate the samples' probability density functions (PDFs) using kernel density estimation (KDE) and plot them as ridgeline area traces (`trace_type="area"`). If you want to plot histograms instead, you can set the `nbins` parameter and change the `trace_type` to `"bar"`. + +```python +fig = ridgeplot(samples=my_samples, nbins=20, trace_type="bar") +fig.show() +``` + +```{raw} html +:file: ../_static/charts/basic_hist.html +``` + + ## Flexible configuration In this example, we will try to replicate the first ridgeline plot in this [_from Data to Viz_ post](https://www.data-to-viz.com/graph/ridgeline.html). The example in the post was created using the _"Perception of Probability Words"_ dataset (see {py:func}`~ridgeplot.datasets.load_probly()`) and the popular [ggridges](https://wilkelab.org/ggridges/) R package. In the end, we will see how the `ridgeplot` Python library can be used to create a (nearly) identical plot, thanks to its extensive configuration options. diff --git a/docs/index.md b/docs/index.md index 13857220..6a4bb3ea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,7 +47,7 @@ For those in a hurry, here's a very basic example on how to quickly get started import numpy as np from ridgeplot import ridgeplot -my_samples = [np.random.normal(n / 1.2, size=600) for n in range(7, 0, -1)] +my_samples = [np.random.normal(n / 1.2, size=600) for n in range(6, 0, -1)] fig = ridgeplot(samples=my_samples) fig.show() ``` From 92ecc23041cac28a530495ee1c5bce29a2e68264 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Sat, 30 Nov 2024 18:18:04 +0000 Subject: [PATCH 03/21] Move trace objects to traces module and rename `y_shifted` to `y_base` --- src/ridgeplot/_figure_factory.py | 10 +++---- src/ridgeplot/_obj/__init__.py | 26 ------------------- src/ridgeplot/_obj/traces/__init__.py | 26 +++++++++++++++++++ .../_obj/{_area.py => traces/area.py} | 10 +++---- src/ridgeplot/_obj/{_bar.py => traces/bar.py} | 4 +-- .../_obj/{_base.py => traces/base.py} | 4 +-- 6 files changed, 40 insertions(+), 40 deletions(-) create mode 100644 src/ridgeplot/_obj/traces/__init__.py rename src/ridgeplot/_obj/{_area.py => traces/area.py} (92%) rename src/ridgeplot/_obj/{_bar.py => traces/bar.py} (93%) rename src/ridgeplot/_obj/{_base.py => traces/base.py} (97%) diff --git a/src/ridgeplot/_figure_factory.py b/src/ridgeplot/_figure_factory.py index 5adba2da..f5fd1436 100644 --- a/src/ridgeplot/_figure_factory.py +++ b/src/ridgeplot/_figure_factory.py @@ -10,8 +10,8 @@ SolidColormode, compute_solid_colors, ) -from ridgeplot._obj import get_trace_cls -from ridgeplot._obj._base import ColoringContext +from ridgeplot._obj.traces import get_trace_cls +from ridgeplot._obj.traces.base import ColoringContext from ridgeplot._types import ( Color, ColorScale, @@ -169,8 +169,8 @@ def create_ridgeplot( for ith_row, (row_traces, row_trace_types, row_labels, row_colors) in enumerate( zip_strict(densities, trace_types, trace_labels, solid_colors) ): - y_shifted = float(-ith_row * y_max * spacing) - tickvals.append(y_shifted) + y_base = float(-ith_row * y_max * spacing) + tickvals.append(y_base) for trace, trace_type, label, color in zip_strict( row_traces, row_trace_types, row_labels, row_colors ): @@ -179,7 +179,7 @@ def create_ridgeplot( label=label, solid_color=color, zorder=ith_trace, - y_shifted=y_shifted, + y_base=y_base, line_color=line_color, line_width=line_width, ) diff --git a/src/ridgeplot/_obj/__init__.py b/src/ridgeplot/_obj/__init__.py index 2e6f26f0..e69de29b 100644 --- a/src/ridgeplot/_obj/__init__.py +++ b/src/ridgeplot/_obj/__init__.py @@ -1,26 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from ridgeplot._obj._area import AreaTrace -from ridgeplot._obj._bar import BarTrace - -if TYPE_CHECKING: - from ridgeplot._obj._base import RidgeplotTrace - from ridgeplot._types import TraceType - - -TRACE_TYPES: dict[TraceType, type[RidgeplotTrace]] = { - "area": AreaTrace, - "bar": BarTrace, -} -"""Mapping of trace types to trace classes.""" - - -def get_trace_cls(trace_type: TraceType) -> type[RidgeplotTrace]: - """Get a trace class by its type.""" - try: - return TRACE_TYPES[trace_type] - except KeyError as err: - types = ", ".join(repr(t) for t in TRACE_TYPES) - raise ValueError(f"Unknown trace type {trace_type!r}. Available types: {types}.") from err diff --git a/src/ridgeplot/_obj/traces/__init__.py b/src/ridgeplot/_obj/traces/__init__.py new file mode 100644 index 00000000..301583f6 --- /dev/null +++ b/src/ridgeplot/_obj/traces/__init__.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ridgeplot._obj.traces.area import AreaTrace +from ridgeplot._obj.traces.bar import BarTrace + +if TYPE_CHECKING: + from ridgeplot._obj.traces.base import RidgeplotTrace + from ridgeplot._types import TraceType + + +TRACE_TYPES: dict[TraceType, type[RidgeplotTrace]] = { + "area": AreaTrace, + "bar": BarTrace, +} +"""Mapping of trace types to trace classes.""" + + +def get_trace_cls(trace_type: TraceType) -> type[RidgeplotTrace]: + """Get a trace class by its type.""" + try: + return TRACE_TYPES[trace_type] + except KeyError as err: + types = ", ".join(repr(t) for t in TRACE_TYPES) + raise ValueError(f"Unknown trace type {trace_type!r}. Available types: {types}.") from err diff --git a/src/ridgeplot/_obj/_area.py b/src/ridgeplot/_obj/traces/area.py similarity index 92% rename from src/ridgeplot/_obj/_area.py rename to src/ridgeplot/_obj/traces/area.py index a5ef685f..366fa563 100644 --- a/src/ridgeplot/_obj/_area.py +++ b/src/ridgeplot/_obj/traces/area.py @@ -6,7 +6,7 @@ from ridgeplot._color.interpolation import slice_colorscale from ridgeplot._color.utils import apply_alpha -from ridgeplot._obj._base import DEFAULT_HOVERTEMPLATE, ColoringContext, RidgeplotTrace +from ridgeplot._obj.traces.base import DEFAULT_HOVERTEMPLATE, ColoringContext, RidgeplotTrace from ridgeplot._utils import normalise_min_max @@ -50,12 +50,12 @@ def _get_coloring_kwargs(self, ctx: ColoringContext) -> dict[str, Any]: return color_kwargs def draw(self, fig: go.Figure, coloring_ctx: ColoringContext) -> go.Figure: - # Draw an invisible trace at constance y=y_shifted so that we - # can set fill="tonexty" below and get a filled area plot. + # Draw an invisible trace at constance y=y_base so that we + # can set fill="tonexty" below and get a filled area plot fig.add_trace( go.Scatter( x=self.x, - y=[self.y_shifted] * len(self.x), + y=[self.y_base] * len(self.x), # make trace 'invisible' # Note: visible=False does not work with fill="tonexty" line=dict(color="rgba(0,0,0,0)", width=0), @@ -69,7 +69,7 @@ def draw(self, fig: go.Figure, coloring_ctx: ColoringContext) -> go.Figure: fig.add_trace( go.Scatter( x=self.x, - y=[y_i + self.y_shifted for y_i in self.y], + y=[y_i + self.y_base for y_i in self.y], name=self.label, fill="tonexty", mode="lines", diff --git a/src/ridgeplot/_obj/_bar.py b/src/ridgeplot/_obj/traces/bar.py similarity index 93% rename from src/ridgeplot/_obj/_bar.py rename to src/ridgeplot/_obj/traces/bar.py index 78c50d06..53c506b8 100644 --- a/src/ridgeplot/_obj/_bar.py +++ b/src/ridgeplot/_obj/traces/bar.py @@ -5,7 +5,7 @@ from plotly import graph_objects as go from ridgeplot._color.interpolation import interpolate_color -from ridgeplot._obj._base import DEFAULT_HOVERTEMPLATE, ColoringContext, RidgeplotTrace +from ridgeplot._obj.traces.base import DEFAULT_HOVERTEMPLATE, ColoringContext, RidgeplotTrace from ridgeplot._utils import normalise_min_max @@ -39,7 +39,7 @@ def draw(self, fig: go.Figure, coloring_ctx: ColoringContext) -> go.Figure: x=self.x, y=self.y, name=self.label, - base=self.y_shifted, + base=self.y_base, marker_line_width=self.line_width if self.line_width is not None else 0.5, # width=1, # TODO: do we need to specify the bar width? **self._get_coloring_kwargs(ctx=coloring_ctx), diff --git a/src/ridgeplot/_obj/_base.py b/src/ridgeplot/_obj/traces/base.py similarity index 97% rename from src/ridgeplot/_obj/_base.py rename to src/ridgeplot/_obj/traces/base.py index bda55cbf..0121d3b5 100644 --- a/src/ridgeplot/_obj/_base.py +++ b/src/ridgeplot/_obj/traces/base.py @@ -57,7 +57,7 @@ def __init__( solid_color: str, zorder: int, # Constant over the trace's row - y_shifted: float, + y_base: float, # Constant over the entire plot line_color: Color | Literal["fill-color"], line_width: float | None, @@ -66,7 +66,7 @@ def __init__( self.label = label self.solid_color = solid_color self.zorder = zorder - self.y_shifted = y_shifted + self.y_base = y_base self.line_color: Color = self.solid_color if line_color == "fill-color" else line_color self.line_width: float = line_width if line_width is not None else self._DEFAULT_LINE_WIDTH From e4262f594050cd0cdc7036d03d3653ab735636c7 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Sat, 30 Nov 2024 18:21:17 +0000 Subject: [PATCH 04/21] Rename `validate_coerce_colorscale` --- src/ridgeplot/_color/colorscale.py | 4 ++-- src/ridgeplot/_figure_factory.py | 4 ++-- tests/unit/color/test_colorscale.py | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ridgeplot/_color/colorscale.py b/src/ridgeplot/_color/colorscale.py index 9d9fa68a..0b085a9a 100644 --- a/src/ridgeplot/_color/colorscale.py +++ b/src/ridgeplot/_color/colorscale.py @@ -37,12 +37,12 @@ def validate_coerce(self, v: Any) -> ColorScale: def infer_default_colorscale() -> ColorScale | Collection[Color] | str: - return validate_and_coerce_colorscale( + return validate_coerce_colorscale( default_plotly_template().layout.colorscale.sequential or px.colors.sequential.Viridis ) -def validate_and_coerce_colorscale( +def validate_coerce_colorscale( colorscale: ColorScale | Collection[Color] | str | None, ) -> ColorScale: """Convert mixed colorscale representations to the canonical diff --git a/src/ridgeplot/_figure_factory.py b/src/ridgeplot/_figure_factory.py index f5fd1436..c010d936 100644 --- a/src/ridgeplot/_figure_factory.py +++ b/src/ridgeplot/_figure_factory.py @@ -4,7 +4,7 @@ from plotly import graph_objects as go -from ridgeplot._color.colorscale import validate_and_coerce_colorscale +from ridgeplot._color.colorscale import validate_coerce_colorscale from ridgeplot._color.interpolation import ( InterpolationContext, SolidColormode, @@ -143,7 +143,7 @@ def create_ridgeplot( spacing = float(spacing) show_yticklabels = bool(show_yticklabels) xpad = float(xpad) - colorscale = validate_and_coerce_colorscale(colorscale) + colorscale = validate_coerce_colorscale(colorscale) # ============================================================== # --- Build the figure diff --git a/tests/unit/color/test_colorscale.py b/tests/unit/color/test_colorscale.py index ae5883eb..d7120fb5 100644 --- a/tests/unit/color/test_colorscale.py +++ b/tests/unit/color/test_colorscale.py @@ -8,7 +8,7 @@ from ridgeplot._color.colorscale import ( infer_default_colorscale, list_all_colorscale_names, - validate_and_coerce_colorscale, + validate_coerce_colorscale, ) if TYPE_CHECKING: @@ -23,32 +23,32 @@ def test_infer_default_colorscale() -> None: - assert infer_default_colorscale() == validate_and_coerce_colorscale("plasma") + assert infer_default_colorscale() == validate_coerce_colorscale("plasma") # ============================================================== -# --- validate_and_coerce_colorscale() +# --- validate_coerce_colorscale() # ============================================================== -def test_validate_and_coerce_colorscale( +def test_validate_coerce_colorscale( valid_colorscale: tuple[ColorScale | Collection[Color] | str, ColorScale] ) -> None: colorscale, expected = valid_colorscale - coerced = validate_and_coerce_colorscale(colorscale=colorscale) + coerced = validate_coerce_colorscale(colorscale=colorscale) values, colors = zip(*coerced) values_expected, colors_expected = zip(*expected) assert values == pytest.approx(values_expected) assert colors == colors_expected -def test_validate_and_coerce_colorscale_fails( +def test_validate_coerce_colorscale_fails( invalid_colorscale: ColorScale | Collection[Color] | str, ) -> None: with pytest.raises( ValueError, match=r"Invalid value .* received for the 'colorscale' property" ): - validate_and_coerce_colorscale(invalid_colorscale) + validate_coerce_colorscale(invalid_colorscale) # ============================================================== @@ -65,4 +65,4 @@ def test_list_all_colorscale_names() -> None: assert "viridis" in all_colorscale_names assert "default" in all_colorscale_names for name in all_colorscale_names: - validate_and_coerce_colorscale(name) + validate_coerce_colorscale(name) From 36122c66c6f77025ab7fc6092fbc0e798d65ccd2 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Sun, 1 Dec 2024 18:51:28 +0000 Subject: [PATCH 05/21] Improve test coverage --- tests/unit/test_ridgeplot.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/unit/test_ridgeplot.py b/tests/unit/test_ridgeplot.py index aa89c5d3..ca39c479 100644 --- a/tests/unit/test_ridgeplot.py +++ b/tests/unit/test_ridgeplot.py @@ -46,6 +46,11 @@ def test_shallow_samples() -> None: ) # fmt: skip +# ============================================================== +# --- param: labels +# ============================================================== + + def test_shallow_labels() -> None: shallow_labels = ["trace 1", "trace 2"] assert ( @@ -61,6 +66,26 @@ def test_y_labels_dedup() -> None: ) # fmt: skip +# ============================================================== +# --- param: trace_type +# ============================================================== + + +def test_shallow_trace_type() -> None: + assert ( + ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type="bar") == + ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type=["bar", "bar"]) == # type: ignore[arg-type] + ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type=[["bar"], ["bar"]]) # type: ignore[arg-type] + ) # fmt: skip + + +def test_unknown_trace_type() -> None: + with pytest.raises( + ValueError, match="Unknown trace type 'foo'. Available types: 'area', 'bar'." + ): + ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type="foo") # type: ignore[arg-type] + + # ============================================================== # --- param: colorscale # ============================================================== From a4c46287a60d43e50cd0d2f57ffb00cc3947914c Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Sun, 1 Dec 2024 18:51:40 +0000 Subject: [PATCH 06/21] Improve type annotations --- src/ridgeplot/_color/colorscale.py | 1 + src/ridgeplot/_kde.py | 12 ++++++++---- tests/unit/test_kde.py | 3 ++- tests/unit/test_missing.py | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ridgeplot/_color/colorscale.py b/src/ridgeplot/_color/colorscale.py index 0b085a9a..777119bd 100644 --- a/src/ridgeplot/_color/colorscale.py +++ b/src/ridgeplot/_color/colorscale.py @@ -29,6 +29,7 @@ def validate_coerce(self, v: Any) -> ColorScale: coerced = super().validate_coerce(v) if coerced is None: # pragma: no cover self.raise_invalid_val(coerced) + coerced = cast(ColorScale, coerced) # This helps us avoid floating point errors when making # comparisons in our test suite. The user should not # be able to notice *any* difference in the output diff --git a/src/ridgeplot/_kde.py b/src/ridgeplot/_kde.py index f48adbd3..c873bbb6 100644 --- a/src/ridgeplot/_kde.py +++ b/src/ridgeplot/_kde.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Callable, Union, cast import numpy as np +import numpy.typing as npt import statsmodels.api as sm from statsmodels.sandbox.nonparametric.kernels import CustomKernel as StatsmodelsKernel @@ -28,7 +29,6 @@ from ridgeplot._vendor.more_itertools import zip_strict if TYPE_CHECKING: - import numpy.typing as npt from ridgeplot._types import Densities, Samples, SamplesTrace, XYCoordinate @@ -165,14 +165,16 @@ def estimate_density_trace( weights=weights, ) density_y = dens.evaluate(density_x) - _validate_densities(x=density_x, y=density_y, kernel=kernel) + density_y = _validate_densities(x=density_x, y=density_y, kernel=kernel) return list(zip(density_x, density_y)) def _validate_densities( - x: npt.NDArray[np.floating[Any]], y: npt.NDArray[np.floating[Any]], kernel: str -) -> None: + x: npt.NDArray[np.floating[Any]], + y: Any, + kernel: str, +) -> npt.NDArray[np.floating[Any]]: # I haven't investigated the root of this issue yet # but statsmodels' KDEUnivariate implementation # can return a float('NaN') if something goes @@ -190,10 +192,12 @@ def _validate_densities( # Fail early if the return type is incorrect # Otherwise, the remaining checks will fail raise RuntimeError(msg) # noqa: TRY004 + y = cast(npt.NDArray[np.floating[Any]], y) wrong_shape = y.shape != x.shape not_finite = ~np.isfinite(y).all() if wrong_shape or not_finite: raise RuntimeError(msg) + return y def estimate_densities( diff --git a/tests/unit/test_kde.py b/tests/unit/test_kde.py index 4860ce9d..749a5354 100644 --- a/tests/unit/test_kde.py +++ b/tests/unit/test_kde.py @@ -126,7 +126,8 @@ def test__validate_densities() -> None: inputs.""" x = np.array([0, 1, 2, 3, 4, 5, 6]) y = np.array([0.1, 0.2, 0.3, 0.4, 0.3, 0.2, 0.1]) - _validate_densities(x=x, y=y, kernel="doesn't matter") + y_valid = _validate_densities(x=x, y=y, kernel="doesn't matter") + np.testing.assert_array_equal(y_valid, y) @pytest.mark.parametrize( diff --git a/tests/unit/test_missing.py b/tests/unit/test_missing.py index 87966d28..1b5ccbc7 100644 --- a/tests/unit/test_missing.py +++ b/tests/unit/test_missing.py @@ -60,7 +60,7 @@ def test_reloading() -> None: ) reload(types_module) - assert_all_are( + assert_all_are( # pragma: no cover missing1, missing2, missing3, From e701e1303a4d09454b023226c6e4a010d29f5780 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Mon, 2 Dec 2024 13:53:10 +0000 Subject: [PATCH 07/21] Improve type checks and switch from mypy to pyright --- .github/workflows/ci.yml | 4 +-- MANIFEST.in | 1 + cicd_utils/README.md | 1 + docs/development/contributing.md | 8 ++--- docs/reference/changelog.md | 1 + pyrightconfig.json | 16 +++++++++ requirements/local-dev.txt | 2 +- requirements/{mypy.txt => typing.txt} | 6 ++-- src/ridgeplot/_color/colorscale.py | 2 +- src/ridgeplot/_color/utils.py | 15 ++++---- src/ridgeplot/_figure_factory.py | 16 ++++++--- src/ridgeplot/_hist.py | 5 ++- src/ridgeplot/_kde.py | 11 +++--- src/ridgeplot/_obj/traces/base.py | 1 + src/ridgeplot/_types.py | 50 +++++++++++++++++++++++++++ src/ridgeplot/datasets/__init__.py | 10 +++--- tests/unit/conftest.py | 12 +++---- tests/unit/test_init.py | 5 +-- tests/unit/test_missing.py | 8 ++--- tests/unit/test_ridgeplot.py | 8 ++--- tox.ini | 16 ++++----- 21 files changed, 133 insertions(+), 65 deletions(-) create mode 100644 pyrightconfig.json rename requirements/{mypy.txt => typing.txt} (70%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e723e92f..d9c30e19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,9 +37,9 @@ jobs: # env: # SKIP: "no-commit-to-branch" # run: tox -e pre-commit-all - - name: Run type checks with mypy + - name: Run type checks shell: bash - run: tox -e mypy-safe + run: tox -e typing # Run test suits for all supported platforms and Python versions software-tests: diff --git a/MANIFEST.in b/MANIFEST.in index ca38a4f4..b115ba8b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,7 @@ include *.md include *.toml include *.yml include *.yaml +include *.json # Stubs recursive-include src py.typed *.pyi diff --git a/cicd_utils/README.md b/cicd_utils/README.md index 7f82cb51..2c2b5481 100644 --- a/cicd_utils/README.md +++ b/cicd_utils/README.md @@ -16,6 +16,7 @@ For this reason, the `cicd_utils` directory needs to be made explicitly discover Static analysis tools will also we need to be made aware of this package: - For mypy, we can add it to the `files` option in `mypy.ini` to help with import discovery. +- For pyright, we can add it to the `extraPaths` option in `pyrightconfig.json`. - For ruff's _isort_-implementation, we also added it to the `known-first-party` list (see `ruff.toml`) ### The `cicd/scripts` directory diff --git a/docs/development/contributing.md b/docs/development/contributing.md index b9c0e76f..dd5e1287 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -158,13 +158,13 @@ pre-commit run --all-files For more information on all the checks being run here, take a look inside the {repo-file}`.pre-commit-config.yaml` configuration file. -The only static check that is not run by pre-commit is [mypy](https://github.com/python/mypy), which is too expensive to run on every commit. To run mypy against all files, run: +The only static check that is not run by pre-commit is [pyright](https://github.com/microsoft/pyright), which is too expensive to run on every commit. To run pyright against all files, run: ```shell -tox -e mypy-incremental +tox -e typing ``` -Just like with pytest, you can also pass extra positional arguments to mypy by running `tox -e mypy-incremental -- `. +Just like with pytest, you can also pass extra positional arguments to pyright by running `tox -e typing -- `. To trigger all static checks, run: @@ -194,7 +194,7 @@ Here is a quick overview of ~~all~~ most of the CI tools and software used in th | [Coverage.py](https://github.com/nedbat/coveragepy) | 📊 Coverage | {repo-file}`.coveragerc` | The code coverage tool for Python | | [Codecov](https://about.codecov.io/) | 📊 Coverage | {repo-file}`.github/workflows/ci.yml` | An external services for tracking, monitoring, and alerting on code coverage metrics. | | [pre-commit](https://pre-commit.com/) | 💅 Linting | {repo-file}`.pre-commit-config.yaml` | Used to to automatically check and fix any formatting rules on every commit. | -| [mypy](https://github.com/python/mypy) | 💅 Linting | {repo-file}`mypy.ini` | A static type checker for Python. We use quite a strict configuration here, which can be tricky at times. Feel free to ask for help from the community by commenting on your issue or pull request. | +| [pyright](https://github.com/microsoft/pyright) | 💅 Linting | {repo-file}`pyrightconfig.json` | A static type checker for Python. We use quite a strict configuration here, which can be tricky at times. Feel free to ask for help from the community by commenting on your issue or pull request. | | [black](https://github.com/psf/black) | 💅 Linting | {repo-file}`pyproject.toml` | "The uncompromising Python code formatter". We use `black` to automatically format Python code in a deterministic manner. Maybe we'll replace this with `ruff` in the future. | | [ruff](https://github.com/astral-sh/ruff) | 💅 Linting | {repo-file}`ruff.toml` | "An extremely fast Python linter and code formatter, written in Rust." For this project, ruff replaced Flake8 (+plugins), isort, pydocstyle, pyupgrade, and autoflake with a single (and faster) tool. | | [EditorConfig](https://editorconfig.org/) | 💅 Linting | {repo-file}`.editorconfig` | This repository uses the `.editorconfig` standard configuration file, which aims to ensure consistent style across multiple programming environments. | diff --git a/docs/reference/changelog.md b/docs/reference/changelog.md index bc9a812b..ba56e41b 100644 --- a/docs/reference/changelog.md +++ b/docs/reference/changelog.md @@ -12,6 +12,7 @@ Unreleased changes ### Internal - Small improvements to type hints and annotations ({gh-pr}`284`) +- Improve type annotations and switch from mypy to pyright ({gh-pr}`287`) --- diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 00000000..08e67a97 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,16 @@ +{ + "include": [ + "src", + "tests", + "docs", + "cicd_utils" + ], + "exclude": [ + "docs/build", + "**/__pycache__" + ], + "extraPaths": [ + "cicd_utils" + ], + "typeCheckingMode": "basic" +} diff --git a/requirements/local-dev.txt b/requirements/local-dev.txt index 5b355dd6..5f7bce80 100644 --- a/requirements/local-dev.txt +++ b/requirements/local-dev.txt @@ -23,5 +23,5 @@ ptpython # And everything else... -r cicd_utils.txt -r docs.txt --r mypy.txt +-r typing.txt -r tests.txt diff --git a/requirements/mypy.txt b/requirements/typing.txt similarity index 70% rename from requirements/mypy.txt rename to requirements/typing.txt index d3c7eb4e..e07fd046 100644 --- a/requirements/mypy.txt +++ b/requirements/typing.txt @@ -1,5 +1,5 @@ -# mypy dependencies -mypy +# pyright +pyright # Third-party stubs types-python-dateutil @@ -9,7 +9,7 @@ types-requests types-tqdm pandas-stubs -# mypy also needs to inherit other environment dependencies in +# pyright also needs to inherit other environment dependencies in # order to correctly infer types for code in tests, docs, etc. -r cicd_utils.txt -r docs.txt diff --git a/src/ridgeplot/_color/colorscale.py b/src/ridgeplot/_color/colorscale.py index 777119bd..d63b4ce2 100644 --- a/src/ridgeplot/_color/colorscale.py +++ b/src/ridgeplot/_color/colorscale.py @@ -13,7 +13,7 @@ from collections.abc import Collection -class ColorscaleValidator(_ColorscaleValidator): # type: ignore[misc] +class ColorscaleValidator(_ColorscaleValidator): def __init__(self) -> None: super().__init__("colorscale", "ridgeplot") diff --git a/src/ridgeplot/_color/utils.py b/src/ridgeplot/_color/utils.py index 5dccb56b..56486612 100644 --- a/src/ridgeplot/_color/utils.py +++ b/src/ridgeplot/_color/utils.py @@ -1,17 +1,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast +from collections.abc import Collection +from typing import Union, cast import plotly.express as px import plotly.graph_objects as go import plotly.io as pio from ridgeplot._color.css_colors import CSS_NAMED_COLORS, CssNamedColor - -if TYPE_CHECKING: - from collections.abc import Collection - - from ridgeplot._types import Color +from ridgeplot._types import Color def default_plotly_template() -> go.layout.Template: @@ -21,7 +18,9 @@ def default_plotly_template() -> go.layout.Template: # TODO: Move this in the future to a separate module # once we add support for color sequences. def infer_default_color_sequence() -> Collection[Color]: # pragma: no cover - return default_plotly_template().layout.colorway or px.colors.qualitative.D3 # type: ignore[no-any-return] + return cast( + Collection[Color], default_plotly_template().layout.colorway or px.colors.qualitative.D3 + ) def to_rgb(color: Color) -> str: @@ -50,7 +49,7 @@ def unpack_rgb(rgb: str) -> tuple[float, float, float, float] | tuple[float, flo prefix = rgb.split("(")[0] + "(" values_str = map(str.strip, rgb.removeprefix(prefix).removesuffix(")").split(",")) values_num = tuple(int(v) if v.isdecimal() else float(v) for v in values_str) - return values_num # type: ignore[return-value] + return cast(Union[tuple[float, float, float, float], tuple[float, float, float]], values_num) def apply_alpha(color: Color, alpha: float) -> str: diff --git a/src/ridgeplot/_figure_factory.py b/src/ridgeplot/_figure_factory.py index c010d936..532ccdfc 100644 --- a/src/ridgeplot/_figure_factory.py +++ b/src/ridgeplot/_figure_factory.py @@ -21,6 +21,9 @@ TraceType, TraceTypesArray, is_flat_str_collection, + is_shallow_trace_types_array, + is_trace_type, + is_trace_types_array, nest_shallow_collection, ) from ridgeplot._utils import ( @@ -41,12 +44,15 @@ def normalise_trace_types( densities: Densities, trace_types: TraceTypesArray | ShallowTraceTypesArray | TraceType, ) -> TraceTypesArray: - if isinstance(trace_types, str): - trace_types = [[trace_types] * len(row) for row in densities] - else: - if is_flat_str_collection(trace_types): - trace_types = nest_shallow_collection(cast(ShallowTraceTypesArray, trace_types)) + if is_trace_type(trace_types): + trace_types = cast(TraceTypesArray, [[trace_types] * len(row) for row in densities]) + elif is_shallow_trace_types_array(trace_types): + trace_types = nest_shallow_collection(trace_types) + trace_types = normalise_row_attrs(trace_types, l2_target=densities) + elif is_trace_types_array(trace_types): trace_types = normalise_row_attrs(trace_types, l2_target=densities) + else: + raise TypeError(f"Invalid trace_type: {trace_types}") return trace_types diff --git a/src/ridgeplot/_hist.py b/src/ridgeplot/_hist.py index ac919ddd..7cbd78fe 100644 --- a/src/ridgeplot/_hist.py +++ b/src/ridgeplot/_hist.py @@ -11,13 +11,12 @@ from ridgeplot._types import ( Densities, - Float, + DensityTrace, Samples, SamplesTrace, SampleWeights, SampleWeightsArray, ShallowSampleWeightsArray, - XYCoordinate, ) @@ -25,7 +24,7 @@ def bin_trace_samples( trace_samples: SamplesTrace, nbins: int, weights: SampleWeights, -) -> list[XYCoordinate[Float]]: +) -> DensityTrace: hist, bins = np.histogram( np.asarray(trace_samples, dtype=float), bins=nbins, diff --git a/src/ridgeplot/_kde.py b/src/ridgeplot/_kde.py index c873bbb6..b0fa7355 100644 --- a/src/ridgeplot/_kde.py +++ b/src/ridgeplot/_kde.py @@ -17,7 +17,7 @@ from ridgeplot._types import ( CollectionL1, - Float, + DensityTrace, Numeric, SampleWeights, SampleWeightsArray, @@ -30,7 +30,7 @@ if TYPE_CHECKING: - from ridgeplot._types import Densities, Samples, SamplesTrace, XYCoordinate + from ridgeplot._types import Densities, Samples, SamplesTrace KDEPoints = Union[int, CollectionL1[Numeric]] @@ -97,8 +97,7 @@ def normalize_sample_weights( """ if _is_sample_weights(sample_weights): return [[sample_weights] * len(row) for row in samples] - # TODO: Investigate this issue with mypy's type narrowing... - sample_weights = cast( # type: ignore[unreachable] + sample_weights = cast( Union[SampleWeightsArray, ShallowSampleWeightsArray], sample_weights, ) @@ -114,7 +113,7 @@ def estimate_density_trace( kernel: str, bandwidth: KDEBandwidth, weights: SampleWeights = None, -) -> list[XYCoordinate[Float]]: +) -> DensityTrace: """Estimates a density trace from a set of samples. For a given set of sample values, computes the kernel densities (KDE) at @@ -161,7 +160,7 @@ def estimate_density_trace( dens.fit( kernel=kernel, fft=kernel == "gau" and weights is None, - bw=bandwidth, + bw=bandwidth, # type: ignore[arg-type] weights=weights, ) density_y = dens.evaluate(density_x) diff --git a/src/ridgeplot/_obj/traces/base.py b/src/ridgeplot/_obj/traces/base.py index 0121d3b5..b75e26ab 100644 --- a/src/ridgeplot/_obj/traces/base.py +++ b/src/ridgeplot/_obj/traces/base.py @@ -52,6 +52,7 @@ class RidgeplotTrace(ABC): def __init__( self, + *, # kw only trace: DensityTrace, label: str, solid_color: str, diff --git a/src/ridgeplot/_types.py b/src/ridgeplot/_types.py index b4173d7e..e4758b61 100644 --- a/src/ridgeplot/_types.py +++ b/src/ridgeplot/_types.py @@ -532,6 +532,56 @@ def is_shallow_samples(obj: Any) -> TypeIs[ShallowSamples]: >>> trace_types_array: ShallowTraceTypesArray = ["area", "bar", "area"] """ + +def is_trace_type(obj: Any) -> TypeIs[TraceType]: + """Type guard for :data:`TraceType`. + + Examples + -------- + >>> is_trace_type("area") + True + >>> is_trace_type("bar") + True + >>> is_trace_type("foo") + False + >>> is_trace_type(42) + False + """ + from typing import get_args + + return isinstance(obj, str) and obj in get_args(TraceType) + + +def is_shallow_trace_types_array(obj: Any) -> TypeIs[ShallowTraceTypesArray]: + """Type guard for :data:`ShallowTraceTypesArray`. + + Examples + -------- + >>> is_shallow_trace_types_array(["area", "bar", "area"]) + True + >>> is_shallow_trace_types_array(["area", "bar", "foo"]) + False + >>> is_shallow_trace_types_array([1, 2, 3]) + False + """ + return isinstance(obj, Collection) and all(map(is_trace_type, obj)) + + +def is_trace_types_array(obj: Any) -> TypeIs[TraceTypesArray]: + """Type guard for :data:`TraceTypesArray`. + + Examples + -------- + >>> is_trace_types_array([["area", "bar"], ["area", "bar"]]) + True + >>> is_trace_types_array([["area", "bar"], ["area", "foo"]]) + False + >>> is_trace_types_array([["area", "bar"], ["area", 42]]) + False + """ + return isinstance(obj, Collection) and all(map(is_shallow_trace_types_array, obj)) + + # Labels --- LabelsArray = CollectionL2[str] diff --git a/src/ridgeplot/datasets/__init__.py b/src/ridgeplot/datasets/__init__.py index adad754e..73b3fa64 100644 --- a/src/ridgeplot/datasets/__init__.py +++ b/src/ridgeplot/datasets/__init__.py @@ -4,9 +4,9 @@ from typing import TYPE_CHECKING if sys.version_info >= (3, 10): - from importlib.resources import files + from importlib.resources import as_file, files else: - from importlib_resources import files + from importlib_resources import as_file, files if TYPE_CHECKING: from typing import Literal @@ -124,7 +124,8 @@ def load_probly( f"Unknown version {version!r} for the probly dataset. " f"Valid versions are {list(versions.keys())}." ) - return pd.read_csv(_DATA_DIR / versions[version]) + with as_file(_DATA_DIR / versions[version]) as data_file: + return pd.read_csv(data_file) def load_lincoln_weather() -> pd.DataFrame: @@ -162,6 +163,7 @@ def load_lincoln_weather() -> pd.DataFrame: https://austinwehrwein.com/data-visualization/plot-inspiration-via-fivethirtyeight/ """ - data = pd.read_csv(_DATA_DIR / "lincoln-weather.csv", index_col="CST") + with as_file(_DATA_DIR / "lincoln-weather.csv") as data_file: + data = pd.read_csv(data_file, index_col="CST") data.index = pd.to_datetime(data.index.to_list()) return data diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 3b1ea98b..55363cfa 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,14 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from collections.abc import Collection +from typing import Union, cast import plotly.express as px import pytest -if TYPE_CHECKING: - from collections.abc import Collection - - from ridgeplot._types import Color, ColorScale +from ridgeplot._types import Color, ColorScale VIRIDIS = ( (0.0, "#440154"), @@ -50,7 +48,7 @@ def viridis_colorscale() -> ColorScale: def valid_colorscale( request: pytest.FixtureRequest, ) -> tuple[ColorScale | Collection[Color] | str, ColorScale]: - return request.param # type: ignore[no-any-return] + return cast(tuple[Union[ColorScale, Collection[Color], str], ColorScale], request.param) INVALID_COLOR_SCALES = [ @@ -68,4 +66,4 @@ def valid_colorscale( @pytest.fixture(scope="session", params=INVALID_COLOR_SCALES) def invalid_colorscale(request: pytest.FixtureRequest) -> ColorScale | Collection[Color] | str: - return request.param # type: ignore[no-any-return] + return cast(Union[ColorScale, Collection[Color], str], request.param) diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index aeb72412..a4693833 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -10,8 +10,9 @@ def test_packaged_installed() -> None: # By definition, if a module has a __path__ attribute, it is a package. assert hasattr(ridgeplot, "__path__") - assert len(ridgeplot.__path__) == 1 - package_path = Path(ridgeplot.__path__[0]) + pkg_path = list(ridgeplot.__path__) + assert len(pkg_path) == 1 + package_path = Path(pkg_path[0]) assert package_path.exists() assert package_path.is_dir() assert package_path.name == "ridgeplot" diff --git a/tests/unit/test_missing.py b/tests/unit/test_missing.py index 1b5ccbc7..c564a95b 100644 --- a/tests/unit/test_missing.py +++ b/tests/unit/test_missing.py @@ -30,7 +30,7 @@ def assert_all_are(*args: Any) -> None: b = args[i + 1] if a is not b: raise AssertionError( - f"{a!r} and {b!r} (i={i}) are not the same object (id: {id(a)} != {id(b)})" + f"{a!r} and {b!r} ({i=}) are not the same object ({id(a)=} != {id(b)=})" ) @@ -43,7 +43,7 @@ def test_reloading() -> None: import ridgeplot._missing as types_module from ridgeplot._missing import MISSING - missing1 = ridgeplot._missing.MISSING + missing1 = ridgeplot._missing.MISSING # type: ignore[attr-defined] missing2 = types_module.MISSING missing3 = MISSING @@ -54,7 +54,7 @@ def test_reloading() -> None: missing1, missing2, missing3, - ridgeplot._missing.MISSING, + ridgeplot._missing.MISSING, # type: ignore[attr-defined] types_module.MISSING, MISSING, ) @@ -64,7 +64,7 @@ def test_reloading() -> None: missing1, missing2, missing3, - ridgeplot._missing.MISSING, + ridgeplot._missing.MISSING, # type: ignore[attr-defined] types_module.MISSING, MISSING, ) diff --git a/tests/unit/test_ridgeplot.py b/tests/unit/test_ridgeplot.py index ca39c479..15bfb600 100644 --- a/tests/unit/test_ridgeplot.py +++ b/tests/unit/test_ridgeplot.py @@ -74,15 +74,13 @@ def test_y_labels_dedup() -> None: def test_shallow_trace_type() -> None: assert ( ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type="bar") == - ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type=["bar", "bar"]) == # type: ignore[arg-type] - ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type=[["bar"], ["bar"]]) # type: ignore[arg-type] + ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type=["bar", "bar"]) == + ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type=[["bar"], ["bar"]]) ) # fmt: skip def test_unknown_trace_type() -> None: - with pytest.raises( - ValueError, match="Unknown trace type 'foo'. Available types: 'area', 'bar'." - ): + with pytest.raises(TypeError, match="Invalid trace_type: foo"): ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type="foo") # type: ignore[arg-type] diff --git a/tox.ini b/tox.ini index e9f581fc..066d5ca1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] labels = - static = pre-commit-all, mypy-safe - static-quick = pre-commit-quick, mypy-incremental + static = pre-commit-all, typing + static-quick = pre-commit-quick, typing tests = tests-unit, tests-e2e, tests-cicd_utils upgrade-requirements = pre-commit-autoupgrade isolated_build = true @@ -68,15 +68,11 @@ commands = pre-commit run ruff --all-files autoupgrade: pre-commit autoupdate {posargs:} -[testenv:mypy-{safe,incremental}] -description = run type checks with mypy -deps = -r requirements/mypy.txt -setenv = - {[testenv]setenv} - _MYPY_DFLT_ARGS=--config-file=mypy.ini --strict +[testenv:typing] +description = run type checks +deps = -r requirements/typing.txt commands = - safe: mypy {env:_MYPY_DFLT_ARGS} --no-incremental --cache-dir=/dev/null {posargs:} - incremental: mypy {env:_MYPY_DFLT_ARGS} --incremental {posargs:} + pyright --skipunannotated [testenv:docs-{live,static}] description = generate Sphinx (live/static) HTML documentation From ea7eb65be015b937be2f75de5673ef845a613d2a Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Mon, 2 Dec 2024 13:55:23 +0000 Subject: [PATCH 08/21] Set `typeCheckingMode` to `standard` --- pyrightconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrightconfig.json b/pyrightconfig.json index 08e67a97..c6a9436e 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -12,5 +12,5 @@ "extraPaths": [ "cicd_utils" ], - "typeCheckingMode": "basic" + "typeCheckingMode": "standard" } From e7b39e58fc472290e432883b6372bd138d04ee5b Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Mon, 2 Dec 2024 19:02:15 +0000 Subject: [PATCH 09/21] Migrate from black to the ruff formatter and enable stricter pyright settings --- .pre-commit-config.yaml | 7 +------ cicd_utils/cicd/compile_plotly_charts.py | 7 ++++--- .../scripts/extract_latest_release_notes.py | 1 + .../ridgeplot_examples/_lincoln_weather.py | 6 +++--- docs/conf.py | 6 +++--- docs/reference/changelog.md | 2 +- pyrightconfig.json | 6 +++++- ruff.toml | 4 ++-- src/ridgeplot/_color/utils.py | 5 ++--- src/ridgeplot/_hist.py | 1 - src/ridgeplot/_kde.py | 7 +------ src/ridgeplot/_obj/traces/area.py | 2 +- src/ridgeplot/_obj/traces/bar.py | 2 +- src/ridgeplot/_ridgeplot.py | 2 +- src/ridgeplot/_utils.py | 2 +- src/ridgeplot/datasets/__init__.py | 2 +- tests/conftest.py | 2 +- tests/unit/color/test_colorscale.py | 2 +- tests/unit/color/test_interpolation.py | 6 +++--- tests/unit/color/test_utils.py | 1 - tests/unit/test_datasets.py | 8 +++++-- tests/unit/test_figure_factory.py | 21 +++++++++---------- tests/unit/test_kde.py | 2 +- tests/unit/test_missing.py | 6 +++--- tests/unit/test_ridgeplot.py | 4 ++-- tests/unit/test_utils.py | 3 +-- 26 files changed, 56 insertions(+), 61 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28ef96c9..9546afac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -105,14 +105,9 @@ repos: hooks: - id: blacken-docs - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.10.0 - hooks: - - id: black-jupyter - - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.7.4 hooks: + - id: ruff-format - id: ruff args: [ --show-fixes, --exit-non-zero-on-fix ] - types_or: [ python, pyi, jupyter ] diff --git a/cicd_utils/cicd/compile_plotly_charts.py b/cicd_utils/cicd/compile_plotly_charts.py index 5a95d493..f74910b5 100755 --- a/cicd_utils/cicd/compile_plotly_charts.py +++ b/cicd_utils/cicd/compile_plotly_charts.py @@ -5,6 +5,7 @@ used in the docs. It saves the HTML and WebP artefacts to the `docs/_static/charts` directory. """ + from __future__ import annotations from copy import deepcopy @@ -116,9 +117,9 @@ def _write_plotlyjs_bundle() -> None: bundle_path.write_text(plotlyjs, encoding="utf-8") -def compile_plotly_charts() -> None: - # Setup logic --- - # _write_plotlyjs_bundle() +def compile_plotly_charts(update_plotlyjs_bundle: bool = False) -> None: + if update_plotlyjs_bundle: + _write_plotlyjs_bundle() # Compile all charts --- if not PATH_STATIC_CHARTS.exists(): diff --git a/cicd_utils/cicd/scripts/extract_latest_release_notes.py b/cicd_utils/cicd/scripts/extract_latest_release_notes.py index 760e0792..e024e0e7 100755 --- a/cicd_utils/cicd/scripts/extract_latest_release_notes.py +++ b/cicd_utils/cicd/scripts/extract_latest_release_notes.py @@ -6,6 +6,7 @@ - The output is written to the `LATEST_RELEASE_NOTES.md` file. - The body of this file is then used as the body of the GitHub release. """ + from __future__ import annotations from pathlib import Path diff --git a/cicd_utils/ridgeplot_examples/_lincoln_weather.py b/cicd_utils/ridgeplot_examples/_lincoln_weather.py index 970b0572..1c7c1456 100644 --- a/cicd_utils/ridgeplot_examples/_lincoln_weather.py +++ b/cicd_utils/ridgeplot_examples/_lincoln_weather.py @@ -22,11 +22,11 @@ def main( df = load_lincoln_weather() - months = df.index.month_name().unique() # type: ignore[attr-defined] + months = df.index.month_name().unique() # pyright: ignore[reportAttributeAccessIssue] samples = [ [ - df[df.index.month_name() == month]["Min Temperature [F]"], # type: ignore[attr-defined] - df[df.index.month_name() == month]["Max Temperature [F]"], # type: ignore[attr-defined] + df[df.index.month_name() == month]["Min Temperature [F]"], # pyright: ignore[reportAttributeAccessIssue] + df[df.index.month_name() == month]["Max Temperature [F]"], # pyright: ignore[reportAttributeAccessIssue] ] for month in months ] diff --git a/docs/conf.py b/docs/conf.py index f78817d4..d6b8e880 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,7 +10,7 @@ try: import importlib.metadata as importlib_metadata except ImportError: - import importlib_metadata # type: ignore[no-redef] + import importlib_metadata # pyright: ignore[no-redef] try: from cicd.compile_plotly_charts import compile_plotly_charts @@ -391,5 +391,5 @@ def setup(app: Sphinx) -> None: compile_plotly_charts() # app.connect("html-page-context", register_jinja_functions) - app.connect("build-finished", lambda *_: _fix_generated_public_api_rst()) - app.connect("build-finished", lambda *_: _fix_html_charts()) + app.connect("build-finished", lambda *_: _fix_generated_public_api_rst()) # pyright: ignore[reportUnknownLambdaType] + app.connect("build-finished", lambda *_: _fix_html_charts()) # pyright: ignore[reportUnknownLambdaType] diff --git a/docs/reference/changelog.md b/docs/reference/changelog.md index ba56e41b..1fe0d4d0 100644 --- a/docs/reference/changelog.md +++ b/docs/reference/changelog.md @@ -12,7 +12,7 @@ Unreleased changes ### Internal - Small improvements to type hints and annotations ({gh-pr}`284`) -- Improve type annotations and switch from mypy to pyright ({gh-pr}`287`) +- Improve type annotations and switch from mypy to stricter pyright settings ({gh-pr}`287`) --- diff --git a/pyrightconfig.json b/pyrightconfig.json index c6a9436e..52090272 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -12,5 +12,9 @@ "extraPaths": [ "cicd_utils" ], - "typeCheckingMode": "standard" + "typeCheckingMode": "strict", + "reportMissingTypeStubs": "none", + "reportUnknownMemberType": "none", + "reportUnknownArgumentType": "none", + "reportUnknownVariableType": "none" } diff --git a/ruff.toml b/ruff.toml index 59da2fc0..961a6fd2 100644 --- a/ruff.toml +++ b/ruff.toml @@ -6,11 +6,11 @@ fix = true line-length = 100 # ================================================ -# Formatting settings (currently not used) +# Formatting settings # ================================================ [format] line-ending = "lf" -docstring-code-format = true +docstring-code-format = false # ================================================ # Linting settings diff --git a/src/ridgeplot/_color/utils.py b/src/ridgeplot/_color/utils.py index 56486612..5122ceef 100644 --- a/src/ridgeplot/_color/utils.py +++ b/src/ridgeplot/_color/utils.py @@ -7,7 +7,7 @@ import plotly.graph_objects as go import plotly.io as pio -from ridgeplot._color.css_colors import CSS_NAMED_COLORS, CssNamedColor +from ridgeplot._color.css_colors import CSS_NAMED_COLORS from ridgeplot._types import Color @@ -24,7 +24,7 @@ def infer_default_color_sequence() -> Collection[Color]: # pragma: no cover def to_rgb(color: Color) -> str: - if not isinstance(color, (str, tuple)): + if not isinstance(color, (str, tuple)): # type: ignore[reportUnnecessaryIsInstance] raise TypeError(f"Expected str or tuple for color, got {type(color)} instead.") if isinstance(color, tuple): r, g, b = color @@ -34,7 +34,6 @@ def to_rgb(color: Color) -> str: elif color.startswith(("rgb(", "rgba(")): rgb = color elif color in CSS_NAMED_COLORS: - color = cast(CssNamedColor, color) return to_rgb(CSS_NAMED_COLORS[color]) else: raise ValueError( diff --git a/src/ridgeplot/_hist.py b/src/ridgeplot/_hist.py index 7cbd78fe..693ee1be 100644 --- a/src/ridgeplot/_hist.py +++ b/src/ridgeplot/_hist.py @@ -8,7 +8,6 @@ from ridgeplot._vendor.more_itertools import zip_strict if TYPE_CHECKING: - from ridgeplot._types import ( Densities, DensityTrace, diff --git a/src/ridgeplot/_kde.py b/src/ridgeplot/_kde.py index b0fa7355..1fafdb21 100644 --- a/src/ridgeplot/_kde.py +++ b/src/ridgeplot/_kde.py @@ -29,7 +29,6 @@ from ridgeplot._vendor.more_itertools import zip_strict if TYPE_CHECKING: - from ridgeplot._types import Densities, Samples, SamplesTrace @@ -97,10 +96,6 @@ def normalize_sample_weights( """ if _is_sample_weights(sample_weights): return [[sample_weights] * len(row) for row in samples] - sample_weights = cast( - Union[SampleWeightsArray, ShallowSampleWeightsArray], - sample_weights, - ) if _is_shallow_sample_weights(sample_weights): sample_weights = nest_shallow_collection(sample_weights) sample_weights = normalise_row_attrs(sample_weights, l2_target=samples) @@ -160,7 +155,7 @@ def estimate_density_trace( dens.fit( kernel=kernel, fft=kernel == "gau" and weights is None, - bw=bandwidth, # type: ignore[arg-type] + bw=bandwidth, # pyright: ignore[reportArgumentType] weights=weights, ) density_y = dens.evaluate(density_x) diff --git a/src/ridgeplot/_obj/traces/area.py b/src/ridgeplot/_obj/traces/area.py index 366fa563..54e53683 100644 --- a/src/ridgeplot/_obj/traces/area.py +++ b/src/ridgeplot/_obj/traces/area.py @@ -73,7 +73,7 @@ def draw(self, fig: go.Figure, coloring_ctx: ColoringContext) -> go.Figure: name=self.label, fill="tonexty", mode="lines", - line=dict(width=self.line_width if self.line_width is not None else 1.5), + line_width=self.line_width, **self._get_coloring_kwargs(ctx=coloring_ctx), # Hover information customdata=[[y_i] for y_i in self.y], diff --git a/src/ridgeplot/_obj/traces/bar.py b/src/ridgeplot/_obj/traces/bar.py index 53c506b8..7c7ef00f 100644 --- a/src/ridgeplot/_obj/traces/bar.py +++ b/src/ridgeplot/_obj/traces/bar.py @@ -40,7 +40,7 @@ def draw(self, fig: go.Figure, coloring_ctx: ColoringContext) -> go.Figure: y=self.y, name=self.label, base=self.y_base, - marker_line_width=self.line_width if self.line_width is not None else 0.5, + marker_line_width=self.line_width, # width=1, # TODO: do we need to specify the bar width? **self._get_coloring_kwargs(ctx=coloring_ctx), # Hover information diff --git a/src/ridgeplot/_ridgeplot.py b/src/ridgeplot/_ridgeplot.py index fbad3231..b89a6fbd 100644 --- a/src/ridgeplot/_ridgeplot.py +++ b/src/ridgeplot/_ridgeplot.py @@ -73,7 +73,7 @@ def _coerce_to_densities( if has_densities: if is_shallow_densities(densities): densities = nest_shallow_collection(densities) - return cast(Densities, densities) + return densities # Transform samples into densities via KDE or histogram binning if is_shallow_samples(samples): diff --git a/src/ridgeplot/_utils.py b/src/ridgeplot/_utils.py index c2a816e7..10d116c7 100644 --- a/src/ridgeplot/_utils.py +++ b/src/ridgeplot/_utils.py @@ -161,7 +161,7 @@ def _get_dim_length(obj: Any) -> int: return len(obj) shape: list[int | set[int]] = [_get_dim_length(arr)] - while isinstance(arr, Collection) and len(arr) > 0: + while len(arr) > 0: try: dim_lengths = set(map(_get_dim_length, arr)) except TypeError: diff --git a/src/ridgeplot/datasets/__init__.py b/src/ridgeplot/datasets/__init__.py index 73b3fa64..5a9f313a 100644 --- a/src/ridgeplot/datasets/__init__.py +++ b/src/ridgeplot/datasets/__init__.py @@ -165,5 +165,5 @@ def load_lincoln_weather() -> pd.DataFrame: """ with as_file(_DATA_DIR / "lincoln-weather.csv") as data_file: data = pd.read_csv(data_file, index_col="CST") - data.index = pd.to_datetime(data.index.to_list()) + data.index = pd.to_datetime(data.index) return data diff --git a/tests/conftest.py b/tests/conftest.py index 4116b80e..290cd7ba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,6 @@ @pytest.fixture(autouse=True, scope="session") -def _patch_plotly_show() -> Generator[None]: +def _patch_plotly_show() -> Generator[None]: # pyright: ignore[reportUnusedFunction] with patch_plotly_show(): yield diff --git a/tests/unit/color/test_colorscale.py b/tests/unit/color/test_colorscale.py index d7120fb5..b4512420 100644 --- a/tests/unit/color/test_colorscale.py +++ b/tests/unit/color/test_colorscale.py @@ -32,7 +32,7 @@ def test_infer_default_colorscale() -> None: def test_validate_coerce_colorscale( - valid_colorscale: tuple[ColorScale | Collection[Color] | str, ColorScale] + valid_colorscale: tuple[ColorScale | Collection[Color] | str, ColorScale], ) -> None: colorscale, expected = valid_colorscale coerced = validate_coerce_colorscale(colorscale=colorscale) diff --git a/tests/unit/color/test_interpolation.py b/tests/unit/color/test_interpolation.py index 5b6b40b6..8f9031aa 100644 --- a/tests/unit/color/test_interpolation.py +++ b/tests/unit/color/test_interpolation.py @@ -10,8 +10,8 @@ ColorscaleInterpolants, InterpolationContext, SolidColormode, - _interpolate_mean_means, - _interpolate_mean_minmax, + _interpolate_mean_means, # pyright: ignore[reportPrivateUsage] + _interpolate_mean_minmax, # pyright: ignore[reportPrivateUsage] interpolate_color, slice_colorscale, ) @@ -49,7 +49,7 @@ def test_interpolate_color_p_not_in_scale(viridis_colorscale: ColorScale) -> Non @pytest.mark.parametrize("p", [-10.0, -1.3, 1.9, 100.0]) def test_interpolate_color_fails_for_p_out_of_bounds(p: float) -> None: with pytest.raises(ValueError, match="should be a float value between 0 and 1"): - interpolate_color(colorscale=..., p=p) # type: ignore[arg-type] + interpolate_color(colorscale=..., p=p) # pyright: ignore[reportArgumentType] # ============================================================== diff --git a/tests/unit/color/test_utils.py b/tests/unit/color/test_utils.py index 96a18d15..b8b87b95 100644 --- a/tests/unit/color/test_utils.py +++ b/tests/unit/color/test_utils.py @@ -8,7 +8,6 @@ from ridgeplot._color.utils import apply_alpha, default_plotly_template, round_color, to_rgb if TYPE_CHECKING: - from ridgeplot._types import Color diff --git a/tests/unit/test_datasets.py b/tests/unit/test_datasets.py index 9a5be3e0..6ac2d499 100644 --- a/tests/unit/test_datasets.py +++ b/tests/unit/test_datasets.py @@ -3,7 +3,11 @@ import pandas as pd import pytest -from ridgeplot.datasets import _DATA_DIR, load_lincoln_weather, load_probly +from ridgeplot.datasets import ( + _DATA_DIR, # pyright: ignore[reportPrivateUsage] + load_lincoln_weather, + load_probly, +) def test_data_dir_contains_data_files() -> None: @@ -29,7 +33,7 @@ def test_load_probly() -> None: df_illinois = load_probly(version="illinois") assert df_illinois.shape == (75, 17) with pytest.raises(ValueError, match="Unknown version"): - load_probly(version="nonexistent") # type: ignore[arg-type] + load_probly(version="nonexistent") # pyright: ignore[reportArgumentType] def test_load_lincoln_weather() -> None: diff --git a/tests/unit/test_figure_factory.py b/tests/unit/test_figure_factory.py index 001209c6..43420550 100644 --- a/tests/unit/test_figure_factory.py +++ b/tests/unit/test_figure_factory.py @@ -11,7 +11,6 @@ class TestCreateRidgeplot: - @pytest.mark.parametrize( "densities", [ @@ -26,14 +25,14 @@ def test_densities_must_be_4d(self, densities: Densities) -> None: with pytest.raises(ValueError, match="Expected a 4D array of densities"): create_ridgeplot( densities=densities, - trace_types=..., # type: ignore[arg-type] - colorscale=..., # type: ignore[arg-type] - opacity=..., # type: ignore[arg-type] - colormode=..., # type: ignore[arg-type] - trace_labels=..., # type: ignore[arg-type] - line_color=..., # type: ignore[arg-type] - line_width=..., # type: ignore[arg-type] - spacing=..., # type: ignore[arg-type] - show_yticklabels=..., # type: ignore[arg-type] - xpad=..., # type: ignore[arg-type] + trace_types=..., # pyright: ignore[reportArgumentType] + colorscale=..., # pyright: ignore[reportArgumentType] + opacity=..., # pyright: ignore[reportArgumentType] + colormode=..., # pyright: ignore[reportArgumentType] + trace_labels=..., # pyright: ignore[reportArgumentType] + line_color=..., # pyright: ignore[reportArgumentType] + line_width=..., # pyright: ignore[reportArgumentType] + spacing=..., # pyright: ignore[reportArgumentType] + show_yticklabels=..., # pyright: ignore[reportArgumentType] + xpad=..., # pyright: ignore[reportArgumentType] ) diff --git a/tests/unit/test_kde.py b/tests/unit/test_kde.py index 749a5354..140ff897 100644 --- a/tests/unit/test_kde.py +++ b/tests/unit/test_kde.py @@ -8,7 +8,7 @@ from ridgeplot._kde import ( KDEPoints, - _validate_densities, + _validate_densities, # pyright: ignore[reportPrivateUsage] estimate_densities, estimate_density_trace, ) diff --git a/tests/unit/test_missing.py b/tests/unit/test_missing.py index c564a95b..ea90a1b6 100644 --- a/tests/unit/test_missing.py +++ b/tests/unit/test_missing.py @@ -43,7 +43,7 @@ def test_reloading() -> None: import ridgeplot._missing as types_module from ridgeplot._missing import MISSING - missing1 = ridgeplot._missing.MISSING # type: ignore[attr-defined] + missing1 = ridgeplot._missing.MISSING # pyright: ignore[reportAttributeAccessIssue] missing2 = types_module.MISSING missing3 = MISSING @@ -54,7 +54,7 @@ def test_reloading() -> None: missing1, missing2, missing3, - ridgeplot._missing.MISSING, # type: ignore[attr-defined] + ridgeplot._missing.MISSING, # pyright: ignore[reportAttributeAccessIssue] types_module.MISSING, MISSING, ) @@ -64,7 +64,7 @@ def test_reloading() -> None: missing1, missing2, missing3, - ridgeplot._missing.MISSING, # type: ignore[attr-defined] + ridgeplot._missing.MISSING, # pyright: ignore[reportAttributeAccessIssue] types_module.MISSING, MISSING, ) diff --git a/tests/unit/test_ridgeplot.py b/tests/unit/test_ridgeplot.py index 15bfb600..e1a89856 100644 --- a/tests/unit/test_ridgeplot.py +++ b/tests/unit/test_ridgeplot.py @@ -81,7 +81,7 @@ def test_shallow_trace_type() -> None: def test_unknown_trace_type() -> None: with pytest.raises(TypeError, match="Invalid trace_type: foo"): - ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type="foo") # type: ignore[arg-type] + ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type="foo") # pyright: ignore[reportArgumentType] # ============================================================== @@ -90,7 +90,7 @@ def test_unknown_trace_type() -> None: def test_colorscale_coercion( - valid_colorscale: tuple[ColorScale | Collection[Color] | str, ColorScale] + valid_colorscale: tuple[ColorScale | Collection[Color] | str, ColorScale], ) -> None: colorscale, coerced = valid_colorscale assert ridgeplot(samples=[[[1, 2, 3], [4, 5, 6]]], colorscale=colorscale) == ridgeplot( diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 7ec1eb7d..6d519fdb 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -9,7 +9,6 @@ from ridgeplot._utils import get_xy_extrema, normalise_min_max if TYPE_CHECKING: - from ridgeplot._types import Densities, DensitiesRow _X = TypeVar("_X") @@ -36,7 +35,7 @@ def test_raise_for_non_2d_array(self) -> None: # valid 2D trace [[(0, 0), (1, 1), (2, 2)]], # invalid 3D trace - [[(3, 3, 3), (4, 4, 4)]], # type: ignore[list-item] + [[(3, 3, 3), (4, 4, 4)]], # pyright: ignore[reportArgumentType] ] ) From bbeae539e08e2767b4633284f8caa0e352c9ff23 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Mon, 2 Dec 2024 19:09:05 +0000 Subject: [PATCH 10/21] Update changelog.md and remove references of the black formatter --- .pre-commit-config.yaml | 5 ----- docs/development/contributing.md | 33 ++++++++++++++++---------------- docs/reference/changelog.md | 1 + pyproject.toml | 4 ---- tox.ini | 2 +- 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9546afac..95846d47 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -100,11 +100,6 @@ repos: args: [ --no-build-isolation ] additional_dependencies: [setuptools-scm] - - repo: https://github.com/adamchainz/blacken-docs - rev: 1.19.1 - hooks: - - id: blacken-docs - - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.7.4 hooks: diff --git a/docs/development/contributing.md b/docs/development/contributing.md index dd5e1287..d0bf31aa 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -184,23 +184,22 @@ Finally, we have a small workflow (see {repo-file}`.github/workflows/check-relea Here is a quick overview of ~~all~~ most of the CI tools and software used in this project, along with their respective configuration files. If you have any questions or need help with any of these tools, feel free to ask for help from the community by commenting on your issue or pull request. -| Tool | Category | config files | Details | -|---------------------------------------------------------|------------------|-------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [Tox](https://github.com/tox-dev/tox) | 🔧 Orchestration | {repo-file}`tox.ini` | We use Tox to reliably run all integration approval steps in reproducible isolated virtual environments. | -| [GitHub Actions](https://github.com/features/actions) | 🔧 Orchestration | {repo-file}`.github/workflows/ci.yml` | Workflow automation for GitHub. We use it to automatically run all integration approval steps on every push or pull request event. | -| [Make](https://www.gnu.org/software/make/) | 🔧 Orchestration | {repo-file}`Makefile` | A build automation tool that we (mis)use to abstract away some bootstrapping and development environment setup steps. | -| [git](https://git-scm.com/) | 🕰 VCS | {repo-file}`.gitignore` | The project's version control system. | -| [pytest](https://github.com/pytest-dev/pytest) | 🧪 Testing | {repo-file}`pytest.ini` | Testing framework for python code. | -| [Coverage.py](https://github.com/nedbat/coveragepy) | 📊 Coverage | {repo-file}`.coveragerc` | The code coverage tool for Python | -| [Codecov](https://about.codecov.io/) | 📊 Coverage | {repo-file}`.github/workflows/ci.yml` | An external services for tracking, monitoring, and alerting on code coverage metrics. | -| [pre-commit](https://pre-commit.com/) | 💅 Linting | {repo-file}`.pre-commit-config.yaml` | Used to to automatically check and fix any formatting rules on every commit. | -| [pyright](https://github.com/microsoft/pyright) | 💅 Linting | {repo-file}`pyrightconfig.json` | A static type checker for Python. We use quite a strict configuration here, which can be tricky at times. Feel free to ask for help from the community by commenting on your issue or pull request. | -| [black](https://github.com/psf/black) | 💅 Linting | {repo-file}`pyproject.toml` | "The uncompromising Python code formatter". We use `black` to automatically format Python code in a deterministic manner. Maybe we'll replace this with `ruff` in the future. | -| [ruff](https://github.com/astral-sh/ruff) | 💅 Linting | {repo-file}`ruff.toml` | "An extremely fast Python linter and code formatter, written in Rust." For this project, ruff replaced Flake8 (+plugins), isort, pydocstyle, pyupgrade, and autoflake with a single (and faster) tool. | -| [EditorConfig](https://editorconfig.org/) | 💅 Linting | {repo-file}`.editorconfig` | This repository uses the `.editorconfig` standard configuration file, which aims to ensure consistent style across multiple programming environments. | -| [bumpversion](https://github.com/c4urself/bump2version) | 📦 Packaging | {repo-file}`.bumpversion.cfg` | A small command line tool to simplify releasing software by updating all version strings in your source code by the correct increment. | -| [setuptools](https://setuptools.pypa.io/en/latest/) | 📦 Packaging | {repo-file}`pyproject.toml` and {repo-file}`MANIFEST.in` | `MANIFEST.in` tells `setuptools` which files to include in the distribution. `pyproject.toml` is the new standard for defining static package metadata. | -| [readthedocs](https://readthedocs.org/) | 📚 Documentation | {repo-file}`.readthedocs.yaml` and {repo-file}`.github/workflows/readthedocs-preview.yml` | An open-source documentation hosting platform. We use it to automatically build and deploy the documentation for this project. | +| Tool | Category | config files | Details | +|---------------------------------------------------------|------------------|-------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Tox](https://github.com/tox-dev/tox) | 🔧 Orchestration | {repo-file}`tox.ini` | We use Tox to reliably run all integration approval steps in reproducible isolated virtual environments. | +| [GitHub Actions](https://github.com/features/actions) | 🔧 Orchestration | {repo-file}`.github/workflows/ci.yml` | Workflow automation for GitHub. We use it to automatically run all integration approval steps on every push or pull request event. | +| [Make](https://www.gnu.org/software/make/) | 🔧 Orchestration | {repo-file}`Makefile` | A build automation tool that we (mis)use to abstract away some bootstrapping and development environment setup steps. | +| [git](https://git-scm.com/) | 🕰 VCS | {repo-file}`.gitignore` | The project's version control system. | +| [pytest](https://github.com/pytest-dev/pytest) | 🧪 Testing | {repo-file}`pytest.ini` | Testing framework for python code. | +| [Coverage.py](https://github.com/nedbat/coveragepy) | 📊 Coverage | {repo-file}`.coveragerc` | The code coverage tool for Python | +| [Codecov](https://about.codecov.io/) | 📊 Coverage | {repo-file}`.github/workflows/ci.yml` | An external services for tracking, monitoring, and alerting on code coverage metrics. | +| [pre-commit](https://pre-commit.com/) | 💅 Linting | {repo-file}`.pre-commit-config.yaml` | Used to to automatically check and fix any formatting rules on every commit. | +| [pyright](https://github.com/microsoft/pyright) | 💅 Linting | {repo-file}`pyrightconfig.json` | A static type checker for Python. We use quite a strict configuration here, which can be tricky at times. Feel free to ask for help from the community by commenting on your issue or pull request. | +| [ruff](https://github.com/astral-sh/ruff) | 💅 Linting | {repo-file}`ruff.toml` | "An extremely fast Python linter and code formatter, written in Rust." For this project, ruff replaced black, Flake8 (+plugins), isort, pydocstyle, pyupgrade, and autoflake with a single (and faster) tool. | +| [EditorConfig](https://editorconfig.org/) | 💅 Linting | {repo-file}`.editorconfig` | This repository uses the `.editorconfig` standard configuration file, which aims to ensure consistent style across multiple programming environments. | +| [bumpversion](https://github.com/c4urself/bump2version) | 📦 Packaging | {repo-file}`.bumpversion.cfg` | A small command line tool to simplify releasing software by updating all version strings in your source code by the correct increment. | +| [setuptools](https://setuptools.pypa.io/en/latest/) | 📦 Packaging | {repo-file}`pyproject.toml` and {repo-file}`MANIFEST.in` | `MANIFEST.in` tells `setuptools` which files to include in the distribution. `pyproject.toml` is the new standard for defining static package metadata. | +| [readthedocs](https://readthedocs.org/) | 📚 Documentation | {repo-file}`.readthedocs.yaml` and {repo-file}`.github/workflows/readthedocs-preview.yml` | An open-source documentation hosting platform. We use it to automatically build and deploy the documentation for this project. | ## Code of Conduct diff --git a/docs/reference/changelog.md b/docs/reference/changelog.md index 1fe0d4d0..d98d21c7 100644 --- a/docs/reference/changelog.md +++ b/docs/reference/changelog.md @@ -13,6 +13,7 @@ Unreleased changes - Small improvements to type hints and annotations ({gh-pr}`284`) - Improve type annotations and switch from mypy to stricter pyright settings ({gh-pr}`287`) +- Switch from `black` to the new `ruff` formatter ({gh-pr}`287`) --- diff --git a/pyproject.toml b/pyproject.toml index 7217266a..c3b81bed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,3 @@ namespaces = false # and we want to push X.devM to TestPyPi # on every merge to the `main` branch local_scheme = "no-local-version" - -[tool.black] -line-length = 100 -include = '\.pyi?$' diff --git a/tox.ini b/tox.ini index 066d5ca1..25972364 100644 --- a/tox.ini +++ b/tox.ini @@ -64,7 +64,7 @@ skip_install = true deps = pre-commit commands = all: pre-commit run --all-files --show-diff-on-failure {posargs:} - quick: pre-commit run black-jupyter --all-files + quick: pre-commit run ruff-format --all-files pre-commit run ruff --all-files autoupgrade: pre-commit autoupdate {posargs:} From c4798b0e03f7fc189da0936ecf5601cccee983dd Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Wed, 4 Dec 2024 22:24:53 +0000 Subject: [PATCH 11/21] Remove deprecated ruff rules --- ruff.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/ruff.toml b/ruff.toml index 961a6fd2..59cc58b6 100644 --- a/ruff.toml +++ b/ruff.toml @@ -74,8 +74,6 @@ ignore = [ "TD003", # Missing issue link on the line following this TODO # flake8-annotations (ANN) - "ANN101", # Missing type annotation for `self` in method - "ANN102", # Missing type annotation for `cls` in classmethod "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} ] From 84c951ae4dc9d950438c7d65168b692b3821dd63 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Wed, 4 Dec 2024 23:09:15 +0000 Subject: [PATCH 12/21] Add tests for `bin_trace_samples` and `bin_samples` --- src/ridgeplot/_hist.py | 19 +++++---- tests/unit/test_hist.py | 92 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 tests/unit/test_hist.py diff --git a/src/ridgeplot/_hist.py b/src/ridgeplot/_hist.py index 693ee1be..a91d02f8 100644 --- a/src/ridgeplot/_hist.py +++ b/src/ridgeplot/_hist.py @@ -22,14 +22,19 @@ def bin_trace_samples( trace_samples: SamplesTrace, nbins: int, - weights: SampleWeights, + weights: SampleWeights = None, ) -> DensityTrace: - hist, bins = np.histogram( - np.asarray(trace_samples, dtype=float), - bins=nbins, - weights=np.asarray(weights, dtype=float) if weights is not None else None, - ) - return list(zip(bins[:-1], hist)) + trace_samples = np.asarray(trace_samples, dtype=float) + if not np.isfinite(trace_samples).all(): + raise ValueError("The samples array should not contain any infs or NaNs.") + if weights is not None: + weights = np.asarray(weights, dtype=float) + if len(weights) != len(trace_samples): + raise ValueError("The weights array should have the same length as the samples array.") + if not np.isfinite(weights).all(): + raise ValueError("The weights array should not contain any infs or NaNs.") + hist, bins = np.histogram(trace_samples, bins=nbins, weights=weights) + return [(float(x), float(y)) for x, y in zip(bins, hist)] def bin_samples( diff --git a/tests/unit/test_hist.py b/tests/unit/test_hist.py new file mode 100644 index 00000000..aaf9029e --- /dev/null +++ b/tests/unit/test_hist.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import numpy as np +import pytest + +from ridgeplot._hist import ( + bin_samples, + bin_trace_samples, +) + +# Example data + +SAMPLES_IN = [1, 2, 2, 3, 4] +NBINS = 4 +DENSITIES_OUT = [(1.0, 1.0), (1.75, 2.0), (2.5, 1.0), (3.25, 1.0)] +X_OUT, Y_OUT = zip(*DENSITIES_OUT) + +WEIGHTS = [1, 1, 1, 1, 9] + +# ============================================================== +# --- estimate_density_trace() +# ============================================================== + + +def test_bin_trace_samples_simple() -> None: + density_trace = bin_trace_samples(trace_samples=SAMPLES_IN, nbins=NBINS) + x, y = zip(*density_trace) + assert x == X_OUT + assert y == Y_OUT + + +@pytest.mark.parametrize("nbins", [2, 5, 8, 11]) +def test_bin_trace_samples_nbins(nbins: int) -> None: + density_trace = bin_trace_samples(trace_samples=SAMPLES_IN, nbins=nbins) + assert len(density_trace) == nbins + + +@pytest.mark.parametrize("non_finite_value", [np.inf, np.nan, float("inf"), float("nan")]) +def test_bin_trace_samples_fails_for_non_finite_values(non_finite_value: float) -> None: + err_msg = "The samples array should not contain any infs or NaNs." + with pytest.raises(ValueError, match=err_msg): + bin_trace_samples(trace_samples=[*SAMPLES_IN[:-1], non_finite_value], nbins=NBINS) + + +def test_bin_trace_samples_weights() -> None: + density_trace = bin_trace_samples( + trace_samples=SAMPLES_IN, + nbins=NBINS, + weights=WEIGHTS, + ) + x, y = zip(*density_trace) + assert x == X_OUT + assert np.argmax(y) == len(y) - 1 + + +def test_bin_trace_samples_weights_not_same_length() -> None: + with pytest.raises( + ValueError, match="The weights array should have the same length as the samples array" + ): + bin_trace_samples(trace_samples=SAMPLES_IN, nbins=NBINS, weights=[1, 1, 1]) + + +@pytest.mark.parametrize("non_finite_value", [np.inf, np.nan, float("inf"), float("nan")]) +def test_bin_trace_samples_weights_fails_for_non_finite_values( + non_finite_value: float, +) -> None: + err_msg = "The weights array should not contain any infs or NaNs." + with pytest.raises(ValueError, match=err_msg): + bin_trace_samples( + trace_samples=SAMPLES_IN, + nbins=NBINS, + weights=[*WEIGHTS[:-1], non_finite_value], + ) + + +# ============================================================== +# --- estimate_densities() +# ============================================================== + + +def test_bin_samples() -> None: + densities = bin_samples( + samples=[[SAMPLES_IN], [SAMPLES_IN]], + nbins=NBINS, + ) + assert len(densities) == 2 + for densities_row in densities: + assert len(densities_row) == 1 + density_trace = next(iter(densities_row)) + x, y = zip(*density_trace) + assert x == X_OUT + assert y == Y_OUT From b4579a0f0b901b51ef73926c1d38602a0ed56613 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Wed, 4 Dec 2024 23:15:15 +0000 Subject: [PATCH 13/21] Update default behaviour for `trace_type` --- src/ridgeplot/_ridgeplot.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/ridgeplot/_ridgeplot.py b/src/ridgeplot/_ridgeplot.py index b89a6fbd..ca39e3b4 100644 --- a/src/ridgeplot/_ridgeplot.py +++ b/src/ridgeplot/_ridgeplot.py @@ -99,7 +99,7 @@ def _coerce_to_densities( def ridgeplot( samples: Samples | ShallowSamples | None = None, densities: Densities | ShallowDensities | None = None, - trace_type: TraceTypesArray | ShallowTraceTypesArray | TraceType = "area", + trace_type: TraceTypesArray | ShallowTraceTypesArray | TraceType | None = None, labels: LabelsArray | ShallowLabelsArray | None = None, # KDE parameters kernel: str = "gau", @@ -185,11 +185,13 @@ def ridgeplot( See :paramref:`samples` above for more details. - trace_type : TraceTypesArray or ShallowTraceTypesArray or TraceType - The type of trace to display. The default is ``"area"``. Choices are - ``"area"`` or ``"bar"``. If a single value is passed, it will be used - for all traces. If a list of values is passed, it should have the same - shape as the samples array. + trace_type : TraceTypesArray or ShallowTraceTypesArray or TraceType or None + The type of trace to display. Choices are ``'area'`` or ``'bar'``. If a + single value is passed, it will be used for all traces. If a list of + values is passed, it should have the same shape as the samples array. + If not specified (default), the traces will be displayed as area plots + (``trace_type='area'``) unless histogram binning is used, in which case + the traces will be displayed as bar plots (``trace_type='bar'``). labels : LabelsArray or ShallowLabelsArray or None A list of string labels for each trace. If not specified (default), the @@ -385,6 +387,9 @@ def ridgeplot( if neither of them is specified. i.e. you may only specify one of them. """ + if trace_type is None: + trace_type = "area" if nbins is None else "bar" + densities = _coerce_to_densities( samples=samples, densities=densities, @@ -394,7 +399,7 @@ def ridgeplot( nbins=nbins, sample_weights=sample_weights, ) - del samples, kernel, bandwidth, kde_points + del samples, kernel, bandwidth, kde_points, nbins, sample_weights if norm: densities = normalise_densities(densities, norm=norm) From 1a0e3cae12ab3fbc5319fa9866f73e5a3de41390 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Wed, 4 Dec 2024 23:42:26 +0000 Subject: [PATCH 14/21] Add tests for `nbins`, `get_trace_cls`, and `BarTrace` --- src/ridgeplot/_obj/traces/__init__.py | 14 ++++-- tests/unit/obj/traces/test_bar.py | 66 +++++++++++++++++++++++++++ tests/unit/obj/traces/test_init.py | 28 ++++++++++++ tests/unit/test_ridgeplot.py | 11 +++++ 4 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 tests/unit/obj/traces/test_bar.py create mode 100644 tests/unit/obj/traces/test_init.py diff --git a/src/ridgeplot/_obj/traces/__init__.py b/src/ridgeplot/_obj/traces/__init__.py index 301583f6..8d2f221b 100644 --- a/src/ridgeplot/_obj/traces/__init__.py +++ b/src/ridgeplot/_obj/traces/__init__.py @@ -4,13 +4,19 @@ from ridgeplot._obj.traces.area import AreaTrace from ridgeplot._obj.traces.bar import BarTrace +from ridgeplot._obj.traces.base import RidgeplotTrace if TYPE_CHECKING: - from ridgeplot._obj.traces.base import RidgeplotTrace from ridgeplot._types import TraceType +__all__ = [ + "AreaTrace", + "BarTrace", + "RidgeplotTrace", + "get_trace_cls", +] -TRACE_TYPES: dict[TraceType, type[RidgeplotTrace]] = { +_TRACE_TYPES: dict[TraceType, type[RidgeplotTrace]] = { "area": AreaTrace, "bar": BarTrace, } @@ -20,7 +26,7 @@ def get_trace_cls(trace_type: TraceType) -> type[RidgeplotTrace]: """Get a trace class by its type.""" try: - return TRACE_TYPES[trace_type] + return _TRACE_TYPES[trace_type] except KeyError as err: - types = ", ".join(repr(t) for t in TRACE_TYPES) + types = ", ".join(repr(t) for t in _TRACE_TYPES) raise ValueError(f"Unknown trace type {trace_type!r}. Available types: {types}.") from err diff --git a/tests/unit/obj/traces/test_bar.py b/tests/unit/obj/traces/test_bar.py new file mode 100644 index 00000000..a9114c31 --- /dev/null +++ b/tests/unit/obj/traces/test_bar.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import pytest + +from ridgeplot._color.interpolation import InterpolationContext +from ridgeplot._obj.traces.bar import BarTrace +from ridgeplot._obj.traces.base import ColoringContext + + +@pytest.fixture +def bar_trace() -> BarTrace: + return BarTrace( + trace=[(0, 0), (1, 1), (2, 0)], + label="Trace 1", + solid_color="red", + zorder=1, + y_base=0, + line_color="black", + line_width=0.5, + ) + + +@pytest.fixture +def interpolation_ctx() -> InterpolationContext: + return InterpolationContext( + densities=[ + [[(0, 0), (1, 1), (2, 0)]], + [[(1, 0), (2, 1), (3, 0)]], + ], + n_rows=2, + n_traces=2, + x_min=0, + x_max=3, + ) + + +class TestBarTrace: + def test_coloring_kwargs_fillgradient( + self, bar_trace: BarTrace, interpolation_ctx: InterpolationContext + ) -> None: + coloring_ctx = ColoringContext( + colorscale=[(0.0, "red"), (1.0, "blue")], + colormode="fillgradient", + opacity=None, + interpolation_ctx=interpolation_ctx, + ) + color_kwargs = bar_trace._get_coloring_kwargs(ctx=coloring_ctx) # pyright: ignore[reportPrivateUsage] + assert color_kwargs == { + "marker_line_color": "black", + "marker_color": ["rgb(255, 0, 0)", "rgb(170.0, 0.0, 85.0)", "rgb(85.0, 0.0, 170.0)"], + } + + def test_coloring_kwargs_fillcolor( + self, bar_trace: BarTrace, interpolation_ctx: InterpolationContext + ) -> None: + coloring_ctx = ColoringContext( + colorscale=[(0.0, "red"), (1.0, "blue")], + colormode="trace-index", + opacity=None, + interpolation_ctx=interpolation_ctx, + ) + color_kwargs = bar_trace._get_coloring_kwargs(ctx=coloring_ctx) # pyright: ignore[reportPrivateUsage] + assert color_kwargs == { + "marker_line_color": "black", + "marker_color": "red", + } diff --git a/tests/unit/obj/traces/test_init.py b/tests/unit/obj/traces/test_init.py new file mode 100644 index 00000000..83a4ba73 --- /dev/null +++ b/tests/unit/obj/traces/test_init.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from ridgeplot._obj.traces import AreaTrace, BarTrace, RidgeplotTrace, get_trace_cls + +if TYPE_CHECKING: + from ridgeplot._types import TraceType + + +@pytest.mark.parametrize( + ("name", "cls"), + [ + ("area", AreaTrace), + ("bar", BarTrace), + ], +) +def test_get_trace_cls(name: TraceType, cls: type[RidgeplotTrace]) -> None: + assert get_trace_cls(name) is cls + + +def test_get_trace_cls_unknown() -> None: + with pytest.raises( + ValueError, match="Unknown trace type 'foo'. Available types: 'area', 'bar'." + ): + get_trace_cls("foo") # pyright: ignore[reportArgumentType] diff --git a/tests/unit/test_ridgeplot.py b/tests/unit/test_ridgeplot.py index e1a89856..5f452c4c 100644 --- a/tests/unit/test_ridgeplot.py +++ b/tests/unit/test_ridgeplot.py @@ -84,6 +84,17 @@ def test_unknown_trace_type() -> None: ridgeplot(samples=[[1, 2, 3], [1, 2, 3]], trace_type="foo") # pyright: ignore[reportArgumentType] +# ============================================================== +# --- param: nbins +# ============================================================== + + +def test_nbins() -> None: + fig = ridgeplot(samples=[[[1, 2, 3], [4, 5, 6]]], nbins=3) + assert len(fig.data) == 2 + assert fig.data[0]._plotly_name == "bar" + + # ============================================================== # --- param: colorscale # ============================================================== From 5f6ca76692628fabd5af7c76c8c9f8f8dd0f8e2e Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Wed, 4 Dec 2024 23:44:28 +0000 Subject: [PATCH 15/21] Update basic hist docs --- cicd_utils/ridgeplot_examples/_basic_hist.py | 2 +- docs/getting_started/getting_started.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cicd_utils/ridgeplot_examples/_basic_hist.py b/cicd_utils/ridgeplot_examples/_basic_hist.py index 9222438d..ee2ae666 100644 --- a/cicd_utils/ridgeplot_examples/_basic_hist.py +++ b/cicd_utils/ridgeplot_examples/_basic_hist.py @@ -13,7 +13,7 @@ def main() -> go.Figure: rng = np.random.default_rng(42) my_samples = [rng.normal(n / 1.2, size=600) for n in range(7, 0, -1)] - fig = ridgeplot(samples=my_samples, nbins=20, trace_type="bar") + fig = ridgeplot(samples=my_samples, nbins=20) fig.update_layout(height=350, width=800) return fig diff --git a/docs/getting_started/getting_started.md b/docs/getting_started/getting_started.md index 2accca34..ee46e00b 100644 --- a/docs/getting_started/getting_started.md +++ b/docs/getting_started/getting_started.md @@ -19,10 +19,10 @@ fig.show() :file: ../_static/charts/basic.html ``` -By default, the `ridgeplot` function will estimate the samples' probability density functions (PDFs) using kernel density estimation (KDE) and plot them as ridgeline area traces (`trace_type="area"`). If you want to plot histograms instead, you can set the `nbins` parameter and change the `trace_type` to `"bar"`. +By default, the `ridgeplot` function will estimate the samples' probability density functions (PDFs) using kernel density estimation (KDE) and plot them as ridgeline area traces (`trace_type="area"`). If you want to plot histograms instead, you can set the `nbins` parameter (which will automatically switch the trace type to `"bar"`). ```python -fig = ridgeplot(samples=my_samples, nbins=20, trace_type="bar") +fig = ridgeplot(samples=my_samples, nbins=20) fig.show() ``` From 06c4b52a625126d9dea36f29f49be7322bf3aa57 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Wed, 4 Dec 2024 23:58:30 +0000 Subject: [PATCH 16/21] Update getting started docs --- docs/_static/charts/basic_hist.html | 2 +- docs/getting_started/getting_started.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/_static/charts/basic_hist.html b/docs/_static/charts/basic_hist.html index 2518bda9..1eab9140 100644 --- a/docs/_static/charts/basic_hist.html +++ b/docs/_static/charts/basic_hist.html @@ -1 +1 @@ -
+
diff --git a/docs/getting_started/getting_started.md b/docs/getting_started/getting_started.md index ee46e00b..a1e6f999 100644 --- a/docs/getting_started/getting_started.md +++ b/docs/getting_started/getting_started.md @@ -19,7 +19,7 @@ fig.show() :file: ../_static/charts/basic.html ``` -By default, the `ridgeplot` function will estimate the samples' probability density functions (PDFs) using kernel density estimation (KDE) and plot them as ridgeline area traces (`trace_type="area"`). If you want to plot histograms instead, you can set the `nbins` parameter (which will automatically switch the trace type to `"bar"`). +By default, the {py:func}`~ridgeplot.ridgeplot()` function will estimate the samples' probability density functions (PDFs) using kernel density estimation (KDE) and plot them as ridgeline area traces ({py:paramref}`trace_type="area" `). If you want to plot histograms instead, you can set the {py:paramref}`~ridgeplot.ridgeplot.nbins` parameter to an integer, which will automatically switch the trace type to `"bar"`. ```python fig = ridgeplot(samples=my_samples, nbins=20) @@ -220,10 +220,10 @@ samples = [ ``` :::{note} -For other use cases (like in the two previous examples), you could use a numpy ndarray to represent the samples. However, since different months have different number of days, we need to use a data container that can hold arrays of different lengths along the same dimension. Irregular arrays like this one are called [ragged arrays](https://en.wikipedia.org/wiki/Jagged_array). There are many different ways you can represent irregular arrays in Python. In this specific example, we used a list of lists of pandas Series. However,`ridgeplot` is designed to handle any object that implements the {py:class}`~typing.Collection`\[{py:class}`~typing.Collection`\[{py:class}`~typing.Collection`\[{py:data}`~ridgeplot._types.Numeric`\]]] protocol (_i.e.,_ any numeric 3D ragged array). +For other use cases (like in the two previous examples), you could use a numpy ndarray to represent the samples. However, since different months have different number of days, we need to use a data container that can hold arrays of different lengths along the same dimension. Irregular arrays like this one are called [ragged arrays](https://en.wikipedia.org/wiki/Jagged_array). There are many different ways you can represent irregular arrays in Python. In this specific example, we used a list of lists of pandas Series. However, {py:func}`~ridgeplot.ridgeplot()` is designed to handle any object that implements the {py:class}`~typing.Collection`\[{py:class}`~typing.Collection`\[{py:class}`~typing.Collection`\[{py:data}`~ridgeplot._types.Numeric`\]]] protocol (_i.e.,_ any numeric 3D ragged array). ::: -Finally, we can pass the `samples` list to the {py:func}`~ridgeplot.ridgeplot()` function and specify any other arguments we want to customize the plot, like adjusting the KDE's bandwidth, the vertical spacing between rows, etc. +Finally, we can pass the {py:paramref}`~ridgeplot.ridgeplot.samples` list to the {py:func}`~ridgeplot.ridgeplot()` function and specify any other arguments we want to customize the plot, like adjusting the KDE's bandwidth, the vertical spacing between rows, etc. ```python fig = ridgeplot( @@ -263,7 +263,7 @@ We are currently investigating the best way to support all color options availab 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. -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 `colorscale` and `colormode` parameters in the call to the {py:func}`~ridgeplot.ridgeplot()` function. _i.e._, +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._, ```python fig = ridgeplot( From 6fa13579fc682385e768d45b13c748b0cd8ba281 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Thu, 5 Dec 2024 00:05:24 +0000 Subject: [PATCH 17/21] Update API docs --- docs/api/internal/_obj/traces.rst | 10 ++++++++++ docs/api/internal/_obj/traces/area.rst | 7 +++++++ docs/api/internal/_obj/traces/bar.rst | 7 +++++++ docs/api/internal/_obj/traces/base.rst | 7 +++++++ docs/api/internal/hist.rst | 7 +++++++ docs/api/internal/obj.rst | 10 ++++++++++ 6 files changed, 48 insertions(+) create mode 100644 docs/api/internal/_obj/traces.rst create mode 100644 docs/api/internal/_obj/traces/area.rst create mode 100644 docs/api/internal/_obj/traces/bar.rst create mode 100644 docs/api/internal/_obj/traces/base.rst create mode 100644 docs/api/internal/hist.rst create mode 100644 docs/api/internal/obj.rst diff --git a/docs/api/internal/_obj/traces.rst b/docs/api/internal/_obj/traces.rst new file mode 100644 index 00000000..04959322 --- /dev/null +++ b/docs/api/internal/_obj/traces.rst @@ -0,0 +1,10 @@ +ridgeplot._obj.traces +================ + +Object-oriented trace interfaces. + +.. toctree:: + :maxdepth: 1 + :glob: + + traces/* diff --git a/docs/api/internal/_obj/traces/area.rst b/docs/api/internal/_obj/traces/area.rst new file mode 100644 index 00000000..83c6e3f3 --- /dev/null +++ b/docs/api/internal/_obj/traces/area.rst @@ -0,0 +1,7 @@ +ridgeplot._obj.traces.area +=========================== + +Area trace object. + +.. automodule:: ridgeplot._obj.traces.area + :private-members: diff --git a/docs/api/internal/_obj/traces/bar.rst b/docs/api/internal/_obj/traces/bar.rst new file mode 100644 index 00000000..2f7e7075 --- /dev/null +++ b/docs/api/internal/_obj/traces/bar.rst @@ -0,0 +1,7 @@ +ridgeplot._obj.traces.bar +=========================== + +Bar trace object. + +.. automodule:: ridgeplot._obj.traces.bar + :private-members: diff --git a/docs/api/internal/_obj/traces/base.rst b/docs/api/internal/_obj/traces/base.rst new file mode 100644 index 00000000..0f5b55c3 --- /dev/null +++ b/docs/api/internal/_obj/traces/base.rst @@ -0,0 +1,7 @@ +ridgeplot._obj.traces.base +=========================== + +Base trace object and utilities. + +.. automodule:: ridgeplot._obj.traces.base + :private-members: diff --git a/docs/api/internal/hist.rst b/docs/api/internal/hist.rst new file mode 100644 index 00000000..9b8bf449 --- /dev/null +++ b/docs/api/internal/hist.rst @@ -0,0 +1,7 @@ +ridgeplot._hist +============== + +Histogram utilities. + +.. automodule:: ridgeplot._hist + :private-members: diff --git a/docs/api/internal/obj.rst b/docs/api/internal/obj.rst new file mode 100644 index 00000000..1b33486f --- /dev/null +++ b/docs/api/internal/obj.rst @@ -0,0 +1,10 @@ +ridgeplot._obj +================ + +Object-oriented interfaces. + +.. toctree:: + :maxdepth: 1 + :glob: + + _obj/* From 3457e529b9bb5b18f3a31bc06603759f3054a807 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Thu, 5 Dec 2024 00:07:49 +0000 Subject: [PATCH 18/21] Add `versionadded` directives --- src/ridgeplot/_ridgeplot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ridgeplot/_ridgeplot.py b/src/ridgeplot/_ridgeplot.py index ca39e3b4..02fbf97c 100644 --- a/src/ridgeplot/_ridgeplot.py +++ b/src/ridgeplot/_ridgeplot.py @@ -193,6 +193,8 @@ def ridgeplot( (``trace_type='area'``) unless histogram binning is used, in which case the traces will be displayed as bar plots (``trace_type='bar'``). + .. versionadded:: 0.3.0 + labels : LabelsArray or ShallowLabelsArray or None A list of string labels for each trace. If not specified (default), the labels will be automatically generated as ``"Trace {n}"``, where ``n`` @@ -241,6 +243,8 @@ def ridgeplot( The number of bins to use when applying histogram binning. If not specified (default), KDE will be used instead of histogram binning. + .. versionadded:: 0.3.0 + sample_weights : SampleWeightsArray or ShallowSampleWeightsArray or SampleWeights or None An (optional) array of KDE weights corresponding to each sample. The weights should have the same shape as the samples array. If not From 10ca2e197515804ee22ad0f87ad46aeaabc15c47 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Thu, 5 Dec 2024 00:08:10 +0000 Subject: [PATCH 19/21] Remove references to deprecated color utilities --- docs/api/index.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/api/index.rst b/docs/api/index.rst index 1708e41f..0421db38 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -9,16 +9,6 @@ API Reference ridgeplot.ridgeplot -Color utilities -=============== - -.. autosummary:: - :toctree: public/ - :nosignatures: - - ridgeplot.list_all_colorscale_names - - Data loading utilities ====================== From 66d25e06d5a9e2fe75b006dd57c918e019f5cf82 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Thu, 5 Dec 2024 00:35:58 +0000 Subject: [PATCH 20/21] Update changelog.md --- docs/reference/changelog.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/reference/changelog.md b/docs/reference/changelog.md index d98d21c7..99380c5f 100644 --- a/docs/reference/changelog.md +++ b/docs/reference/changelog.md @@ -5,14 +5,23 @@ This document outlines the list of changes to ridgeplot between each release. Fo Unreleased changes ------------------ +### Features + +- Add support for histogram and bar traces ({gh-pr}`287`) + ### Documentation - Small improvements to `ridgeplot()`'s docstring ({gh-pr}`284`) +- Misc improvements to the API docs and the getting-started and contributing guides ({gh-pr}`287`) ### Internal - Small improvements to type hints and annotations ({gh-pr}`284`) -- Improve type annotations and switch from mypy to stricter pyright settings ({gh-pr}`287`) +- Introduce an internal `ridgeplot._obj` package to hold object-oriented interfaces ({gh-pr}`287`) + +### CI/CD + +- Improve type annotations and switch from mypy to pyright with stricter settings ({gh-pr}`287`) - Switch from `black` to the new `ruff` formatter ({gh-pr}`287`) --- From 3e46c04a27876e1bc982ce0405aae0c5753b4e27 Mon Sep 17 00:00:00 2001 From: Tomas Pereira de Vasconcelos Date: Thu, 5 Dec 2024 00:36:20 +0000 Subject: [PATCH 21/21] Document bar trace nuances --- src/ridgeplot/_figure_factory.py | 11 +++++++++-- src/ridgeplot/_obj/traces/bar.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ridgeplot/_figure_factory.py b/src/ridgeplot/_figure_factory.py index 532ccdfc..dac0822d 100644 --- a/src/ridgeplot/_figure_factory.py +++ b/src/ridgeplot/_figure_factory.py @@ -104,8 +104,15 @@ def update_layout( showticklabels=True, **axes_common, ) - # TODO: Review default layout for bar traces... - fig.update_layout(barmode="stack", bargap=0, bargroupgap=0) + # Settings for bar/histogram traces: + fig.update_layout( + # barmode can be either 'stack' or 'relative' + barmode="stack", + # bargap and bargroupgap should be set + # to 0 to avoid gaps between bars + bargap=0, + bargroupgap=0, + ) return fig diff --git a/src/ridgeplot/_obj/traces/bar.py b/src/ridgeplot/_obj/traces/bar.py index 7c7ef00f..ba621e03 100644 --- a/src/ridgeplot/_obj/traces/bar.py +++ b/src/ridgeplot/_obj/traces/bar.py @@ -41,7 +41,7 @@ def draw(self, fig: go.Figure, coloring_ctx: ColoringContext) -> go.Figure: name=self.label, base=self.y_base, marker_line_width=self.line_width, - # width=1, # TODO: do we need to specify the bar width? + width=None, # Plotly automatically picks the right width **self._get_coloring_kwargs(ctx=coloring_ctx), # Hover information customdata=[[y_i] for y_i in self.y],