MetadataController

class MetadataController(available_subclasses: Optional[Mapping[str, List[str]]] = None, **kwargs)

Bases: MetaSubsetTabController

Subclass of MetaSubsetTabController for managing metadata view-model logic.

Relays plot data, query results, and column/unit updates to the view.

Public Methods

MetadataController.build_all_points_histogram(loader: str, sql_filter: str, experiments_and_channels: Dict[str, List[int | None]] | None, plot_type: str, bins: Any, sizes: bool, hist_min: float | None, hist_max: float | None, dataset_label: str) → None

Tally one event-data subset into an all-points histogram for the View.

A request slot. The events are walked and binned below rather than in the widget: nothing above the Model wants them, and walking them there meant walking them twice. The query is set only once the tally succeeded, which is what lets the View tell a fetch that failed from one that returned nothing.

Parameters:
  • loader (str) – the database loader plugin’s key

  • sql_filter (str) – the subset filter’s WHERE-clause body, empty for all rows

  • experiments_and_channels (Optional[Dict[str, List[Optional[int]]]]) – the experiment and channel scope, or None

  • plot_type (str) – the all-points histogram variant being drawn

  • bins (Any) – a bin count, or a bin width when sizes is True, or None

  • sizes (bool) – does bins refer to a bin width (True) or a count (False)

  • hist_min (Optional[float]) – the shared lower limit so far, or None for the first dataset

  • hist_max (Optional[float]) – the shared upper limit so far, or None for the first dataset

  • dataset_label (str) – the label this subset is drawn under

Returns:

None

Return type:

None

MetadataController.build_event_overlay(loader: str, sql_filter: str, experiments_and_channels: Dict[str, List[int | None]] | None, plot_type: str) → None

Put one event-data subset on a shared normalised axis for the View to draw.

A request slot, the same shape as build_all_points_histogram().

Parameters:
  • loader (str) – the database loader plugin’s key

  • sql_filter (str) – the subset filter’s WHERE-clause body, empty for all rows

  • experiments_and_channels (Optional[Dict[str, List[Optional[int]]]]) – the experiment and channel scope, or None

  • plot_type (str) – either ‘Raw Event Overlay’ or ‘Filtered Event Overlay’

Returns:

None

Return type:

None

MetadataController.calculate_heatmap(xdata: ndarray[tuple[int, ...], dtype[float64]], ydata: ndarray[tuple[int, ...], dtype[float64]], log_flags: Sequence[bool], bins: Any, sizes: bool, ax: Axes, x_label: str, y_label: str, dataset_label: str) → None

Filter and bin the heatmap’s two columns, and hand the result back to draw.

A request slot, the same shape as ClusteringController.cluster: the View asks, this calls the Model, and the answer goes back through a setter. The drawing context arrives and departs unchanged; this slot marshals and does not interpret it.

An invalid bin entry raises out of the Model and is reported here rather than propagating, because nothing between here and _overlay_plot catches it and an exception escaping a Qt slot reaches nobody.

Parameters:
  • xdata (npt.NDArray[np.float64]) – the raw x values

  • ydata (npt.NDArray[np.float64]) – the raw y values

  • log_flags (Sequence[bool]) – log-scale the x and y values before binning?

  • bins (Any) – number of bins, or size of bins when sizes is True, or None to estimate

  • sizes (bool) – does the bins parameter refer to bin sizes (True) or counts (False)

  • ax (Axes) – the axis object the View will draw on

  • x_label (str) – the x axis label, already formatted

  • y_label (str) – the y axis label, already formatted

  • dataset_label (str) – string to label the dataset

Returns:

None

Return type:

None

MetadataController.calculate_histogram_bins(datasets: Sequence[ndarray[tuple[int, ...], dtype[float64]]], logx: bool, bins: Any, sizes: bool, hist_min: float | None, hist_max: float | None, norm: bool, ax: Axes, x_label: str, column: str, dataset_label: str) → None

Filter every overlaid dataset, bin them onto shared edges, and count them.

A request slot, the same shape as estimate_kernel_densities() - and it has to be, because the two write the same accumulator and the same pair of shared limits, so a difference between them would show up as a plot disagreeing with itself.

Parameters:
  • datasets (Sequence[npt.NDArray[np.float64]]) – one raw column array per overlaid dataset, newest last

  • logx (bool) – log-scale the values before binning them?

  • bins (Any) – a bin count, or a bin width when sizes is True, or None

  • sizes (bool) – does bins refer to a bin size (True) or a count (False)

  • hist_min (Optional[float]) – the shared lower limit so far, or None for the first dataset

  • hist_max (Optional[float]) – the shared upper limit so far, or None for the first dataset

  • norm (bool) – normalise each dataset to a fraction rather than a count

  • ax (Axes) – the axis object the View will draw on

  • x_label (str) – the x axis label, already formatted

  • column (str) – the column’s own name, for the message when nothing survives

  • dataset_label (str) – the newest dataset’s label

