Skip to content

Commit

Permalink
feat(client): Allow chart data download
Browse files Browse the repository at this point in the history
  • Loading branch information
clementprdhomme committed Nov 22, 2024
1 parent 7446b0b commit d060ce2
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 21 deletions.
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"cmdk": "1.0.0",
"date-fns": "4.1.0",
"express": "4.21.1",
"lodash-es": "4.17.21",
"mapbox-gl": "3.7.0",
"next": "14.2.15",
"nuqs": "2.0.4",
Expand All @@ -74,6 +75,7 @@
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/eslint-plugin-query": "5.59.7",
"@types/lodash-es": "^4",
"@types/node": "22.7.6",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.1",
Expand Down
77 changes: 75 additions & 2 deletions client/src/components/dataset-card/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { getMonth } from "date-fns";
import { getMonth, getYear } from "date-fns";
import { format } from "date-fns/format";
import { camelCase } from "lodash-es";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import * as React from "react";
Expand All @@ -22,11 +23,15 @@ import {
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import YearChart from "@/components/year-chart";
import useLocation from "@/hooks/use-location";
import { useLocationByCode } from "@/hooks/use-location-by-code";
import useMapLayers from "@/hooks/use-map-layers";
import useYearChartData from "@/hooks/use-year-chart-data";
import { cn } from "@/lib/utils";
import CalendarDaysIcon from "@/svgs/calendar-days.svg";
import ChevronDownIcon from "@/svgs/chevron-down.svg";
import DownloadIcon from "@/svgs/download.svg";
import GraphIcon from "@/svgs/graph.svg";
import PauseIcon from "@/svgs/pause.svg";
import PlayIcon from "@/svgs/play.svg";
import QuestionMarkIcon from "@/svgs/question-mark.svg";
Expand All @@ -50,6 +55,7 @@ interface DatasetCardProps {

const DatasetCard = ({ id, name, defaultLayerId, layers, metadata }: DatasetCardProps) => {
const [layersConfiguration, { addLayer, updateLayer, removeLayer }] = useMapLayers();
const [location] = useLocation();

const defaultSelectedLayerId = useMemo(
() => getDefaultSelectedLayerId(defaultLayerId, layers, layersConfiguration),
Expand Down Expand Up @@ -101,6 +107,15 @@ const DatasetCard = ({ id, name, defaultLayerId, layers, metadata }: DatasetCard
[layers, selectedLayerId],
);

const { data: chartData, isLoading: chartIsLoading } = useYearChartData(
selectedLayerId,
selectedDate,
);

const { data: locationData, isLoading: locationIsLoading } = useLocationByCode(
location.code.slice(-1)[0],
);

const onToggleAnimation = useCallback(() => {
const newIsAnimated = !isAnimated;

Expand Down Expand Up @@ -207,6 +222,37 @@ const DatasetCard = ({ id, name, defaultLayerId, layers, metadata }: DatasetCard
[selectedLayerId, isDatasetActive, addLayer, updateLayer, layers, layersConfiguration],
);

const onClickSaveChartData = useCallback(() => {
if (chartIsLoading || !chartData || locationIsLoading || !locationData || !selectedDate) {
return;
}

const data = {
dataset: name,
datasetMetadata: Object.entries(metadata ?? {}).reduce((res, [key, value]) => {
if (key === "id") {
return res;
}

return {
...res,
[camelCase(key)]: value,
};
}, {}),
year: getYear(selectedDate),
location: locationData.name,
...chartData,
};

const blob = new Blob([JSON.stringify(data)], { type: "application/json" });

const link = document.createElement("a");
link.download = `${name} - ${locationData.name}.json`;
link.href = URL.createObjectURL(blob);
link.click();
link.remove();
}, [name, metadata, chartData, chartIsLoading, selectedDate]);

// When the layer is animated, show each month of the year in a loop
useEffect(() => {
if (isAnimated && selectedDate !== undefined && selectedLayerId !== undefined) {
Expand Down Expand Up @@ -237,6 +283,28 @@ const DatasetCard = ({ id, name, defaultLayerId, layers, metadata }: DatasetCard
{name}
</Label>
<div className="flex items-center gap-1 pt-1.5">
{selectedDate !== undefined && selectedLayerId !== undefined && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="group/chart"
disabled={chartIsLoading || !chartData || locationIsLoading || !locationData}
onClick={onClickSaveChartData}
>
<span className="sr-only">Save chart data</span>
<GraphIcon
className="!size-4 transition-colors group-hover/chart:text-casper-blue-300"
aria-hidden
/>
</Button>
</TooltipTrigger>
<TooltipContent>Save chart data</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{!!selectedLayer?.attributes!.download_link && (
<TooltipProvider>
<Tooltip>
Expand Down Expand Up @@ -329,7 +397,12 @@ const DatasetCard = ({ id, name, defaultLayerId, layers, metadata }: DatasetCard
)}
{selectedDate !== undefined && selectedLayerId !== undefined && (
<div className="mt-3">
<YearChart layerId={selectedLayerId} date={selectedDate} active={isDatasetActive} />
<YearChart
data={chartData}
date={selectedDate}
loading={chartIsLoading}
active={isDatasetActive}
/>
</div>
)}
{selectedDate !== undefined && dateRange !== undefined && isDatasetActive && (
Expand Down
34 changes: 18 additions & 16 deletions client/src/components/year-chart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Text, TextProps } from "@visx/text";
import { extent } from "d3-array";
import { interpolateRgb, piecewise } from "d3-interpolate";
import { format } from "date-fns/format";
import { uniqueId } from "lodash-es";
import { ComponentProps, useCallback, useMemo } from "react";

import { Skeleton } from "@/components/ui/skeleton";
Expand All @@ -19,8 +20,9 @@ import tailwindConfig from "@/lib/tailwind-config";
import { cn } from "@/lib/utils";

interface YearChartProps {
layerId: number;
data: ReturnType<typeof useYearChartData>["data"];
date: string;
loading: boolean;
active: boolean;
}

Expand All @@ -35,17 +37,15 @@ const Y_AXIS_TICK_WIDTH = 5;
const Y_AXIS_TICK_COUNT = 5;
const GRADIENT_OPACITY_EXTENT = [0.9, 0.7];

const YearChart = ({ layerId, date, active }: YearChartProps) => {
const YearChart = ({ data, date, loading, active }: YearChartProps) => {
const { parentRef, width } = useParentSize({ ignoreDimensions: ["height"] });
const height = useMemo(
() => Math.max(Math.min(width / 2.7, CHART_MAX_HEIGHT), CHART_MIN_HEIGHT),
[width],
);

const { data, isLoading } = useYearChartData(layerId, date);

const unitWidth = useMemo(() => {
if (isLoading || !data) {
if (loading || !data) {
return 0;
}

Expand All @@ -59,21 +59,21 @@ const YearChart = ({ layerId, date, active }: YearChartProps) => {
}, Y_AXIS_TICK_WIDTH) ?? Y_AXIS_TICK_WIDTH;

return (data.unit?.length ?? 0) * 8 + subtract;
}, [data, isLoading]);
}, [data, loading]);

const xScale = useMemo(() => {
if (isLoading || !data) {
if (loading || !data) {
return undefined;
}

return scalePoint({
range: [0, width - Y_AXIS_WIDTH - X_AXIS_OFFSET_RIGHT - unitWidth],
domain: data.data.map(({ x }) => x),
});
}, [width, data, isLoading, unitWidth]);
}, [width, data, loading, unitWidth]);

const yScale = useMemo(() => {
if (isLoading || !data) {
if (loading || !data) {
return undefined;
}

Expand All @@ -82,18 +82,18 @@ const YearChart = ({ layerId, date, active }: YearChartProps) => {
domain: extent(data.data.map(({ y }) => y)) as [number, number],
nice: Y_AXIS_TICK_COUNT,
});
}, [height, data, isLoading]);
}, [height, data, loading]);

const fillColorScale = useMemo(() => {
if (isLoading || !data) {
if (loading || !data) {
return undefined;
}

return scaleLinear({
range: data.colorRange,
domain: data.colorDomain,
}).interpolate(() => piecewise(interpolateRgb, data.colorRange));
}, [data, isLoading]);
}, [data, loading]);

const xAxisTickLabelProps = useCallback<
NonNullable<Exclude<ComponentProps<typeof AxisBottom>["tickLabelProps"], Partial<TextProps>>>
Expand Down Expand Up @@ -134,6 +134,8 @@ const YearChart = ({ layerId, date, active }: YearChartProps) => {
};
}, []);

const gradientId = useMemo(() => uniqueId(), []);

const gradientColorStops = useMemo(() => {
if (!fillColorScale || !yScale) {
return [];
Expand Down Expand Up @@ -162,8 +164,8 @@ const YearChart = ({ layerId, date, active }: YearChartProps) => {

return (
<div ref={parentRef}>
{isLoading && !data && <Skeleton style={{ width: `${width}px`, height: `${height}px` }} />}
{!isLoading && !!data && !!xScale && !!yScale && !!fillColorScale && (
{loading && !data && <Skeleton style={{ width: `${width}px`, height: `${height}px` }} />}
{!loading && !!data && !!xScale && !!yScale && !!fillColorScale && (
<svg
width={width}
height={height}
Expand All @@ -173,7 +175,7 @@ const YearChart = ({ layerId, date, active }: YearChartProps) => {
"grayscale-0": active,
})}
>
<LinearGradient id={`year-chart-gradient-${layerId}`}>
<LinearGradient id={`year-chart-gradient-${gradientId}`}>
{gradientColorStops.map((stop, index) => (
<stop key={index} {...stop} />
))}
Expand Down Expand Up @@ -204,7 +206,7 @@ const YearChart = ({ layerId, date, active }: YearChartProps) => {
x={(d) => xScale(d.x) ?? 0}
y={(d) => yScale(d.y) ?? 0}
yScale={yScale}
fill={`url('#year-chart-gradient-${layerId}')`}
fill={`url('#year-chart-gradient-${gradientId}')`}
/>
<LinePath
data={data.data}
Expand Down
5 changes: 3 additions & 2 deletions client/src/hooks/use-year-chart-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ interface YearChartData {
colorDomain: [number, number];
}

export default function useYearChartData(layerId: number, date: string) {
export default function useYearChartData(layerId?: number, date?: string) {
const [location] = useLocation();

const year = useMemo(() => getYear(date), [date]);
const year = useMemo(() => getYear(date ?? new Date()), [date]);

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error
Expand Down Expand Up @@ -60,6 +60,7 @@ export default function useYearChartData(layerId: number, date: string) {
},
{
query: {
enabled: layerId !== undefined && date !== undefined,
placeholderData: { data: [] },
select: (data) => {
if (!data?.data?.length) {
Expand Down
6 changes: 6 additions & 0 deletions client/src/svgs/graph.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 19 additions & 1 deletion client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4522,7 +4522,16 @@ __metadata:
languageName: node
linkType: hard

"@types/lodash@npm:^4.14.172":
"@types/lodash-es@npm:^4":
version: 4.17.12
resolution: "@types/lodash-es@npm:4.17.12"
dependencies:
"@types/lodash": "npm:*"
checksum: 10c0/5d12d2cede07f07ab067541371ed1b838a33edb3c35cb81b73284e93c6fd0c4bbeaefee984e69294bffb53f62d7272c5d679fdba8e595ff71e11d00f2601dde0
languageName: node
linkType: hard

"@types/lodash@npm:*, @types/lodash@npm:^4.14.172":
version: 4.17.13
resolution: "@types/lodash@npm:4.17.13"
checksum: 10c0/c3d0b7efe7933ac0369b99f2f7bff9240d960680fdb74b41ed4bd1b3ca60cca1e31fe4046d9abbde778f941a41bc2a75eb629abf8659fa6c27b66efbbb0802a9
Expand Down Expand Up @@ -5912,6 +5921,7 @@ __metadata:
"@turf/bbox": "npm:7.1.0"
"@turf/helpers": "npm:7.1.0"
"@turf/meta": "npm:7.1.0"
"@types/lodash-es": "npm:^4"
"@types/mapbox-gl": "npm:3.4.0"
"@types/node": "npm:22.7.6"
"@types/react": "npm:18.3.1"
Expand Down Expand Up @@ -5940,6 +5950,7 @@ __metadata:
express: "npm:4.21.1"
husky: "npm:9.1.6"
jiti: "npm:1.21.6"
lodash-es: "npm:4.17.21"
mapbox-gl: "npm:3.7.0"
next: "npm:14.2.15"
nuqs: "npm:2.0.4"
Expand Down Expand Up @@ -9147,6 +9158,13 @@ __metadata:
languageName: node
linkType: hard

"lodash-es@npm:4.17.21":
version: 4.17.21
resolution: "lodash-es@npm:4.17.21"
checksum: 10c0/fb407355f7e6cd523a9383e76e6b455321f0f153a6c9625e21a8827d10c54c2a2341bd2ae8d034358b60e07325e1330c14c224ff582d04612a46a4f0479ff2f2
languageName: node
linkType: hard

"lodash.debounce@npm:^4.0.8":
version: 4.0.8
resolution: "lodash.debounce@npm:4.0.8"
Expand Down

0 comments on commit d060ce2

Please sign in to comment.