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¶
The list of plottable columns, filtered by a search box, draggable out. |
|
One channel's drop target. |
|
The well, the six zones, the plot-type override and the canvas. |
|
The matplotlib canvas every built graph is drawn on. |
Functions¶
|
The fixed eight-hue series order for the active theme. |
|
The page-opacity preference as a plain float, for matplotlib. |
|
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.QWidgetThe 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.
- set_frame(frame: pandas.DataFrame | None) None[source]¶
Re-list the plottable columns for a new table.
Noneempties 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.
- class spacr.qt.widgets.graph_builder.DropZone(channel: str, parent=None)[source]¶
Bases:
PySide6.QtWidgets.QFrameOne channel’s drop target.
Emits
column_changedwith(channel, column_or_empty). The empty string rather thanNoneso the signal can be typedstr, strand 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
columnon this channel (Noneempties 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.
- class spacr.qt.widgets.graph_builder.GraphBuilderPanel(parent=None, *, link=None, source: str = 'graph_builder', fold_key: str = '')[source]¶
Bases:
PySide6.QtWidgets.QWidgetThe well, the six zones, the plot-type override and the 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 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.
- 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.QWidgetThe 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 forGateCanvas.- 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 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, orNonewhen 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_specand the redraw path own its contents.
- 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.
- set_channel(channel: str, column: str | None) None[source]¶
Rebind one channel and redraw.
- Parameters:
channel – the channel’s name, such as
xorcolour.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 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.
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
Figureto 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.
Falsefor 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