Skip to content

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

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

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

Conversation

@igoroctaviano

@igoroctaviano igoroctaviano commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

dmv-branch: feat/bulk-ann-deckgl

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 f726405...8d5b9a3 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 14, 2026 4:43p.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 8d5b9a3):

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

(expires Fri, 21 Aug 2026 16:46:43 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 18 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.
Resolve conflicts:
- Keep @playwright/test from branch
- Remove semantic-release (removed in master PR #423)
- Use npm published DMV version (^0.48.23) instead of git hash
@github-actions

Copy link
Copy Markdown

📦 Firebase Preview - Using Published DMV

This preview is using the published dicom-microscopy-viewer from package.json:

Version ^0.48.23

To link a DMV branch for testing (requires an open PR in DMV):

  • Add dmv-branch: <branch-name> to the PR description, OR
  • Use the same branch name in both repos (automatic matching)

igoroctaviano and others added 2 commits August 14, 2026 09:31
pnpm 11's dependency status check fails when the git-hosted
dicom-microscopy-viewer's tarball URL isn't in allowBuilds. Instead of
hardcoding a SHA that goes stale, dynamically insert the resolved
DMV_SHA into pnpm-workspace.yaml before install.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔗 Firebase Preview - Linked to DMV Branch

This preview is using a linked dicom-microscopy-viewer branch:

Branch feat/bulk-ann-deckgl
Commit 9f3049d
Source dmv-branch

To change the DMV branch, edit dmv-branch: <branch-name> in the PR description.

- Use setState callback when referencing previous state (AnnotationGroupItem)
- Add explicit return undefined in useEffect (AnnotationGroupLoadIndicator)
- Add skipcq comments for valid dynamic delete patterns (SlideViewer)
- Use explicit React imports instead of namespace imports (ui components)
- Use stable string keys for slider thumbs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔗 Firebase Preview - Linked to DMV Branch

This preview is using a linked dicom-microscopy-viewer branch:

Branch feat/bulk-ann-deckgl
Commit 9f3049d
Source dmv-branch

To change the DMV branch, edit dmv-branch: <branch-name> in the PR description.

Radix primitives are designed to be used with namespace imports for
clarity and consistency. Added skipcq comments to suppress DeepSource
JS-C1003 warnings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔗 Firebase Preview - Linked to DMV Branch

This preview is using a linked dicom-microscopy-viewer branch:

Branch feat/bulk-ann-deckgl
Commit 9f3049d
Source dmv-branch

To change the DMV branch, edit dmv-branch: <branch-name> in the PR description.

The e2e visual regression tests were failing because they used the
published npm version of dicom-microscopy-viewer instead of the linked
git branch that contains bulk annotation features.

This adds the same DMV branch resolution and linking logic that exists
in deploy-to-firebase.yml:
- Resolve DMV branch from PR body (dmv-branch:) or matching branch name
- Dynamically update package.json and pnpm-workspace.yaml allowBuilds
- Build DMV from git dependency if dist files are missing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔗 Firebase Preview - Linked to DMV Branch

This preview is using a linked dicom-microscopy-viewer branch:

Branch feat/bulk-ann-deckgl
Commit 9f3049d
Source dmv-branch

To change the DMV branch, edit dmv-branch: <branch-name> in the PR description.

The Playwright container doesn't include the GitHub CLI, causing DMV
branch resolution to silently fail. Also, the container's default shell
is /bin/sh which doesn't support `set -o pipefail`.

Changes:
- Add step to install gh CLI before DMV branch resolution
- Use `shell: bash` for steps that need bash features

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔗 Firebase Preview - Linked to DMV Branch

This preview is using a linked dicom-microscopy-viewer branch:

Branch feat/bulk-ann-deckgl
Commit 9f3049d
Source dmv-branch

To change the DMV branch, edit dmv-branch: <branch-name> in the PR description.

igoroctaviano added a commit that referenced this pull request Aug 14, 2026
The e2e visual regression workflow is for testing bulk annotation
rendering, which belongs in the feat/bump-dmv-bulk-ann branch (PR #405),
not in this viv-loader branch. This branch doesn't have the e2e test
infrastructure (build:e2e, test:e2e scripts).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
igoroctaviano added a commit that referenced this pull request Aug 14, 2026
- Remove e2e-visual.yml workflow (belongs in PR #405, not this branch)
- Add skipcq for void operator usage (JS-0098)
- Add skipcq for high complexity functions (JS-R1005):
  - buildPointLayersFromGraphicData
  - buildPathLayersFromGraphicData
  - loadBulkAnnotationMetadataAndJobs
- Rename short variables (JS-C1002):
  - g→typedGeom, t→geomType
  - b→bulkApi
  - d→typedDataset
- Use regular strings instead of template literals (JS-R1004)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@fedorov fedorov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fill has incorrect behavior - appears only at certain zoom level. Also, it seems like the outline appearance changes across resolutions.

Small nitpick - it would be good to maintain consistent styling for the UI controls in the popup - was it intentional to change from rounded to squared items?

https://app.screencast.com/Ukf9HXK6Sb9cp

@fedorov

fedorov commented Aug 14, 2026

Copy link
Copy Markdown
Member

Also - if you didn't notice from the video - it is quite annoying that the structure popup does not disappear when mouse pointer is out of the viewport!

@fedorov

fedorov commented Aug 14, 2026

Copy link
Copy Markdown
Member

Also, when multiple groups are being loaded, I do not think it helps that notifications stack up!

image

@fedorov

fedorov commented Aug 14, 2026

Copy link
Copy Markdown
Member

~~AND it appears that BoneMarrow bounding boxes are not rendered... ~~ -- looks like this might have been peculiar to one of the wg26 connectathon servers!

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

🔗 Firebase Preview - Linked to DMV Branch

This preview is using a linked dicom-microscopy-viewer branch:

Branch feat/bulk-ann-deckgl
Commit 9f3049d
Source dmv-branch

To change the DMV branch, edit dmv-branch: <branch-name> in the PR description.

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.

3 participants