Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/components/data/DataView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ export function DataView({
display={display}
height={plotHeight}
pinLimit={2}
margins
/>
</section>
</CardContent>
Expand Down
10 changes: 1 addition & 9 deletions src/components/panels/CellView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,9 @@ type PlotAreaProps = {
display: SeriesDisplay;
height?: number;
pinLimit?: number;
margins?: boolean;
};

export function PlotArea({
series,
display,
height,
pinLimit,
margins,
}: PlotAreaProps) {
export function PlotArea({ series, display, height, pinLimit }: PlotAreaProps) {
if (series.loading) {
return (
<div className="grid gap-3">
Expand Down Expand Up @@ -227,7 +220,6 @@ export function PlotArea({
transposed={display.transposed}
colormapId={display.colormapId}
scale={display.fingerprintScale}
margins={margins}
height={height}
pinLimit={pinLimit}
/>
Expand Down
124 changes: 38 additions & 86 deletions src/components/plots/FingerprintPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ import {
} from "@/lib/plots/chartTheme";
import {
drawFingerprint,
type FingerprintCell,
fingerprintCellAt,
fingerprintGrid,
type FingerprintLayout,
FINGERPRINT_STRIP_HEIGHT,
type FingerprintTarget,
fingerprintTargetAt,
} from "@/lib/plots/fingerprint";
import {
binCountLabel,
Expand Down Expand Up @@ -50,27 +49,18 @@ type FingerprintPlotProps = {
transposed: boolean;
colormapId: ColormapId;
scale?: BinUnit;
margins?: boolean;
height?: number;
pinLimit?: number;
};

type Hover = {
target: FingerprintTarget;
type Hover = FingerprintCell & {
left: number;
top: number;
flip: boolean;
anchor: TooltipAnchor | null;
};

function targetKey(target: FingerprintTarget): string {
if (target.kind === "cell") return `cell-${target.col}-${target.hour}`;
if (target.kind === "column") return `column-${target.col}`;
return `hour-${target.hour}`;
}

const sameTarget = (a: Hover, b: Hover) =>
targetKey(a.target) === targetKey(b.target);
const sameCell = (a: Hover, b: Hover) => a.col === b.col && a.hour === b.hour;

export function FingerprintPlot({
values,
Expand All @@ -81,7 +71,6 @@ export function FingerprintPlot({
transposed,
colormapId,
scale = "day",
margins = false,
height: heightOverride,
pinLimit = 1,
}: FingerprintPlotProps) {
Expand Down Expand Up @@ -113,12 +102,11 @@ export function FingerprintPlot({
}, []);

const height =
(heightOverride ??
(transposed ? TRANSPOSED_MIN_HEIGHT : TIME_SERIES_PLOT_HEIGHT)) +
(margins ? FINGERPRINT_STRIP_HEIGHT : 0);
heightOverride ??
(transposed ? TRANSPOSED_MIN_HEIGHT : TIME_SERIES_PLOT_HEIGHT);
const layout: FingerprintLayout = useMemo(
() => ({ grid, width, height, transposed, margins }),
[grid, width, height, transposed, margins],
() => ({ grid, width, height, transposed }),
[grid, width, height, transposed],
);
const extents = useMemo(() => asymmetricExtents(grid.mean), [grid]);
const nCols = grid.bins.length;
Expand Down Expand Up @@ -161,7 +149,7 @@ export function FingerprintPlot({
const rect = event.currentTarget.getBoundingClientRect();
const left = event.clientX - rect.left;
const top = event.clientY - rect.top;
const target = fingerprintTargetAt(layout, left, top);
const cell = fingerprintCellAt(layout, left, top);
const anchor = anchorBesidePanel(
wrapperRef.current,
event.clientX,
Expand All @@ -172,39 +160,7 @@ export function FingerprintPlot({
event.clientX,
event.clientY,
);
setHover(target ? { target, left, top, flip, anchor } : null);
};

const stripPointAt = (
kind: "column" | "hour",
index: number,
): TooltipPoint | undefined => {
const stats =
kind === "column" ? grid.margins.byColumn : grid.margins.byHour;
if (index < 0 || index >= stats.mean.length) return undefined;
const count = stats.count[index] ?? 0;
const std = formatSeriesValue(stats.std[index] as number);
if (kind === "column") {
const bin = grid.bins[index]!;
return {
label: formatBinLabel(bin, scale),
shortLabel: formatBinShortLabel(bin, scale),
value: stats.mean[index] as number,
spread:
count > 0
? `Std ${std} across ${count} hour${count === 1 ? "" : "s"}`
: undefined,
};
}
const time = `${String(index).padStart(2, "0")}:00`;
const suffix = timeBasisLabel ? ` ${shortTimeBasis(timeBasisLabel)}` : "";
return {
label: `${time}${suffix}`,
shortLabel: time,
value: stats.mean[index] as number,
spread:
count > 0 ? `Std ${std} across ${binCountLabel(count, scale)}` : undefined,
};
setHover(cell ? { ...cell, left, top, flip, anchor } : null);
};

const pointAt = (
Expand All @@ -229,32 +185,27 @@ export function FingerprintPlot({
: undefined,
};
};
const pointFor = ({ target }: Hover) =>
target.kind === "cell"
? pointAt(target.col, target.hour, "full")!
: target.kind === "column"
? stripPointAt("column", target.col)!
: stripPointAt("hour", target.hour)!;
const previousFor = ({ target }: Hover) => {
if (target.kind === "column") return stripPointAt("column", target.col - 1);
if (target.kind === "hour") return stripPointAt("hour", target.hour - 1);
return target.hour > 0
? pointAt(target.col, target.hour - 1, "time")
: pointAt(target.col - 1, hoursPerDay - 1, "full");
};
// A change against a pin only reads sensibly between like targets.
const comparable = (a: Hover, b: Hover) => a.target.kind === b.target.kind;
const previousOf = (cell: FingerprintCell) =>
cell.hour > 0
? pointAt(cell.col, cell.hour - 1, "time")
: pointAt(cell.col - 1, hoursPerDay - 1, "full");

const separate = pinLimit > 1;
const pinned = separate ? null : (pins[0] ?? null);
const lastPin = pins.at(-1);
const shown = pinned ?? hover;
const hoverPoint =
hover && !desktop
? { ...pointFor(hover), left: hover.left, top: hover.top }
? {
...pointAt(hover.col, hover.hour, "full")!,
left: hover.left,
top: hover.top,
}
: null;
const comparing =
pinned && hover && !sameTarget(hover, pinned) && comparable(hover, pinned);
pinned &&
hover &&
(hover.col !== pinned.col || hover.hour !== pinned.hour);

return (
<div className="grid gap-2">
Expand All @@ -269,22 +220,22 @@ export function FingerprintPlot({
onClick={() => {
if (desktop && hover?.anchor) {
setPins((current) =>
togglePin(current, hover, pinLimit, sameTarget),
togglePin(current, hover, pinLimit, sameCell),
);
}
}}
/>
{desktop && separate
? pins.map((pin) => (
<PinnedTooltip
key={targetKey(pin.target)}
point={pointFor(pin)}
previous={previousFor(pin)}
key={`${pin.col}-${pin.hour}`}
point={pointAt(pin.col, pin.hour, "full")!}
previous={previousOf(pin)}
position={{ x: pin.left, y: pin.top, flip: pin.flip }}
units={units}
onClose={() =>
setPins((current) =>
current.filter((other) => !sameTarget(other, pin)),
current.filter((other) => !sameCell(other, pin)),
)
}
/>
Expand All @@ -293,7 +244,7 @@ export function FingerprintPlot({
{desktop && separate
? pins.map((pin) => (
<span
key={targetKey(pin.target)}
key={`${pin.col}-${pin.hour}`}
aria-hidden="true"
className="pointer-events-none absolute size-2 -translate-1/2 rounded-full bg-foreground ring-2 ring-background"
style={{ left: pin.left, top: pin.top }}
Expand Down Expand Up @@ -323,23 +274,25 @@ export function FingerprintPlot({
{desktop &&
separate &&
hover?.anchor &&
!pins.some((pin) => sameTarget(pin, hover)) ? (
!pins.some((pin) => sameCell(pin, hover)) ? (
<MapSideTooltip
point={pointFor(hover)}
point={pointAt(hover.col, hover.hour, "full")!}
previous={
lastPin && comparable(lastPin, hover)
? pointFor(lastPin)
: previousFor(hover)
lastPin
? pointAt(lastPin.col, lastPin.hour, "full")
: previousOf(hover)
}
anchor={hover.anchor}
units={units}
/>
) : null}
{desktop && !separate && shown?.anchor ? (
<MapSideTooltip
point={pointFor(shown)}
previous={previousFor(shown)}
compared={comparing ? pointFor(hover) : undefined}
point={pointAt(shown.col, shown.hour, "full")!}
previous={previousOf(shown)}
compared={
comparing ? pointAt(hover.col, hover.hour, "full") : undefined
}
anchor={shown.anchor}
units={units}
onClose={pinned ? () => setPins([]) : undefined}
Expand Down Expand Up @@ -372,7 +325,6 @@ export function FingerprintPlot({
{binCountLabel(nCols, scale)} × {hoursPerDay} hours
{timeBasisLabel ? ` · ${timeBasisLabel}` : ""}
{units ? ` · ${units}` : ""}
{margins ? " · strips show the mean ±1 std of each row and column" : ""}
</p>
</div>
);
Expand Down
29 changes: 2 additions & 27 deletions src/hooks/useCellExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { useCallback, useState } from "react";
import { captureFingerprint } from "@/lib/export/captureFingerprint";
import { captureMap } from "@/lib/export/captureMap";
import { captureTimeSeries } from "@/lib/export/captureTimeSeries";
import { buildMarginCsv, buildSeriesCsv } from "@/lib/export/csv";
import { buildMarginTables, marginColumns } from "@/lib/export/margins";
import { buildSeriesCsv } from "@/lib/export/csv";
import { downloadBlob } from "@/lib/export/download";
import { buildSquareFingerprintCanvas } from "@/lib/export/fingerprintSquareLogo";
import { buildReportPdf, type ReportAssets } from "@/lib/export/pdf";
Expand Down Expand Up @@ -121,16 +120,14 @@ export function useCellExport({
}).toDataURL("image/png");

const rows = buildSeriesRows(values, prov);
const margins = buildMarginTables(displayValues ?? values, prov);
const columns = marginColumns(prov);
const [pdf, workbook] = await Promise.all([
buildReportPdf({
prov,
assets,
values: displayValues ?? values,
attribution: mapCapture.attribution,
}),
buildSeriesWorkbook(rows, prov, margins),
buildSeriesWorkbook(rows, prov),
]);

const archive = await buildZip([
Expand All @@ -144,28 +141,6 @@ export function useCellExport({
name: `${base}.csv`,
data: new TextEncoder().encode(buildSeriesCsv(rows, prov)),
},
{
name: `${base}_daily_cycle.csv`,
data: new TextEncoder().encode(
buildMarginCsv(
margins.dailyCycle,
columns.dailyCycle,
"mean daily cycle, each hour across all days",
prov,
),
),
},
{
name: `${base}_seasonal_course.csv`,
data: new TextEncoder().encode(
buildMarginCsv(
margins.seasonalCourse,
columns.seasonalCourse,
"seasonal course, each day across its hours",
prov,
),
),
},
{
name: `${base}_badge.png`,
data: dataUrlToBytes(squareDataUrl),
Expand Down
1 change: 0 additions & 1 deletion src/lib/export/captureFingerprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export function captureFingerprint({
width: EXPORT_PLOT_WIDTH,
height: EXPORT_PLOT_HEIGHT,
transposed: false,
margins: true,
},
{
colormapId,
Expand Down
16 changes: 0 additions & 16 deletions src/lib/export/csv.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { formatUtcOffset } from "@/lib/zarr/localTime";
import { isoDate, type ExportProvenance } from "./provenance";
import type { MarginRow } from "./margins";
import type { SeriesRow } from "./rows";

export const CSV_COLUMNS =
Expand Down Expand Up @@ -75,18 +74,3 @@ export function buildSeriesCsv(

return `${lines.join("\n")}\n`;
}

/** One of the fingerprint's strips: mean, std and count along one axis. */
export function buildMarginCsv(
rows: MarginRow[],
columns: string[],
description: string,
prov: ExportProvenance,
): string {
const lines = buildHeader(rows.length, prov);
lines.push(`# table: ${description}`, columns.join(","));
for (const row of rows) {
lines.push([row.key, row.mean ?? "", row.std ?? "", row.count].join(","));
}
return `${lines.join("\n")}\n`;
}
Loading
Loading