From d060ce2322b9edeca65f27a4eef9fa6ed72f5371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Prod=27homme?= Date: Fri, 22 Nov 2024 10:00:37 +0100 Subject: [PATCH] feat(client): Allow chart data download --- client/package.json | 2 + client/src/components/dataset-card/index.tsx | 77 +++++++++++++++++++- client/src/components/year-chart/index.tsx | 34 +++++---- client/src/hooks/use-year-chart-data.ts | 5 +- client/src/svgs/graph.svg | 6 ++ client/yarn.lock | 20 ++++- 6 files changed, 123 insertions(+), 21 deletions(-) create mode 100644 client/src/svgs/graph.svg 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 && ( { "grayscale-0": active, })} > - + {gradientColorStops.map((stop, index) => ( ))} @@ -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}')`} /> getYear(date), [date]); + const year = useMemo(() => getYear(date ?? new Date()), [date]); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error @@ -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) { diff --git a/client/src/svgs/graph.svg b/client/src/svgs/graph.svg new file mode 100644 index 0000000..0dc15c8 --- /dev/null +++ b/client/src/svgs/graph.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/client/yarn.lock b/client/yarn.lock index aa93444..233e0db 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -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 @@ -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" @@ -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" @@ -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"