spacr.qt.widgets.fast_plots

Provide interactive Qt regression plots backed by pyqtgraph.

Pyqtgraph keeps marks in a QGraphicsScene so Qt can composite pan, zoom, selection, and hover updates without asking Python to redraw every artist. In the recorded 1,215-point volcano benchmark, a log-axis update took 4.7 ms and full point recoloring took 45 ms, compared with about 115 ms for a Matplotlib redraw.

The application uses these widgets for on-screen interaction and retains Matplotlib for publication-oriented vector exports. Each plot accepts a pandas.DataFrame and returns a widget without importing other spaCR modules, which keeps the components independently testable and reusable.

Classes

BinnedPlot

A histogram whose bars remember which rows they were built from.

ControlSeparation

How far apart the positive and negative controls sit.

EffectDistribution

Where the screen's effects sit, and how wide the null under them is.

EffectRankPlot

Every coefficient ranked by effect, as a dot with its interval.

FastPlot

A pyqtgraph plot with the controls every plot here wants.

GroupedPlot

Base class for live plots with a categorical x-axis.

GuideAgreementPlot

Per gene: do its own guides push the same way?

InfluencePlot

Leverage against standardised residual, with Cook's distance on top.

PValueHistogram

The single most informative check that a correction means anything.

QQPlot

Observed against expected quantiles -- is the null calibrated?

ResidualPlot

Residual against fitted -- the check for a mis-specified mean.

ResultsTable

The coefficient table, sortable and searchable, wired to a plot.

ScaleLocationPlot

Plot the square root of absolute standardised residual against fitted.

VolcanoPlot

Effect against -log10(p), with the FDR carried by colour and a line.

Functions

add_style_entries(→ list)

Put EVERY field of style onto menu, grouped and editable.

add_style_file_entries(→ list)

Save, load and default a whole style -- the design.

apply_default_style(→ list)

Start style from this project's saved default. Returns the fields

apply_style_dict(→ list)

Write values into style. Returns the field names that changed.

colour_for(→ PySide6.QtGui.QColor)

Stable colour for category index.

load_style(→ list)

Read a saved style into style. Returns the fields that changed.

mark_advice(→ str)

Explain when a summary mark is poorly supported by group size.

menu_entries(→ list)

Every action a user can actually TRIGGER on menu, submenus included.

menu_groups(→ list)

The names a reader sees dividing menu into parts, in order.

menu_reading_order(→ list)

menu as a reader meets it: entry texts, "|" at every boundary.

pick_colour(parent[, initial, title])

Ask for a colour with QT'S OWN dialog. Re-exported, not implemented.

save_style(→ str)

Write style to path as JSON. Returns the path written.

style_as_dict(→ dict)

style as plain JSON-able data.

style_field_choices(style, name[, choices])

The closed set name may take, or ().

style_field_group(→ str)

Which menu group name belongs on.

style_field_kind(→ str)

Return the editor type for a figure-style field.

style_field_label(→ str)

What the entry for name reads.

style_kind(→ str)

A stable name for the KIND of style style is.

style_menu_fields(→ set)

The style FIELDS a built menu offers, by name.

Module Contents

class spacr.qt.widgets.fast_plots.BinnedPlot(*args, **kwargs)[source]

Bases: FastPlot

A histogram whose bars remember which rows they were built from.

Two panels here are histograms of a coefficient table – the p-values and the effects – and both need the three things a scatter gets for free: which rows are in which bar, a click that lands in the bar under the cursor, and an outline marking the bar a row selected elsewhere falls in. That machinery is subtle – half-open bins with the last one closed, a row-to-bar index built without a per-coefficient Python loop – and it is exactly the kind of thing that drifts when it is written twice.

A BAR IS NOT A POINT, which is the rule this whole class is shaped by. A bar holding a hundred coefficients cannot select one of them, and picking the first, the strongest or the nearest would be a guess dressed up as an answer – the same mistake as joining on a position. So a bar of many hands the whole set over for the table to narrow to, and only a bar holding exactly one row selects it like any other mark.

Build the plot and its controls.

add_bars(brush=None)[source]

Put the bars from the last _fill_bins() onto the plot.

bin_at(x) → int | None[source]

The bar under data coordinate x, or None beyond the axis.

Parameters:

x – position on the x axis in data units (not view units on a log axis); outside the outer bin edges gives None.

highlight_bin(index: int) → bool[source]

Outline one bar. The histogram’s answer to ringing a point.

Parameters:

index – zero-based bar number; out of range (or no histogram drawn yet) gives False.

keys_in_bin(index: int) → list[source]

Every identifier the bar at index was built from.

Parameters:

index – zero-based bar number; out of range gives [], and rows without an identifier are left out.

select_bin(index: int) → list[source]

Answer a click on one bar. Returns the identifiers inside it.

A BAR HOLDING A HUNDRED COEFFICIENTS CANNOT SELECT ONE OF THEM, and picking the first, the strongest or the nearest would be a guess dressed up as an answer – the same mistake as joining on a position. So the honest split: a bar that holds exactly one row selects it like any other point, and a bar that holds more says what it holds and hands the whole set over for the table to narrow to.

Parameters:

index – zero-based bar number; out of range selects nothing.

class spacr.qt.widgets.fast_plots.ControlSeparation(parent=None)[source]

Bases: GroupedPlot

How far apart the positive and negative controls sit.

This is the assay window. If the controls do not separate, nothing further down the pipeline can be trusted, and it is worth seeing before the volcano rather than after arguing about a hit list.

Parameters:

parent – parent widget.

Build the plot and its controls.

group_of(row: int) → str | None[source]

Which group the flat row row belongs to.

Parameters:

row – zero-based position in the flat sequence of all groups’ values, in the order the groups were given; None is returned for a row in no group.

group_sizes() → list[source]

How many FINITE values each control group contributed.

Not the group’s length: a NaN is a well that produced no measurement, and counting it would overstate the evidence behind the separation.

Returns:

one count per group.

redraw() → None[source]

Draw the stored groups again with the current mark.

set_groups(groups: dict, *, keys: dict | None = None)[source]

Draw the groups. Returns the number of points plotted.

Parameters:
  • groups – {'negative': array, 'positive': array, ...}

  • keys – {'negative': identifiers, ...}, one identifier per value of the SAME group, in the same order. Given them, every dot is clickable and selects the coefficient behind it.

THE GROUPS ARE THE SECOND FORM OF THE SORT TRAP. These arrays are slices of the table taken by condition, so a dot’s position within its own group is not its row – and the negative controls are drawn before the screen, so it is not its position on the plot either. Rows are therefore laid out in one flat sequence up front and carried into each scatter, rather than being inferred from the drawing order.

class spacr.qt.widgets.fast_plots.EffectDistribution(parent=None)[source]

Bases: BinnedPlot

Where the screen’s effects sit, and how wide the null under them is.

The interactive twin of spacr.figures.panels.effect_distribution(). The volcano says which coefficients are extreme; this says what “extreme” is worth on THIS screen, which is the number a reader needs before they believe any of them. A screen whose effects are a tight bell with nothing in the tails has no hits however small its p-values are.

σ IS A MAD, NOT A STANDARD DEVIATION, and that is the point of the panel rather than a detail of it: a standard deviation is inflated by exactly the outliers a screen exists to find, so a cut measured from one is pulled outwards by the hits and then fails to call them. The median absolute deviation is not, and ×1.4826 makes it the consistent estimator for a normal – the same statistic spacr.figures.panels.control_threshold() measures the effect-size cut from, so the dashed lines here and the lines on the volcano cannot disagree about where three sigmas is.

Parameters:

parent – parent widget.

Build the plot and its controls.

set_effects(values, bins: int = 50, *, keys=None, untested: int = 0)[source]

Draw the histogram. Returns the number of usable effects.

Parameters:
  • values – one fitted effect per frame row, blanks included.

  • bins – how many bars across the data’s own range.

  • keys – one identifier per element of values, in frame order. Given them, clicking a bar names the coefficients inside it.

  • untested –

    how many nuisance terms the CALLER left out, so the plot can say so. It is the caller that knows spaCR’s term grammar, which is why the drop is not done here.

    IT IS THE FAMILY, NOT THE AXIS, and the measurement says so: on the TSG101 screen σ (MAD) is 0.229228 over the tested family and 0.229036 with the intercept added, a difference of 0.08%. Dropping it does not visibly move this picture – it makes the picture be OF something, namely the 1,212 coefficients the q-values describe, which is the same family spacr.figures.panels.effect_distribution() draws and the same one the effect-size cut is measured from.

THE RANGE IS THE DATA’S OWN, unlike the p-value histogram’s. An effect size has no fixed domain, and pinning one would invent a scale the fit never produced.

class spacr.qt.widgets.fast_plots.EffectRankPlot(parent=None)[source]

Bases: FastPlot

Every coefficient ranked by effect, as a dot with its interval.

The interactive twin of spacr.figures.panels.effect_rank(), and the panel that answers what a volcano structurally cannot: HOW BIG, and how sure. A volcano ranks by significance, so an effect of 0.02 measured on six hundred wells outranks one of 2.0 measured on four; ranking by the effect itself puts them the other way round, and the interval drawn through each dot is what says which of the two to believe.

A BAR CHART OF COEFFICIENTS IS THE WRONG PICTURE, which is why this is dots and lines. A bar replaces every observation with one height and hides the uncertainty that decides whether to believe any of them – and on a ranked list, that uncertainty is the only question worth asking.

THE SAVED PANEL DRAWS THE STRONGEST FOURTEEN AND THIS ONE DRAWS THEM ALL. That is the difference a zoomable plot is FOR: a sheet has one cell and has to choose, a screen does not. The opening view is the strongest LABELLED, because that is as many names as a y-axis can carry and still be read, and “Reset view” reaches the rest – the same rule GuideAgreementPlot follows for its over-represented gene, and for the same reason: a point outside the opening view is still a point, and a point that was dropped is gone.

Parameters:

parent – parent widget.

Build the plot and its controls.

set_results(frame, *, effect: str = 'coefficient', error_column: str | None = None, significance_column: str | None = None, label_column: str = 'feature', key_column: str | None = None, alpha: float = 0.05, drop_untested: bool = True) → int[source]

Draw frame ranked by absolute effect. Return the number of dots drawn.

