spacr.qt.screens.gate_editor

Workflow inputs and outputs

Gate Editor

Load a table or use Merge tables to create a working set with validated keys and aggregation. Define threshold or polygon gates on actual feature/coordinate columns, then apply the saved gate to compatible objects. Use Annotate to write the displayed gates to an annotation column; review the selected objects and choose binary or multiclass labels.

Open: Home → Gate Editor.

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.

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

Outputs

  • Reusable gates — Saved threshold/polygon gate definitions or a selected object set; apply a gate to the same feature definitions.

  • Training annotations — A chosen annotation column in measurements/measurements.db, table png_list; labels belong to object identities. Relevant tables, depending on the route: png_list. Relevant columns, depending on the route: prcfo.

Before this module

  • Image UMAP: Supply the matching coordinate/feature columns when defining a selection.

  • Measure: Use the actual measured feature definitions and units.

After this module

  • Annotate: Apply compatible gates, then review candidate labels.

  • Classify: Write reviewed gate selections to an annotation column, then select that same column and object population in Classify. Keep validation objects separate from training labels.

API reference.

Module tutorial.

V2 — Gate Editor: draw a region, name it, and it becomes a filter.

The flow-cytometry gesture, on spaCR measurement tables. Drag a threshold across a histogram or a polygon round the cloud on a two-parameter scatter, name it, and the shape becomes a spacr.selection.DataFilter clause that every open view honours — the UMAP, the plate map, the crop grid, the Graph Builder, Small Multiples.

What it is for. Selecting a population by its measurements once Measure has run: the cells above an intensity threshold, or one cluster in a two-feature scatter. Gates can sit inside other gates, and each shows its object count and its percentage of both its parent gate and the whole table.

What it needs. A measurement table: one table of a measurements.db, where the object tables are offered first, or a CSV or TSV file. Several databases can be loaded as one table; plate identifiers that collide between them are reported rather than silently pooled. Database tables are read as a sample set by sample_fraction and capped by max_points, both in the Gate Editor settings. Merge tables beside the table picker opens the same validated default/custom merge workflow as Graph Builder. Name the result, inspect row counts, unmatched records and the aggregation preview, then create it. Multiple child tables are aggregated independently onto composite image, time and object keys. Saved gates embed the derived-table definition and reconstruct it against the same source/schema. Export evaluates the full merged table and writes gates using the original base object’s identity. External tabular merges remain gateable; image annotation/export are unavailable when image/object provenance cannot be verified.

What it produces. Select measurement rows with threshold, rectangle, ellipse, polygon, or wand tools. Use box, cylinder, or prism shapes in the 3D view. Combine existing gates to define additional populations. Publishing a gate restricts each linked view to its selected population.

Save the selection strategy to a JSON file and load it for another plate. The default filename is gates.json. Export each gate as a column of the filters table in the measurements database. Save the graph as PNG or PDF.

Before export, compare the current gates, including unsaved edits, with analysis locks that contain their saved strategy. Record the verdict for each gate in filter_export_provenance. A missing lock does not mean the gates were verified. These records describe only the exported gates. They do not verify unrecorded merge definitions or unrelated pipeline settings.

What to do next. Look at the gated population in the views that follow the shared filter, such as Image UMAP, Graph Builder and the crop grid, and load the saved gating strategy on the next plate.

Assembles:

register() is not called at import; read its docstring.

Classes

GateEditorScreen

A table, two axis pickers, the gating surface, and save/load.

Functions

make_gate_editor_screen(→ PySide6.QtWidgets.QWidget)

Factory handed to spacr.qt.app.register_app().

register(→ bool)

Put the Gate Editor in the app registry. Idempotent.

Module Contents

class spacr.qt.screens.gate_editor.GateEditorScreen(parent=None, *, link=None, threaded: bool = True)[source]

Bases: spacr.qt.widgets.derived_table_source.DerivedTableSource, PySide6.QtWidgets.QWidget

A table, two axis pickers, the gating surface, and save/load.

