Skip to content
Merged
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts

# Lovable design reference exports (local visual reference only, never commit)
*Lovable*.html
*Lovable_files/

# Claude Code local per-user settings
.claude/settings.local.json
Expand Down
84 changes: 55 additions & 29 deletions src/components/data/DataView.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
"use client";

import { useRef, useSyncExternalStore } from "react";
import { ArrowLeft } from "lucide-react";
import { useRef, useState, useSyncExternalStore } from "react";
import { Collapsible } from "@base-ui/react/collapsible";
import { ArrowLeft, ChevronDown } from "lucide-react";
import { formatLatitude, formatLongitude } from "@/lib/map/geogrid";
import { formatSelectedYearsLabel } from "@/lib/zarr/timeRange";
import type { TimeBasis } from "@/lib/zarr/localTime";
import type { CellSeries } from "@/hooks/useCellSeries";
import { useSeriesDisplay } from "@/hooks/useSeriesDisplay";
import { CellExport } from "@/components/panels/CellExport";
import { PlotArea, PlotControls } from "@/components/panels/CellView";
import { ScaleField } from "@/components/panels/ScaleField";
import { TimeBasisField } from "@/components/panels/TimeBasisField";
import { YearPicker } from "@/components/panels/YearPicker";
import { SeriesSummary } from "@/components/data/SeriesSummary";
Expand All @@ -35,6 +37,16 @@ function usePlotHeight() {
);
}

const SIDEBAR_QUERY = "(width >= 64rem)";

function useSidebarLayout() {
return useSyncExternalStore(
subscribeResize,
() => matchMedia(SIDEBAR_QUERY).matches,
() => true,
);
}