Returns:

None

Return type:

None

MetadataController.count_categories(datasets: List[ndarray[tuple[int, ...], dtype[Any]]], labels: List[str], ax: Axes, x_label: str, y_label: str) → None

Tally each overlaid dataset’s categories, and hand them back to be drawn.

A request slot, the same shape as estimate_kernel_densities(). The drawing context arrives and departs unchanged; this slot marshals and does not interpret it.

A column of a type the tally cannot sort raises out of the Model and is reported here rather than escaping a Qt slot - which is what used to happen, since nothing between the View and _overlay_plot catches it.

Parameters:
  • datasets (List[npt.NDArray[Any]]) – one array of raw column values per overlaid dataset

  • labels (List[str]) – one label per dataset, index-aligned with datasets

  • ax (Axes) – the axis object the View will draw on

  • x_label (str) – the x axis label, already formatted

  • y_label (str) – the y axis label

Returns:

None

Return type:

None

MetadataController.estimate_kernel_densities(datasets: Sequence[ndarray[tuple[int, ...], dtype[float64]]], logx: bool, bins: Any, sizes: bool, hist_min: float | None, hist_max: float | None, ax: Axes, x_label: str, column: str, dataset_label: str) → None

Filter every overlaid dataset, then estimate each one’s density.

A request slot, the same shape as calculate_heatmap(). The drawing context arrives and departs unchanged; this slot marshals and does not interpret it.

The newest dataset is the last of datasets and has not been accumulated by the View yet, so a subset that loses every point to the filter is refused here and leaves no label behind.

Parameters:
  • datasets (Sequence[npt.NDArray[np.float64]]) – one raw column array per overlaid dataset, newest last

  • logx (bool) – log-scale the values before binning them?

  • bins (Any) – number of bins, or size of bins when sizes is True, or None to estimate

  • sizes (bool) – does the bins parameter refer to bin sizes (True) or counts (False)

  • hist_min (Optional[float]) – the shared lower limit so far, or None for the first dataset

  • hist_max (Optional[float]) – the shared upper limit so far, or None for the first dataset

  • ax (Axes) – the axis object the View will draw on

  • x_label (str) – the x axis label, already formatted

  • column (str) – the column’s own name, for the message when nothing survives

  • dataset_label (str) – the newest dataset’s label

Returns:

None

Return type:

None

MetadataController.export_csv_subset(loader: str, folder: str, name: str, subset_filter: str | None, experiments_and_channels: Dict[str, List[str]] | None, export_index: int) → None

Start writing one filtered subset to CSV in a worker thread.

The plugin returns a progress generator rather than a value, so this hands it straight to set_generator along with the export’s index, the loader key and the metaclass. RawDataController.commit_events is the same shape.

The View’s index only advances for an export that was staged, which is what on_subset_export_started says - and staged now means “counted, non-empty, and handed to a worker”. An export that fails partway through its generator still consumes its name, because by then it is a running job rather than a refused one.

export_subset_to_csv is a generator, so call() only constructs it and the try/except below sees nothing that happens inside its body - including its own empty-subset check, which fires on the worker’s first advance, after the index has already advanced. That is why the subset is counted first through count_subset_events, which runs the same query the export runs, through the same builder. An empty subset is refused here, before a worker exists.

Parameters:
  • loader (str) – the database loader plugin’s key

  • folder (str) – the folder to write the subset into

  • name (str) – the name to append to the exported filenames

  • subset_filter (Optional[str]) – the single selected filter, or None for the whole dataset

  • experiments_and_channels (Optional[Dict[str, List[str]]]) – the experiment and channel scope

  • export_index (int) – the index this export’s worker is keyed under

Returns:

None

Return type:

None

MetadataController.filter_3d_scatterplot(columns: Sequence[ndarray[tuple[int, ...], dtype[float64]]], log_flags: Sequence[bool], ax: Axes, axis_labels: Sequence[str], dataset_label: str) → None

The same for a 3-D scatterplot’s three columns.

Separate from filter_scatterplot() because the View’s two drawing halves are separate: a 3-D scatter rebuilds the axes and carries a z label.

Parameters:
  • columns (Sequence[npt.NDArray[np.float64]]) – the raw x, y and z values

  • log_flags (Sequence[bool]) – log-scale each column?

  • ax (Axes) – the axis object the View will draw on

  • axis_labels (Sequence[str]) – the axis labels, already formatted

  • dataset_label (str) – string to label the dataset

Returns:

None

Return type:

None

MetadataController.fit_capture_rate(data: ndarray[tuple[int, ...], dtype[float64]], bins: Any, sizes: bool, ax: Axes, x_label: str, y_label: str, dataset_label: str) → None

Bin and fit the capture rate, and hand the result back to the View to draw.

