diff --git a/docs/examples/datasets.ipynb b/docs/examples/datasets.ipynb new file mode 100644 index 0000000..598d566 --- /dev/null +++ b/docs/examples/datasets.ipynb @@ -0,0 +1,261 @@ +{ + "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": [ + "## 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", + "# 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)\")" + ] + }, + { + "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": {}, + "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 diff --git a/xarray_plotly/accessor.py b/xarray_plotly/accessor.py index a947b4a..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: @@ -349,9 +350,26 @@ 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). + Supports Python-style negative indexing: -1 = last, -2 = second-to-last, etc. + """ if var is None: - return self._ds.to_array(dim="variable") + 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") + # 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] 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: