Repository navigation
Public API for the HyperSpy backend: data coordinates, identity, lifecycle - #84
Merged
Merged
Conversation
add_event_handler stamped _event_types onto every handler, which a bound method refuses, so registering obj.method raised AttributeError. The stamp is now best-effort, and handlers are keyed by (instance, function) for bound methods so remove_handler(obj.method) finds the registration even though each attribute access builds a new method object. Assisted-by: claude-code:claude-opus-5-5
widget.set(..., notify=False) is the documented way to move a widget from Python without echoing through its pointer_move handlers. The private _notify flag stays as an alias so existing callers keep working. Assisted-by: claude-code:claude-opus-5-5
Per-marker kwargs (radius, widths, heights, angles, U, V, sizes) now broadcast a length-1 sequence across the group instead of rejecting it when the group has more than one marker, and group-wide kwargs (linewidths, alpha, fontsize, outline_width, single colours) unwrap one. This matches matplotlib, which stores singleton styles as 1-tuples. Assisted-by: claude-code:claude-opus-5-5
Widgets and transform='data' markers on a Plot2D are placed in image pixels, but users work in the calibrated axis units. Plot2D.transform returns a live ImageTransform that converts points, (x, y) pairs and lengths both ways, mirroring the renderer's _axisValToImg2d mapping: pixel-centre axes for imshow, cell edges for pcolormesh, linear continuation past either end, and support for offset, non-unit scale, descending (origin='lower') and non-uniform axes. Plot2D also gains data_to_pixel / pixel_to_data shorthands and read-only x_axis / y_axis properties. Assisted-by: claude-code:claude-opus-5-5
A widget handle now exposes its owning plot and its kind, so callers (drag callbacks, removers, backends) no longer tag widgets with their own attributes or read _plot / _type. Widget.plot drops back to None when the widget is removed, and read-only properties can no longer be shadowed by an attribute assignment routed into the wire state. Assisted-by: claude-code:claude-opus-5-5
2-D widgets are placed in image pixels, so callers working in calibrated units had to convert every position, length and drag readout themselves. Every Plot2D add_*_widget (and add_widget) now takes coords="data": the widget converts its geometry through Plot2D.transform on every read and write, so attribute access, get(), to_dict(), set() and therefore drag callbacks all see calibrated values. The wire state stays in pixels (serialised via the new private _wire_dict), so the renderer and JS drags are untouched. Each widget class declares the role of its geometric fields (position, length, signed vector, point list, strokes) in _GEOMETRY. get() and to_dict() take a coords= override, and set_coords() switches frames without moving the widget. 1-D and bar adders accept coords="data" (their only frame) so one call works on any panel. Assisted-by: claude-code:claude-opus-5-5
transform='data' markers on a 2-D panel are positioned in image pixels. A group created with coords="data" (via the Plot2D add_* methods or markers.add) keeps its offsets, segments, vertices, widths/heights, radius and U/V in calibrated units and converts them through Plot2D.transform when serialised. Because conversion happens at render time the markers stay on their data position across set_extent. size_units='px' lengths and axes/display transforms are left alone. Assisted-by: claude-code:claude-opus-5-5
Axes now exposes its owning figure and attached plot through read-only properties, so callers stop tagging axes with their own attributes or reading _plot. image_plot returns the primary plot only when it is a 2-D image (Plot2D/PlotMesh). Overlays (extra lines, markers, widgets) live on the plot and can never shadow it. Assigning the same figure to ax.figure is tolerated for matplotlib-style code. Assisted-by: claude-code:claude-opus-5-5
Axes.set_xlabel / set_ylabel / set_title delegate to the attached plot, and before one exists they store the label and apply it (once) when imshow / plot / ... attaches the first plot, on grid axes and insets alike. Callers no longer need to buffer labels themselves. Assisted-by: claude-code:claude-opus-5-5
Figure.on_close(callback) registers a figure-level 'close' handler (a connection id is returned for remove_handler), and close() now fires 'close' on the figure registry before the panels', each with its own Event, and still hides the widget if a handler raises. Figure.closed and Figure.displayed expose the lifecycle state, and display() shows the figure at most once (force=True to repeat); being shown through the rich repr also counts as displayed. The lifecycle tests also cover the deferred Axes label setters. Assisted-by: claude-code:claude-opus-5-5
Assisted-by: claude-code:claude-opus-5-5
Assisted-by: claude-code:claude-opus-5-5
Lets code holding a marker group read its type, registry name and properties without reaching into _type / _name / _data. Assisted-by: claude-code:claude-opus-5-5
Axes.imshow(extent=(left, right, bottom, top)) and Plot2D.set_extent_edges() derive the per-pixel-centre axes from the image's outer edges, so callers coming from matplotlib no longer compute pixel centres themselves. top is the y at the top of the displayed image for either origin. The tests also cover the MarkerGroup accessors added in the next commit. Assisted-by: claude-code:claude-opus-5-5
Figure.get_axes only returned axes registered by _register_panel, i.e. ones with a plot, so apl.subplots(1, 1) gave fig.get_axes() == []. Track axes created by add_subplot and include the still-empty ones; plotted axes are listed exactly as before. Assisted-by: claude-code:claude-opus-5-5
_points_to_pixels turns an empty (0, 2) array into [] via tolist(), and _offsets_2d rejected [] as not (N, 2), so an empty coords="data" group could neither be created nor updated. Treat any empty offsets as zero markers. Assisted-by: claude-code:claude-opus-5-5
A coords="data" rectangle converted its corner (x, y) and size (w, h) field by field, so with origin="lower" or a descending x axis the pixel rectangle was anchored at the wrong corner (off-image in the descending case). Define the data frame as the span [x, x + w] x [y, y + h] with (x, y) the corner of smallest data value, convert both ends of each span and normalise to (min, length) in pixels, and invert the same way for reads, get/to_dict and drags. Partial sets fill the other half from the current geometry, and omitted constructor arguments default to the same pixel rectangle as in pixel coordinates. Marker rectangles/squares are centred and use unsigned lengths, so they were already right. Assisted-by: claude-code:claude-opus-5-5
The try/finally in Figure.close wrapped both the figure-level and the per-panel close dispatch, so a raising figure handler skipped every panel's close handlers (and a raising panel skipped later panels). Fire each registry on its own, still mark the figure closed and hidden, then re-raise the first exception. Assisted-by: claude-code:claude-opus-5-5
Widget.__repr__ printed the raw _data, i.e. image pixels, for a coords="data" widget whose attributes read in data units. Build it from to_dict() and tag data-frame widgets on 2-D panels. Assisted-by: claude-code:claude-opus-5-5
Labels set through Axes.set_xlabel/set_ylabel/set_title were applied
once to the first attached plot and then forgotten, so
ax.set_xlabel("A"); ax.imshow(a); ax.imshow(b) lost the label. Keep
them on the axes and re-apply them in _attach (grid and inset).
Labels set on the plot object itself stay with that plot. Adds tests
for grid axes and insets before attach and for replacement.
Assisted-by: claude-code:claude-opus-5-5
coords="data" widgets store pixels and stay put on screen after set_extent, while coords="data" marker groups keep their data position. Say so in Widget.coords and the getting started page. Covered by test_recalibration_keeps_pixels. Assisted-by: claude-code:claude-opus-5-5
Figure.close overrides ipywidgets' Widget.close but never called it, so the comm stayed open and ipywidgets' global instance registry kept every figure (and its plots and data) alive forever: a closed figure was never collectable. Call the base close after the handlers and the hide. Python-side state stays usable (set_data, to_html). Assisted-by: claude-code:claude-opus-5-5
Assisted-by: claude-code:claude-opus-5-5
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #84 +/- ##
==========================================
+ Coverage 91.24% 92.07% +0.83%
==========================================
Files 41 42 +1
Lines 4991 5489 +498
==========================================
+ Hits 4554 5054 +500
+ Misses 437 435 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The coords example was inserted mid-paragraph, so the sentence after it ended the literal block without a blank line (a -W failure). Keep the show_handles caveat with show_handles and put the example last. Assisted-by: claude-code:claude-opus-5-5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Companion to hyperspy/hyperspy#3623. HyperSpy's anyplotlib backend was reaching into private state (
_state,_plot,_hspy_*attributes it patched onto figures and axes) and doing its own data↔pixel maths. This branch adds the public API the backend needs, so the HyperSpy side can drop those workarounds.Why
Review of #3623 asked for the backend contract to be backend-neutral, not "matplotlib plus patches". The anyplotlib backend was the worst offender: it kept a shadow copy of each image's calibration, converted every widget and marker position to image pixels by hand, wrapped figures to intercept
close, and replayed axis labels set before the first plot. Every one of those had a bug in calibrated or flipped coordinates.What
Coordinates
Plot2D.transform(ImageTransform) withdata_to_pixel/pixel_to_data, plus publicx_axis/y_axis.imshow(extent=)andPlot2D.set_extent_edgestake matplotlib-style edge extents.coords="data"(default stays"pixel").Widget.get(key, coords=)reads either frame. Data-coordinate rectangles are converted as spans, so they stay correct on flipped axes.Identity and introspection
Widget.kind,Widget.plot.MarkerGroup.marker_type,nameandget().Axes.figure,primary_plot,plots,image_plot,has_plot.Figure.get_axes()now lists empty axes too. This is an API change: callers that assumed every axis has a plot need to checkhas_plot.Events and updates
Widget.set(..., notify=False)updates a widget without firing its own callbacks (used to sync from Python without feedback loops).Figure lifecycle
Figure.on_close,closed,displayed,display(). Figure and panel close handlers run independently, so one failing handler doesn't stop the rest.close()now also closes the underlying ipywidget so a closed figure can be garbage-collected. Behaviour change: the notebook view disappears on close, likeplt.close(). Without this, every closed figure leaked.Smaller fixes
Docs: the close/lifecycle behaviour, bound-method handlers,
notify=False, data-coordinate widgets and markers in Getting Started, and a note that data-coordinate widgets keep their pixel position on re-calibration while markers follow the new calibration.Tests
The full suite passes, including the Playwright tests. Each commit was checked on its own: the plot2d edge-extent commit was moved after the
MarkerGroupaccessors it tests.On the HyperSpy side,
hyperspy/drawing/backends/anyplotlibon #3623 uses only this public API. It now requiresanyplotlib>=0.11.0, so this needs to be in the next release.Assisted-by: claude-code:claude-opus-5-5