Refactor Stage Overview Charts¶
Context¶
The stage overview page displays pie charts (screening/annotation progress), area charts (progress over time, currently disabled), and leaderboard tables (per-reviewer stats). The code has grown organically and is hard to maintain:
project-screening-stats.selectors.tsis 1200+ lines mixing 15 interfaces, 20+ pure functions, and only 6 actual ngrx selectors- Generic chart interfaces (
IPieSeries,IAreaSeries) are defined in a screening-specific file but imported everywhere - Components use outdated
BehaviorSubject+ngOnChanges+combineLatestpatterns instead of Angular signals MemberProgressComponenthas an emptyComponentStorethat does nothing- Dead code: all area chart functions return
[]/null, investigator history selectors returnnull, commented-out code blocks IAnnotationMembershipDatais defined twice identically- 70+ lines of inline HTML string-building in the annotation pie formatter
- Colors hardcoded across ~6 builder functions with no central palette
- Leaderboard columns defined as top-level constants with no connection to their domain
- Duplicated SCSS across pie/area/leaderboard container styles
Additionally, the annotation pie chart hardcodes a target of 2 sessions (getAnnotationPieData maps keys ['0'], ['1'], ['2'] as specific states and lumps >2 into "over annotated"). But each stage has a configurable sessionCountTarget (set via review-settings UI, stored on the stage entity, defaulting to 2). When the target is 1 or 3+, the pie chart should reflect that — e.g. with target=1, "complete" means 1/1, and 2+ is "over annotated". Screening already handles this correctly via getScreeningPieDataForAgreementMode() which branches by agreement mode.
The goal is to make this code maintainable and logically organised, make the annotation pie chart respect the configured session target, then document how historical time-series data could be added once the backend supports it.
Status Summary¶
| Step | Description | Status |
|---|---|---|
| 1 | Extract shared chart interfaces | ✅ Done |
| 2 | Centralise chart color palette | ✅ Done |
| 3 | Split screening selectors file | ✅ Done |
| 4 | Split annotation selectors | ✅ Done |
| 5 | Annotation pie respects sessionCountTarget |
✅ Done |
| 6 | Verify screening agreement modes | ✅ Code verified, tests added |
| 7 | Clean up membership/leaderboard selectors | ✅ Done |
| 8 | Remove dead code across components | ✅ Done |
| 9 | Extract annotation pie HTML formatter | ✅ Done |
| 10 | Modernise component reactive patterns (signals) | ⏳ Not started — deferred to follow-up PR |
| 11 | Extract shared Highcharts config | ✅ Done |
| 12 | Deduplicate SCSS | ✅ Done |
| 13 | Add tests for extracted pure functions | ✅ 5/5 spec files done |
Blocking merge: SonarCloud quality gate — coverage 35.2% (needs 80%), duplication 6.2% (needs ≤3%) Resolved. Comprehensive test coverage added (10 spec files), duplication reduced by extracting helpers.
Bug fixes from PR review (2026-03-28)¶
- Removed duplicate "Complete" detail slice in annotation pie series (flagged by Sentry, Codex, Copilot)
- Fixed
inProgressTiedfilter usinginclusion === 1instead of=== 0in custom screening pie - Fixed invalid CSS in pie formatter divider (
margin 0 10px→margin: 0 10px) - Restored explicit border color (
#e2dfdf) andmargin-bottomin shared SCSS mixin - Used
String()for tally count indexing to satisfy strict TS typing - Removed unreachable guard and unused
partialCompletedCountsaccumulator
Part 1: Refactoring¶
Step 1 — Extract shared chart interfaces ✅¶
Created src/services/web/src/app/shared/charts/chart-series.models.ts
Moved from project-screening-stats.selectors.ts:
- SessionStates, IPieSeriesData, IPieSeries, IAreaSeriesData, IAreaSeries
Updated imports in all 6 consumer files. Original file re-exports for backward compatibility.
Step 2 — Centralise chart color palette ✅¶
Created src/services/web/src/app/shared/charts/chart-colors.ts
Extracted all ~40 hardcoded hex color values from pie/area series builders into named constants:
- SCREENING_COLORS — grouped by agreement mode variant
- ANNOTATION_COLORS — grouped by session state
- SCREENING_AREA_COLORS — for screening time-series charts
- ANNOTATION_AREA_COLORS — for annotation time-series charts
Step 3 — Split the 1200-line screening selectors file ✅¶
Target directory: src/services/web/src/app/core/state/entities/project-screening-stats/
| New file | Contents |
|---|---|
screening-stats.models.ts |
All screening-specific interfaces |
screening-agreement.ts |
DefinedScreeningAgreementMode, toDefinedScreeningAgreementMode(), AgreementInfo, convertToThreshold(), getStudyAgreementRatio() |
screening-pie-series.builders.ts |
All pie data computation and series builder functions |
screening-area-series.builders.ts |
All area series stubs + builders — kept as clean stubs |
project-screening-stats.selectors.ts |
Only ngrx createSelector() calls (~60 lines) |
Step 4 — Split the annotation selectors similarly ✅¶
Target directory: src/services/web/src/app/core/state/entities/stage-annotation-stats/
| New file | Contents |
|---|---|
annotation-stats.models.ts |
IAnnotationTallyCount, IAnnotationPieData, IAnnotationHistoryCount, IAnnotationAreaData |
annotation-pie-series.builders.ts |
getAnnotationPieData(), getAnnotationPieSeries() |
annotation-area-series.builders.ts |
getAnnotationAreaData(), getAnnotationAreaSeries() — real history-to-series transforms, ready once the selector receives backend history data |
stage-annotation-stats.selectors.ts |
Only ngrx selectors |
Step 5 — Make annotation pie chart respect sessionCountTarget ✅¶
Problem: getAnnotationPieData() hardcoded a target of 2 sessions. But each stage has a configurable sessionCountTarget (1, 2, 3+).
Fix: Refactored getAnnotationPieData() and getAnnotationPieSeries() to accept sessionCountTarget: number and dynamically compute slices. The selector now reads sessionCountTarget from the stage entity via selectSessionCountTargetForCurrentStage.
Step 6 — Verify screening already respects agreement mode ✅¶
Screening already branches on agreement mode via getScreeningPieDataForAgreementMode() (Single/ManualDual/AutomatedDual/Custom). Verified working correctly. Tests for all four agreement mode dispatch paths added in screening-pie-series.builders.spec.ts.
Step 7 — Clean up membership/leaderboard selectors ✅¶
- Removed duplicate
IAnnotationMembershipDatainterface - Removed
getAnnotationInvestigatorAreaSeries()andgetScreeningInvestigatorAreaSeries()(returned empty arrays) - Removed
dateFormat()(unused utility) - Removed commented-out
recordDatereferences
Step 8 — Remove dead code across components ✅¶
- Deleted empty
MemberProgressStore extends ComponentStore<{}>and replaced withtakeUntilDestroyedpattern - Removed commented-out settings buttons from all templates
- Removed commented-out investigatorName link from leaderboard template
- Removed unused
lodash-esforEachimport from annotation selectors
Step 9 — Extract annotation pie HTML formatter ✅¶
Created src/services/web/src/app/stage/stage-overview/current-progress-pie/annotation-pie-formatter.ts
Extracted formatAnnotationPieDataLabel, formatAnnotationPieTooltip, and formatScreeningPieDataLabel as standalone pure functions.
Step 10 — Modernise component reactive patterns ⏳¶
Status: Not started. Components still use @Input() decorators, BehaviorSubject, ngOnChanges, and combineLatest. Deferred to a follow-up PR to keep this one focused on extraction and the sessionCountTarget fix.
Convert all 4 chart components from BehaviorSubject + OnChanges + combineLatest to Angular signals:
current-progress-pie.component.ts:
- Convert @Input() chartData → chartData = input<IPieSeries[]>([])
- Convert @Input() showDecision → showDecision = input<boolean | null>(null)
- Convert @Input() stageReviewMode → stageReviewMode = input<StageReviewMode>()
- Replace chartOptions$ observable with chartOptions = computed(() => ...) calling the synchronous getChartOptions() (which was always synchronous logic wrapped in new Observable)
- Remove BehaviorSubject instances, ngOnChanges, ngOnInit
- Update template: [options]="chartOptions()" instead of chartOptions$ | async
time-progress.component.ts:
- Same pattern: signal inputs + computed() for chart options
- Remove BehaviorSubjects, ngOnChanges
member-progress.component.ts:
- Convert @Input() leaderboardData setter → leaderboardData = input<MembershipData[]>([])
- Use effect() to sync leaderboardData() → datasource.data instead of the BehaviorSubject + subscription
- Convert other @Input()s to signal inputs
member-time-progress.component.ts:
- Use inject() for Store and MAT_DIALOG_DATA
- Simplify ngOnInit — the switchMap + of() wrapping synchronous logic can be map()
- Consider converting to signal-based with toSignal()
Step 11 — Extract shared Highcharts config ✅¶
Created src/services/web/src/app/shared/charts/highcharts-defaults.ts
Extracted duplicated config from time-progress.component.ts and member-time-progress.component.ts:
- AREA_CHART_X_AXIS: datetime axis config
- AREA_CHART_PERCENT_PLOT_OPTIONS / AREA_CHART_VALUE_PLOT_OPTIONS: stacking area config (percent and value variants)
- AREA_CHART_Y_AXIS_PERCENT / AREA_CHART_Y_AXIS_VALUE: y-axis config
- AREA_CHART_TOOLTIP / MEMBER_AREA_CHART_TOOLTIP: shared tooltip formats
- NO_CREDITS: { credits: { enabled: false } }
Step 12 — Deduplicate SCSS ✅¶
The .option-container, .progress-label, .settings-label styles were copy-pasted across current-progress-pie.component.scss, time-progress.component.scss, and member-progress.component.scss.
Created src/services/web/src/app/stage/stage-overview/_stage-overview-shared.scss with a stage-overview-container mixin. All three component SCSS files now @use the shared mixin.
Step 13 — Add tests for extracted pure functions ⏳¶
Create spec files for each new builder/utility file:
| Test file | What it tests | Status |
|---|---|---|
screening-agreement.spec.ts |
Mode conversion, agreement ratio calculation, isCompleted boundary cases |
✅ Done (13 tests) |
annotation-pie-series.builders.spec.ts |
Annotation tally → pie data conversion for sessionCountTarget=1, 2, 3; verify "over annotated" bucketing changes with target | ✅ Done (10 tests) |
screening-pie-series.builders.spec.ts |
Each agreement mode's pie series output, color values, data filtering | ✅ Done (47+97 tests) |
annotation-pie-formatter.spec.ts |
HTML output for each annotation state label, tooltip for different point types | ✅ Done (16+188 tests) |
chart-colors.spec.ts |
Snapshot test — ensures colors don't accidentally change | ✅ Done (6 tests) |
Note: All spec files complete. Additional spec files added in SonarCloud fix commit: screening-area-series.builders.spec.ts (426 tests), annotation-area-series.builders.spec.ts (238 tests), project-screening-stats.selectors.spec.ts (109 tests), stage-annotation-stats.selectors.spec.ts (46 tests), highcharts-defaults.spec.ts (143 tests).
Part 2: Historical Time-Series Data¶
The historical area charts (progress over time) are part of the broader MongoDB Stats Pre-calculation initiative. The full technical design — including the pmProjectStats collection, sparse daily snapshots, delta value objects, stats audit job, and frontend fill-forward logic — lives in:
The initiative is tracked as Epic #1831, with user-facing context in Discussion #1822.
How Part 1 connects to Part 2¶
The refactoring in Part 1 creates the frontend extension points that Part 2 will consume. In the pre-calculation plan, live current-stats SignalR wiring is Step 13, while historical API wiring and area-chart activation are Step 15.
| Extension point | Current state | Wired when |
|---|---|---|
screening-area-series.builders.ts |
Placeholder history helpers; selector path still returns null |
Step 15 |
annotation-area-series.builders.ts |
History transforms implemented; selector path still returns null |
Step 15 |
selectScreeningHistoryCountsForCurrentProject |
Returns null |
Step 15 |
selectAnnotationHistoryCountsForCurrentProject |
Returns null |
Step 15 |
showStageOverviewAreaCharts feature flag |
Defaults to false and gates area-chart rendering |
Step 15 |
MemberTimeProgressComponent |
Wired to membership history selectors that currently return null |
Step 15 |
Separately, live current-stats notifications are a Step 13 concern: signal-r.service.ts already handles ProjectStatsNotification and defines project-stats subscribe helpers, but the current-project effect still needs to invoke those helpers.
No code changes are needed in this PR for Part 2. The extension points are the contract.
Verification¶
cd src/services/web && pnpm exec ng test --no-watch— all tests passcd src/services/web && pnpm exec ng build— production build succeedscd src/services/web && pnpm exec ng lint— no lint errors- After each step, verify the above before proceeding to the next
- Visual check: pie charts render identically for default target=2 (same colors, labels, tooltips), leaderboard tables sort and paginate correctly, area charts remain hidden behind feature flag
- Annotation pie correctness: with
sessionCountTarget=1, studies with 2+ sessions show as "over annotated"; withsessionCountTarget=3, studies with 2 complete sessions show as "in progress" - Screening pie correctness: verify each agreement mode (Single, ManualDual, AutomatedDual, Custom) produces correct slice categories