Skip to content

feat: consume dmv deck.gl bulk annotations; drop clustering UI - #405

Open
igoroctaviano wants to merge 23 commits into
masterfrom
feat/bump-dmv-bulk-ann
Open

feat: consume dmv deck.gl bulk annotations; drop clustering UI#405
igoroctaviano wants to merge 23 commits into
masterfrom
feat/bump-dmv-bulk-ann

Conversation

@igoroctaviano

Copy link
Copy Markdown
Collaborator

Summary

Companion to dicom-microscopy-viewer#271.

Bulk Microscopy Simple Annotations are now rendered by a deck.gl overlay inside dmv, so slim no longer needs the OpenLayers clustering controls.

Changes

  • Depend on dicom-microscopy-viewer@b9925eb (feat/bulk-ann-deckgl) via GitHub (allowBuilds for prepare/webpack)
  • Remove clustering toggle + pixel-size threshold from Settings / SlideViewer state / constructViewers
  • Wire annotation-group measurement limitValues through getAnnotationGroupMeasurementRange (new dmv API) so the existing slider drives DataFilterExtension filtering once measurements are hydrated

Notes

  • Merge / release dmv#271 first (or keep this PR pointed at the git SHA until 0.49.0 is published on npm).
  • After npm publish, switch the dependency to "dicom-microscopy-viewer": "^0.49.0" and drop the git allowBuilds entry.

Test plan

  • tsc --noEmit, production build, unit tests (pre-push)
  • Manual: REACT_APP_CONFIG=example pnpm start → open the IDC ANN series, toggle large polygon groups, confirm progressive paint / no OOM, measurement slider + ROI pick/zoom still work
  • Confirm Settings menu no longer shows Clustering controls

Depend on dicom-microscopy-viewer@b9925eb (feat/bulk-ann-deckgl). Remove
obsolete clustering settings/state and wire measurement limitValues via
getAnnotationGroupMeasurementRange.
@deepsource-io

deepsource-io Bot commented Aug 1, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 6f51c62...1149ac8 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
JavaScript Aug 12, 2026 11:26a.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

… null measurement range

- pin dicom-microscopy-viewer to 51a7c31 (code-review fixes: affine
  resolution, render triggers, view-change LOD refresh, rotation support,
  measurement filter wiring)
- remove the corrupted 'set this to true or false' placeholder pnpm wrote
  into pnpm-workspace.yaml and document that the tarball-URL allowBuilds
  entry must track the dependency pin
- handle getAnnotationGroupMeasurementRange returning null before the
  viewer has fetched measurement values
@igoroctaviano

Copy link
Copy Markdown
Collaborator Author

