|
1 | | -# xarray-plotly-accessor |
2 | | -Convenience plotting accessor for xarray |
| 1 | +# xarray-plotly |
3 | 2 |
|
4 | | -## Background: A PR to xarray which got rejected |
| 3 | +**Interactive Plotly Express plotting accessor for xarray** |
5 | 4 |
|
6 | | -### Is your feature request related to a problem? |
| 5 | +[](https://badge.fury.io/py/xarray-plotly) |
| 6 | +[](https://pypi.org/project/xarray-plotly/) |
7 | 7 |
|
8 | | -The current `.plot` accessor in xarray is built on matplotlib, which has several limitations for modern data exploration workflows: |
| 8 | +xarray-plotly provides a `pxplot` accessor for xarray DataArray objects that enables interactive plotting using Plotly Express with automatic dimension-to-slot assignment. |
9 | 9 |
|
10 | | -1. **Static outputs**: Matplotlib plots are non-interactive. Users cannot zoom, pan, hover for values, or toggle traces without significant additional code. |
| 10 | +## Installation |
11 | 11 |
|
12 | | -2. **Post-creation modification is cumbersome**: Customizing matplotlib plots after creation requires understanding the complex `Axes`/`Figure` object hierarchy. Common tasks like adjusting labels, colors, or adding annotations often require many lines of boilerplate. |
13 | | - |
14 | | -3. **Multi-dimensional data visualization**: When datasets have 3+ dimensions, users must manually slice/aggregate before plotting. There's no built-in support for faceting, animation, or interactive dimension exploration. |
15 | | - |
16 | | -4. **Maintenance burden**: The matplotlib plotting code in xarray is substantial and complex. A Plotly-based alternative could reduce this burden since Plotly's express API handles much of the layout logic internally. |
17 | | - |
18 | | -5. **Missing modern visualization patterns**: Interactive heatmaps with hover info, animated time series, linked brushing between facets - these are increasingly expected in data science workflows but require significant custom code with matplotlib. |
19 | | - |
20 | | -### Describe the solution you'd like |
21 | | - |
22 | | -A new **`pxplot`** accessor (Plotly Express plot) that provides: |
23 | | - |
24 | | -### Core API Design |
25 | | - |
26 | | -```python |
27 | | -import xarray as xr |
28 | | - |
29 | | -ds = xr.Dataset(...) |
30 | | -da = xr.DataArray(...) |
31 | | - |
32 | | -# Basic usage - automatic dimension assignment |
33 | | -ds.pxplot.line() |
34 | | -ds.pxplot.bar() |
35 | | -ds.pxplot.area() |
36 | | -da.pxplot.heatmap() |
37 | | - |
38 | | -# Explicit dimension-to-slot mapping |
39 | | -ds.pxplot.line(x='time', color='scenario', facet_col='region') |
40 | | - |
41 | | -# Returns plotly.graph_objects.Figure for easy customization |
42 | | -fig = ds.pxplot.bar(color='variable') |
43 | | -fig.update_layout(title='My Custom Title') |
44 | | -fig.show() |
45 | | -``` |
46 | | - |
47 | | -### Automatic Dimension → Slot Assignment |
48 | | - |
49 | | -Dimensions are assigned to plot "slots" **by their order** in the Dataset/DataArray: |
50 | | - |
51 | | -| Slot | Purpose | |
52 | | -|------|---------| |
53 | | -| `x` | X-axis | |
54 | | -| `color` | Trace grouping/stacking | |
55 | | -| `facet_col` | Subplot columns | |
56 | | -| `facet_row` | Subplot rows | |
57 | | -| `animation_frame` | Animation slider | |
58 | | - |
59 | | -**Default slot order** (per plot type): |
60 | | -```python |
61 | | -SLOT_ORDER = ('x', 'color', 'facet_col', 'facet_row', 'animation_frame') |
62 | | -``` |
63 | | - |
64 | | -**Assignment is purely positional** - no name-based heuristics: |
65 | | -```python |
66 | | -# Dataset with dims: ('time', 'scenario', 'region') |
67 | | -# Auto-assigns: time→x, scenario→color, region→facet_col |
68 | | -ds.pxplot.line() |
69 | | - |
70 | | -# Dataset with dims: ('region', 'time', 'scenario') |
71 | | -# Auto-assigns: region→x, time→color, scenario→facet_col |
72 | | -ds.pxplot.line() |
73 | | -``` |
74 | | - |
75 | | -**Override modes for each slot:** |
76 | | -- `'dim_name'`: Explicitly use that dimension for this slot |
77 | | -- `None`: Skip this slot (don't assign any dimension to it) |
78 | | - |
79 | | -```python |
80 | | -# Explicit assignment |
81 | | -ds.pxplot.line(x='time', color='scenario') |
82 | | - |
83 | | -# Skip color slot: time→x, scenario→facet_col (color unused) |
84 | | -ds.pxplot.bar(x='time', color=None) |
| 12 | +```bash |
| 13 | +pip install xarray-plotly |
85 | 14 | ``` |
86 | 15 |
|
87 | | -**Error on unassigned dimensions:** |
88 | | -If there are more dimensions than available slots, an error is raised. Users must reduce dimensionality first: |
89 | | -```python |
90 | | -# 6 dims but only 5 slots → Error |
91 | | -ds.pxplot.line() # raises ValueError: 1 unassigned dimension(s): ['extra_dim']. |
92 | | - # Use .sel(), .isel(), or .mean() to reduce. |
93 | | - |
94 | | -# Fix by reducing dimensions before plotting |
95 | | -ds.sel(extra_dim='value').pxplot.line() |
96 | | -ds.mean('extra_dim').pxplot.line() |
97 | | -``` |
98 | | - |
99 | | -### Handling Multi-Variable Datasets |
100 | | - |
101 | | -For Datasets with multiple data variables, treat `'variable'` as a pseudo-dimension: |
102 | | - |
103 | | -```python |
104 | | -ds = xr.Dataset({ |
105 | | - 'temperature': (['time', 'station'], temp_data), |
106 | | - 'humidity': (['time', 'station'], humid_data), |
107 | | -}) |
| 16 | +Or with uv: |
108 | 17 |
|
109 | | -# 'variable' can be assigned to color to compare temperature vs humidity |
110 | | -ds.pxplot.line(x='time', color='variable', facet_col='station') |
| 18 | +```bash |
| 19 | +uv add xarray-plotly |
111 | 20 | ``` |
112 | 21 |
|
113 | | -### Proposed Methods |
114 | | - |
115 | | -```python |
116 | | -# Dataset accessor |
117 | | -@xr.register_dataset_accessor('pxplot') |
118 | | -class DatasetPxplotAccessor: |
119 | | - def line(self, *, x='auto', color='auto', facet_col='auto', |
120 | | - facet_row='auto', animation_frame='auto', **px_kwargs) -> go.Figure |
121 | | - |
122 | | - def bar(self, *, x='auto', color='auto', ...) -> go.Figure |
123 | | - |
124 | | - def area(self, *, x='auto', color='auto', ...) -> go.Figure # stacked area |
125 | | - |
126 | | - def scatter(self, *, x='auto', y='auto', color='auto', ...) -> go.Figure |
127 | | - |
128 | | - def heatmap(self, *, x='auto', y='auto', facet_col='auto', ...) -> go.Figure |
129 | | - |
130 | | - |
131 | | -# DataArray accessor |
132 | | -@xr.register_dataarray_accessor('pxplot') |
133 | | -class DataArrayPxplotAccessor: |
134 | | - def line(self, ...) -> go.Figure |
135 | | - def heatmap(self, ...) -> go.Figure |
136 | | - # etc. |
137 | | -``` |
138 | | - |
139 | | -### Slot Order per Plot Type |
140 | | - |
141 | | -Different plot types define their own default slot order: |
142 | | -```python |
143 | | -# line/bar/area: x is primary |
144 | | -LINE_SLOT_ORDER = ('x', 'color', 'facet_col', 'facet_row', 'animation_frame') |
145 | | - |
146 | | -# heatmap: needs x and y for the grid |
147 | | -HEATMAP_SLOT_ORDER = ('x', 'y', 'facet_col', 'facet_row', 'animation_frame') |
148 | | - |
149 | | -# scatter: x and y are primary |
150 | | -SCATTER_SLOT_ORDER = ('x', 'y', 'color', 'facet_col', 'facet_row', 'animation_frame') |
151 | | -``` |
152 | | - |
153 | | -Global configuration (e.g., custom slot orders, default colorscales) could be added later if needed. |
154 | | - |
155 | | -### Example Usage |
| 22 | +## Quick Start |
156 | 23 |
|
157 | 24 | ```python |
158 | 25 | import xarray as xr |
159 | 26 | import numpy as np |
160 | | - |
161 | | -# Create sample dataset - dims order: (time, city, scenario) |
162 | | -ds = xr.Dataset({ |
163 | | - 'temperature': (['time', 'city', 'scenario'], np.random.randn(100, 3, 2)), |
164 | | - 'precipitation': (['time', 'city', 'scenario'], np.random.randn(100, 3, 2)), |
165 | | -}, coords={ |
166 | | - 'time': pd.date_range('2020', periods=100), |
167 | | - 'city': ['NYC', 'LA', 'Chicago'], |
168 | | - 'scenario': ['baseline', 'warming'], |
169 | | -}) |
170 | | - |
171 | | -# Dims: (time, city, scenario) + 'variable' (2 data vars) |
172 | | -# Slot order: (x, color, facet_col, facet_row) |
173 | | -# Result: time→x, city→color, scenario→facet_col, variable→facet_row |
174 | | -fig = ds.pxplot.line() |
175 | | - |
176 | | -# Interactive: zoom, pan, hover for values, toggle traces |
177 | | -fig.show() |
178 | | - |
179 | | -# Easy customization after creation |
180 | | -fig.update_layout( |
181 | | - title='Climate Projections', |
182 | | - xaxis_title='Date', |
183 | | - template='plotly_dark', |
| 27 | +import xarray_plotly # registers the accessor |
| 28 | + |
| 29 | +# Create sample data |
| 30 | +da = xr.DataArray( |
| 31 | + np.random.randn(100, 3, 2).cumsum(axis=0), |
| 32 | + dims=["time", "city", "scenario"], |
| 33 | + coords={ |
| 34 | + "time": np.arange(100), |
| 35 | + "city": ["NYC", "LA", "Chicago"], |
| 36 | + "scenario": ["baseline", "warming"], |
| 37 | + }, |
| 38 | + name="temperature", |
184 | 39 | ) |
185 | 40 |
|
186 | | -# Override: put variable on color instead |
187 | | -# time→x, variable→color, city→facet_col, scenario→facet_row |
188 | | -fig = ds.pxplot.line(color='variable') |
| 41 | +# Create an interactive line plot |
| 42 | +# Dimensions auto-assign: time→x, city→color, scenario→facet_col |
| 43 | +fig = da.pxplot.line() |
| 44 | +fig.show() |
189 | 45 |
|
190 | | -# Reduce dims first if you have too many |
191 | | -ds.sel(scenario='baseline').pxplot.line() # 3 dims → fits in 3 slots |
| 46 | +# Easy customization |
| 47 | +fig.update_layout(title="Temperature Projections", template="plotly_dark") |
192 | 48 | ``` |
193 | 49 |
|
194 | | -### Describe alternatives you've considered |
195 | | - |
196 | | -### 1. External package (status quo) |
197 | | -Users can use `hvplot` (HoloViews-based) which provides a similar accessor. However: |
198 | | -- hvplot adds significant dependencies (HoloViews, Bokeh, Panel) |
199 | | -- Returns HoloViews objects, not Plotly figures |
200 | | -- Different ecosystem with its own learning curve |
201 | | - |
202 | | -### 2. Improve matplotlib plotting |
203 | | -Could add faceting/animation to current `.plot`, but: |
204 | | -- Matplotlib's static nature is fundamental |
205 | | -- Would require major refactoring of existing code |
206 | | -- Doesn't address the post-creation modification pain point |
207 | | - |
208 | | -### 3. Keep as third-party package |
209 | | -A `pxplot` package could exist independently. However: |
210 | | -- Discoverability suffers (users don't know it exists) |
211 | | -- Integration with xarray options/config is harder |
212 | | -- Fragmentation of the ecosystem |
213 | | - |
214 | | -### Additional context |
| 50 | +## Features |
215 | 51 |
|
216 | | -### Why Plotly Express specifically? |
| 52 | +- **Interactive plots**: Zoom, pan, hover for values, toggle traces |
| 53 | +- **Automatic dimension assignment**: Dimensions fill plot slots by position |
| 54 | +- **Easy customization**: Returns Plotly `Figure` objects |
| 55 | +- **Multiple plot types**: `line()`, `bar()`, `area()`, `scatter()`, `box()`, `imshow()` |
| 56 | +- **Faceting and animation**: Built-in support for subplot grids and animations |
217 | 57 |
|
218 | | -1. **Minimal API surface**: `px.line()`, `px.bar()`, etc. handle most layout concerns internally |
219 | | -2. **Returns modifiable objects**: `go.Figure` has a clean API for updates |
220 | | -3. **Wide adoption**: Plotly is well-known in the data science community |
221 | | -4. **Good defaults**: Sensible hover info, legends, and interactivity out of the box |
222 | | -5. **Export flexibility**: HTML, PNG, PDF, or embed in Dash/Jupyter |
| 58 | +## Dimension Assignment |
223 | 59 |
|
224 | | -### Implementation Notes |
225 | | - |
226 | | -The dimension→slot assignment algorithm is simple and predictable: |
| 60 | +Dimensions are automatically assigned to plot "slots" based on their order: |
227 | 61 |
|
228 | 62 | ```python |
229 | | -SLOT_ORDER = ('x', 'color', 'facet_col', 'facet_row', 'animation_frame') |
230 | | - |
231 | | -def assign_slots(ds, *, x=auto, color=auto, facet_col=auto, ...): |
232 | | - """ |
233 | | - Positional assignment: dimensions fill slots in order. |
234 | | - - Explicit assignments lock a dimension to a slot |
235 | | - - None skips a slot |
236 | | - - Remaining dims fill remaining slots by position |
237 | | - - Error if dims left over after all slots filled |
238 | | - """ |
239 | | - dims = list(ds.dims) |
240 | | - if len(ds.data_vars) > 1: |
241 | | - dims.append('variable') # pseudo-dimension |
242 | | - |
243 | | - slots = {} |
244 | | - used = set() |
245 | | - slot_queue = list(SLOT_ORDER) |
246 | | - |
247 | | - # Pass 1: Process explicit assignments |
248 | | - for slot, value in [('x', x), ('color', color), ...]: |
249 | | - if value is None: |
250 | | - slot_queue.remove(slot) # skip this slot |
251 | | - elif value is not auto: |
252 | | - slots[slot] = value |
253 | | - used.add(value) |
254 | | - slot_queue.remove(slot) |
| 63 | +# dims: (time, city, scenario) |
| 64 | +# auto-assigns: time→x, city→color, scenario→facet_col |
| 65 | +da.pxplot.line() |
255 | 66 |
|
256 | | - # Pass 2: Fill remaining slots with remaining dims (by position) |
257 | | - remaining_dims = [d for d in dims if d not in used] |
258 | | - for slot, dim in zip(slot_queue, remaining_dims): |
259 | | - slots[slot] = dim |
260 | | - used.add(dim) |
| 67 | +# Override with explicit assignments |
| 68 | +da.pxplot.line(x="time", color="scenario", facet_col="city") |
261 | 69 |
|
262 | | - # Check for unassigned dimensions |
263 | | - unassigned = [d for d in dims if d not in used] |
264 | | - if unassigned: |
265 | | - raise ValueError( |
266 | | - f"Unassigned dimension(s): {unassigned}. " |
267 | | - "Reduce with .sel(), .isel(), or .mean() before plotting." |
268 | | - ) |
269 | | - |
270 | | - return slots |
| 70 | +# Skip a slot with None |
| 71 | +da.pxplot.line(color=None) # time→x, city→facet_col |
271 | 72 | ``` |
272 | 73 |
|
273 | | -### Proof of Concept |
274 | | - |
275 | | -I have implemented this pattern in a domain-specific package ([flixopt](https://github.com/flixOpt/flixopt)) as `.fxplot` and it has proven valuable for exploring multi-dimensional optimization results. The patterns are generic and would benefit the broader xarray community. |
| 74 | +## Available Methods |
276 | 75 |
|
277 | | -### Dependency Considerations |
| 76 | +| Method | Description | Slot Order | |
| 77 | +|--------|-------------|------------| |
| 78 | +| `line()` | Line plot | x → color → line_dash → symbol → facet_col → facet_row → animation_frame | |
| 79 | +| `bar()` | Bar chart | x → color → pattern_shape → facet_col → facet_row → animation_frame | |
| 80 | +| `area()` | Stacked area | x → color → pattern_shape → facet_col → facet_row → animation_frame | |
| 81 | +| `scatter()` | Scatter plot | x → color → size → symbol → facet_col → facet_row → animation_frame | |
| 82 | +| `box()` | Box plot | x → color → facet_col → facet_row → animation_frame | |
| 83 | +| `imshow()` | Heatmap | y → x → facet_col → animation_frame | |
278 | 84 |
|
279 | | -This would add `plotly` as an optional dependency: |
280 | | -```python |
281 | | -try: |
282 | | - import plotly.express as px |
283 | | - import plotly.graph_objects as go |
284 | | -except ImportError: |
285 | | - raise ImportError("pxplot requires plotly. Install with: pip install plotly") |
286 | | -``` |
| 85 | +## Documentation |
287 | 86 |
|
288 | | -## Summary |
| 87 | +Full documentation with examples: [https://felix.github.io/xarray-plotly](https://felix.github.io/xarray-plotly) |
289 | 88 |
|
290 | | -A `pxplot` accessor would provide: |
291 | | -- Interactive plots with zero additional code |
292 | | -- Simple, predictable positional assignment of dimensions to visual slots |
293 | | -- Explicit overrides and `None` to skip slots |
294 | | -- Easy post-creation customization via Plotly's `go.Figure` API |
295 | | -- Reduced maintenance burden compared to matplotlib's complexity |
296 | | -- Modern visualization patterns (faceting, animation) built-in |
| 89 | +## License |
297 | 90 |
|
298 | | -This aligns with xarray's philosophy of making multi-dimensional data easy to work with, extending that ease to visualization. |
| 91 | +MIT |
0 commit comments