Parameters:
  • parent – parent widget.

  • link – the LinkedSelection this screen’s views join, so a selection made here reaches the others. None joins the shared one; pass a private one in a test.

  • threaded – whether the work runs off the GUI thread. False runs it inline, which is what makes a test deterministic.

Build the screen: header, working-set chips, axis pickers, plot and side panels.

Parameters:
  • parent – parent widget, or None.

  • link – shared selection link, passed through to the gate panel and the filter panel so both answer to the same selection.

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

active_jobs() → int[source]

How many background jobs this screen is running.

Returns:

the job count.

align_side_panel() → int[source]

Line the Filter / Search page up with the graph and gate table.

Settings above the graph share one row. The tabs of the panel on the right sit level with that row, and the containers under both start at the same height. The row is the graph panel’s own tool row – the table chips and the X / Y / Z pickers were moved into it – so the tabs already start level with it; what differs is where each container begins below its row. The tab page is moved down by that difference, measured each time, so whatever the row gains (the 3D plane controls under it) the page follows.

A tab bar given a fixed height does not move the page under it – QTabWidget places the page from the bar’s size hint – so the page’s own offset is what is set.

Returns:

the offset applied to the page, in pixels.

annotate_from_gates() → None[source]

Label every object from the gates currently shown.

The SHOWN gates, not all of them: ticking a gate on and off is already how the user says which ones count, so asking again in a dialog would be asking a question they have already answered.

apply_settings(settings: spacr.qt.widgets.gate_settings.GateEditorSettings) → None[source]

Take new settings, re-reading the table only if one of them needs it.

Two settings cost a read – the sample fraction and the row cap. The rest are drawing, and re-reading a large table because the user nudged a colour map is the lag this dialog exists to remove.

Parameters:

settings – the complete new GateEditorSettings; it replaces the current settings and is passed to the gate canvas and the search panel.

ask_axis_cutoffs(axis: str) → Tuple | None[source]

Ask for the lowest and highest value axis should show.

Returns the pair that was applied, or None when the request was cancelled or could not be read.

Parameters:

axis – "x" or "y"; an axis with no measurement chosen returns None without asking.

axis_column(axis: str) → str[source]

The measurement drawn on "x" or "y", or "".

Parameters:

axis – "x" or "y"; converted to str, and any other value gives "".

axis_menu_items(axis: str)[source]

The axis menu as data, so its CONTENTS can be tested offscreen.

Separated from the QMenu for the same reason graph_menu_items() is: an offscreen Qt cannot grab for a popup, so a test that builds a real menu hangs.

Parameters:

axis – "x" or "y"; the menu is built for the measurement currently drawn on it.

axis_under(point) → str | None[source]

Which axis a right-click at point landed on, or None.

point is in the canvas widget’s own coordinates, which is what Qt hands a custom context menu. Getting from there to the figure means two conversions and both are easy to get wrong: the matplotlib canvas is a CHILD of the gate canvas rather than the same widget, and matplotlib’s display coordinates count upward from the BOTTOM while Qt counts downward from the top.

Returns None for a click inside the plotting rectangle, which is where the plot’s own menu belongs.

Parameters:

point – the click position as a QPoint in the gate canvas widget’s own coordinates.

choose_load_filters() → None[source]

Ask which saved filters to load.

choose_load_gates() → None[source]

Ask which saved gates to load.

choose_save_filters() → None[source]

Ask where to save the current filters.

choose_save_gates() → None[source]

Ask where to save the current gates.

choose_table() → None[source]

Ask which table in the project to gate.

clear_axis_cutoffs(axis: str) → bool[source]

Let axis follow the data again. Returns whether it was cut.

Parameters:

axis – "x" or "y"; an axis with no measurement chosen, or with no cutoffs set, returns False.

closeEvent(event)[source]

Let the panel close first, so it can unlink its canvas.

Parameters:

event – the Qt close event.

database_labels() → List[str][source]

The name each loaded database carries in the provenance column.