Parameters:
  • frame – the coefficient table.

  • effect – the fitted-effect column.

  • error_column – the standard error, so an interval can be drawn. None looks for ERROR_COLUMNS; a table carrying none gets dots and no bars, and the status line SAYS the effects are drawn without their uncertainty rather than leaving a reader to assume they are exact.

  • significance_column – what decides the colour. None looks for CORRECTED_P_COLUMNS; NO_SIGNIFICANCE says this table has none, which is a different statement from “go and look”.

  • label_column – the column a dot is named by when the frame carries no gene or guide of its own.

  • key_column – the identifier every other view joins on.

  • alpha – the cut a coefficient is coloured at.

  • drop_untested –

    leave the nuisance terms off, as the volcano and spacr.figures.panels.effect_rank() both do.

    NOT FOR THE AXIS, and that is worth writing down because it is the obvious reason and it is wrong here. The volcano drops the intercept because it OWNS the p-axis – 3.6x the tallest real hit on plate1_dv. Measured on the TSG101 screen, its COEFFICIENT is 0.190 against a tested maximum of 4.37, so by effect it ranks 547 of 1,213 and stretches nothing at all.

    It is dropped because it is not a hypothesis. Its q_value is NaN – perform_regression leaves the covariates out of the multiple-testing family – so it would sit halfway down a ranked list of hypotheses, permanently grey, with no verdict available for it and nothing on the picture saying why. A fit carrying plate row and column terms has ~25 more of them.

THE SORT IS THE TRAP, and here it is the plot’s whole shape: drawn dot n is the nth LARGEST effect and almost never row n of the table. The frame rows are therefore carried through the sort explicitly (rows=) rather than re-derived from the drawing order – see FastPlot.add_scatter(), where the same trap is written out for the Q-Q.

class spacr.qt.widgets.fast_plots.FastPlot(title: str = '', x_label: str = '', y_label: str = '', parent=None)[source]

Bases: PySide6.QtWidgets.QWidget

A pyqtgraph plot with the controls every plot here wants.

Variables:
  • point_clicked – emitted with the position of a clicked point IN THIS PLOT’S OWN FRAME. It is not an index into anyone else’s table; see key_selected for the link that survives sorting and filtering.

  • key_selected – emitted with the identifier of a clicked point.

  • keys_selected – emitted with the identifiers of EVERY row behind the thing that was clicked. A scatter point is one row and emits both; a histogram bar is a hundred rows and can only honestly emit this one.

Parameters:
  • title – heading above the plot.

  • x_label – caption under the horizontal axis.

  • y_label – caption beside the vertical axis.

  • parent – parent widget.

Constructing this without pyqtgraph installed does NOT raise: the widget lays itself out, sets plots_available to False and says why it is empty, because a missing optional plotting library should not take a screen down with it.

Build the plot, its axes and its context menu.

Parameters:
  • title – the plot’s title.

  • x_label – the x axis’s label.

  • y_label – the y axis’s label.

  • parent – parent widget.

add_beeswarm(labels, contributions, feature_values=None, *, rings: int = 0, size: float = 5.0, colormap: str = '') → int[source]

One row per feature, every observation’s contribution as a point.

The shape of a SHAP summary: for each of the top features, every sample’s contribution as a dot on that feature’s row, spread by how many share a value, and coloured by how large that sample’s own feature value was. Reading it is the point of the chart – a wide row matters more than a narrow one, and the colour split along it says which direction the feature pushes.

DRAWN HERE RATHER THAN BY THE LIBRARY. shap.summary_plot draws into a matplotlib figure it makes itself, so it cannot be handed a pyqtgraph scene; it was the last thing on this path that kept the second renderer alive. It is not much of a chart to reproduce, and reproducing it is what makes the saved file and the tab the same picture.

Parameters:
  • labels – one feature name per row, in the order they should appear.

  • contributions – each sample’s contribution for each feature, as (n_samples, n_features).

  • feature_values – the same shape, holding the feature’s own value per sample; it becomes the point colour. Without it every point is one colour and the direction of the effect is not shown.

  • rings – unused; accepted so the three chart methods share a signature.

  • size – point diameter.

  • colormap – colour scale for feature_values; BEESWARM_SCALE when empty.

Returns:

the number of points drawn.

add_curve(x, y, *, colour=None, width: float = 2.0, low=None, high=None, name: str = '') → int[source]

A line through ordered points, optionally inside a spread band.

The shape of a convergence or sweep chart: one series against an ordered x, with the spread it was summarised from drawn behind it. A line with no band claims a precision the data does not have, and it is the band that tells a reader where the curve stops meaning anything.

Parameters:
  • x (array-like) – Ordered point coordinates. Entries for which either coordinate is non-finite are omitted.

  • y (array-like) – Ordered point coordinates. Entries for which either coordinate is non-finite are omitted.

  • colour (color-like or None, default=None) – Line colour. None uses the first categorical colour.

  • width (float, default=2.0) – Line width in pixels.

  • low (array-like or None, default=None) – Lower and upper band boundaries aligned with x and y. The band is drawn only when both arrays are provided.

  • high (array-like or None, default=None) – Lower and upper band boundaries aligned with x and y. The band is drawn only when both arrays are provided.

  • name (str, default="") – Legend label. An empty string adds no label.

Returns:

int – Number of points drawn.

add_group_mark(position: float, values, kind: str = 'points', *, colour=None, rows=None, width: float = 0.6, size: float = 7.0, seed: int = 0, centre: str = 'mean', spread: str = 'sem') → int[source]

Draw one group’s observations or summary at position.

Parameters:
  • position (float) – Group position on the categorical axis.

  • values (array-like) – Observations in the group.

  • kind (str, default="points") – Mark key from MARK_TYPES.

  • colour (color-like or None, default=None) – Mark color. The first categorical color is used by default.

  • rows (array-like or None, default=None) – Source-frame row for each observation. Individual points remain clickable when these identifiers are supplied.

  • width (float, default=0.6) – Mark width in x-axis units.

  • size (float, default=7.0) – Point-marker size.

  • seed (int, default=0) – Random seed for reproducible horizontal jitter.

  • centre ({"mean", "median"}, default="mean") – Summary used by point, jitter, and line marks.

  • spread ({"sd", "sem", "var", "none"}, default="sem") – What the bar’s whisker MEANS, from spacr.figures.spread.SPREAD_CHOICES. The three are not interchangeable – SD describes the observations, SEM the confidence in their mean, and at n=3000 they differ by a factor of fifty-five – so a caller that draws one has to say which, and none draws no whisker at all.

Returns:

int – Number of finite observations represented by the mark.

add_line(*, x=None, y=None, colour: str = '#C44E52', style=Qt.DashLine, width: float = 1.5, label: str = '')[source]

A threshold line. x for vertical, y for horizontal.

add_radar(labels, values, *, colour=None, rings: int = 0, label_pad: float = 1.16) → int[source]

Draw a closed radar polygon, one spoke per label.

A RADAR IS A POLYGON, NOT AN AXIS. pyqtgraph has no polar view, and matplotlib’s subplot_kw=dict(polar=True) was the only reason two of this module’s figures could not move to the screen’s renderer. The chart itself is elementary once that is said out loud: each label takes an angle, each value a radius, and the whole thing is a line through the resulting points with the axes hidden.

THE GRID IS DRAWN, not inherited. A radar read against a square grid is unreadable – the reference a reader needs is the concentric rings, which say what a radius is worth.

Parameters:
  • labels (sequence of str) – One name per spoke, clockwise from the top.

  • values (array-like) – One radius per label. Negative values are clipped to zero: a radar has no inside-out.

  • colour (color-like or None, default=None) – Polygon colour. The first categorical colour is used by default.

  • rings (int, default=0) – Grid rings; RADAR_RINGS when 0.

  • label_pad (float, default=1.16) – Where the names sit, as a multiple of the outer ring.

Returns:

int – Number of spokes drawn.

add_ranked_bars(labels, values, *, colour=None, highlight: int = 0, thickness: float = 0.72, descending: bool = True) → int[source]

Draw a HORIZONTAL bar per label, longest at the top.

The shape a feature-importance chart is: twenty names against one number each. It is horizontal because twenty names on a vertical axis are unreadable at any font size that fits them – which is why matplotlib’s barh was reached for, and why this plot could not replace it until now.

NOT add_group_mark. That method draws at categorical positions in x, one group at a time, and every one of its eight marks assumes the measurement is y. A named orientation flag through all of them would be eight branches doubled to serve one chart; this is the one chart, drawn directly.

Parameters:
  • labels (sequence of str) – One name per bar, in the caller’s own order.

  • values (array-like) – One number per label.

  • colour (color-like or None, default=None) – Bar colour. The first categorical colour is used by default.

  • highlight (int, default=0) – How many of the leading bars carry the accent. The house rule is that everything is grey except what the sentence is about, so 0 means “no claim is being made about any particular one”.

  • thickness (float, default=0.72) – Bar thickness as a fraction of the row spacing.

  • descending (bool, default=True) – Sort by value, largest first. False keeps the caller’s order, which is what a chart of an already-ranked table wants.

Returns:

int – Number of bars drawn.

add_scatter(x, y, *, colours=None, brush_list=None, size: float = 8.0, size_list=None, labels: Sequence[str] = (), symbol: str = 'o', symbol_list=None, name: str = '', rows=None) → pyqtgraph.ScatterPlotItem[source]

Add points and wire up clicking them.

Parameters:
  • x – x coordinates in the order supplied. An entry is omitted when it or its paired y coordinate is non-finite.

  • y – y coordinates aligned one-for-one with x.

  • colours – one QColor per point, or None for a single colour.

  • symbol_list – one pyqtgraph symbol per point, or None for symbol everywhere. This provides an independent categorical encoding without constructing a combined colour-by-shape legend.

  • size_list – one diameter per point, or None for size everywhere. A plain float array – pyqtgraph takes it straight into its own arrays, so unlike a brush per point this costs nothing per point.

  • labels – per-point text, shown on hover and on click.

  • rows –

    the FRAME ROW each element of x/y came from. Default None means the arrays are already in frame order.

    THIS IS THE WHOLE OF THE Q-Q TRAP. A Q-Q plot is SORTED by p-value, so its nth drawn point is not its nth table row; a control panel is split into groups, so its nth point is not the nth row either. Left to assume otherwise, every one of those plots would carry an index that looks like a row, joins like a row, and names a different guide – silently, and in the direction nobody questions, because something did light up.

add_smoother(x, y, *, method: str = 'lowess', colour: str = '#55A868') → str[source]

Lay one diagnostic curve over the points already drawn.

