Skip to content

EPR-6: Per-pixel time series chart on map readout#40

Open
aivanchenk wants to merge 2 commits into
mainfrom
EPR-6--time-plot-series
Open

EPR-6: Per-pixel time series chart on map readout#40
aivanchenk wants to merge 2 commits into
mainfrom
EPR-6--time-plot-series

Conversation

@aivanchenk

Copy link
Copy Markdown
Collaborator

Summary

  • Add a Recharts line chart to the map readout that plots daily-mean NEE for the selected pixel and history window
  • Aggregate hourly Zarr samples to daily means via dailyMeanSeries() and wire full Float32Array series through EarthMapMapReadout
  • Show a loading chart shell with progress while pixel data is fetched; expand readout layout/styles for the chart section
  • Minor hero/global style cleanup in the same branch

Test plan

  • Open /map, click a pixel, confirm the time series chart renders with sensible axes and units
  • Change the history slider and confirm the chart reloads for the new window
  • Verify loading state appears briefly while data fetches, then resolves to chart or error
  • Toggle light/dark theme and confirm chart colors match map chrome
  • Run npm test

Made with Cursor

@aivanchenk aivanchenk requested a review from lazarusA June 23, 2026 21:31

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lib/zarr/series.ts
Comment on lines +7 to +32
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import useEffect and useState to manage client-side mounting state and prevent hydration mismatch warnings.

Suggested change
import { useMemo } from "react";
import { useEffect, useState, useMemo } from "react";

Comment on lines +35 to +44
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;

Comment on lines +3 to +9
import {
CartesianGrid,
LineChart,
ResponsiveContainer,
XAxis,
YAxis,
} from "recharts";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import useEffect and useState to manage client-side mounting state and prevent hydration mismatch warnings.

import { useEffect, useState } from "react";
import {
  CartesianGrid,
  LineChart,
  ResponsiveContainer,
  XAxis,
  YAxis,
} from "recharts";

Comment on lines +26 to +28
const { isLight } = useTheme();
const { grid, tick } = timeSeriesChartTheme(isLight);
const dayCount = dayCountForHistory(historyYears);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 }} />;
  }

@github-actions

Copy link
Copy Markdown

🚀 Vercel preview deployed

Preview URL: https://earth-prints-hd3if81ul-anastasiia-s-projects10.vercel.app

@lazarusA

lazarusA commented Jun 24, 2026

Copy link
Copy Markdown
Member

Something is off with the styles on the landing page, it also happens sometimes in light mode. The outer-hard edges
Screenshot 2026-06-24 at 12 37 24

Also, maybe let's not spend too much time on this one, since I guess at some point we will like to actually show a finger-print, if time allows.

Also, could we please make this
Screenshot 2026-06-24 at 12 38 32

a side menu or something (open to suggestions) for each element, like location, variable, year, time series, such I can see the map behind on mobile or smaller screens.

Probably location should be shown in the box-patch itself.

I haven't look at the code yet, but I will later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants