From 96f8832209b846db13c05ced8d11c51b1095ac96 Mon Sep 17 00:00:00 2001 From: AlexanderMelde <2115644+AlexanderMelde@users.noreply.github.com> Date: Sun, 3 Mar 2024 23:07:12 +0100 Subject: [PATCH] fix: add some interfaces and types to fix compile errors --- package-lock.json | 26 ++- package.json | 9 +- src/app/api/models.ts | 77 +++++++ src/app/api/test-data/route.ts | 156 +++++++------- src/app/api/test-data2/route.ts | 67 +++--- src/app/api/test-data3/route.ts | 39 ++-- src/app/api/test-data4/route.ts | 25 +-- src/app/data.ts | 51 +++-- src/app/page.tsx | 78 +++---- src/components/CarsharingChart.tsx | 64 +++--- src/components/EnergyMixChart.tsx | 75 ++++--- src/components/EnergySectorChart.tsx | 72 ++++--- src/components/GreenhouseGasesChart.tsx | 74 +++---- src/components/PlotFigure.jsx | 272 +++++++++++++----------- src/components/SoilTemperatureChart.tsx | 70 +++--- src/components/example-chart.tsx | 91 ++++---- src/components/example-chart2.tsx | 56 ++--- src/components/example-chart3.tsx | 74 +++---- src/components/models.ts | 10 + 19 files changed, 746 insertions(+), 640 deletions(-) create mode 100644 src/app/api/models.ts create mode 100644 src/components/models.ts diff --git a/package-lock.json b/package-lock.json index cda4b02..c46ecb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@observablehq/plot": "^0.6.13", "@types/d3": "^7.4.3", + "cross-env": "^7.0.3", "d3": "^7.8.5", "d3-dsv": "^3.0.1", "d3-geo": "^3.1.0", @@ -1441,11 +1442,27 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3478,8 +3495,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isoformat": { "version": "0.2.1", @@ -4090,7 +4106,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -4687,7 +4702,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4699,7 +4713,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -5332,7 +5345,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, diff --git a/package.json b/package.json index b30f415..497225e 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,14 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "cross-env NODE_ENV=production next build", "start": "next start", "lint": "next lint" }, "dependencies": { "@observablehq/plot": "^0.6.13", "@types/d3": "^7.4.3", + "cross-env": "^7.0.3", "d3": "^7.8.5", "d3-dsv": "^3.0.1", "d3-geo": "^3.1.0", @@ -20,14 +21,14 @@ "swr": "^2.2.5" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "eslint": "^8", + "eslint-config-next": "14.1.1", "postcss": "^8", "tailwindcss": "^3.3.0", - "eslint": "^8", - "eslint-config-next": "14.1.1" + "typescript": "^5" } } diff --git a/src/app/api/models.ts b/src/app/api/models.ts new file mode 100644 index 0000000..8046aa4 --- /dev/null +++ b/src/app/api/models.ts @@ -0,0 +1,77 @@ +export interface PortalResponse { + help: string, + success: boolean, + result: { + license_title: string, + maintainer: string, + resources: PortalResource[], + } +} + +export interface PortalResource { + mimetype: string; + url: string; +} + +export interface SensorDataEntry { + time: Date; + min: number; + max: number; + mean: number; +} + +export interface SensorDataEntryJSON { + time: string; + min: number; + max: number; + mean: number; +} + +export interface FeinstaubDataEntryOriginal { + ID: string; + Datum: string; + Zeit: string; + Breitengrad: string; + Laengengrad: string; + "PM2.5": string; + PM10: string; + temp: string; + humi: string; + pres: string; + WSpeed: string; + WAngle: string; + clouds: string; +} + +export interface FeinstaubDataEntry { + ID: string; + Datum: string; + Zeit: string; + Breitengrad: string; + Laengengrad: string; + "PM2.5": string; + PM10: string; + temp: string; + humi: string; + pres: string; + WSpeed: string; + WAngle: string; + clouds: string; + combinedTime: number; +} + +export interface GreenHouseGasEntry { + year: string; + category: string; + type: string; + co2: string; + note: string; +} + +export interface GreenHouseGasEntryImproved { + year: string; + category: string; + type: string; + co2: number; + note: string; +} \ No newline at end of file diff --git a/src/app/api/test-data/route.ts b/src/app/api/test-data/route.ts index bfde4cd..e9b1f70 100644 --- a/src/app/api/test-data/route.ts +++ b/src/app/api/test-data/route.ts @@ -1,92 +1,80 @@ import * as d3 from "d3"; -import * as geo from "d3-geo"; import * as dsv from "d3-dsv"; -import { NextRequest, NextResponse } from "next/server"; +import {NextRequest, NextResponse} from "next/server"; +import {PortalResponse, SensorDataEntry, SensorDataEntryJSON} from "@/app/api/models"; const fmt = dsv.dsvFormat(";"); export async function GET( - req: NextRequest, + req: NextRequest, ): Promise { - let agg: { - time: Date; - min: number; - max: number; - mean: number; - }[] = []; - - /* - TODO: unterschiedliche Tiefe, auch bodenfeuchte, lat & lon. - */ - - const f = await fetch( - "https://transparenz.karlsruhe.de/api/3/action/package_show?id=sensordaten-karlsruhe", - ).then((x) => x.json()); - - const urls = f["result"]["resources"].filter((x) => x.mimetype == "text/csv") - .map((x) => x.url); - - for ( - const url of urls - ) { - const response = await fetch(url); - - const data = fmt.parse(await response.text()); - - const data2 = d3.map( - data, - (row) => ({ - "bodentemperatur": row["bodentemperatur"], - "combinedTime": Date.parse( - row["Datum"].split("-").toReversed().join("-") + "T" + - row["Uhrzeit"] + - ":00.000Z", - ), - }), - ); - - const data3 = d3.map( - data2, - (row) => ({ - time: new Date(row["combinedTime"]), - bodentemperatur: Number.parseFloat( - row["bodentemperatur"].replace(",", "."), - ), - }), - ); - - const data4 = d3.filter( - data3, - (row) => row.bodentemperatur < 100 && row.bodentemperatur > -100, - ); - - const groups = d3.group( - data4, - (row) => d3.utcDay.floor(row.time), - ); - - const groups2 = d3.map( - groups, - ([l, r]) => ({ key: l, val: d3.map(r, (x) => x.bodentemperatur) }), - ); - - const agg2 = d3.map( - groups2, - (r) => ({ - time: r.key, - min: d3.min(r.val)!, - max: d3.max(r.val)!, - mean: d3.mean(r.val)!, - }), - ); - - agg = [...agg, ...agg2]; - } - - const agg2 = d3.sort(agg, (l, r) => l.time.getTime() - r.time.getTime()); - const agg3 = d3.map(agg2, (x) => ({ ...x, time: x.time.toJSON() })); - - return NextResponse.json({ - data: agg3, - }); + let agg: SensorDataEntry[] = []; + + /* + TODO: unterschiedliche Tiefe, auch bodenfeuchte, lat & lon. + */ + + const fetchResponse = await fetch("https://transparenz.karlsruhe.de/api/3/action/package_show?id=sensordaten-karlsruhe") + const portalResponse: PortalResponse = await fetchResponse.json(); + + const portalResourceURLs = portalResponse.result.resources.filter(portalResource => portalResource.mimetype == "text/csv").map(({url}) => url); + + for (const url of portalResourceURLs) { + const response = await fetch(url); + const data = fmt.parse(await response.text()); + + const data2 = d3.map( + data, + (row) => ({ + "bodentemperatur": row["bodentemperatur"], + "combinedTime": Date.parse( + row["Datum"].split("-").toReversed().join("-") + "T" + + row["Uhrzeit"] + + ":00.000Z", + ), + }), + ); + + const data3 = d3.map( + data2, + (row) => ({ + time: new Date(row["combinedTime"]), + bodentemperatur: Number.parseFloat( + row["bodentemperatur"].replace(",", "."), + ), + }), + ); + + const data4 = d3.filter( + data3, + (row) => row.bodentemperatur < 100 && row.bodentemperatur > -100, + ); + + const groups = d3.group( + data4, + (row) => d3.utcDay.floor(row.time), + ); + + const groups2 = d3.map( + groups, + ([l, r]) => ({key: l, val: d3.map(r, (x) => x.bodentemperatur)}), + ); + + const agg2 = d3.map( + groups2, + (r) => ({ + time: r.key, + min: d3.min(r.val)!, + max: d3.max(r.val)!, + mean: d3.mean(r.val)!, + }), + ); + + agg = [...agg, ...agg2]; + } + + const agg2 = d3.sort(agg, (l, r) => l.time.getTime() - r.time.getTime()); + const agg3: SensorDataEntryJSON[] = d3.map(agg2, (x) => ({...x, time: x.time.toJSON()})); + + return NextResponse.json({data: agg3}); } diff --git a/src/app/api/test-data2/route.ts b/src/app/api/test-data2/route.ts index fe6c8a5..f566c00 100644 --- a/src/app/api/test-data2/route.ts +++ b/src/app/api/test-data2/route.ts @@ -1,40 +1,39 @@ -import * as d3 from "d3"; -import * as geo from "d3-geo"; import * as dsv from "d3-dsv"; -import { NextRequest, NextResponse } from "next/server"; -import { STR } from "./ODD24_Monitoring_Klimaschutz_KA2022"; +import {NextRequest, NextResponse} from "next/server"; +import {STR} from "./ODD24_Monitoring_Klimaschutz_KA2022"; const fmt = dsv.dsvFormat(";"); +export type returnEntryType = [string, string, string, number]; + +interface dataRowType { + Maßnahme: string; + Jahr: string; + + [year: string]: string; +} + export async function GET(req: NextRequest): Promise { - let agg: { - time: string; - min: number; - max: number; - mean: number; - }[] = []; - - const data = fmt.parse(STR); - - const data2 = data.map((x) => - Object.fromEntries(Object.entries(x).map((x) => [x[0].trim(), x[1].trim()])) - ); - - const data3 = data2.flatMap((x) => - Object.keys(x).filter((x) => x.startsWith("20")) - .map( - ( - y, - ) => [ - x["Maßnahme"], - x["Jahr"], - y, - Number.parseFloat(x[y].replace(".", "").replace(",", ".")), - ], - ) - ); - - return NextResponse.json({ - data: data3, - }); + const dataRows = fmt.parse(STR); + + const trimmedDataRows: dataRowType[] = dataRows.map((dataRow) => { + const trimmedKeyValuePairs = Object.entries(dataRow).map(([key, value]) => [key.trim(), value.trim()]); + return Object.fromEntries(trimmedKeyValuePairs) + }); + + const returnData: returnEntryType[] = trimmedDataRows.flatMap((dataRow: dataRowType) => { + const yearKeys = Object.keys(dataRow).filter(key => key.startsWith("20")) + return yearKeys.map(yearKey => { + const yearValue = dataRow[yearKey]; + const yearValueCleaned = yearValue.replace(".", "").replace(",", "."); + const yearValueNumber = Number.parseFloat(yearValueCleaned) + const returnEntry: returnEntryType = [dataRow.Maßnahme, dataRow.Jahr, yearKey, yearValueNumber] + return returnEntry + }); + } + ); + + return NextResponse.json({ + data: returnData, + }); } diff --git a/src/app/api/test-data3/route.ts b/src/app/api/test-data3/route.ts index d6d24e7..1b20c0e 100644 --- a/src/app/api/test-data3/route.ts +++ b/src/app/api/test-data3/route.ts @@ -1,34 +1,27 @@ import * as d3 from "d3"; -import * as geo from "d3-geo"; import * as dsv from "d3-dsv"; -import { NextRequest, NextResponse } from "next/server"; -import { SRC } from "./mobile Feinstaubdaten Hackathon"; +import {NextRequest, NextResponse} from "next/server"; +import {SRC} from "./mobile Feinstaubdaten Hackathon"; +import {FeinstaubDataEntry, FeinstaubDataEntryOriginal} from "@/app/api/models"; const fmt = dsv.dsvFormat(","); export async function GET(req: NextRequest): Promise { - let agg: { - time: string; - min: number; - max: number; - mean: number; - }[] = []; + const rawData: FeinstaubDataEntryOriginal[] = fmt.parse(SRC); - const data = fmt.parse(SRC); + const dataFilteredByIDs = d3.filter( + rawData, + (x) => Number.parseInt(x["ID"]) >= 0 && Number.parseInt(x["ID"]) <= 4, + ); - const data2 = d3.filter( - data, - (x) => Number.parseInt(x["ID"]) >= 0 && Number.parseInt(x["ID"]) <= 4, - ); + const dataWithCombinedTime: FeinstaubDataEntry[] = d3.map(dataFilteredByIDs, (row) => ({ + ...row, + combinedTime: Date.parse(row.Datum + "T" + row.Zeit + ".000Z"), + })); - const data3 = d3.map(data2, (row) => ({ - ...row, - combinedTime: Date.parse(row["Datum"] + "T" + row["Zeit"] + ".000Z"), - })); + const sortedData: FeinstaubDataEntry[] = d3.sort(dataWithCombinedTime, (x, y) => x.combinedTime - y.combinedTime); - const data4 = d3.sort(data3, (x, y) => x["combinedTime"] - y["combinedTime"]); - - return NextResponse.json({ - data: data4, - }); + return NextResponse.json({ + data: sortedData, + }); } diff --git a/src/app/api/test-data4/route.ts b/src/app/api/test-data4/route.ts index d912805..ae35423 100644 --- a/src/app/api/test-data4/route.ts +++ b/src/app/api/test-data4/route.ts @@ -1,22 +1,9 @@ import * as d3 from "d3"; -import * as geo from "d3-geo"; -import * as dsv from "d3-dsv"; -import { NextRequest, NextResponse } from "next/server"; +import {NextRequest, NextResponse} from "next/server"; import csvInput from './data'; +import {GreenHouseGasEntry} from "@/app/api/models"; -export async function GET( - req: NextRequest, -): Promise { - let agg: { - time: string; - min: number; - max: number; - mean: number; - }[] = []; - - const data = d3.csvParse(csvInput); - - return NextResponse.json({ - data: data, - }); -} +export async function GET(req: NextRequest,): Promise { + const data: GreenHouseGasEntry[] = d3.csvParse(csvInput); + return NextResponse.json({data: data,}); +} \ No newline at end of file diff --git a/src/app/data.ts b/src/app/data.ts index 22ad951..2f43ccb 100644 --- a/src/app/data.ts +++ b/src/app/data.ts @@ -1,38 +1,37 @@ "use client"; import useSWR from "swr"; +import {FeinstaubDataEntry, GreenHouseGasEntry, SensorDataEntryJSON} from "@/app/api/models"; const fetcher = ( - input: URL | RequestInfo, - init?: RequestInit | undefined, + input: URL | RequestInfo, + init?: RequestInit | undefined, ) => fetch(input, init).then((res) => res.json()); -export function useData1() { - const { data, error, isLoading } = useSWR(`/api/test-data`, fetcher); - - return { - data: data, - isLoading, - isError: error, - }; +export function useData1(): { + data: { data: SensorDataEntryJSON[] }, + isLoading: boolean, + isError: boolean, +} { + const {data, error, isLoading} = useSWR(`/api/test-data`, fetcher); + return {data, isLoading, isError: error}; } -export function useData4() { - const { data, error, isLoading } = useSWR(`/api/test-data4`, fetcher); - - - return { - data: data, - isLoading, - isError: error, - }; +export function useData3(): { + data: { data: FeinstaubDataEntry[] }, + isLoading: boolean, + isError: boolean, +} { + const {data, error, isLoading} = useSWR(`/api/test-data3`, fetcher); + return {data, isLoading, isError: error}; } -export function useData3() { - const { data, error, isLoading } = useSWR(`/api/test-data3`, fetcher); - return { - data: data, - isLoading, - isError: error, - }; +export function useData4(): { + data: { data: GreenHouseGasEntry[] }, + isLoading: boolean, + isError: boolean, +} { + const {data, error, isLoading} = useSWR(`/api/test-data4`, fetcher); + return {data, isLoading, isError: error}; } + diff --git a/src/app/page.tsx b/src/app/page.tsx index f8009ca..edb4210 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,44 +5,46 @@ import ExampleChart from "@/components/example-chart"; import ExampleChart3 from "@/components/example-chart3"; import GreenhouseGasesChart from "@/components/GreenhouseGasesChart"; import CarsharingChart from "@/components/CarsharingChart"; +import Image from "next/image"; export default function Home() { - return ( -
-
-

- Klimadashboard der Stadt{" "} - Karlsruhe -

-
-
-
-

Klima

-
- - - -
-
-
-

Energieverbrauch

-
- - -
-
-
-

Verkehr

-
- - -
-
-
-
- Made with ❤️ @"Open Data Days 2024" - Code for Karlsruhe -
-
- ); + return ( +
+
+

+ Klimadashboard der Stadt{" "} + Karlsruhe +

+
+
+
+

Klima

+
+ + + +
+
+
+

Energieverbrauch

+
+ + +
+
+
+

Verkehr

+
+ + +
+
+
+
+ Made with ❤️ @"Open Data Days 2024" + Code for Karlsruhe +
+
+ ); } diff --git a/src/components/CarsharingChart.tsx b/src/components/CarsharingChart.tsx index 36a3c6e..bc47f3f 100644 --- a/src/components/CarsharingChart.tsx +++ b/src/components/CarsharingChart.tsx @@ -1,46 +1,48 @@ "use client"; -import PlotFigure from "@/components/PlotFigure"; import Card from "@/components/Card"; import * as Plot from "@observablehq/plot"; -import * as d3 from "d3"; -import { useEffect, useMemo, useRef } from "react"; +import {ElementRef, useEffect, useMemo, useRef} from "react"; import useSWR from "swr"; +import {returnEntryType} from "@/app/api/test-data2/route"; +import {yearValuePair} from "@/components/models"; const fetcher = (input: URL | RequestInfo, init?: RequestInit | undefined) => - fetch(input, init).then((res) => res.json()); + fetch(input, init).then((res) => res.json()); const CarsharingChart = () => { - const containerRef = useRef(); + const containerRef = useRef>(null); - const { data } = useSWR("api/test-data2", fetcher); + const {data} = useSWR("api/test-data2", fetcher); - const data2 = useMemo( - () => - data?.data - ?.filter((d) => d[1] === "Angemeldete Carsharing-Nutzer*") - .map((d) => ({ - year: d[2], - value: d[3], - })), - [data] - ); + const data2 = useMemo( + () => { + const testData2: returnEntryType[] | undefined = data?.data; + const carsharingEntries = testData2?.filter(entry => entry[1] === "Angemeldete Carsharing-Nutzer*"); + const carsharingData: yearValuePair[] | undefined = carsharingEntries?.map(entry => ({ + year: entry[2], + value: entry[3] + })) + return carsharingData + }, + [data] + ); - useEffect(() => { - if (data2) { - const plot = Plot.plot({ - color: { legend: true, scheme: "BuYlRd" }, - marks: [Plot.lineY(data2, { x: "year", y: "value" })], - }); - containerRef.current.append(plot); - return () => plot.remove(); - } - }, [data2]); + useEffect(() => { + if (data2) { + const plot = Plot.plot({ + color: {legend: true, scheme: "BuYlRd"}, + marks: [Plot.lineY(data2, {x: "year", y: "value"})], + }); + containerRef.current?.append(plot); + return () => plot.remove(); + } + }, [data2]); - return ( - -
- - ); + return ( + +
+ + ); }; export default CarsharingChart; diff --git a/src/components/EnergyMixChart.tsx b/src/components/EnergyMixChart.tsx index 34d431a..c4e4b44 100644 --- a/src/components/EnergyMixChart.tsx +++ b/src/components/EnergyMixChart.tsx @@ -1,53 +1,52 @@ "use client"; -import PlotFigure from "@/components/PlotFigure"; import Card from "@/components/Card"; import * as Plot from "@observablehq/plot"; -import * as d3 from "d3"; -import { useEffect, useMemo, useRef } from "react"; +import {ElementRef, useEffect, useMemo, useRef} from "react"; import useSWR from "swr"; +import {returnEntryType} from "@/app/api/test-data2/route"; +import {yearSourceValue} from "@/components/models"; const fetcher = (input: URL | RequestInfo, init?: RequestInit | undefined) => - fetch(input, init).then((res) => res.json()); + fetch(input, init).then((res) => res.json()); const EnergyMixChart = () => { - const containerRef = useRef(); + const containerRef = useRef>(null); - const { data } = useSWR("api/test-data2", fetcher); + const {data} = useSWR("api/test-data2", fetcher); - const data2 = useMemo( - () => - data?.data - ?.filter((d) => d[0] === "Energieverbrauch nach Energieträgern") - .map((d) => ({ - year: d[2], - source: d[1], - value: d[3], - })), - [data], - ); + const data2 = useMemo( + () => { + const testData2: returnEntryType[] | undefined = data?.data; + const energyMixEntries = testData2?.filter(entry => entry[0] === "Energieverbrauch nach Energieträgern"); + const energyMixData: yearSourceValue[] | undefined = energyMixEntries?.map(entry => ({ + year: entry[2], + source: entry[1], + value: entry[3] + })) + return energyMixData + }, + [data] + ); - useEffect(() => { - if (data2) { - const plot = Plot.plot({ - color: { legend: true, scheme: "BuYlRd" }, - marks: [ - Plot.barY(data2, { x: "year", y: "value", fill: "source" }), - Plot.crosshair(data2, { x: "year", y: "value" }), - ], - }); - containerRef.current.append(plot); - return () => plot.remove(); - } - }, [data2]); + useEffect(() => { + if (data2) { + const plot = Plot.plot({ + color: {legend: true, scheme: "BuYlRd"}, + marks: [ + Plot.barY(data2, {x: "year", y: "value", fill: "source"}), + Plot.crosshair(data2, {x: "year", y: "value"}), + ], + }); + containerRef.current!.append(plot); + return () => plot.remove(); + } + }, [data2]); - return ( - -
- - ); + return ( + +
+ + ); }; export default EnergyMixChart; diff --git a/src/components/EnergySectorChart.tsx b/src/components/EnergySectorChart.tsx index 9c2d078..beac4e2 100644 --- a/src/components/EnergySectorChart.tsx +++ b/src/components/EnergySectorChart.tsx @@ -1,50 +1,52 @@ "use client"; -import PlotFigure from "@/components/PlotFigure"; import Card from "@/components/Card"; import * as Plot from "@observablehq/plot"; -import * as d3 from "d3"; -import { useEffect, useMemo, useRef } from "react"; +import {ElementRef, useEffect, useMemo, useRef} from "react"; import useSWR from "swr"; +import {returnEntryType} from "@/app/api/test-data2/route"; +import {yearSourceValue} from "@/components/models"; const fetcher = (input: URL | RequestInfo, init?: RequestInit | undefined) => - fetch(input, init).then((res) => res.json()); + fetch(input, init).then((res) => res.json()); const EnergySectorChart = () => { - const containerRef = useRef(); + const containerRef = useRef>(null); - const { data } = useSWR("api/test-data2", fetcher); + const {data} = useSWR("api/test-data2", fetcher); - const data2 = useMemo( - () => - data?.data - ?.filter((d) => d[0] === "Energieverbrauch nach Sektoren") - .map((d) => ({ - year: d[2], - source: d[1], - value: d[3], - })), - [data], - ); + const data2 = useMemo( + () => { + const testData2: returnEntryType[] | undefined = data?.data; + const energySectorEntries = testData2?.filter(entry => entry[0] === "Energieverbrauch nach Sektoren"); + const energySectorData: yearSourceValue[] | undefined = energySectorEntries?.map(entry => ({ + year: entry[2], + source: entry[1], + value: entry[3] + })) + return energySectorData + }, + [data] + ); - useEffect(() => { - if (data2) { - const plot = Plot.plot({ - color: { legend: true, scheme: "BuYlRd" }, - marks: [ - Plot.barY(data2, { x: "year", y: "value", fill: "source" }), - Plot.crosshair(data2, { x: "year", y: "value" }), - ], - }); - containerRef.current.append(plot); - return () => plot.remove(); - } - }, [data2]); + useEffect(() => { + if (data2) { + const plot = Plot.plot({ + color: {legend: true, scheme: "BuYlRd"}, + marks: [ + Plot.barY(data2, {x: "year", y: "value", fill: "source"}), + Plot.crosshair(data2, {x: "year", y: "value"}), + ], + }); + containerRef.current!.append(plot); + return () => plot.remove(); + } + }, [data2]); - return ( - -
- - ); + return ( + +
+ + ); }; export default EnergySectorChart; diff --git a/src/components/GreenhouseGasesChart.tsx b/src/components/GreenhouseGasesChart.tsx index a19238c..548fa4b 100644 --- a/src/components/GreenhouseGasesChart.tsx +++ b/src/components/GreenhouseGasesChart.tsx @@ -1,48 +1,44 @@ "use client"; -import { useEffect, useRef } from "react"; +import {ElementRef, useEffect, useRef} from "react"; import * as Plot from "@observablehq/plot"; -import * as d3 from "d3"; -import { useData4 } from "@/app/data"; +import {useData4} from "@/app/data"; import Card from "./Card"; +import {GreenHouseGasEntryImproved} from "@/app/api/models"; const GreenhouseGasesChart: React.FC = () => { - const { data, isError, isLoading } = useData4(); - - const containerRef = useRef(); - - useEffect(() => { - if (isLoading || !data) return; - - const betterData = data.data.map(({ co2, ...x }) => ({ - co2: parseInt(co2), - ...x, - })); - - const plot = Plot.plot({ - color: { legend: true }, - marks: [ - Plot.rectY(betterData, { - x: "year", - y: "co2", - fill: "category", - }), - Plot.crosshair(betterData, { x: "year", y: "co2" }), - ], - }); - - containerRef.current.append(plot); - return () => plot.remove(); - }, [isLoading, data]); - - return ( - -
- - ); + const {data, isError, isLoading} = useData4(); + + const containerRef = useRef>(null); + + useEffect(() => { + if (isLoading || !data) return; + + const betterData: GreenHouseGasEntryImproved[] = data.data.map(({co2, ...x}) => ({ + co2: parseInt(co2), + ...x, + })); + + const plot = Plot.plot({ + color: {legend: true}, + marks: [ + Plot.rectY(betterData, {x: "year", y: "co2", fill: "category"}), + Plot.crosshair(betterData, {x: "year", y: "co2"}), + ], + }); + + containerRef.current!.append(plot); + return () => plot.remove(); + }, [isLoading, data]); + + return ( + +
+ + ); }; export default GreenhouseGasesChart; diff --git a/src/components/PlotFigure.jsx b/src/components/PlotFigure.jsx index 98500b5..e288d8d 100644 --- a/src/components/PlotFigure.jsx +++ b/src/components/PlotFigure.jsx @@ -1,139 +1,169 @@ import * as Plot from "@observablehq/plot"; -import { createElement as h } from "react"; +import {createElement as h} from "react"; import Card from "./Card"; // For client-side rendering, see https://codesandbox.io/s/plot-react-csr-p4cr7t?file=/src/PlotFigure.jsx // Based on https://github.com/observablehq/plot/blob/main/docs/components/PlotRender.js -export default function PlotFigure({ options, title, description }) { - return ( - - {Plot.plot({ ...options, document: new Document() }).toHyperScript()} - - ); +export default function PlotFigure({options, title, description}) { + return ( + + {Plot.plot({...options, document: new PlotDocument()}).toHyperScript()} + + ); } -class Document { - constructor() { - this.documentElement = new Element(this, "html"); - } - createElementNS(namespace, tagName) { - return new Element(this, tagName); - } - createElement(tagName) { - return new Element(this, tagName); - } - createTextNode(value) { - return new TextNode(this, value); - } - querySelector() { - return null; - } - querySelectorAll() { - return []; - } +class PlotDocument { + constructor() { + this.documentElement = new Element(this, "html"); + } + + createElementNS(namespace, tagName) { + return new Element(this, tagName); + } + + createElement(tagName) { + return new Element(this, tagName); + } + + createTextNode(value) { + return new TextNode(this, value); + } + + querySelector() { + return null; + } + + querySelectorAll() { + return []; + } } class Style { - static empty = new Style(); - setProperty() {} - removeProperty() {} + static empty = new Style(); + + setProperty() { + } + + removeProperty() { + } } class Element { - constructor(ownerDocument, tagName) { - this.ownerDocument = ownerDocument; - this.tagName = tagName; - this.attributes = {}; - this.children = []; - this.parentNode = null; - } - setAttribute(name, value) { - this.attributes[name] = String(value); - } - setAttributeNS(namespace, name, value) { - this.setAttribute(name, value); - } - getAttribute(name) { - return this.attributes[name]; - } - getAttributeNS(name) { - return this.getAttribute(name); - } - hasAttribute(name) { - return name in this.attributes; - } - hasAttributeNS(name) { - return this.hasAttribute(name); - } - removeAttribute(name) { - delete this.attributes[name]; - } - removeAttributeNS(namespace, name) { - this.removeAttribute(name); - } - addEventListener() { - // ignored; interaction needs real DOM - } - removeEventListener() { - // ignored; interaction needs real DOM - } - dispatchEvent() { - // ignored; interaction needs real DOM - } - append(...children) { - for (const child of children) { - this.appendChild( - child?.ownerDocument ? child : this.ownerDocument.createTextNode(child) - ); - } - } - appendChild(child) { - this.children.push(child); - child.parentNode = this; - return child; - } - insertBefore(child, after) { - if (after == null) { - this.children.push(child); - } else { - const i = this.children.indexOf(after); - if (i < 0) throw new Error("insertBefore reference node not found"); - this.children.splice(i, 0, child); - } - child.parentNode = this; - return child; - } - querySelector() { - return null; - } - querySelectorAll() { - return []; - } - set textContent(value) { - this.children = [this.ownerDocument.createTextNode(value)]; - } - set style(value) { - this.attributes.style = value; - } - get style() { - return Style.empty; - } - toHyperScript() { - return h( - this.tagName, - this.attributes, - this.children.map((c) => c.toHyperScript()) - ); - } + constructor(ownerDocument, tagName) { + this.ownerDocument = ownerDocument; + this.tagName = tagName; + this.attributes = {}; + this.children = []; + this.parentNode = null; + } + + set textContent(value) { + this.children = [this.ownerDocument.createTextNode(value)]; + } + + get style() { + return Style.empty; + } + + set style(value) { + this.attributes.style = value; + } + + setAttribute(name, value) { + this.attributes[name] = String(value); + } + + setAttributeNS(namespace, name, value) { + this.setAttribute(name, value); + } + + getAttribute(name) { + return this.attributes[name]; + } + + getAttributeNS(name) { + return this.getAttribute(name); + } + + hasAttribute(name) { + return name in this.attributes; + } + + hasAttributeNS(name) { + return this.hasAttribute(name); + } + + removeAttribute(name) { + delete this.attributes[name]; + } + + removeAttributeNS(namespace, name) { + this.removeAttribute(name); + } + + addEventListener() { + // ignored; interaction needs real DOM + } + + removeEventListener() { + // ignored; interaction needs real DOM + } + + dispatchEvent() { + // ignored; interaction needs real DOM + } + + append(...children) { + for (const child of children) { + this.appendChild( + child?.ownerDocument ? child : this.ownerDocument.createTextNode(child) + ); + } + } + + appendChild(child) { + this.children.push(child); + child.parentNode = this; + return child; + } + + insertBefore(child, after) { + if (after == null) { + this.children.push(child); + } else { + const i = this.children.indexOf(after); + if (i < 0) throw new Error("insertBefore reference node not found"); + this.children.splice(i, 0, child); + } + child.parentNode = this; + return child; + } + + querySelector() { + return null; + } + + querySelectorAll() { + return []; + } + + toHyperScript() { + return h( + this.tagName, + this.attributes, + this.children.map((c) => c.toHyperScript()) + ); + } } class TextNode { - constructor(ownerDocument, nodeValue) { - this.ownerDocument = ownerDocument; - this.nodeValue = String(nodeValue); - } - toHyperScript() { - return this.nodeValue; - } + constructor(ownerDocument, nodeValue) { + this.ownerDocument = ownerDocument; + this.nodeValue = String(nodeValue); + } + + toHyperScript() { + return this.nodeValue; + } } diff --git a/src/components/SoilTemperatureChart.tsx b/src/components/SoilTemperatureChart.tsx index 7da3856..fe2ec25 100644 --- a/src/components/SoilTemperatureChart.tsx +++ b/src/components/SoilTemperatureChart.tsx @@ -1,49 +1,49 @@ "use client"; -import { useEffect, useRef } from "react"; +import {ElementRef, useEffect, useRef} from "react"; import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; -import { useData1 } from "@/app/data"; +import {useData1} from "@/app/data"; import Card from "./Card"; const SoilTemperatureChart: React.FC = () => { - const { data, isError, isLoading } = useData1(); + const {data, isError, isLoading} = useData1(); - const containerRef = useRef(); + const containerRef = useRef>(null); - const d = () => - d3.map(data.data, (x) => ({ ...x, time: new Date(x.time) })) - .flatMap( - (x) => [{ time: x.time, v: x.min, c: "min" }, { - time: x.time, - v: x.max, - c: "max", - }, { time: x.time, v: x.mean, c: "mean" }], - ); + useEffect(() => { + const dataFactory = () => + d3.map(data.data, (x) => ({...x, time: new Date(x.time)})) + .flatMap( + (x) => [{time: x.time, v: x.min, c: "min"}, { + time: x.time, + v: x.max, + c: "max", + }, {time: x.time, v: x.mean, c: "mean"}], + ); - useEffect(() => { - if (isLoading || !data) return; - const plot = Plot.plot({ - color: { legend: true }, - marks: [ - Plot.line(d(), { - x: "time", - y: "v", - stroke: "c", - }), - Plot.ruleY([0]), - Plot.crosshair(d(), { x: "time", y: "v" }), - ], - }); - containerRef.current.append(plot); - return () => plot.remove(); - }, [isLoading, data]); + if (isLoading || !data) return; + const plot = Plot.plot({ + color: {legend: true}, + marks: [ + Plot.line(dataFactory(), { + x: "time", + y: "v", + stroke: "c", + }), + Plot.ruleY([0]), + Plot.crosshair(dataFactory(), {x: "time", y: "v"}), + ], + }); + containerRef.current!.append(plot); + return () => plot.remove(); + }, [isLoading, data]); - return ( - -
- - ); + return ( + +
+ + ); }; export default SoilTemperatureChart; diff --git a/src/components/example-chart.tsx b/src/components/example-chart.tsx index 0994345..5dbac48 100644 --- a/src/components/example-chart.tsx +++ b/src/components/example-chart.tsx @@ -1,52 +1,59 @@ "use client"; -import { useEffect, useRef } from "react"; +import {ElementRef, useEffect, useRef} from "react"; import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; -import { useData1 } from "@/app/data"; +import {useData1} from "@/app/data"; import Card from "./Card"; +import {SensorDataEntry} from "@/app/api/models"; + +interface aggregationEntry { + time: Date; + v: number; + c: "max" | "min" | "mean" +} const ExampleChart: React.FC = () => { - const { data, isError, isLoading } = useData1(); - - const containerRef = useRef(); - - const d = () => - d3.map(data.data, (x) => ({ ...x, time: new Date(x.time) })) - .flatMap( - (x) => [{ time: x.time, v: x.min, c: "min" }, { - time: x.time, - v: x.max, - c: "max", - }, { time: x.time, v: x.mean, c: "mean" }], - ); - - useEffect(() => { - if (isLoading || !data) return; - const plot = Plot.plot({ - color: { legend: true }, - marks: [ - Plot.line(d(), { - x: "time", - y: "v", - stroke: "c", - }), - Plot.ruleY([0]), - Plot.crosshair(d(), { x: "time", y: "v" }), - ], - }); - containerRef.current.append(plot); - return () => plot.remove(); - }, [isLoading, data]); - - return ( - -
- - ); + const {data, isError, isLoading} = useData1(); + + const containerRef = useRef>(null); + + + useEffect(() => { + const dataFactory = () => + d3.map(data.data, (entry) => ({...entry, time: new Date(entry.time)})) + .flatMap( + (x: SensorDataEntry) => { + const aggregationEntries: aggregationEntry[] = [ + {time: x.time, v: x.min, c: "min"}, + {time: x.time, v: x.max, c: "max"}, + {time: x.time, v: x.mean, c: "mean"} + ]; + return aggregationEntries; + } + ); + + if (isLoading || !data) return; + const plot = Plot.plot({ + color: {legend: true}, + marks: [ + Plot.line(dataFactory(), {x: "time", y: "v", stroke: "c",}), + Plot.ruleY([0]), + Plot.crosshair(dataFactory(), {x: "time", y: "v"}), + ], + }); + containerRef.current!.append(plot); + return () => plot.remove(); + }, [isLoading, data]); + + return ( + +
+ + ); }; export default ExampleChart; diff --git a/src/components/example-chart2.tsx b/src/components/example-chart2.tsx index d94f50f..9b0989f 100644 --- a/src/components/example-chart2.tsx +++ b/src/components/example-chart2.tsx @@ -1,42 +1,42 @@ "use client"; -import { useEffect, useRef } from "react"; +import {ElementRef, useEffect, useRef} from "react"; import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; -import { useData1 } from "@/app/data"; +import {useData1} from "@/app/data"; import Card from "./Card"; const ExampleChart: React.FC = () => { - const { data, isError, isLoading } = useData1(); + const {data, isError, isLoading} = useData1(); - const containerRef = useRef(); + const containerRef = useRef>(null); - const d = () => d3.map(data.data, (x) => ({ ...x, time: new Date(x.time) })); + useEffect(() => { + const dataFactory = () => d3.map(data.data, (x) => ({...x, time: new Date(x.time)})); - useEffect(() => { - if (isLoading || !data) return; - const plot = Plot.plot({ - color: { legend: true, scheme: "BuRd" }, - marks: [ - Plot.cellX(d(), { - x: (d) => d.time.getUTCDate(), - y: (d) => d.time.getUTCMonth(), - fill: "max", - }), - ], - }); - containerRef.current.append(plot); - return () => plot.remove(); - }, [isLoading, data]); + if (isLoading || !data) return; + const plot = Plot.plot({ + color: {legend: true, scheme: "BuRd"}, + marks: [ + Plot.cellX(dataFactory(), { + x: (d) => d.time.getUTCDate(), + y: (d) => d.time.getUTCMonth(), + fill: "max", + }), + ], + }); + containerRef.current!.append(plot); + return () => plot.remove(); + }, [isLoading, data]); - return ( - -
- - ); + return ( + +
+ + ); }; export default ExampleChart; diff --git a/src/components/example-chart3.tsx b/src/components/example-chart3.tsx index 67ba2bc..a4388e1 100644 --- a/src/components/example-chart3.tsx +++ b/src/components/example-chart3.tsx @@ -1,46 +1,48 @@ "use client"; -import { useEffect, useRef } from "react"; +import {ElementRef, useEffect, useRef} from "react"; import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; -import { useData3 } from "@/app/data"; +import {useData3} from "@/app/data"; import Card from "./Card"; const ExampleChart: React.FC = () => { - const { data, isError, isLoading } = useData3(); - - const containerRef = useRef(); - - const d = () => - d3.map(data.data, (x) => ({ ...x, time: new Date(x.combinedTime) })); - - useEffect(() => { - if (isLoading || !data) return; - const plot = Plot.plot({ - marks: [ - Plot.rectY( - d(), - Plot.binX({ y: "p50" }, { - x: "time", - y: "PM10", - fill: "ID", - }), - ), - Plot.ruleY([0]), - ], - }); - containerRef.current.append(plot); - return () => plot.remove(); - }, [isLoading, data]); - - return ( - -
- - ); + const {data, isError, isLoading} = useData3(); + + const containerRef = useRef>(null); + + useEffect(() => { + + const dataFactory = () => + d3.map(data.data, (entry) => ({...entry, time: new Date(entry.combinedTime)})); + + if (isLoading || !data) return; + const plot = Plot.plot({ + marks: [ + Plot.rectY( + dataFactory(), + Plot.binX({y: "p50"}, { + x: entry => entry.time, + // @ts-ignore + y: "PM10", + fill: "ID", + }), + ), + Plot.ruleY([0]), + ], + }); + containerRef.current!.append(plot); + return () => plot.remove(); + }, [isLoading, data]); + + return ( + +
+ + ); }; export default ExampleChart; diff --git a/src/components/models.ts b/src/components/models.ts new file mode 100644 index 0000000..07b84e5 --- /dev/null +++ b/src/components/models.ts @@ -0,0 +1,10 @@ +export interface yearValuePair { + year: string; + value: number; +} + +export interface yearSourceValue { + year: string; + source: string; + value: number; +}