Asked of spacr.multi_database rather than derived here, so a chip and the source_database value it stands for cannot disagree – a chip reading plate1 beside a legend reading measurements (2) is provenance the user cannot follow.

eventFilter(watched, event) → bool[source]

Keep the side panel’s page level with the graph as rows come and go.

Parameters:
  • watched – the graph’s splitter or the side tabs.

  • event – the event.

Returns:

False, so the event is handled as usual.

export_gates() → None[source]

Write every gate to the database as a column of filters.

graph_menu_items()[source]

The plot menu as data: [(label, enabled, callback, why)].

Separated from the QMenu so the CONTENTS can be tested without a display. An offscreen Qt cannot grab for a popup, so a test that builds a real menu hangs – which is how this method came to exist.

Every callback is an EXISTING method. The menu is a second route to the same code, never a second implementation.

is_busy() → bool[source]

Whether anything is still running.

What the window asks before closing: a gate applied to a table that is still loading would be applied to half of it.

Returns:

True while work is outstanding.

load_filters(path: str) → List[str][source]

Apply a saved filter set. Reports columns this table does not have.

Saying so matters more here than it looks. A filter set saved against one plate and loaded against another is an ordinary thing to do, and a set that half-applies selects the wrong rows while looking like it worked.

Parameters:

path – a JSON filter-set file written by save_filters(). A file that cannot be read is reported on the source line and gives an empty list.

load_gates(path: str) → bool[source]

Read a gating strategy and apply it to the loaded table.

A strategy naming a measurement this table does not carry loads anyway and reports the problem: the gates are still there to look at and fix, which is more use than refusing the file.

Parameters:

path – a JSON gate file written by save_gates(). A file that cannot be read is reported on the source line and gives False.

load_path(path: str, table: str | None = None) → None[source]

Read a CSV or one table of a measurement database, off the GUI thread.

Parameters:
  • path – a CSV, TSV or TXT file (by extension), read as one table; any other path is opened as a SQLite measurement database and its tables are listed in the picker. It becomes the only database in the working set.

  • table – the database table to read, also selected in the picker when the database has it; None reads the picker’s current table.

load_paths(paths, table: str | None = None) → None[source]

Load several measurement databases as one frame.

Every decision that can go quietly wrong – plate-id collisions, mismatched column sets, provenance – is delegated to spacr.multi_database, so this screen and Image UMAP cannot disagree about them.

A collision is REPORTED, not resolved. Two databases that each hold a plate called plate1 are two experiments, and pooling them would compute every per-well number over both at once with nothing on screen to say so. The user is told which plate ids clash, because they are the only one who can say whether they are the same plate.

Parameters:
  • paths – the SQLite measurement databases to merge, as paths; each is converted to str, and an empty list does nothing.

  • table – the table to read from every database; None uses the first table of the first database.

open_settings() → None[source]

Show the settings window.

Not modal, and not re-created: a settings window you have to close to see what it did is a settings window you cannot tune anything with. The same dialog is raised again so its tab and scroll position survive, which is the difference between adjusting a value and hunting for it.

reduce_to_components() → str | None[source]

Project every measurement onto components, and gate on those.

This is what xD MEANS here. More measurements than can be drawn is not a drawing problem to be solved with another axis – past three there is no fourth to add – so the measurements are projected and the projection is gated.

The components come back as ORDINARY COLUMNS, so every existing tool works on them unchanged: the same rectangle, oval, polygon, wand and cluster, saved and exported the same way. A gate on PC1 vs PC2 is the same kind of object as a gate on area vs intensity.

Returns:

an error to show, or None on success.

remove_database(name: str) → None[source]

Drop one database from the working set and re-merge the rest.

By its CHIP’s label or by its path, because the chip shows the label and a caller usually holds the path.

This is also the resolution the screen offers for a plate-id collision, and the reason it does not offer on_collision='qualify' instead: qualifying rewrites plate1 to runA-plate1, which makes the keys unique by hiding which experiment a plate belongs to inside its own id, where nothing can block on it or colour by it. Dropping one of the two databases keeps every remaining number meaning what it says.

