xarray-plotly 0.0.4__tar.gz → 0.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/.github/workflows/ci.yml +32 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/.github/workflows/docs.yml +2 -1
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/PKG-INFO +3 -3
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/README.md +2 -2
- xarray_plotly-0.0.6/docs/examples/datasets.ipynb +261 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/mkdocs.yml +1 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/tests/test_accessor.py +49 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/xarray_plotly/accessor.py +24 -2
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/xarray_plotly/config.py +12 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/xarray_plotly/plotting.py +20 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/xarray_plotly.egg-info/PKG-INFO +3 -3
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/xarray_plotly.egg-info/SOURCES.txt +1 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/.github/dependabot.yml +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/.github/workflows/dependabot-auto-merge.yml +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/.github/workflows/release.yml +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/.gitignore +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/.pre-commit-config.yaml +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/CONTRIBUTING.md +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/LICENSE +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/docs/api.md +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/docs/examples/dimensions.ipynb +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/docs/examples/figure.ipynb +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/docs/examples/kwargs.ipynb +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/docs/examples/plot-types.ipynb +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/docs/getting-started.ipynb +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/docs/index.md +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/pyproject.toml +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/setup.cfg +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/tests/__init__.py +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/tests/test_common.py +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/tests/test_config.py +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/xarray_plotly/__init__.py +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/xarray_plotly/common.py +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/xarray_plotly/py.typed +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/xarray_plotly.egg-info/dependency_links.txt +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/xarray_plotly.egg-info/requires.txt +0 -0
- {xarray_plotly-0.0.4 → xarray_plotly-0.0.6}/xarray_plotly.egg-info/top_level.txt +0 -0
|
@@ -40,3 +40,35 @@ jobs:
|
|
|
40
40
|
with:
|
|
41
41
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
42
42
|
fail_ci_if_error: false
|
|
43
|
+
|
|
44
|
+
docs:
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v6
|
|
48
|
+
|
|
49
|
+
- name: Install uv
|
|
50
|
+
uses: astral-sh/setup-uv@v7
|
|
51
|
+
|
|
52
|
+
- name: Install dependencies
|
|
53
|
+
run: uv sync --extra docs
|
|
54
|
+
|
|
55
|
+
- name: Build docs
|
|
56
|
+
run: uv run mkdocs build
|
|
57
|
+
|
|
58
|
+
ci-success:
|
|
59
|
+
name: CI Success
|
|
60
|
+
needs: [test, docs]
|
|
61
|
+
if: always()
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- name: Check all jobs passed
|
|
65
|
+
run: |
|
|
66
|
+
if [[ "${{ needs.test.result }}" != "success" ]]; then
|
|
67
|
+
echo "Test job failed or was cancelled"
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
if [[ "${{ needs.docs.result }}" != "success" ]]; then
|
|
71
|
+
echo "Docs job failed or was cancelled"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
echo "All CI checks passed!"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name: Docs
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
|
|
4
|
+
pull_request:
|
|
5
5
|
branches: [main]
|
|
6
6
|
release:
|
|
7
7
|
types: [published]
|
|
@@ -26,6 +26,7 @@ jobs:
|
|
|
26
26
|
run: uv run mkdocs build
|
|
27
27
|
|
|
28
28
|
- name: Deploy to GitHub Pages
|
|
29
|
+
if: github.event_name == 'release'
|
|
29
30
|
uses: peaceiris/actions-gh-pages@v4
|
|
30
31
|
with:
|
|
31
32
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xarray_plotly
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Interactive Plotly Express plotting accessor for xarray
|
|
5
5
|
Author: Felix
|
|
6
6
|
License: MIT
|
|
@@ -44,8 +44,8 @@ Dynamic: license-file
|
|
|
44
44
|
|
|
45
45
|
**Interactive Plotly Express plotting for xarray**
|
|
46
46
|
|
|
47
|
-
[](https://badge.fury.io/py/xarray-plotly)
|
|
48
|
+
[](https://pypi.org/project/xarray-plotly/)
|
|
49
49
|
[](https://github.com/FBumann/xarray_plotly/actions)
|
|
50
50
|
[](https://fbumann.github.io/xarray_plotly/)
|
|
51
51
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Interactive Plotly Express plotting for xarray**
|
|
4
4
|
|
|
5
|
-
[](https://badge.fury.io/py/xarray-plotly)
|
|
6
|
+
[](https://pypi.org/project/xarray-plotly/)
|
|
7
7
|
[](https://github.com/FBumann/xarray_plotly/actions)
|
|
8
8
|
[](https://fbumann.github.io/xarray_plotly/)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"metadata": {},
|
|
6
|
+
"source": [
|
|
7
|
+
"# Dataset Plotting\n",
|
|
8
|
+
"\n",
|
|
9
|
+
"Plot multiple variables from an xarray Dataset with automatic or custom slot assignment."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"cell_type": "code",
|
|
14
|
+
"execution_count": null,
|
|
15
|
+
"metadata": {},
|
|
16
|
+
"outputs": [],
|
|
17
|
+
"source": [
|
|
18
|
+
"import numpy as np\n",
|
|
19
|
+
"import xarray as xr\n",
|
|
20
|
+
"\n",
|
|
21
|
+
"from xarray_plotly import config, xpx\n",
|
|
22
|
+
"\n",
|
|
23
|
+
"config.notebook()"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"cell_type": "code",
|
|
28
|
+
"execution_count": null,
|
|
29
|
+
"metadata": {},
|
|
30
|
+
"outputs": [],
|
|
31
|
+
"source": [
|
|
32
|
+
"# Create a Dataset with multiple variables\n",
|
|
33
|
+
"time = np.arange(50)\n",
|
|
34
|
+
"cities = [\"NYC\", \"LA\", \"Chicago\"]\n",
|
|
35
|
+
"\n",
|
|
36
|
+
"ds = xr.Dataset(\n",
|
|
37
|
+
" {\n",
|
|
38
|
+
" \"temperature\": ([\"time\", \"city\"], 20 + 5 * np.random.randn(50, 3).cumsum(axis=0) / 10),\n",
|
|
39
|
+
" \"humidity\": ([\"time\", \"city\"], 50 + 10 * np.random.randn(50, 3).cumsum(axis=0) / 10),\n",
|
|
40
|
+
" \"pressure\": ([\"time\", \"city\"], 1013 + np.random.randn(50, 3).cumsum(axis=0)),\n",
|
|
41
|
+
" },\n",
|
|
42
|
+
" coords={\"time\": time, \"city\": cities},\n",
|
|
43
|
+
")\n",
|
|
44
|
+
"ds"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"cell_type": "markdown",
|
|
49
|
+
"metadata": {},
|
|
50
|
+
"source": [
|
|
51
|
+
"## Plot All Variables\n",
|
|
52
|
+
"\n",
|
|
53
|
+
"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:"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"cell_type": "code",
|
|
58
|
+
"execution_count": null,
|
|
59
|
+
"metadata": {},
|
|
60
|
+
"outputs": [],
|
|
61
|
+
"source": [
|
|
62
|
+
"# All variables: time -> x, variable -> color, city -> line_dash\n",
|
|
63
|
+
"xpx(ds).line()"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"cell_type": "markdown",
|
|
68
|
+
"metadata": {},
|
|
69
|
+
"source": [
|
|
70
|
+
"## Control Where \"variable\" Goes\n",
|
|
71
|
+
"\n",
|
|
72
|
+
"The `\"variable\"` dimension can be assigned to any slot:"
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"cell_type": "code",
|
|
77
|
+
"execution_count": null,
|
|
78
|
+
"metadata": {},
|
|
79
|
+
"outputs": [],
|
|
80
|
+
"source": [
|
|
81
|
+
"# Variables as facet columns\n",
|
|
82
|
+
"xpx(ds).line(facet_col=\"variable\")"
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"cell_type": "code",
|
|
87
|
+
"execution_count": null,
|
|
88
|
+
"metadata": {},
|
|
89
|
+
"outputs": [],
|
|
90
|
+
"source": [
|
|
91
|
+
"# Variables as rows, cities as columns\n",
|
|
92
|
+
"xpx(ds).line(facet_row=\"variable\", facet_col=\"city\")"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"cell_type": "markdown",
|
|
97
|
+
"metadata": {},
|
|
98
|
+
"source": [
|
|
99
|
+
"## Configure Default \"variable\" Position\n",
|
|
100
|
+
"\n",
|
|
101
|
+
"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",
|
|
102
|
+
"\n",
|
|
103
|
+
"You can change this globally with `config.set_options()`:"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"cell_type": "code",
|
|
108
|
+
"execution_count": null,
|
|
109
|
+
"metadata": {},
|
|
110
|
+
"outputs": [],
|
|
111
|
+
"source": [
|
|
112
|
+
"# Default: position=1 (second) -> variable goes to color\n",
|
|
113
|
+
"# Note: to_array() puts \"variable\" first, but xpx() reorders it to position 1\n",
|
|
114
|
+
"print(\"Raw to_array() dims:\", ds.to_array().dims) # (variable, time, city)\n",
|
|
115
|
+
"print(\"After xpx reorder: (time, variable, city)\") # time->x, variable->color\n",
|
|
116
|
+
"xpx(ds).line(title=\"Default: variable as color (position=1)\")"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"cell_type": "code",
|
|
121
|
+
"execution_count": null,
|
|
122
|
+
"metadata": {},
|
|
123
|
+
"outputs": [],
|
|
124
|
+
"source": [
|
|
125
|
+
"# Position 0: variable goes first (x-axis) - usually not what you want!\n",
|
|
126
|
+
"with config.set_options(dataset_variable_position=0):\n",
|
|
127
|
+
" fig = xpx(ds).line(title=\"position=0: variable on x-axis (probably not desired)\")\n",
|
|
128
|
+
"fig"
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"cell_type": "code",
|
|
133
|
+
"execution_count": null,
|
|
134
|
+
"metadata": {},
|
|
135
|
+
"outputs": [],
|
|
136
|
+
"source": [
|
|
137
|
+
"# Position -1: variable goes last -> city gets color, variable gets line_dash\n",
|
|
138
|
+
"with config.set_options(dataset_variable_position=-1):\n",
|
|
139
|
+
" fig = xpx(ds).line(title=\"position=-1: variable as line_dash\")\n",
|
|
140
|
+
"fig"
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"cell_type": "markdown",
|
|
145
|
+
"metadata": {},
|
|
146
|
+
"source": [
|
|
147
|
+
"## Plot a Single Variable\n",
|
|
148
|
+
"\n",
|
|
149
|
+
"Use `var=\"name\"` to plot just one variable:"
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"cell_type": "code",
|
|
154
|
+
"execution_count": null,
|
|
155
|
+
"metadata": {},
|
|
156
|
+
"outputs": [],
|
|
157
|
+
"source": [
|
|
158
|
+
"xpx(ds).line(var=\"temperature\", title=\"Temperature Only\")"
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"cell_type": "markdown",
|
|
163
|
+
"metadata": {},
|
|
164
|
+
"source": [
|
|
165
|
+
"## Different Plot Types"
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"cell_type": "code",
|
|
170
|
+
"execution_count": null,
|
|
171
|
+
"metadata": {},
|
|
172
|
+
"outputs": [],
|
|
173
|
+
"source": [
|
|
174
|
+
"# Bar chart - latest values by city\n",
|
|
175
|
+
"xpx(ds.isel(time=-1)).bar(x=\"city\", color=\"variable\", barmode=\"group\")"
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"cell_type": "code",
|
|
180
|
+
"execution_count": null,
|
|
181
|
+
"metadata": {},
|
|
182
|
+
"outputs": [],
|
|
183
|
+
"source": [
|
|
184
|
+
"# Box plot - distribution by variable\n",
|
|
185
|
+
"xpx(ds).box(x=\"variable\", color=\"city\")"
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"cell_type": "code",
|
|
190
|
+
"execution_count": null,
|
|
191
|
+
"metadata": {},
|
|
192
|
+
"outputs": [],
|
|
193
|
+
"source": [
|
|
194
|
+
"# Area chart\n",
|
|
195
|
+
"xpx(ds).area(var=\"humidity\", title=\"Humidity Over Time\")"
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"cell_type": "code",
|
|
200
|
+
"execution_count": null,
|
|
201
|
+
"metadata": {},
|
|
202
|
+
"outputs": [],
|
|
203
|
+
"source": [
|
|
204
|
+
"# Scatter\n",
|
|
205
|
+
"xpx(ds).scatter(var=\"temperature\", title=\"Temperature Scatter\")"
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"cell_type": "code",
|
|
210
|
+
"execution_count": null,
|
|
211
|
+
"metadata": {},
|
|
212
|
+
"outputs": [],
|
|
213
|
+
"source": [
|
|
214
|
+
"# Pie chart - snapshot at one time\n",
|
|
215
|
+
"xpx(ds.isel(time=-1)).pie(var=\"temperature\", names=\"city\", title=\"Temperature Distribution\")"
|
|
216
|
+
]
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"cell_type": "markdown",
|
|
220
|
+
"metadata": {},
|
|
221
|
+
"source": [
|
|
222
|
+
"## Combining Slot Assignments\n",
|
|
223
|
+
"\n",
|
|
224
|
+
"Mix explicit assignments with auto-assignment:"
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"cell_type": "code",
|
|
229
|
+
"execution_count": null,
|
|
230
|
+
"metadata": {},
|
|
231
|
+
"outputs": [],
|
|
232
|
+
"source": [
|
|
233
|
+
"# Explicit: variable -> facet_col, let city auto-assign to color\n",
|
|
234
|
+
"xpx(ds).line(facet_col=\"variable\", color=\"city\")"
|
|
235
|
+
]
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"cell_type": "code",
|
|
239
|
+
"execution_count": null,
|
|
240
|
+
"metadata": {},
|
|
241
|
+
"outputs": [],
|
|
242
|
+
"source": [
|
|
243
|
+
"# Skip color slot with None\n",
|
|
244
|
+
"xpx(ds).line(var=\"temperature\", color=None)"
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
"metadata": {
|
|
249
|
+
"kernelspec": {
|
|
250
|
+
"display_name": "Python 3",
|
|
251
|
+
"language": "python",
|
|
252
|
+
"name": "python3"
|
|
253
|
+
},
|
|
254
|
+
"language_info": {
|
|
255
|
+
"name": "python",
|
|
256
|
+
"version": "3.12.0"
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
"nbformat": 4,
|
|
260
|
+
"nbformat_minor": 4
|
|
261
|
+
}
|
|
@@ -69,6 +69,7 @@ nav:
|
|
|
69
69
|
- Getting Started: getting-started.ipynb
|
|
70
70
|
- Examples:
|
|
71
71
|
- Plot Types: examples/plot-types.ipynb
|
|
72
|
+
- Dataset Plotting: examples/datasets.ipynb
|
|
72
73
|
- Dimensions & Facets: examples/dimensions.ipynb
|
|
73
74
|
- Plotly Express Options: examples/kwargs.ipynb
|
|
74
75
|
- Figure Customization: examples/figure.ipynb
|
|
@@ -292,3 +292,52 @@ class TestDatasetPlotlyAccessor:
|
|
|
292
292
|
"""Test box plot with all variables."""
|
|
293
293
|
fig = self.ds.plotly.box()
|
|
294
294
|
assert isinstance(fig, go.Figure)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TestImshowBounds:
|
|
298
|
+
"""Tests for imshow global bounds and robust mode."""
|
|
299
|
+
|
|
300
|
+
def test_imshow_global_bounds(self) -> None:
|
|
301
|
+
"""Test that imshow uses global min/max by default."""
|
|
302
|
+
da = xr.DataArray(
|
|
303
|
+
np.array([[[1, 2], [3, 4]], [[5, 6], [7, 100]]]),
|
|
304
|
+
dims=["time", "y", "x"],
|
|
305
|
+
)
|
|
306
|
+
fig = da.plotly.imshow(animation_frame="time")
|
|
307
|
+
# Check coloraxis for zmin/zmax (plotly stores them there)
|
|
308
|
+
coloraxis = fig.layout.coloraxis
|
|
309
|
+
assert coloraxis.cmin == 1.0
|
|
310
|
+
assert coloraxis.cmax == 100.0
|
|
311
|
+
|
|
312
|
+
def test_imshow_robust_bounds(self) -> None:
|
|
313
|
+
"""Test that robust=True uses percentile-based bounds."""
|
|
314
|
+
# Create data with outlier
|
|
315
|
+
data = np.random.rand(10, 20) * 100
|
|
316
|
+
data[0, 0] = 10000 # extreme outlier
|
|
317
|
+
da = xr.DataArray(data, dims=["y", "x"])
|
|
318
|
+
|
|
319
|
+
fig = da.plotly.imshow(robust=True)
|
|
320
|
+
# With robust=True, cmax should be much less than the outlier
|
|
321
|
+
coloraxis = fig.layout.coloraxis
|
|
322
|
+
assert coloraxis.cmax < 10000
|
|
323
|
+
assert coloraxis.cmax < 200 # Should be around 98th percentile (~98)
|
|
324
|
+
|
|
325
|
+
def test_imshow_user_zmin_zmax_override(self) -> None:
|
|
326
|
+
"""Test that user-provided zmin/zmax overrides auto bounds."""
|
|
327
|
+
da = xr.DataArray(np.random.rand(10, 20) * 100, dims=["y", "x"])
|
|
328
|
+
fig = da.plotly.imshow(zmin=0, zmax=50)
|
|
329
|
+
coloraxis = fig.layout.coloraxis
|
|
330
|
+
assert coloraxis.cmin == 0
|
|
331
|
+
assert coloraxis.cmax == 50
|
|
332
|
+
|
|
333
|
+
def test_imshow_animation_consistent_bounds(self) -> None:
|
|
334
|
+
"""Test that animation frames have consistent color bounds."""
|
|
335
|
+
da = xr.DataArray(
|
|
336
|
+
np.array([[[0, 10], [20, 30]], [[40, 50], [60, 70]]]),
|
|
337
|
+
dims=["time", "y", "x"],
|
|
338
|
+
)
|
|
339
|
+
fig = da.plotly.imshow(animation_frame="time")
|
|
340
|
+
# All frames should use global min (0) and max (70)
|
|
341
|
+
coloraxis = fig.layout.coloraxis
|
|
342
|
+
assert coloraxis.cmin == 0.0
|
|
343
|
+
assert coloraxis.cmax == 70.0
|
|
@@ -7,6 +7,7 @@ from xarray import DataArray, Dataset
|
|
|
7
7
|
|
|
8
8
|
from xarray_plotly import plotting
|
|
9
9
|
from xarray_plotly.common import SlotValue, auto
|
|
10
|
+
from xarray_plotly.config import _options
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class DataArrayPlotlyAccessor:
|
|
@@ -249,6 +250,7 @@ class DataArrayPlotlyAccessor:
|
|
|
249
250
|
y: SlotValue = auto,
|
|
250
251
|
facet_col: SlotValue = auto,
|
|
251
252
|
animation_frame: SlotValue = auto,
|
|
253
|
+
robust: bool = False,
|
|
252
254
|
**px_kwargs: Any,
|
|
253
255
|
) -> go.Figure:
|
|
254
256
|
"""Create an interactive heatmap image.
|
|
@@ -260,7 +262,9 @@ class DataArrayPlotlyAccessor:
|
|
|
260
262
|
y: Dimension for y-axis (rows). Default: first dimension.
|
|
261
263
|
facet_col: Dimension for subplot columns. Default: third dimension.
|
|
262
264
|
animation_frame: Dimension for animation. Default: fourth dimension.
|
|
265
|
+
robust: If True, use 2nd/98th percentiles for color bounds (handles outliers).
|
|
263
266
|
**px_kwargs: Additional arguments passed to `plotly.express.imshow()`.
|
|
267
|
+
Use `zmin` and `zmax` to manually set color scale bounds.
|
|
264
268
|
|
|
265
269
|
Returns:
|
|
266
270
|
Interactive Plotly Figure.
|
|
@@ -271,6 +275,7 @@ class DataArrayPlotlyAccessor:
|
|
|
271
275
|
y=y,
|
|
272
276
|
facet_col=facet_col,
|
|
273
277
|
animation_frame=animation_frame,
|
|
278
|
+
robust=robust,
|
|
274
279
|
**px_kwargs,
|
|
275
280
|
)
|
|
276
281
|
|
|
@@ -349,9 +354,26 @@ class DatasetPlotlyAccessor:
|
|
|
349
354
|
return list(self.__all__) + list(super().__dir__())
|
|
350
355
|
|
|
351
356
|
def _get_dataarray(self, var: str | None) -> DataArray:
|
|
352
|
-
"""Get DataArray from Dataset, either single var or all via to_array().
|
|
357
|
+
"""Get DataArray from Dataset, either single var or all via to_array().
|
|
358
|
+
|
|
359
|
+
When combining all variables, "variable" is placed at the position
|
|
360
|
+
specified by config.dataset_variable_position (default 1, second position).
|
|
361
|
+
Supports Python-style negative indexing: -1 = last, -2 = second-to-last, etc.
|
|
362
|
+
"""
|
|
353
363
|
if var is None:
|
|
354
|
-
|
|
364
|
+
da = self._ds.to_array(dim="variable")
|
|
365
|
+
pos = _options.dataset_variable_position
|
|
366
|
+
# Move "variable" to configured position
|
|
367
|
+
if len(da.dims) > 1 and pos != 0:
|
|
368
|
+
dims = list(da.dims)
|
|
369
|
+
dims.remove("variable")
|
|
370
|
+
# Use Python-style indexing (handles negative indices correctly)
|
|
371
|
+
# Clamp to valid range: -1 -> last, -2 -> second-to-last, etc.
|
|
372
|
+
n = len(dims)
|
|
373
|
+
insert_pos = max(0, n + pos + 1) if pos < 0 else min(pos, n)
|
|
374
|
+
dims.insert(insert_pos, "variable")
|
|
375
|
+
da = da.transpose(*dims)
|
|
376
|
+
return da
|
|
355
377
|
return self._ds[var]
|
|
356
378
|
|
|
357
379
|
def line(
|
|
@@ -58,6 +58,9 @@ class Options:
|
|
|
58
58
|
label_include_units: Append units to labels. Default True.
|
|
59
59
|
label_unit_format: Format string for units. Use `{units}` as placeholder.
|
|
60
60
|
slot_orders: Slot orders per plot type. Keys are plot types, values are tuples.
|
|
61
|
+
dataset_variable_position: Position of "variable" dim when plotting all Dataset
|
|
62
|
+
variables. Default 1 (second position, typically color). Set to 0 for first
|
|
63
|
+
position (x-axis), or -1 for last position.
|
|
61
64
|
"""
|
|
62
65
|
|
|
63
66
|
label_use_long_name: bool = True
|
|
@@ -67,6 +70,7 @@ class Options:
|
|
|
67
70
|
slot_orders: dict[str, tuple[str, ...]] = field(
|
|
68
71
|
default_factory=lambda: dict(DEFAULT_SLOT_ORDERS)
|
|
69
72
|
)
|
|
73
|
+
dataset_variable_position: int = 1
|
|
70
74
|
|
|
71
75
|
def to_dict(self) -> dict[str, Any]:
|
|
72
76
|
"""Return options as a dictionary."""
|
|
@@ -76,6 +80,7 @@ class Options:
|
|
|
76
80
|
"label_include_units": self.label_include_units,
|
|
77
81
|
"label_unit_format": self.label_unit_format,
|
|
78
82
|
"slot_orders": self.slot_orders,
|
|
83
|
+
"dataset_variable_position": self.dataset_variable_position,
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
|
|
@@ -106,6 +111,7 @@ def set_options(
|
|
|
106
111
|
label_include_units: bool | None = None,
|
|
107
112
|
label_unit_format: str | None = None,
|
|
108
113
|
slot_orders: dict[str, tuple[str, ...]] | None = None,
|
|
114
|
+
dataset_variable_position: int | None = None,
|
|
109
115
|
) -> Generator[None, None, None]:
|
|
110
116
|
"""Set xarray_plotly options globally or as a context manager.
|
|
111
117
|
|
|
@@ -115,6 +121,8 @@ def set_options(
|
|
|
115
121
|
label_include_units: Append units to labels.
|
|
116
122
|
label_unit_format: Format string for units. Use `{units}` as placeholder.
|
|
117
123
|
slot_orders: Slot orders per plot type.
|
|
124
|
+
dataset_variable_position: Position of "variable" dim when plotting all Dataset
|
|
125
|
+
variables. Default 1 (second, typically color). Use 0 for first, -1 for last.
|
|
118
126
|
|
|
119
127
|
Yields:
|
|
120
128
|
None when used as a context manager.
|
|
@@ -136,6 +144,7 @@ def set_options(
|
|
|
136
144
|
"label_include_units": _options.label_include_units,
|
|
137
145
|
"label_unit_format": _options.label_unit_format,
|
|
138
146
|
"slot_orders": dict(_options.slot_orders),
|
|
147
|
+
"dataset_variable_position": _options.dataset_variable_position,
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
# Apply new values (modify in place to keep reference)
|
|
@@ -149,6 +158,8 @@ def set_options(
|
|
|
149
158
|
_options.label_unit_format = label_unit_format
|
|
150
159
|
if slot_orders is not None:
|
|
151
160
|
_options.slot_orders = dict(slot_orders)
|
|
161
|
+
if dataset_variable_position is not None:
|
|
162
|
+
_options.dataset_variable_position = dataset_variable_position
|
|
152
163
|
|
|
153
164
|
try:
|
|
154
165
|
yield
|
|
@@ -159,6 +170,7 @@ def set_options(
|
|
|
159
170
|
_options.label_include_units = old_values["label_include_units"]
|
|
160
171
|
_options.label_unit_format = old_values["label_unit_format"]
|
|
161
172
|
_options.slot_orders = old_values["slot_orders"]
|
|
173
|
+
_options.dataset_variable_position = old_values["dataset_variable_position"]
|
|
162
174
|
|
|
163
175
|
|
|
164
176
|
def notebook(renderer: str = "notebook") -> None:
|
|
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
|
+
import numpy as np
|
|
9
10
|
import plotly.express as px
|
|
10
11
|
|
|
11
12
|
from xarray_plotly.common import (
|
|
@@ -398,6 +399,7 @@ def imshow(
|
|
|
398
399
|
y: SlotValue = auto,
|
|
399
400
|
facet_col: SlotValue = auto,
|
|
400
401
|
animation_frame: SlotValue = auto,
|
|
402
|
+
robust: bool = False,
|
|
401
403
|
**px_kwargs: Any,
|
|
402
404
|
) -> go.Figure:
|
|
403
405
|
"""
|
|
@@ -418,8 +420,12 @@ def imshow(
|
|
|
418
420
|
Dimension for subplot columns. Default: third dimension.
|
|
419
421
|
animation_frame
|
|
420
422
|
Dimension for animation. Default: fourth dimension.
|
|
423
|
+
robust
|
|
424
|
+
If True, compute color bounds using 2nd and 98th percentiles
|
|
425
|
+
for robustness against outliers. Default: False.
|
|
421
426
|
**px_kwargs
|
|
422
427
|
Additional arguments passed to `plotly.express.imshow()`.
|
|
428
|
+
Use `zmin` and `zmax` to manually set color scale bounds.
|
|
423
429
|
|
|
424
430
|
Returns
|
|
425
431
|
-------
|
|
@@ -440,6 +446,20 @@ def imshow(
|
|
|
440
446
|
]
|
|
441
447
|
plot_data = darray.transpose(*transpose_order) if transpose_order else darray
|
|
442
448
|
|
|
449
|
+
# Compute global color bounds if not provided
|
|
450
|
+
if "zmin" not in px_kwargs or "zmax" not in px_kwargs:
|
|
451
|
+
values = plot_data.values
|
|
452
|
+
if robust:
|
|
453
|
+
# Use percentiles for outlier robustness
|
|
454
|
+
zmin = float(np.nanpercentile(values, 2))
|
|
455
|
+
zmax = float(np.nanpercentile(values, 98))
|
|
456
|
+
else:
|
|
457
|
+
# Use global min/max across all data
|
|
458
|
+
zmin = float(np.nanmin(values))
|
|
459
|
+
zmax = float(np.nanmax(values))
|
|
460
|
+
px_kwargs.setdefault("zmin", zmin)
|
|
461
|
+
px_kwargs.setdefault("zmax", zmax)
|
|
462
|
+
|
|
443
463
|
return px.imshow(
|
|
444
464
|
plot_data,
|
|
445
465
|
facet_col=slots.get("facet_col"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xarray_plotly
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Interactive Plotly Express plotting accessor for xarray
|
|
5
5
|
Author: Felix
|
|
6
6
|
License: MIT
|
|
@@ -44,8 +44,8 @@ Dynamic: license-file
|
|
|
44
44
|
|
|
45
45
|
**Interactive Plotly Express plotting for xarray**
|
|
46
46
|
|
|
47
|
-
[](https://badge.fury.io/py/xarray-plotly)
|
|
48
|
+
[](https://pypi.org/project/xarray-plotly/)
|
|
49
49
|
[](https://github.com/FBumann/xarray_plotly/actions)
|
|
50
50
|
[](https://fbumann.github.io/xarray_plotly/)
|
|
51
51
|
[](https://opensource.org/licenses/MIT)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|