Skip to content

Components

Conditions graph

Cloud cover, the moon's altitude, the moon rise marker, the prime view window and the current time on one night.

Preview

MOON RISEPRIME VIEWNOW18:0021:0000:0003:0006:00
Cloud coverMoon altitudePrime viewNow

Variants

Pick a variant with the controls. The preview and both code blocks follow.

Cloud cover
Moon altitude
Moon rise
Prime view
Now
MOON RISEPRIME VIEWNOW18:0021:0000:0003:0006:00
Variant.tsx
<AppConditionsGraph />

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
<AppConditionsGraph /><AppConditionsGraph layers={{ cloud: true, moon: 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-graphs.tsx · AppConditionsGraph
/** * ConditionsGraph: grid, a smoothed cloud area, the moon's altitude area, the moon rise marker, the prime view * window and the NOW marker. Colours are stops at Pogson opacity steps; sizes are Fibonacci numbers. */export function AppConditionsGraph({ layers = {}, height = 233, cloudData = CLOUD_HOURLY }: { layers?: GraphLayers; height?: number; cloudData?: readonly number[] }) {  const { cloud = true, moon = true, moonRise = true, prime = true, now = true } = layers;  const [ref, w] = useElementWidth<HTMLDivElement>(560);  const h = height - LABEL_H;  const span = NIGHT_END - NIGHT_START;  const x = (m: number): number => ((m - NIGHT_START) / span) * w;  const cloudPts: Pt[] = cloudData.map((c, i) => [x(NIGHT_START + (i * span) / (cloudData.length - 1)), h - (c / 100) * h] as const);  const cloudFill = `M${cloudPts[0][0]} ${h} L${cloudPts[0][0]} ${cloudPts[0][1]} ${smooth(cloudPts).replace(/^M\S+ \S+/, "")} L${cloudPts.at(-1)![0]} ${h} L${w} ${h} Z`;  const moonPts: Pt[] = [];  for (let m = NIGHT_START; m <= NIGHT_END; m += 15) moonPts.push([x(m), h - (Math.max(0, moonAltitude(m)) / 90) * h]);  const moonLine = "M" + moonPts.map(([px, py]) => `${px} ${py}`).join(" L");  const win = prime ? primeView() : null;  const riseX = x(MOON_RISE_MINUTES);  const labelY = h * 0.75;  const nowX = x(NOW);  const nowTop = h * 0.35;  const winStart = win ? x(win.from) : 0;  const winEnd = win ? x(win.to) : 0;  const winMid = Math.max(F.f55, Math.min(w - F.f55, (winStart + winEnd) / 2));  return (    <div ref={ref} className="app-ui app-graph" style={{ height }}>      <svg viewBox={`0 0 ${w} ${height}`} height={height} role="img" aria-label="Conditions graph for an example night">        <GraphDefs />        {[1, 2, 3].map((i) => <line key={`h${i}`} x1={0} x2={w} y1={(h * i) / 4} y2={(h * i) / 4} stroke={stop(GRAPH.ink, 6)} />)}        {[1, 2, 3, 4].map((i) => <line key={`v${i}`} y1={0} y2={h} x1={(w * i) / 4} x2={(w * i) / 4} stroke={stop(GRAPH.ink, 6)} />)}        {cloud && (          <g>            <path d={cloudFill} fill="url(#g-cloud)" />            <path d={smooth(cloudPts)} fill="none" stroke={stop(GRAPH.cloud, 3)} strokeWidth={F.f2} />          </g>        )}        {moon && (          <g>            <path d={`M0 ${h} L${moonLine.slice(1)} L${w} ${h} Z`} fill={stop(GRAPH.moon, 4)} />            <path d={moonLine} fill="none" stroke={stop(GRAPH.moonLine, 2)} strokeWidth={F.f2} />          </g>        )}        {moonRise && (          <g>            <line x1={riseX} x2={riseX} y1={h} y2={labelY + F.f8} stroke={stop(GRAPH.moonLine, 2)} />            <circle cx={riseX} cy={labelY + F.f5} r={F.f8} fill={stop(GRAPH.rise, 3)} />            <circle cx={riseX} cy={labelY + F.f5} r={F.f3} fill={stop(GRAPH.rise)} />            <GraphPill x={riseX + F.f13} y={labelY + F.f5} text="MOON RISE" tracking={1} color={stop("deep-space-50")} fill={stop(GRAPH.moon, 4)} stroke={stop(GRAPH.moonLine, 3)} />          </g>        )}        {win && (          <g>            <rect x={winStart} y={0} width={winEnd - winStart} height={h} fill="url(#g-prime)" />            <GraphPill x={winMid - pillWidth("PRIME VIEW", 0.5) / 2} y={F.f8 + F.f21 / 2} text="PRIME VIEW" radius={F.f21} color={stop(GRAPH.prime)} fill={stop(GRAPH.prime, 5)} stroke={stop(GRAPH.prime, 3)} />          </g>        )}        {now && <NowMarker x={nowX} top={nowTop} bottom={h} />}        {[0, 1, 2, 3, 4].map((i) => {          const m = NIGHT_START + (span * i) / 4;          const midnight = Math.round(m) % 1440 === 0;          return (            <text key={i} x={(w * i) / 4} y={h + F.f21} fontSize={T.s2n} fontWeight={midnight ? 700 : 400} fill={midnight ? stop(GRAPH.ink) : stop("space-grey-300")} textAnchor={i === 0 ? "start" : i === 4 ? "end" : "middle"}>              {clock(m)}            </text>          );        })}      </svg>    </div>  );}
app-ui.css
.app-graph { position: relative; width: 100%; }.app-graph svg { display: block; width: 100%; overflow: visible; }.app-legend { display: flex; flex-wrap: wrap; gap: var(--spacing-f21); margin-top: var(--spacing-f13); }

API reference

React propFlutter parameterTypeDefaultMeaning
layerswhich of cloudCoverData, moonCurve, moonRiseTime, primeViewWindow are passedobjectall onEach layer draws only when its data is present.
heightparent heightnumber200Includes 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.

RoleCSS tokenDart tokenApp widget today
Cloudspace-grey-50 at --mag-3 to --mag-6, stroke --mag-3AstrColors.spaceGrey[50], Mag.m3Colors.white 25% to 5%
Moondeep-space-300 at --mag-2, line deep-space-200AstrColors.deepSpace[300], [200]GraphTheme.moonFillColor #1E1B4B
Moon riseaurora-pink-400AstrColors.auroraPink[400]#A855F7
Nowsodium-airglow-400, pulsingAstrColors.sodiumAirglow[400]GraphTheme.nowIndicatorColor #F97316
Prime viewaurora-green-400 band at --mag-5AstrColors.auroraGreen[400]GraphTheme.primeViewColor #10B981
Gridspace-grey-50 at --mag-6Mag.m6white 5%
Strokes, dotsFibonacci 2, 3, 5, 8AstrSpace.f2, f3, f5, f81.5, 3, 6
Labels--text-s-2AstrType.sNeg29 and 10