Skip to content

[DEPRECATED] feat(ecg): ECG Next Viewport Enhancements - #2821

Open
Harshika-Chandvani wants to merge 18 commits into
cornerstonejs:mainfrom
Harshika-Chandvani:dev-ecg
Open

Harshika-Chandvani wants to merge 18 commits into
cornerstonejs:mainfrom
Harshika-Chandvani:dev-ecg

Conversation

@Harshika-Chandvani

@Harshika-Chandvani Harshika-Chandvani commented Jul 22, 2026 •

Copy link
Copy Markdown

DEPRECATED - in favour of:
PR 1 (#2899) — Core Primitive: GenericViewport ECG architecture & metadata parsing.
PR 2 (#2900) — Layouts & Calculations: Multi-lead presets, sensitivity scaling, and grid alignment.
PR 3 (#2901) — Tools & Demo: WaveformRegionOverlayTool, annotation calibration (ms/mV), and demo.

Context

First step toward a dedicated ECG mode in OHIF, following guidance from the team during office hours (see OHIF/Viewers#6068).

The team recommended building on the Cornerstone side first — extending the base viewport rather than creating a fully custom one — before layering an OHIF mode on top.

Changes & Results

New: GenericViewport abstract base class (RenderingEngine/GenericViewport/GenericViewport.ts)
Rendering-backend-agnostic base owning binding orchestration, shared view state, per-dataset presentation state, coordinate transforms, and camera events.

New: ECGViewport extending GenericViewport (GenericViewport/ECG/ECGViewport.ts)
Full reimplementation: setDisplaySets, scroll, scrollToTime, setZoom, setPan, getVisibleChannels, getWaveformData, resetViewState.

New: ECGViewportLegacyAdapter
Thin adapter re-exposing the existing setEcg / setChannelVisibility / getProperties / setProperties API — no breaking changes to existing callers.

New: CanvasECGRenderPath — canvas 2D rendering extracted into a standalone render path.

New: ECGResolvedView — coordinate transforms: canvas pixels ↔ ECG world space (time ms, amplitude mV).

Updated: UltrasoundDirectionalTool — added ECGGenericViewport to supported viewport type guard.

Updated: getCalibratedUnits — ECG X-axis measurements now output milliseconds (not raw seconds).

The existing ECGViewport class is untouched — no breaking changes. No new Zustand stores or event patterns.

Verification & Testing

Screenshot 2026-07-31 at 10 01 24 PM Screenshot 2026-07-31 at 10 04 11 PM

Unit tests added in packages/core/test/ecgViewport_test.js covering:

  • Data load and display set binding
  • Channel visibility toggling
  • Scroll and time-range clamping
  • canvasToWorld / worldToCanvas round-trips
  • resetViewState

For manual testing, see the example app at packages/core/examples/genericEcg/index.ts.

Checklist

PR

  • My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • OS: macOS 15
  • Node version: v22.20.0
  • Browser: Chrome

Summary by CodeRabbit

  • New Features
    • Added configurable ECG layout presets (12x1, 6x2, 3x4, 3x4+1) with sweep speed, sensitivity, optional amplitude labels, and calibrated grid rendering.
    • Improved ECG example with a “Show/Hide All Traces” toggle and Left/Right arrow navigation.
    • Added waveform-capability detection for tool compatibility.
  • Bug Fixes
    • Improved ECG viewport scaling, coordinate mapping, channel/layout segmentation, and safer duration/window scrolling; ECG image data now includes calibration when available.
    • Enhanced ECG physical time unit handling for additional directions.
  • Documentation
    • Marked the legacy ECG viewport as deprecated and expanded ECG option docs.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ECG rendering now supports configurable sweep speed, sensitivity, amplitude labels, and multi-column lead layouts. Generic ECG viewports add scrolling, camera, image identity, calibration, and coordinate-mapping APIs. Examples, adapters, tests, annotation tools, metadata, and ECG measurement units are updated.

Changes

ECG rendering and viewport behavior

Layer / File(s) Summary
ECG presentation contracts
packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewportTypes.ts, packages/core/src/types/ECGViewportProperties.ts, packages/core/src/utilities/ECGUtilities.ts, packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewportLegacyAdapter.ts
ECG configuration and layout metadata now include calibration, amplitude labels, layout type, and per-segment geometry.
Calibrated multi-layout rendering
packages/core/src/utilities/ECGUtilities.ts, packages/core/src/RenderingEngine/GenericViewport/ECG/CanvasECGRenderPath.ts
Metrics, grids, traces, labels, and frame rendering now use configurable calibration and lead layouts.
Viewport navigation and coordinate mapping
packages/core/src/RenderingEngine/GenericViewport/ECG/ECGResolvedView.ts, packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewport.ts, packages/core/src/RenderingEngine/ECGViewport.ts
ECG viewports now support layout-aware coordinate conversion, scrolling, camera updates, image URI checks, calibration output, and guarded canvas sizing.
Examples, adapters, and viewport coverage
packages/core/examples/genericEcg/index.ts, packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewportLegacyAdapter.ts, packages/core/test/ecgViewport_test.js
The example adds trace visibility and keyboard scrolling; adapter properties and viewport tests cover both viewport implementations.

ECG tooling support

Layer / File(s) Summary
ECG annotation, capability, metadata, and measurement support
packages/tools/src/tools/annotation/UltrasoundDirectionalTool.ts, packages/tools/src/utilities/getCalibratedUnits.ts, packages/core/src/utilities/viewportCapabilities.ts, packages/core/src/utilities/index.ts, packages/metadata/src/utilities/metadataProvider/ecgFromInstance.ts
Generic waveform viewports are recognized by capability checks, ECG calibration metadata uses millisecond encoding, and ECG measurements report milliseconds.

Non-functional maintenance

Layer / File(s) Summary
Import and formatting maintenance
packages/core/src/RenderingEngine/GenericViewport/ECG/ecgProjectionSnapshot.ts, packages/core/src/RenderingEngine/GenericViewport/GenericViewport.ts, packages/tools/src/utilities/spatial/*
Imports, documentation placement, unused locals, type formatting, helper formatting, and equivalent test formatting are adjusted.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ECGViewport
  participant CanvasECGRenderPath
  participant ECGUtilities
  participant Canvas
  User->>ECGViewport: scroll or change ECG presentation
  ECGViewport->>CanvasECGRenderPath: request drawFrame
  CanvasECGRenderPath->>ECGUtilities: compute metrics and channel layouts
  ECGUtilities->>Canvas: draw calibrated grid, traces, and labels
Loading

Suggested reviewers: sedghi

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the ECG viewport enhancement and uses a concise feature-oriented format. The deprecated marker adds status context but does not obscure the main change.
Description check ✅ Passed The description includes the required context, changes, testing information, checklist, and tested environment. The testing section uses the equivalent heading “Verification & Testing.” However, the d…
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@wayfarer3130 wayfarer3130 changed the title feat(ecg): GenericViewport base class + ECG viewport refactor feat(ecg): ECG Next Viewport Enhancements Jul 27, 2026
@wayfarer3130

Copy link
Copy Markdown
Collaborator

In general it is easier to review things individually rather than in bulk.
Also, you should be adding things to the ECG adapter class, not the new ECG_NEXT viewport. You might want to enhance the flags that control which viewports get the "new" viewport so that it becomes possible to set the new viewport on a per-view basis. Then, set it up so that for the "ECG" view, the new viewport version gets used, or else have a way to specify the new viewport generically. That would then allow you to put things like scroll and the camera classes into the adapters version rather than into the new class itself. For ECG, I'm fine with just switching the default to be the new ECG_NEXT viewport, via an adapter/setting so that you only need fixes in the new viewport version. The old one should then be deprecated, although probably not removed.

@wayfarer3130

Copy link
Copy Markdown
Collaborator

For the physical units === -1, you need a NEW value for ms, not a change to the existing one - use -2 for ms if you want that, otherwise it is a breaking change.

@wayfarer3130

Copy link
Copy Markdown
Collaborator

scroll() has an incompatible signature
ECGViewport.ts:321 declares:

scroll(options?: { delta?: number }): void
Every other viewport that participates in scrolling — including the sibling generic families — takes a positional numeric delta:

StackViewport.ts:2858 → scroll(delta: number, debounce = true, loop = false)
VideoViewport.ts:410 → scroll(delta = 1, _debounceLoading = true, loop = false) (with an explicit "compatibility argument kept for stack-like callers" note)
PlanarViewport.ts:1408 → scroll(delta: number)
WSIViewport.ts:312 → scroll(delta: number)
Critically, the core scroll utility that OHIF's stack-scroll / mouse-wheel tools go through calls it positionally — scroll.ts:73-77:

(viewport as IStackViewport).scroll(delta, options.debounceLoading, options.loop);
So when OHIF wheel-scrolls an ECG viewport, options receives the number delta, options?.delta is undefined, and the code falls back to ?? 1. Result: every scroll tick advances one full window forward regardless of direction or magnitude — scrolling backward is impossible. This is exactly the "wrong for compatible usage" symptom you suspected.

Fix: match the family convention:

scroll(delta = 1, _debounceLoading = true, _loop = false): void {
// delta = number of viewport-widths to shift
}
Then update the example's arrow-key handler (examples/genericEcg/index.ts:33-35) from scroll({ delta: 0.25 }) to scroll(0.25). Keep scrollToTime(timeMs) and getDurationMs() as-is — those are legitimate ECG-specific additions and correctly placed.

The scroll needs to be compatible and should be in the adapter class

Comment thread packages/core/src/RenderingEngine/GenericViewport/GenericViewport.ts Outdated

type ECGViewportProperties = ViewportProperties & {
visibleChannels?: number[];
sweepSpeed?: number;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There should be some consideration for what is displayed as part of the viewport, and what is external. Also, things like the layout type should probably come from a set of constants which actually define the layouts so there is a full object definition allowing other layouts to be added. I'm not quite sure where that should get added/setup. Perhaps a register new layout on the base class as a static? Or perhaps just allow it inline.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

export interface ECGLayout {
  id: string;
  label: string;
  rows: number;
  columns: number;
  hasRhythmStrip?: boolean;
}

Then computeECGHeight, computeECGChannelLayouts, and computeECGRenderMetrics can look up the descriptor directly instead of using repeated if/else chains.

Two quick questions for you on this:

Does { id, label, rows, columns, hasRhythmStrip } cover everything needed, or are there other layout parameters you'd like included?
Would you prefer registering new layouts via a static method (e.g. ECGViewport.registerLayout(...)), or having ECG_LAYOUTS as a standalone object that callers can extend?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It seems like there is a lot of hard coded layout information here - wondering if it can be split and unit tests added for various types of calculations.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed! I plan to split the pure math and layout functions (computeECGHeight, computeECGChannelLayouts, and computeECGRenderMetrics). Since these calculation functions have no DOM or canvas dependencies, add full unit test coverage for all layout variants (12x1, 6x2, 3x4, 3x4+1).

Quick question on file organization: Is it okay if I extract these calculation functions into a separate ECGLayoutCalculations.ts file, or would you prefer keeping them inside ECGUtilities.ts alongside static layout constants?

I'll include this refactor and test suite in my follow-up PR right after your feedback!

Comment thread packages/tools/src/tools/annotation/UltrasoundDirectionalTool.ts Outdated
Comment thread packages/tools/src/tools/annotation/UltrasoundDirectionalTool.ts
@Harshika-Chandvani
Harshika-Chandvani marked this pull request as ready for review July 28, 2026 13:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (4)
packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewport.ts (1)

505-507: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the existing top-level type import instead of an inline import(...) type.

The file already has a import type { ... } from '../../../types' block (Lines 8-10); add IImageCalibration there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewport.ts` around
lines 505 - 507, Update the top-level type import in ECGViewport to include
IImageCalibration, then replace the inline import(...) type assertion for
waveform.calibration with the imported IImageCalibration type while preserving
the existing undefined union.
packages/core/src/RenderingEngine/GenericViewport/ECG/ECGResolvedView.ts (1)

186-197: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache the computed layouts like getCanvasMapping does.

getChannelLayouts() recomputes the full layout (channel iteration, row-height pass, and an O(rows × items) find in the offset loop) on every canvasToWorld/worldToCanvas call — these run per annotation handle per frame. The class already memoizes cachedCanvasMapping; the same ||= treatment applies here since the state is immutable per instance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/RenderingEngine/GenericViewport/ECG/ECGResolvedView.ts`
around lines 186 - 197, Cache the result of getChannelLayouts() using the same
||= memoization pattern as getCanvasMapping, storing it on an instance field so
repeated canvasToWorld/worldToCanvas calls reuse the computed layouts. Preserve
the existing computeECGChannelLayouts inputs and behavior.
packages/core/examples/genericEcg/index.ts (1)

186-193: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Guard the global keydown handler.

The listener is bound to window for the page lifetime and reacts to arrow keys even when focus is in a toolbar input, and it never calls preventDefault(), so the page also scrolls. Binding to element (with tabindex) or checking event.target would keep the demo behaviour predictable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/examples/genericEcg/index.ts` around lines 186 - 193, Update
the keyboard handler around the window keydown listener so arrow-key scrolling
only runs when the ECG demo element is focused or when the event target is
outside interactive controls such as inputs and the toolbar. Prevent the
browser’s default scrolling when handling ArrowLeft or ArrowRight, while
preserving the existing viewport.scroll behavior.
packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewportTypes.ts (1)

58-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the layout union into a shared type alias.

The same '12x1' | '6x2' | '3x4' | '3x4+1' literal union is repeated in ECGViewportProperties.ts, ECGUtilities.ts (three signatures), and ECGResolvedView.ts. A single exported ECGLayoutType (or the layout-descriptor map already discussed in the earlier review thread) would keep them in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewportTypes.ts`
around lines 58 - 65, Extract the repeated ECG layout literal union into an
exported shared type alias, preferably ECGLayoutType, and update the layoutType
declaration plus the corresponding usages in ECGViewportProperties, the three
ECGUtilities signatures, and ECGResolvedView to reference it. Preserve the
existing four supported layout values and use the shared alias consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/examples/genericEcg/index.ts`:
- Around line 176-182: Update the Show/Hide All Traces synchronization near
addCheckboxToToolbar to retain the checkbox references returned by the helper
for each channel, rather than querying `#cornerstone-element-container`. Iterate
over that stored collection when applying the allVisible checked state so every
per-channel checkbox stays synchronized.

In `@packages/core/src/RenderingEngine/GenericViewport/ECG/ECGResolvedView.ts`:
- Around line 48-67: Update the layout selection in ECGResolvedView to avoid
falling back to channelLayouts[0] when subCanvasPos is outside a row’s bounds.
If no row contains the point, select the channel layout with the smallest
vertical distance to the point, preserving normal containing-row selection and
ensuring gaps or out-of-bounds positions resolve to the nearest row.

In `@packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewport.ts`:
- Around line 304-322: Update ECGViewport.setCamera to handle
cameraPatch.focalPoint in addition to scale updates. Mirror the legacy ECG
mapping by converting the focal point between canvas/world coordinates,
calculating the change delta from the current focal point, and passing that
delta to setPan(...), while preserving existing parallelScale and scale
behavior.

In
`@packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewportLegacyAdapter.ts`:
- Around line 46-53: Update ECGViewportLegacyAdapter.setProperties so partial
updates do not overwrite unspecified presentation fields with undefined. Before
calling setDisplaySetPresentation, omit undefined entries from the properties
object or merge them with the current presentation, preserving existing values
while applying only supplied fields.

In `@packages/core/src/utilities/ECGUtilities.ts`:
- Around line 297-300: Update the 3x4+1 layout handling in ECGUtilities so the
rhythm strip selects Lead II by channel identity rather than assuming
visibleChannels[1]. Give its layout entry a unique rhythm identity that cannot
collide with regular leadIndex values, and update ECGResolvedView
lookup/conversion logic to consistently match that identity for rhythm
annotations.
- Around line 546-560: Update the amplitude-label rendering around the
calibration block so mV values are calculated relative to each lead row’s
baseline rather than the canvas top, preserving a clinically meaningful per-lead
±mV scale; use the existing layout baseline information for each row or render
one calibration pulse. Also derive the label font size through the same
world-to-canvas scaling used by drawECGLabels instead of using the fixed
world-unit value 10.
- Around line 211-237: Update the row-height calculation around the visible
channel loop and totalHeight accumulation to use the same shared row-height
helper as computeECGChannelLayouts, including its 100 * channelScale * 1.25
fallback for empty rows. Replace any duplicated empty-row logic with that helper
so ecgHeight and rowYOffsets remain consistent for partially populated layouts.
- Around line 484-509: Update the calibrated grid-spacing logic in the
surrounding ECG grid-generation function to account for worldToCanvasRatio and
prevent excessive line counts when effective spacing falls below a few screen
pixels. Skip minor lines and coarsen major spacing as needed, while preserving
the physical 1 mm/5 mm spacing when it remains sufficiently visible; keep the
legacy auto-fit branch unchanged.
- Around line 409-413: Update the channelScale calculation in ECGUtilities to
apply the waveform’s actual ADC-to-mV calibration before converting to pixels,
rather than assuming one raw sample unit equals 1 mV. Thread the channel
sensitivity and units from getImageData().calibration through the ECG rendering
path, including drawECGTraces, and use that calibration when computing
channelScale so waveform amplitudes and mV grid labels remain consistent.
- Around line 584-602: Update computeECGChannelLayouts where
defaultStart/defaultEnd are resolved so each layout’s startSample/endSample is
intersected with the visible startIndex/endIndex window rather than taking
precedence over it. Preserve layout bounds while clamping the effective range to
the scroll time window, including the existing channel-length limits and valid
sampleCount behavior.

In `@packages/tools/src/tools/annotation/UltrasoundDirectionalTool.ts`:
- Around line 238-241: Persist the text-box state in the `editData` object
created by the surrounding drag flow, using the existing `movingTextBox`
property that `_dragCallback` reads. Map the local `_movingTextBox` value set in
the handle-selection logic into that property so text-box drags retain the flag
and do not enter the all-points movement path.

In `@packages/tools/src/utilities/getCalibratedUnits.ts`:
- Around line 128-133: The ECG millisecond paths currently overwrite the legacy
-1 calibration marker. Introduce a distinct ECG-specific enum value, then use it
for the millisecond scale conversion at
packages/tools/src/utilities/getCalibratedUnits.ts:128-133 and for millisecond
probe values and units at
packages/tools/src/utilities/getCalibratedUnits.ts:210-219, preserving -1’s
existing behavior elsewhere.

---

Nitpick comments:
In `@packages/core/examples/genericEcg/index.ts`:
- Around line 186-193: Update the keyboard handler around the window keydown
listener so arrow-key scrolling only runs when the ECG demo element is focused
or when the event target is outside interactive controls such as inputs and the
toolbar. Prevent the browser’s default scrolling when handling ArrowLeft or
ArrowRight, while preserving the existing viewport.scroll behavior.

In `@packages/core/src/RenderingEngine/GenericViewport/ECG/ECGResolvedView.ts`:
- Around line 186-197: Cache the result of getChannelLayouts() using the same
||= memoization pattern as getCanvasMapping, storing it on an instance field so
repeated canvasToWorld/worldToCanvas calls reuse the computed layouts. Preserve
the existing computeECGChannelLayouts inputs and behavior.

In `@packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewport.ts`:
- Around line 505-507: Update the top-level type import in ECGViewport to
include IImageCalibration, then replace the inline import(...) type assertion
for waveform.calibration with the imported IImageCalibration type while
preserving the existing undefined union.

In `@packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewportTypes.ts`:
- Around line 58-65: Extract the repeated ECG layout literal union into an
exported shared type alias, preferably ECGLayoutType, and update the layoutType
declaration plus the corresponding usages in ECGViewportProperties, the three
ECGUtilities signatures, and ECGResolvedView to reference it. Preserve the
existing four supported layout values and use the shared alias consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3738e52a-d63f-406e-97fd-fe5e5603b122

📥 Commits

Reviewing files that changed from the base of the PR and between 0b601d8 and f5591bd.

📒 Files selected for processing (17)
  • packages/core/examples/genericEcg/index.ts
  • packages/core/src/RenderingEngine/ECGViewport.ts
  • packages/core/src/RenderingEngine/GenericViewport/ECG/CanvasECGRenderPath.ts
  • packages/core/src/RenderingEngine/GenericViewport/ECG/ECGResolvedView.ts
  • packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewport.ts
  • packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewportLegacyAdapter.ts
  • packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewportTypes.ts
  • packages/core/src/RenderingEngine/GenericViewport/ECG/ecgProjectionSnapshot.ts
  • packages/core/src/RenderingEngine/GenericViewport/GenericViewport.ts
  • packages/core/src/types/ECGViewportProperties.ts
  • packages/core/src/utilities/ECGUtilities.ts
  • packages/core/test/ecgViewport_test.js
  • packages/tools/src/tools/annotation/UltrasoundDirectionalTool.ts
  • packages/tools/src/utilities/getCalibratedUnits.ts
  • packages/tools/src/utilities/spatial/areViewportsSpatiallyLinked.ts
  • packages/tools/src/utilities/spatial/spatial.spec.ts
  • packages/tools/src/utilities/spatial/types.ts
💤 Files with no reviewable changes (1)
  • packages/core/src/RenderingEngine/GenericViewport/ECG/ecgProjectionSnapshot.ts

Comment thread packages/core/examples/genericEcg/index.ts
Comment thread packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewport.ts Outdated
Comment thread packages/core/src/RenderingEngine/GenericViewport/ECG/ECGViewportLegacyAdapter.ts Outdated
Comment thread packages/core/src/utilities/ECGUtilities.ts Outdated
Comment thread packages/core/src/utilities/ECGUtilities.ts Outdated
Comment thread packages/core/src/utilities/ECGUtilities.ts
Comment thread packages/core/src/utilities/ECGUtilities.ts
Comment thread packages/tools/src/tools/annotation/UltrasoundDirectionalTool.ts Outdated
Comment thread packages/tools/src/utilities/getCalibratedUnits.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/tools/src/utilities/getCalibratedUnits.ts`:
- Around line 244-247: Update the calibrationType assignment in
getCalibratedUnits to classify both -1 and -2 physicalUnitsYDirection values as
ECG regions, while preserving the existing US Region classification for other
values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2f559bf0-1e7c-4375-8241-93f6b41a1e72

📥 Commits

Reviewing files that changed from the base of the PR and between f5591bd and 9640fff.

📒 Files selected for processing (5)
  • packages/core/src/utilities/index.ts
  • packages/core/src/utilities/viewportCapabilities.ts
  • packages/metadata/src/utilities/metadataProvider/ecgFromInstance.ts
  • packages/tools/src/tools/annotation/UltrasoundDirectionalTool.ts
  • packages/tools/src/utilities/getCalibratedUnits.ts

Comment thread packages/tools/src/utilities/getCalibratedUnits.ts Outdated
Comment thread packages/core/examples/genericEcg/index.ts
@Harshika-Chandvani

Copy link
Copy Markdown
Author

Hi @wayfarer3130,

Following our discussion during OHIF Office Hours on the Modular Waveform Viewport Architecture, we are transitioning the changes from this PR (#2821) into a series of smaller, focused, and review-friendly PRs:

📦 Modular PR Roadmap:
PR 1 (Core Primitive Viewport): PR #2899 (Opened & ready for review)
Primitive ECGViewport extending GenericViewport as an untouched abstract base.
Pure graphic rendering (waveform traces + calibration grid lines only, zero text labels).
Normalized AABB2 traceRegions support for dynamic layouts (12x1, 6x2, 3x4+1, 15-lead).
Discrete 3D world coordinate mapping ($Z = \text{lead/region index}$) for tool isolation.
PR 2 (Cornerstone Tools & Consolidated Example): (Next)
WaveformRegionOverlayTool in @cornerstonejs/tools (rendering lead labels like "I", "aVR", "V1-V6" and region bounding boxes).
Physical unit measurement calibration (ms for time, mV for voltage).
Example Consolidation: In response to Bill's feedback, we will directly replace packages/core/examples/ecg with the updated GenericViewport + overlay tooling implementation (including DICOMweb/local loading, lead toggles, and annotation tools) and remove the duplicate genericEcg example.
PR 3 (OHIF Viewers Integration):
ECG Hanging Protocols driven by displaySetOptions.
External layout generators (12x1, 6x2, 3x4+1) and clinical analytics (HR, QRS, PR, QT).
Dedicated OHIF ECG Mode (ohif-ecg-mode).
We will close / supersede PR #2821 in favor of these modular PRs so each layer can be reviewed and merged cleanly.

Please check out #2899 when you get a moment. Thank you!

wayfarer3130 and others added 4 commits September 18, 2026 17:03
`ECGResolvedView.worldToCanvas` looked a layout cell up with
`find(item => item.leadIndex === z)`. In the `3x4+1` layout the grid cell
for lead II and the rhythm strip both carried `leadIndex === 1`, so the
lookup always returned the grid cell and an annotation on the rhythm strip
moved to the wrong cell. `leadIndex` was also an index into the *filtered*
visible-channel list, so hiding one lead renumbered every lead after it.

`leadIndex` now identifies one layout cell, and it is stable:

- A grid cell carries the index of its channel in the unfiltered channel
  list, so the identifier survives a change of the visible leads or of the
  layout. `getVisibleECGChannelEntries` returns those indices, and both
  callers (`CanvasECGRenderPath`, `ECGResolvedView`) pass them through the
  new `leadIndices` argument.
- The `3x4+1` rhythm strip carries a synthetic identifier above the last
  channel index, and it sets `isRhythm`. `getImageData` reserves that index
  in `dimensions[2]`, because `indexWithinDimensions` otherwise rejects a
  handle placed on the rhythm strip.

Two related defects go with it:

- `computeECGChannelLayouts` dropped every channel past the nominal grid
  size, so a 15-lead ECG lost three traces in the `12x1` layout. The grid
  now grows above the nominal size, and the column count drives the time
  segmentation instead of a hard-coded 2 or 4.
- `computeECGHeight` and `computeECGChannelLayouts` chose the rhythm lead
  by different rules (`visibleChannels[1]` against a name match on `ii`),
  so the reserved height disagreed with the drawn layout. Both now derive
  from one `computeECGLayoutGrid` helper, and the shared rule uses `\bii\b`
  so lead III no longer matches.

`ECGResolvedView` caches the layout, which it previously recomputed on
every `canvasToWorld` and `worldToCanvas` call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nits

Nine defects from the review of this pull request. Each one is independent of
the others, except that the amplitude scale and the measurement units both need
the channel sensitivity, which nothing read before.

Amplitude scale. `computeECGRenderMetrics` set the calibrated `channelScale` to
`sensitivityMmMv * ECG_PX_PER_MM`, which is pixels for each millivolt, and it
applied that value to raw sample units. The scale was too large by the
reciprocal of the millivolts for each unit, which is 1000 for a typical ECG: a
`sensitivityMmMv` of 10 gave an `ecgHeight` of about 2.3 million pixels, the
traces collapsed to a hairline, and the major-grid loop ran about 120 000 times
for each frame. `ecgFromInstance` now reads `ChannelSensitivity`,
`ChannelSensitivityCorrectionFactor` and `ChannelSensitivityUnitsSequence`, and
it reports the millivolts for each raw unit as `mvPerUnit`. `loadECGWaveform`
carries the value on each channel, and the scale uses it. The default stays
0.001 mV for each unit when the instance omits the sensitivity.

Grid. `drawECGGrid` placed the vertical lines from the deprecated
`ECG_SECONDS_WIDTH` (150 px/s) while `ecgWidth` came from
`sweepSpeed * ECG_PX_PER_MM` (94.475 px/s at 25 mm/s), so a major block covered
0.317 s in place of 0.2 s. The metrics now report `pxPerSecond` and the resolved
`sweepSpeed`, and one formula serves the calibrated path and the auto-fit path.
The deprecated `RenderingEngine/ECGViewport` keeps its previous spacing, because
the formula reproduces it exactly from its own pixels for each second.

Traces. `drawECGTraces` lost its clamp against `channel.data.length` when the
layout started to set `endSample` for every cell. A channel array shorter than
`numberOfSamples` yielded `undefined` samples and `ctx.lineTo(x, NaN)`, which
blanked the trace. The clamp is explicit again, and a cell whose window falls
past the end of the data still draws its baseline.

Camera. `ECGViewport.setCamera` added a world-space difference straight to a pan
in canvas pixels, and the vertical direction was inverted because `buildICamera`
sets `viewUp: [0, -1, 0]`. The focal point now goes through `worldToCanvas`, as
`WSIViewportLegacyAdapter` does.

Measurement units. `ecgCalibrationProvider` wrote `physicalUnitsYDirection: -2`
to mean "the X axis is in milliseconds", so X-axis information sat in a Y-axis
field. `getCalibratedUnits` then read `UNIT_MAPPING[-2]` for the area unit and
reported `ms² ECG Region`. An ECG region now writes -1 for millivolts, the X
axis keeps the DICOM code for seconds, and the display layer converts to
milliseconds. The area unit reports `ms·mV`, because the area of an ECG region
is a time multiplied by an amplitude and not a squared quantity. A region stored
with -2 still reads.

The same branch decided the measured axis by comparing a time in seconds against
an amplitude in millivolts, which have no common unit, and it then wrote
`scale = scaleY`, which made `calculateLengthInIndex` apply the amplitude scale
to the X component. The axis now comes from the fraction of each axis that the
annotation spans, and the two scales stay on their own axes.

Viewport guard. `UltrasoundDirectionalTool` replaced `instanceof StackViewport`
with `viewportSupportsImageSlices`, but the legacy `VolumeViewport` exposes all
four of those methods, so the tool accepted a volume viewport. The guard now
asks `getCurrentMode` for a generic viewport, and falls back to the volume
capability for a legacy viewport, which `StackViewport` does not carry.

Image identity. `ECGViewport.hasImageURI` used `dataId.includes(imageURI)`,
which matched a UID that is a prefix of the bound UID. It compares whole
identifiers now.

Hit test. The nearest-cell fall-back of `ECGResolvedView.canvasToWorld` measured
the vertical distance alone, so a point outside the grid in a multi-column
layout always mapped to column 0. It measures the distance to the rectangle of
the cell in both axes.

Tests: 4 new files, 33 tests. The core, metadata and tools suites pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Design items D2, D4 and D7 of the review.

D2. `ECGResolvedView` took the render metrics as a constructor argument, and
`ECGViewport.getResolvedView` read them from `binding.rendering`, where
`drawFrame` had written them during the previous draw. A transform was therefore
one frame stale, and before the first draw it used the placeholder metrics of
`{ecgWidth: 1, ecgHeight: 1, channelScale: 1}`. The render path owned the world
geometry, which inverts the ownership that the view contract states: a resolved
view "is an ephemeral snapshot produced from data, canvas geometry, and
ViewState; it owns world/canvas transforms and renderer geometry".

`ECGResolvedView` now computes the metrics, the channel layouts and the canvas
transform from the data, the canvas and the view state, and it caches each one.
`ECGCanvasRenderContext` gains `getResolvedView`, so `drawFrame` reads all three
in place of computing them. The viewport therefore keeps one copy of the view
state and of the presentation, and the transform that a tool uses is always the
geometry that the frame drew.

`ECGCanvasRendering` no longer holds `metrics`, `currentCamera` or
`currentDataPresentation`, so `applyViewState` and `updateDataPresentation` have
nothing to store: the canvas redraws in full for each frame.

D4. `ChannelLayout` in `ECGViewportTypes` was dead, and it declared 4 of the 12
fields that `ECGChannelLayout` declares. It is deleted. `RenderWindowMetrics`
was a structural copy of `ECGRenderMetrics`, and `CanvasECGRenderPath` cast
between the two, which hid any future drift. It is now an alias.

D7. `getCamera` and `setCamera` are the legacy `ICamera` surface, and the Video,
WSI and Planar families all keep them on their legacy adapter. They move to
`ECGViewportLegacyAdapter`, together with the private `setFocalPoint`.
`ViewportType.ECG` resolves to that adapter, so an application that asks for a
legacy ECG viewport still finds both methods. A direct `ECG_NEXT` viewport now
exposes `getZoom`, `setZoom`, `getPan`, `setPan` and `getViewState` only, which
matches `WHOLE_SLIDE_NEXT`. `getRotation` stays on the core class, as it does on
WSI and Planar.

One defect fell out of the change. `getSignalScale` in `ecgProjectionSnapshot`
measured the sampling density between canvas `[0, 0]` and `[1, 0]`.
`canvasToWorld` clamps a sample index to the signal, and the ECG content is
centred, so the canvas origin normally lies outside the drawn content: both
samples clamped to sample 0 and the projection reported a density of 0. The
finite difference now starts at the centre of the canvas. The stub metrics in
`viewportProjectionService.jest.js` had masked this, and that test now derives
its expectations from the resolved view.

Tests: `packages/core/test/ecgResolvedView.jest.js`, 15 tests. They prove that
the metrics are real with no draw, that they follow a change of the time window,
of the sensitivity, of the sweep speed and of the layout, and that the transform
round trips through every cell of the `3x4+1` layout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wayfarer3130 wayfarer3130 changed the title feat(ecg): ECG Next Viewport Enhancements [DEPRECATED] feat(ecg): ECG Next Viewport Enhancements Sep 21, 2026
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