A request slot, the same shape as calculate_heatmap(). A fit that will not converge raises RuntimeError out of curve_fit, and is reported rather than allowed to escape a Qt slot, where it would reach nobody.

The inter-event times are computed below rather than in the View: gaps between consecutive events are the measurement, not the drawing. Both conditions that used to be judged on them belong with the computation - too little surviving data is reported instead of raised, which reaches the user with the count in it rather than as update_plot’s generic “no data available after filtering”.

Parameters:
  • data (npt.NDArray[np.float64]) – the event times as they came out of the column, unsorted

  • bins (Any) – a bin count, or a bin width when sizes is True, or None

  • sizes (bool) – does bins refer to a bin width (True) or a count (False)

  • ax (Axes) – the axis object the View will draw on

  • x_label (str) – the x axis label, already formatted

  • y_label (str) – the y axis label

  • dataset_label (str) – string to label the dataset

Returns:

None

Return type:

None

MetadataController.load_metadata_subset(loader: str, columns: List[str], sql_filter: str, experiments_and_channels: Dict[str, List[int | None]] | None) → None

Fetch one metadata subset - query, rows and units - and hand it to the View.

All three parts are fetched here and handed over together, because asking for them one at a time made a failure indistinguishable from a success: a query that could not be built left the previous subset’s query behind and passed the == "" guard, and a units lookup that failed left the previous column’s units to be appended, labelling the axis wrongly. The View clears all three before asking and this method sets them only once every part has been fetched, so a partial failure is visible as one.

This is also where the SQL the user sees comes from: the query echoed to the status panel is the one that runs, not the smaller one that validated the filter, and it is echoed only when it differs from the last one shown.

Parameters:
  • loader (str) – the database loader plugin’s key

  • columns (List[str]) – the columns this plot type needs, in axis order

  • sql_filter (str) – the subset filter’s WHERE-clause body, empty for all rows

  • experiments_and_channels (Optional[Dict[str, List[Optional[int]]]]) – the experiment and channel scope, or None

Returns:

None

Return type:

None

MetadataController.request_column_type(loader: str, column: str) → None

Fetch one column’s declared type, for the categorical-histogram guard.

The View clears the answer before asking, so a lookup that failed leaves it None and the guard refuses the plot. Refusing on an unknown type is deliberate: the guard exists to keep a categorical column off a numeric axis, and it cannot do that on a type it did not get.

Parameters:
  • loader (str) – the database loader plugin’s key

  • column (str) – the column whose type is wanted

Returns:

None

Return type:

None

MetadataController.request_column_units(loader: str, column: str, axis: str) → None

Fetch one column’s units and apply them to one axis label.

The axis travels with the request and back out again, so the answer knows which label it belongs to without anything being parked here in between. This is Metadata’s alone, because the protein tab has no units label to write to.

Parameters:
  • loader (str) – the database loader plugin’s key

  • column (str) – the column whose units are wanted

  • axis (str) – the axis whose label they belong to

Returns:

None

Return type:

None

MetadataController.request_plot_features(loader: str, experiment_id: int, channel_id: int, event_id: int) → None

Fetch one event’s fitted features and hand them to the View.

Asked once per event on the plot path, so a failure is logged and the event is plotted without features rather than stopping the plot. The View clears the six feature attributes before each request and reads them back after, so an event whose lookup failed gets none rather than the previous event’s.

update_features’ label-length validation is called here rather than inlined, and its ValueError is caught for the same reason the bus caught it: one fitter returning mismatched labels should not abandon the plot.

Parameters:
  • loader (str) – the database loader plugin’s key

  • experiment_id (int) – the event’s experiment id

  • channel_id (int) – the event’s channel id

  • event_id (int) – the event’s id within that channel

Returns:

None

Return type:

None

MetadataController.update_features(vertical: list[float] | None = None, horizontal: list[float] | None = None, points: list[tuple[float, float]] | None = None, vlabels: list[str] | None = None, hlabels: list[str] | None = None, plabels: list[str] | None = None) → None

Update the plot with visual annotations including vertical lines, horizontal lines, and point markers.

Validates that each visual feature has a corresponding label (or explicit None) if labels are provided.

Parameters:
  • vertical (Optional[list[float]]) – Vertical line positions for the event being plotted.

  • horizontal (Optional[list[float]]) – Horizontal line positions for the event being plotted.

  • points (Optional[list[tuple[float, float]]]) – (x, y) point coordinates for the event being plotted.

  • vlabels (Optional[list[str]]) – Labels for the vertical lines.

  • hlabels (Optional[list[str]]) – Labels for the horizontal lines.

  • plabels (Optional[list[str]]) – Labels for the point markers.

Raises:

ValueError – If a label list is provided and its length does not match the corresponding feature list.

Private Methods

MetadataController._init() → None

Initialize the metadata view and model.

MetadataController._setup_connections() → None

Wire this tab’s own eleven intents on top of the nine the subset base wires.

Returns:

None

Return type:

None