high priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

MonthlyActivityBarChart accepts: data (List<MonthlyActivityData>), isLoading (bool), error (String?), selectedPeriod (StatsPeriod), and onBarTapped (void Function(String month, String? activityType, int count)?) parameters
When isLoading is true, a ShimmerSkeleton placeholder matching the chart's bounding box dimensions is displayed instead of the chart
When error is non-null, an error card with a retry button is displayed; the retry button emits an onRetry callback
When data is empty for the selected period, an empty-state illustration with the text 'No activities recorded for this period' is shown
Every bar rod is wrapped in a Semantics widget with label: '{month}, {activityType}: {count} activities' (English) and excludeSemantics: true on the underlying chart node
A toggle button labeled 'Show as table' / 'Show as chart' (screen-reader-announced as 'Toggle data view') switches between the fl_chart bar chart and an accessible DataTable with columns: Month, Activity Type, Count
The DataTable view is keyboard and switch-access navigable; focus order follows reading order (left to right, top to bottom)
Chart height is fixed at 240 dp on mobile, 320 dp on tablet (responsive via LayoutBuilder); bars do not overflow their bounding box at any screen width from 320 dp to 1024 dp
Tapping a bar invokes onBarTapped with the correct month, activityType (null if unfiltered single-bar mode), and count
All text in the chart (axis labels, legend) meets WCAG 2.2 AA contrast ratio of at least 4.5:1 against the background using design tokens
Widget rebuilds only when data, isLoading, error, or selectedPeriod change — no spurious repaints on parent rebuilds (use const constructors and equatable data classes)

Technical Requirements

frameworks
Flutter
fl_chart
flutter_riverpod or flutter_bloc
flutter_test
data models
MonthlyActivityData
StatsPeriod
ActivityTypeSummary
performance requirements
First paint of chart with 12 months of data completes within one frame (16 ms) after data is available
Switching between chart and table view must not jank — use AnimatedSwitcher with 200 ms fade
security requirements
No raw user identifiers rendered in chart labels
All color values sourced from WCAG-validated design tokens
ui components
ShimmerSkeleton (reuse shared widget)
ErrorCard with retry
EmptyState illustration widget
AccessibleToggleButton ('Show as table' / 'Show as chart')
DataTable (Flutter Material DataTable)

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/monthly_activity_bar_chart.dart`. Inject the BarChartAdapter via constructor (not hard-coded) so widget tests can pass a FakeBarChartAdapter that returns deterministic BarChartData. Use AnimatedSwitcher (key on child) to transition between chart and table views — this avoids setState jank. For Semantics, wrap each BarChartRodData's corresponding hit area with a Semantics node; the cleanest approach is to overlay transparent Semantics widgets positioned at each bar's bounding rect using a CustomMultiChildLayout or Stack, since fl_chart does not natively expose Semantics.

Retrieve bar positions from BarChartData after layout using a post-frame callback. The DataTable fallback must use the same MonthlyActivityData list as the chart — no separate fetch. Ensure the toggle button has a minimum tap area of 48×48 dp (Material touch target standard). For the Norwegian locale, import `package:intl/date_symbol_data_local.dart` and call `initializeDateFormatting('nb_NO')` before rendering month labels.

Testing Requirements

Unit/widget tests (flutter_test): (1) Renders skeleton when isLoading=true and chart is absent from tree. (2) Renders error card when error is non-null. (3) Renders empty-state when data=[]. (4) Renders chart when data has 12 months — golden test at 375 px width.

(5) Toggle button switches to DataTable; DataTable contains correct row count. (6) Semantics tree contains a label matching '{month}, {activityType}: {count} activities' for each bar. (7) onBarTapped fires with correct arguments when a bar is tapped (use WidgetTester.tap on a test bar). Accessibility test: run flutter_test's SemanticsController and assert no unlabeled interactive elements.

Golden tests for loading, error, empty, and populated states.

Component
Monthly Activity Bar 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.