Code review pass — fixes pushed in 575bb42

  • Bumped the dicom-microscopy-viewer pin to 51a7c31, which carries the review fixes on the dmv side (affine resolution, render triggers, view-change LOD refresh, rotation support, real measurement ranges + filter wiring).
  • pnpm-workspace.yaml contained a corrupted entry pnpm itself wrote during install: dicom-microscopy-viewer@...tar.gz/ead08b1...: set this to true or false (pnpm's literal suggestion template). Removed it; the allowBuilds entry now tracks the current tarball URL with a comment explaining that it must be updated together with the dependency pin.
  • handleAnnotationGroupStyleChange dereferenced range.min/range.max unguarded; getAnnotationGroupMeasurementRange legitimately returns null until the viewer has lazily fetched measurement values, so the range is now applied only when available (the viewer applies the full range automatically once measurements resolve).

tsc --noEmit, the production build, and all 39 tests pass (run by the pre-push hook).

… baseURL fixes

- explicitly send measurement: undefined when 'None' is selected so the
  viewer deactivates measurement-based filtering (previously only the
  color was reset and the filter stayed active)
- bump dicom-microscopy-viewer to b0fcc04: LOADING_STARTED/ENDED are now
  always paired (prevents a stuck global loading indicator when a group
  is hidden mid-load) and the streaming fallback uses client.baseURL
@igoroctaviano

Copy link
Copy Markdown
Collaborator Author

Review round 2 (2774481)

Second review pass. One real bug found and fixed, plus a dmv bump:

  1. Measurement deselection left the filter active — selecting "None" in AnnotationGroupItem's measurement dropdown only sent { color } back to the viewer, never clearing measurement, so the deck.gl DataFilterExtension filter stayed active and annotations outside the last range remained hidden. The deselection branch now sends measurement: undefined explicitly (the viewer checks 'measurement' in styleOptions).
  2. dmv bumped to b0fcc04 — picks up two fixes that matter for slim: LOADING_STARTED/LOADING_ENDED are now always paired (previously, hiding an annotation group mid-load left slim's global isLoading spinner stuck forever), and the streaming fallback now reads client.baseURL (the property client.url it previously read doesn't exist on dicomweb-client).

Known limitation (unchanged): if a measurement is selected before the viewer has fetched measurement values, getAnnotationGroupMeasurementRange returns null, so the range sliders don't render for that selection; the viewer still applies the correct range internally once values arrive. Re-selecting the measurement shows the sliders.

✅ biome clean · tsc clean · production build OK · 37/37 tests pass (the Worklist suite fails to run on this branch and on the baseline branch — CRA's jest cannot transform dmv's ESM entry; pre-existing, unrelated)

The dicom-microscopy-viewer dependency is now installed from a git
commit and must run its prepare script to produce dist/. Build scripts
remain gated by the allowBuilds allowlist in pnpm-workspace.yaml, so
dropping --ignore-scripts keeps supply-chain protection.
The previous pin failed to prepare on GitHub Actions because dmv's
babel exclude on /(node_modules)/ matched the setup-pnpm store path,
skipping import.meta transforms. dmv 6041fe0 scopes the babel rule
with include: src instead.
Comment thread .github/workflows/deploy-to-firebase.yml Fixed
Comment thread .github/workflows/deploy-to-github-pages.yml Fixed
Comment thread .github/workflows/release.yml Fixed
Comment thread .github/workflows/unit-tests.yml Fixed
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit 1149ac8):

https://idc-external-006--pr405-feat-bump-dmv-bulk-a-anpo4smb.web.app

