Skip to content

Clean history#24

Closed
CSSFrancis wants to merge 215 commits into
mainfrom
clean-history
Closed

Clean history#24
CSSFrancis wants to merge 215 commits into
mainfrom
clean-history

Conversation

@CSSFrancis

Copy link
Copy Markdown
Owner

Remove AI agent commits from history.

Add initial README.md with project overview and usage examples.
…segments, points, polygons, rectangles, and vertical lines with live updates in anyplotlib
…tangles, squares, polygons, texts, points, horizontal lines, and vertical lines for enhanced plotting capabilities
…grid cell allocation, removing aspect-locking logic. Update tests to verify correct canvas dimensions for various image aspect ratios and configurations. Add documentation for new layout behavior and introduce a navigator for `figure_esm.js`.
- Introduce `on_key` callback API for registering key-press handlers.
- Implement functionality to add various shapes (rectangles, circles, annuli) at cursor position.
- Allow deletion of the last clicked widget using the Delete key.
- Update documentation to reflect new key binding features and usage examples.
…2D methods and ensure top-level imports are accessible.
…; create new RST files for callbacks, figure plots, markers, and widgets
…tailed parameter descriptions, examples, and return values
…rameter descriptions for subplot specification
…ay and improve scaling logic for interactive widgets
CSSFrancis and others added 26 commits May 23, 2026 19:22
Co-Authored-By: Carter Francis <cartsfrancis@gmail.com>
Adds a _PYODIDE_MOCK_PACKAGES = [...] variable that examples can declare
to register packages unavailable in Pyodide as micropip mock packages
before the wheel install, preventing dep-resolution failures.

Changes:
- _scraper.py: parse _PYODIDE_MOCK_PACKAGES_RE, emit data-pyodide-mock-packages
  attribute on the <script type="text/x-python"> tag
- _directive.py: same parsing for the anywidget-figure directive
- anywidget_bridge.js: collect data-pyodide-mock-packages from all script
  tags BEFORE the wheel install step (step 6) and register them as mock
  packages; also carry mockPackages through the srcGroups map for per-example
  use in step 9
Major additions across rendering, embedding, and performance:

Rendering & plot types
- True-colour RGB imshow ((H,W,3|4) arrays)
- 3D voxels geometry + draggable PlaneWidget slice selectors
- scatter3d per-point colours, set_highlight, reference sphere, bounds
- Turntable 3D camera (matplotlib azim/elev; reaches any viewpoint)
- Mini-TeX label formatting (superscripts/Greek/symbols) + fontsize control
- IPF and voxel-grain explorer examples; label-formatting example

Embedding (no Jupyter required)
- anyplotlib.embed: figure_state/to_html/save_html/esm_path + FigureBridge
- figure_esm.js mount() API for Electron / plain web pages
- docs/embedding.rst guide

WebGPU (progressive enhancement, canvas fallback)
- Instanced points + voxels on the GPU via gpu="auto"; airtight Canvas2D
  fallback (no navigator.gpu / null adapter / device loss). No JS deps.
- Hardware-verified on NVIDIA Pascal; WEBGPU_PLAN.md tracks phases.

Performance (smoothness under Pyodide)
- Figure.batch() coalesces panel pushes (one per panel per frame)
- Geometry channel: heavy buffers ride a separate hash-gated trait, re-sent
  only on change — view updates (highlight/camera/planes) no longer
  retransmit geometry. Voxel explorer per-frame traffic -65%.
- Voxel sprite cache + typed-array projection; RGB imshow skips LUT rebuild

Packaging
- LICENSE (MIT), wheel excludes tests/baselines, py3.13 classifier
- RELEASE_PLAN.md; committed uv.lock

Tests: +~90 across labels, embed, RGB, voxels/planes, GPU fallback,
batch, geometry channel. Full suite 1323 passed / 58 skipped.
The default 11px bold 2D title was drawn strip-tight (12px strip, centered),
leaving ~0px top margin.  macOS Chromium hints fonts ~1px taller than the
Windows dev box, so on macOS CI the title ascender reached row 0 of the
title canvas and test_no_clipping flagged it as clipped.

Clamp the drawn title size to padT-4 (reserve ~4px total vertical margin).
Since _padT grows for large/TeX titles, this only affects the 12px default
strip, capping an 11px title to ~8px — a sub-pixel change well within the
visual-regression tolerance (all 24 baselines still match).  Now ~2px top
and bottom margin on the dev box, which absorbs the cross-platform hinting
variance.
Self-review of the geometry channel found two cleanups:

- Figure._push hashed (md5 of a 346 KB json.dumps) the geometry blob on
  EVERY push to detect change — ~1.4 ms of pure CPU per view-only frame
  just to discover nothing changed, partly undercutting the transfer win.
  Replace with a direct equality check against the last-sent geom dict: the
  b64 strings are the same objects when unchanged, so equality short-circuits
  (~0.05 ms/call, 28x faster) and no serialise happens until geometry really
  changes.  Drops the now-unused hashlib import; renames _geom_hash ->
  _geom_last.

- _applyGeom had a dead if/else (both branches identical).  Collapse to a
  single "apply cache when present" with a clearer comment.

No behaviour change; full suite 1323 passed / 58 skipped.
A touch-to-mouse bridge (_attachTouch, attached per panel in
_attachPanelEvents) translates touch gestures into the existing mouse/wheel
handlers via real MouseEvent/WheelEvent dispatch, so every panel kind and
every example becomes touch-capable with no handler rewrites:

  - 1 finger  drag  -> pan / orbit / drag a widget, ROI, marker or plane
  - 2 fingers pinch -> zoom (wheel), centred on the gesture midpoint
  - double-tap      -> dblclick -> the panel's double_click event

move/up dispatch to document (handlers listen there for off-canvas drags);
down/wheel/dblclick to the overlay canvas.  Overlay canvases set
touch-action:none so the browser yields gestures to the plot rather than
scrolling the page.

Tests: Playwright touch emulation (has_touch=True) across 2D/3D/1D drag,
pinch zoom, and double-tap; 416 existing mouse-interaction tests unchanged.
Full suite 1330 passed / 58 skipped.
Two issues made the voxel explorer's slice selector feel broken:

1. Snap-back (underlying code): Plot3D.to_state_dict() returned dict(_state)
   without refreshing overlay_widgets from the live widgets, so any view-only
   push that goes through the targeted-fields path (set_highlight / set_view)
   re-serialised a stale plane position and overwrote the in-progress drag in
   JS. The drag would jump back to the last Python-known integer position.
   Fix: to_state_dict() now always rebuilds overlay_widgets from the live
   _widgets, so every push path carries the current plane positions.

2. Faulty / jumpy highlight (example): the explorer snapped the highlight to
   integer voxel indices, so the marker jumped while the plane glided. Now it
   tracks smooth float positions (fx,fy,fz) for the highlight and the planes
   that follow, keeping integer indices only for slicing the 2D images.

Adds 3 regression tests (TestPlaneDragNoSnapBack) asserting set_highlight /
set_view preserve a live plane position. 3D suite + example execution green.
The voxel highlight appeared to land on random voxels in large (256³)
volumes because the example rendered a sparse random subsample of the
whole volume — the highlight projected to the correct (ix,iy,iz) but
almost never coincided with a displayed cube, so it floated in empty
space.

Render the voxels lying ON the three slice planes instead, re-cut live
on each drag. The marker is now always anchored on a real cube at the
slice intersection, the volume shows actual slice contents, and the
on-plane count is ~3·(N/step)² regardless of N, so it stays fast at 256³.

- Plot3D.set_point_colors: accept voxels panels, not just scatter, so
  slabs can be recoloured live each drag.
- Add the voxel grain explorer to the example smoke tests.
- Add a set_point_colors-on-voxels regression test.
A 256³ grain explorer produced ~8112 slab voxels, which crossed the
GPU_VOXEL_THRESHOLD (8000) and switched the panel to the Phase-1 WebGPU
voxel path. That path is not hardware-verified in CI — headless Chromium
exposes no WebGPU adapter, so the canvas fallback is what every test
exercised — and on real GPUs it rendered a sparse, see-through volume
(cubes appearing to float / vanish) instead of solid slice slabs.

- Raise GPU_VOXEL_THRESHOLD 8000 -> 20000 so mid-size volumes stay on the
  depth-sorted, visual-regression-tested Canvas2D renderer. The grain
  explorer (~8k cubes) now renders its full slabs (verified at N=256).
- GPU voxel pipeline: cullMode 'none'. The MVP swaps rows (r0,r2,r1) and
  negates depth, so cube winding can't be relied on for back-face culling;
  culling dropped the wrong faces. Translucent cubes need all faces anyway.
- GPU geometry cache now keys on point_colors_b64 too, so set_point_colors
  recolours voxels live instead of reusing a stale colour buffer.
- Add a browser regression test that a ~8k-voxel volume renders dense slabs
  on the canvas path (gpuCanvas hidden).
Correct the diagnosis from the previous commit. Large voxel volumes rendered 'empty' (only plane widgets + highlight, no cubes) in WebGPU-enabled browsers (PyCharm JCEF = Chrome 137). It was NOT the shader or back-face culling.

Real cause: the 3D panel stacks gpuCanvas (z-index 0, WebGPU voxels) under plotCanvas (z-index 1, decorations). Activating the GPU path cleared the plotCanvas bitmap but left its opaque CSS background, so the element painted over every GPU-drawn voxel.

- plotCanvas background -> transparent while GPU active; restored on fallback, device loss, and state-no-longer-wants-GPU.
- Revert earlier workarounds now shown unnecessary: GPU_VOXEL_THRESHOLD back to 8000, cullMode back to 'back' for voxels.
- Verified the voxel WGSL + _gpuMatrix on real hardware (NVIDIA TITAN X via native wgpu): three solid slabs; cull 'back' == 'none'.
- Retarget regression test to the DOM layering invariant; the active-GPU transparent swap is hardware-verified separately (no WebGPU adapter in CI).
Safari's experimental WebGPU can activate, render correctly for a while, then throw mid-draw or lose the device. The GPU path makes the decoration plotCanvas transparent and takes GPU-only branches (proj==null etc.), so a mid-draw throw left the frame half-built: voxels AND axes vanished, and only a window resize (forcing a full redraw) brought them back.

The catch block now disposes the GPU panel, restores the opaque plotCanvas background, and re-renders the whole panel once on the canvas path in the same frame (guarded against re-entry via p._gpuFellBack, reset per draw3d call). Verified by simulating an adapter+device that activates then throws on createCommandEncoder: no page errors, gpuCanvas hidden, plotCanvas opaque, voxels + axes rendered. Note: the kernel 'Task was destroyed but it is pending' warnings are ipykernel shutdown noise, unrelated to this.
The docs ⚡ bridge dispatched every interaction event with
pyodide.runPythonAsync(code-string), which parses + compiles a fresh Python
code string per event — ~1.2 ms/event in WASM, the dominant per-frame cost
of the Pyodide interaction path on a drag (30-60 events/sec). That's why 3D
orbit / plane-drag felt sluggish in the docs vs a live Jupyter kernel.

Define a pre-compiled `_awi_dispatch(fig_id, data)` function once at boot and
call its PyProxy directly from the message handler (~0.02 ms/event, ~50x
faster, measured end-to-end in Pyodide). The proxy is fetched lazily and
cached (robust to boot-step ordering) with a runPythonAsync fallback.

The Python->JS return path was already efficient (a compiled observer closure
calling js.window._anywidgetPush), so no change needed there.

All 12 bridge-boot tests pass.
Speed up Pyodide interaction dispatch ~50x (
… plots

A blank coordinate canvas in the matplotlib transData + PathCollection model:
set_xlim/ylim (+ aspect) and draw scatter/plot/fill/text as collection markers
in DATA coords. Reuses the 1-D data->canvas transform (kind='1d', hidden curve),
so no renderer change is needed for native data-coord drawing. Foundation for an
orix plotting backend (stereographic / IPF / pole figures). See ORIX_BACKEND_PLAN.md.
…[...])

PlotXY.scatter(c=[...]) now renders a per-offset colour gradient (matplotlib
PathCollection parity) instead of a single fill colour. Clarify in the orix-
backend plan that stereographic/IPF plotting stays in orix; anyplotlib only
provides the generic data-coord 2-D surface.
_plotRect1d now takes the panel and, when state.aspect==='equal', shrinks
and centres the box so one data unit spans equal pixels on x and y — the
matplotlib apply_aspect model. Baked into the shared rect helper so draw1d /
drawMarkers1d / overlay / hit-test all derive from the identical adjusted
box (matplotlib's transData is relative to the axes box).

A wide-panel IPF triangle now renders undistorted instead of stretched.
Tests: test_aspect_equal_renders_square vs test_aspect_auto_fills_panel.

Closes both renderer gaps the PlotXY demo exposed (per-point scatter
colours landed in 4f4e780); anyplotlib is now a complete-enough generic
2-D backend. The stereographic/IPF projection stays in orix.
PlotXY.pcolormesh(x, y, c) draws a matplotlib-style quad mesh in data
coords: (N+1,M+1) corner grids + an (N,M) scalar field (mapped through a
colorcet/matplotlib LUT between vmin/vmax) or colour-string array. Masked /
non-finite cells are skipped, so an orix pole_density_function histogram
(masked outside the fundamental sector) renders natively as an inverse pole
density-function heatmap — no matplotlib raster.

Renderer: the 1-D polygons path now honours per-polygon fill_color / color
arrays (matplotlib PathCollection), the same change made earlier for points;
edges default to the face colour for a seamless heatmap.

Tests: test_pcolormesh_builds_polygon_mesh (structure + masked-cell drop) and
test_pcolormesh_renders_gradient (chromatic gradient render).
A marker group (wired through add_polygons and PlotXY.pcolormesh) accepts a
clip_path: a (K,2) data-coord polygon the group is clipped to, matplotlib's
set_clip_path. drawMarkers1d applies it as a canvas clip inside the per-set
save()/restore(). Use it to keep a pcolormesh mesh inside a curved boundary
(an IPF fundamental sector) so the coarse edge cells don't overflow.

Test: test_pcolormesh_clip_path_clips_mesh (a triangle clip drops ~half a
square mesh's cells).
The 1-D dblclick handler already computed xdata; add ydata by inverting the
linear y transform. draw1d caches the rendered y bounds (p._1dDMin/_1dDMax) so
the inverse uses exactly what was drawn — for a PlotXY coordinate axis the y
range is y_range, not the hidden zero-curve's data_min/max. Matches the 2-D
image path so a coordinate axis can be picked in data space.

Test: test_double_click_reports_data_coords (centre click → centre of x/y range).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants