Files
SixFlagsSuperCalendar/components/charts/WaitTimeTodayChart.tsx
T
josh b401f28fef
Build and Deploy / Lint, typecheck, test (push) Successful in 33s
Build and Deploy / Build & Push (push) Successful in 1m0s
fix: show outage duration in the chart label
The outage marker now reads "#N · 1h 28m" instead of just "#N" so the
duration is visible at a glance without hovering. Positioned above the
band ("position: top") rather than inside it — when the label string is
wider than the band, Recharts' insideTop placement silently drops the
ReferenceArea rect; placing the label above sidesteps that.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 19:16:24 -04:00

204 lines
6.1 KiB
TypeScript

"use client";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
ReferenceArea,
} from "recharts";
import { computeOutages, outageLookup, formatOutageDuration } from "@/lib/outage";
export interface TodaySample {
recordedAt: string;
localTime: string;
isOpen: boolean;
waitMinutes: number | null;
fastLaneMinutes: number | null;
}
interface Props {
samples: TodaySample[];
hasFastLane: boolean;
}
const TIME_FMT = new Intl.DateTimeFormat([], {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
export default function WaitTimeTodayChart({ samples, hasFastLane }: Props) {
const outages = computeOutages(samples);
const outageByTime = outageLookup(samples, outages);
// X-axis time is rendered in the viewer's local timezone (Intl with no
// tz arg) so an Eastern-time user sees ET regardless of which park.
// Each point also carries its outage (if any) so the custom tooltip can
// render "Outage #N — Hh Mm" without re-scanning.
const data = samples.map((s) => {
const time = TIME_FMT.format(new Date(s.recordedAt));
const outage = !s.isOpen ? outageByTime.get(time) ?? null : null;
return {
time,
wait: s.isOpen ? s.waitMinutes : null,
fl: s.isOpen ? s.fastLaneMinutes : null,
outageN: outage?.n ?? null,
outageDurationMin: outage?.durationMin ?? null,
};
});
const tickInterval = Math.max(1, Math.floor(data.length / 8));
return (
<div style={{ width: "100%", height: 280 }}>
<ResponsiveContainer>
<LineChart data={data} margin={{ top: 8, right: 16, left: 0, bottom: 4 }}>
<CartesianGrid stroke="#272727" strokeDasharray="3 3" vertical={false} />
<XAxis
dataKey="time"
stroke="#737373"
tick={{ fontSize: 11, fill: "#737373" }}
interval={tickInterval}
tickLine={false}
/>
<YAxis
stroke="#737373"
tick={{ fontSize: 11, fill: "#737373" }}
tickLine={false}
axisLine={false}
domain={[0, "auto"]}
padding={{ bottom: 6 }}
label={{ value: "min", position: "insideLeft", angle: -90, offset: 16, style: { fontSize: 10, fill: "#737373" } }}
/>
{/* Outage markers — render before the lines so they sit underneath. */}
{outages.map((o) => (
<ReferenceArea
key={`outage-${o.n}`}
x1={o.startTimeLabel}
x2={o.endTimeLabel}
fill="#ff4d8d"
fillOpacity={0.08}
stroke="#ff4d8d"
strokeOpacity={0.35}
strokeDasharray="3 3"
ifOverflow="extendDomain"
label={{
value: `#${o.n} · ${formatOutageDuration(o.durationMin)}`,
position: "top",
fill: "#ff4d8d",
fontSize: 10,
fontWeight: 600,
offset: 4,
}}
/>
))}
<Tooltip content={<RideTooltip hasFastLane={hasFastLane} />} />
<Legend wrapperStyle={{ fontSize: "0.72rem", paddingTop: 4 }} />
<Line
name="Wait"
type="monotone"
dataKey="wait"
stroke="#4ade80"
strokeWidth={2}
dot={false}
connectNulls={false}
isAnimationActive={false}
/>
{hasFastLane && (
<Line
name="Fast Lane"
type="monotone"
dataKey="fl"
stroke="#ff4d8d"
strokeWidth={2}
dot={false}
connectNulls={false}
isAnimationActive={false}
/>
)}
</LineChart>
</ResponsiveContainer>
</div>
);
}
// ── Custom tooltip ─────────────────────────────────────────────────────────
// When hovering on an outage span, replace the standard wait/Fast Lane rows
// with a single "Outage #N — duration" line. Otherwise show the per-series
// values, formatting 0 explicitly as "walk-on".
interface TooltipDatum {
outageN: number | null;
outageDurationMin: number | null;
}
interface TooltipEntry {
value?: number | null;
name?: string | number;
dataKey?: string | number;
color?: string;
payload?: TooltipDatum;
}
interface RideTooltipProps {
active?: boolean;
payload?: TooltipEntry[];
label?: string | number;
hasFastLane: boolean;
}
function RideTooltip({ active, payload, label, hasFastLane }: RideTooltipProps) {
if (!active || !payload || payload.length === 0) return null;
const datum = payload[0]?.payload;
const cardStyle: React.CSSProperties = {
background: "#1c1c1c",
border: "1px solid #333",
borderRadius: 6,
fontSize: "0.75rem",
color: "#f5f5f5",
padding: "8px 10px",
lineHeight: 1.4,
};
if (datum?.outageN && datum.outageDurationMin !== null) {
return (
<div style={cardStyle}>
<div style={{ color: "#b0b0b0", fontSize: "0.68rem", marginBottom: 2 }}>{label}</div>
<div style={{ color: "#ff4d8d", fontWeight: 600 }}>
Outage #{datum.outageN} {formatOutageDuration(datum.outageDurationMin)}
</div>
</div>
);
}
return (
<div style={cardStyle}>
<div style={{ color: "#b0b0b0", fontSize: "0.68rem", marginBottom: 4 }}>{label}</div>
{payload.map((entry, i) => {
const v = entry.value;
const display =
v === null || v === undefined
? "—"
: v === 0
? "walk-on"
: `${v} min`;
const dotColor = entry.color ?? "#fff";
return (
<div key={String(entry.dataKey ?? i)} style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ width: 8, height: 2, background: dotColor, borderRadius: 1 }} />
<span style={{ color: "#b0b0b0", flex: 1 }}>{entry.name}</span>
<span style={{ fontWeight: 600 }}>{display}</span>
</div>
);
})}
{!hasFastLane && null}
</div>
);
}