function Chip({ children }: { children: React.ReactNode }) {
return (
<span className="inline-flex h-6 items-center rounded-md border bg-editor-bg-depth px-2 text-xs text-muted-foreground tabular-nums">
Expand All @@ -59,6 +71,8 @@ export function DataView({
const display = useSeriesDisplay(series, timeBasis);
const fadeRef = useRef<HTMLDivElement>(null);
const plotHeight = usePlotHeight();
const wide = useSidebarLayout();
const [detailsOpen, setDetailsOpen] = useState(false);
const { selection, selectedYears, units } = series;

return (
Expand All @@ -79,33 +93,39 @@ export function DataView({
/>
<div className="mx-auto grid max-w-7xl gap-6 px-3 pt-28 pb-40 md:px-6 md:pt-20 md:pb-60">
{selection ? (
<div className="grid items-start gap-6 lg:grid-cols-[340px_minmax(0,1fr)]">
<aside className="grid content-start gap-3 max-lg:order-last">
<Card>
<CardContent>
<YearPicker
selectedYears={selectedYears}
cachedYears={series.cachedYears}
loading={series.loading}
onSelectYears={series.selectYears}
/>
</CardContent>
</Card>
<Card>
<CardContent>
<TimeBasisField
value={timeBasis}
onChange={onTimeBasisChange}
/>
</CardContent>
</Card>
<Card>
<CardContent>
<CellExport series={series} display={display} />
</CardContent>
</Card>
</aside>
<div className="grid min-w-0 gap-6">
<div className="grid items-start gap-6 lg:grid-cols-[340px_minmax(0,1fr)] lg:grid-rows-[auto_1fr] lg:gap-y-3">
<Card className="lg:col-start-1 lg:row-start-1">
<CardContent>
<Collapsible.Root
open={wide || detailsOpen}
onOpenChange={setDetailsOpen}
className="grid"
>
{wide ? null : (
<Collapsible.Trigger className="group flex w-fit items-center gap-1 rounded-sm text-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50">
Details · Years {formatSelectedYearsLabel(selectedYears)}
<ChevronDown className="size-4 transition-transform group-data-panel-open:rotate-180" />
</Collapsible.Trigger>
)}
<Collapsible.Panel className="h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-150 ease-out data-ending-style:h-0 data-starting-style:h-0 motion-reduce:transition-none">
<div className="grid gap-6 max-lg:pt-4">
<YearPicker
selectedYears={selectedYears}
cachedYears={series.cachedYears}
loading={series.loading}
onSelectYears={series.selectYears}
/>
<ScaleField display={display} />
<TimeBasisField
value={timeBasis}
onChange={onTimeBasisChange}
/>
</div>
</Collapsible.Panel>
</Collapsible.Root>
</CardContent>
</Card>
<div className="grid min-w-0 gap-6 lg:col-start-2 lg:row-span-2 lg:row-start-1">
<Card>
<CardContent className="grid gap-6">
<header className="grid items-start gap-4 max-md:pr-10">
Expand Down Expand Up @@ -140,6 +160,7 @@ export function DataView({
display={display}
height={plotHeight}
pinLimit={2}
margins
/>
</section>
</CardContent>
Expand All @@ -151,6 +172,11 @@ export function DataView({
className="grid grid-cols-2 gap-3 xl:grid-cols-4"
/>
</div>
<Card className="lg:col-start-1 lg:row-start-2">
<CardContent>
<CellExport series={series} display={display} />
</CardContent>
</Card>
</div>
) : null}
</div>
Expand Down
23 changes: 19 additions & 4 deletions src/components/panels/CellView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ArrowLeftRight, ChevronDown } from "lucide-react";
import { formatLatitude, formatLongitude } from "@/lib/map/geogrid";
import { formatSelectedYearsLabel, ZARR_TIME } from "@/lib/zarr/timeRange";
import { TIME_SERIES_PLOT_HEIGHT } from "@/lib/plots/chartTheme";
import type { TimeBasis } from "@/lib/zarr/localTime";
import { shortTimeBasis, type TimeBasis } from "@/lib/zarr/localTime";
import type { CellSeries, SeriesProgress } from "@/hooks/useCellSeries";
import type { PlotView, SeriesDisplay } from "@/hooks/useSeriesDisplay";
import { TimeSeriesPlot } from "@/components/plots/TimeSeriesPlot";
Expand Down Expand Up @@ -108,6 +108,9 @@ export function CellView({
);
}

const activeTriggerClass =
"data-active:bg-brand/15 data-active:text-brand dark:data-active:bg-brand/15 dark:data-active:text-brand";

export function PlotControls({ display }: { display: SeriesDisplay }) {
const disabled = display.isEmptyCell;

Expand All @@ -121,14 +124,14 @@ export function PlotControls({ display }: { display: SeriesDisplay }) {
<TabsTrigger
value="line"
disabled={disabled}
className="data-active:bg-brand/15 data-active:text-brand dark:data-active:bg-brand/15 dark:data-active:text-brand"
className={activeTriggerClass}
>
Line
</TabsTrigger>
<TabsTrigger
value="fingerprint"
disabled={disabled}
className="data-active:bg-brand/15 data-active:text-brand dark:data-active:bg-brand/15 dark:data-active:text-brand"
className={activeTriggerClass}
>
Fingerprint
</TabsTrigger>
Expand Down Expand Up @@ -163,9 +166,16 @@ type PlotAreaProps = {
display: SeriesDisplay;
height?: number;
pinLimit?: number;
margins?: boolean;
};

export function PlotArea({ series, display, height, pinLimit }: PlotAreaProps) {
export function PlotArea({
series,
display,
height,
pinLimit,
margins,
}: PlotAreaProps) {
if (series.loading) {
return (
<div className="grid gap-3">
Expand Down Expand Up @@ -201,6 +211,9 @@ export function PlotArea({ series, display, height, pinLimit }: PlotAreaProps) {
values={display.displayValues}
units={series.units}
hoursPerDay={ZARR_TIME.hoursPerDay}
selectedYears={series.selectedYears}
resolution={display.lineResolution}
timeBasisSuffix={shortTimeBasis(display.timeBasisLabel)}
height={height}
pinLimit={pinLimit}
/>
Expand All @@ -213,6 +226,8 @@ export function PlotArea({ series, display, height, pinLimit }: PlotAreaProps) {
timeBasisLabel={display.timeBasisLabel}
transposed={display.transposed}
colormapId={display.colormapId}
scale={display.fingerprintScale}
margins={margins}
height={height}
pinLimit={pinLimit}
/>
Expand Down
58 changes: 58 additions & 0 deletions src/components/panels/ScaleField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import type { BinUnit } from "@/lib/plots/timeBins";
import type { LineResolution } from "@/lib/plots/lineSeries";
import type { SeriesDisplay } from "@/hooks/useSeriesDisplay";
import { FieldLegend, FieldSet } from "@/components/ui/field";
import { SegmentedTabs } from "@/components/ui/segmented-tabs";

const LINE_RESOLUTIONS = [
{ value: "hour", label: "Hourly" },
{ value: "day", label: "Daily" },
{ value: "week", label: "Weekly" },
{ value: "month", label: "Monthly" },
] as const;

export function ScaleField({ display }: { display: SeriesDisplay }) {
const disabled = display.isEmptyCell;

if (display.plotView === "line") {
return (
<FieldSet>
<FieldLegend variant="label">Line resolution</FieldLegend>
<SegmentedTabs<LineResolution>
value={display.lineResolution}
onChange={display.setLineResolution}
options={LINE_RESOLUTIONS}
disabled={disabled}
aria-label="Line resolution"
/>
</FieldSet>
);
}

const fingerprintScales = [
{ value: "day", label: "Daily" },
{ value: "week", label: "Weekly" },
{ value: "month", label: "Monthly" },
{
value: "year",
label: "Yearly",
disabled: !display.canShowYearly,
title: display.canShowYearly ? undefined : "Pick two or more years",
},
] as const;

return (
<FieldSet>
<FieldLegend variant="label">Fingerprint scale</FieldLegend>
<SegmentedTabs<BinUnit>
value={display.fingerprintScale}
onChange={display.setFingerprintScale}
options={fingerprintScales}
disabled={disabled}
aria-label="Fingerprint scale"
/>
</FieldSet>
);
}
Loading
Loading