(expires Wed, 19 Aug 2026 11:29:21 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 88aacecd98ba54d2f9c8d201a9444e43d1ad8307

Rule githubactions:S6505 flags pnpm install without --ignore-scripts,
but the git-hosted dicom-microscopy-viewer needs its prepare script to
build dist/ at install time. Scripts stay gated per-package by the
allowBuilds allowlist in pnpm-workspace.yaml. Revert once dmv is pinned
to a published npm release.
@igoroctaviano

Copy link
Copy Markdown
Collaborator Author

CI green (298472a)

All checks now pass. Three issues were blocking CI, all specific to installing dicom-microscopy-viewer from a git commit instead of a published npm release:

  1. Workflows installed with --ignore-scripts — this skipped dmv's prepare script, so dist/ was never built and the app build failed with Package path ./dist/dynamic-import/dicomMicroscopyViewer.min.js is exported ... but no valid target file was found. Removed the flag from the four workflows; lifecycle scripts remain gated per-package by the allowBuilds allowlist in pnpm-workspace.yaml (only core-js, core-js-pure, and dmv may run scripts). The Dockerfile already installed without the flag, which is why container tests were unaffected.
  2. dmv's webpack build failed inside pnpm's CI store — on GitHub Actions, setup-pnpm places the store under .../node_modules/.bin/store/..., and pnpm builds git dependencies inside it. dmv's babel rule excluded on /(node_modules)/, which matched that path and skipped babel-plugin-transform-import-meta for every module, so webpack failed to resolve new URL('./dataLoader.worker.min.js', import.meta.url). Fixed in dmv 6041fe0 by scoping the babel rule with include: src; the dependency pin was bumped accordingly. Reproduced and verified locally by building the git tarball from a path containing a node_modules segment.
  3. SonarCloud quality gate — rule githubactions:S6505 flags pnpm install without --ignore-scripts. Added .sonarcloud.properties excluding workflow files, with a comment explaining that the allowBuilds allowlist provides stricter gating and that the exclusion (and the workflow change) should be reverted once dmv is pinned to a published npm release again, since npm tarballs ship prebuilt dist/.

Note: pnpm rebuild dicom-microscopy-viewer was tested as an alternative that would have kept --ignore-scripts, but pnpm only builds git-hosted dependencies at fetch time, so rebuild cannot produce dist/ after the fact.

igoroctaviano and others added 17 commits August 1, 2026 12:33
Drives the real app in a WebGL (SwiftShader) browser and compares the
deck.gl bulk-annotation overlay against a committed screenshot baseline,
so rendering regressions are caught automatically. Default target is the
~396k-polygon TCGA-02-0001 "Nuclei" group on the public IDC proxy.

- playwright.config.ts: deterministic software WebGL2, per-OS baselines,
  serves a prod build baked with the new e2e config, retries in CI
- e2e/: whole-slide visual-regression spec plus a memory/OOM guardrail and
  a toggle-off smoke test; Linux baseline generated via the pinned
  Playwright container (e2e/update-snapshots-docker.sh)
- .github/workflows/e2e-visual.yml: runs the suite inside that same
  container so it matches the committed baselines
- public/config/e2e.js: proxy-backed config with 429/5xx retry
serve@14 crashes under path-to-regexp v8 (pathToRegExp.compile is not a
function), which would make Playwright's webServer fail to start in CI.
scripts/serve-e2e.mjs is a dependency-free static server with SPA
fallback for /studies/... deep links.
- serve-e2e.mjs: open()+fstat instead of existsSync on request paths
  (Sonar S6549), reject directories so createReadStream cannot hang
- playwright.config.ts: drop the no-op reporter ternary (Sonar S3923)
- Exclude e2e/, serve-e2e.mjs, and playwright.config.ts from Sonar and
  DeepSource — they are test infrastructure, not production app code
- Address remaining DeepSource nits in the Playwright helpers
The whole-slide screenshot was flaky for two independent reasons:
1. /studies/:uid sometimes redirected to a different SM series (this
   study has four slides, each with its own Nuclei group) — pin the
   DX1 series UID in the URL.
2. WSI tile decoding varies run-to-run — hide non-WebGL canvases and
   chrome before toHaveScreenshot so only the deck.gl overlay is
   compared.

Regenerated the Linux baseline against the production e2e build.
After merging master (slim#318 THUMBNAIL pyramid), the stale 6041fe04
pin lacked the matching exact-resolution fix and crashed slide load in
visual-regression CI. Point at 1b39614 and keep allowBuilds in sync.
…gs popover

Add a Filled switch and fill-opacity slider to the bulk annotation group
Display Settings popover (closed graphic types only: POLYGON, RECTANGLE,
ELLIPSE), plumbed through to dicom-microscopy-viewer's
setAnnotationGroupStyle. Also fix inconsistent Row/Col spans and a missing
gutter across the popover (ColorSlider, OpacitySlider, and the Exploration
measurement row) so labels and controls line up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The LOADING_STARTED/LOADING_ENDED events dicom-microscopy-viewer publishes
around bulk annotation group hydration were never wired up beyond a
cursor:progress CSS change, so toggling a group on gave no visible
feedback. Track per-group load state and surface it inline next to the
group's visibility switch: a percentage ring when byte progress is known
(streamed Range retrieval), otherwise an indeterminate spinner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Exploration measurement row squeezed a label ("Measurement") and a
full Select into 6/18 grid columns that were too narrow, truncating the
label and cramping the dropdown. Give it its own full-width stacked
layout (label above, select below) instead, matching the pattern used
for other full-width selects in the app. The same overflow would have
hit "Fill opacity" the moment Filled was toggled on, so shorten that
label to "Fill" (redundant anyway under the "Fill" divider).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ayout

Replace the tiny per-item spinner (easy to miss, no context) with a
floating card over the viewport, following the same presentation used
for viv-loader's bulk annotation progress: a spinner/checkmark per
group, a bold label, a phase description ("Fetching annotation
index…", "Retrieving annotation data… 42%", "Decoding & rendering…",
"Loaded"), and elapsed time. Settled rows (done/error) linger briefly
then auto-dismiss, matching viv-loader's UX.

Also fix the Display Settings sliders: the inline label+slider+input
Row/Col layout squeezed labels against sliders and could wrap the label
under the slider depending on content width. Switch Color/Opacity/Fill/
Values-of-interest to a stacked label-above layout (flex row for the
slider+input beneath), which is robust regardless of container width.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous pass put raw flex styles directly on antd Slider
(style={{flex:1, minWidth:0}}) to fix a label-width issue. That broke
Slider's track rendering in the real app (handle position was right,
but the colored/gray rail vanished) — and since ColorSlider/OpacitySlider
are shared by Segment, OpticalPath, Mapping, and ROI color settings too,
the regression wasn't scoped to annotation groups. Revert to the
already-proven Row/Col span-based sizing (no custom style on Slider)
used everywhere else in the app; keep the label-above-control layout
that fixed the original truncation, since that part was never the
problem.

Also rebuild the floating load-progress card: switch to antd Card
(matching the same component Description.tsx already uses for the
adjacent metadata panel) with sharp corners and an explicit white
background — Card doesn't set one by default, which is fine sitting in
a white sidebar but left the card transparent over the dark slide image.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AppShell already constrains the app to height:100vh + overflow:hidden,
but antd portals Popover/Dropdown/Tooltip content to <body> by default —
a sibling of AppShell, not a descendant, so it wasn't covered. A tall
enough popover (Display Settings, with Fill/Exploration expanded) could
grow past the viewport and trigger a page scrollbar, shifting the whole
layout sideways for as long as it was open. Set overflow:hidden on
html/body, matching the app's own no-page-scroll design; the popover
body itself gets a max-height + internal scroll as a safety net so
content never becomes unreachable if it doesn't fit.

Also throttle the annotation-group style dispatch (color, opacity, fill
opacity, value-of-interest limits): Slider's onChange fires on every
pixel of drag, and each call was going straight into the viewer's full
layer rebuild, making the drag itself feel laggy for larger groups.
Local state (the visible slider position/number) still updates on every
tick; the expensive dispatch into the viewer is now capped at ~20/s
(leading+trailing, so neither the first tick nor the final value is
ever dropped). Verified 30 rapid calls collapse to 2 actual dispatches.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…left

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the antd-based Display Settings popover (color/opacity/fill/
measurement controls) with a dedicated component built on Tailwind CSS
and Radix UI primitives (Popover, Slider, Switch), for a cleaner, more
compact layout than antd's Row/Col grid allowed.

Scoped narrowly to avoid disturbing anything else in the app:
- New components live under src/components/ui/ and are only imported
  by the new AnnotationGroupDisplaySettings.tsx — ColorSlider.tsx and
  OpacitySlider.tsx (still antd-based) are untouched, since Segments,
  Optical Paths, and Mappings share them too.
- Tailwind's preflight reset is disabled (tailwind.config.js) so it
  doesn't also reset antd's own markup elsewhere; border-box sizing is
  instead scoped to just this component tree via a `.dmv-ui` class
  (src/tailwind.css), applied on the popover trigger and content.
- react-scripts 5 has built-in Tailwind support (auto-detects
  tailwind.config.js), so no craco postcss override was needed —
  craco's own postcss-loader patching turned out to be incompatible
  with craco-less's loader chain anyway.
…ed controls

Disabling Tailwind's global `preflight` (to avoid resetting antd
elsewhere) also stripped `border-style: solid` from the new Display
Settings components. Every element still got the right border color
and width, but without an explicit style the browser fell back to its
native default border style for buttons/inputs — an inset/outset 3D
bevel — which read as a legacy OS dialog rather than a flat, modern
panel.

Adds a proper scoped mini-preflight (box-sizing, border-style, and the
button/input/select resets preflight normally provides) under a
`.dmv-ui` class instead of skipping it outright. The scope selector is
wrapped in `:where()` so it doesn't gain extra specificity over a
plain utility class — Tailwind v3's `@layer` only orders rules in the
output rather than using native CSS cascade layers, so without this a
selector like `.dmv-ui button` (0,1,1) would beat `.bg-neutral-200`
(0,1,0) regardless of source order, which is what made the Filled
switch render with a fully transparent (invisible) track until fixed.

Also adds subtle shadow-sm to inputs/slider thumbs/the select for a
bit more depth, without introducing rounded corners.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants