spacr.qt.widgets.graph_builder

Graph Builder — drag a column onto a channel and the chart appears.

The direct-manipulation surface that replaces “which of the forty plot_* functions do I want, and what does it expect?”. Six drop zones — x, y, colour, size, facet-row, facet-column — a well of the columns worth plotting, and a canvas that re-renders the moment a zone changes.

What is in here and what is not

Everything about what to draw lives in spacr.qt.widgets.graph_spec: the spec object, the plot-type inference, the facet grid, the shared scales and the large-data policy. This module is the chrome and the matplotlib calls. The split is not tidiness — small multiples, the gate editor, the feature explorer and the campaign control charts are all “the graph builder with one more rule”, and they need the engine without inheriting a drag-and-drop panel.

Linked, and asymmetric on purpose

GraphCanvas mixes in spacr.qt.linked_selection.LinkedView, so it is one of the views that talk to each other:

  • a brush (drag a rectangle across a panel) publishes the rows it swept as the shared selection;

  • an incoming selection rings those rows and dims the rest. It never removes a point — a selection highlights, it does not hide;

  • an incoming filter does remove rows, and the axes re-scale to what is left, because a filter genuinely narrows the population.

The brush is evaluated as a predicate over the frame, not as a hit test against drawn marks, which is what keeps it exact when a panel was drawn as a density raster or from a sample.

Colour

Categorical series take a fixed eight-hue order — never cycled, never re-assigned when a filter changes the series count, so a gene keeps its colour between two charts. The order is the validated reference palette (light and dark steps kept separately rather than flipped), and a continuous colour column gets a single-hue light-to-dark ramp. Beyond eight levels the extras fold into one “other” grey rather than inventing hues nobody can tell apart.

Classes

ColumnWell

The list of plottable columns, filtered by a search box, draggable out.

DropZone

One channel's drop target.

GraphBuilderPanel

The well, the six zones, the plot-type override and the canvas.

GraphCanvas

The matplotlib canvas every built graph is drawn on.

Functions

categorical_colours(→ Tuple[str, ...])

The fixed eight-hue series order for the active theme.

page_alpha(→ float)

The page-opacity preference as a plain float, for matplotlib.

sequential_colours(→ Tuple[str, ...])

The single-hue magnitude ramp for the active theme, light → dark.

Module Contents

class spacr.qt.widgets.graph_builder.ColumnWell(parent=None)[source]

Bases: PySide6.QtWidgets.QWidget

The list of plottable columns, filtered by a search box, draggable out.

Only the columns spacr.qt.widgets.graph_spec.plottable_columns() offers — the same rule the Local Data Filter uses to decide what is worth a control. A measurement table has hundreds of columns and listing all of them is the same as listing none.

Parameters:

parent – parent widget.

Build the well of draggable columns.

Parameters:

parent – parent widget.

columns() → Tuple[str, ...][source]

Every offered column, whatever the search box currently shows.

set_frame(frame: pandas.DataFrame | None) → None[source]

Re-list the plottable columns for a new table.

None empties the well rather than leaving the previous table’s columns on screen, which would offer drags that cannot land.

Parameters:

frame – the table to read columns from, or None.

visible_columns() → List[str][source]

The columns the search box is currently letting through.

Read off the LIST rather than refiltered, so it is what the user can actually see and drag.

Returns:

the visible column names, in list order.

class spacr.qt.widgets.graph_builder.DropZone(channel: str, parent=None)[source]

Bases: PySide6.QtWidgets.QFrame

One channel’s drop target.

Emits column_changed with (channel, column_or_empty). The empty string rather than None so the signal can be typed str, str and connected across a queued connection without a custom metatype.

Parameters:
  • channel – which channel this zone accepts. It is carried in every column_changed, so the host does not have to remember which zone it connected.

  • parent – parent widget.

Build one channel’s drop target.

Parameters:
  • channel – the channel this zone binds.

  • parent – parent widget.

dragEnterEvent(event)[source]

Light up when a droppable column arrives over the zone.

Parameters:

event – the Qt drag event.

dragLeaveEvent(event)[source]

Drop the highlight when the pointer leaves.

Parameters:

event – the Qt drag event.

dragMoveEvent(event)[source]

Keep accepting while a droppable column stays over the zone.

Parameters:

event – the Qt drag event.

dropEvent(event)[source]

Bind the dropped column to this channel.

Parameters:

event – the Qt drop event.

set_column(column: str | None) → None[source]

Put column on this channel (None empties it) and announce it.

Silent when nothing changes: the panel rebuilds the chart on every emission, and a re-drop of the same column would otherwise cost a full re-render for no visible difference.

Parameters:

