From 0da30f9421c4c240822d3e91ce29f6dccf9a41f3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:05:21 +0100 Subject: [PATCH 1/8] Add barlike option to area plot Summary of changes: 1. plotting.py: Added barlike parameter to area() with helper _apply_barlike_style() 2. accessor.py: Updated both DataArrayPlotlyAccessor.area() and DatasetPlotlyAccessor.area() 3. Tests: Added 3 tests for barlike (basic, trace styling, animation frames) 4. Notebook: Created docs/examples/barlike.ipynb demonstrating the feature Usage: xpx(da).area(barlike=True) # Looks like bars, renders faster --- docs/examples/barlike.ipynb | 193 ++++++++++++++++++++++++++++++++++++ tests/test_accessor.py | 27 +++++ xarray_plotly/accessor.py | 6 ++ xarray_plotly/plotting.py | 25 ++++- 4 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 docs/examples/barlike.ipynb diff --git a/docs/examples/barlike.ipynb b/docs/examples/barlike.ipynb new file mode 100644 index 0000000..2979e65 --- /dev/null +++ b/docs/examples/barlike.ipynb @@ -0,0 +1,193 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Bar-like Area Charts\n", + "\n", + "Area charts can be styled to look like bar charts using `barlike=True`. This renders much faster than actual bar charts for large datasets because it uses a single polygon per trace instead of individual rectangles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import xarray as xr\n", + "\n", + "from xarray_plotly import config, xpx\n", + "\n", + "config.notebook()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Sample data\n", + "da = xr.DataArray(\n", + " np.random.rand(12, 3) * 100,\n", + " dims=[\"month\", \"category\"],\n", + " coords={\n", + " \"month\": [\n", + " \"Jan\",\n", + " \"Feb\",\n", + " \"Mar\",\n", + " \"Apr\",\n", + " \"May\",\n", + " \"Jun\",\n", + " \"Jul\",\n", + " \"Aug\",\n", + " \"Sep\",\n", + " \"Oct\",\n", + " \"Nov\",\n", + " \"Dec\",\n", + " ],\n", + " \"category\": [\"A\", \"B\", \"C\"],\n", + " },\n", + " name=\"sales\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Regular Area Chart" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "xpx(da).area()" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## Bar-like Area Chart\n", + "\n", + "With `barlike=True`, the area chart uses stepped lines and removes outlines to look like bars:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "xpx(da).area(barlike=True)" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## Comparison with Actual Bar Chart" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "xpx(da).bar()" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "## With Animation\n", + "\n", + "The `barlike` styling also applies to animation frames:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "# Data with animation dimension\n", + "da_anim = xr.DataArray(\n", + " np.random.rand(12, 3, 5) * 100,\n", + " dims=[\"month\", \"category\", \"year\"],\n", + " coords={\n", + " \"month\": [\n", + " \"Jan\",\n", + " \"Feb\",\n", + " \"Mar\",\n", + " \"Apr\",\n", + " \"May\",\n", + " \"Jun\",\n", + " \"Jul\",\n", + " \"Aug\",\n", + " \"Sep\",\n", + " \"Oct\",\n", + " \"Nov\",\n", + " \"Dec\",\n", + " ],\n", + " \"category\": [\"A\", \"B\", \"C\"],\n", + " \"year\": [2020, 2021, 2022, 2023, 2024],\n", + " },\n", + " name=\"sales\",\n", + ")\n", + "\n", + "xpx(da_anim).area(animation_frame=\"year\", barlike=True)" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## When to Use\n", + "\n", + "Use `barlike=True` when:\n", + "- You have large datasets where bar charts render slowly\n", + "- You want stacked bar-like visualization with better performance\n", + "- You need animations with many frames\n", + "\n", + "Use actual `bar()` when:\n", + "- You need precise bar positioning (grouped bars)\n", + "- You need pattern fills\n", + "- Visual accuracy is more important than performance" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/test_accessor.py b/tests/test_accessor.py index 9786112..3bd17d9 100644 --- a/tests/test_accessor.py +++ b/tests/test_accessor.py @@ -146,6 +146,33 @@ def test_area_returns_figure(self) -> None: fig = self.da_2d.plotly.area() assert isinstance(fig, go.Figure) + def test_area_barlike_returns_figure(self) -> None: + """Test that area(barlike=True) returns a Plotly Figure.""" + fig = self.da_2d.plotly.area(barlike=True) + assert isinstance(fig, go.Figure) + + def test_area_barlike_trace_styling(self) -> None: + """Test that barlike applies correct trace styling.""" + fig = self.da_2d.plotly.area(barlike=True) + for trace in fig.data: + assert trace.line.width == 0 + assert trace.line.shape == "hv" + assert trace.fillcolor is not None + + def test_area_barlike_animation_frames(self) -> None: + """Test that barlike styling applies to animation frames.""" + da = xr.DataArray( + np.random.rand(5, 3, 4), + dims=["time", "city", "year"], + ) + fig = da.plotly.area(animation_frame="year", barlike=True) + assert len(fig.frames) > 0 + for frame in fig.frames: + for trace in frame.data: + assert trace.line.width == 0 + assert trace.line.shape == "hv" + assert trace.fillcolor is not None + def test_scatter_returns_figure(self) -> None: """Test that scatter() returns a Plotly Figure.""" fig = self.da_2d.plotly.scatter() diff --git a/xarray_plotly/accessor.py b/xarray_plotly/accessor.py index ff45d26..b02b6a5 100644 --- a/xarray_plotly/accessor.py +++ b/xarray_plotly/accessor.py @@ -131,6 +131,7 @@ def area( facet_col: SlotValue = auto, facet_row: SlotValue = auto, animation_frame: SlotValue = auto, + barlike: bool = False, **px_kwargs: Any, ) -> go.Figure: """Create an interactive stacked area chart. @@ -144,6 +145,7 @@ def area( facet_col: Dimension for subplot columns. Default: fourth dimension. facet_row: Dimension for subplot rows. Default: fifth dimension. animation_frame: Dimension for animation. Default: sixth dimension. + barlike: If True, style as bar chart (stepped, no outline). Faster than bars. **px_kwargs: Additional arguments passed to `plotly.express.area()`. Returns: @@ -157,6 +159,7 @@ def area( facet_col=facet_col, facet_row=facet_row, animation_frame=animation_frame, + barlike=barlike, **px_kwargs, ) @@ -472,6 +475,7 @@ def area( facet_col: SlotValue = auto, facet_row: SlotValue = auto, animation_frame: SlotValue = auto, + barlike: bool = False, **px_kwargs: Any, ) -> go.Figure: """Create an interactive stacked area chart. @@ -484,6 +488,7 @@ def area( facet_col: Dimension for subplot columns. facet_row: Dimension for subplot rows. animation_frame: Dimension for animation. + barlike: If True, style as bar chart (stepped, no outline). Faster than bars. **px_kwargs: Additional arguments passed to `plotly.express.area()`. Returns: @@ -498,6 +503,7 @@ def area( facet_col=facet_col, facet_row=facet_row, animation_frame=animation_frame, + barlike=barlike, **px_kwargs, ) diff --git a/xarray_plotly/plotting.py b/xarray_plotly/plotting.py index 638de94..d4247c6 100644 --- a/xarray_plotly/plotting.py +++ b/xarray_plotly/plotting.py @@ -167,6 +167,14 @@ def bar( ) +def _apply_barlike_style(traces: tuple) -> None: + """Apply bar-like styling to area traces (in-place).""" + for trace in traces: + color = trace.line.color + trace.fillcolor = color + trace.line = {"width": 0, "color": color, "shape": "hv"} + + def area( darray: DataArray, *, @@ -176,6 +184,7 @@ def area( facet_col: SlotValue = auto, facet_row: SlotValue = auto, animation_frame: SlotValue = auto, + barlike: bool = False, **px_kwargs: Any, ) -> go.Figure: """ @@ -200,6 +209,10 @@ def area( Dimension for subplot rows. Default: fifth dimension. animation_frame Dimension for animation. Default: sixth dimension. + barlike + If True, style the area chart to look like a bar chart using stepped + lines with no outline. Renders faster than bar charts for large data. + Default: False. **px_kwargs Additional arguments passed to `plotly.express.area()`. @@ -218,11 +231,14 @@ def area( animation_frame=animation_frame, ) + if barlike: + px_kwargs.setdefault("line_shape", "hv") + df = to_dataframe(darray) value_col = get_value_col(darray) labels = {**build_labels(darray, slots, value_col), **px_kwargs.pop("labels", {})} - return px.area( + fig = px.area( df, x=slots.get("x"), y=value_col, @@ -235,6 +251,13 @@ def area( **px_kwargs, ) + if barlike: + _apply_barlike_style(fig.data) + for frame in fig.frames: + _apply_barlike_style(frame.data) + + return fig + def box( darray: DataArray, From 9e2d00a3e77e35dfda266454abdee0e735b715a0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:14:39 +0100 Subject: [PATCH 2/8] Add barlike option to area plot Summary of changes: 1. plotting.py: Added barlike parameter to area() with helper _apply_barlike_style() 2. accessor.py: Updated both DataArrayPlotlyAccessor.area() and DatasetPlotlyAccessor.area() 3. Tests: Added 3 tests for barlike (basic, trace styling, animation frames) 4. Notebook: Created docs/examples/barlike.ipynb demonstrating the feature Usage: xpx(da).area(barlike=True) # Looks like bars, renders faster --- .../{barlike.ipynb => fast_bar.ipynb} | 36 +++----- tests/test_accessor.py | 18 ++-- xarray_plotly/accessor.py | 77 ++++++++++++++-- xarray_plotly/config.py | 1 + xarray_plotly/plotting.py | 92 +++++++++++++++---- 5 files changed, 172 insertions(+), 52 deletions(-) rename docs/examples/{barlike.ipynb => fast_bar.ipynb} (76%) diff --git a/docs/examples/barlike.ipynb b/docs/examples/fast_bar.ipynb similarity index 76% rename from docs/examples/barlike.ipynb rename to docs/examples/fast_bar.ipynb index 2979e65..6537730 100644 --- a/docs/examples/barlike.ipynb +++ b/docs/examples/fast_bar.ipynb @@ -5,9 +5,9 @@ "id": "0", "metadata": {}, "source": [ - "# Bar-like Area Charts\n", + "# Fast Bar Charts\n", "\n", - "Area charts can be styled to look like bar charts using `barlike=True`. This renders much faster than actual bar charts for large datasets because it uses a single polygon per trace instead of individual rectangles." + "The `fast_bar()` method creates bar-like visualizations using stacked areas. This renders much faster than actual bar charts for large datasets because it uses a single polygon per trace instead of individual rectangles." ] }, { @@ -62,7 +62,7 @@ "id": "3", "metadata": {}, "source": [ - "## Regular Area Chart" + "## Fast Bar Chart" ] }, { @@ -72,7 +72,7 @@ "metadata": {}, "outputs": [], "source": [ - "xpx(da).area()" + "xpx(da).fast_bar()" ] }, { @@ -80,9 +80,7 @@ "id": "5", "metadata": {}, "source": [ - "## Bar-like Area Chart\n", - "\n", - "With `barlike=True`, the area chart uses stepped lines and removes outlines to look like bars:" + "## Comparison with Regular Bar Chart" ] }, { @@ -92,7 +90,7 @@ "metadata": {}, "outputs": [], "source": [ - "xpx(da).area(barlike=True)" + "xpx(da).bar()" ] }, { @@ -100,7 +98,7 @@ "id": "7", "metadata": {}, "source": [ - "## Comparison with Actual Bar Chart" + "## Comparison with Area Chart" ] }, { @@ -110,7 +108,7 @@ "metadata": {}, "outputs": [], "source": [ - "xpx(da).bar()" + "xpx(da).area()" ] }, { @@ -120,7 +118,7 @@ "source": [ "## With Animation\n", "\n", - "The `barlike` styling also applies to animation frames:" + "The `fast_bar` styling also applies to animation frames:" ] }, { @@ -155,7 +153,7 @@ " name=\"sales\",\n", ")\n", "\n", - "xpx(da_anim).area(animation_frame=\"year\", barlike=True)" + "xpx(da_anim).fast_bar(animation_frame=\"year\")" ] }, { @@ -165,15 +163,11 @@ "source": [ "## When to Use\n", "\n", - "Use `barlike=True` when:\n", - "- You have large datasets where bar charts render slowly\n", - "- You want stacked bar-like visualization with better performance\n", - "- You need animations with many frames\n", - "\n", - "Use actual `bar()` when:\n", - "- You need precise bar positioning (grouped bars)\n", - "- You need pattern fills\n", - "- Visual accuracy is more important than performance" + "| Method | Use when... |\n", + "|--------|-------------|\n", + "| `fast_bar()` | Large datasets, animations with many frames, performance matters |\n", + "| `bar()` | Need precise bar positioning, grouped bars, pattern fills |\n", + "| `area()` | Want smooth continuous fills, standard area chart appearance |" ] } ], diff --git a/tests/test_accessor.py b/tests/test_accessor.py index 3bd17d9..74b89bc 100644 --- a/tests/test_accessor.py +++ b/tests/test_accessor.py @@ -146,26 +146,26 @@ def test_area_returns_figure(self) -> None: fig = self.da_2d.plotly.area() assert isinstance(fig, go.Figure) - def test_area_barlike_returns_figure(self) -> None: - """Test that area(barlike=True) returns a Plotly Figure.""" - fig = self.da_2d.plotly.area(barlike=True) + def test_fast_bar_returns_figure(self) -> None: + """Test that fast_bar() returns a Plotly Figure.""" + fig = self.da_2d.plotly.fast_bar() assert isinstance(fig, go.Figure) - def test_area_barlike_trace_styling(self) -> None: - """Test that barlike applies correct trace styling.""" - fig = self.da_2d.plotly.area(barlike=True) + def test_fast_bar_trace_styling(self) -> None: + """Test that fast_bar applies correct trace styling.""" + fig = self.da_2d.plotly.fast_bar() for trace in fig.data: assert trace.line.width == 0 assert trace.line.shape == "hv" assert trace.fillcolor is not None - def test_area_barlike_animation_frames(self) -> None: - """Test that barlike styling applies to animation frames.""" + def test_fast_bar_animation_frames(self) -> None: + """Test that fast_bar styling applies to animation frames.""" da = xr.DataArray( np.random.rand(5, 3, 4), dims=["time", "city", "year"], ) - fig = da.plotly.area(animation_frame="year", barlike=True) + fig = da.plotly.fast_bar(animation_frame="year") assert len(fig.frames) > 0 for frame in fig.frames: for trace in frame.data: diff --git a/xarray_plotly/accessor.py b/xarray_plotly/accessor.py index b02b6a5..80b604c 100644 --- a/xarray_plotly/accessor.py +++ b/xarray_plotly/accessor.py @@ -131,7 +131,6 @@ def area( facet_col: SlotValue = auto, facet_row: SlotValue = auto, animation_frame: SlotValue = auto, - barlike: bool = False, **px_kwargs: Any, ) -> go.Figure: """Create an interactive stacked area chart. @@ -145,7 +144,6 @@ def area( facet_col: Dimension for subplot columns. Default: fourth dimension. facet_row: Dimension for subplot rows. Default: fifth dimension. animation_frame: Dimension for animation. Default: sixth dimension. - barlike: If True, style as bar chart (stepped, no outline). Faster than bars. **px_kwargs: Additional arguments passed to `plotly.express.area()`. Returns: @@ -159,7 +157,41 @@ def area( facet_col=facet_col, facet_row=facet_row, animation_frame=animation_frame, - barlike=barlike, + **px_kwargs, + ) + + def fast_bar( + self, + *, + x: SlotValue = auto, + color: SlotValue = auto, + facet_col: SlotValue = auto, + facet_row: SlotValue = auto, + animation_frame: SlotValue = auto, + **px_kwargs: Any, + ) -> go.Figure: + """Create a bar-like chart using stacked areas for better performance. + + Slot order: x -> color -> facet_col -> facet_row -> animation_frame + + Args: + x: Dimension for x-axis. Default: first dimension. + color: Dimension for color/stacking. Default: second dimension. + facet_col: Dimension for subplot columns. Default: third dimension. + facet_row: Dimension for subplot rows. Default: fourth dimension. + animation_frame: Dimension for animation. Default: fifth dimension. + **px_kwargs: Additional arguments passed to `plotly.express.area()`. + + Returns: + Interactive Plotly Figure. + """ + return plotting.fast_bar( + self._da, + x=x, + color=color, + facet_col=facet_col, + facet_row=facet_row, + animation_frame=animation_frame, **px_kwargs, ) @@ -475,7 +507,6 @@ def area( facet_col: SlotValue = auto, facet_row: SlotValue = auto, animation_frame: SlotValue = auto, - barlike: bool = False, **px_kwargs: Any, ) -> go.Figure: """Create an interactive stacked area chart. @@ -488,7 +519,6 @@ def area( facet_col: Dimension for subplot columns. facet_row: Dimension for subplot rows. animation_frame: Dimension for animation. - barlike: If True, style as bar chart (stepped, no outline). Faster than bars. **px_kwargs: Additional arguments passed to `plotly.express.area()`. Returns: @@ -503,7 +533,42 @@ def area( facet_col=facet_col, facet_row=facet_row, animation_frame=animation_frame, - barlike=barlike, + **px_kwargs, + ) + + def fast_bar( + self, + var: str | None = None, + *, + x: SlotValue = auto, + color: SlotValue = auto, + facet_col: SlotValue = auto, + facet_row: SlotValue = auto, + animation_frame: SlotValue = auto, + **px_kwargs: Any, + ) -> go.Figure: + """Create a bar-like chart using stacked areas for better performance. + + Args: + var: Variable to plot. If None, plots all variables with "variable" dimension. + x: Dimension for x-axis. + color: Dimension for color/stacking. + facet_col: Dimension for subplot columns. + facet_row: Dimension for subplot rows. + animation_frame: Dimension for animation. + **px_kwargs: Additional arguments passed to `plotly.express.area()`. + + Returns: + Interactive Plotly Figure. + """ + da = self._get_dataarray(var) + return plotting.fast_bar( + da, + x=x, + color=color, + facet_col=facet_col, + facet_row=facet_row, + animation_frame=animation_frame, **px_kwargs, ) diff --git a/xarray_plotly/config.py b/xarray_plotly/config.py index e28cab0..0f704a3 100644 --- a/xarray_plotly/config.py +++ b/xarray_plotly/config.py @@ -26,6 +26,7 @@ "animation_frame", ), "bar": ("x", "color", "pattern_shape", "facet_col", "facet_row", "animation_frame"), + "fast_bar": ("x", "color", "facet_col", "facet_row", "animation_frame"), "area": ( "x", "color", diff --git a/xarray_plotly/plotting.py b/xarray_plotly/plotting.py index d4247c6..cdf7d1d 100644 --- a/xarray_plotly/plotting.py +++ b/xarray_plotly/plotting.py @@ -175,6 +175,81 @@ def _apply_barlike_style(traces: tuple) -> None: trace.line = {"width": 0, "color": color, "shape": "hv"} +def fast_bar( + darray: DataArray, + *, + x: SlotValue = auto, + color: SlotValue = auto, + facet_col: SlotValue = auto, + facet_row: SlotValue = auto, + animation_frame: SlotValue = auto, + **px_kwargs: Any, +) -> go.Figure: + """ + Create a bar-like chart using stacked areas for better performance. + + Uses `px.area` with stepped lines and no outline to create a bar-like + appearance. Renders faster than `bar()` for large datasets because it + uses a single polygon per trace instead of individual rectangles. + + The y-axis shows DataArray values. Dimensions fill slots in order: + x -> color -> facet_col -> facet_row -> animation_frame + + Parameters + ---------- + darray + The DataArray to plot. + x + Dimension for x-axis. Default: first dimension. + color + Dimension for color/stacking. Default: second dimension. + facet_col + Dimension for subplot columns. Default: third dimension. + facet_row + Dimension for subplot rows. Default: fourth dimension. + animation_frame + Dimension for animation. Default: fifth dimension. + **px_kwargs + Additional arguments passed to `plotly.express.area()`. + + Returns + ------- + plotly.graph_objects.Figure + """ + slots = assign_slots( + list(darray.dims), + "fast_bar", + x=x, + color=color, + facet_col=facet_col, + facet_row=facet_row, + animation_frame=animation_frame, + ) + + df = to_dataframe(darray) + value_col = get_value_col(darray) + labels = {**build_labels(darray, slots, value_col), **px_kwargs.pop("labels", {})} + + fig = px.area( + df, + x=slots.get("x"), + y=value_col, + color=slots.get("color"), + facet_col=slots.get("facet_col"), + facet_row=slots.get("facet_row"), + animation_frame=slots.get("animation_frame"), + line_shape="hv", + labels=labels, + **px_kwargs, + ) + + _apply_barlike_style(fig.data) + for frame in fig.frames: + _apply_barlike_style(frame.data) + + return fig + + def area( darray: DataArray, *, @@ -184,7 +259,6 @@ def area( facet_col: SlotValue = auto, facet_row: SlotValue = auto, animation_frame: SlotValue = auto, - barlike: bool = False, **px_kwargs: Any, ) -> go.Figure: """ @@ -209,10 +283,6 @@ def area( Dimension for subplot rows. Default: fifth dimension. animation_frame Dimension for animation. Default: sixth dimension. - barlike - If True, style the area chart to look like a bar chart using stepped - lines with no outline. Renders faster than bar charts for large data. - Default: False. **px_kwargs Additional arguments passed to `plotly.express.area()`. @@ -231,14 +301,11 @@ def area( animation_frame=animation_frame, ) - if barlike: - px_kwargs.setdefault("line_shape", "hv") - df = to_dataframe(darray) value_col = get_value_col(darray) labels = {**build_labels(darray, slots, value_col), **px_kwargs.pop("labels", {})} - fig = px.area( + return px.area( df, x=slots.get("x"), y=value_col, @@ -251,13 +318,6 @@ def area( **px_kwargs, ) - if barlike: - _apply_barlike_style(fig.data) - for frame in fig.frames: - _apply_barlike_style(frame.data) - - return fig - def box( darray: DataArray, From 9606c0961524bd3a58543ef96beebd229fb82353 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:26:31 +0100 Subject: [PATCH 3/8] =?UTF-8?q?=E2=8F=BA=20Done.=20The=20fast=5Fbar()=20me?= =?UTF-8?q?thod=20now=20handles=20all=20cases:=20=20=20=E2=94=8C=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=AC?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=90=20=20=20=E2=94=82=20=20=20=20=20Dat?= =?UTF-8?q?a=20=20=20=20=20=E2=94=82=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20Behavior=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=E2=94=82=20=20=20=E2=94=9C=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=BC=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=A4?= =?UTF-8?q?=20=20=20=E2=94=82=20All=20positive=20=E2=94=82=20Stacked=20(st?= =?UTF-8?q?ackgroup=3D1)=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=E2=94=82=20=20=20=E2=94=9C=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=BC=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=A4?= =?UTF-8?q?=20=20=20=E2=94=82=20All=20negative=20=E2=94=82=20Stacked=20(st?= =?UTF-8?q?ackgroup=3D1)=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=E2=94=82=20=20=20=E2=94=9C=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=BC=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=A4?= =?UTF-8?q?=20=20=20=E2=94=82=20Mixed=20+/-=20=20=20=20=E2=94=82=20No=20st?= =?UTF-8?q?acking,=20fill=20to=20zero=20(tozeroy)=20=E2=94=82=20=20=20?= =?UTF-8?q?=E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=B4=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80?= =?UTF-8?q?=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98=20=20=20Changes?= =?UTF-8?q?=20made:=20=20=20-=20plotting.py:=20Added=20=5Fhas=5Fmixed=5Fsi?= =?UTF-8?q?gns()=20detection,=20updated=20=5Fapply=5Fbarlike=5Fstyle()=20t?= =?UTF-8?q?o=20handle=20mixed=20signs=20=20=20-=20test=5Faccessor.py:=20Ad?= =?UTF-8?q?ded=202=20new=20tests=20for=20mixed/same-sign=20behavior=20=20?= =?UTF-8?q?=20-=20fast=5Fbar.ipynb:=20Updated=20with=20negative=20and=20mi?= =?UTF-8?q?xed=20value=20examples,=20documented=20the=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note: For mixed data, series will overlap since stacking doesn't work well with mixed signs. The notebook documents that bar() should be used if proper stacking of mixed data is needed. --- docs/examples/fast_bar.ipynb | 97 +++++++++++++++++++++++++++++++++++- mkdocs.yml | 3 ++ tests/test_accessor.py | 21 ++++++++ xarray_plotly/plotting.py | 34 +++++++++++-- 4 files changed, 149 insertions(+), 6 deletions(-) diff --git a/docs/examples/fast_bar.ipynb b/docs/examples/fast_bar.ipynb index 6537730..f4acacc 100644 --- a/docs/examples/fast_bar.ipynb +++ b/docs/examples/fast_bar.ipynb @@ -165,10 +165,103 @@ "\n", "| Method | Use when... |\n", "|--------|-------------|\n", - "| `fast_bar()` | Large datasets, animations with many frames, performance matters |\n", - "| `bar()` | Need precise bar positioning, grouped bars, pattern fills |\n", + "| `fast_bar()` | Large datasets, animations with many frames, same-sign data (all positive or all negative) |\n", + "| `bar()` | Need precise bar positioning, grouped bars, pattern fills, or mixed positive/negative stacking |\n", "| `area()` | Want smooth continuous fills, standard area chart appearance |" ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## Negative Values\n", + "\n", + "Testing with all negative values:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "# All negative values\n", + "da_negative = xr.DataArray(\n", + " -np.random.rand(6, 3) * 100,\n", + " dims=[\"month\", \"category\"],\n", + " coords={\n", + " \"month\": [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"],\n", + " \"category\": [\"A\", \"B\", \"C\"],\n", + " },\n", + " name=\"loss\",\n", + ")\n", + "\n", + "xpx(da_negative).fast_bar()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "# Comparison: regular bar with negative values\n", + "xpx(da_negative).bar()" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "## Mixed Values (Positive and Negative)\n", + "\n", + "For mixed positive/negative data, `fast_bar()` automatically disables stacking and fills each series to zero independently. This prevents visual artifacts from stacked areas but means series will overlap." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "# Mixed positive and negative values\n", + "da_mixed = xr.DataArray(\n", + " np.array(\n", + " [\n", + " [50, -30, 20],\n", + " [-40, 60, -10],\n", + " [30, -50, 40],\n", + " [-20, 70, -30],\n", + " [60, -40, 50],\n", + " [-30, 80, -20],\n", + " ]\n", + " ),\n", + " dims=[\"month\", \"category\"],\n", + " coords={\n", + " \"month\": [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"],\n", + " \"category\": [\"A\", \"B\", \"C\"],\n", + " },\n", + " name=\"profit_loss\",\n", + ")\n", + "\n", + "xpx(da_mixed).fast_bar()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "# Comparison: regular bar with mixed values\n", + "xpx(da_mixed).bar()" + ] } ], "metadata": { diff --git a/mkdocs.yml b/mkdocs.yml index 1a1f0f1..5a38157 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,4 +73,7 @@ nav: - Dimensions & Facets: examples/dimensions.ipynb - Plotly Express Options: examples/kwargs.ipynb - Figure Customization: examples/figure.ipynb + - Combining Figures: examples/combining.ipynb + - Figure Manipulation: examples/manipulation.ipynb + - Fast Bar Charts: examples/fast_bar.ipynb - API Reference: api.md diff --git a/tests/test_accessor.py b/tests/test_accessor.py index 74b89bc..7b6ed33 100644 --- a/tests/test_accessor.py +++ b/tests/test_accessor.py @@ -173,6 +173,27 @@ def test_fast_bar_animation_frames(self) -> None: assert trace.line.shape == "hv" assert trace.fillcolor is not None + def test_fast_bar_mixed_signs_no_stacking(self) -> None: + """Test that fast_bar disables stacking for mixed positive/negative data.""" + da = xr.DataArray( + np.array([[50, -30], [-40, 60]]), + dims=["time", "category"], + ) + fig = da.plotly.fast_bar() + for trace in fig.data: + assert trace.stackgroup is None + assert trace.fill == "tozeroy" + + def test_fast_bar_same_sign_stacks(self) -> None: + """Test that fast_bar uses stacking for same-sign data.""" + da = xr.DataArray( + np.random.rand(5, 3) * 100, + dims=["time", "category"], + ) + fig = da.plotly.fast_bar() + for trace in fig.data: + assert trace.stackgroup is not None + def test_scatter_returns_figure(self) -> None: """Test that scatter() returns a Plotly Figure.""" fig = self.da_2d.plotly.scatter() diff --git a/xarray_plotly/plotting.py b/xarray_plotly/plotting.py index cdf7d1d..b4feaaa 100644 --- a/xarray_plotly/plotting.py +++ b/xarray_plotly/plotting.py @@ -167,12 +167,31 @@ def bar( ) -def _apply_barlike_style(traces: tuple) -> None: - """Apply bar-like styling to area traces (in-place).""" +def _apply_barlike_style(traces: tuple, mixed_signs: bool = False) -> None: + """Apply bar-like styling to area traces (in-place). + + Parameters + ---------- + traces + The traces to style. + mixed_signs + If True, disable stacking and fill to zero for mixed positive/negative data. + """ for trace in traces: color = trace.line.color trace.fillcolor = color trace.line = {"width": 0, "color": color, "shape": "hv"} + if mixed_signs: + trace.stackgroup = None + trace.fill = "tozeroy" + + +def _has_mixed_signs(values: np.ndarray) -> bool: + """Check if data contains both positive and negative values.""" + finite = values[np.isfinite(values)] + if len(finite) == 0: + return False + return bool(np.any(finite > 0) and np.any(finite < 0)) def fast_bar( @@ -195,6 +214,10 @@ def fast_bar( The y-axis shows DataArray values. Dimensions fill slots in order: x -> color -> facet_col -> facet_row -> animation_frame + Note: For mixed positive/negative data, stacking is disabled and each + series fills independently to zero. For best stacking behavior with + mixed data, use `bar()` instead. + Parameters ---------- darray @@ -226,6 +249,9 @@ def fast_bar( animation_frame=animation_frame, ) + # Check for mixed positive/negative values + mixed_signs = _has_mixed_signs(darray.values) + df = to_dataframe(darray) value_col = get_value_col(darray) labels = {**build_labels(darray, slots, value_col), **px_kwargs.pop("labels", {})} @@ -243,9 +269,9 @@ def fast_bar( **px_kwargs, ) - _apply_barlike_style(fig.data) + _apply_barlike_style(fig.data, mixed_signs) for frame in fig.frames: - _apply_barlike_style(frame.data) + _apply_barlike_style(frame.data, mixed_signs) return fig From 548041167614903f72299b383f1743a1748113b2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:37:08 +0100 Subject: [PATCH 4/8] Summary of changes: fast_bar() now properly handles mixed positive/negative data: - Same-sign data (all positive or all negative): Uses single stackgroup for normal stacking - Mixed-sign data: Splits each trace into positive and negative parts with separate stackgroups - Positives stack upward from zero (stackgroup='positive') - Negatives stack downward from zero (stackgroup='negative') - This matches bar chart behavior with barmode='relative' Implementation: - Added _split_traces_by_sign() helper that creates separate traces for positive and negative values - Updated fast_bar() to detect mixed signs and apply the split - Animation frames are also handled correctly --- docs/examples/fast_bar.ipynb | 6 +-- tests/test_accessor.py | 11 ++-- xarray_plotly/plotting.py | 98 ++++++++++++++++++++++++++++-------- 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/docs/examples/fast_bar.ipynb b/docs/examples/fast_bar.ipynb index f4acacc..356ea3f 100644 --- a/docs/examples/fast_bar.ipynb +++ b/docs/examples/fast_bar.ipynb @@ -165,8 +165,8 @@ "\n", "| Method | Use when... |\n", "|--------|-------------|\n", - "| `fast_bar()` | Large datasets, animations with many frames, same-sign data (all positive or all negative) |\n", - "| `bar()` | Need precise bar positioning, grouped bars, pattern fills, or mixed positive/negative stacking |\n", + "| `fast_bar()` | Large datasets, animations with many frames, performance matters |\n", + "| `bar()` | Need precise bar positioning, grouped bars, pattern fills |\n", "| `area()` | Want smooth continuous fills, standard area chart appearance |" ] }, @@ -219,7 +219,7 @@ "source": [ "## Mixed Values (Positive and Negative)\n", "\n", - "For mixed positive/negative data, `fast_bar()` automatically disables stacking and fills each series to zero independently. This prevents visual artifacts from stacked areas but means series will overlap." + "For mixed positive/negative data, `fast_bar()` automatically splits traces into separate stackgroups: positives stack upward from zero, negatives stack downward. This matches bar chart stacking behavior." ] }, { diff --git a/tests/test_accessor.py b/tests/test_accessor.py index 7b6ed33..4e71e96 100644 --- a/tests/test_accessor.py +++ b/tests/test_accessor.py @@ -173,16 +173,17 @@ def test_fast_bar_animation_frames(self) -> None: assert trace.line.shape == "hv" assert trace.fillcolor is not None - def test_fast_bar_mixed_signs_no_stacking(self) -> None: - """Test that fast_bar disables stacking for mixed positive/negative data.""" + def test_fast_bar_mixed_signs_separate_stacks(self) -> None: + """Test that fast_bar uses separate stackgroups for mixed positive/negative data.""" da = xr.DataArray( np.array([[50, -30], [-40, 60]]), dims=["time", "category"], ) fig = da.plotly.fast_bar() - for trace in fig.data: - assert trace.stackgroup is None - assert trace.fill == "tozeroy" + # Should have separate positive and negative stackgroups + stackgroups = {trace.stackgroup for trace in fig.data} + assert "positive" in stackgroups + assert "negative" in stackgroups def test_fast_bar_same_sign_stacks(self) -> None: """Test that fast_bar uses stacking for same-sign data.""" diff --git a/xarray_plotly/plotting.py b/xarray_plotly/plotting.py index b4feaaa..9cf8bb1 100644 --- a/xarray_plotly/plotting.py +++ b/xarray_plotly/plotting.py @@ -167,23 +167,12 @@ def bar( ) -def _apply_barlike_style(traces: tuple, mixed_signs: bool = False) -> None: - """Apply bar-like styling to area traces (in-place). - - Parameters - ---------- - traces - The traces to style. - mixed_signs - If True, disable stacking and fill to zero for mixed positive/negative data. - """ +def _apply_barlike_style(traces: tuple) -> None: + """Apply bar-like styling to area traces (in-place).""" for trace in traces: color = trace.line.color trace.fillcolor = color trace.line = {"width": 0, "color": color, "shape": "hv"} - if mixed_signs: - trace.stackgroup = None - trace.fill = "tozeroy" def _has_mixed_signs(values: np.ndarray) -> bool: @@ -194,6 +183,60 @@ def _has_mixed_signs(values: np.ndarray) -> bool: return bool(np.any(finite > 0) and np.any(finite < 0)) +def _split_traces_by_sign(traces: tuple) -> list: + """Split traces into separate positive and negative stackgroups. + + For mixed positive/negative data, this creates proper bar-like stacking + where positives stack upward and negatives stack downward from zero. + """ + import plotly.graph_objects as go + + new_traces = [] + for trace in traces: + y_values = np.array(trace.y) + color = trace.line.color + name = trace.name + + # Positive trace (negatives become 0) + y_pos = np.clip(y_values, 0, None) + if np.any(y_pos > 0): + new_traces.append( + go.Scatter( + x=trace.x, + y=y_pos, + name=name, + stackgroup="positive", + line={"width": 0, "shape": "hv", "color": color}, + fillcolor=color, + mode="lines", + legendgroup=name, + xaxis=trace.xaxis, + yaxis=trace.yaxis, + ) + ) + + # Negative trace (positives become 0) + y_neg = np.clip(y_values, None, 0) + if np.any(y_neg < 0): + new_traces.append( + go.Scatter( + x=trace.x, + y=y_neg, + name=name, + stackgroup="negative", + line={"width": 0, "shape": "hv", "color": color}, + fillcolor=color, + mode="lines", + legendgroup=name, + showlegend=False, + xaxis=trace.xaxis, + yaxis=trace.yaxis, + ) + ) + + return new_traces + + def fast_bar( darray: DataArray, *, @@ -214,9 +257,8 @@ def fast_bar( The y-axis shows DataArray values. Dimensions fill slots in order: x -> color -> facet_col -> facet_row -> animation_frame - Note: For mixed positive/negative data, stacking is disabled and each - series fills independently to zero. For best stacking behavior with - mixed data, use `bar()` instead. + For mixed positive/negative data, positives stack upward and negatives + stack downward from zero, similar to bar chart behavior. Parameters ---------- @@ -239,6 +281,8 @@ def fast_bar( ------- plotly.graph_objects.Figure """ + import plotly.graph_objects as go + slots = assign_slots( list(darray.dims), "fast_bar", @@ -256,7 +300,7 @@ def fast_bar( value_col = get_value_col(darray) labels = {**build_labels(darray, slots, value_col), **px_kwargs.pop("labels", {})} - fig = px.area( + base_fig = px.area( df, x=slots.get("x"), y=value_col, @@ -269,9 +313,23 @@ def fast_bar( **px_kwargs, ) - _apply_barlike_style(fig.data, mixed_signs) - for frame in fig.frames: - _apply_barlike_style(frame.data, mixed_signs) + if mixed_signs: + # Split traces into positive/negative stackgroups for proper stacking + new_traces = _split_traces_by_sign(base_fig.data) + fig = go.Figure(data=new_traces, layout=base_fig.layout) + + # Handle animation frames + new_frames = [] + for frame in base_fig.frames: + frame_traces = _split_traces_by_sign(frame.data) + new_frames.append(go.Frame(data=frame_traces, name=frame.name)) + fig.frames = new_frames + else: + # Simple case: just apply bar-like styling + fig = base_fig + _apply_barlike_style(fig.data) + for frame in fig.frames: + _apply_barlike_style(frame.data) return fig From 127ff951e95f4f5f41a57750e0569baae46c44d0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:45:10 +0100 Subject: [PATCH 5/8] Done. The implementation is now much cleaner. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: ┌─────────────────────┬─────────────────────────────────────────┐ │ Trace type │ Behavior │ ├─────────────────────┼─────────────────────────────────────────┤ │ All positive values │ stackgroup='positive' - stacks upward │ ├─────────────────────┼─────────────────────────────────────────┤ │ All negative values │ stackgroup='negative' - stacks downward │ ├─────────────────────┼─────────────────────────────────────────┤ │ Mixed +/- values │ stackgroup=None, dashed line, no fill │ └─────────────────────┴─────────────────────────────────────────┘ Code is simpler: - _style_traces_as_bars() - single function that classifies and styles all traces - No trace splitting needed - Works correctly for facets and animations Notebook updated with examples showing: 1. Split columns (Profit positive, Loss negative) → proper stacking 2. Truly mixed columns → dashed lines indicating user should use bar() --- docs/examples/fast_bar.ipynb | 63 +++++++++++--- tests/test_accessor.py | 19 +++- xarray_plotly/plotting.py | 164 ++++++++++++++++------------------- 3 files changed, 140 insertions(+), 106 deletions(-) diff --git a/docs/examples/fast_bar.ipynb b/docs/examples/fast_bar.ipynb index 356ea3f..b345c89 100644 --- a/docs/examples/fast_bar.ipynb +++ b/docs/examples/fast_bar.ipynb @@ -219,7 +219,12 @@ "source": [ "## Mixed Values (Positive and Negative)\n", "\n", - "For mixed positive/negative data, `fast_bar()` automatically splits traces into separate stackgroups: positives stack upward from zero, negatives stack downward. This matches bar chart stacking behavior." + "`fast_bar()` classifies each trace (color group) by its values:\n", + "- **Purely positive** → stacks upward (stackgroup='positive')\n", + "- **Purely negative** → stacks downward (stackgroup='negative') \n", + "- **Mixed signs** → shown as dashed line, no stacking\n", + "\n", + "When one column is all positive and another all negative, they stack correctly in separate groups:" ] }, { @@ -229,24 +234,58 @@ "metadata": {}, "outputs": [], "source": [ - "# Mixed positive and negative values\n", - "da_mixed = xr.DataArray(\n", + "# Column A positive, Column B negative - stacks correctly\n", + "da_split = xr.DataArray(\n", " np.array(\n", " [\n", - " [50, -30, 20],\n", - " [-40, 60, -10],\n", - " [30, -50, 40],\n", - " [-20, 70, -30],\n", - " [60, -40, 50],\n", - " [-30, 80, -20],\n", + " [50, -20],\n", + " [60, -40],\n", + " [30, -30],\n", + " [70, -25],\n", + " [40, -35],\n", + " [55, -15],\n", " ]\n", " ),\n", " dims=[\"month\", \"category\"],\n", " coords={\n", " \"month\": [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"],\n", - " \"category\": [\"A\", \"B\", \"C\"],\n", + " \"category\": [\"Profit\", \"Loss\"],\n", + " },\n", + " name=\"financials\",\n", + ")\n", + "\n", + "xpx(da_split).fast_bar()" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "Truly mixed columns (each has both + and -) are shown as dashed lines:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "# Truly mixed columns - shown as dashed lines (use bar() for these)\n", + "da_mixed = xr.DataArray(\n", + " np.array(\n", + " [\n", + " [50, -30],\n", + " [-40, 60],\n", + " [30, -50],\n", + " ]\n", + " ),\n", + " dims=[\"month\", \"category\"],\n", + " coords={\n", + " \"month\": [\"Jan\", \"Feb\", \"Mar\"],\n", + " \"category\": [\"A\", \"B\"],\n", " },\n", - " name=\"profit_loss\",\n", ")\n", "\n", "xpx(da_mixed).fast_bar()" @@ -255,7 +294,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "19", "metadata": {}, "outputs": [], "source": [ diff --git a/tests/test_accessor.py b/tests/test_accessor.py index 4e71e96..a8f23d2 100644 --- a/tests/test_accessor.py +++ b/tests/test_accessor.py @@ -173,14 +173,25 @@ def test_fast_bar_animation_frames(self) -> None: assert trace.line.shape == "hv" assert trace.fillcolor is not None - def test_fast_bar_mixed_signs_separate_stacks(self) -> None: - """Test that fast_bar uses separate stackgroups for mixed positive/negative data.""" + def test_fast_bar_mixed_signs_dashed(self) -> None: + """Test that fast_bar shows mixed-sign traces as dashed lines.""" da = xr.DataArray( - np.array([[50, -30], [-40, 60]]), + np.array([[50, -30], [-40, 60]]), # Both columns have mixed signs + dims=["time", "category"], + ) + fig = da.plotly.fast_bar() + # Mixed traces should have no stacking and dashed lines + for trace in fig.data: + assert trace.stackgroup is None + assert trace.line.dash == "dash" + + def test_fast_bar_separate_sign_columns(self) -> None: + """Test that fast_bar uses separate stackgroups when columns have different signs.""" + da = xr.DataArray( + np.array([[50, -30], [60, -40]]), # Column 0 positive, column 1 negative dims=["time", "category"], ) fig = da.plotly.fast_bar() - # Should have separate positive and negative stackgroups stackgroups = {trace.stackgroup for trace in fig.data} assert "positive" in stackgroups assert "negative" in stackgroups diff --git a/xarray_plotly/plotting.py b/xarray_plotly/plotting.py index 9cf8bb1..7ea590a 100644 --- a/xarray_plotly/plotting.py +++ b/xarray_plotly/plotting.py @@ -167,74 +167,78 @@ def bar( ) -def _apply_barlike_style(traces: tuple) -> None: - """Apply bar-like styling to area traces (in-place).""" - for trace in traces: - color = trace.line.color - trace.fillcolor = color - trace.line = {"width": 0, "color": color, "shape": "hv"} - - -def _has_mixed_signs(values: np.ndarray) -> bool: - """Check if data contains both positive and negative values.""" - finite = values[np.isfinite(values)] - if len(finite) == 0: - return False - return bool(np.any(finite > 0) and np.any(finite < 0)) - - -def _split_traces_by_sign(traces: tuple) -> list: - """Split traces into separate positive and negative stackgroups. - - For mixed positive/negative data, this creates proper bar-like stacking - where positives stack upward and negatives stack downward from zero. +def _classify_trace_sign(y_values: np.ndarray) -> str: + """Classify a trace as 'positive', 'negative', or 'mixed' based on its values.""" + y_arr = np.asarray(y_values) + y_clean = y_arr[np.isfinite(y_arr) & (np.abs(y_arr) > 1e-9)] + if len(y_clean) == 0: + return "zero" + has_pos = bool(np.any(y_clean > 0)) + has_neg = bool(np.any(y_clean < 0)) + if has_pos and has_neg: + return "mixed" + elif has_neg: + return "negative" + elif has_pos: + return "positive" + return "zero" + + +def _style_traces_as_bars(fig: go.Figure) -> None: + """Style area chart traces to look like bar charts with proper pos/neg stacking. + + Classifies each trace (by name) across all data and animation frames, + then assigns stackgroups: positive traces stack upward, negative stack downward. """ - import plotly.graph_objects as go + # Collect all traces (main + animation frames) + all_traces = list(fig.data) + for frame in fig.frames: + all_traces.extend(frame.data) + + # Classify each trace name by aggregating sign info across all occurrences + sign_flags: dict[str, dict[str, bool]] = {} + for trace in all_traces: + if trace.name not in sign_flags: + sign_flags[trace.name] = {"has_pos": False, "has_neg": False} + if trace.y is not None and len(trace.y) > 0: + y_arr = np.asarray(trace.y) + y_clean = y_arr[np.isfinite(y_arr) & (np.abs(y_arr) > 1e-9)] + if len(y_clean) > 0: + if np.any(y_clean > 0): + sign_flags[trace.name]["has_pos"] = True + if np.any(y_clean < 0): + sign_flags[trace.name]["has_neg"] = True + + # Build classification map + class_map: dict[str, str] = {} + for name, flags in sign_flags.items(): + if flags["has_pos"] and flags["has_neg"]: + class_map[name] = "mixed" + elif flags["has_neg"]: + class_map[name] = "negative" + elif flags["has_pos"]: + class_map[name] = "positive" + else: + class_map[name] = "zero" - new_traces = [] - for trace in traces: - y_values = np.array(trace.y) + # Apply styling to all traces + for trace in all_traces: color = trace.line.color - name = trace.name - - # Positive trace (negatives become 0) - y_pos = np.clip(y_values, 0, None) - if np.any(y_pos > 0): - new_traces.append( - go.Scatter( - x=trace.x, - y=y_pos, - name=name, - stackgroup="positive", - line={"width": 0, "shape": "hv", "color": color}, - fillcolor=color, - mode="lines", - legendgroup=name, - xaxis=trace.xaxis, - yaxis=trace.yaxis, - ) - ) - - # Negative trace (positives become 0) - y_neg = np.clip(y_values, None, 0) - if np.any(y_neg < 0): - new_traces.append( - go.Scatter( - x=trace.x, - y=y_neg, - name=name, - stackgroup="negative", - line={"width": 0, "shape": "hv", "color": color}, - fillcolor=color, - mode="lines", - legendgroup=name, - showlegend=False, - xaxis=trace.xaxis, - yaxis=trace.yaxis, - ) - ) - - return new_traces + cls = class_map.get(trace.name, "positive") + + if cls in ("positive", "negative"): + trace.stackgroup = cls + trace.fillcolor = color + trace.line = {"width": 0, "color": color, "shape": "hv"} + elif cls == "mixed": + # Mixed: no stacking, show as dashed line + trace.stackgroup = None + trace.fill = None + trace.line = {"width": 2, "color": color, "shape": "hv", "dash": "dash"} + else: # zero + trace.stackgroup = None + trace.fill = None + trace.line = {"width": 0, "color": color, "shape": "hv"} def fast_bar( @@ -257,8 +261,9 @@ def fast_bar( The y-axis shows DataArray values. Dimensions fill slots in order: x -> color -> facet_col -> facet_row -> animation_frame - For mixed positive/negative data, positives stack upward and negatives - stack downward from zero, similar to bar chart behavior. + Traces are classified by their values: purely positive traces stack upward, + purely negative traces stack downward. Traces with mixed signs are shown + as dashed lines without stacking. Parameters ---------- @@ -281,8 +286,6 @@ def fast_bar( ------- plotly.graph_objects.Figure """ - import plotly.graph_objects as go - slots = assign_slots( list(darray.dims), "fast_bar", @@ -293,14 +296,11 @@ def fast_bar( animation_frame=animation_frame, ) - # Check for mixed positive/negative values - mixed_signs = _has_mixed_signs(darray.values) - df = to_dataframe(darray) value_col = get_value_col(darray) labels = {**build_labels(darray, slots, value_col), **px_kwargs.pop("labels", {})} - base_fig = px.area( + fig = px.area( df, x=slots.get("x"), y=value_col, @@ -313,23 +313,7 @@ def fast_bar( **px_kwargs, ) - if mixed_signs: - # Split traces into positive/negative stackgroups for proper stacking - new_traces = _split_traces_by_sign(base_fig.data) - fig = go.Figure(data=new_traces, layout=base_fig.layout) - - # Handle animation frames - new_frames = [] - for frame in base_fig.frames: - frame_traces = _split_traces_by_sign(frame.data) - new_frames.append(go.Frame(data=frame_traces, name=frame.name)) - fig.frames = new_frames - else: - # Simple case: just apply bar-like styling - fig = base_fig - _apply_barlike_style(fig.data) - for frame in fig.frames: - _apply_barlike_style(frame.data) + _style_traces_as_bars(fig) return fig From 963f5a236789f6a72f4ee56acb16948fa3f0ab89 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:55:28 +0100 Subject: [PATCH 6/8] Add warning for mixed columns --- xarray_plotly/plotting.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/xarray_plotly/plotting.py b/xarray_plotly/plotting.py index 7ea590a..69acf6c 100644 --- a/xarray_plotly/plotting.py +++ b/xarray_plotly/plotting.py @@ -4,6 +4,7 @@ from __future__ import annotations +import warnings from typing import TYPE_CHECKING, Any import numpy as np @@ -211,9 +212,11 @@ def _style_traces_as_bars(fig: go.Figure) -> None: # Build classification map class_map: dict[str, str] = {} + mixed_traces: list[str] = [] for name, flags in sign_flags.items(): if flags["has_pos"] and flags["has_neg"]: class_map[name] = "mixed" + mixed_traces.append(name) elif flags["has_neg"]: class_map[name] = "negative" elif flags["has_pos"]: @@ -221,6 +224,16 @@ def _style_traces_as_bars(fig: go.Figure) -> None: else: class_map[name] = "zero" + # Warn about mixed traces + if mixed_traces: + warnings.warn( + f"fast_bar: traces {mixed_traces} have mixed positive/negative values " + "and cannot be stacked. They are shown as dashed lines. " + "Consider using bar() for proper stacking of mixed data.", + UserWarning, + stacklevel=3, + ) + # Apply styling to all traces for trace in all_traces: color = trace.line.color From fbd6e8de91e97ec71b9334b67d674859e50d655f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:57:40 +0100 Subject: [PATCH 7/8] Improve notebook --- docs/examples/fast_bar.ipynb | 234 ++++++++++++++++------------------- 1 file changed, 106 insertions(+), 128 deletions(-) diff --git a/docs/examples/fast_bar.ipynb b/docs/examples/fast_bar.ipynb index b345c89..0f4d2df 100644 --- a/docs/examples/fast_bar.ipynb +++ b/docs/examples/fast_bar.ipynb @@ -26,43 +26,34 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "id": "2", "metadata": {}, - "outputs": [], "source": [ - "# Sample data\n", - "da = xr.DataArray(\n", - " np.random.rand(12, 3) * 100,\n", - " dims=[\"month\", \"category\"],\n", - " coords={\n", - " \"month\": [\n", - " \"Jan\",\n", - " \"Feb\",\n", - " \"Mar\",\n", - " \"Apr\",\n", - " \"May\",\n", - " \"Jun\",\n", - " \"Jul\",\n", - " \"Aug\",\n", - " \"Sep\",\n", - " \"Oct\",\n", - " \"Nov\",\n", - " \"Dec\",\n", - " ],\n", - " \"category\": [\"A\", \"B\", \"C\"],\n", - " },\n", - " name=\"sales\",\n", - ")" + "## Basic Example" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "3", "metadata": {}, + "outputs": [], "source": [ - "## Fast Bar Chart" + "# Quarterly revenue data by product and region\n", + "np.random.seed(42)\n", + "da = xr.DataArray(\n", + " np.random.rand(4, 3, 2) * 100 + 50,\n", + " dims=[\"quarter\", \"product\", \"region\"],\n", + " coords={\n", + " \"quarter\": [\"Q1\", \"Q2\", \"Q3\", \"Q4\"],\n", + " \"product\": [\"Widgets\", \"Gadgets\", \"Gizmos\"],\n", + " \"region\": [\"North\", \"South\"],\n", + " },\n", + " name=\"revenue\",\n", + ")\n", + "\n", + "xpx(da).fast_bar()" ] }, { @@ -72,7 +63,8 @@ "metadata": {}, "outputs": [], "source": [ - "xpx(da).fast_bar()" + "# Comparison with regular bar()\n", + "xpx(da).bar()" ] }, { @@ -80,7 +72,7 @@ "id": "5", "metadata": {}, "source": [ - "## Comparison with Regular Bar Chart" + "## With Faceting" ] }, { @@ -90,7 +82,7 @@ "metadata": {}, "outputs": [], "source": [ - "xpx(da).bar()" + "xpx(da).fast_bar(facet_col=\"region\")" ] }, { @@ -98,7 +90,7 @@ "id": "7", "metadata": {}, "source": [ - "## Comparison with Area Chart" + "## With Animation" ] }, { @@ -108,7 +100,20 @@ "metadata": {}, "outputs": [], "source": [ - "xpx(da).area()" + "# Multi-year data for animation\n", + "np.random.seed(123)\n", + "da_anim = xr.DataArray(\n", + " np.random.rand(4, 3, 5) * 100 + 20,\n", + " dims=[\"quarter\", \"product\", \"year\"],\n", + " coords={\n", + " \"quarter\": [\"Q1\", \"Q2\", \"Q3\", \"Q4\"],\n", + " \"product\": [\"Widgets\", \"Gadgets\", \"Gizmos\"],\n", + " \"year\": [2020, 2021, 2022, 2023, 2024],\n", + " },\n", + " name=\"revenue\",\n", + ")\n", + "\n", + "xpx(da_anim).fast_bar(animation_frame=\"year\")" ] }, { @@ -116,9 +121,7 @@ "id": "9", "metadata": {}, "source": [ - "## With Animation\n", - "\n", - "The `fast_bar` styling also applies to animation frames:" + "## Faceting + Animation" ] }, { @@ -128,32 +131,21 @@ "metadata": {}, "outputs": [], "source": [ - "# Data with animation dimension\n", - "da_anim = xr.DataArray(\n", - " np.random.rand(12, 3, 5) * 100,\n", - " dims=[\"month\", \"category\", \"year\"],\n", + "# 4D data: quarter x product x region x year\n", + "np.random.seed(456)\n", + "da_4d = xr.DataArray(\n", + " np.random.rand(4, 3, 2, 4) * 80 + 30,\n", + " dims=[\"quarter\", \"product\", \"region\", \"year\"],\n", " coords={\n", - " \"month\": [\n", - " \"Jan\",\n", - " \"Feb\",\n", - " \"Mar\",\n", - " \"Apr\",\n", - " \"May\",\n", - " \"Jun\",\n", - " \"Jul\",\n", - " \"Aug\",\n", - " \"Sep\",\n", - " \"Oct\",\n", - " \"Nov\",\n", - " \"Dec\",\n", - " ],\n", - " \"category\": [\"A\", \"B\", \"C\"],\n", - " \"year\": [2020, 2021, 2022, 2023, 2024],\n", + " \"quarter\": [\"Q1\", \"Q2\", \"Q3\", \"Q4\"],\n", + " \"product\": [\"Widgets\", \"Gadgets\", \"Gizmos\"],\n", + " \"region\": [\"North\", \"South\"],\n", + " \"year\": [2021, 2022, 2023, 2024],\n", " },\n", - " name=\"sales\",\n", + " name=\"revenue\",\n", ")\n", "\n", - "xpx(da_anim).fast_bar(animation_frame=\"year\")" + "xpx(da_4d).fast_bar(facet_col=\"region\", animation_frame=\"year\")" ] }, { @@ -161,146 +153,132 @@ "id": "11", "metadata": {}, "source": [ - "## When to Use\n", - "\n", - "| Method | Use when... |\n", - "|--------|-------------|\n", - "| `fast_bar()` | Large datasets, animations with many frames, performance matters |\n", - "| `bar()` | Need precise bar positioning, grouped bars, pattern fills |\n", - "| `area()` | Want smooth continuous fills, standard area chart appearance |" - ] - }, - { - "cell_type": "markdown", - "id": "12", - "metadata": {}, - "source": [ - "## Negative Values\n", + "## Positive and Negative Values\n", "\n", - "Testing with all negative values:" + "`fast_bar()` classifies each trace by its values:\n", + "- **Purely positive** → stacks upward\n", + "- **Purely negative** → stacks downward\n", + "- **Mixed signs** → warning + dashed line (use `bar()` instead)" ] }, { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "12", "metadata": {}, "outputs": [], "source": [ - "# All negative values\n", - "da_negative = xr.DataArray(\n", - " -np.random.rand(6, 3) * 100,\n", + "# Profit (positive) and Loss (negative) - stacks correctly\n", + "np.random.seed(789)\n", + "da_split = xr.DataArray(\n", + " np.column_stack(\n", + " [\n", + " np.random.rand(6) * 80 + 20, # Revenue: positive\n", + " -np.random.rand(6) * 50 - 10, # Costs: negative\n", + " ]\n", + " ),\n", " dims=[\"month\", \"category\"],\n", " coords={\n", " \"month\": [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"],\n", - " \"category\": [\"A\", \"B\", \"C\"],\n", + " \"category\": [\"Revenue\", \"Costs\"],\n", " },\n", - " name=\"loss\",\n", + " name=\"financials\",\n", ")\n", "\n", - "xpx(da_negative).fast_bar()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "14", - "metadata": {}, - "outputs": [], - "source": [ - "# Comparison: regular bar with negative values\n", - "xpx(da_negative).bar()" - ] - }, - { - "cell_type": "markdown", - "id": "15", - "metadata": {}, - "source": [ - "## Mixed Values (Positive and Negative)\n", - "\n", - "`fast_bar()` classifies each trace (color group) by its values:\n", - "- **Purely positive** → stacks upward (stackgroup='positive')\n", - "- **Purely negative** → stacks downward (stackgroup='negative') \n", - "- **Mixed signs** → shown as dashed line, no stacking\n", - "\n", - "When one column is all positive and another all negative, they stack correctly in separate groups:" + "xpx(da_split).fast_bar()" ] }, { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "13", "metadata": {}, "outputs": [], "source": [ - "# Column A positive, Column B negative - stacks correctly\n", - "da_split = xr.DataArray(\n", - " np.array(\n", + "# With animation - sign classification is consistent across frames\n", + "np.random.seed(321)\n", + "da_split_anim = xr.DataArray(\n", + " np.stack(\n", " [\n", - " [50, -20],\n", - " [60, -40],\n", - " [30, -30],\n", - " [70, -25],\n", - " [40, -35],\n", - " [55, -15],\n", - " ]\n", + " np.column_stack([np.random.rand(6) * 80 + 20, -np.random.rand(6) * 50 - 10])\n", + " for _ in range(4)\n", + " ],\n", + " axis=-1,\n", " ),\n", - " dims=[\"month\", \"category\"],\n", + " dims=[\"month\", \"category\", \"year\"],\n", " coords={\n", " \"month\": [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"],\n", - " \"category\": [\"Profit\", \"Loss\"],\n", + " \"category\": [\"Revenue\", \"Costs\"],\n", + " \"year\": [2021, 2022, 2023, 2024],\n", " },\n", " name=\"financials\",\n", ")\n", "\n", - "xpx(da_split).fast_bar()" + "xpx(da_split_anim).fast_bar(animation_frame=\"year\")" ] }, { "cell_type": "markdown", - "id": "17", + "id": "14", "metadata": {}, "source": [ - "Truly mixed columns (each has both + and -) are shown as dashed lines:" + "## Mixed Sign Warning\n", + "\n", + "When a trace has both positive and negative values, `fast_bar()` shows a warning and displays it as a dashed line:" ] }, { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "15", "metadata": {}, "outputs": [], "source": [ - "# Truly mixed columns - shown as dashed lines (use bar() for these)\n", + "# Both columns have mixed signs - triggers warning\n", "da_mixed = xr.DataArray(\n", " np.array(\n", " [\n", " [50, -30],\n", " [-40, 60],\n", " [30, -50],\n", + " [-20, 40],\n", " ]\n", " ),\n", " dims=[\"month\", \"category\"],\n", " coords={\n", - " \"month\": [\"Jan\", \"Feb\", \"Mar\"],\n", + " \"month\": [\"Jan\", \"Feb\", \"Mar\", \"Apr\"],\n", " \"category\": [\"A\", \"B\"],\n", " },\n", ")\n", "\n", + "# This will show a warning\n", "xpx(da_mixed).fast_bar()" ] }, { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "16", "metadata": {}, "outputs": [], "source": [ - "# Comparison: regular bar with mixed values\n", + "# For mixed data, use bar() instead\n", "xpx(da_mixed).bar()" ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "## When to Use\n", + "\n", + "| Method | Use when... |\n", + "|--------|-------------|\n", + "| `fast_bar()` | Large datasets, animations, performance matters, data is same-sign per trace |\n", + "| `bar()` | Need grouped bars, pattern fills, or have mixed +/- values per trace |\n", + "| `area()` | Want smooth continuous fills |" + ] } ], "metadata": { From 6fdb707a3988a26d7a31b17ae572f8d0e177de2e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:48:04 +0100 Subject: [PATCH 8/8] Ad fastbar to accessor.py --- xarray_plotly/accessor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray_plotly/accessor.py b/xarray_plotly/accessor.py index 80b604c..41f60b4 100644 --- a/xarray_plotly/accessor.py +++ b/xarray_plotly/accessor.py @@ -34,7 +34,7 @@ class DataArrayPlotlyAccessor: ``` """ - __all__: ClassVar = ["line", "bar", "area", "scatter", "box", "imshow", "pie"] + __all__: ClassVar = ["line", "bar", "fast_bar", "area", "scatter", "box", "imshow", "pie"] def __init__(self, darray: DataArray) -> None: self._da = darray @@ -384,7 +384,7 @@ class DatasetPlotlyAccessor: ``` """ - __all__: ClassVar = ["line", "bar", "area", "scatter", "box", "pie"] + __all__: ClassVar = ["line", "bar", "fast_bar", "area", "scatter", "box", "pie"] def __init__(self, dataset: Dataset) -> None: self._ds = dataset