Parameters:
Returns:

what to say about it – the method, its note, and the band when it reports one – or the refusal, which is a sentence the caller shows rather than an exception it swallows. Returns "" when method is empty, so “none” is not a special case at every call site.

The curve carries no p-value and cannot acquire one: it comes back as a spacr.nonparametric_fits.Curve, which has no such attribute. A smoother that bends where the straight trend line is flat is the finding – it means the mean model is missing a term – and that is a statement about the FIT, not a test of a guide.

apply_text_style() → None[source]

Put the chosen size and ink onto both axes and the title.

One place, because the size and the colour are set from two different menu entries and each has to leave the other’s choice standing – applying them separately is how “font size” quietly reverts “font colour” and the user concludes one of the two is broken.

aspect_ratio() → float | None[source]

The locked ratio of y units to x units, or None if unlocked.

auto_range_axes() → None[source]

Give both axes back to the data. The way out of a typed limit.

A control that can only be set is a trap: a user who pins x to the wrong decade has no way back to the picture they started from except reloading the run.

axis_items() → list[source]

The four axes, for the line control to reach.

THE SPINES AND THE TICK MARKS ARE LINES. They were unreachable: the axis takes foreground at CONSTRUCTION and nothing changed it afterwards, so the first report on the design – “doesnt look like there is an option to change the axis color for the volcano plot” – was exactly right, and line_items() deliberately excludes anything that is not a plot item, so no existing call could have reached them.

axis_limits() → tuple[source]

((x from, x to), (y from, y to)) as shown, IN DATA UNITS.

Data units whatever the scale, because that is the only answer that means one thing. pyqtgraph’s view range is in DRAWN units, so on a logged axis it reads (-6, 0) where the plot shows a millionth to one – and a caller pre-filling a dialog from it would offer the user a logarithm to edit.

beeswarm_frame()[source]

The last beeswarm as a long frame, or None.

build_style_menu()[source]

The right-click menu, built from what the plot actually has on it.

SEPARATE FROM SHOWING IT so the menu can be inspected without a modal event loop. QMenu.exec blocks until the user picks something and is not patchable from a test – it is a C++ slot, and assigning over it leaves the real one dispatching – so a test that reached in to read the entries hung the suite instead of failing it.

THE ORDER IS THE ORDER OF USE, not alphabetical, and it is the whole design of the design:

  • the two entries every user reaches for stay at the TOP LEVEL and one click away. A menu reorganised until nothing is one click away is worse than the flat list it replaced.

  • then what changes the CLAIM – which rows are drawn, which p-value the axis means, where the cut is, what zero is measured from. These come first because they change what the figure says.

  • then what changes the LOOK – the mark, the colour, the axes, the appearance, the size.

  • then, alone under its own heading, the one entry that re-runs the analysis. A user reaching for “Point size” must not be one slip away from starting a fit.

Groups appear only when this plot HAS the thing they hold, so a Q-Q is not offered a p-value axis it does not draw and a volcano is not offered a violin.

canvas_ratio() → float | None[source]

Height over width for the current shape, or None when free.

canvas_shape() → str[source]

Which of CANVAS_SHAPES the FIGURE is held at.

clear_column_mapping() → int[source]

Put the original colours and shapes back. Returns items restored.

The brushes and symbols each scatter was BUILT with are kept the first time a mapping touches it, because they are the only record of what the plot’s own colouring said – the compartment split, the single-guide genes, the influential wells. Recomputing them here would need this class to know every subclass’s rule, and a “restore” that guessed would quietly replace one sentence with another.

clear_highlight() → None[source]

Drop the highlight, leaving the data as it is.

clear_screen_size() → None[source]

Let the layout size the plot again, keeping its original floors.

NOT setMinimumSize(0, 0). RegressionResultsPanel gives the volcano setMinimumHeight(240) so a splitter cannot collapse it to a sliver; releasing the widget to nothing would silently drop that floor, and the plot would then vanish the first time the user dragged the divider.

clear_y_split() → None[source]

Put the y axis back in one piece. The way out of a split.

closeEvent(event) → None[source]

Retire the parentless menus that belong to this plot.

Parameters:

event – the close event; passed to the base class once the menus are retired.

colour_by_column(column: str, colormap: str = 'viridis') → int[source]

Colour every point by column through colormap. Returns n.

This maps a continuous data column to a visual channel rather than assigning one fixed point colour. The status line reports the numeric range and the number of missing values; missing values are drawn grey instead of being placed at the bottom of the scale.

Parameters:

column – a continuous (numeric) column of the plotted table.

Raises:

ValueError – for a column that is not there or not continuous, and for a colormap this build does not provide.

colour_map_reason() → str[source]

Why “colour by a column” cannot act here, or "".

The rule, applied to a menu: an entry that cannot do anything is greyed out AND SAYS WHY. Silently absent leaves the user hunting for a control they were told about; present-but-inert leaves them clicking it and concluding the application is broken.

comparison_groups() → dict | None[source]

Return labeled values for export-time statistical comparison.

Returns:

dict or None – Mapping of group labels to values. The base plot has no defined comparison; grouped subclasses override this method.

comparison_unit() → str[source]

Return the experimental unit used by exported statistics.

curve_frame()[source]

The last curve as a frame, or None. What the bundle writes.

export(path: str | None = None) → str | None[source]

Write the plot out: PDF, SVG or PNG, by the name given.

PDF and SVG use Qt’s vector painters, producing vector output rather than embedding a bitmap. PNG uses pyqtgraph’s image exporter.

The page is export_size(), which the right-click menu sets. It is independent of the widget’s on-screen size.

export_bundle(folder: str | None = None, name: str = '') → str | None[source]

Export the graph with its data, statistics, and settings.

Parameters:
  • folder (str, optional) – Parent directory for the bundle. A directory chooser opens when omitted.

  • name (str, optional) – Bundle and graph name. The plot title is used when omitted.

Returns:

str or None – Created bundle directory, or None when directory selection is cancelled.

Notes

PDF and PNG files are exported from the same plot state. The bundle also records the displayed data, statistical comparison, and relevant plot settings.

export_settings() → dict[source]

Return plot metadata written to the bundle settings file.

export_size() → tuple[source]

(width mm, height mm) of a saved page.

A height of None means “follow the plot’s own aspect” – which is what a FREE canvas does. A shape set by set_canvas_shape() is an answer to that question, so it is honoured here: this is the one place every export path reads, so it is the one place the shape has to land to reach all three of them.

export_styled(path: str, *, ink: str = '', background: str = '', grid: bool | None = None, **styling) → str | None[source]

Write the plot with the FILE’s styling, leaving the screen alone.

styling takes the same keywords styled_snapshot() does – font_size, line_width, aspect, x_title, y_title – so the preview and the file go through one styling path and cannot disagree.

Parameters:

path – destination file; its extension picks the format (PDF, SVG or PNG), as for export().

follow_the_theme() → None[source]

Put BOTH colour controls back to automatic.

The way out of a colour, and it has to exist: the design is a preference that froze because a resolved default was written back over the word “auto”, and a control a user can only set is the same freeze performed by hand.

font_colour() → str | None[source]

The ink chosen for text, or None while it follows the theme.

font_size() → int | None[source]

The point size the axes are drawn at, or None for the default.

frame()[source]

The table this plot was drawn from, or None.

The two column-mapping controls need it – “cmap (choose any column)” and “point shape (choose any column)” both name a column of THIS plot’s own table – and a plot handed bare arrays honestly has none, which is why those entries grey out rather than offering a list of nothing.

graph_spec()[source]

Return the data specification used to draw this plot, if present.

Plots that retain a specification can be redrawn using another compatible graph type. Rendering-only plots return None.

grid_shown() → bool[source]

Whether the grid is drawn behind the marks.

highlight_key(key) → bool[source]

Ring the point identified by key. Returns whether one was found.

False is a real answer, not a failure: a key can be absent because its point was not plotted (an unusable p-value) or because it is a nuisance term this plot deliberately leaves off. Saying so beats ringing something near it.

Parameters:

key – the row identifier to select, compared as str; None clears the selection and the ring.

highlight_keys(keys) → int[source]

Ring every key in keys. Returns how many were found and drawn.

A KEY THAT IS NOT ON THE PLOT IS STILL SELECTED. It can be missing because its point was not plotted – an unusable p-value, a nuisance term this plot leaves off – and dropping it from the selection would make the count on screen disagree with what the consumers receive. So the ring is what is conditional here, never the membership.

Parameters:

keys – the identifiers to select, in pick order; compared as str, duplicates dropped, and the last one is the current selection. None or empty clears the selection.

key_for_row(row: int) → str | None[source]

The identifier at frame position row, if this plot has keys.

Parameters:

row – zero-based frame position; out of range gives None.

level_note() → str[source]

The sentence naming what is drawn and what is not, or "".

line_colour() → str | None[source]

The ink chosen for lines, or None while each keeps its own.

line_colour_reason() → str[source]

Return why line-colour styling is unavailable, or an empty string.

The control applies to data lines and axes, so it is available for any rendered plot that contains either.

line_items() → list[source]

Every LINE on this plot, for a restyle to reach.

The reference lines and threshold lines added by add_line(), the Q-Q’s diagonal, the residual and scale-location trends, and the summary line across a points/jitter group – all of them, because each is a visible line with colour and width controls, and a control that reached three of five kinds would be worse than none.

The scatters are excluded because they have their own controls, and the selection ring is excluded because it is a cursor rather than data: recolouring it to match the threshold lines would make the selection invisible against them.

line_reason() → str[source]

Return why line styling is unavailable, or an empty string.

Axis spines are excluded because the control applies to plot marks and reference lines.

log_axes() → tuple[source]

(log x, log y) – whether each axis is drawn logarithmically.

log_reason(axis: str) → str[source]

Why axis cannot be drawn on a log scale, or "".

The rule, and the decision: a value at or below zero has no logarithm, and the answer is to REFUSE the axis rather than drop the points. Dropping them would make a volcano whose visible point count is a number nobody can account for.

Parameters:

axis – "x" or "y".

note_selection(key, found: bool) → None[source]

Say a row was picked – unless this plot already said MORE about it.

A click travels: the dot announces its key, the table selects that row, and every other plot then marks it. That last step arrives AFTER the clicked plot has written its own answer, and the clicked plot knows the most – which control group a dot is in and what its effect was, how many of a gene’s guides agree, what the p-value is. The plots that merely received the key know only the key, so letting them write last replaces the answer with the question.

