EPR-6: Per-pixel time series chart on map readout#40
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive design system to EarthPrints, migrating global styles to standardized tokens and primitives, and refactoring layout components like the navigation and brand elements to use them. It also adds interactive time-series charting to the map using Recharts, supported by new data processing utilities and tests. The reviewer feedback focuses on improving the robustness of the daily mean calculations against non-integer inputs and NaN values, as well as resolving potential hydration mismatch warnings in Next.js by deferring the rendering of Recharts components until they are mounted on the client.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export function dailyMeanSeries( | ||
| values: ArrayLike<number>, | ||
| hoursPerDay = 24, | ||
| ): DailyMeanPoint[] { | ||
| if (values.length === 0 || hoursPerDay <= 0) return []; | ||
|
|
||
| const dayCount = Math.ceil(values.length / hoursPerDay); | ||
| const points: DailyMeanPoint[] = []; | ||
|
|
||
| for (let day = 0; day < dayCount; day++) { | ||
| const start = day * hoursPerDay; | ||
| const end = Math.min(start + hoursPerDay, values.length); | ||
| let sum = 0; | ||
|
|
||
| for (let i = start; i < end; i++) { | ||
| sum += values[i]!; | ||
| } | ||
|
|
||
| points.push({ | ||
| day: day + 1, | ||
| value: sum / (end - start), | ||
| }); | ||
| } | ||
|
|
||
| return points; | ||
| } |
There was a problem hiding this comment.
The dailyMeanSeries function can be made more robust and defensive. Currently, if hoursPerDay is not an integer, the loop indices will be floats, resulting in values[i] returning undefined and the sum becoming NaN. Additionally, scientific datasets (like NEE from Zarr stores) frequently contain NaN or missing values (e.g., over oceans or due to sensor gaps). If any single hourly value is NaN, the entire daily mean becomes NaN and breaks the chart.
We should round hoursPerDay to the nearest integer and skip NaN/undefined values when calculating the daily mean.
export function dailyMeanSeries(
values: ArrayLike<number>,
hoursPerDay = 24,
): DailyMeanPoint[] {
const hours = Math.round(hoursPerDay);
if (values.length === 0 || hours <= 0) return [];
const dayCount = Math.ceil(values.length / hours);
const points: DailyMeanPoint[] = [];
for (let day = 0; day < dayCount; day++) {
const start = day * hours;
const end = Math.min(start + hours, values.length);
let sum = 0;
let count = 0;
for (let i = start; i < end; i++) {
const val = values[i];
if (val !== undefined && val !== null && !Number.isNaN(val)) {
sum += val;
count++;
}
}
if (count > 0) {
points.push({
day: day + 1,
value: sum / count,
});
}
}
return points;
}| @@ -0,0 +1,102 @@ | |||
| "use client"; | |||
|
|
|||
| import { useMemo } from "react"; | |||
| const { isLight } = useTheme(); | ||
| const data = useMemo( | ||
| () => dailyMeanSeries(values, hoursPerDay), | ||
| [values, hoursPerDay], | ||
| ); | ||
|
|
||
| const { stroke, grid, tick, tooltipBg, tooltipBorder } = | ||
| timeSeriesChartTheme(isLight); | ||
|
|
||
| if (data.length === 0) return null; |
There was a problem hiding this comment.
In Next.js App Router, client components are still pre-rendered on the server. Recharts relies on browser-only APIs (like measuring container dimensions via ResizeObserver or generating sequential element IDs) which differ between the server-rendered HTML and the client-hydrated DOM, leading to hydration mismatch warnings.
Adding a mounted state ensures the chart is only rendered on the client, completely avoiding these hydration warnings.
const { isLight } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const data = useMemo(
() => dailyMeanSeries(values, hoursPerDay),
[values, hoursPerDay],
);
const { stroke, grid, tick, tooltipBg, tooltipBorder } =
timeSeriesChartTheme(isLight);
if (!mounted) {
return <div style={{ height: TIME_SERIES_PLOT_HEIGHT }} />;
}
if (data.length === 0) return null;
| import { | ||
| CartesianGrid, | ||
| LineChart, | ||
| ResponsiveContainer, | ||
| XAxis, | ||
| YAxis, | ||
| } from "recharts"; |
| const { isLight } = useTheme(); | ||
| const { grid, tick } = timeSeriesChartTheme(isLight); | ||
| const dayCount = dayCountForHistory(historyYears); |
There was a problem hiding this comment.
Similar to the main plot, the loading skeleton chart also uses Recharts and will trigger hydration mismatch warnings during server-side pre-rendering. Deferring its rendering until the component has mounted on the client prevents these warnings.
const { isLight } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const { grid, tick } = timeSeriesChartTheme(isLight);
const dayCount = dayCountForHistory(historyYears);
if (!mounted) {
return <div style={{ height: TIME_SERIES_PLOT_HEIGHT }} />;
}
|
🚀 Vercel preview deployed Preview URL: https://earth-prints-hd3if81ul-anastasiia-s-projects10.vercel.app |


Summary
dailyMeanSeries()and wire fullFloat32Arrayseries throughEarthMap→MapReadoutTest plan
/map, click a pixel, confirm the time series chart renders with sensible axes and unitsnpm testMade with Cursor