high priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

ActivityChartWidget is a ConsumerWidget that accepts an ActivityChartData and a ValueChanged<GranularityMode> onGranularityChanged callback
Bar chart is rendered when granularity is day or week; line chart is rendered when granularity is month
Switching granularity triggers onGranularityChanged and the parent rebuilds with new data — no internal state mutation
Bar chart uses fl_chart BarChart with grouped bars per ChartSeries, each bar rod using the ChartSeries.color
Line chart uses fl_chart LineChart with one LineChartBarData per ChartSeries
All chart colors have ≥4.5:1 contrast ratio against the widget's background color (design token surface color)
X axis labels are rendered using design token text style (caption size) and do not overlap at day granularity (7 labels) or month granularity (12 labels)
Y axis labels are rendered on the left side, formatted as integers for session counts
Granularity toggle row above the chart has three segments (Day, Week, Month), each with minimum 44×44dp tap target
Active granularity segment is visually highlighted using the design token accent color
Chart container has a fixed height of 220dp and fills available width
No fl_chart-internal exceptions thrown for empty series data (guarded by maxY default from ActivityChartData)
Widget uses only design token constants — no hardcoded colors, sizes, or font sizes

Technical Requirements

frameworks
Flutter
Riverpod
fl_chart
data models
ActivityChartData
ChartSeries
ChartDataPoint
GranularityMode
performance requirements
Chart renders within one frame (16ms) for up to 365 data points
No widget rebuild outside ActivityChartWidget scope when granularity changes
security requirements
Aggregate data only — no individual user identifiers in chart rendering
ui components
fl_chart BarChart
fl_chart LineChart
GranularityToggleRow (reuse or adapt TimeWindowSelector pattern)
Design token color and typography constants

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Place widget in lib/features/stats/presentation/widgets/activity_chart_widget.dart. Use a Column with GranularityToggleRow on top and the chart below. For the conditional bar/line switch, use an if/else returning either BarChart or LineChart — do not use AnimatedSwitcher unless a cross-fade transition is explicitly required. For fl_chart BarChart, create BarChartGroupData with one BarChartRodData per ChartSeries per X position.

For LineChart, create one LineChartBarData per ChartSeries with spots derived from ChartDataPoint.x and .y. Use FlTitlesData to configure axis labels with a getTitlesWidget function that returns a Text using AppTextStyles.caption. Set clipData: FlClipData.all() to prevent chart rendering outside bounds. Do not use fl_chart's built-in touch handlers in this task — tooltips are handled in task-006.

Testing Requirements

Widget tests in test/stats/activity_chart_widget_test.dart. Required scenarios: (1) with day granularity, a BarChart widget is present in the tree; (2) with month granularity, a LineChart widget is present in the tree; (3) tapping 'Month' granularity toggle calls onGranularityChanged with GranularityMode.month; (4) granularity toggle segments are all ≥44dp in height and width; (5) widget renders without exception when ActivityChartData.series is empty; (6) chart container has height of 220dp. Integration test (optional): pump widget with real Riverpod provider override to verify end-to-end data flow.

Component
Activity Chart Widget
ui medium
Epic Risks (4)
high impact high prob technical

fl_chart renders chart elements on a Canvas, making individual bars and data points invisible to the Flutter Semantics tree by default. Without explicit Semantics wrappers, VoiceOver and TalkBack users receive no chart information, violating the WCAG 2.2 AA requirement mandated by all three partner organizations.

Mitigation & Contingency

Mitigation: Wrap the fl_chart widget in a Semantics node with a dynamically generated textual description of the chart data (e.g., 'Bar chart: January 12, February 8, March 15 sessions'). Implement a collapsible data table alternative beneath the chart that screen readers can navigate row by row. Validate with VoiceOver on iOS and TalkBack on Android before the epic is marked complete.

Contingency: If fl_chart's Canvas rendering cannot be made accessible within the epic timeline, ship the chart hidden from the Semantics tree with ExcludeSemantics and promote the data table alternative to first-class UI so screen reader users have full access to the information. Log a tech-debt item to revisit native chart accessibility in a future sprint.

medium impact medium prob scope

Coordinators managing up to 5 chapters (NHF requirement) require the PeerMentorStatsList to display chapter affiliation labels for each row. With large chapter lists and many peer mentors, the list could become overwhelming and cause layout overflow or scrolling performance issues on lower-end Android devices.

Mitigation & Contingency

Mitigation: Implement chapter filtering as a segmented control above the list so coordinators can scope the list to one chapter at a time. Use ListView.builder (lazy rendering) rather than a Column of all rows. Profile scroll performance on a low-end Android device (Pixel 4a equivalent) with 50 peer mentors in scope during development.

Contingency: If multi-chapter display causes unacceptable performance, ship with single-chapter scope as the default view and a chapter switcher dropdown, deferring the combined cross-chapter list to a follow-up sprint.

low impact low prob technical

Summary cards and the chart widget rebuilding simultaneously on provider state change could cause a visible jank frame on slower devices, degrading perceived quality especially since this screen is intended to feel motivating and polished for gamification purposes.

Mitigation & Contingency

Mitigation: Use AnimatedSwitcher with a short fade transition (150ms) on the stats cards and chart so that data replacement feels intentional rather than jarring. Profile with Flutter DevTools on a mid-range device and ensure no frame exceeds 16ms during a time-window switch.

Contingency: If animation introduces complexity that delays delivery, ship without animation and use a loading skeleton (shimmer effect) during re-fetch instead, which is simpler and equally effective at masking the data swap.

high impact low prob security

If the role-based screen dispatch is misconfigured, a peer mentor could navigate to the coordinator stats screen and see aggregated chapter data for all peer mentors, which is a data privacy violation and a compliance risk for all three organizations.

Mitigation & Contingency

Mitigation: Implement role guard at the router level using an existing role-route-guard component so the coordinator screen route is unreachable for peer mentor roles. Add a widget test that mounts the coordinator screen with a peer mentor session token and asserts that the guard redirects to the no-access screen.

Contingency: If a bypass is found in QA, add a secondary in-screen role assertion in the coordinator screen's initState that throws an AuthorizationException and navigates to the error screen, ensuring defence in depth regardless of router configuration.