Parameters:
  • key – the shared row identifier to name in the status line. It is compared as text with the plot’s existing detail so a richer click report for the same row is not overwritten.

  • found – whether this plot actually drew that row. False is a real answer and is said out loud: a coefficient with an unusable p-value is on no plot, a nuisance term is off the volcano on purpose, and a guide is not a point on a per-gene plot at all.

numeric_columns() → list[source]

Columns a COLOUR SCALE could read, in table order.

A cmap belongs only on a continuous quantity: mapping one onto a nominal category is the mistake the house style warns about, because it puts an order into the picture that the data does not have.

So: numeric dtype, not boolean – True/False is two categories wearing a number’s dtype – and at least two distinct finite values, because a column with one value maps every point to the same colour and a scale with no range is not a scale.

offer_baselines(options) → None[source]

Offer “measure the effects from …” on the right-click menu.

Parameters:

options – [(label, callback, checked)].

Separate from offer_refit() because the two are different kinds of thing and a user must be able to tell them apart: a baseline moves where zero is drawn on a fit that has already happened, a re-fit replaces the fit.

offer_compartments(options) → None[source]

Offer “colour by localisation” as a submenu.

Parameters:

options – [(label, callback, checked)].

offer_corrections(options) → None[source]

Offer the multiple-testing correction, ON the graph.

Parameters:

options – [(label, callback, checked)], or empty when this plot has no correction to redo.

spacr.multiple_testing.METHODS holds thirteen methods and the run picks one; comparing BH against Bonferroni against Storey on the screen in front of you is a two-second question that otherwise costs a re-run.

offer_encodings(options) → None[source]

Offer every field-acceptable way of SHOWING the adjusted p.

Parameters:

options – [(label, callback, checked)], or empty when this plot has no corrected p to encode.

The design: “id like the user to have access to visualizing adjusted P in all the ways that are acceptable to the field. showing as color, showing the descrete P on the axis buy showing the line where the adjusted p threshold lands, etc.”

BESIDE THE CORRECTION AND ABOVE THE RESTYLING, because these are statements about what the picture MEANS – which channel carries the FDR – and not about how it looks. Two of them compete for the colour channel and one composes with whatever is on it; the entries say so and the caption says which is in force.

offer_levels(options, *, note: str = '') → None[source]

Configure result-level choices on the plot and context menu.

Level selection changes which rows are displayed rather than their styling, so it receives a separate menu section and an on-plot control. The control remains visible when a run contains both gene- and guide-level fits, making the active subset explicit.

Parameters:
  • options – Sequence of (label, callback, checked) entries.

  • note – Host-supplied description of the displayed and excluded levels. It is shown beside the control and in the status line and is retained across redraws.

offer_marks(options) → None[source]

Offer “draw the groups as …” on the right-click menu.

Parameters:

options – [(label, callback, checked)], the same shape as offer_baselines() – one entry per MARK_TYPES the host can draw.

Offered by the host rather than built in, and for the same reason offer_refit() is: only the plot that owns the arrays knows whether its x-axis is a set of GROUPS at all. A volcano’s x is an effect size, and “show it as a violin” is not a question that has an answer there.

offer_p_values(options) → None[source]

Offer raw vs adjusted p-values for the y-axis.

Parameters:

options – [(label, callback, checked)], or empty when there is no correction to switch to – an entry promising “adjusted” on an uncorrected run offers a number that is not there.

offer_refit(callback, label: str = 'Re-fit with another model…')[source]

Add an action that CHANGES THE NUMBERS to the right-click menu.

Parameters:
  • callback – called with no arguments when the user picks it.

  • label – what the action says.

Everything else on that menu changes how the figure looks and nothing else. This one re-runs the regression, so it is put under its own heading rather than in the list – a user reaching for “Point size” must not be one slip away from starting a fit.

Offered by the host rather than built in, because the plot knows nothing about settings, count data or where a run writes, and should not learn: the same widget draws a simulation and a sweep trial.

offer_smoothers(on_change, *, chosen: str = 'lowess') → None[source]

Offer the four diagnostic smoothers on the right-click menu.

Parameters:
  • on_change – called (method_name) when one is picked, or with "" for none. The host redraws from it.

  • chosen – which one is currently drawn.

SEPARATE FROM offer_refit() DELIBERATELY, and the menu says so. These curves are laid over a fit that has already happened; none of them replaces it, and none of them decides a hit. They therefore belong to a different category from the inferential entries in regression_type.

offer_style(style, on_change=None, *, choices=None, use_default: bool = True) → None[source]

Put a figure’s OWN style object onto this plot’s right-click menu.

Parameters:
  • style – any dataclass describing how the figure looks – spacr.volcano_style.VolcanoStyle and whatever joins it.

  • on_change – called (name, value) when the user changes one, which is where the host redraws.

  • choices – {field: values} for fields that are a closed set.

  • use_default –

    start style from this project’s saved default for its kind, if there is one. This is where a house style actually reaches a figure – point 5 is otherwise a preference nothing reads.

    APPLIED ONCE PER STYLE OBJECT, not on every call. A host that re-offers the SAME object after the user has edited it gets nothing done to it; a host that builds a fresh one per redraw had no edits to lose. Re-asserting it unconditionally is the mistake that cost this module a day on the p-axis: the host redraws on every level, baseline and compartment change, and any one of them would silently undo a choice the user had made.

The design: the entries come from dataclasses.fields(style), so a style that gains a field gains a menu entry and the two cannot fall out of step. Offered by the host for the same reason offer_refit() is – only the host knows which style object is driving the picture, and the same widget draws a simulation and a sweep trial.

offer_thresholds(options, *, multiplier=None, on_multiplier=None) → None[source]

Offer the effect-size cut: how it is measured and how wide.

Parameters:
  • options – [(label, callback, checked)] – the modes.

  • multiplier – the current width, shown on its own entry.

  • on_multiplier – called with the new number when it is changed.

On the PLOT because the settings-panel controls for these grey out under inference='nonparametric' – correctly, since the permutation path uses no control-spread cut – and users may otherwise not being able to find them.

pinned_limits() → dict[source]

Return the axis limits the user entered for this plot.

Each value is a (low, high) pair in data coordinates. An axis that still follows automatic ranging has the value None; its current visible range is deliberately not returned, because restoring that transient range would turn automatic ranging off. The returned dictionary is a copy and can be stored or changed safely.

Returns:

{"x": limits_or_none, "y": limits_or_none}.

point_reason() → str[source]

Why the point controls cannot act here, or "".

A p-value histogram is bars. “Point size” on it is the plainest case of a control that looks live and does nothing.

radar_frame()[source]

The last radar as a frame, or None. What the bundle writes.

ranked_frame()[source]

The last ranked-bar chart as a frame, or None.

WHAT THE BUNDLE WRITES BESIDE THE PICTURE. A bar chart’s data is its labels and its numbers, and without this the folder beside the figure would hold an empty data.csv.

raster_pixels(source_width: float, source_height: float, width_mm: float | None = None, height_mm: float | None = None) → tuple[source]

Return raster-export dimensions as (width, height) pixels.

When export DPI is configured, width is derived from the page width; otherwise, source pixel width is retained. Height follows the selected canvas ratio, explicit page ratio, or source aspect ratio, in that order.

Parameters:
  • source_width – width of the source scene rectangle in pixels; values below 1 are treated as 1.

  • source_height – height of the source scene rectangle in pixels; values below 1 are treated as 1.

resizeEvent(event) → None[source]

Re-impose the canvas shape whenever the box around it changes.

Parameters:

event – the resize event; passed to the base class, and the new size is read back from the widget itself.

restyle(background: str | None = None, foreground: str | None = None) → None[source]

Re-read the figure colours, or take the ones given.

Needed because pyqtgraph resolves foreground at construction: without this a theme switch leaves every open plot drawing its old ink, and on a dark-to-light switch that ink is invisible.

save_styled()[source]

Open the shared export styling and preview dialog.

Returns:

int – Qt dialog result code: accepted or rejected.

select_in_rect(x0, y0, x1, y1, *, add: bool = False) → List[str][source]

Select every plotted point inside the data-coordinate rectangle.

Points without a plotted position cannot be selected. Rectangle selection therefore contains exactly the points visibly enclosed by the supplied bounds.

Parameters:
  • x0 – x coordinate of the first rectangle corner; it need not be the lower bound.

  • y0 – y coordinate of the first rectangle corner; it need not be the lower bound.

  • x1 – x coordinate of the opposite rectangle corner.

  • y1 – y coordinate of the opposite rectangle corner.

  • add – extend the current selection rather than replacing it, as used when a modifier key is held during selection.

Returns:

selected keys in pick order.

selected_keys() → List[str][source]

Return selected identifiers in pick order.

Linked gene, image, and cell-table views read this shared selection.

set_aspect_ratio(ratio: float | None) → None[source]

Lock one y unit to ratio x units. None unlocks it.

Parameters:

ratio – how many x units one y unit is drawn as wide. 1.0 is the square-units lock a Q-Q wants, where the 45-degree diagonal is only meaningful if the axes share a scale.

set_axis_limits(x=None, y=None) → None[source]

Pin an axis to (from, to) IN DATA UNITS. None skips it.

DATA UNITS, and converted here, because the transform is this class’s own: a user who types 1e-6, 1 on a logged axis means a millionth to one, not log10 of those, and pyqtgraph’s ranges are in the drawn units it knows nothing about the meaning of.

AUTO-RANGE IS TURNED OFF ON THE AXIS THAT IS PINNED, and only that one. pyqtgraph re-fits the view to the data on the next redraw otherwise, so a limit the user typed would survive until the first recolour and then silently spring back – which reads as the control not working rather than as a redraw. The pin is REMEMBERED as well as applied, so a scale change re-imposes the same data window rather than leaving the view where the old units put it.

A NON-POSITIVE LIMIT ON A LOGGED AXIS IS REFUSED, with the reason in the status line: it has no logarithm, and quietly substituting a bound the user did not type is how a figure comes to show a range nobody chose.

Parameters:
  • x – (from, to) for the bottom axis, or None.

  • y – (from, to) for the left axis, or None.

set_canvas_shape(name: str) → None[source]

Set the canvas shape to square, wide, tall, or free.

The shape constrains both the on-screen canvas and exported page; it does not alter the data-unit aspect ratio configured by set_aspect_ratio().

Parameters:

name – "square", "wide", "tall" or "free" (see CANVAS_SHAPES); any other name raises ValueError.

set_export_size(width_mm: float, height_mm: float | None = None) → None[source]

Set the PAGE a PDF or SVG is written onto, in millimetres.

THIS DOES NOT MOVE THE PLOT ON SCREEN. See set_screen_size().

Parameters:
  • width_mm – page width. EXPORT_WIDTH_MM is a journal’s double-column width and is the default.

  • height_mm – page height, or None to follow the plot’s own aspect so nothing is stretched.

set_font_colour(colour) → None[source]

Draw every piece of text on the plot in colour.

Separate from restyle(), which resolves the THEME’s ink. This is the user overriding it for one figure, so it is re-applied after a theme switch rather than being quietly reverted by one.

Parameters:

colour – anything QColor accepts (a name, "#rrggbb" or a QColor); None makes the text follow the theme again.

set_font_size(points: int) → None[source]

Draw the labels, TICKS and title at points.

THE TICKS ARE THE HALF THAT WAS MISSING. The old handler passed tickFont=None – which asks for pyqtgraph’s default rather than for a size – so “Font size: 20” enlarged the two axis labels and left every tick number at its original size. Measured on the volcano before this change: the bottom axis’ tick font came back as None at every setting, i.e. the control moved two strings out of about twenty.

Parameters:

points – font size in points, converted to int.

set_grid(on: bool) → None[source]

Draw the grid, or stop. Reachable from the Appearance group.

Parameters:

on – True shows the x and y grid lines, False hides them.

set_keys(keys) → int[source]

Give each frame row its identifier. Returns the number of rows.

Duplicates are kept as the FIRST row carrying the key, and counted: an identifier that names two rows cannot select one of them, and picking silently is how the wrong point gets highlighted.

None – either as the whole argument or as one entry – means “this row has no identifier”. Such a row still draws and can still be clicked; it reports no identifier to other components, which is the truthful answer and is not the same as reporting the empty string, which would collide with every other unidentified row.

Parameters:

keys – one identifier per frame row, in row order; each is stored as str, and None or NaN marks a row without one.

set_line_colour(colour) → int[source]

Colour EVERY line, axis spines and tick marks included.

None puts every line back to the colour it was drawn with and the axes back to the theme’s – the “Follow the theme” half, which a user who has set a colour needs or the freeze the API is intended to fix just happens by hand instead of by accident.

Parameters:

colour – anything QColor accepts, or None to follow the theme.

set_line_style(colour=None, width: float | None = None) → int[source]

Apply colour and width changes to plot lines.

Existing dash patterns are preserved. Colour changes also reach axis spines and tick marks, while width changes apply only to plot marks and reference lines.

Parameters:
  • colour (QColor-compatible, optional) – New colour, None to retain each line’s colour, or the internal "\theme" sentinel to restore theme colours.

  • width (float, optional) – Pen width in pixels. None retains current widths.

Returns:

int – Number of line items updated.

set_log_axes(x=None, y=None) → tuple[source]

Put an axis onto a log scale, or take it off one.

THE DATA IS TRANSFORMED, NOT ONLY THE AXIS. PlotItem.setLogMode relabels the axis and then walks its items asking each to re-transform itself – and ScatterPlotItem HAS NO setLogMode, while every point on every plot in this module is one. The axis therefore claimed a log scale and the dots did not move: a reader took p-values off a ruler that did not describe the marks beside it, with nothing on screen to say so. That is a wrong figure, not an inert control.

Parameters:
  • x – True, False, or None to leave the bottom axis alone.

  • y – the same for the left axis.

Returns:

log_axes() afterwards, because a request can be REFUSED – see log_reason() – and a caller that assumed it was honoured would be making the original mistake again.

set_screen_size(width: int, height: int) → None[source]

Set the plot’s fixed on-screen size in pixels.

This does not change export dimensions; use set_export_size() to configure the exported page.

Parameters:
  • width – fixed width in pixels, converted to int.

  • height – fixed height in pixels, converted to int.

set_status(text: str) → None[source]

What this plot has to say about ITSELF. Survives a selection.

Parameters:

text – the headline sentence; the status line is rewritten from it, the level note and the style note.

set_status_note(note: str) → None[source]

Add a sentence about the CLICKED thing, keeping the headline.

The diagnostics’ status lines carry the numbers they exist for – the inflation factor, the control medians, how many genes rest on one guide – and overwriting those with the name of whatever was just clicked trades the panel’s whole content for a string the user can already read in the table. Both fit.

Parameters:

note – the sentence about the clicked item, shown last; an empty string removes it.

set_style_note(note: str) → None[source]

What a RESTYLE has to say. Survives a redraw and a selection.

A colour scale is unreadable without its range and a shape mapping is unreadable without its key, so those sentences are not decoration – they are the legend. They cannot live in the headline, which every redraw rewrites, nor in the click note, which every click rewrites; either would leave the reader looking at a picture whose key had been overwritten by something unrelated.

Parameters:

note – the restyle’s sentence, e.g. a colour scale’s range; an empty string removes it.

set_y_split(low, high) → str[source]

Hide an empty interval on the y-axis.

Parameters:
  • low (float) – Lower boundary of the hidden interval, in data units.

  • high (float) – Upper boundary of the hidden interval, in data units.

Returns:

str – Empty string when the split is applied; otherwise, a user-facing explanation of why it was refused.

Notes

The split uses a piecewise-linear display transform while retaining the original data values in tick labels. It is refused when a plotted point lies inside the proposed interval, either boundary is non-finite, high <= low, or a logarithmic y-axis would receive a non-positive lower boundary.

Removing an empty interval can make the remaining dynamic range easier to inspect. It does not separate tied p-values or adjusted p-values; use VolcanoPlot.set_p_axis() to choose which statistic is drawn.

shape_by_column(column: str) → int[source]

Draw each value of column as its own marker. Returns n shaped.

Parameters:

column – a column of the plotted table; its values are compared as strings, and each distinct one gets its own marker.

Raises:

ValueError – for a column that is not there, or one with more values than there are shapes a reader can tell apart. Refused rather than truncated: reusing a circle for the ninth and the first value would draw two different things identically, which is worse than not offering the column at all.

shape_columns() → list[source]

Columns a POINT SHAPE could read, in table order.

Low cardinality and nothing else: two to MAX_SHAPE_VALUES distinct values. Both ends are real limits rather than tidiness – one value gives every point the same shape and says nothing, and past eight the shapes stop being tellable apart at scatter-plot size, so the reader is decoding a key instead of reading a figure.

DTYPE IS NOT THE TEST. n_guides is an integer column with four values and is exactly what a reader wants shapes for, while feature is a string column with 1,215 and is exactly what they do not. Counting the values answers both; asking the dtype answers neither.

shape_reason() → str[source]

Why “shape by a column” cannot act here, or "".

snapshot(width: int = SNAPSHOT_PX[0], *, ground=None)[source]

A picture of this plot, even on a page nobody has opened.

Parameters:

width – pixels across. The height follows from the plot’s own aspect, exactly as export() leaves it.

Returns:

a QPixmap, or None when there is nothing to show.

WHY THIS IS NOT grab(). A live plot on a stacked page the user has never raised has never been through a layout pass, so its size is whatever its parent last guessed. Measured on the real regression screen, the volcano inside the collapsed gene splitter of an unshown page: volcano.size() is 100x9 and grab() returns a 100x9 pixmap of ONE colour. That is the “blank box with a caption under it” that got the live tile deleted from the figure grid instead of fixed.

Resizing the widget first does not fix it either, and that is worth writing down because it is the obvious repair: resize is honoured by size() and ignored by grab(), because the splitter the widget sits in owns its geometry and re-imposes it. Measured, on a freshly built screen: resize(520, 380) then grab() still returns 100x9, with or without layout.activate(), setGeometry, processEvents or an explicit grab rectangle.

So this renders THE SCENE rather than the widget, through the same pyqtgraph exporter export() writes files with. The scene has no opinion about how big the widget on screen happens to be: 520x390 and 236 distinct colours from the very widget that grabs blank.

None for an empty plot is the other half. A tile showing an empty plot invites a click that opens an empty plot, and a run that has fitted nothing yet should have no tile at all rather than a misleading one.

styled_snapshot(width: int = SNAPSHOT_PX[0], *, ink: str = '', background: str = '', grid: bool | None = None, **styling)[source]

Render a preview with the styling used for file export.

Parameters:
  • width (int, default=SNAPSHOT_PX[0]) – Preview width in pixels. Height follows the plot’s aspect ratio.

  • ink (str, optional) – Temporary foreground color. An empty string keeps the current foreground.

  • background (str, optional) – Temporary background color. An empty string keeps the current background.

  • grid (bool or None, optional) – Temporary grid visibility. None keeps the current setting.

Returns:

QPixmap or None – Styled plot image, or None when the plot has no content.

Notes

The same temporary styling context is used by export_styled(). The on-screen plot is restored after rendering, including when export raises an exception.

toggle_key(key) → List[str][source]

Add key to the selection, or remove it if it is already in.

The modifier-click half of the platform gesture.

Parameters:

key – the row identifier, compared as str; None leaves the selection unchanged.

y_split() → tuple | None[source]

(low, high) of the hidden band IN DATA UNITS, or None.

y_split_reason(low, high) → str[source]

Why (low, high) cannot be hidden on the y axis, or "".

A SPLIT THAT SWALLOWS POINTS IS REFUSED, and this is the one place that rule is stated. A broken axis is a piecewise-linear ruler whose tick labels still read the data’s own values, so every mark stays at its own number – but a mark INSIDE the hidden band has no number left on the ruler to sit at, and drawing it in the break would put a point somewhere its value is not. That is the same thing y-axis jitter does, and the design forbids it for the same reason.

So the answer is the count, and the user moves the band.

Parameters:
  • low – bottom of the band to hide, in y data units.

  • high – top of the band to hide, in y data units; it must be above low, and both must be finite (and positive on a log axis).

class spacr.qt.widgets.fast_plots.GroupedPlot(*args, **kwargs)[source]

Bases: FastPlot

Base class for live plots with a categorical x-axis.

The right-click menu exposes every mark in MARK_TYPES. Subclasses retain their source groups so switching marks redraws the same observations and uses mark_advice() to explain unsuitable small-sample summaries.

mark_changed[source]

Emitted with the new mark key after a successful change.

Type:

PySide6.QtCore.Signal

Build the plot and its controls.
group_sizes() → list[source]

