spacr.qt.widgets.graph_builder

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

A drag-and-drop alternative to choosing among the forty plot_* functions and their arguments. 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 exists because 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 adding hues that are hard to 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, too many to list usefully.

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 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 would receive selection broadcasts after its C++ half is gone, which crashes.

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 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:1197

GraphCanvas._xy.axis(column, levels)

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

spacr/qt/widgets/graph_builder.py:1130

_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:569

_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:622

_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:636

_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:608

_canvas_class.OwnedTimerFigureCanvas.paintEvent(self, event)

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

spacr/qt/widgets/graph_builder.py:600