high priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

ActivityTypeDonutChart accepts: data (List<ActivityTypeCount>), isLoading (bool), error (String?), and onSegmentTapped (void Function(String activityTypeId, int count, double percentage)?) parameters
When isLoading is true, a circular ShimmerSkeleton placeholder sized to the chart container is displayed
When data is empty, an EmptyState widget with text 'No activity types recorded for this period' is displayed and the legend is hidden
When error is non-null, an ErrorCard with retry button is displayed
Every donut segment has a Semantics widget with label: '{activityType}, {percentage}%, {count} activities' and excludeSemantics: true on the fl_chart PieChart node
The legend below the chart lists all activity types with their color swatch, label, and percentage; each legend row is also semantics-labeled: '{activityType}: {percentage}%'
Tapping a segment highlights it (radius +8 dp) and triggers onSegmentTapped with correct activityTypeId, count, and percentage (rounded to 1 decimal)
The donut chart is square, sized to min(availableWidth, 240 dp) on mobile and min(availableWidth * 0.45, 320 dp) on tablet using LayoutBuilder
All segment colors and text meet WCAG 2.2 AA contrast ratios (3:1 for large graphical elements, 4.5:1 for text labels)
The widget re-renders correctly when data changes (e.g., period switch) — old segments animate out and new segments animate in within 500 ms
No direct fl_chart imports exist in this widget file — all chart logic is delegated to the DonutChartAdapter

Technical Requirements

frameworks
Flutter
fl_chart (via adapter)
Riverpod or BLoC
data models
ActivityTypeCount
StatsPeriod
performance requirements
Widget mounts and first-paints within one frame after receiving non-empty data
Legend renders up to 10 rows without vertical overflow on a 375 dp wide screen
security requirements
No activity type labels contain user PII
Color tokens validated at adapter layer — widget trusts adapter output
ui components
ShimmerSkeleton (circular variant)
EmptyState widget
ErrorCard with retry
DonutLegendRow (from adapter layer)
Stack with center total label overlay

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Place the widget in `lib/features/statistics/widgets/activity_type_donut_chart.dart`. Use a Column layout: [PieChart (in SizedBox), SizedBox(height: 16), DonutLegendList]. Wrap the PieChart in a Stack to overlay the center total label — use Align(alignment: Alignment.center) with a IgnorePointer widget so the center label does not intercept touch events meant for the chart. For Semantics on segments, use the same overlay technique as MonthlyActivityBarChart: a post-frame callback reads each segment's bounding box and renders invisible Semantics widgets in a Stack above the chart.

For the percentage rounding, use `(count / total * 1000).round() / 10` to get one decimal place; ensure the sum of rounded percentages adjusts the largest segment to make total exactly 100%. On data change (key the PieChart on a hash of the data list) to trigger fl_chart's built-in animation. Confirm the DonutChartAdapter is provided via the same DI mechanism used across the feature to maintain consistency.

Testing Requirements

Widget tests (flutter_test): (1) Skeleton displayed when isLoading=true, PieChart absent. (2) EmptyState displayed when data=[]. (3) ErrorCard displayed when error non-null. (4) With 3 activity types, legend contains 3 rows with correct labels — verified via find.text().

(5) Tapping segment index 0 calls onSegmentTapped with correct activityTypeId. (6) Semantics tree includes labels for all segments and legend rows — use SemanticsController.simulateSemanticsAction. Golden tests for loading, empty, error, and 4-segment populated states at 375 px width. Accessibility audit: flutter_test's AccessibilityGuideline for minimum tap target size on each segment and legend row.

Component
Activity Type Donut Chart
ui medium
Epic Risks (4)
medium impact medium prob technical

PeerMentorStatsList must handle rosters of 40+ mentors efficiently. A naive ListView implementation that re-renders all rows on filter change may cause frame drops on mid-range devices, degrading the experience for coordinators managing large chapters.

Mitigation & Contingency

Mitigation: Use ListView.builder with const constructors for row widgets. Profile the list with Flutter DevTools on a release build against a 60-mentor dataset before submitting for review. Implement sort in the BLoC layer, not in the widget.

Contingency: If frame drops persist, introduce pagination (load 20 mentors, scroll to load more) and add a search filter to reduce visible row count in practice.

medium impact low prob integration

The ActivityTypeDonutChart must render org-configured activity type labels from the org-labels system. If the terminology provider is not available or returns stale labels, chart segments will display raw key strings instead of human-readable organisation-specific names, confusing coordinators.

Mitigation & Contingency

Mitigation: Always resolve labels through the OrganizationLabelsProvider before passing data to the donut chart widget. Implement a fallback that formats the raw key as a readable string (e.g., 'peer_support' → 'Peer Support') if the provider returns null.

Contingency: If the org-labels system is unavailable, display the formatted fallback label and log a warning. Do not block chart rendering on label resolution — render with fallbacks immediately.

high impact high prob technical

fl_chart widgets do not natively expose semantic labels for individual bars and donut segments. Without explicit Semantics wrappers, VoiceOver and TalkBack users will receive no meaningful chart information, failing accessibility requirements critical for Blindeforbundet and NHF deployments.

Mitigation & Contingency

Mitigation: Wrap each fl_chart widget in a Semantics widget with a descriptive label summarising the chart data. Implement the data table fallback toggle from the start, not as an afterthought. Validate with VoiceOver on iOS and TalkBack on Android during development.

Contingency: If fl_chart's rendering pipeline prevents semantic overlay from working reliably, replace chart widgets with a custom Canvas-based implementation that has full semantic control, or use a different charting library with better accessibility support.

medium impact medium prob technical

The period filter state must be preserved when navigating away from the dashboard (e.g., drilling into a mentor's detail screen and pressing back). If the BLoC is re-created on navigation, the filter resets to default, forcing coordinators to re-select their period every time they drill down, degrading operational workflow efficiency.

Mitigation & Contingency

Mitigation: Provide the StatsBloc at a route level above the dashboard screen using a BlocProvider scoped to the statistics navigation shell, not inside the screen widget itself. Verify persistence with a widget integration test that navigates away and back.

Contingency: If route-level scoping is not achievable within the current navigation architecture, persist the last-used filter to a local session store and restore it on screen re-entry.