From 91ed65b6d5986ce510af35c22c6b7ae3a1de7ac0 Mon Sep 17 00:00:00 2001 From: nofurtherinformation Date: Thu, 16 May 2024 15:28:25 -0500 Subject: [PATCH] time series chart --- app/tract/[tract]/page.tsx | 8 +- components/LineChart/LineChart.tsx | 234 +++++++++++------------- components/LineChart/types.ts | 32 ++-- components/StoreList/StoreList.tsx | 7 +- components/TimeseriesChart/Renderer.tsx | 148 +++++++++++++-- components/TimeseriesChart/types.ts | 5 +- package.json | 2 + pnpm-lock.yaml | 38 ++++ utils/data/config.ts | 16 +- utils/data/service/service.ts | 36 ++-- utils/state/map.ts | 20 +- utils/state/middleware.ts | 3 +- utils/state/thunks.ts | 11 +- utils/state/types.ts | 5 +- 14 files changed, 383 insertions(+), 182 deletions(-) diff --git a/app/tract/[tract]/page.tsx b/app/tract/[tract]/page.tsx index 413abe2..dc847c3 100644 --- a/app/tract/[tract]/page.tsx +++ b/app/tract/[tract]/page.tsx @@ -234,10 +234,10 @@ const TractPage: React.FC = async ({ params }) => { - {/*
- -
*/} -
+
+ +
+
diff --git a/components/LineChart/LineChart.tsx b/components/LineChart/LineChart.tsx index fbb5de7..7f8c158 100644 --- a/components/LineChart/LineChart.tsx +++ b/components/LineChart/LineChart.tsx @@ -1,18 +1,20 @@ -import { withParentSize } from "@visx/responsive" +"use client" import { scaleLinear, scaleTime } from "@visx/scale" import { Axis, Tooltip } from "@visx/xychart" import React, { useCallback } from "react" -import { DataRecord, ResponsiveXYChartProps } from "./types" +import { LineChartProps } from "./types" import { timeSeriesConfig } from "utils/data/config" import { Threshold } from "@visx/threshold" import { LinePath } from "@visx/shape" import { AxisBottom, AxisLeft } from "@visx/axis" -import { localPoint } from '@visx/event'; -import { bisector } from '@visx/vendor/d3-array'; +import { localPoint } from "@visx/event" +import { bisector } from "@visx/vendor/d3-array" +import { useParentSize } from "@cutting/use-get-parent-size" +import { curveLinear } from "@visx/curve" -const bisectDate = bisector((d: any) => new Date(d.date)).left; -const getMinMax = (data: DataRecord[], keys: string[]) => { - const minMax: Record = {} +const bisectDate = bisector((d: any) => new Date(d.date)).left +const getMinMax = >(data: Array, keys: Array) => { + const minMax: Record = {} as any keys.forEach((key) => { minMax[key] = { @@ -47,135 +49,119 @@ const getMinMax = (data: DataRecord[], keys: string[]) => { } return minMax } +const niceNumberFormatter = new Intl.NumberFormat("en-US", { + maximumFractionDigits: 2, + // compact + notation: "compact", +}) -const yearKey = "year" -const ResponsiveXYChart = withParentSize( - ({ parentWidth, parentHeight, data, timeseriesConfigKey, dataKey }) => { - // Define the margin for the chart - const margin = { top: 20, right: 30, bottom: 50, left: 40 } - const config = timeSeriesConfig[timeseriesConfigKey] - const xMax = parentWidth - margin.left - margin.right - const yMax = parentHeight - margin.top - margin.bottom - const parsedData = config.columns.map((c) => { - return { - year: new Date(`${c}-02-01`), - // @ts-ignore - average: data[`average_${c}`], - // @ts-ignore - median: data[`median_${c}`], - // @ts-ignore - q75: data[`q75_${c}`], - // @ts-ignore - q25: data[`q25_${c}`], - } - }) - - const minMax = getMinMax(parsedData, [yearKey, "average", "q25", "q75"]) +const LineChart = >({ data, dataKey, yearKey, parentRef, children }: LineChartProps) => { + const { width, height } = useParentSize(parentRef) + const parentWidth = width ?? 100 + const parentHeight = height ?? 100 + // Define the margin for the chart + const margin = { top: 20, right: 30, bottom: 50, left: 40 } + const xMax = parentWidth + const yMax = parentHeight - margin.top - margin.bottom + const minMax = getMinMax(data, [dataKey, yearKey]) - // Scales for the chart - const dateScale = scaleTime({ - // @ts-ignore - domain: [minMax[yearKey].min, minMax[yearKey].max], - range: [margin.left, xMax + margin.left], - }) - const valueScale = scaleLinear({ - // @ts-ignore - domain: [minMax.q25.min, minMax.q75.max], - range: [yMax, margin.bottom], - nice: true, - }) - const handleTooltip = useCallback( - (event: React.TouchEvent | React.MouseEvent) => { - const { x } = localPoint(event) || { x: 0 }; - const x0 = dateScale.invert(x); - const index = bisectDate(parsedData, x0, 1); - const d0 = parsedData[index - 1]; - const d1 = parsedData[index]; - let d = d0; - if (d1 && d1.year) { - // @ts-ignore - d = x0.valueOf() - (d0.year).valueOf() > (d1.year).valueOf() - x0.valueOf() ? d1 : d0; - } - console.log(d, x) - // showTooltip({ - // tooltipData: d, - // tooltipLeft: x, - // tooltipTop: stockValueScale(getStockValue(d)), - // }); - }, - [ - // showTooltip, stockValueScale, - dateScale], - ); + const modifiedChildren = React.Children.map(children||[], child => + React.cloneElement(child, { xMax, yMax, yMin: margin.bottom, xMin: margin.left }) + ); + // Scales for the chart + const dateScale = scaleTime({ + // @ts-ignore + domain: [minMax[yearKey].min, minMax[yearKey].max], + range: [margin.left, xMax], + }) + const valueScale = scaleLinear({ + // @ts-ignore + domain: [minMax[dataKey].min, minMax[dataKey].max], + range: [yMax, margin.bottom], + nice: true, + }) + // const handleTooltip = useCallback( + // (event: React.TouchEvent | React.MouseEvent) => { + // const { x } = localPoint(event) || { x: 0 } + // const x0 = dateScale.invert(x) + // const index = bisectDate(parsedData, x0, 1) + // const d0 = parsedData[index - 1] + // const d1 = parsedData[index] + // let d = d0 + // if (d1 && d1.year) { + // // @ts-ignore + // d = x0.valueOf() - d0.year.valueOf() > d1.year.valueOf() - x0.valueOf() ? d1 : d0 + // } + // console.log(d, x) + // // showTooltip({ + // // tooltipData: d, + // // tooltipLeft: x, + // // tooltipTop: stockValueScale(getStockValue(d)), + // // }); + // }, + // [ + // // showTooltip, stockValueScale, + // dateScale, + // ] + // ) - return ( - - dateScale(d.year) ?? 0} - y={(d) => valueScale(d.average) ?? 0} - /> - {/* @ts-ignore */} - + {modifiedChildren} + dateScale(d[yearKey]) ?? 0} + y={(d) => valueScale(d[dataKey]) ?? 0} + curve={curveLinear} + shapeRendering="geometricPrecision" + /> + {/* @ts-ignore */} + {/* dateScale(d[yearKey])} // y0={d => valueScale(0.2)} // y1={d => valueScale(0.4)} - y0={d => valueScale(d["q75"])} - y1={d => valueScale(d["q25"])} + y0={(d) => valueScale(d["q75"])} + y1={(d) => valueScale(d["q25"])} clipAboveTo={0} - // clipBelowTo={yMax} - // curve={curveBasis} - belowAreaProps={{ - fill: 'violet', - fillOpacity: 0., - }} - aboveAreaProps={{ - fill: 'gray', - fillOpacity: 0.1, - }} - /> - d.getFullYear()} /> - 520 ? 10 : 5} /> - - ( -
- {/* @ts-ignore */} -
{`Date: ${tooltipData?.nearestDatum?.datum?.[yearKey]?.getFullYear() + 1}`}
- {/* @ts-ignore */} + // clipBelowTo={yMax} + // curve={curveBasis} + belowAreaProps={{ + fill: "violet", + fillOpacity: 0, + }} + aboveAreaProps={{ + fill: "gray", + fillOpacity: 0.1, + }} + /> */} + d.getFullYear()} /> + 520 ? 10 : 5} /> + niceNumberFormatter.format(num)}/> + ( +
+ {JSON.stringify(tooltipData?.nearestDatum)} + {/*
{`Date: ${tooltipData?.nearestDatum?.datum?.[yearKey]?.getFullYear() + 1}`}
{`${"average"}: ${tooltipData?.nearestDatum?.datum?.["average"]}`}
- {/* @ts-ignore */}
{`${"q25"}: ${tooltipData?.nearestDatum?.datum?.["q25"]}`}
- {/* @ts-ignore */} -
{`${"q75"}: ${tooltipData?.nearestDatum?.datum?.["q75"]}`}
-
- )} - /> - - ) - } -) - -const LineChart: React.FC> = ({ - data, - timeseriesConfigKey, - dataKey, -}) => { - return +
{`${"q75"}: ${tooltipData?.nearestDatum?.datum?.["q75"]}`}
*/} +
+ )} + /> + + ) } export default LineChart diff --git a/components/LineChart/types.ts b/components/LineChart/types.ts index eec19fd..223df62 100644 --- a/components/LineChart/types.ts +++ b/components/LineChart/types.ts @@ -1,21 +1,19 @@ -import { timeSeriesConfig } from "utils/data/config"; +import React from "react"; -export interface DataRecord { - [key: string]: any; +type DataType> = Array; +export interface DimensionProps { + xMax: number; + yMax: number; + xMin: number; + yMin: number; } -export type TimeseriesConfigKey = keyof typeof timeSeriesConfig; -export type TimeseriesConfigEntry = typeof timeSeriesConfig[TimeseriesConfigKey]; -export type TimeseriesConfigColumn = TimeseriesConfigEntry["columns"][number]; -export type TimeseriesColumns = ('year' | 'median' | 'average'| 'q75'| 'q25') - -export interface LineChartProps { - data: Record - dataKey: TimeseriesColumns - timeseriesConfigKey: TimeseriesConfigKey; -} - -export interface ResponsiveXYChartProps extends LineChartProps { - parentWidth: number; - parentHeight: number; +export interface LineChartProps> { + data: DataType + parentRef: React.RefObject + dataKey: keyof T + yearKey: keyof T + children?: React.ReactElement | React.ReactElement[]; + // lowerBandKey: keyof T + // upperBandKey: keyof T } \ No newline at end of file diff --git a/components/StoreList/StoreList.tsx b/components/StoreList/StoreList.tsx index 193dd7f..f1e4a31 100644 --- a/components/StoreList/StoreList.tsx +++ b/components/StoreList/StoreList.tsx @@ -48,11 +48,13 @@ export const StoreList: React.FC> = ({ } return ( -
+

Store{data?.length > 1 ? "s" : ""} in {title || "service area"}

- +
+ +
{columns.map((_col, i) => { @@ -75,6 +77,7 @@ export const StoreList: React.FC> = ({ ))}
+
) } diff --git a/components/TimeseriesChart/Renderer.tsx b/components/TimeseriesChart/Renderer.tsx index 2980a1d..535c8bf 100644 --- a/components/TimeseriesChart/Renderer.tsx +++ b/components/TimeseriesChart/Renderer.tsx @@ -1,23 +1,98 @@ "use client" import React, { useEffect, useState } from "react" import LineChart from "components/LineChart" -import { getTimeseriesChartProps } from "./types" +import { TimeseriesChartProps } from "./types" import { timeSeriesConfig } from "utils/data/config" -import { TimeseriesColumns } from "components/LineChart/types" +import { requestTimeseries } from "utils/state/map" +import { store, useAppDispatch, useAppSelector } from "utils/state/store" +import { globals } from "utils/state/globals" +import { initializeDb, loadTimeseriesData } from "utils/state/thunks" +import { Provider } from "react-redux" +import { DimensionProps } from "components/LineChart/types" +import * as ToggleGroup from "@radix-ui/react-toggle-group" -const TimeseriesChart: React.FC = ({ id }) => { - const [status, setStatus] = useState<"loading" | "loaded" | "error">("loading") - const [dataset, setDataset] = useState("hhi") - const [variable, setVariable] = useState("average") +const datasetOptions = Object.entries(timeSeriesConfig).map(([key, value]) => ({ + key, + value, +})) + +const TimeseriesChart: React.FC = ({ id, placeName }) => { + const parentRef = React.useRef(null) + const [_snap, setSnapshot] = useState(0) + const [tsVariable, setTsVariable] = useState("hhi") + + const tsConfig = timeSeriesConfig[tsVariable] + const tsData = globals?.globalDs?.timeseriesResults?.[id]?.[tsVariable] + const ready = tsData?.length > 0 + + const dispatch = useAppDispatch() + const timeseriesLoaded = useAppSelector((state) => state.map.timeseriesDatasets) + const dbStatus = useAppSelector((state) => state.map.dbStatus) + + useEffect(() => { + dispatch(requestTimeseries(true)) + return () => {} + }, [dispatch]) + + useEffect(() => { + if (timeseriesLoaded.includes(tsVariable)) { + const main = async () => { + globals.globalDs.getTimeseries(id, tsVariable).then(() => { + setSnapshot(performance.now()) + }) + } + main() + } else if (dbStatus === "uninitialized") { + dispatch(initializeDb()) + } else if (dbStatus === "ready") { + dispatch(loadTimeseriesData(tsVariable)) + } + return () => {} + }, [dispatch, tsVariable, timeseriesLoaded, dbStatus]) + const Labels = labels[tsVariable] + const toggleGroupItemClasses = + "px-4 hover:bg-violet3 color-mauve11 data-[state=on]:bg-violet6 data-[state=on]:text-violet12 flex h-[35px] items-center justify-center bg-white text-base leading-4 first:rounded-l last:rounded-r focus:z-10 focus:shadow-[0_0_0_2px] focus:shadow-black focus:outline-none" return (
- TimeseriesChart {id} -
- {status === "loaded" ? ( - null - // - ) : status === "loading" ? ( +

+ {placeName} over time +

+
+ {setTsVariable(value as keyof typeof timeSeriesConfig)}} + > + + Market Concentration + + + Market Concentration (Dollar Stores) + + + Food Access + + + Food Access (Dollar Stores) + + +
+
+ {ready ? ( + + + + ) : !ready ? (

Loading data please wait

) : (

Failed to load data

@@ -27,4 +102,51 @@ const TimeseriesChart: React.FC = ({ id }) => { ) } -export default TimeseriesChart +const HhiLabels: React.FC> = ({ xMin, xMax, yMin, yMax }) => { + if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { + return null + } + // svg label at top right corner of chart + return ( + <> + + Less Concentrated Market + + + More Concentrated Market + + + ) +} + +const GravityLabels: React.FC> = ({ xMin, xMax, yMin, yMax }) => { + if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { + return null + } + // svg label at top right corner of chart + return ( + <> + + Poorer Food Supply Accessibility + + + Better Food Supply Accessibility + + + ) +} + +const labels: Record>> = { + hhi: HhiLabels, + hhiDs: HhiLabels, + gravity: GravityLabels, + gravityDs: GravityLabels, +} +const TimeseriesChartOuter: React.FC = ({ id, placeName }) => { + return ( + + + + ) +} +export default TimeseriesChartOuter diff --git a/components/TimeseriesChart/types.ts b/components/TimeseriesChart/types.ts index 1ed48e7..8829ed0 100644 --- a/components/TimeseriesChart/types.ts +++ b/components/TimeseriesChart/types.ts @@ -1,3 +1,4 @@ -export type getTimeseriesChartProps = { - id: string +export type TimeseriesChartProps = { + id: string, + placeName: string, } \ No newline at end of file diff --git a/package.json b/package.json index 6b4a66c..ad88196 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "coupling-graph": "npx madge --extensions js,jsx,ts,tsx,css,md,mdx ./ --exclude '.next|tailwind.config.js|reset.d.ts|prettier.config.js|postcss.config.js|playwright.config.ts|next.config.js|next-env.d.ts|instrumentation.ts|e2e/|README.md|.storybook/|.eslintrc.js' --image graph.svg" }, "dependencies": { + "@cutting/use-get-parent-size": "^2.1.13", "@deck.gl/geo-layers": "^8.9.34", "@deck.gl/layers": "^8.9.34", "@deck.gl/mapbox": "^8.9.34", @@ -126,6 +127,7 @@ "remark-mdx": "^3.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", + "resize-observer-polyfill": "^1.5.1", "simple-statistics": "^7.8.3", "ss": "^0.0.1", "tailwind-merge": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d195ae8..e5cbb10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@cutting/use-get-parent-size': + specifier: ^2.1.13 + version: 2.1.13(react-dom@18.2.0)(react@18.2.0)(resize-observer-polyfill@1.5.1)(use-debounce@10.0.0) '@deck.gl/geo-layers': specifier: ^8.9.34 version: 8.9.34(@deck.gl/core@8.9.34)(@deck.gl/extensions@8.9.34)(@deck.gl/layers@8.9.34)(@deck.gl/mesh-layers@8.9.34)(@loaders.gl/core@3.4.14)(@loaders.gl/gltf@3.4.14)(@loaders.gl/images@3.4.14)(@luma.gl/core@8.5.21)(@luma.gl/engine@8.5.21)(@luma.gl/gltools@8.5.21)(@luma.gl/shadertools@8.5.21)(@luma.gl/webgl@8.5.21) @@ -314,6 +317,9 @@ dependencies: remark-rehype: specifier: ^11.1.0 version: 11.1.0 + resize-observer-polyfill: + specifier: ^1.5.1 + version: 1.5.1 simple-statistics: specifier: ^7.8.3 version: 7.8.3 @@ -2069,6 +2075,25 @@ packages: requiresBuild: true optional: true + /@cutting/assert@0.1.4: + resolution: {integrity: sha512-k1LE8tCMKllAkb7ipdwCeK9DJVZlXu1IKW4ROXnvclsxtDPLLgkBx0oFRrrZeJ6J6nIubVZjC+DK7ilMboK/5A==} + dev: false + + /@cutting/use-get-parent-size@2.1.13(react-dom@18.2.0)(react@18.2.0)(resize-observer-polyfill@1.5.1)(use-debounce@10.0.0): + resolution: {integrity: sha512-IcO12FZLi43hLKCyfipvvnMkeAsV0sopTT61kOG576M5MCqUn9mJSYJb4COe/xJl5UKkGYyaGWBJ8iD5aS0hBQ==} + peerDependencies: + react: '>= 18.3.1' + react-dom: '>= 18.3.1' + resize-observer-polyfill: '>= ^1.5.x' + use-debounce: '>= 9.0.x' + dependencies: + '@cutting/assert': 0.1.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + resize-observer-polyfill: 1.5.1 + use-debounce: 10.0.0(react@18.2.0) + dev: false + /@deck.gl/aggregation-layers@8.9.34(@deck.gl/core@8.9.34)(@deck.gl/layers@8.9.34)(@luma.gl/core@8.5.21): resolution: {integrity: sha512-/JEDlj5MNFX8yHWPO5ljooGMdA2EPuZydbT6wrQD1WMydgp8dcEF+zVRLXTDWH1Mq+HLj6JHT1IhENHXN5TZFA==} peerDependencies: @@ -27338,6 +27363,10 @@ packages: resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==} dev: false + /resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + dev: false + /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -30006,6 +30035,15 @@ packages: scheduler: 0.19.0 dev: false + /use-debounce@10.0.0(react@18.2.0): + resolution: {integrity: sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==} + engines: {node: '>= 16.0.0'} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + dev: false + /use-deep-compare@1.2.1(react@17.0.2): resolution: {integrity: sha512-JTnOZAr0fq1ix6CQ4XANoWIh03xAiMFlP/lVAYDdAOZwur6nqBSdATn1/Q9PLIGIW+C7xmFZBCcaA4KLDcQJtg==} peerDependencies: diff --git a/utils/data/config.ts b/utils/data/config.ts index 01de0bc..c4c4a3a 100644 --- a/utils/data/config.ts +++ b/utils/data/config.ts @@ -174,20 +174,32 @@ export const columnsDict = { }, } as const +const minYear = 1997 +const maxYear = 2023 export const timeSeriesConfig = { hhi: { - columns: new Array(2021 - 1997).fill(null).map((_, i) => i + 1997), + file: "concentration_metrics_wide.parquet", + label: "Market Concentration", + columns: new Array(maxYear - minYear).fill(null).map((_, i) => i + minYear), }, hhiDs: { - columns: new Array(2021 - 1997).fill(null).map((_, i) => i + 1997), + file: "concentration_metrics_wide_ds.parquet", + label: "Market Concentration with Dollar Stores", + columns: new Array(maxYear - minYear).fill(null).map((_, i) => i + minYear), }, gravity: { + file: "gravity_no_dollar_pivoted.parquet", + label: "Food Access", columns: [2000, ...new Array(2021 - 2010).fill(null).map((_, i) => i + 2010)], }, gravityDs: { + file: "gravity_dollar_pivoted.parquet", + label: "Food Access with Dollar Stores", columns: [2000, ...new Array(2021 - 2010).fill(null).map((_, i) => i + 2010)], }, } as const +export type timeSeriesDatasets = typeof timeSeriesConfig[keyof typeof timeSeriesConfig]['file'] +export const defaultTimeseriesDataset = "concentration_metrics_wide.parquet" export const columnGroups: ColumnGroups = { "Market Concentration": { diff --git a/utils/data/service/service.ts b/utils/data/service/service.ts index e8d50dc..86adfd4 100644 --- a/utils/data/service/service.ts +++ b/utils/data/service/service.ts @@ -25,6 +25,7 @@ export const getDecimalsFromRange = (range: number) => { export class DataService> { data: Record>> = {} dbStatus: "none" | "loading" | "loaded" | "error" = "none" + timeseriesTables: string[] = [] baseURL: string = window?.location?.origin || "" conn?: AsyncDuckDBConnection tooltipResults: any = {} @@ -300,25 +301,36 @@ export class DataService> { this.tooltipResults[id] = formattedData return this.tooltipResults[id] } + rotate(data: Array>, columnLabel: string, valueLabel: string, columns: Array) { + const rotatedData = [] + for (const column of columns) { + rotatedData.push({ + [columnLabel]: column, + // @ts-ignore + [valueLabel]: data[column] + }) + } + return rotatedData + } async getTimeseries(id: string, variable: keyof typeof timeSeriesConfig) { if (this.timeseriesResults[id]?.[variable]) return const config = timeSeriesConfig[variable] + const file = config.file const columns = config.columns - .map( - (c) => ` - sum("${c}" * "TOTAL_POPULATION") / sum("TOTAL_POPULATION") as average_${c}, - median("${c}") as median_${c}, - approx_quantile("${c}", 0.75) as q75_${c}, - approx_quantile("${c}", 0.25) as q25_${c} - ` - ) - .join(", ") - const result = await this.runQuery(`SELECT ${columns} FROM ${dataTableName}`) + + const result = await this.runQuery(`SELECT * FROM ${file} WHERE "${this.idColumn}" LIKE '${id}'`) + if (!this.timeseriesResults[id]) { this.timeseriesResults[id] = {} } - - this.timeseriesResults[id][variable] = result[0] + const rotated = this.rotate(result[0], "year", "value", columns as any[]) + const dateParsed = rotated.map((r) => { + return { + ...r, + year: new Date(`01-02-${r.year}`), + } + }) + this.timeseriesResults[id][variable] = dateParsed } } diff --git a/utils/state/map.ts b/utils/state/map.ts index f189e6f..0a185e3 100644 --- a/utils/state/map.ts +++ b/utils/state/map.ts @@ -1,8 +1,8 @@ "use client" import { createSlice } from "@reduxjs/toolkit" import type { PayloadAction } from "@reduxjs/toolkit" -import { columnGroups, columnsDict, defaultColumn, defaultColumnGroup} from "utils/data/config" -import { fetchCentroidById, initializeDb } from "utils/state/thunks" +import { columnGroups, columnsDict, defaultColumn, defaultColumnGroup, defaultTimeseriesDataset, timeSeriesDatasets} from "utils/data/config" +import { fetchCentroidById, initializeDb, loadTimeseriesData } from "utils/state/thunks" import { MapState } from "./types" import { globals } from "./globals" @@ -16,7 +16,10 @@ const initialState: MapState = { snapshot: 0, breaks: [], colors: [], - tooltipStatus: undefined + tooltipStatus: undefined, + timeseriesDatasets: [], + timeseriesRequested: false, + currentTimeseriesDataset: defaultTimeseriesDataset, } export const mapSlice = createSlice({ @@ -44,6 +47,12 @@ export const mapSlice = createSlice({ state.currentColumn = action.payload state.colorFilter = undefined }, + requestTimeseries: (state, action: PayloadAction) => { + state.timeseriesRequested = true + }, + setTimeSeriesLoaded: (state, action: PayloadAction) => { + state.timeseriesDatasets.push(action.payload) + }, setTooltipInfo: (state, action: PayloadAction<{ x: number; y: number; id: string } | null>) => { state.tooltip = action.payload const id = action.payload?.id @@ -105,6 +114,9 @@ export const mapSlice = createSlice({ }), builder.addCase(initializeDb.fulfilled, (state, action) => { state.dbStatus = action.payload + }), + builder.addCase(loadTimeseriesData.fulfilled, (state, action) => { + state.timeseriesDatasets.push(action.meta.arg) }) }, }) @@ -119,6 +131,8 @@ export const { setCurrentFilter, setCurrentColumnGroup, upcertColorFilter, + requestTimeseries, + setTimeSeriesLoaded } = mapSlice.actions export default mapSlice.reducer diff --git a/utils/state/middleware.ts b/utils/state/middleware.ts index ea9361a..a713062 100644 --- a/utils/state/middleware.ts +++ b/utils/state/middleware.ts @@ -1,9 +1,10 @@ import { createListenerMiddleware } from "@reduxjs/toolkit" -import { mapSlice, setTooltipReady } from "utils/state/map" +import { mapSlice, setTooltipReady, setTimeSeriesLoaded } from "utils/state/map" import { MapState } from "utils/state/types" import { globals } from "./globals" import { columnsDict } from "utils/data/config" import { BivariateColorParamteres, MonovariateColorParamteres } from "utils/data/service/types" +import { loadTimeseriesData } from "./thunks" // Create the middleware instance and methods export const mapDataMiddleware = createListenerMiddleware<{ map: MapState }>() diff --git a/utils/state/thunks.ts b/utils/state/thunks.ts index 8ad1bf5..08109cf 100644 --- a/utils/state/thunks.ts +++ b/utils/state/thunks.ts @@ -1,9 +1,10 @@ import { createAsyncThunk } from "@reduxjs/toolkit" import { getDuckDB } from "duckdb-wasm-kit" import { DataService, dataTableName } from "utils/data/service/service" -import { idColumn } from "utils/data/config" +import { idColumn, timeSeriesDatasets } from "utils/data/config" import { globals } from "utils/state/globals" import { AsyncDuckDBConnection } from "@duckdb/duckdb-wasm" +import { timeSeriesConfig } from "utils/data/config" export const fetchCentroidById = createAsyncThunk("map/setCentroid", async (id: string) => { if (id === null) { @@ -54,3 +55,11 @@ export const initializeDb = createAsyncThunk("map/initDb", async () => { }) return "ready" }) + +export const loadTimeseriesData = createAsyncThunk("map/loadTimeseriesData", async (dataset: keyof typeof timeSeriesConfig) => { + const file = timeSeriesConfig[dataset].file + const buffer = await fetch(`${window.location.origin}/data/${file}`).then((r) => r.arrayBuffer()) + const dataArray = new Uint8Array(buffer) + await globals.globalDb.registerFileBuffer(file, dataArray) + return dataset +}) diff --git a/utils/state/types.ts b/utils/state/types.ts index 14057a7..90a237e 100644 --- a/utils/state/types.ts +++ b/utils/state/types.ts @@ -1,4 +1,4 @@ -import { columnGroups, columnsDict } from "utils/data/config" +import { columnGroups, columnsDict, timeSeriesConfig, timeSeriesDatasets } from "utils/data/config" export interface MapState { breaks: Array @@ -21,4 +21,7 @@ export interface MapState { } | null, tooltipStatus?: 'pending' | 'ready' snapshot: number + timeseriesRequested: boolean + timeseriesDatasets: Array + currentTimeseriesDataset?: timeSeriesDatasets } \ No newline at end of file