Parameters:

name – the database’s chip label or its path. An unknown name, or the last remaining database, does nothing.

remove_table(name: str) → None[source]

Drop a table from the working set.

The last one cannot be dropped: a gate editor with no table is a screen with nothing on it, and the user’s next move would be to load the same table again.

Parameters:

name – the table name. A table not in the working set, or the last remaining table, does nothing.

save_filters(path: str) → str[source]

Write the current filter set to path.

Parameters:

path – the file to write the current filter set to, as JSON; an existing file is overwritten.

save_gates(path: str) → str[source]

Write the gating strategy to path.

Parameters:

path – the file to write the gating strategy to, as JSON; an existing file is overwritten.

save_graph(path: str = '') → str[source]

Write the current graph to a PNG or PDF.

The format comes from the figure-format PREFERENCE rather than from a setting of this screen’s own; the design is explicit that a second place to answer “am I making PDFs” is one too many. The file dialog still lets a single save differ, because “save as” is when a user thinks about format.

Rendering goes through render_figure_to_png, the same helper the figure queue uses, rather than savefig: it applies the figure colour, line and text-size preferences, caps the display raster, and in PDF mode writes a genuine vector page beside the PNG with its fonts embedded as TrueType. Calling matplotlib directly would give none of that.

THE FILE GETS THE PRINT STYLE. Decision 2026-09-25: “saved graphs (PDF/PNG) get a WHITE PRINT STYLE (white background, dark text/axes/lines) whatever the screen theme”. The render passes for_print=True, which styles a detached copy white with dark ink, so the graph on screen keeps the theme’s colours. The extension the user picked decides whether the PDF is written, so choosing PDF in the dialog under a PNG preference still gives a PDF.

Parameters:

path – destination. Empty opens a file dialog.

Returns:

the path written, or “” when cancelled or nothing is drawn.

set_axis_cutoffs(axis: str, low, high) → None[source]

Show only low to high of the measurement on axis.

Either end may be None, meaning the data decides it.

Parameters:
  • axis – "x" or "y"; an axis with no measurement chosen does nothing.

  • low – the lowest value to show, or None.

  • high – the highest value to show, or None.

Raises:

spacr.qt.widgets.gate_canvas.CutoffError – when the low end is not below the high one.

set_axis_scale(axis: str, scale: str) → None[source]

Lay axis out on scale, from the menu or from the window.

The menu is a second ROUTE to the scale the settings window already holds, never a second copy of it: this writes the same field, so the two cannot come to disagree about how the plot is drawn.

Parameters:
  • axis – "x" or "y"; it names the <axis>_scale settings field, and a value with no such field does nothing.

  • scale – a matplotlib axis scale, one of AXIS_SCALES ("linear", "log", "symlog" or "logit").

set_frame(frame: pandas.DataFrame, *, label: str = '') → None[source]

Point the screen at a table to gate.

Parameters:

frame – the rows, or None to clear.

settings() → spacr.qt.widgets.gate_settings.GateEditorSettings[source]

The screen’s settings, in the shape a settings file wants.

Returns:

the settings dict.

show_aggregation_rules() → None[source]

The per-column merge rules, for the columns actually loaded.

spacr.qt.screens.gate_editor.make_gate_editor_screen(app_key: str | None = None) → PySide6.QtWidgets.QWidget[source]

Factory handed to spacr.qt.app.register_app().

spacr.qt.screens.gate_editor.register() → bool[source]

Put the Gate Editor in the app registry. Idempotent.

The row itself – the key, the name, the blurb, the section, the “no headless run” sentence, the API doc link and the nine translations of the display name – is declared in spacr.qt.app_catalog. spacr.qt.app.register_app() distributes those into the four tables each used to need a hand-edit in, and this function’s whole job is to name which row. That is what lets the app be registered without importing this module at all: the launch reads the table, and the screen is imported when somebody opens it.

Returns:

True if this call is what registered it.