spacr.qt.widgets.gate_editor¶
Drawing gates — the chart, the shapes on it, and the hierarchy beside it.
The chrome over spacr.qt.widgets.gate_spec. Everything about what a gate
means — the geometry, the chain, the percentages, the filter clause — is in
there; this module is three drawing tools and a tree.
GateCanvas subclasses
spacr.qt.widgets.graph_builder.GraphCanvas for the same reason
spacr.qt.widgets.trellis_view.TrellisCanvas does: the marks, the hue
order, the density raster and the large-data policy are one implementation.
What it adds is a mode. With no tool armed the drag is the inherited brush;
with a tool armed the drag draws a shape and nothing is published until the
gate is named.
The three tools, and why the drag means different things¶
Threshold — a horizontal sweep on a one-column plot. Only the x extent is read, because the y axis of a histogram is a count and gating on a count is not a thing anyone means.
Rectangle — a drag on a two-column plot, both extents read.
Polygon — click a vertex at a time, then close it. Three vertices minimum, enforced where the gate is built rather than where it is applied.
The population is the parent’s, always¶
A gate is drawn inside whatever is selected in the tree, and the canvas shows that parent’s population rather than the whole table — because a gate drawn on a picture of everything and then applied to a subset is a gate nobody placed. The header says which population is on screen and how many objects that is.
Classes¶
A |
|
Canvas, tools and hierarchy: the whole gating surface. |
|
The gating hierarchy, with each gate's n and its percentage of parent. |
Functions¶
|
Size |
Module Contents¶
- class spacr.qt.widgets.gate_editor.GateCanvas(parent=None, *, link=None, source: str = 'gate_editor')[source]¶
Bases:
spacr.qt.widgets.graph_builder.GraphCanvasA
GraphCanvasyou can draw gates on.Adds interactive gate drawing, dragging and hit-testing to the shared canvas, and pins the axes while a gate exists – see
RESCALE_ON_FILTERfor why that is not optional here.Emits
gate_drawnwith a finishedGatethat has no name yet – naming is the host’s job, because a gate is not a gate until it is named and a dialog does not belong in a canvas.- Parameters:
parent – parent widget.
link – the
LinkedSelectionthis view joins, so selecting here selects in every other view on it.Nonejoins the shared one; pass a private one in a test so the selection does not reach the rest of the application.source – this view’s name on that link, stamped onto everything it publishes – which is how a view knows not to answer its own selection.
Build a gating canvas over one graph spec.
- Parameters:
parent – parent widget.
link – the shared selection to join, if any.
source – the table being gated.
- anchor_axis() str[source]¶
Which world axis a 3-D drag rotates about.
- Returns:
the axis name, defaulting to
z.
- anchor_plane() Tuple[str, str, str] | None[source]¶
(first, second, normal)of the plane a drag draws on.READ FROM THE PICKED AXIS, not from the camera. Three planes are visible in the volume;
set_anchor_axis()says which one is armed, and turning the view does not change it. The normal is the picked axis, and the other two are the plane – so picking Z means “draw on X/Y and extend along Z”, which is what the axis labels on the plot already say.- Returns:
None only in 2D, where there is no third measurement for a shape to be extended along.
- apply_settings(settings) None[source]¶
Apply drawing settings and redraw the canvas once.
Missing attributes retain their defaults so older saved settings and lightweight settings objects remain usable.
- Parameters:
settings – a
GateEditorSettingsor any object with some of its attributes (default_tool,point_size,colour_map,log_xand so on), read withgetattr().
- box_from_view() spacr.qt.widgets.gate_spec.Gate | None[source]¶
A box gate enclosing the volume’s current limits.
The view is the gesture. Spinning and zooming until a population fills the box is already the act of choosing it, and a rectangle dragged on a rotated projection has no defined extent along the axis pointing at the viewer – reading one off would invent a number.
- clear_pending() None[source]¶
Throw away a part-drawn gate and tell the panel the count is zero.
The signal matters as much as the clearing: the panel’s Finish button is enabled by the vertex count, and clearing without saying so would leave it offering to close a polygon that no longer exists.
- close_polygon(*, name: str = '(unnamed)', emit: bool = True) spacr.qt.widgets.gate_spec.Gate | None[source]¶
Finish the polygon being clicked out.
- Returns:
the gate, or
Nonewhen fewer than three vertices have been clicked — the canvas does not raise at the user for clicking twice and changing their mind.
- close_polygon_now() None[source]¶
Close the pending polygon and emit at most one completed gate.
The first-vertex shortcut and the Close button both use this method.
- decorate_axes(ax) None[source]¶
Grid and log scales.
Log is applied only where it is legal: a log axis over data that reaches zero or below draws nothing at all, which reads as the plot having broken rather than as the setting being inapplicable.
- Parameters:
ax – the Matplotlib axes to decorate in place.
- drag_mode() str[source]¶
What dragging does right now: spin the view, or draw.
- Returns:
the mode’s name, defaulting to
spin.
- gate_at(x: float, y: float) str | None[source]¶
Name of the topmost gate containing
(x, y), or None.Topmost = last drawn, which is the one the user sees on top and therefore the one they mean by clicking there.
Deliberately does NOT consult
population(). Hit-testing a gate is pure geometry – is this coordinate inside this shape – and needs no rows at all. Asking for the population first meant that whenever it was unavailable, dragging died silently:population()returns None when the active gate no longer exists, which is exactly the state left behind by DELETING a gate. The remaining gate then looked “fixed and I cannot move”.Only gates on the current axes are tested, so a gate belonging to a different pair cannot be grabbed invisibly.
- Parameters:
x – the point’s x coordinate in data units; tested as the value of each gate’s first column.
y – the point’s y coordinate in data units; tested as the value of a two-column gate’s second column.
- gate_colour(name: str) str[source]¶
The colour a gate is drawn in, stable for the life of the set.
By POSITION in the gate set, so a gate keeps its colour while others are added, and so the outline, the ringed objects and the row in the gate list all agree – which is the point: a plot with four gates on it should be readable without clicking each one.
Falls back to the accent when a gate is not in the set (a shape being dragged out has no position yet).
- Parameters:
name – the gate’s name.
- gate_from_drag(x0: float, y0: float, x1: float, y1: float, *, name: str = '(unnamed)') spacr.qt.widgets.gate_spec.Gate | None[source]¶
Build the armed tool’s gate from a swept rectangle.
Public so the interaction can be driven without synthesising mouse events — the same seam the Graph Builder’s
brush()provides.- Parameters:
x0 – x of the drag’s start, in data units; the low bound of a threshold gate.
y0 – y of the drag’s start, in data units.
x1 – x of the drag’s end; the high bound of a threshold gate.
y1 – y of the drag’s end.
- Returns:
a threshold, rectangle or ellipse gate for the armed tool, or
Nonefor any other tool, missing columns, or a degenerate ellipse.
- gate_from_screen_outline(outline, *, name: str = '(unnamed)') spacr.qt.widgets.gate_spec.Gate | None[source]¶
A
ViewGatefor an outline drawn on the screen.- Parameters:
outline – the outline’s vertices in canvas pixels, in the order drawn.
- Returns:
the gate, or None when the outline has fewer than three distinct corners or no area, or the volume is not on screen.
- handle_at(event) Tuple[str, str] | None[source]¶
The anchor under the pointer as
(gate name, role), or None.Measured in pixels for the same reason polygon-closing is: a tolerance in data units would be unusable on one axis whenever the two measurements have different ranges, which is nearly always.
Only ENABLED gates are grabbable. A hidden gate is not on screen, and an invisible anchor that catches the mouse is indistinguishable from the plot being broken.
- Parameters:
event – a Matplotlib mouse event; its
inaxesand itsxandydisplay (pixel) coordinates are read.
- is_gate_enabled(name: str) bool[source]¶
Whether
nameis drawn. Unknown gates are on: a gate that has never been toggled has never been turned off.- Parameters:
name – the gate’s name.
- paintEvent(event) None[source]¶
Keep the plotted 2D or 3D points over one solid rounded surface.
- Parameters:
event – the Qt paint event for this canvas.
- Returns:
None.
- point_colormap()[source]¶
The colour map the user chose, by name.
An unknown name falls back rather than raising: matplotlib’s registry changes between versions, and a colour map that no longer exists must not take the whole plot with it (INVARIANTS 10).
- population() pandas.DataFrame | None[source]¶
Return the locally filtered table displayed beneath gate overlays.
Selecting a gate does not subset this frame; the selected gate only determines the parent of the next gate drawn.
- render_now() None[source]¶
Draw the parent’s population, then the gates on top of it.
In 3D the volume replaces all of it: the gate tools are 2D gestures on a flat axes, and running them against a rotated projection would produce gates whose coordinates mean nothing.
- screen_to_volume(event)[source]¶
Data coordinates on the explicitly selected anchor plane.
- Parameters:
event – a Matplotlib mouse event; its
xandydisplay (pixel) coordinates are projected onto the anchor plane.- Returns:
(first column, value, second column, value), orNonewhen there is no volume view.
- set_anchor_axis(axis: str) None[source]¶
Pick which plane a drag draws on. Chosen, never inferred.
The first version of this read the plane off the camera and returned nothing unless the view was square-on, so turning the volume silently changed what the next gate would mean. The user picks a plane and it stays picked.
- Parameters:
axis –
"x","y"or"z", the normal of the plane drawn on; anything else is taken as"z".
- set_drag_mode(mode: str) None[source]¶
'spin'or'draw'. They were competing for one button.- Parameters:
mode –
"spin"or"draw"; anything else is taken as"spin".
- set_gate_enabled(name: str, on: bool) None[source]¶
Turn a gate’s outline and highlight on or off.
Off means NOT DRAWN, never deleted and never removed from the set: the gate keeps its shape, its parent and its children, and comes back exactly as it was. Its rows stay on the plot either way – toggling changes what is marked, not what exists.
- Parameters:
name – the gate’s name.
on –
Truedraws it,Falsehides it.
- set_gates(gates: spacr.qt.widgets.gate_spec.GateSet, *, active: str | None = None) None[source]¶
Display
gatesand selectactiveas the hierarchy parent.- Parameters:
gates – the gate set to draw; it replaces the current one.
- set_mode(mode: str, *, z_column: str = '') None[source]¶
Switch between the 2D scatter and the 3D volume.
- Parameters:
mode –
"2D","3D"or"xD"; anything else is taken as"2D".
- set_pending_depth(low, high) None[source]¶
The slab depth the next volume gate is made with.
The depth is represented as a slab you drag out over “all the way through”: the depth is a second gesture after the shape is drawn, so the gate is finite from the start rather than something to narrow afterwards in a panel.
(None, None)means full depth, which is what an undragged shape means and what the 2D gate on that plane already meant.- Parameters:
low – lower bound along the plane’s normal axis, in data units, or
Nonefor unbounded.high – upper bound likewise; the two are swapped if given in the wrong order.
- set_spin_axis(axis: str) None[source]¶
Constrain subsequent volume rotation to one data axis, or free it.
"x","y"and"z"turn the volume about that measurement’s own axis, which stays put on screen.""is the default: a trackball that turns about both screen axes at once, so every orientation is reachable. Anything else falls back to free.- Parameters:
axis –
"x","y","z", or""for free rotation.
- set_tool(tool: str) None[source]¶
Arm a drawing tool, or
""to go back to brushing.- Parameters:
tool – a gate kind from
GATE_KINDS(e.g."rectangle","polygon"), or""; anything else raisesGateError. Any part-drawn gate is discarded.
- set_volume_shape(shape: str) None[source]¶
Which of
VOLUME_SHAPESa drag draws.- Parameters:
shape – a shape key such as
"box","lasso"or"polygon"; empty is taken as"box".
- snap_to_nearest_axis() Tuple[float, float][source]¶
Turn the volume square-on to whichever axis it is nearest.
A volume stopped at an arbitrary angle cannot be read off at all – the point of snapping is that a 3D gate is always finally judged from a view where one measurement is flat.
- view_projection(ax=None) numpy.ndarray | None[source]¶
The camera now on screen, as a
ViewGatestores it.matplotlib’s own projection matrix for the current angles and limits, rescaled so its homogeneous coordinate is positive in front of the camera. Taken fresh from
get_projrather than from the matrix of the last draw, so a gate drawn straight after a spin uses the angle the spin left.- Returns:
a 4 x 4 array, or None when the volume is not on screen.
- volume_axis_map()[source]¶
How screen pixels map to data on the two axes the user selected.
Returns
(x_column, y_column, invert)whereinvert(dx, dy)turns a movement in PIXELS into one in data units, or None when the view is not square-on.Built by projecting the data’s own corners and measuring where they land, rather than by trusting a formula for the projection matrix: matplotlib has changed how
ax.Mis spelled more than once, and a drag that silently lands in the wrong measurement is worse than one that refuses.The first implementation chose the two axes that happened to face the camera. That made the X/Y/Z plane buttons cosmetic: turning the view could make a gate land on a different plane from the blue aura. The selected plane is now the source of truth. A genuinely edge-on plane is refused because its two data dimensions collapse to one screen line and no inverse exists.
- volume_shape() str[source]¶
Which solid a 3-D gate is drawn as.
- Returns:
the shape’s name, defaulting to
box.
- property gates: spacr.qt.widgets.gate_spec.GateSet[source]¶
The gates drawn on this canvas.
- Returns:
the gate set.
- class spacr.qt.widgets.gate_editor.GateEditorPanel(parent=None, *, link=None, source: str = 'gate_editor')[source]¶
Bases:
PySide6.QtWidgets.QWidgetCanvas, tools and hierarchy: the whole gating surface.
publish()is the point of the screen — it turns the selected gate into aDataFilterclause and pushes it onto the shared filter, so every open view narrows to the gated population.- Parameters:
parent – parent widget.
link – the
LinkedSelectionthis view joins, so selecting here selects in every other view on it.Nonejoins the shared one; pass a private one in a test so the selection does not reach the rest of the application.source – this view’s name on that link, stamped onto everything it publishes – which is how a view knows not to answer its own selection.
Build the gating surface: canvas, tools and hierarchy.
- Parameters:
parent – parent widget.
- apply_settings(settings) None[source]¶
Take the settings that change how the gates surface draws.
The canvas takes the drawing ones. Sampling is the screen’s job – it owns the table and the read – and the 3D ones belong to a workspace that does not exist yet. A setting silently read in two places is how the two get to disagree.
The CLUSTERING ones are kept here rather than passed on, because the Cluster button is on this panel and used to ignore them entirely.
- Parameters:
settings – a
GateEditorSettings(or a compatible object); kept by the panel and passed toGateCanvas.apply_settings().
- closeEvent(event)[source]¶
Close the canvas first, so it can unlink from the shared selection.
- Parameters:
event – the Qt close event.
- publish() spacr.selection.DataFilter | None[source]¶
Publish objects inside the selected gate as a shared selection.
Objects outside the gate remain visible. The status label reports missing input, evaluation errors, and tables that lack shareable object identifiers.
- reset_view() None[source]¶
Undo a zoom and a spin in one place.
One button for both because from the user’s side there is one problem – “the graph is not where it was” – and having to know whether they zoomed or rotated to get out of it is the kind of distinction only the implementation cares about.
- run_cluster(*, ask: bool = True) None[source]¶
Find dense populations and add one gate per cluster.
Clusters become REAL gates rather than a separate kind of selection, so each is editable, nestable, serialisable and usable as a filter the moment it appears – everything a hand-drawn gate can do, because it is one.
- Parameters:
ask – open the parameter dialog first. The Cluster… button does; the Search TAB does not, because the tab IS the parameter editor – asking again there would be asking twice for the same numbers. Both read the same settings object, which is what stops the two from disagreeing.
- set_frame(frame: pandas.DataFrame | None) None[source]¶
Point the panel at a new table.
- Parameters:
frame – the rows to gate, or None to clear.
- set_gates(gates: spacr.qt.widgets.gate_spec.GateSet) None[source]¶
Replace the whole set — loading a saved gating strategy.
- Parameters:
gates – the gate set, handed to both the canvas and the tree.
- set_namer(namer) None[source]¶
Install
namer() -> strto name a freshly drawn gate.Injectable so a test can name gates without a modal dialog standing in a headless run’s way — the same reason
DataFilterPaneltakes an injectable link.- Parameters:
namer – zero-argument callable returning the new gate’s name.
- set_projection_active(on: bool) None[source]¶
Show the xD button as on or off without re-emitting.
Used when a projection was asked for and could not be made: the button must not keep claiming something that did not happen.
- Parameters:
on – the checked state to show.
- set_spec(spec: spacr.qt.widgets.graph_spec.GraphSpec) None[source]¶
Draw a different chart under the gates.
- Parameters:
spec – the graph spec for the canvas.
- set_spin_controls_visible(visible: bool) None[source]¶
Show or hide the 3-D plane controls.
Hidden for a 2-D chart, where an axis picker and a spin toggle are controls for something the view cannot do.
- Parameters:
visible – True to show them.
- property gates: spacr.qt.widgets.gate_spec.GateSet[source]¶
The gates currently drawn.
- Returns:
the gate set.
- class spacr.qt.widgets.gate_editor.GateTree(parent=None)[source]¶
Bases:
PySide6.QtWidgets.QWidgetThe gating hierarchy, with each gate’s n and its percentage of parent.
Both percentages are shown, from
GateStats— 90% of a parent that is 2% of the table is 1.8% of the objects, and a strategy that prints only the first is flattering itself.- Parameters:
parent – parent widget.
Build the hierarchy view.
- Parameters:
parent – parent widget.
- active_gate() str[source]¶
The name of the selected gate.
- Returns:
the gate’s name, or
""when nothing is selected.
- is_enabled(name: str) bool[source]¶
Whether
nameis ticked. Unknown gates are on.- Parameters:
name – the gate’s name.
- select(name: str) None[source]¶
Select a gate by name, wherever it sits in the hierarchy.
- Parameters:
name – the gate’s name.
- set_colour_source(source) None[source]¶
Tell the tree where gate colours come from – see
_colour_for.- Parameters:
source – callable taking a gate name and returning a colour string, normally
GateCanvas.gate_colour(), orNone.
- set_gates(gates: spacr.qt.widgets.gate_spec.GateSet, frame: pandas.DataFrame | None) None[source]¶
Show a gate hierarchy, counted against a table.
BOTH ARGUMENTS TOGETHER. The counts and percentages are a property of the gates AND the rows they were applied to, so a tree given new gates against the old frame would show numbers belonging to neither.
- Parameters:
gates – the hierarchy to show.
frame – the rows to count against, or None for no counts.
- spacr.qt.widgets.gate_editor.fit_to_text(widget, *, padding: int = 16, lines: int = 1) None[source]¶
Size
widgetso its own text cannot be clipped.Measured with the widget’s REAL font metrics, so it follows the theme, the platform and the user’s DPI rather than a number that was right on one machine. Height is set as well as width: the reports are “cutt of on the sides usually its from the top asn sometimes botom”, and a control sized only horizontally clips its ascenders exactly the way described.
A minimum, never a fixed size – a layout may still give the widget more, and a widget that cannot grow is the other half of this same bug.
Nested helpers¶
- GateCanvas._apply_spin_speed.scaled(event)¶
Scale one wheel step by the spin box’s own speed setting.
spacr/qt/widgets/gate_editor.py:1144
- GateCanvas._draw_box.bound(low, high, column)¶
One axis’s limits: the gate’s own, or the column’s actual range.
Falls back to the DATA when a side is unset, so a half-open gate still draws as a box rather than running off the axis.
spacr/qt/widgets/gate_editor.py:1016
- GateCanvas._gate_from_volume_drag.side(column)¶
The stored bounds for one column, or
(None, None).spacr/qt/widgets/gate_editor.py:2304
- GateCanvas._on_scroll.zoomed(limits, anchor)¶
Scale one axis’s limits about the anchor the pointer is over.
Anchored on the POINTER rather than the centre, so the point under the cursor stays put – which is what makes a scroll feel like zooming in on something rather than the plot sliding away.
spacr/qt/widgets/gate_editor.py:2915
- GateCanvas._volume_handles.placed(values: Dict[str, float]) np.ndarray¶
A point given by column, laid out in the volume’s order.
spacr/qt/widgets/gate_editor.py:1844
- GateCanvas.volume_axis_map.invert(dx, dy)¶
Turn a screen-pixel delta back into a data delta.
Uses the INVERSE of the projection taken once outside, so a drag is measured in the units the axis is in rather than in pixels – the same drag near the origin and far from it means the same change.
spacr/qt/widgets/gate_editor.py:928
- GateCanvas.volume_axis_map.screen(point)¶
Project one data point to screen pixels.
spacr/qt/widgets/gate_editor.py:900
- GateTree._apply_threshold.value(edit)¶
One threshold field as a number, or
Nonewhen blank.spacr/qt/widgets/gate_editor.py:3497
- _ClusterSettingsDialog.__init__._setting(name)¶
One setting from the source, falling back per NAME rather than wholesale.
A source that carries some settings and not others is the ordinary case, and taking the fallback object entire would discard the ones it did carry.
spacr/qt/widgets/gate_editor.py:3581