Add the usage chart with range and grouping controls
Angular · Angular · advanced · greenfield
Adds the usage chart to the analytics page: range buttons (7d/30d/90d), a Daily/Monthly grouping toggle, and a bar list of bucketed totals. An effect watches the controls and loads usage for the selected range, bucketing the response by the selected grouping; controls lock while a request is in flight and a failed load offers Retry. Verified by switching ranges and watching the buckets reload.
The analytics page of a SaaS admin. HttpClient is the app-wide HTTP layer; the usage API is fast but uncancellable. Monthly grouping exists because 90d at day granularity is too dense to read.
Requirements
- The usage chart loads points from `GET /api/usage?range={range}` whenever the selected range (7d / 30d / 90d) changes, including on init, showing a loading state while the request is in flight. The API returns day-level points sorted ascending; `ts` is a `YYYY-MM-DD` string.
- The chart can be grouped Daily or Monthly. Changing EITHER the range or the grouping must update the rendered chart — a grouping change may rebucket the already-loaded points or refetch, either is acceptable, but the chart must reflect the new grouping when the user toggles it.
- All controls are disabled while a request is in flight (the API has no cancellation, so overlapping requests are prevented at the source). A failed load clears the chart and shows an error with a Retry button.
- Use signals and `effect()` per the codebase conventions.
Files touched
- src/app/usage/usage-chart.component.ts
--- src/app/usage/usage-chart.component.ts
+import { Component, effect, inject, signal } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+
+interface UsagePoint {
+ ts: string; // YYYY-MM-DD, sorted ascending
+ count: number;
+}
+
+interface Bucket {
+ label: string;
+ total: number;
+}
+
+function bucketBy(points: UsagePoint[], granularity: 'day' | 'month'): Bucket[] {
+ const totals = new Map<string, number>();
+ for (const point of points) {
+ const label = granularity === 'month' ? point.ts.slice(0, 7) : point.ts;
+ totals.set(label, (totals.get(label) ?? 0) + point.count);
+ }
+ return [...totals.entries()].map(([label, total]) => ({ label, total }));
+}
+
+@Component({
+ selector: 'app-usage-chart',
+ standalone: true,
+ template: `
+ <section class="usage-chart" aria-label="Usage">
+ <header class="usage-controls">
+ @for (option of RANGES; track option) {