column – column name for this channel, converted with str(); None or an empty string empties the zone.

property column: str | None[source]

The column bound to this channel, if any.

Returns:

the column name, or None when the zone is empty.

class spacr.qt.widgets.graph_builder.GraphBuilderPanel(parent=None, *, link=None, source: str = 'graph_builder', fold_key: str = '')[source]

Bases: PySide6.QtWidgets.QWidget

The well, the six zones, the plot-type override and the canvas.

Parameters:
  • parent – parent widget.

  • link – the LinkedSelection this view joins, so selecting here selects in every other view on it. None joins 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 well, the drop zones and the canvas.

Parameters:
  • parent – parent widget.

  • fold_key – the host module’s key. Given, the width the user drags the shelf | graph split to is remembered under "<fold_key>::graph" and a fold of the Columns shelf or the Graph under "<fold_key>/Columns" and "<fold_key>/Graph"; empty remembers nothing, which is what a test or a second host that has not chosen a key wants.

clear_channels() → None[source]

Empty every drop zone, leaving the table loaded.

closeEvent(event)[source]

Stop background work and unlink before going away.

Parameters:

event – the Qt close event.

set_frame(frame: pandas.DataFrame | None) → None[source]

Point the whole panel at a new table: the well and the canvas both.

Parameters:

frame – the table to plot, or None to clear.

set_spec(spec: spacr.qt.widgets.graph_spec.GraphSpec) → None[source]

Push a whole spec in — restoring a saved chart, or a preset.

Parameters:

spec – the complete chart specification; the canvas redraws and the drop zones are updated to match it.

zone(channel: str) → DropZone[source]

One channel’s drop zone, for a caller that needs to drive it.

Parameters:

channel – the channel’s name.

Returns:

the zone widget, or None when there is no such channel.

property spec: spacr.qt.widgets.graph_spec.GraphSpec[source]

The graph the canvas is drawing.

Returns:

the spec.

class spacr.qt.widgets.graph_builder.GraphCanvas(parent=None, *, link=None, source: str = 'graph_builder')[source]

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

The matplotlib canvas every built graph is drawn on.

A LinkedView, so a selection made here propagates to the other views sharing its model, and the base class for GateCanvas.

Parameters:
  • parent – parent widget.

  • link – the LinkedSelection this view joins, so selecting here selects in every other view on it. None joins 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 canvas and link it to the shared selection.

Parameters:

parent – parent widget.

axes_at(row: int = 0, col: int = 0)[source]

The matplotlib axes at one facet position.

Parameters:
  • row – the grid row, from 0.

  • col – the grid column, from 0.

Returns:

the axes, or None when that position was not drawn.

brush(x0: float, y0: float, x1: float, y1: float, *, row: int = 0, col: int = 0, publish: bool = True) → spacr.selection.Selection | None[source]

Select every row of one panel inside the rectangle, and publish it.

Evaluated against the panel’s unsampled rows, so a brush over a density raster or a sampled scatter still names every row in the rectangle rather than only the ones that got drawn.

Parameters:
  • x0 – horizontal start of the rectangle in the panel’s data coordinates; on a categorical axis these are tick positions, one per level, and the two ends may come in either order.

  • y0 – vertical start of the rectangle, in the same coordinates.

  • x1 – horizontal end of the rectangle.

  • y1 – vertical end of the rectangle; ignored on a histogram or bar chart, whose vertical axis is a count.

Returns:

the published Selection, or None when this table carries no object keys to name rows with.

closeEvent(event)[source]

Unlink from the shared selection before going away.

A LINKED VIEW THAT OUTLIVES ITS WINDOW is a selection broadcast to a widget whose C++ half is gone, which is a crash rather than a leak.

Parameters:

event – the Qt close event.

decorate_axes(ax) → None[source]

Called after each panel is drawn. Nothing by default.

Where a subclass puts grid lines and log scales – after the data, so it cannot change what was plotted, only how it is read.

Parameters:

ax – the Matplotlib axes of the panel that was just drawn.

figure()[source]

The matplotlib Figure this canvas draws on.

Public so a caller can EXPORT the graph without reaching into a private attribute. Do not draw on it from outside – set_spec and the redraw path own its contents.

notice() → str[source]

The line under the chart: what was drawn, and out of how much.

on_linked_filter_changed(data_filter) → None[source]

A filter genuinely narrows the population: redraw and re-scale.

Parameters:

data_filter – the linked filter that changed; it is not read here, only a debounced redraw is started.

on_linked_selection_changed(selection: spacr.selection.Selection) → None[source]

A selection only highlights — never a row fewer on screen.

Parameters:

selection – the linked selection that changed; it is not read directly, and the highlight is recomputed from the current linked selection.