Observations per group, for mark_advice().

mark() → str[source]

Which mark the groups are currently drawn with.

mark_note() → str[source]

The sentence about the CURRENT mark, or "".

Three things now. A start that could not honour the DEFAULT GRAPH TYPE setting says so first – a user who asked for violins and got a jitter is owed the reason on the plot rather than in a log. Then the two a user who picks “bar” has done at once: they have chosen a mark that may misrepresent the spread, and they have given up the ability to click a guide – one rectangle stands for forty-one rows and cannot honestly select one of them.

abstract redraw() → None[source]

Draw the last data handed in, with whatever the mark now is.

Subclasses re-run their own set_* from what they stored. Nothing is recovered from the picture – the arrays are kept – so switching marks cannot show a different set of observations than the mark before it showed.

set_mark(kind: str) → bool[source]

Draw the groups as kind. Returns True if the mark changed.

Parameters:

kind – a mark key from MARK_TYPES, e.g. "points", "bar", "box" or "violin"; it becomes the user’s choice, so later redraws keep it.

Raises:

ValueError – on a mark this module cannot draw. Loudly, because the only callers are this class’s own menu and a test – a silent fallback would make a typo look like a working option.

classmethod starting_mark(counts=()) → tuple[source]

The mark to draw first, and why it is not the one chosen.

Parameters:

counts – observations per group, when they are known. An empty one asks only “what was chosen”, which is all that can be answered before the data arrives.

Returns:

(mark, note). The note is empty whenever the mark drawn is the mark asked for.

THE SETTING DECIDES, NOT THE WIDGET. A saved DEFAULT GRAPH TYPE for this plot’s DATA_SHAPE is what gets drawn before the first right-click; with nothing saved the plot keeps DEFAULT_MARK, because a preference nobody expressed must not move existing views.

A BROKEN PREFERENCE STORE DRAWS THE PLOT ANYWAY. This is the opening frame of a panel; refusing to draw it over a settings read would cost the whole figure.

class spacr.qt.widgets.fast_plots.GuideAgreementPlot(parent=None)[source]

Bases: GroupedPlot

Per gene: do its own guides push the same way?

The interactive twin of spacr.figures.panels.guide_agreement(), and the one thing a volcano structurally cannot show. A gene called by one guide out of six and a gene whose six guides agree are the same dot on a volcano, ranked by the same number, and only one of them is corroborated evidence.

Measured on the TSG101 screen: 389 genes, of which 102 rest on a single surviving guide – including 244480, whose gene-level p of 2.9e-13 ranks it above everything else in the screen and IS that one guide’s p-value.

THE HOUSE RULE DECIDES THE COLOURING. Everything is grey except what the sentence is about, and the sentence here is “these ones rest on a single guide”, so those are the only points that get colour.

Parameters:

parent – parent widget.

Build the plot and its controls.

group_sizes() → list[source]

Genes per distinct guide count – the groups a box would draw.

redraw() → None[source]

Draw the stored support table again with the current mark.

set_support(support, *, keys=None, key_column: str = 'feature')[source]

Draw one point per gene. Returns the number of genes plotted.

Parameters:
  • support – the frame spacr.guide_concordance.guide_support() returns – n_guides, concordance, single_guide per gene – indexed by gene or carrying a gene column.

  • keys – one identifier per row of support. Default is support[key_column] when that column is there.

THE KEY IS THE GENE-LEVEL TERM, NOT THE GENE ID. A gene appears in the coefficient table as gene_fraction:gene[244480], and that is what the volcano, the table and the gene tile all join on. Handing this plot the bare 244480 would make a second key space that nothing else can resolve, so the caller passes the term and clicking a gene here selects exactly the row clicking its dot on the volcano would.

class spacr.qt.widgets.fast_plots.InfluencePlot(parent=None)[source]

Bases: FastPlot

Leverage against standardised residual, with Cook’s distance on top.

The interactive twin of spacr.regression_qc._panel_influence(). The question it answers is the one a screen cannot answer from the volcano: is a hit the shape of the data, or the shape of ONE WELL? A well far to the right has an unusual combination of guides; a well far up or down is poorly predicted; a well that is both is one whose removal moves the coefficients, and Cook’s distance is the product that says so.

The wells past the 4/n screening rule are the only ones coloured, which is the house rule – everything else is grey, because the sentence here is “these ones are worth going back to the microscope for”.

Parameters:

parent – parent widget.

Build the plot and its controls.

set_influence(leverage, std_resid, cooks, labels: Sequence[str] = (), n_params: int = 0, reason: str = '')[source]

Draw it. Returns the number of wells plotted.

Every array comes from spacr.regression_qc – ctx.leverage, ctx.std_resid and spacr.regression_qc.cooks_distance() – rather than being recomputed here, so the live panel and the saved report cannot name different wells as influential.

Parameters:
  • leverage – hat value per well, the x coordinate; aligned with the other two arrays.

  • std_resid – standardised residual per well, the y coordinate; wells where it or the leverage is not finite are not drawn.

  • cooks – Cook’s distance per well; wells above 4 / n (n the number plotted) are drawn larger in the influential colour.

class spacr.qt.widgets.fast_plots.PValueHistogram(parent=None)[source]

Bases: BinnedPlot

The single most informative check that a correction means anything.

Under the null, p-values are uniform. A histogram that is flat with a spike at zero is a screen with real hits in it; one that slopes, or piles up near one, says the model is misspecified and every q-value downstream of it is decoration.

Parameters:

parent – parent widget.

Build the plot and its controls.

set_p_values(values, bins: int = 50, *, keys=None)[source]

Draw the histogram. Returns the number of usable p-values.

Parameters:
  • values – one p-value per frame row, blanks included.

  • bins – how many bars across [0, 1].

  • keys – one identifier per element of values, in frame order. Given them, clicking a bar names the coefficients inside it.

class spacr.qt.widgets.fast_plots.QQPlot(parent=None)[source]

Bases: FastPlot

Observed against expected quantiles – is the null calibrated?

Points on the diagonal mean the test is behaving. A curve that lifts off it early means inflation: the design is confounded, and the hits at the top of the volcano are partly an artefact of that rather than biology.

Parameters:

parent – parent widget.

Build the plot and its controls.

set_p_values(values, *, keys=None)[source]

Draw the Q-Q. Returns the number of usable tests.

Parameters:
  • values – one p-value per frame row, including missing entries; only finite positive values are ranked and drawn.

  • keys – one identifier per element of values, IN THE ORDER THEY WERE HANDED IN – i.e. in frame order, including the ones with no usable p-value. Given them, every point is clickable and selects the coefficient it was computed from.

THE SORT IS THE TRAP. A Q-Q is ranked by p, so the nth drawn point is the nth SMALLEST p-value and almost never the nth row of the table. The rows are therefore carried through the sort explicitly (rows=) rather than re-derived from the drawing order, which is the mistake that lights up the wrong guide and looks entirely correct doing it.

class spacr.qt.widgets.fast_plots.ResidualPlot(parent=None)[source]

Bases: FastPlot

Residual against fitted – the check for a mis-specified mean.

A horizontal band is what a well-specified model gives. A funnel means the variance grows with the fit and the standard errors are wrong, which is a p-value problem rather than a cosmetic one.

Parameters:

parent – parent widget.

Build the plot and its controls.

set_residuals(fitted, residuals, labels: Sequence[str] = ())[source]

Show a fit’s residuals against its fitted values.

THE PLOT THAT SHOWS A BAD MODEL. A residual cloud with structure in it means the fit is missing something, and that is visible here in a way no single summary number reports.

Parameters:
  • fitted – the fitted values.

  • residuals – the residual for each.

  • labels – optional per-point labels.

class spacr.qt.widgets.fast_plots.ResultsTable(parent=None)[source]

Bases: PySide6.QtWidgets.QWidget

The coefficient table, sortable and searchable, wired to a plot.

Use the table to inspect exact values that cannot be read reliably from a scatter plot. Selecting a row can synchronize a linked plot.

Variables:
  • row_selected – emitted with the frame row index of the selected row.

  • key_selected – emitted with the selected row’s identifier. This is the one to connect a plot to: an index only means anything to the frame it came from, and the table’s frame and the plot’s frame are not required to be the same one.

Parameters:

parent – parent widget.

Build the table and its filter row.

configure(*, placeholder: str | None = None, significance_filter: bool | None = None) → None[source]

Adapt the controls to what the table actually holds.

This widget is reused for tables that are not coefficient tables – the sweep’s runs, for one – and a filter offering “significant only” over a list of trials is a control that cannot do anything, sitting next to a placeholder telling the user to type a gene into it.

copy_visible() → str[source]

Put the visible rows on the clipboard as TSV, and return them.

key_for_row(index: int) → str | None[source]

The identifier at frame position index, or None.

Parameters:

index – zero-based position in the frame (not the sorted table); its key-column value is returned as str.

select_frame_row(index: int) → bool[source]

Scroll to and select the row for frame position index.

This is the other half of clicking a point on the volcano: the dot and the numbers behind it should be two views of one thing.

Parameters:

index – zero-based position in the frame; the table row is found wherever sorting has put it.

select_key(key) → bool[source]

Select the row whose identifier is key. The safe direction.

A plot has no business knowing where a row sits in this table – the user sorts it, filters it, and after the redesign it may not even be drawn from the same frame. It knows the key, and the key is enough.

A hidden row is unhidden to select it: silently doing nothing because the filter box excludes the point the user just clicked reads as a broken click.

Parameters:

key – the identifier, compared as str with the text of the key column’s cells.

select_keys(keys) → int[source]

Select table rows whose keys occur in keys.

Both selection signals are emitted here so every consumer receives the same ordered selection. Rows remain visible; show_keys() performs filtering when a plot interaction requires it.

Parameters:

keys – row keys to select, in the desired selection order.

Returns:

number of matching rows selected.

selected_keys() → list[source]

Return selected row identifiers in visual order.

The method reads the live widget selection so Ctrl-click changes are represented immediately.

set_frame(frame, *, alpha: float = 0.05, significance_column: str | None = None, key_column: str | None = None) → int[source]

Fill the table. Returns the row count.

Parameters:

frame – the results table, one table row per frame row and one column per frame column; None or an empty frame shows “Nothing to show.”

show_keys(keys) → int[source]

Narrow the table to a set of identifiers. None clears it.

The other end of FastPlot.keys_selected: a histogram bar is a hundred coefficients and cannot select one of them, but “show me the hundred” is a question the table can answer exactly. Returns how many rows are visible afterwards.

