diff --git a/AGENTS.md b/AGENTS.md index f873c7dc..ffb56a6c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -305,6 +305,10 @@ src/morphobricks_preprocessing/ (plan §12 Q26, Q31). Since PP-9 it holds the page methods, __init__ and the shared helpers; the step methods live on mixin classes in the modules below, and it re-exports every public name it had (`__all__`) + notebook_steps.py steps(): the shipped notebook's 13 steps, one at a time (PR 17a; pure + composition of the cards below); notebook_stepper.py the strip, Back/Next + and the overview; notebook_step_states.py each step's done/one-line state + notebook_names.py the names card's rows (conditions on step 4, replicates on step 5) notebook_folder.py step 1: folder, check, issues, labels (_FolderStep) notebook_design.py step 1: the experiment design card (_DesignCard) notebook_axis_step.py step 2: role dropdowns, axis check, missing-axes card (_AxisStep) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5916904b..ca2f3c88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ are in [docs/plan.md](docs/plan.md) §12. Upgrading from v0.3: see the ## Unreleased +- **Notebook: one step at a time (UX proposal 2026-11-09, PR 17a).** `PreprocessingNotebook.steps()` + shows the shipped notebook's 13 steps one at a time, with a strip of chips (done green with a tick, + current blue, to do grey, changed by a reopen orange; a chip opens its step) and Back/Next (Next is + never blocked: the save and the run still refuse what is missing). The order is the owner's: images + found, axes, pixel sizes, conditions, replicates (pairing with the replicate and technical-repeat + names, then the consistency card only when it warns), compared against, image type (its own page; + the tag grid has no type column there), stains, tag channels, masks, check and save (starting with a + one-line overview of steps 1-10, each with *change*), run, describe the masks. Pure composition: the + cards, answers, draft, save and run are unchanged; a reopened experiment opens on its first step + still to do (the axes, confirmed once per session). The notebook file shows `notebook.steps()` and + `results_step()`; `folder_step()` and `axis_step()` render as before for embedders. Text: no RGB + sentence on a described run's axes page, no broken picture before the preview, "same way?" *above*, + the reader and CSV lines of *Check and save* behind *Details*, "folder" instead of "unit", and the + steps named by their new numbers. The run page's box plots and curves order the replicates of a + condition by replicate ID (`rep1` before `rep2`), not by folder name. - **Re-pin the vendored Spec to 1bed024 (MorphoBricks-Spec PR 16, owner decisions RF1 to RF6); `read_records` follows section 4.6.** The Spec now fixes the bytes and values of `morphobricks.records.jsonl` and names one refusal, `R_RECORDS_FILE_INVALID`, with the reasons diff --git a/notebooks/MorphoBricks_Preprocessing.ipynb b/notebooks/MorphoBricks_Preprocessing.ipynb index 062498d3..20fcccfa 100644 --- a/notebooks/MorphoBricks_Preprocessing.ipynb +++ b/notebooks/MorphoBricks_Preprocessing.ipynb @@ -30,8 +30,8 @@ "In the app:\n", "\n", "1. Give the **experiment folder** and optional **output/masks folders**, then click **Check folder**.\n", - "2. Confirm the labels/design and review the axis roles and image type.\n", - "3. Click **Compute histograms**.\n", + "2. Go through the steps one at a time with **Next** (or click a step in the strip at the top): axes, pixel sizes, conditions, replicates, comparisons, image type, stains, channel tags and masks.\n", + "3. On **Check and save**, read your experiment in words and save it; then **Run** computes the histograms.\n", "4. Review the QC results; pooled statistics are optional.\n", "\n", "Detailed explanations appear inside the app only where a decision is required.\n" @@ -313,16 +313,7 @@ "from IPython.display import display\n", "\n", "app = widgets.VBox(\n", - " [\n", - " notebook.welcome(),\n", - " notebook.folder_step(),\n", - " notebook.axis_step(),\n", - " notebook.channels_step(),\n", - " notebook.masks_step(),\n", - " notebook.check_save_step(),\n", - " notebook.run_step(),\n", - " notebook.results_step(),\n", - " ],\n", + " [notebook.welcome(), notebook.steps(), notebook.results_step()],\n", " layout=widgets.Layout(width=\"99%\"),\n", ")\n", "display(app)" diff --git a/pyproject.toml b/pyproject.toml index 71312f01..c75ad4c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,6 +150,10 @@ baseline = "tools/code_rules_baseline.json" "morphobricks_preprocessing.notebook_description" = "ui" "morphobricks_preprocessing.notebook_description_saved" = "ui" "morphobricks_preprocessing.notebook_description_view" = "ui" +"morphobricks_preprocessing.notebook_names" = "ui" +"morphobricks_preprocessing.notebook_step_states" = "ui" +"morphobricks_preprocessing.notebook_stepper" = "ui" +"morphobricks_preprocessing.notebook_steps" = "ui" "morphobricks_preprocessing.notebook_design" = "ui" "morphobricks_preprocessing.notebook_masks" = "ui" "morphobricks_preprocessing.notebook_masks_screen" = "ui" diff --git a/src/morphobricks_preprocessing/notebook_axis_preview.py b/src/morphobricks_preprocessing/notebook_axis_preview.py index 4e3c2474..50ae8c82 100644 --- a/src/morphobricks_preprocessing/notebook_axis_preview.py +++ b/src/morphobricks_preprocessing/notebook_axis_preview.py @@ -90,11 +90,13 @@ def _render_preview(self) -> None: ) except (PlaneReadError, IndexError, ValueError) as exc: self.preview_image.value = b"" + ui.set_visible(self.preview_image, False) self.preview_caption.value = ui.status( "warn", "This plane cannot be shown with these roles.", [str(exc)] ) return self.preview_image.value = plane_png(plane) + ui.set_visible(self.preview_image, True) self.preview_caption.value = self._preview_label() + preview_caption( header.relative_path, plane ) diff --git a/src/morphobricks_preprocessing/notebook_axis_step.py b/src/morphobricks_preprocessing/notebook_axis_step.py index 0fcc4aed..a5328478 100644 --- a/src/morphobricks_preprocessing/notebook_axis_step.py +++ b/src/morphobricks_preprocessing/notebook_axis_step.py @@ -89,6 +89,7 @@ def _build_preview_card(self) -> None: tooltip="Read one plane with the roles above and show it", ) self.preview_image = widgets.Image(format="png", layout=widgets.Layout(max_width="420px")) + ui.set_visible(self.preview_image, False) # no broken-picture icon before a preview self.preview_caption = widgets.HTML() self.preview_checked = ui.ConfirmToggle( "Does the preview look like a normal image of your sample?", @@ -246,6 +247,7 @@ def _reset_preview(self) -> None: for slider in self.preview_sliders.values(): ui.set_visible(slider, False) self.preview_image.value = b"" + ui.set_visible(self.preview_image, False) self.preview_caption.value = "" self.confirmed_status.value = "" diff --git a/src/morphobricks_preprocessing/notebook_channels.py b/src/morphobricks_preprocessing/notebook_channels.py index 49fdff9c..fdb8b2a6 100644 --- a/src/morphobricks_preprocessing/notebook_channels.py +++ b/src/morphobricks_preprocessing/notebook_channels.py @@ -28,7 +28,7 @@ INTRO = ( "What happens here: first name what you stained (the labels), then say, for every " - "unit, which label each channel is, looking at its picture. Nothing is chosen for you: " + "folder, which label each channel is, looking at its picture. Nothing is chosen for you: " "names found in the image headers are offered only as suggestions, and each one needs your " "click." ) diff --git a/src/morphobricks_preprocessing/notebook_channels_grid.py b/src/morphobricks_preprocessing/notebook_channels_grid.py index 8a10795e..472781ce 100644 --- a/src/morphobricks_preprocessing/notebook_channels_grid.py +++ b/src/morphobricks_preprocessing/notebook_channels_grid.py @@ -51,8 +51,8 @@ def partial_text(label: str, used: int, total: int) -> str: """Return the sentence for a label used in only some units, with its consequence.""" return ( - f"used in {used} of {total} units: an analysis of {html.escape(label)} will be " - "refused for the other units" + f"used in {used} of {total} folders: an analysis of {html.escape(label)} will be " + "refused for the other folders" ) @@ -66,6 +66,8 @@ def __init__( on_change: Callable[[], None] = lambda: None, ) -> None: self.store, self.types, self._on_change = store, types, on_change + # False when the image type has a page of its own (the stepper): no type column here. + self.place_types = True self.layouts: Sequence[UnitLayout] = () self.labels: list[str] = [] self.thumbs: Thumbnails | None = None @@ -84,9 +86,9 @@ def __init__( "B. Tag each channel", [ ui.help_text( - "For every unit, say which label each channel is, looking at its picture. " + "For every folder, say which label each channel is, looking at its picture. " "The same label should be in the same column in every row; a label used in " - "only some units is shown in orange. Every channel and every image type " + "only some folders is shown in orange. Every channel and every image type " "must be answered." ), self.contrast, @@ -105,7 +107,8 @@ def __init__( def open(self, layouts: Sequence[UnitLayout], labels: list[str], thumbs: Thumbnails) -> None: """Show the grid for these units and labels (the confirmed label list of step A).""" self.layouts, self.labels, self.thumbs = layouts, labels, thumbs - self.types.open(layouts) + if self.place_types: + self.types.open(layouts) self.detail.children = [] self.refresh() ui.set_visible(self.widget, True) @@ -193,7 +196,8 @@ def refresh(self) -> None: self._building = True try: self.cells, self.confirm_buttons = {}, [] - self.types.open(self.layouts) + if self.place_types: + self.types.open(self.layouts) width = max((u.channels for u in self.layouts if not u.rgb), default=3) width = max(width, 3 if any(u.rgb for u in self.layouts) else 1) groups: dict[tuple, list[UnitLayout]] = {} @@ -201,9 +205,9 @@ def refresh(self) -> None: groups.setdefault(layout.signature if layout.usable else (0,), []).append(layout) blocks: list[widgets.Widget] = [] header = widgets.GridBox( - [widgets.HTML("unit")] + [widgets.HTML("folder")] + [widgets.HTML(f"channel {i + 1} (C{i})") for i in range(width)] - + [widgets.HTML("image type")], + + [widgets.HTML("image type" if self.place_types else "")], layout=columns(width), ) blocks.append(header) @@ -221,7 +225,7 @@ def _group(self, units: Sequence[UnitLayout], width: int) -> widgets.Widget: if first.usable: names = " · ".join(quoted(first.name(i)) for i in range(first.channels)) what = f"Header names: {names}" if not first.rgb else "Header says: colour (RGB)" - text = f"{what} · {len(units)} unit(s) with this layout." + text = f"{what} · {len(units)} folder(s) with this layout." proposal = self._proposal(first) pending = [u for u in units if self._pending(u)] if proposal is not None: @@ -233,10 +237,10 @@ def _group(self, units: Sequence[UnitLayout], width: int) -> widgets.Widget: out.append(widgets.HTML(f"