spacr.qt.screens.graph_builder

Workflow inputs and outputs

Graph Builder

Choose variables, groups and plotting settings from the loaded table, then export the figure with its analysis context.

Open: Home → Graph Builder.

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.

Outputs

  • Figures and table exports — The output location chosen by the tool; exports describe the selected data and filters.

Before this module

  • Measure: Choose explicit variables, groups and filters.

API reference.

Module tutorial.

The Graph Builder screen — a table, a filter, and a chart you drag together.

Assembles three things that already exist into one surface:

What it is for. Exploring a measurement table without plotting code, usually after Measure or Classify: drag columns onto the chart and it redraws as each one lands.

What it needs. One table of a measurements.db or a CSV or TSV file, chosen with Load table. The object tables and png_list are offered first; every other table in the database stays available.

What it produces. A chart with six drop zones: x, y, colour, size, facet row and facet column. Only x and y decide the chart type – one continuous column gives a histogram, one categorical column a bar chart of counts, two continuous columns a scatter plot, one of each a box plot and two categorical columns a heatmap of counts – and violin and line plots are explicit choices. A brushed rectangle becomes the shared selection, highlighted in the UMAP and on the plate map.

What to do next. Press Open selection in Annotate to see the brushed objects as image crops, narrow every view with the Local Data Filter beside the chart, or move to Gate Editor when a population should become a named gate that can be saved and re-applied.

The screen goes into the app registry through spacr.qt.app.register_app() rather than through a row in the table inside app.py, and its styling goes through spacr.qt.theme.register_widget_qss(). Both seams exist so that a screen built in parallel with five others is a new file rather than a merge conflict in two thousand-line ones.

register() is not called at import — read its docstring for why, and for the one line plus four side-table entries that finish the wiring. The screen itself is complete: build it with make_graph_builder_screen(), hand it a frame, and everything below works.

Classes

GraphBuilderScreen

Drag columns onto channels; the chart follows.

Functions

make_graph_builder_screen(→ PySide6.QtWidgets.QWidget)

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

read_table(→ pandas.DataFrame)

Read a CSV or one table of a SQLite measurement database.

register(→ bool)

Put the Graph Builder in the app registry. Idempotent.

table_names(→ List[str])

Every user table in the SQLite file at path, in a useful order.

Module Contents

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

Bases: PySide6.QtWidgets.QWidget

Drag columns onto channels; the chart follows.

Parameters:
  • link – a private LinkedSelection for tests. None joins the process-wide one, which is the point of the screen in normal use.

  • parent – parent widget; ownership only.

  • threaded – False runs every table read inline instead of on the job runner’s thread. A TEST NEEDS THE RESULT ON THE LINE AFTER THE CALL; a user needs the window to keep painting while a large table loads. The jobs are the same either way – they still register, and a file that cannot be read still comes back through _on_frame_loaded() as a _Loaded carrying a problem – so only the waiting differs.

Build the screen: the graph builder beside the shared filter.

The registry key is named here rather than inherited: a screen that builds itself rather than being the generic AppScreen has none, and fold installation dispatches on exactly that – so this screen could declare folds and never be handed them.

Parameters:
  • parent – parent widget, or None.

  • link – shared selection link, passed to the builder and the filter.

  • threaded – read the database on a worker thread. Set False in tests so a load finishes before it returns.

active_jobs() → int[source]

How many worker threads are still winding down.

choose_table() → None[source]

Ask which table in the project to use.

closeEvent(event)[source]

Stop background work and unlink before going away.

Parameters:

event – the Qt close event.

is_busy() → bool[source]

True while a table read is in flight.

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

Load a CSV or one table of a SQLite measurement database.

NOTHING HERE TOUCHES THE FILE. The read has always run on a worker thread – SELECT * FROM cell into pandas measures 1.5 s for a 200 000-row measurement table on a warm local SSD – but listing the tables was kept inline on the argument that one sqlite_master query costs 0.4 ms. That argument holds only for a disk that answers. Measured on one workstation, a single stat under /nas_mnt – an autofs mount whose share was asleep – had not returned after TWENTY SECONDS, and sqlite3.connect opens the file before it can read a byte of sqlite_master. A user picking a measurements.db off a sleeping share, or dropping a project folder that resolves onto one, froze the whole window with no traceback: a stalled event loop is not a crash.

So the listing goes to the worker with the read, as one job, and the picker is populated by _on_frame_loaded() when the answer arrives. A path_probe pre-flight would not have helped: it answers optimistically from cache, and it is the connect itself that parks.

Returns as soon as the job is dispatched; _on_frame_loaded() finishes on the GUI thread, whether the file read or not – a failure comes back as data in the _Loaded rather than as an exception, so that it is dropped along with everything else when the load it belongs to has been superseded.

Parameters:

path – a .csv, .tsv or .txt file, read as delimited text, or any other file, opened read-only as a SQLite database.

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

Plot frame. The one call a host needs.

Parameters:

frame – the table to chart; handed to the graph builder and the filter panel, and its row and column counts label the source unless label is given.

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

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

spacr.qt.screens.graph_builder.read_table(path: str, table: str | None = None, limit: int | None = None) → pandas.DataFrame[source]

Read a CSV or one table of a SQLite measurement database.

Parameters:
  • path – CSV, TSV, text, or SQLite database path. Delimited files are read directly; every other suffix is opened as SQLite in read-only mode.

  • limit – optional row cap, applied in SQL. The chart’s own large-data policy handles size once the frame is in memory; this is only for the case where the file is too big to read at all.

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

Put the Graph Builder in the app registry. Idempotent.

Called at import from the bottom of spacr.qt.app — see _SELF_REGISTERING_APPS there. It is called from there rather than at the top of this module because app.py imports spacr.qt.widgets at its line 41, before register_app exists, so nothing reachable from the top of that file can register during its import; and a registration that happens later is one that some importer’s snapshot of the registry predates.

That used to be fatal as well as untidy, because SECTIONS was rebound rather than mutated, so a late registration into the previously empty Explore section was invisible to every module that had already imported the name. It is a list mutated in place now, so a late registration is seen everywhere — but registering from one deterministic point is still what keeps the app inventory the same on every import path, and the ledgers that check it honest.

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. Safe to call again: a module imported twice, or a test that re-imports it, must not raise on the duplicate key.

spacr.qt.screens.graph_builder.table_names(path: str) → List[str][source]

Every user table in the SQLite file at path, in a useful order.

Parameters:

path – path to a SQLite measurement database, opened read-only; the preferred tables (object, cell, nucleus, …) come first, then the rest alphabetically, with sqlite_ internals left out.