spacr.qt.screens.image_scatter

Workflow inputs and outputs

Image Scatter

Plot selected measurement columns with object-crop inspection.

Open: Image UMAP → Image Scatter.

Inputs and outputs below include conditional alternatives. The guidance and handoff notes say which route applies.

Inputs

  • Measured objects — measurements/measurements.db; object tables depend on the enabled cell, nucleus, pathogen and organelle masks. Relevant tables, depending on the route: cell, nucleus, pathogen, cytoplasm. Relevant columns, depending on the route: plateID, rowID, columnID, fieldID.

  • Object crops — data/**/*_png when save_png is enabled; png_list indexes saved crops. Supported workflows can instead stream crops from merged arrays and masks. Relevant tables, depending on the route: png_list. Relevant columns, depending on the route: png_path, prcfo.

Outputs

  • Projection and clusters — Image UMAP/PCA coordinate tables, selected clusters and figures for the loaded measurement data.

  • Figures and table exports — The output location chosen by the tool; exports describe the selected data and filters.

API reference.

Module tutorial.

V3 — a scatter plot where every point is the cell it stands for.

A measurement scatter is thousands of dots, and the question anybody actually has about it is what does that one look like. Without an answer, an outlier is a coordinate: you can see that something at (0.9, 12.4) is unusual and you cannot tell whether it is a real phenotype, a debris fragment or two cells segmented as one. That judgement is the whole reason to look at a plot of images rather than a plot of numbers, and it needs the image.

So: hover shows the crop, click opens it. The click goes through spacr.qt.linked_selection.open_objects(), so this screen does not import the annotation grid and the grid grows no method for it.

Not stuttering is a design constraint, not an optimisation

A hover handler runs sixty-plus times a second and a PNG decode costs milliseconds, so the obvious implementation — decode the crop under the cursor in mouseMoveEvent — produces a plot that lags behind the mouse and reads as broken. Three things keep it honest, and all three are load-bearing:

  • The point cloud is painted once into a pixmap and blitted. Hover repaints a ring, never ninety thousand dots.

  • Crop paths are resolved for the whole plot in one database pass at load time (spacr.qt.crop_thumbs.crop_paths_for_keys()), so hover is a dict lookup rather than a query.

  • Decoding is behind a short debounce and an LRU (spacr.qt.crop_thumbs.CropThumbnails). Sweeping the cursor across a cluster decodes what the cursor stopped on, not everything it crossed; a crop already in the cache appears with no delay at all.

Classes

ImageScatterScreen

A measurement scatter with the crop under the cursor beside it.

ScatterCanvas

Points in data space, painted once and blitted.

Functions

list_tables(→ List[str])

Every table in db_path, sorted. Worker-thread safe.

load_scatter_frame(→ pandas.DataFrame)

Read one measurement table, ready to plot. Runs on a worker thread.

make_image_scatter_screen(→ ImageScatterScreen)

Build the screen. The one constructor every caller goes through.

numeric_columns(→ List[str])

Columns worth putting on an axis, in the frame's own order.

Module Contents

class spacr.qt.screens.image_scatter.ImageScatterScreen(parent=None, *, threaded: bool = True)[source]

Bases: spacr.qt.linked_selection.LinkedView, PySide6.QtWidgets.QWidget

A measurement scatter with the crop under the cursor beside it.

Parameters:
  • threaded – False loads inline, so a test drives the screen without a worker thread and gets the same signals in the same order.

  • parent – parent widget; ownership only.

Build the screen, join the shared selection and arm its drop zone.

Parameters:
  • parent – parent widget, or None.

  • threaded – run reads on a worker thread. Set False in tests so a load finishes before it returns.

closeEvent(event) → None[source]

Stop background work before going away.

Parameters:

event – the Qt close event.

database() → str[source]

The measurements database this screen is pointed at, or “”.

key_at(index: int) → str[source]

The object key of point index, or "".

Parameters:

index – the point’s position in the plotted arrays; out-of-range gives "".

load_table() → None[source]

Read the chosen table and resolve every point’s crop, once.

on_linked_filter_changed(data_filter) → None[source]

The population narrowed: say so. A filter hides, so re-plot.

Applied to the plotted frame rather than to the source, so clearing the filter widens it back without another database read.

Parameters:

data_filter – the shared filter; its describe() text is shown in the status line before the plotted frame is re-plotted.

on_linked_selection_changed(selection) → None[source]

Ring the points another view selected.

Parameters:

selection – the shared selection whose points are ringed.

open_point(index: int) → Any[source]

Route point index’s object to whatever shows crops.

Parameters:

index – the point’s position in the plotted arrays.

Returns:

what the opener returned, or None when there is no key or nowhere to open it.

open_source() → None[source]

List the tables in the chosen database, off the GUI thread.

path_at(index: int) → str[source]

The crop path of point index, or "".

Parameters:

index – the point’s position in the plotted arrays; out-of-range gives "".

