arrayview 0.8.0__tar.gz → 0.10.0__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.
Files changed (68) hide show
  1. {arrayview-0.8.0 → arrayview-0.10.0}/.gitignore +3 -1
  2. {arrayview-0.8.0 → arrayview-0.10.0}/AGENTS.md +3 -15
  3. {arrayview-0.8.0 → arrayview-0.10.0}/PKG-INFO +5 -2
  4. {arrayview-0.8.0 → arrayview-0.10.0}/README.md +4 -1
  5. {arrayview-0.8.0 → arrayview-0.10.0}/pyproject.toml +1 -1
  6. arrayview-0.10.0/scripts/release.sh +78 -0
  7. arrayview-0.10.0/src/arrayview/ARCHITECTURE.md +197 -0
  8. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_app.py +1 -2
  9. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_launcher.py +26 -11
  10. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_render.py +8 -25
  11. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_server.py +293 -46
  12. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_session.py +13 -84
  13. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_shell.html +8 -1
  14. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_stdio_server.py +88 -2
  15. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_viewer.html +1377 -947
  16. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_vscode.py +31 -28
  17. {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_api.py +59 -0
  18. {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_browser.py +7 -2
  19. {arrayview-0.8.0 → arrayview-0.10.0}/tests/visual_smoke.py +3 -1
  20. {arrayview-0.8.0 → arrayview-0.10.0}/uv.lock +1 -1
  21. {arrayview-0.8.0 → arrayview-0.10.0}/.claude/skills/invocation-consistency/SKILL.md +0 -0
  22. {arrayview-0.8.0 → arrayview-0.10.0}/.claude/skills/modes-consistency/SKILL.md +0 -0
  23. {arrayview-0.8.0 → arrayview-0.10.0}/.claude/skills/ui-consistency-audit/SKILL.md +0 -0
  24. {arrayview-0.8.0 → arrayview-0.10.0}/.claude/skills/viewer-ui-checklist/SKILL.md +0 -0
  25. {arrayview-0.8.0 → arrayview-0.10.0}/.claude/skills/visual-bug-fixing/SKILL.md +0 -0
  26. {arrayview-0.8.0 → arrayview-0.10.0}/.github/workflows/docs.yml +0 -0
  27. {arrayview-0.8.0 → arrayview-0.10.0}/.github/workflows/python-publish.yml +0 -0
  28. {arrayview-0.8.0 → arrayview-0.10.0}/.python-version +0 -0
  29. {arrayview-0.8.0 → arrayview-0.10.0}/.tmp-vsix/extension/extension.js +0 -0
  30. {arrayview-0.8.0 → arrayview-0.10.0}/.tmp-vsix/extension/package.json +0 -0
  31. {arrayview-0.8.0 → arrayview-0.10.0}/LICENSE +0 -0
  32. {arrayview-0.8.0 → arrayview-0.10.0}/docs/comparing.md +0 -0
  33. {arrayview-0.8.0 → arrayview-0.10.0}/docs/configuration.md +0 -0
  34. {arrayview-0.8.0 → arrayview-0.10.0}/docs/display.md +0 -0
  35. {arrayview-0.8.0 → arrayview-0.10.0}/docs/index.md +0 -0
  36. {arrayview-0.8.0 → arrayview-0.10.0}/docs/loading.md +0 -0
  37. {arrayview-0.8.0 → arrayview-0.10.0}/docs/logo.png +0 -0
  38. {arrayview-0.8.0 → arrayview-0.10.0}/docs/measurement.md +0 -0
  39. {arrayview-0.8.0 → arrayview-0.10.0}/docs/remote.md +0 -0
  40. {arrayview-0.8.0 → arrayview-0.10.0}/docs/stylesheets/extra.css +0 -0
  41. {arrayview-0.8.0 → arrayview-0.10.0}/docs/viewing.md +0 -0
  42. {arrayview-0.8.0 → arrayview-0.10.0}/matlab/arrayview.m +0 -0
  43. {arrayview-0.8.0 → arrayview-0.10.0}/mkdocs.yml +0 -0
  44. {arrayview-0.8.0 → arrayview-0.10.0}/plans/webview/LOG.md +0 -0
  45. {arrayview-0.8.0 → arrayview-0.10.0}/scripts/demo.py +0 -0
  46. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/__init__.py +0 -0
  47. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/__main__.py +0 -0
  48. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_config.py +0 -0
  49. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_icon.png +0 -0
  50. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_io.py +0 -0
  51. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_platform.py +0 -0
  52. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_segmentation.py +0 -0
  53. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_torch.py +0 -0
  54. {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/arrayview-opener.vsix +0 -0
  55. {arrayview-0.8.0 → arrayview-0.10.0}/tests/conftest.py +0 -0
  56. {arrayview-0.8.0 → arrayview-0.10.0}/tests/make_vectorfield_test_arrays.py +0 -0
  57. {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_cli.py +0 -0
  58. {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_config.py +0 -0
  59. {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_interactions.py +0 -0
  60. {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_large_arrays.py +0 -0
  61. {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_mode_consistency.py +0 -0
  62. {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_mode_matrix.py +0 -0
  63. {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_rgb_pixel_art.py +0 -0
  64. {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_torch.py +0 -0
  65. {arrayview-0.8.0 → arrayview-0.10.0}/tests/ui_audit.py +0 -0
  66. {arrayview-0.8.0 → arrayview-0.10.0}/vscode-extension/LICENSE +0 -0
  67. {arrayview-0.8.0 → arrayview-0.10.0}/vscode-extension/extension.js +0 -0
  68. {arrayview-0.8.0 → arrayview-0.10.0}/vscode-extension/package.json +0 -0
@@ -31,4 +31,6 @@ benchmarks/data
31
31
  .superpowers/brainstorm
32
32
  site/
33
33
  van_gogh/
34
- docs/superpowers
34
+ cig_presentation/
35
+ docs/superpowers
36
+ .playwright-mcp/
@@ -1,19 +1,6 @@
1
- # ArrayView
2
-
3
- Interactive viewer for multi-dimensional arrays and medical/scientific volumes. FastAPI server + single-file HTML/JS frontend (`_viewer.html`), displayed via native window, browser, VS Code Simple Browser, or Jupyter iframe.
1
+ Read `src/arrayview/ARCHITECTURE.md` for codebase orientation.
4
2
 
5
- ## Core Files
6
-
7
- | File | Role |
8
- |------|------|
9
- | `_launcher.py` | Entry points, process management, display routing |
10
- | `_server.py` | FastAPI app, REST/WebSocket, HTML templates |
11
- | `_session.py` | Sessions, state, caches, render thread |
12
- | `_render.py` | Colormaps, LUTs, slice extraction, RGBA |
13
- | `_viewer.html` | All frontend (JS/CSS embedded, single file) |
14
- | `_vscode.py` | Extension management, signal-file IPC |
15
- | `_stdio_server.py` | Stdio transport for direct webview mode |
16
- | `_segmentation.py` | nnInteractive segmentation client |
3
+ # ArrayView
17
4
 
18
5
  ## Skills
19
6
 
@@ -36,6 +23,7 @@ Load the relevant skill before touching the corresponding area.
36
23
  - Do not regress working display paths when fixing another
37
24
  - For visual/animation features, propose 2-3 options BEFORE implementing
38
25
  - UI visibility changes go through reconcilers (`_reconcileUI`/`_reconcileLayout`/`_reconcileCompareState`/`_reconcileCbVisibility`), not inline `style.display` or `classList` toggles in mode functions
26
+ - All colorbar state (animation, window/level, hover, drag) flows through `primaryCb` ColorBar instance — never read/write legacy globals. Multiview colorbars sync via `primaryCb`.
39
27
 
40
28
  ## Execution
41
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -103,7 +103,10 @@ for epoch in range(100):
103
103
 
104
104
  ## Once open
105
105
 
106
- `c` colormaps · `d` dynamic range · `v` 3-plane · `z` mosaic · `Shift+O` overlay toggle · `?` help · colorbar dblclick histogram
106
+ **Navigation:** scroll slices · `h`/`l` cycle dims · `j`/`k` slices · `=`/`-` zoom · drag pan
107
+ **Views:** `v` 3-plane · `z` mosaic · `q` qMRI · `n` compare · `=` immersive
108
+ **Display:** `c`/`C` colormaps · `d`/`D` dynamic range · `f` FFT · `m` complex · `p` projections · `L` log
109
+ **Tools:** `S` segmentation · `u` ruler · `s` screenshot · `?` help
107
110
 
108
111
 
109
112
  ## nnInteractive Segmentation
@@ -68,7 +68,10 @@ for epoch in range(100):
68
68
 
69
69
  ## Once open
70
70
 
71
- `c` colormaps · `d` dynamic range · `v` 3-plane · `z` mosaic · `Shift+O` overlay toggle · `?` help · colorbar dblclick histogram
71
+ **Navigation:** scroll slices · `h`/`l` cycle dims · `j`/`k` slices · `=`/`-` zoom · drag pan
72
+ **Views:** `v` 3-plane · `z` mosaic · `q` qMRI · `n` compare · `=` immersive
73
+ **Display:** `c`/`C` colormaps · `d`/`D` dynamic range · `f` FFT · `m` complex · `p` projections · `L` log
74
+ **Tools:** `S` segmentation · `u` ruler · `s` screenshot · `?` help
72
75
 
73
76
 
74
77
  ## nnInteractive Segmentation
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.8.0"
7
+ version = "0.10.0"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # ---------------------------------------------------------------------------
5
+ # release.sh — bump version, commit, tag, push, create GitHub release
6
+ # ---------------------------------------------------------------------------
7
+
8
+ BUMP="minor"
9
+ DRY_RUN=true
10
+
11
+ usage() {
12
+ cat <<EOF
13
+ Usage: $(basename "$0") [OPTIONS]
14
+
15
+ Options:
16
+ --bump {major,minor,patch} Version bump type (default: minor)
17
+ --execute Actually run (default is dry-run)
18
+ -h, --help Show this help
19
+ EOF
20
+ }
21
+
22
+ while [[ $# -gt 0 ]]; do
23
+ case "$1" in
24
+ --bump) BUMP="$2"; shift 2 ;;
25
+ --execute) DRY_RUN=false; shift ;;
26
+ -h|--help) usage; exit 0 ;;
27
+ *) echo "Unknown option: $1"; usage; exit 1 ;;
28
+ esac
29
+ done
30
+
31
+ if [[ ! "$BUMP" =~ ^(major|minor|patch)$ ]]; then
32
+ echo "Error: --bump must be major, minor, or patch (got '$BUMP')"
33
+ exit 1
34
+ fi
35
+
36
+ # --- Guard: clean working tree on main ---
37
+ branch=$(git rev-parse --abbrev-ref HEAD)
38
+ if [[ "$branch" != "main" ]]; then
39
+ echo "Error: must be on main (currently on '$branch')"
40
+ exit 1
41
+ fi
42
+
43
+ if ! git diff --quiet || ! git diff --cached --quiet; then
44
+ echo "Error: working tree is dirty — commit or stash first"
45
+ exit 1
46
+ fi
47
+
48
+ # --- Bump version ---
49
+ uv version --bump "$BUMP"
50
+ VERSION=$(uv version --short)
51
+ TAG="v${VERSION}"
52
+
53
+ echo "Version bumped to $VERSION (tag: $TAG)"
54
+
55
+ run() {
56
+ echo "+ $*"
57
+ if [[ "$DRY_RUN" == true ]]; then
58
+ return
59
+ fi
60
+ "$@"
61
+ }
62
+
63
+ # --- Commit, tag, push, release ---
64
+ run git add pyproject.toml
65
+ run git commit -m "release: $TAG"
66
+ run git push origin main
67
+ run git tag "$TAG"
68
+ run git push origin "$TAG"
69
+ run gh release create "$TAG" \
70
+ --title "[Pre-release] $TAG" \
71
+ --generate-notes \
72
+ --prerelease
73
+
74
+ if [[ "$DRY_RUN" == true ]]; then
75
+ echo ""
76
+ echo "(dry-run — re-run with --execute to apply)"
77
+ git checkout pyproject.toml
78
+ fi
@@ -0,0 +1,197 @@
1
+ # Architecture
2
+
3
+ ## System Overview
4
+
5
+ ```
6
+ CLI / Python API
7
+ ├─ view() / _launcher.py → FastAPI server (_server.py) [network mode]
8
+ │ /ws/{sid} WebSocket, /load register arrays, /seg/* segmentation
9
+ └─ Stdio server (_stdio_server.py) [direct webview mode]
10
+ stdin/stdout JSON+binary, no network — VS Code extension spawns subprocess
11
+
12
+ Server (either mode)
13
+ ├─ _session.py Session objects, caches, render thread
14
+ ├─ _render.py Slice extraction → RGBA → PNG pipeline
15
+ └─ _io.py File loading (numpy, nifti, zarr, DICOM, …)
16
+
17
+ Frontend (_viewer.html — single self-contained HTML file)
18
+ ├─ CSS Dark theme, mode-specific layouts
19
+ ├─ Canvas 2D rendering via ImageBitmap / putImageData
20
+ ├─ WebSocket Binary slice transport, JSON metadata
21
+ └─ UI Colorbars, eggs, dynamic islands, overlays
22
+ ```
23
+
24
+ ## Display Routing
25
+
26
+ | Environment | Default display | Server mode |
27
+ |--------------------|------------------------------------|-------------|
28
+ | Jupyter | Inline iframe | network |
29
+ | VS Code local | Simple Browser (network) | network |
30
+ | VS Code tunnel | Direct webview (stdio) | stdio |
31
+ | Julia | System browser | network |
32
+ | CLI / Python script | Native pywebview | network |
33
+ | SSH terminal (ni) | VS Code ext via TCP relay (prints URL on relay failure) | network |
34
+
35
+ Detection logic lives in `_platform.py`. Display opening logic lives in `_launcher.py` (section: ViewHandle and view() API) and `_vscode.py`.
36
+
37
+ ## File Map
38
+
39
+ | File | Lines | Owns |
40
+ |------|------:|------|
41
+ | `__init__.py` | 5 | Public API re-exports: `view`, `arrayview`, `ViewHandle`, `TrainingMonitor`, `view_batch`, `zarr_chunk_preset` |
42
+ | `__main__.py` | 4 | `python -m arrayview` entry point |
43
+ | `_app.py` | 179 | Backward-compat shim — re-exports everything from the split modules |
44
+ | `_config.py` | 121 | `~/.arrayview/config.toml` read/write, valid window modes/env keys |
45
+ | `_io.py` | 253 | Data loading: numpy, NIfTI (lazy nibabel), zarr, DICOM, raw files |
46
+ | `_launcher.py` | 2817 | **Main entry.** CLI parser, `view()` API, `ViewHandle`, server lifecycle, SSH relay, demo arrays, file watching |
47
+ | `_platform.py` | 396 | Environment detection: Jupyter, VS Code, SSH, tunnel, Julia, native-window capability |
48
+ | `_render.py` | 834 | Rendering pipeline: colormap LUTs, slice extraction, RGBA/RGB/mosaic rendering, overlay compositing, preload |
49
+ | `_segmentation.py` | 227 | nnInteractive segmentation client (pure HTTP, no nnInteractive dependency) |
50
+ | `_server.py` | 3258 | FastAPI app, all REST + WebSocket routes, HTML template serving |
51
+ | `_session.py` | 344 | `Session` class, global state (sockets, loops), render thread, prefetch, cache budgets, constants |
52
+ | `_stdio_server.py` | 791 | Stdio transport for VS Code direct webview — JSON stdin, binary stdout |
53
+ | `_torch.py` | 217 | PyTorch integration: `view_batch()`, `TrainingMonitor` (lazy torch import) |
54
+ | `_vscode.py` | 1014 | VS Code extension install/management, signal-file IPC, shared-memory IPC, browser opening |
55
+ | `_viewer.html` | 14480 | **The entire frontend** — CSS + JS in a single file, all viewing modes |
56
+ | `_shell.html` | 174 | Tab-bar shell for native pywebview — wraps viewer iframes, manages multi-tab sessions |
57
+
58
+ ## Frontend (_viewer.html)
59
+
60
+ The frontend is a single self-contained HTML file (~15k lines). No build step, no external dependencies. Organized by section separators (`/* ── Section Name ── */` for CSS, `// ── Section Name ──` for JS).
61
+
62
+ ### Major Sections
63
+
64
+ **CSS (lines ~7–1500)**
65
+ | Section | What it covers |
66
+ |---------|----------------|
67
+ | Theme Variables and Base Layout | CSS custom properties, dark theme palette, root layout grid |
68
+ | ColorBar and Dynamic Islands | Colorbar positioning, egg badges, info bar, dimension sliders |
69
+ | Generic ColorBar class styles | Shared `.av-colorbar` styles used by the ColorBar JS class |
70
+ | Compare, Overlay, and Prompt Styles | Side-by-side panes, overlay blend, prompt dialogs |
71
+ | Help and Info Overlays | Help shortcut overlay, array-info panel |
72
+ | Immersive, Fullscreen, and Compact Mode | Zen mode hide rules, fullscreen layout, compact overrides |
73
+
74
+ **JavaScript (lines ~1500–14750)**
75
+ | Section | What it covers |
76
+ |---------|----------------|
77
+ | Constants and Transport Setup | WS URL construction, stdio/postMessage transport abstraction |
78
+ | Viewer State Variables | All mutable state: current slice indices, zoom, mode flags |
79
+ | Mode Registry | Mode name → enter/exit function mapping |
80
+ | PanManager | Canvas panning state machine (normal + compare modes) |
81
+ | Canvas Scaling and Layout | `scaleCanvas()`, `mvScaleAllCanvases()`, `compareScaleCanvases()`, `qvScaleAllCanvases()` |
82
+ | Compare Mode | Multi-pane compare infrastructure, drag-to-reorder, sub-modes |
83
+ | Colorbar Rendering and Histogram | Colorbar draw routines, histogram morph, fullscreen overlay colorbar |
84
+ | ColorBar class | Reusable `ColorBar` class (~900 lines) — draw, histogram, window/level, hover |
85
+ | WebSocket and Data Transport | Binary slice receive, request queueing, reconnect |
86
+ | Initialization and Metadata Fetch | `/meta` fetch, loading screen, initial render |
87
+ | Info Bar and Pixel Display | Bottom info bar, hover pixel readout, coordinate display |
88
+ | State Persistence and Restore | URL hash state, `sessionStorage` save/restore |
89
+ | Rendering Pipeline | `updateView()`, play/animate, screenshot capture |
90
+ | ROI and Selection Modes | Rectangle/ellipse ROI drawing, statistics computation |
91
+ | nnInteractive Segmentation | Click-to-segment UI, mask overlay, undo stack |
92
+ | Keyboard Shortcuts | All hotkey bindings — single master switch/case block |
93
+ | Mode Transitions | Compare/multiview/qMRI enter/exit, crosshair animation |
94
+ | Scroll, Zoom, and Pan | Mouse wheel slice scroll, pinch zoom, scroll-to-zoom |
95
+ | Immersive Mode, Cross-Fade, and Visual Effects | Zen mode, fullscreen (K), animated transitions |
96
+ | UI Validation and Reconciliation | Reconcilers that enforce consistent UI state across modes |
97
+ | Colormap Strip and Wipe/Flicker Compare Tools | Colormap preview strip, A/B wipe, flicker, checkerboard |
98
+ | Ruler, Line Profile, and Mini-Map | Distance measurement, 1D intensity profile, overview mini-map |
99
+ | Compact Mode and Touch Input | Compact layout (K), touch gesture handling |
100
+ | File Picker and Session Management | File browser, drag-and-drop, session switching |
101
+
102
+ ### Mode Matrix
103
+
104
+ Each viewing mode uses a specific scale function and reconciler set:
105
+
106
+ | Mode | Scale function | Layout | Reconciler notes |
107
+ |------|---------------|--------|------------------|
108
+ | Normal | `scaleCanvas()` | Single canvas, colorbar below | Base reconciler |
109
+ | Immersive (Zen) | `scaleCanvas()` | Canvas fills viewport, UI hidden | Animated enter/exit, dimbar+colorbar drag |
110
+ | Compact | `scaleCanvas()` | Reduced chrome | K-key toggle |
111
+ | Multiview (3-pane oblique) | `mvScaleAllCanvases()` | 3 canvases (axial/coronal/sagittal) | Layout container visibility reconciler |
112
+ | Compare | `compareScaleCanvases()` | 2+ side-by-side panes | Compare sub-mode reconciler (diff/overlay/wipe/flicker/checkerboard) |
113
+ | Diff | `compareScaleCanvases()` | Compare variant — center pane shows A-B | Diff colorbar instances |
114
+ | Registration | `compareScaleCanvases()` | Compare variant — overlay blend | Cross-fade overlay |
115
+ | qMRI | `qvScaleAllCanvases()` | Multi-map quantitative display | Separate scale pipeline |
116
+
117
+ ### CSS Architecture
118
+
119
+ - **Dark theme only.** Background `#0c0c0c`, text `#d8d8d8`, accents in yellow (`#f5c842`).
120
+ - **Monospace UI.** Font: system monospace for data, system sans-serif for labels.
121
+ - **No CSS framework.** All custom properties defined in `:root`.
122
+ - **Mode-specific overrides** use `.immersive-mode`, `.compact-mode`, `.multiview-mode` classes on `body`.
123
+ - **`av-loading` class** on `body` during initial load — hides UI until data arrives. Removal triggers layout.
124
+ - **Hard UI invariants** block at line ~157 — structural rules that must never be overridden.
125
+
126
+ ## Key Concepts
127
+
128
+ ### Sessions
129
+ A `Session` object (`_session.py`) holds one array's data, metadata, and three LRU caches (raw slices, RGBA tiles, mosaics). Sessions are stored in the global `SESSIONS` dict keyed by `sid` (hex UUID). Multiple sessions enable compare mode and the file picker.
130
+
131
+ ### Eggs (Mode Badges)
132
+ Pill-shaped badges below the canvas showing active visualization transforms. **Composable** — they stack: `FFT` `LOG` `MAGNITUDE` `PHASE` `REAL` `IMAG` `RGB` `ALPHA` `PROJECTION`. Each egg toggles a transform in the rendering pipeline.
133
+
134
+ **ROI and SEGMENT are NOT eggs.** They are interaction modes that take over canvas input, are mutually incompatible, and each has its own dynamic island UI.
135
+
136
+ ### Dynamic Islands
137
+ Floating UI panels that appear/disappear based on context: ROI statistics, segmentation controls, colorbar hover, dimension sliders. Must be tested across all viewing modes (normal, immersive, multiview, compare).
138
+
139
+ ### Reconcilers
140
+ Functions in the "UI Validation and Reconciliation" section (~line 13666) that enforce consistent UI state. When mode changes happen, reconcilers update visibility of containers, colorbars, dynamic islands, and compare sub-mode UI. There are four:
141
+ 1. **Unified UI reconciler** — master state enforcer
142
+ 2. **Layout container visibility reconciler** — show/hide mode-specific containers
143
+ 3. **Compare sub-mode state reconciler** — diff/overlay/wipe/flicker/checkerboard state
144
+ 4. **CB / island visibility reconciler** — colorbar and dynamic island show/hide
145
+
146
+ ### Render Thread
147
+ A dedicated daemon thread (`_session.py`) runs all CPU-heavy rendering off the async event loop. `_render()` posts work to `_RENDER_QUEUE` and returns an awaitable `Future`. The prefetch pool (separate 1-thread executor) warms caches for neighboring slices.
148
+
149
+ ## High-Risk Areas
150
+
151
+ ### CSS Pitfalls
152
+ - **Canvas buffer vs CSS resolution mismatches.** The canvas element size (CSS pixels) and its buffer size (device pixels) must stay in sync. `scaleCanvas()` handles this — never set canvas dimensions outside a scale function.
153
+ - **Selector targeting wrong wrappers.** Normal mode has one `#canvas-wrapper`; compare mode has per-pane wrappers. CSS rules must scope correctly.
154
+ - **`av-loading` class interactions.** Many elements are hidden while `av-loading` is on `body`. Removing it too early causes layout flash; too late causes blank screen.
155
+
156
+ ### Dynamic Islands
157
+ Must verify island positioning and visibility across normal, immersive, multiview, and compare modes. Islands use absolute/fixed positioning that breaks if parent containers change.
158
+
159
+ ### Layout Debugging
160
+ When debugging layout issues: identify the root cause (which scale function, which reconciler, which CSS rule) before applying fixes. Symptoms in one mode often originate from shared code affecting all modes.
161
+
162
+ ### WebSocket Binary Protocol
163
+ Slice data arrives as raw binary (RGBA bytes). The header format and byte offsets are tightly coupled between `_server.py` (Python) and the WebSocket handler in `_viewer.html` (JS). Changes to one must match the other.
164
+
165
+ ### Lazy Imports
166
+ `_launcher.py` deliberately avoids importing numpy, _session, _render, and _io at module level. This saves ~300-350ms on CLI fast paths (when the server is already running). Breaking this lazy-loading pattern degrades startup time for every invocation.
167
+
168
+ ## Data Flow
169
+
170
+ ### Request Lifecycle: `view()` to Pixels
171
+
172
+ ```
173
+ 1. view(array) # _launcher.py
174
+ ├─ Create Session(data) # _session.py — assigns sid, inits caches
175
+ ├─ Start server if not running # _launcher.py → _server.py (uvicorn)
176
+ ├─ Register session via /load # HTTP POST to FastAPI
177
+ └─ Open display # _launcher.py → _vscode.py / pywebview / browser
178
+
179
+ 2. Browser loads _viewer.html # _server.py serves from package resources
180
+ ├─ Establish WebSocket /ws/{sid} # or stdio transport for VS Code direct
181
+ └─ Fetch /meta/{sid} # Session metadata: shape, dtype, colormaps
182
+
183
+ 3. User scrolls / interacts
184
+ ├─ JS sends slice request # WebSocket binary or JSON
185
+ ├─ Server dispatches to render thread
186
+ │ ├─ extract_slice() # _render.py — pull 2D slice from ND array
187
+ │ ├─ apply_complex_mode() # _render.py — FFT, magnitude, phase, etc.
188
+ │ ├─ render_rgba() / render_rgb() # _render.py — apply colormap LUT → RGBA
189
+ │ └─ PNG encode # sent as binary WebSocket frame
190
+ └─ JS receives binary
191
+ ├─ Decode PNG → ImageBitmap
192
+ ├─ drawImage() to canvas
193
+ └─ Update info bar, colorbar, eggs
194
+ ```
195
+
196
+ ### Stdio Transport Variant (VS Code Direct Webview)
197
+ Same pipeline, but `_stdio_server.py` replaces the FastAPI+WebSocket layer. Messages are JSON on stdin, binary responses are length-prefixed on stdout. The VS Code extension bridges between the webview's `postMessage` and the subprocess stdio.
@@ -33,8 +33,7 @@ from arrayview._session import ( # noqa: F401
33
33
  Session,
34
34
  SESSIONS,
35
35
  COLORMAPS,
36
- DR_PERCENTILES,
37
- DR_LABELS,
36
+
38
37
  ZARR_LARGE_XY_TILE,
39
38
  ZARR_T_DEPTH,
40
39
  PREFETCH_NEIGHBORS,
@@ -3,6 +3,8 @@
3
3
  This module was extracted from _app.py during the modular refactor.
4
4
  """
5
5
 
6
+ # ── Imports and Lazy Loading ─────────────────────────────────────
7
+
6
8
  import argparse
7
9
  import asyncio
8
10
  import io
@@ -125,9 +127,8 @@ def _vprint(*args, **kwargs) -> None:
125
127
  _session_mod._vprint(*args, **kwargs)
126
128
 
127
129
 
128
- # ---------------------------------------------------------------------------
129
- # Subprocess GUI Launcher
130
- # ---------------------------------------------------------------------------
130
+ # ── Subprocess GUI Launcher ───────────────────────────────────────
131
+
131
132
  _ICON_PNG_PATH: str | None = None
132
133
 
133
134
 
@@ -380,9 +381,7 @@ def _open_webview_cli(
380
381
  return True
381
382
 
382
383
 
383
- # ---------------------------------------------------------------------------
384
- # Server port utilities
385
- # ---------------------------------------------------------------------------
384
+ # ── Server Port Utilities ─────────────────────────────────────────
386
385
 
387
386
 
388
387
  def _server_alive(port: int, timeout: float = 0.5) -> bool:
@@ -479,9 +478,7 @@ def _relay_array_to_server(
479
478
  )
480
479
 
481
480
 
482
- # ---------------------------------------------------------------------------
483
- # Zero-config SSH relay: send array to VS Code extension on SSH client machine
484
- # ---------------------------------------------------------------------------
481
+ # ── Zero-Config SSH Relay ─────────────────────────────────────────
485
482
 
486
483
  _RELAY_MAGIC = b"AVRELAY1"
487
484
  _RELAY_DEFAULT_PORT = 17789
@@ -633,6 +630,9 @@ async def _serve_background(port: int, stop_when_closed: bool = False):
633
630
  _OVERLAY_PALETTE = ["ff4444", "44cc44", "4488ff", "ffcc00", "ff44ff", "44ffff"]
634
631
 
635
632
 
633
+ # ── ViewHandle and view() API ────────────────────────────────────
634
+
635
+
636
636
  class ViewHandle(str):
637
637
  """Returned by :func:`view`. Behaves as a URL string for backward compatibility
638
638
  and additionally exposes ``.update(arr)`` to push a new array into the viewer
@@ -1256,7 +1256,9 @@ def view(
1256
1256
  if inline:
1257
1257
  from IPython.display import IFrame, display as _ipy_display
1258
1258
 
1259
- iframe = IFrame(src=url_viewer, width="100%", height=height)
1259
+ # Add inline=1 param so the viewer starts in immersive mode
1260
+ _inline_url = url_viewer + "&inline=1"
1261
+ iframe = IFrame(src=_inline_url, width="100%", height=height)
1260
1262
  if n_arrays == 1:
1261
1263
  return iframe
1262
1264
  # Multi-array inline: display the IFrame and return a uniform tuple of handles.
@@ -1317,6 +1319,9 @@ def view(
1317
1319
  return tuple(ViewHandle(url_viewer, s, port) for s in [session.sid] + _compare_sids)
1318
1320
 
1319
1321
 
1322
+ # ── Server Lifecycle ──────────────────────────────────────────────
1323
+
1324
+
1320
1325
  def _is_script_mode() -> bool:
1321
1326
  """True when running as a plain Python script (not interactive REPL, not Jupyter, not Julia)."""
1322
1327
  if _in_jupyter() or _is_julia_env():
@@ -1531,8 +1536,9 @@ def _view_subprocess(
1531
1536
  _print_viewer_location(url_viewer)
1532
1537
 
1533
1538
  if inline:
1539
+ _inline_url = url_viewer + "&inline=1"
1534
1540
  iframe_html = (
1535
- f"<iframe src='{url_viewer}' width='100%'"
1541
+ f"<iframe src='{_inline_url}' width='100%'"
1536
1542
  f" height='{height}' frameborder='0'></iframe>"
1537
1543
  )
1538
1544
  # IJulia kernel: push HTML through Julia's display stack (routes to Jupyter
@@ -1727,6 +1733,9 @@ def _serve_daemon(
1727
1733
  os._exit(0)
1728
1734
 
1729
1735
 
1736
+ # ── Demo Array and File Watching ──────────────────────────────────
1737
+
1738
+
1730
1739
  def _make_demo_array() -> "np.ndarray":
1731
1740
  """Return a (128, 128, 32, 3) float32 RGB plasma animation.
1732
1741
 
@@ -1809,6 +1818,9 @@ def _start_watch_thread(filepath: str, sid: str, port: int) -> None:
1809
1818
  t.start()
1810
1819
 
1811
1820
 
1821
+ # ── Config Subcommand ─────────────────────────────────────────────
1822
+
1823
+
1812
1824
  def _handle_config_command(args: list[str]) -> None:
1813
1825
  """Handle 'arrayview config' subcommands."""
1814
1826
  from arrayview._config import (
@@ -1894,6 +1906,9 @@ def _handle_config_command(args: list[str]) -> None:
1894
1906
  sys.exit(1)
1895
1907
 
1896
1908
 
1909
+ # ── CLI Entry Point (arrayview command) ───────────────────────────
1910
+
1911
+
1897
1912
  def arrayview():
1898
1913
  """Command Line Interface Entry Point."""
1899
1914
  # Handle "arrayview config ..." subcommand before argparse
@@ -6,7 +6,6 @@ import numpy as np
6
6
 
7
7
  from arrayview._session import (
8
8
  COLORMAPS,
9
- DR_PERCENTILES,
10
9
  SESSIONS,
11
10
  )
12
11
 
@@ -105,15 +104,10 @@ def mosaic_shape(batch):
105
104
  # ---------------------------------------------------------------------------
106
105
 
107
106
 
108
- def _compute_vmin_vmax(session, data, dr, complex_mode=0):
107
+ def _compute_vmin_vmax(session, data, dr=0, complex_mode=0):
109
108
  if complex_mode == 1 and np.iscomplexobj(session.data):
110
109
  return (-float(np.pi), float(np.pi))
111
- if complex_mode == 0 and len(session.shape) <= 3 and dr in session.global_stats:
112
- vmin, vmax = session.global_stats[dr]
113
- if vmin != vmax:
114
- return vmin, vmax
115
- pct_lo, pct_hi = DR_PERCENTILES[dr % len(DR_PERCENTILES)]
116
- return float(np.percentile(data, pct_lo)), float(np.percentile(data, pct_hi))
110
+ return float(np.percentile(data, 1)), float(np.percentile(data, 99))
117
111
 
118
112
 
119
113
  def extract_slice(session, dim_x, dim_y, idx_list):
@@ -237,18 +231,8 @@ def _prepare_display(
237
231
  return data, vmin_override, vmax_override
238
232
  if log_scale:
239
233
  data = np.log1p(np.abs(data)).astype(np.float32)
240
- if complex_mode == 0 and len(session.shape) <= 3 and dr in session.global_stats:
241
- raw_vmin, raw_vmax = session.global_stats[dr]
242
- vmin = float(np.log1p(abs(raw_vmin)))
243
- vmax = float(np.log1p(abs(raw_vmax)))
244
- if vmin == vmax:
245
- pct_lo, pct_hi = DR_PERCENTILES[dr % len(DR_PERCENTILES)]
246
- vmin = float(np.percentile(data, pct_lo))
247
- vmax = float(np.percentile(data, pct_hi))
248
- else:
249
- pct_lo, pct_hi = DR_PERCENTILES[dr % len(DR_PERCENTILES)]
250
- vmin = float(np.percentile(data, pct_lo))
251
- vmax = float(np.percentile(data, pct_hi))
234
+ vmin = float(np.percentile(data, 1))
235
+ vmax = float(np.percentile(data, 99))
252
236
  else:
253
237
  vmin, vmax = _compute_vmin_vmax(session, data, dr, complex_mode)
254
238
  return data, vmin, vmax
@@ -698,12 +682,11 @@ def render_mosaic(
698
682
  frames = [np.log1p(np.abs(f)).astype(np.float32) for f in frames]
699
683
  all_data = np.stack(frames)
700
684
 
701
- if log_scale:
702
- pct_lo, pct_hi = DR_PERCENTILES[dr % len(DR_PERCENTILES)]
703
- vmin = float(np.percentile(all_data, pct_lo))
704
- vmax = float(np.percentile(all_data, pct_hi))
685
+ if complex_mode == 1 and np.iscomplexobj(session.data):
686
+ vmin, vmax = -float(np.pi), float(np.pi)
705
687
  else:
706
- vmin, vmax = _compute_vmin_vmax(session, all_data, dr, complex_mode)
688
+ vmin = float(np.percentile(all_data, 1))
689
+ vmax = float(np.percentile(all_data, 99))
707
690
 
708
691
  if mosaic_cols is not None:
709
692
  cols = mosaic_cols