diff --git a/client/package.json b/client/package.json
index 4f64393..8a0874a 100644
--- a/client/package.json
+++ b/client/package.json
@@ -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",
@@ -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",
diff --git a/client/src/components/dataset-card/index.tsx b/client/src/components/dataset-card/index.tsx
index ef7c5c3..14233c6 100644
--- a/client/src/components/dataset-card/index.tsx
+++ b/client/src/components/dataset-card/index.tsx
@@ -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";
@@ -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";
@@ -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),
@@ -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;
@@ -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) {
@@ -237,6 +283,28 @@ const DatasetCard = ({ id, name, defaultLayerId, layers, metadata }: DatasetCard
{name}
+ {selectedDate !== undefined && selectedLayerId !== undefined && (
+
+
+
+
+
+ Save chart data
+
+
+ )}
{!!selectedLayer?.attributes!.download_link && (
@@ -329,7 +397,12 @@ const DatasetCard = ({ id, name, defaultLayerId, layers, metadata }: DatasetCard
)}
{selectedDate !== undefined && selectedLayerId !== undefined && (
-
+
)}
{selectedDate !== undefined && dateRange !== undefined && isDatasetActive && (
diff --git a/client/src/components/year-chart/index.tsx b/client/src/components/year-chart/index.tsx
index 92e745b..b8b8d73 100644
--- a/client/src/components/year-chart/index.tsx
+++ b/client/src/components/year-chart/index.tsx
@@ -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";
@@ -19,8 +20,9 @@ import tailwindConfig from "@/lib/tailwind-config";
import { cn } from "@/lib/utils";
interface YearChartProps {
- layerId: number;
+ data: ReturnType["data"];
date: string;
+ loading: boolean;
active: boolean;
}
@@ -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;
}
@@ -59,10 +59,10 @@ 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;
}
@@ -70,10 +70,10 @@ const YearChart = ({ layerId, date, active }: YearChartProps) => {
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;
}
@@ -82,10 +82,10 @@ 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;
}
@@ -93,7 +93,7 @@ const YearChart = ({ layerId, date, active }: YearChartProps) => {
range: data.colorRange,
domain: data.colorDomain,
}).interpolate(() => piecewise(interpolateRgb, data.colorRange));
- }, [data, isLoading]);
+ }, [data, loading]);
const xAxisTickLabelProps = useCallback<
NonNullable["tickLabelProps"], Partial>>
@@ -134,6 +134,8 @@ const YearChart = ({ layerId, date, active }: YearChartProps) => {
};
}, []);
+ const gradientId = useMemo(() => uniqueId(), []);
+
const gradientColorStops = useMemo(() => {
if (!fillColorScale || !yScale) {
return [];
@@ -162,8 +164,8 @@ const YearChart = ({ layerId, date, active }: YearChartProps) => {
return (
- {isLoading && !data &&
}
- {!isLoading && !!data && !!xScale && !!yScale && !!fillColorScale && (
+ {loading && !data &&
}
+ {!loading && !!data && !!xScale && !!yScale && !!fillColorScale && (