Parameters:

keys – identifiers to keep visible, compared as str against the key column; None removes the restriction.

class spacr.qt.widgets.fast_plots.ScaleLocationPlot(parent=None)[source]

Bases: FastPlot

Plot the square root of absolute standardised residual against fitted.

The interactive twin of spacr.regression_qc._panel_scale_location(), used as the variance-homogeneity panel. A residual-vs-fitted plot shows the mean and the variance at once and a reader has to separate them by eye; taking the square root of the absolute standardised residual removes the sign, so what is left is only the spread. A rising trend means the standard errors – and therefore every p-value on the volcano – are wrong in a direction that depends on the fitted value.

Drawn on the STANDARDISED residual, so it is empty for a model class that has no error scale (quantile regression, a hinge classifier). That is a real answer and is said out loud rather than drawn from y - fitted and labelled as though it were the same quantity.

Parameters:

parent – parent widget.

Build the plot and its controls.

set_scale_location(fitted, std_resid, labels: Sequence[str] = (), reason: str = '')[source]

Draw it. Returns the number of wells plotted.

Parameters:
  • fitted – one fitted response per well, aligned with std_resid and labels; pairs containing a non-finite value are not drawn.

  • std_resid – RegressionQCContext.std_resid. All-NaN when the model class has no error scale – see spacr.regression_qc.resolve_residual_standardisation().

  • reason – what to say when there is no standardised residual; pass ctx.standardisation.reason.

class spacr.qt.widgets.fast_plots.VolcanoPlot(parent=None)[source]

Bases: FastPlot

Effect against -log10(p), with the FDR carried by colour and a line.

The y axis is the raw P value and is continuous. The reasoning is worth keeping beside the code because the observation that produced it looked like a bug and was not.

Benjamini-Hochberg’s adjusted P is a cumulative minimum taken from the largest P downwards – q_(i) = min over j >= i of (n * p_(j) / j) – and the min is both what enforces monotonicity and what creates ties: the moment a later rank produces a smaller value, EVERY earlier rank is pulled down onto it. Measured on reference runs: 823 q values with 31 distinct, 19 tied levels covering 811 coefficients, GRA14 and 225160 both at 4.1150e-03. A volcano drawn against that has a staircase for a y-axis, and no transform can separate two tests that hold the same number.

So the height is the RAW P, which is continuous and is the evidence per test, and the correction decides the COLOUR and the LINE, which is the discrete thing it actually is. The same two numbers are on the plot, each doing the job it can do, and nothing is invented – which is also why the one thing this plot will not do is jitter the y axis to separate ties. That moves a point away from its own value, and this class exists because a plot was showing something the data did not say.

Parameters:

parent – parent widget.

Build the plot and its controls.

caption() → str[source]

The sentence naming which quantity is the height and which the call.

correction() → str[source]

The correction being DRAWN, canonical, whoever chose it.

families() → dict[source]

{family: (critical raw p or None, called, tested)} as drawn.

local_fdr_values()[source]

The local FDR per row, computed once and per FAMILY.

LAZY ON PURPOSE. The beta-uniform fit is 25 ms on the real screen’s 1,215 coefficients – more than drawing the whole plot – and the default axis does not use it. Computing it on every redraw would put the lag back that this module’s whole first half exists to remove.

name_the_effect(path: str) → str[source]

Title the horizontal axis for the analysis that produced it.

Parameters:

path – 'fitted' or 'permutation'; anything else is treated as fitted, because a run whose path cannot be read is far more likely to be an ordinary fit than a permutation.

Returns:

the label applied.

p_axis() → str[source]

Which of P_AXES the y axis measures.

q_colour() → str[source]

Which of Q_COLOURS has the colour channel.

q_mark() → str[source]

Which of Q_MARKS is composed on top of the colour.

redraw() → int[source]

Draw the last table again with the current axis and correction.

run_correction() → str[source]

The correction the RUN used, or "" if the table does not say.

set_correction(method) → None[source]

Recompute the correction on the spot. None goes back to the run’s.

Parameters:

method – a multiple-testing method name or alias accepted by spacr.multiple_testing.canonical_method(), or None; an unknown name raises ValueError. Choosing "none" moves an adjusted p axis back to raw.

set_p_axis(kind: str) → None[source]

Draw the height against the raw P, the adjusted P, or the lfdr.

"adjusted" IS KEPT AND IS NOT THE DEFAULT. It is honest and stepped, because that is what BH is, and a user reproducing a published figure drawn that way needs to be able to.

Parameters:

kind – "raw", "adjusted" or "lfdr" (see P_AXES); anything else raises ValueError, and "adjusted" falls back to "raw" while no correction is in force.

set_q_colour(mode: str) → None[source]

Carry the FDR as a binary call, or as a continuous ramp over q.

THEY CANNOT BOTH BE ON, and not because this refuses: a dot has one colour. The design says so and says what follows from it – whichever is chosen, the other is not shown, and the LEGEND says which is in force. That is the localisation rule again: one sentence per figure.

THE RAMP DOES NOT REPLACE THE CALL AS THE DEFAULT. F.1 is the field’s own volcano – continuous height, binary colour, and the colour carrying the claim – and a ramp answers a different question: it shows how far each test is from the threshold, which is a reading aid and not a call. It is an OFFER.

Parameters:

mode – "call" (binary significance colour) or "ramp" (continuous over q); anything else raises ValueError.

set_q_mark(mode: str) → None[source]

Put the FDR into the SIZE or the OPACITY of the mark, or neither.

THE ONE ENCODING THAT COMPOSES. Size and opacity are channels the colour is not using, so this can be on at the same time as either colouring – a volcano coloured by condition with the marks sized by q says both things at once, which is what the colour ramp cannot do.

Both are offers, not defaults. Neither is on until it is asked for.

Parameters:

mode – "none", "size" or "opacity"; anything else raises ValueError.

set_results(frame, *, effect: str = 'coefficient', p_column: str = 'p_value', label_column: str = 'feature', category_column: str | None = None, symbol_column: str | None = None, opacity_column: str | None = None, alpha: float = 0.05, effect_threshold: float | None = None, key_column: str | None = None, drop_untested: bool = True, compartment: str | None = None, q_column: str | None = None, run_method: str | None = None)[source]

Draw frame. Returns the number of points actually plotted.

Parameters:
  • frame – the coefficient table. Rows without a finite effect and raw p-value are left off the plot, while their original positions remain the identifiers used for linked selections.

  • symbol_column – optional categorical column encoded by marker shape, independently of colour.

  • opacity_column – optional categorical column encoded by opacity. Encodings are applied in the stable order colour, shape, opacity. The caller rejects requests for additional visual channels.

  • p_column – the RAW P value. Not the corrected one – the height is the raw P by default and the correction is recomputed here.

  • q_column – the run’s own corrected column, for the plot to check itself against. Found in the table when omitted.

  • run_method – which correction the run used. Read off the table’s multiple_testing_method column when omitted.

  • compartment – one TAGM/LOPIT compartment to pick out against grey. ONE, not all 27 – see spacr.localisation. It REPLACES any category colouring rather than combining with it: a volcano where a coloured dot might be coloured for its condition or for its compartment has no sentence.

spacr.qt.widgets.fast_plots.add_style_entries(menu, style, on_change=None, *, choices=None, labels=None) → list[source]

Put EVERY field of style onto menu, grouped and editable.

The point 3: the menu is built FROM THE STYLE OBJECT’S FIELDS rather than from a hand-written list per figure, so “as many settings as possible, depending on the graph” is automatic – a style gains a field, the menu gains an entry, and the two cannot fall out of step. The acceptance test compares what this produces against dataclasses.fields(style), which is only a meaningful check because NOTHING IS SKIPPED: a field this cannot edit is still listed, greyed, and says why, exactly as the design requires of every other control here.

Parameters:
  • menu – a QMenu to add groups to.

  • style – any dataclass instance describing how a figure looks.

  • on_change – called (name, value) when the user changes one. Without it the entries are built and inert, which is what a test reading the menu wants and what a caller with nothing to redraw gets.

  • choices – {field: values} for fields that are a closed set, overriding the field’s own metadata and the style class’s CHOICES.

  • labels – {field: {value: what to call it}}, for a closed set whose stored values are not what a reader should be shown – a marker is stored as "o" and read as “Circle”. Anything unnamed shows its stored value, so a partial map is fine.

Returns:

the QAction objects added, in menu order.

The groups match build_style_menu() and use the same order – Data, Axes, Appearance, Size – so a figure’s own settings and the plot’s read as one menu rather than two conventions side by side.

menu MUST OUTLIVE THE CALL. The groups are parented to it, but a QMenu() built with no parent of its own is Python-owned and takes every action here with it when the local holding it goes out of scope. FastPlot.build_style_menu() parents its menu to the widget, which is why the application does not meet this.

spacr.qt.widgets.fast_plots.add_style_file_entries(menu, style, on_change=None, *, parent=None, note=None, ask_path=None) → list[source]

Save, load and default a whole style – the design.

Parameters:
  • menu – the “Figure style” group to add to.

  • style – the style dataclass the figure is drawn from.

  • on_change – called (None, style) when a load or a default changes it, i.e. where the host redraws.

  • parent – the widget file dialogs are parented to.

  • note – called with one sentence saying what happened, for the plot’s status line. A save that says nothing is a save the user repeats because they cannot tell whether it worked.

  • ask_path – (mode, suggested) -> path for a test to answer instead of a modal. None uses QFileDialog.

Returns:

the actions added, in menu order.

THE DEFAULT IS PER STYLE KIND, not per figure. That is what makes it a house style: every volcano this project draws from now on starts from it, which is the difference between saving a style and re-picking one.

spacr.qt.widgets.fast_plots.apply_default_style(style, on_change=None) → list[source]

Start style from this project’s saved default. Returns the fields it changed, or [] when there is no default for this kind.

Called by the HOST before it draws, not by the menu: only the host knows when a figure is new, and applying a default to a style the user has already edited would undo their edits at the next redraw – the same mistake as a host that re-asserts an axis choice, which cost this module a day.

Parameters:

style – the style dataclass instance to update in place; the default saved in preferences for its style_kind() is applied.

spacr.qt.widgets.fast_plots.apply_style_dict(style, values, on_change=None) → list[source]

Write values into style. Returns the field names that changed.

