From 79a7e8c347545df0745968a1f67f406964e1975e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:04:36 +0100 Subject: [PATCH 1/4] Add Dataset notebook --- docs/examples/datasets.ipynb | 213 +++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 214 insertions(+) create mode 100644 docs/examples/datasets.ipynb diff --git a/docs/examples/datasets.ipynb b/docs/examples/datasets.ipynb new file mode 100644 index 0000000..370c2f9 --- /dev/null +++ b/docs/examples/datasets.ipynb @@ -0,0 +1,213 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Dataset Plotting\n", + "\n", + "Plot multiple variables from an xarray Dataset with automatic or custom slot assignment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a Dataset with multiple variables\n", + "time = np.arange(50)\n", + "cities = [\"NYC\", \"LA\", \"Chicago\"]\n", + "\n", + "ds = xr.Dataset(\n", + " {\n", + " \"temperature\": ([\"time\", \"city\"], 20 + 5 * np.random.randn(50, 3).cumsum(axis=0) / 10),\n", + " \"humidity\": ([\"time\", \"city\"], 50 + 10 * np.random.randn(50, 3).cumsum(axis=0) / 10),\n", + " \"pressure\": ([\"time\", \"city\"], 1013 + np.random.randn(50, 3).cumsum(axis=0)),\n", + " },\n", + " coords={\"time\": time, \"city\": cities},\n", + ")\n", + "ds" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot All Variables\n", + "\n", + "When you call a plot method on a Dataset without specifying `var`, all variables are combined into a single DataArray with a new `\"variable\"` dimension:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# All variables: time -> x, variable -> color, city -> line_dash\n", + "xpx(ds).line()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Control Where \"variable\" Goes\n", + "\n", + "The `\"variable\"` dimension can be assigned to any slot:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Variables as facet columns\n", + "xpx(ds).line(facet_col=\"variable\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Variables as rows, cities as columns\n", + "xpx(ds).line(facet_row=\"variable\", facet_col=\"city\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot a Single Variable\n", + "\n", + "Use `var=\"name\"` to plot just one variable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "xpx(ds).line(var=\"temperature\", title=\"Temperature Only\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Different Plot Types" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Bar chart - latest values by city\n", + "xpx(ds.isel(time=-1)).bar(x=\"city\", color=\"variable\", barmode=\"group\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Box plot - distribution by variable\n", + "xpx(ds).box(x=\"variable\", color=\"city\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Area chart\n", + "xpx(ds).area(var=\"humidity\", title=\"Humidity Over Time\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Scatter\n", + "xpx(ds).scatter(var=\"temperature\", title=\"Temperature Scatter\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Pie chart - snapshot at one time\n", + "xpx(ds.isel(time=-1)).pie(var=\"temperature\", names=\"city\", title=\"Temperature Distribution\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Combining Slot Assignments\n", + "\n", + "Mix explicit assignments with auto-assignment:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Explicit: variable -> facet_col, let city auto-assign to color\n", + "xpx(ds).line(facet_col=\"variable\", color=\"city\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Skip color slot with None\n", + "xpx(ds).line(var=\"temperature\", color=None)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/mkdocs.yml b/mkdocs.yml index 23fddb3..1a1f0f1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,6 +69,7 @@ nav: - Getting Started: getting-started.ipynb - Examples: - Plot Types: examples/plot-types.ipynb + - Dataset Plotting: examples/datasets.ipynb - Dimensions & Facets: examples/dimensions.ipynb - Plotly Express Options: examples/kwargs.ipynb - Figure Customization: examples/figure.ipynb From 3de8bf881f64dbc401af2e1dbff472aa0dafda2a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:12:13 +0100 Subject: [PATCH 2/4] Added configurable dataset_variable_position to dataset plots --- xarray_plotly/accessor.py | 22 ++++++++++++++++++++-- xarray_plotly/config.py | 12 ++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/xarray_plotly/accessor.py b/xarray_plotly/accessor.py index a947b4a..d69ca1c 100644 --- a/xarray_plotly/accessor.py +++ b/xarray_plotly/accessor.py @@ -349,9 +349,27 @@ def __dir__(self) -> list[str]: return list(self.__all__) + list(super().__dir__()) def _get_dataarray(self, var: str | None) -> DataArray: - """Get DataArray from Dataset, either single var or all via to_array().""" + """Get DataArray from Dataset, either single var or all via to_array(). + + When combining all variables, "variable" is placed at the position + specified by config.dataset_variable_position (default 1, second position). + """ if var is None: - return self._ds.to_array(dim="variable") + from xarray_plotly.config import _options + + da = self._ds.to_array(dim="variable") + pos = _options.dataset_variable_position + # Move "variable" to configured position + if len(da.dims) > 1 and pos != 0: + dims = list(da.dims) + dims.remove("variable") + # Handle negative indices and bounds + if pos < 0: + dims.append("variable") + else: + dims.insert(min(pos, len(dims)), "variable") + da = da.transpose(*dims) + return da return self._ds[var] def line( diff --git a/xarray_plotly/config.py b/xarray_plotly/config.py index e18c931..e28cab0 100644 --- a/xarray_plotly/config.py +++ b/xarray_plotly/config.py @@ -58,6 +58,9 @@ class Options: label_include_units: Append units to labels. Default True. label_unit_format: Format string for units. Use `{units}` as placeholder. slot_orders: Slot orders per plot type. Keys are plot types, values are tuples. + dataset_variable_position: Position of "variable" dim when plotting all Dataset + variables. Default 1 (second position, typically color). Set to 0 for first + position (x-axis), or -1 for last position. """ label_use_long_name: bool = True @@ -67,6 +70,7 @@ class Options: slot_orders: dict[str, tuple[str, ...]] = field( default_factory=lambda: dict(DEFAULT_SLOT_ORDERS) ) + dataset_variable_position: int = 1 def to_dict(self) -> dict[str, Any]: """Return options as a dictionary.""" @@ -76,6 +80,7 @@ def to_dict(self) -> dict[str, Any]: "label_include_units": self.label_include_units, "label_unit_format": self.label_unit_format, "slot_orders": self.slot_orders, + "dataset_variable_position": self.dataset_variable_position, } @@ -106,6 +111,7 @@ def set_options( label_include_units: bool | None = None, label_unit_format: str | None = None, slot_orders: dict[str, tuple[str, ...]] | None = None, + dataset_variable_position: int | None = None, ) -> Generator[None, None, None]: """Set xarray_plotly options globally or as a context manager. @@ -115,6 +121,8 @@ def set_options( label_include_units: Append units to labels. label_unit_format: Format string for units. Use `{units}` as placeholder. slot_orders: Slot orders per plot type. + dataset_variable_position: Position of "variable" dim when plotting all Dataset + variables. Default 1 (second, typically color). Use 0 for first, -1 for last. Yields: None when used as a context manager. @@ -136,6 +144,7 @@ def set_options( "label_include_units": _options.label_include_units, "label_unit_format": _options.label_unit_format, "slot_orders": dict(_options.slot_orders), + "dataset_variable_position": _options.dataset_variable_position, } # Apply new values (modify in place to keep reference) @@ -149,6 +158,8 @@ def set_options( _options.label_unit_format = label_unit_format if slot_orders is not None: _options.slot_orders = dict(slot_orders) + if dataset_variable_position is not None: + _options.dataset_variable_position = dataset_variable_position try: yield @@ -159,6 +170,7 @@ def set_options( _options.label_include_units = old_values["label_include_units"] _options.label_unit_format = old_values["label_unit_format"] _options.slot_orders = old_values["slot_orders"] + _options.dataset_variable_position = old_values["dataset_variable_position"] def notebook(renderer: str = "notebook") -> None: From 7beece7b885e28975c93b58aeb4a4adcc33c5a14 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:13:37 +0100 Subject: [PATCH 3/4] Add to datasets.ipynb --- docs/examples/datasets.ipynb | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/examples/datasets.ipynb b/docs/examples/datasets.ipynb index 370c2f9..b6dc5b2 100644 --- a/docs/examples/datasets.ipynb +++ b/docs/examples/datasets.ipynb @@ -92,6 +92,53 @@ "xpx(ds).line(facet_row=\"variable\", facet_col=\"city\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configure Default \"variable\" Position\n", + "\n", + "By default, `\"variable\"` is placed as the **second** dimension so it maps to `color`. This keeps your first dimension (e.g., time) on the x-axis.\n", + "\n", + "You can change this globally with `config.set_options()`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Default: position=1 (second) -> variable goes to color\n", + "# Dimensions are: (time, variable, city)\n", + "print(\"Default dimension order:\", ds.to_array().dims)\n", + "xpx(ds).line(title=\"Default: variable as color (position=1)\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Position 0: variable goes first (x-axis) - usually not what you want!\n", + "with config.set_options(dataset_variable_position=0):\n", + " fig = xpx(ds).line(title=\"position=0: variable on x-axis (probably not desired)\")\n", + "fig" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Position -1: variable goes last -> city gets color, variable gets line_dash\n", + "with config.set_options(dataset_variable_position=-1):\n", + " fig = xpx(ds).line(title=\"position=-1: variable as line_dash\")\n", + "fig" + ] + }, { "cell_type": "markdown", "metadata": {}, From ad61ad2eb3eaee9315f58ff3cfffe26aef923fd9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:28:57 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=E2=8F=BA=20Fixed=20the=20review=20issues:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Notebook comment - Clarified that to_array() returns (variable, ...) but xpx() reorders it 2. Negative indexing - Now supports Python-style: -1=last, -2=second-to-last, etc. 3. Import - Moved _options to module level --- docs/examples/datasets.ipynb | 5 +++-- xarray_plotly/accessor.py | 14 +++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/examples/datasets.ipynb b/docs/examples/datasets.ipynb index b6dc5b2..598d566 100644 --- a/docs/examples/datasets.ipynb +++ b/docs/examples/datasets.ipynb @@ -110,8 +110,9 @@ "outputs": [], "source": [ "# Default: position=1 (second) -> variable goes to color\n", - "# Dimensions are: (time, variable, city)\n", - "print(\"Default dimension order:\", ds.to_array().dims)\n", + "# Note: to_array() puts \"variable\" first, but xpx() reorders it to position 1\n", + "print(\"Raw to_array() dims:\", ds.to_array().dims) # (variable, time, city)\n", + "print(\"After xpx reorder: (time, variable, city)\") # time->x, variable->color\n", "xpx(ds).line(title=\"Default: variable as color (position=1)\")" ] }, diff --git a/xarray_plotly/accessor.py b/xarray_plotly/accessor.py index d69ca1c..ca66f59 100644 --- a/xarray_plotly/accessor.py +++ b/xarray_plotly/accessor.py @@ -7,6 +7,7 @@ from xarray_plotly import plotting from xarray_plotly.common import SlotValue, auto +from xarray_plotly.config import _options class DataArrayPlotlyAccessor: @@ -353,21 +354,20 @@ def _get_dataarray(self, var: str | None) -> DataArray: When combining all variables, "variable" is placed at the position specified by config.dataset_variable_position (default 1, second position). + Supports Python-style negative indexing: -1 = last, -2 = second-to-last, etc. """ if var is None: - from xarray_plotly.config import _options - da = self._ds.to_array(dim="variable") pos = _options.dataset_variable_position # Move "variable" to configured position if len(da.dims) > 1 and pos != 0: dims = list(da.dims) dims.remove("variable") - # Handle negative indices and bounds - if pos < 0: - dims.append("variable") - else: - dims.insert(min(pos, len(dims)), "variable") + # Use Python-style indexing (handles negative indices correctly) + # Clamp to valid range: -1 -> last, -2 -> second-to-last, etc. + n = len(dims) + insert_pos = max(0, n + pos + 1) if pos < 0 else min(pos, n) + dims.insert(insert_pos, "variable") da = da.transpose(*dims) return da return self._ds[var]