Components
Cloud cover graph
The hourly cloud forecast as a smoothed area over the night, with the time grid, hour labels and the pulsing NOW marker. It is the cloud layer of the conditions graph on its own.
Preview
Variants
Pick a variant with the controls. The preview and both code blocks follow.
Forecast
Variant.tsx
<AppCloudCoverGraph />Implementation
The React tab is this site's own component, the same source that draws the preview, styled by the CSS below it. The Flutter tab is the Dart a screen builds it with, from the app's tokens.
Usage
<AppCloudCoverGraph /><AppCloudCoverGraph data={[80, 85, 90, 95, 92, 88, 90, 94, 96, 92, 90, 93, 95]} />app-graphs.tsx · AppCloudCoverGraph
/** * The hourly cloud forecast: the smoothed gradient area, grid, hour labels and NOW marker of the conditions graph * with only the cloud layer. Data is a percentage at evenly spaced hours across the night. */export function AppCloudCoverGraph({ data = CLOUD_HOURLY, height = 144 + F.f34 }: { data?: readonly number[]; height?: number }) { return <AppConditionsGraph cloudData={data} height={height} layers={{ moon: false, moonRise: false, prime: false }} />;}app-graphs.tsx · smooth
/** Cubic segments with tension 0.2, exactly as _drawCloudCover builds them. */function smooth(points: Pt[]): string { let d = `M${points[0][0]} ${points[0][1]}`; for (let i = 0; i < points.length - 1; i++) { const p0 = points[Math.max(0, i - 1)]; const p1 = points[i]; const p2 = points[i + 1]; const p3 = points[Math.min(points.length - 1, i + 2)]; const c1 = [p1[0] + (p2[0] - p0[0]) * 0.2, p1[1] + (p2[1] - p0[1]) * 0.2]; const c2 = [p2[0] - (p3[0] - p1[0]) * 0.2, p2[1] - (p3[1] - p1[1]) * 0.2]; d += ` C${c1[0]} ${c1[1]} ${c2[0]} ${c2[1]} ${p2[0]} ${p2[1]}`; } return d;}app-ui.css
.app-graph { position: relative; width: 100%; }.app-graph svg { display: block; width: 100%; overflow: visible; }.app-pulse { transform-box: fill-box; transform-origin: center; animation: app-pulse 2s ease-in-out infinite; }Usage
ConditionsGraph(cloudCoverData: hourly,startTime: start,endTime: end,)conditions_graph.dart · ConditionsGraph
class ConditionsGraph extends StatelessWidget { const ConditionsGraph({ super.key, this.themeColor = Colors.indigo, this.moonRiseTime, this.moonCurve, this.cloudCoverData, required this.startTime, required this.endTime, this.primeViewWindow, }); final Color themeColor; final DateTime? moonRiseTime; final List<GraphPoint>? moonCurve; final List<HourlyForecast>? cloudCoverData; final DateTime startTime; final DateTime endTime; final PrimeViewWindow? primeViewWindow; @override Widget build(BuildContext context) { return Stack( children: <Widget>[ // Graph Painter CustomPaint( size: Size.infinite, painter: _ConditionsGraphPainter( themeColor: themeColor, moonRiseTime: moonRiseTime, moonCurve: moonCurve, cloudCoverData: cloudCoverData, startTime: startTime, endTime: endTime, primeViewWindow: primeViewWindow, ), ), // Labels (X-Axis) Positioned( bottom: 0, left: 0, right: 0, height: 30, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: _buildTimeLabels(), ), ), ], ); } List<Widget> _buildTimeLabels() { final List<Widget> labels = <Widget>[]; final Duration duration = endTime.difference(startTime); for (int i = 0; i <= 4; i++) { final DateTime time = startTime.add(Duration(minutes: (duration.inMinutes * (i / 4)).round())); final bool isMidnight = time.hour == 0; labels.add( Text( '${time.hour.toString().padLeft(2, '0')}:00', style: TextStyle( color: isMidnight ? Colors.white : Colors.grey, fontSize: 10, fontWeight: isMidnight ? FontWeight.bold : FontWeight.normal ), ), ); } return labels; }}conditions_graph.dart · _drawCloudCover
// 2. Draw Cloud Cover (Area Chart)_drawCloudCover(canvas, width, height);API reference
| React prop | Flutter parameter | Type | Default | Meaning |
|---|---|---|---|---|
| data | cloudCoverData | number[] / List<HourlyForecast> | sample night | Cloud cover percent at evenly spaced hours. |
| height | parent height | number | 160 | Includes 30 px for the time labels. |
Design tokens
Every value on this component is a system token. The last column is what the app widget uses today, so the gap to close is visible.
| Role | CSS token | Dart token | App widget today |
|---|---|---|---|
| Fill | space-grey-50 at --mag-3 to --mag-6 | AstrColors.spaceGrey[50], Mag.m3, m6 | Colors.white 25% to 5% |
| Stroke | --mag-3, --spacing-f2 | Mag.m3, AstrSpace.f2 | 1.5 |
| Grid | space-grey-50 at --mag-6 | Mag.m6 | white 5% |
| Now | sodium-airglow-400 | AstrColors.sodiumAirglow[400] | #F97316 |
| Smoothing | cubic, tension 0.2 | (p2 - p0) * 0.2 | 0.2 |