panel_axes() → Dict[Tuple[int, int], object][source]

{(row, col): Axes} for every panel, empty ones included.

point_colormap()[source]

The colour map for a continuous colour axis.

A hook: the Gate Editor lets the user choose one, and the choice has to reach the drawing rather than only the settings dict.

render_now() → None[source]

Rebuild the figure from the current frame, spec, filter and selection.

selected_count() → int[source]

Rows of the drawn frame the shared selection names.

set_channel(channel: str, column: str | None) → None[source]

Rebind one channel and redraw.

Parameters:
  • channel – the channel’s name, such as x or colour.

  • column – the column to bind, or None to clear it.

set_frame(frame: pandas.DataFrame | None) → None[source]

Point the canvas at a table.

Channels naming a column the new table does not have are emptied rather than carried over: a spec that half-resolves would draw a chart of fewer variables than the zones claim.

Parameters:

frame – the table to plot, or None for no table; channels naming a column it lacks are emptied.

set_spec(spec: spacr.qt.widgets.graph_spec.GraphSpec, *, immediate: bool = True) → None[source]

Replace the spec and redraw.

Parameters:

spec – the chart specification to draw; column kinds are recomputed from it for the current table.

property grid[source]

The facet grid the last draw laid out, or None when unfaceted.

Returns:

the grid.

property kinds: Dict[str, str][source]

The loaded table’s column kinds, with the spec’s role overrides.

Public because “is this column continuous here?” is the question every caller of spec() asks next, and re-deriving it would risk two answers.

property render_data: spacr.qt.widgets.graph_spec.RenderData | None[source]

What the last draw actually plotted, or None before the first.

The rendered data rather than the source table: a large frame is sampled or binned before it is drawn, and this is what is on screen.

Returns:

the render data, or None.

property scales[source]

The axis limits the last draw used.

Returns:

the scales.

property spec: spacr.qt.widgets.graph_spec.GraphSpec[source]

The graph this canvas is drawing.

Returns:

the spec.

spacr.qt.widgets.graph_builder.categorical_colours() → Tuple[str, ...][source]

The fixed eight-hue series order for the active theme.

spacr.qt.widgets.graph_builder.page_alpha() → float[source]

The page-opacity preference as a plain float, for matplotlib.

Matplotlib takes alpha as a number, not as a QSS colour, so pane_surface() is no help to an axes patch. Degrades to the theme’s designed scrim when preferences cannot be read, which is what a first run mid-generation gets.

spacr.qt.widgets.graph_builder.sequential_colours() → Tuple[str, ...][source]

The single-hue magnitude ramp for the active theme, light → dark.

Nested helpers

GraphCanvas._draw_points.update(new_mask) → None

Redraw the points for a new selection mask.

spacr/qt/widgets/graph_builder.py:1166

GraphCanvas._xy.axis(column, levels)

One axis’s values and its level order, or empty when unset.

spacr/qt/widgets/graph_builder.py:1099

_canvas_class.OwnedTimerFigureCanvas.__init__(self, figure, *, panel: bool = True)

Wrap a figure in a canvas that owns its own redraw timer.

Parameters:
  • figure – the Matplotlib Figure to draw. Held by the canvas, which is what “owned” means here – the timer is a child of the canvas, so the figure and the redraw it schedules are destroyed together and a queued redraw cannot outlive the widget it would paint.

  • panel – draw the page surface under the figure.

False for a canvas that is already sitting ON a panel — the scree plot inside the PCA shelf, say. Two surfaces stacked read 0.49 at a requested 30 %, a shade no position of the slider can reach, so the inner one shows the outer panel through instead.

spacr/qt/widgets/graph_builder.py:540

_canvas_class.OwnedTimerFigureCanvas._spacr_draw(self)

Draw once, if a draw is still pending.

The flag is cleared FIRST so a draw that schedules another does not lose it.

spacr/qt/widgets/graph_builder.py:591

_canvas_class.OwnedTimerFigureCanvas.cancel_pending_draw(self)

Drop any queued redraw. Safe on a canvas Qt has already deleted.

spacr/qt/widgets/graph_builder.py:605

_canvas_class.OwnedTimerFigureCanvas.draw_idle(self)

Ask for a redraw on the OWNED timer rather than a static one.

Matplotlib’s Qt canvas uses static QTimer.singleShot, whose callback is not owned by the canvas and can run after Qt has deleted it. The timer here is a child of the canvas, so it dies with what it would draw.

spacr/qt/widgets/graph_builder.py:577

_canvas_class.OwnedTimerFigureCanvas.paintEvent(self, event)

Draw the page panel, then let matplotlib draw over it.

spacr/qt/widgets/graph_builder.py:569