FORWARDS-COMPATIBLE LOADING, which is the half a bare **values would lose: a style file written by a later spaCR carries fields this one has never heard of, and a file written by an earlier one is missing some. An unknown field is skipped rather than raising, and a missing one keeps whatever the style already had – so a house style saved today is still loadable after the dataclass grows.

on_change is called ONCE, with (None, style), rather than per field: a host that redraws per field would redraw sixty times for one load, and the interesting question for the host is “the whole style changed”, not “line_width did”.

Parameters:
  • style – the style dataclass instance, modified in place.

  • values – mapping of field name to value; None applies nothing, and a list is turned into a tuple where the field currently holds a tuple.

spacr.qt.widgets.fast_plots.colour_for(index: int, alpha: int = 255) → PySide6.QtGui.QColor[source]

Stable colour for category index.

Parameters:

index – category number; wraps around the length of PALETTE.

spacr.qt.widgets.fast_plots.load_style(style, path, on_change=None) → list[source]

Read a saved style into style. Returns the fields that changed.

Parameters:
  • style – the style dataclass instance to update in place.

  • path – a JSON style file written by save_style().

Raises:

ValueError – for a file that is not a style, and for a style of the WRONG KIND. Refused rather than partially applied: a volcano’s style loaded into a heatmap would set the four fields whose names happen to match and leave the rest, which looks like a corrupted figure rather than like a mistake.

spacr.qt.widgets.fast_plots.mark_advice(kind: str, counts) → str[source]

Explain when a summary mark is poorly supported by group size.

Parameters:
  • kind (str) – Mark key from MARK_TYPES.

  • counts (iterable of int) – Number of observations in each group.

Returns:

str – Decision-ready warning, or an empty string when no warning is needed.

spacr.qt.widgets.fast_plots.menu_entries(menu) → list[source]

Every action a user can actually TRIGGER on menu, submenus included.

THE MIGRATION THIS EXISTS FOR. QMenu.actions() returns a SUBMENU’S OWN action and not what is inside it, so sixty-odd assertions written against a flat menu read every grouped entry as a removed feature – which is what reverted the first attempt at the design. Asserting REACHABILITY instead of depth makes those assertions true of the flat menu and of the grouped one alike, so the restructure stops being a rewrite of the suite.

Two things are skipped because a user cannot do them:

  • separators, which are not entries at all;

  • addSection labels, which Qt implements as a separator that HAS text – so isSeparator() catches both, and a naive text filter would count a heading as a feature.

Parameters:

menu – a QMenu.

Returns:

the QAction objects, in the order a reader meets them, with each submenu’s contents spliced in where the submenu sits.

spacr.qt.widgets.fast_plots.menu_groups(menu) → list[source]

The names a reader sees dividing menu into parts, in order.

An addSection heading and a submenu title are the same idea to a reader and two different objects to Qt – which is exactly why a test that names one cannot be written against the other, and why moving a heading into a submenu title read as a deleted feature the first time the design was attempted.

Parameters:

menu – a QMenu.

spacr.qt.widgets.fast_plots.menu_reading_order(menu) → list[source]

menu as a reader meets it: entry texts, "|" at every boundary.

A separator, a section heading and the edge of a submenu are all one thing to the person reading the menu – a break – and are three different things to Qt. This flattens them into the same mark, so “these two entries are kept apart” is a claim that survives being reorganised into submenus.

Parameters:

menu – a QMenu.

spacr.qt.widgets.fast_plots.pick_colour(parent, initial=None, title: str = 'Colour')[source]

Ask for a colour with QT’S OWN dialog. Re-exported, not implemented.

The implementation is spacr.qt.widgets.colour_picker.pick_colour(), and this module goes through it rather than keeping a second copy. QColorDialog.getColor defaults to the PLATFORM’s chooser, and on a GNOME session that request is brokered through xdg-desktop-portal – the tens-of-seconds stall behind slow native colour dialogs. One helper, because an option that has to be remembered at each call site is one that gets forgotten at the seventh – and there WERE seven here: the six the design counted plus _ask_style_value(), which its count missed.

Imported inside the function because this module is imported at GUI start on installs that may not have every sibling widget module, and a colour picker is not worth an import-time dependency at the top of the file.

Parameters:

parent – the dialog’s parent widget, or None.

Returns:

a QColor. Check isValid() – an invalid one is the user cancelling, which is an answer and not a failure.

spacr.qt.widgets.fast_plots.save_style(style, path) → str[source]

Write style to path as JSON. Returns the path written.

The serialisation already existed (VolcanoStyle.from_dict / asdict); what did not was any way for a user to reach it, which made a restyle something they redid every time they needed the picture.

Parameters:
  • style – the style dataclass instance to save, together with its style_kind().

  • path – destination file; .json is appended when it has no suffix, and an existing file is overwritten.

spacr.qt.widgets.fast_plots.style_as_dict(style) → dict[source]

style as plain JSON-able data.

dataclasses.asdict recurses and turns tuples into lists, which is what JSON does anyway – so a pair field round-trips as a list and apply_style_dict() puts it back as a tuple rather than leaving two representations of one value in the store.

Parameters:

style – the style dataclass instance to convert with dataclasses.asdict.

spacr.qt.widgets.fast_plots.style_field_choices(style, name: str, choices=None)[source]

The closed set name may take, or ().

Looked for in three places, most specific first: the argument, the field’s own metadata["choices"], and a CHOICES mapping on the style’s class. Three, because the styles in this package declare their sets in all three ways and a mechanism that read only one would silently turn a closed set into a free-text box.

Parameters:
  • style – the style dataclass instance whose field metadata and class CHOICES are consulted.

  • name – the field’s name.

spacr.qt.widgets.fast_plots.style_field_group(name: str) → str[source]

Which menu group name belongs on.

Parameters:

name – a style field name; the first group (Data, Axes, Size) with a fragment found in it, case-insensitively, wins, and anything else is "Appearance".

spacr.qt.widgets.fast_plots.style_field_kind(name: str, value, choices=None, declared: str = '') → str[source]

Return the editor type for a figure-style field.

Parameters:
  • name (str) – Field name. Colour-related suffixes select the colour editor.

  • value (Any) – Current value. Its runtime type takes precedence when it is not None.

  • choices (collection, optional) – Allowed values for a closed selection.

  • declared (str, optional) – Type annotation used when value is None.

Returns:

str – One of STYLE_FIELD_KINDS.

spacr.qt.widgets.fast_plots.style_field_label(name: str, value, kind: str) → str[source]

What the entry for name reads.

The CURRENT VALUE is in the label for everything but a flag, which shows its state as a tick. A menu of settings that does not say what they are set to is one the user has to open each entry to read.

Parameters:
  • name – the field name; underscores become spaces and the first letter is capitalised.

  • value – the field’s current value, shown after the name; None reads as “automatic”.

  • kind – the editor kind from style_field_kind(); "flag" and "unsupported" show the name alone, "multi" lists the chosen items and "pair" and "number" format with :g.

spacr.qt.widgets.fast_plots.style_kind(style) → str[source]

A stable name for the KIND of style style is.

VolcanoStyle -> "volcano". It is what a saved default is keyed on, so it has to be derived from the class rather than passed in: a caller that had to name its own kind would eventually name two of them the same and one lab’s house style would land on another figure type.

Parameters:

style – a style instance; its class name, less a Style suffix, is converted to snake case ("figure" if nothing is left).

spacr.qt.widgets.fast_plots.style_menu_fields(menu) → set[source]

The style FIELDS a built menu offers, by name.

What a reader meets is prose – “Marker size: 26…” – so the field’s own name travels on its action as the object name. That is what lets “the right-click menu and the side panel offer the same settings” be asserted as a comparison of two sets rather than by reading two lists of words and hoping they mean the same thing.

Parameters:

menu – a QMenu that add_style_entries() has been called on.

Nested helpers

FastPlot._install_axis_hooks._add(item, *args, **kwargs)

Add the item AND record it, so the canvas can clear its own drawings.

spacr/qt/widgets/fast_plots.py:1950

FastPlot._install_axis_hooks._label(axis, text=None, units=None, unitPrefix=None, **kwargs)

Set an axis label and remember it for the style round-trip.

spacr/qt/widgets/fast_plots.py:1955

FastPlot._install_rubber_band.drag(event, axis=None)

Interpret a drag: rubber-band select, or pan with a modifier.

spacr/qt/widgets/fast_plots.py:4539

FastPlot._install_split_ticks.tick_strings(values, scale, spacing)

Tick labels for a split axis, in the ORIGINAL data’s values.

The axis is drawn on a compressed coordinate, so labelling it with the drawn position would put numbers on it that the data never had.

spacr/qt/widgets/fast_plots.py:2358

FastPlot._install_split_ticks.tick_values(minVal, maxVal, size)

Tick positions, split around the break when there is one.

Falls through to the original when no split is set, so an ordinary axis is untouched rather than routed through the split code.

spacr/qt/widgets/fast_plots.py:2339

FastPlot._wear_the_print_look.repaint(getter, setter, current)

Swap one chrome colour for its print equivalent, if there is one.

spacr/qt/widgets/fast_plots.py:3651

VolcanoPlot._q_ramp._brush(step: int)

One step of the ramp as a brush, cached.

Cached because a scatter asks for a brush PER POINT: building them fresh allocates one QBrush per point per repaint, which is the whole cost of drawing a large scatter.

spacr/qt/widgets/fast_plots.py:6063

_add_style_entry._shown(option)

The label for one option: its friendly name, or “automatic” for None.

spacr/qt/widgets/fast_plots.py:854

add_style_file_entries._ask(mode: str, suggested: str) → str

Ask for a path, through the injected picker or a file dialog.

INJECTED so the menu can be exercised without a modal dialog, which is otherwise the only way to reach these entries in a test.

spacr/qt/widgets/fast_plots.py:723

add_style_file_entries._clear_default() → None

Forget the saved default, so new figures use the built-in one.

spacr/qt/widgets/fast_plots.py:779

add_style_file_entries._load() → None

Read a style from a chosen file and apply it.

spacr/qt/widgets/fast_plots.py:753

add_style_file_entries._make_default() → None

Make the current style the one new figures start from.

spacr/qt/widgets/fast_plots.py:767

add_style_file_entries._save() → None

Write the current style to a chosen file.

spacr/qt/widgets/fast_plots.py:741

add_style_file_entries._say(message: str) → None

Report progress through the caller’s note, if one was given.

spacr/qt/widgets/fast_plots.py:718