pyviewarr 0.3.0__tar.gz → 0.3.2__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.
@@ -12,3 +12,6 @@ __pycache__
12
12
  src/pyviewarr/static
13
13
  viewarr/target
14
14
  viewarr/pkg
15
+
16
+ # playwright test artifacts
17
+ test-results/
@@ -0,0 +1,286 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyviewarr
3
+ Version: 0.3.2
4
+ License-File: LICENSE
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: anywidget<0.10,>=0.9
7
+ Requires-Dist: numpy<3,>=2.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: jupyterlab; extra == 'dev'
10
+ Requires-Dist: matplotlib>=3.5; extra == 'dev'
11
+ Requires-Dist: pytest>=7.0; extra == 'dev'
12
+ Requires-Dist: watchfiles; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # pyviewarr
16
+
17
+ 🤖 _**Clanker code disclaimer:** written (largely) with large language model coding tools. Use at your own risk._ 🤖
18
+
19
+ A faster, more intuitive way to explore 2D data (i.e. monochromatic images) in Python notebooks
20
+
21
+ ![Screenshot of pyviewarr displaying a galaxy image in a notebook](./screenshots/pyviewarr_screenshot.png)
22
+
23
+
24
+ ## Installation
25
+
26
+ ```sh
27
+ pip install pyviewarr
28
+ ```
29
+
30
+ or with [uv](https://github.com/astral-sh/uv):
31
+
32
+ ```sh
33
+ uv add pyviewarr
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ First, open a notebook interface like JupyterLab (or [another](https://anywidget.dev/en/community/) supporting anywidget).
39
+
40
+ ### Basic
41
+
42
+ `pyviewarr.show()` returns a widget instance. If it's the last line of your cell, it will display automatically.
43
+
44
+ ```python
45
+ import numpy as np
46
+
47
+ x = np.arange(16).reshape(4, 4)
48
+ pyviewarr.show(x)
49
+ ```
50
+
51
+ ![](./screenshots/basic.png)
52
+
53
+ Width and height of the widget can be set in the call to `show()`:
54
+
55
+ ```python
56
+ pyviewarr.show(x, width=500, height=400)
57
+ ```
58
+
59
+ ![](./screenshots/basic_small.png)
60
+
61
+ You can also explicitly display with [`IPython.display.display()`](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.display):
62
+
63
+
64
+ ```python
65
+ widget = pyviewarr.show(x)
66
+ from IPython.display import display
67
+ display(widget)
68
+ ```
69
+
70
+ If there are more than two axes, the last two axes are treated as Y and X (NumPy convention) and navigation buttons are shown for any others.
71
+
72
+ Navigating in a large datacube remains responsive because only one slice/image is sent to the viewer at a time.
73
+
74
+ ### Image stretching
75
+
76
+ Mapping array values to images for display involves normalization (stretching) which can include nonlinear transformations.
77
+
78
+ For all of these manipulations, you can reset them with the reset button:
79
+
80
+ ![Reset button](screenshots/reset.png)
81
+
82
+ #### Stretching
83
+
84
+ Right-clicking and dragging on the image viewer widget changes the contrast and bias. (If you have used ds9, this will feel familiar.)
85
+
86
+ #### Limits
87
+
88
+ When the image loads, the viewer sets the color limits to `min(image)` and `max(image)`. Type in new limits if you want.
89
+
90
+ If you want to avoid resetting the limits (e.g. when paging through a data cube) use the lock button to fix the limits at their current values.
91
+
92
+ #### Linear/Log
93
+
94
+ Only three colormaps are included: gray, inferno, and magma (from matplotlib). Log stretch behaves like ds9 as well, such that negative values are rescaled and don't make invalid pixel values.
95
+
96
+ #### Diverging (±)
97
+
98
+ When examining residuals, you may want a diverging colormap centered at zero. Use the ± button to switch the limits to symmetric and the available colormaps to RdBu and RdYlBu (from matplotlib).
99
+
100
+ ### Zooming, panning, rotation
101
+
102
+ Zoom by clicking the +/- buttons at lower right, scrolling with your mouse/trackpad, or using the `-` and `=` keys. Reset zoom with the "reset zoom to fit" button or the `0` key.
103
+
104
+ Pan by clicking and dragging. Cmd/Ctrl-Click to center the pixel under the cursor.
105
+
106
+ Use the left and right arrow buttons to rotate in 15º increments, or enter a number in degrees (counter-clockwise) in the box between them.
107
+
108
+ By default, rotation is about the image center, but you can set a pivot point by Cmd/Ctrl-Shift-clicking the desired point.
109
+
110
+ ### Matplotlib integration
111
+
112
+ Once you've rotated and panned and stretched and zoomed you may want to save a plot, but the viewer doesn't handle plotting.
113
+
114
+ Fortunately, the widget has a `plot_to_matplotlib()` method that takes a matplotlib.axes.Axes instance:
115
+
116
+ ```python
117
+ fig, ax = plt.subplots()
118
+ widget.plot_to_matplotlib(ax)
119
+ ```
120
+
121
+ **Note:** Rotating the axes makes the matplotlib tick labels useless, so they are hidden.
122
+
123
+ If you just want the contrast / bias / stretch, you can get a `norm` from the `get_normalization()` method:
124
+
125
+ ```python
126
+ norm = widget.get_normalization()
127
+ import matplotlib.pyplot as plt
128
+ plt.imshow(x, norm=norm)
129
+ ```
130
+
131
+ ### Widget API
132
+
133
+ #### `ViewerConfig`
134
+
135
+ Using the `ViewerConfig` dataclass or passing its arguments to `show()` lets you set the initial configured state of the viewer.
136
+
137
+ ```python
138
+ pyviewarr.show(x, vmin=-10, vmax=10)
139
+ ```
140
+
141
+ See [example](./preconfigured_state_example.ipynb).
142
+
143
+ #### Setting data
144
+
145
+ If you retain a reference to the widget, you can set its contents from a later cell (if you want).
146
+
147
+ ```python
148
+ widget = pyviewarr.show(x)
149
+ widget
150
+ ```
151
+
152
+ Now you can change the live widget's contents by supplying a new array to `set_array()`.
153
+
154
+ ```
155
+ new_arr = np.random.randn(512, 512).astype(np.float32) * 100
156
+ widget.set_array(new_arr)
157
+ ```
158
+
159
+ ## Development
160
+
161
+ Be sure to clone with `git clone --recurse-submodules` (or, if you cloned first and **then** read this, `git submodule update --init --recursive` to initialize an existing clone).
162
+
163
+ The frontend part of the widget (i.e. `viewarr` itself) is written in Rust with egui, requiring a Rust compiler toolchain for `wasm32-unknown-unknown` and the [`wasm-pack`](https://drager.github.io/wasm-pack/installer/) tool installed.
164
+
165
+ Install rust with rustup: https://rustup.rs/
166
+
167
+ Now that you have `cargo`, install wasm-pack with `cargo install wasm-pack`.
168
+
169
+ Using [uv](https://github.com/astral-sh/uv) for development will automatically manage virtual environments and dependencies for you.
170
+
171
+ For live reloading in development:
172
+
173
+ ```sh
174
+ export ANYWIDGET_HMR=1
175
+ ```
176
+
177
+ Export this variable before starting JupyterLab so it will use hot module reloading.
178
+
179
+ ```sh
180
+ uv run jupyter lab example.ipynb
181
+ ```
182
+
183
+ Alternatively, create and manage your own virtual environment:
184
+
185
+ ```sh
186
+ python -m venv .venv
187
+ source .venv/bin/activate
188
+ pip install -e ".[dev]"
189
+ ```
190
+
191
+ The widget front-end code bundles it's JavaScript dependencies. After setting up Python,
192
+ make sure to install these dependencies locally:
193
+
194
+ ```sh
195
+ npm install
196
+ ```
197
+
198
+ While developing, you can run the following in a separate terminal to automatically
199
+ rebuild JavaScript as you make changes:
200
+
201
+ ```sh
202
+ npm run dev
203
+ ```
204
+
205
+ Open `example.ipynb` in JupyterLab, VS Code, or your favorite editor
206
+ to start developing. Changes made in `js/` will be reflected
207
+ in the notebook.
208
+
209
+ To do a full build, use the project script:
210
+
211
+ ```sh
212
+ npm run build:all
213
+ ```
214
+
215
+ ### End-to-end tests (Galata + Playwright)
216
+
217
+ The widget has browser E2E tests using JupyterLab's Galata helpers.
218
+
219
+ First-time setup for browser binaries:
220
+
221
+ ```sh
222
+ npm run test:e2e:install
223
+ ```
224
+
225
+ Run E2E tests headless:
226
+
227
+ ```sh
228
+ npm run test:e2e
229
+ ```
230
+
231
+ Run E2E tests with a visible browser:
232
+
233
+ ```sh
234
+ npm run test:e2e:headed
235
+ ```
236
+
237
+ The test server uses `tests/e2e/jupyter_server_config.py`, which enables Galata's JupyterLab test hooks.
238
+
239
+ ## Releasing
240
+
241
+ Note that `viewarr/` submodule changes will need to be committed and pushed first, then the submodule reference in this repository can be updated.
242
+
243
+ Release checklist:
244
+
245
+ 1. Update changelog entries and set `[project].version` in `pyproject.toml` to the release version (e.g. `0.3.2`), then commit.
246
+ 2. Create the Git tag/release for that version (e.g. `v0.3.2`).
247
+ 3. Immediately after release, bump `pyproject.toml` to the next development version (e.g. `0.3.3dev`) and commit.
248
+
249
+ GitHub releases are automatically pushed to PyPI by the workflow in [`.github/workflows/ci.yml`](./.github/workflows/ci.yml).
250
+
251
+ ## Changelog
252
+
253
+ ### Unreleased (since `v0.3.2`)
254
+
255
+ - No unreleased changes yet.
256
+
257
+ ### `v0.3.2`
258
+
259
+ #### pyviewarr changes
260
+
261
+ - Updated bundled `viewarr` submodule from `3797ee7` to `cc78498`.
262
+
263
+ #### Included `viewarr` changes
264
+
265
+ - Fixed diverging/symmetric colorbar limit behavior so `vmax` is the only editable limit, coerced positive, and `vmin` is displayed as `-vmax` in that mode.
266
+ - Fixed state callback / `getValueRange()` limit reporting in diverging/symmetric mode to reflect the effective displayed range.
267
+
268
+ ### `v0.3.1`
269
+
270
+ #### pyviewarr changes
271
+
272
+ - Added browser end-to-end testing with Playwright + JupyterLab Galata (`playwright.config.ts`, `tests/e2e/`).
273
+ - Added CI coverage for Playwright E2E tests in [`.github/workflows/ci.yml`](./.github/workflows/ci.yml).
274
+ - Expanded development documentation for build/test workflows.
275
+ - Updated bundled `viewarr` submodule from `3d74f69` to `3797ee7`.
276
+
277
+ #### Included `viewarr` changes
278
+
279
+ - Fixed Cmd/Ctrl-click centering coordinate mapping.
280
+ - Pinned wasm-bindgen stack to avoid `glow`/`js-sys` CI build breakages.
281
+ - Added/expanded viewer state APIs in JS/TS:
282
+ - `setViewerState(...)` for partial bulk state updates.
283
+ - `getZoom(...)` / `setZoom(...)`.
284
+ - `setColormap(...)` / `setColormapReversed(...)`.
285
+ - New `StretchMode` and `ViewerStateConfig` typings.
286
+ - Improved colormap name handling/parsing with canonical names and aliases (`gray`/`grayscale`/`greyscale`, etc.).
@@ -0,0 +1,272 @@
1
+ # pyviewarr
2
+
3
+ 🤖 _**Clanker code disclaimer:** written (largely) with large language model coding tools. Use at your own risk._ 🤖
4
+
5
+ A faster, more intuitive way to explore 2D data (i.e. monochromatic images) in Python notebooks
6
+
7
+ ![Screenshot of pyviewarr displaying a galaxy image in a notebook](./screenshots/pyviewarr_screenshot.png)
8
+
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ pip install pyviewarr
14
+ ```
15
+
16
+ or with [uv](https://github.com/astral-sh/uv):
17
+
18
+ ```sh
19
+ uv add pyviewarr
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ First, open a notebook interface like JupyterLab (or [another](https://anywidget.dev/en/community/) supporting anywidget).
25
+
26
+ ### Basic
27
+
28
+ `pyviewarr.show()` returns a widget instance. If it's the last line of your cell, it will display automatically.
29
+
30
+ ```python
31
+ import numpy as np
32
+
33
+ x = np.arange(16).reshape(4, 4)
34
+ pyviewarr.show(x)
35
+ ```
36
+
37
+ ![](./screenshots/basic.png)
38
+
39
+ Width and height of the widget can be set in the call to `show()`:
40
+
41
+ ```python
42
+ pyviewarr.show(x, width=500, height=400)
43
+ ```
44
+
45
+ ![](./screenshots/basic_small.png)
46
+
47
+ You can also explicitly display with [`IPython.display.display()`](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.display):
48
+
49
+
50
+ ```python
51
+ widget = pyviewarr.show(x)
52
+ from IPython.display import display
53
+ display(widget)
54
+ ```
55
+
56
+ If there are more than two axes, the last two axes are treated as Y and X (NumPy convention) and navigation buttons are shown for any others.
57
+
58
+ Navigating in a large datacube remains responsive because only one slice/image is sent to the viewer at a time.
59
+
60
+ ### Image stretching
61
+
62
+ Mapping array values to images for display involves normalization (stretching) which can include nonlinear transformations.
63
+
64
+ For all of these manipulations, you can reset them with the reset button:
65
+
66
+ ![Reset button](screenshots/reset.png)
67
+
68
+ #### Stretching
69
+
70
+ Right-clicking and dragging on the image viewer widget changes the contrast and bias. (If you have used ds9, this will feel familiar.)
71
+
72
+ #### Limits
73
+
74
+ When the image loads, the viewer sets the color limits to `min(image)` and `max(image)`. Type in new limits if you want.
75
+
76
+ If you want to avoid resetting the limits (e.g. when paging through a data cube) use the lock button to fix the limits at their current values.
77
+
78
+ #### Linear/Log
79
+
80
+ Only three colormaps are included: gray, inferno, and magma (from matplotlib). Log stretch behaves like ds9 as well, such that negative values are rescaled and don't make invalid pixel values.
81
+
82
+ #### Diverging (±)
83
+
84
+ When examining residuals, you may want a diverging colormap centered at zero. Use the ± button to switch the limits to symmetric and the available colormaps to RdBu and RdYlBu (from matplotlib).
85
+
86
+ ### Zooming, panning, rotation
87
+
88
+ Zoom by clicking the +/- buttons at lower right, scrolling with your mouse/trackpad, or using the `-` and `=` keys. Reset zoom with the "reset zoom to fit" button or the `0` key.
89
+
90
+ Pan by clicking and dragging. Cmd/Ctrl-Click to center the pixel under the cursor.
91
+
92
+ Use the left and right arrow buttons to rotate in 15º increments, or enter a number in degrees (counter-clockwise) in the box between them.
93
+
94
+ By default, rotation is about the image center, but you can set a pivot point by Cmd/Ctrl-Shift-clicking the desired point.
95
+
96
+ ### Matplotlib integration
97
+
98
+ Once you've rotated and panned and stretched and zoomed you may want to save a plot, but the viewer doesn't handle plotting.
99
+
100
+ Fortunately, the widget has a `plot_to_matplotlib()` method that takes a matplotlib.axes.Axes instance:
101
+
102
+ ```python
103
+ fig, ax = plt.subplots()
104
+ widget.plot_to_matplotlib(ax)
105
+ ```
106
+
107
+ **Note:** Rotating the axes makes the matplotlib tick labels useless, so they are hidden.
108
+
109
+ If you just want the contrast / bias / stretch, you can get a `norm` from the `get_normalization()` method:
110
+
111
+ ```python
112
+ norm = widget.get_normalization()
113
+ import matplotlib.pyplot as plt
114
+ plt.imshow(x, norm=norm)
115
+ ```
116
+
117
+ ### Widget API
118
+
119
+ #### `ViewerConfig`
120
+
121
+ Using the `ViewerConfig` dataclass or passing its arguments to `show()` lets you set the initial configured state of the viewer.
122
+
123
+ ```python
124
+ pyviewarr.show(x, vmin=-10, vmax=10)
125
+ ```
126
+
127
+ See [example](./preconfigured_state_example.ipynb).
128
+
129
+ #### Setting data
130
+
131
+ If you retain a reference to the widget, you can set its contents from a later cell (if you want).
132
+
133
+ ```python
134
+ widget = pyviewarr.show(x)
135
+ widget
136
+ ```
137
+
138
+ Now you can change the live widget's contents by supplying a new array to `set_array()`.
139
+
140
+ ```
141
+ new_arr = np.random.randn(512, 512).astype(np.float32) * 100
142
+ widget.set_array(new_arr)
143
+ ```
144
+
145
+ ## Development
146
+
147
+ Be sure to clone with `git clone --recurse-submodules` (or, if you cloned first and **then** read this, `git submodule update --init --recursive` to initialize an existing clone).
148
+
149
+ The frontend part of the widget (i.e. `viewarr` itself) is written in Rust with egui, requiring a Rust compiler toolchain for `wasm32-unknown-unknown` and the [`wasm-pack`](https://drager.github.io/wasm-pack/installer/) tool installed.
150
+
151
+ Install rust with rustup: https://rustup.rs/
152
+
153
+ Now that you have `cargo`, install wasm-pack with `cargo install wasm-pack`.
154
+
155
+ Using [uv](https://github.com/astral-sh/uv) for development will automatically manage virtual environments and dependencies for you.
156
+
157
+ For live reloading in development:
158
+
159
+ ```sh
160
+ export ANYWIDGET_HMR=1
161
+ ```
162
+
163
+ Export this variable before starting JupyterLab so it will use hot module reloading.
164
+
165
+ ```sh
166
+ uv run jupyter lab example.ipynb
167
+ ```
168
+
169
+ Alternatively, create and manage your own virtual environment:
170
+
171
+ ```sh
172
+ python -m venv .venv
173
+ source .venv/bin/activate
174
+ pip install -e ".[dev]"
175
+ ```
176
+
177
+ The widget front-end code bundles it's JavaScript dependencies. After setting up Python,
178
+ make sure to install these dependencies locally:
179
+
180
+ ```sh
181
+ npm install
182
+ ```
183
+
184
+ While developing, you can run the following in a separate terminal to automatically
185
+ rebuild JavaScript as you make changes:
186
+
187
+ ```sh
188
+ npm run dev
189
+ ```
190
+
191
+ Open `example.ipynb` in JupyterLab, VS Code, or your favorite editor
192
+ to start developing. Changes made in `js/` will be reflected
193
+ in the notebook.
194
+
195
+ To do a full build, use the project script:
196
+
197
+ ```sh
198
+ npm run build:all
199
+ ```
200
+
201
+ ### End-to-end tests (Galata + Playwright)
202
+
203
+ The widget has browser E2E tests using JupyterLab's Galata helpers.
204
+
205
+ First-time setup for browser binaries:
206
+
207
+ ```sh
208
+ npm run test:e2e:install
209
+ ```
210
+
211
+ Run E2E tests headless:
212
+
213
+ ```sh
214
+ npm run test:e2e
215
+ ```
216
+
217
+ Run E2E tests with a visible browser:
218
+
219
+ ```sh
220
+ npm run test:e2e:headed
221
+ ```
222
+
223
+ The test server uses `tests/e2e/jupyter_server_config.py`, which enables Galata's JupyterLab test hooks.
224
+
225
+ ## Releasing
226
+
227
+ Note that `viewarr/` submodule changes will need to be committed and pushed first, then the submodule reference in this repository can be updated.
228
+
229
+ Release checklist:
230
+
231
+ 1. Update changelog entries and set `[project].version` in `pyproject.toml` to the release version (e.g. `0.3.2`), then commit.
232
+ 2. Create the Git tag/release for that version (e.g. `v0.3.2`).
233
+ 3. Immediately after release, bump `pyproject.toml` to the next development version (e.g. `0.3.3dev`) and commit.
234
+
235
+ GitHub releases are automatically pushed to PyPI by the workflow in [`.github/workflows/ci.yml`](./.github/workflows/ci.yml).
236
+
237
+ ## Changelog
238
+
239
+ ### Unreleased (since `v0.3.2`)
240
+
241
+ - No unreleased changes yet.
242
+
243
+ ### `v0.3.2`
244
+
245
+ #### pyviewarr changes
246
+
247
+ - Updated bundled `viewarr` submodule from `3797ee7` to `cc78498`.
248
+
249
+ #### Included `viewarr` changes
250
+
251
+ - Fixed diverging/symmetric colorbar limit behavior so `vmax` is the only editable limit, coerced positive, and `vmin` is displayed as `-vmax` in that mode.
252
+ - Fixed state callback / `getValueRange()` limit reporting in diverging/symmetric mode to reflect the effective displayed range.
253
+
254
+ ### `v0.3.1`
255
+
256
+ #### pyviewarr changes
257
+
258
+ - Added browser end-to-end testing with Playwright + JupyterLab Galata (`playwright.config.ts`, `tests/e2e/`).
259
+ - Added CI coverage for Playwright E2E tests in [`.github/workflows/ci.yml`](./.github/workflows/ci.yml).
260
+ - Expanded development documentation for build/test workflows.
261
+ - Updated bundled `viewarr` submodule from `3d74f69` to `3797ee7`.
262
+
263
+ #### Included `viewarr` changes
264
+
265
+ - Fixed Cmd/Ctrl-click centering coordinate mapping.
266
+ - Pinned wasm-bindgen stack to avoid `glow`/`js-sys` CI build breakages.
267
+ - Added/expanded viewer state APIs in JS/TS:
268
+ - `setViewerState(...)` for partial bulk state updates.
269
+ - `getZoom(...)` / `setZoom(...)`.
270
+ - `setColormap(...)` / `setColormapReversed(...)`.
271
+ - New `StretchMode` and `ViewerStateConfig` typings.
272
+ - Improved colormap name handling/parsing with canonical names and aliases (`gray`/`grayscale`/`greyscale`, etc.).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyviewarr"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  dependencies = ["anywidget>=0.9,<0.10", "numpy>=2.0,<3"]
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"