set_database(path: str) → bool[source]

Point the screen at path and list its tables.

The supported way for a caller to seed the source. Image Scatter is opened from Image UMAP as one of the views of the same objects, and a view that made the user retype the database the screen beside it is already reading would be a second module rather than a second view.

An empty path is ignored rather than clearing the box: “no path known” must not throw away a path the user typed.

Parameters:

path – path to the SQLite measurement database; stripped, and ignored when empty.

Returns:

True when the path was taken and the table listing started.

set_frame(frame: pandas.DataFrame, *, keys: Sequence[str] | None = None, paths: Dict[str, str] | None = None, x: str = '', y: str = '', note: str = '') → None[source]

Plot frame. The seam a test — or another screen — goes through.

Parameters:
  • frame – measurement rows to display in the scatter plot.

  • keys – one object key per row. Derived from the frame when it carries spacr.selection.OBJECT_KEY_COLUMNS and omitted. Without keys the plot still draws, but a click cannot open anything and says so rather than doing nothing.

  • paths – {key: crop path}, resolved once by the caller.

class spacr.qt.screens.image_scatter.ScatterCanvas(parent=None)[source]

Bases: PySide6.QtWidgets.QFrame

Points in data space, painted once and blitted.

Kept separate from the screen so the hit-testing and the caching can be tested without a database, and so a second consumer (a facetted view, a comparison grid) can reuse it.

Parameters:

parent – parent widget.

Create an empty scatter canvas with mouse tracking on.

Parameters:

parent – parent widget, or None.

__len__() → int[source]

Return the number of plotted points.

index_at(x: float, y: float) → int[source]

Nearest plotted point within HIT_RADIUS, or -1.

One vectorised pass. A spatial index would be faster asymptotically and is not worth the invalidation rules: at 200 000 points this is well under a millisecond, and the row cap in load_scatter_frame() keeps it there.

Parameters:
  • x – horizontal widget coordinate, in pixels.

  • y – vertical widget coordinate, in pixels.

leaveEvent(event) → None[source]

Clear the hover when the pointer leaves the canvas.

OTHERWISE THE THUMBNAIL STICKS: the last hovered point stays drawn over a canvas the pointer has left, which reads as a selection.

Parameters:

event – the Qt leave event.

mouseMoveEvent(event) → None[source]

Track which point is under the pointer, for the thumbnail.

Parameters:

event – the Qt mouse event.

mousePressEvent(event) → None[source]

Select the point under the pointer.

Parameters:

event – the Qt mouse event.

paintEvent(event) → None[source]

Draw the points and the hovered thumbnail.

Parameters:

event – the Qt paint event.

point_position(index: int) → Tuple[float, float] | None[source]

Widget coordinates of point index, or None if not drawn.

Parameters:

index – the point’s position in the plotted arrays.

resizeEvent(event) → None[source]

Re-lay the points for the new size.

Parameters:

event – the Qt resize event.

set_points(x: Sequence[float], y: Sequence[float], *, x_label: str = '', y_label: str = '') → int[source]

Plot x against y. Returns how many points are plottable.

Rows with a non-finite coordinate are kept in the arrays (so indices still line up with the caller’s frame) but never drawn and never hit — dropping them would silently renumber every point and make a click open the wrong object.

Parameters:
  • x – horizontal values, one per row, flattened to a float array; non-finite values are kept but not drawn.

  • y – vertical values, one per x; a length mismatch raises ValueError.

set_selected(mask: Sequence[bool]) → None[source]

Ring these points. A selection highlights; it never hides.

Parameters:

mask – one boolean per point; a mask of the wrong length clears the selection.

property hovered: int[source]

Index of the point under the cursor, or -1.

property plottable: numpy.ndarray[source]

Mask of the points with a finite coordinate pair.

spacr.qt.screens.image_scatter.list_tables(db_path: str) → List[str][source]

Every table in db_path, sorted. Worker-thread safe.

spacr.qt.screens.image_scatter.load_scatter_frame(db_path: str, table: str, *, limit: int = 200000) → pandas.DataFrame[source]

Read one measurement table, ready to plot. Runs on a worker thread.

Deliberately a module-level function taking strings: it must never touch a widget, and the surest way to guarantee that is for it not to have one.

Parameters:
  • db_path – path to the SQLite measurements database.

  • table – measurement table to read from the database.

  • limit – a hard row cap. A million-row table plotted at three pixels a point is a solid rectangle, so the cap costs no information and keeps a mis-click on the wrong table from being a two-minute freeze.

Raises:

FileNotFoundError – no such database — said plainly, because an empty plot from a wrong path looks exactly like an empty table.

spacr.qt.screens.image_scatter.make_image_scatter_screen(**_kwargs) → ImageScatterScreen[source]

Build the screen. The one constructor every caller goes through.

spacr.qt.screens.image_scatter.numeric_columns(frame: pandas.DataFrame) → List[str][source]

Columns worth putting on an axis, in the frame’s own order.