Skip to content

Commit 79e792f

Browse files
authored
Merge pull request #2 from datopian/feat/resource-chart
Add charts to the homepage
2 parents feebd1a + 2ae632f commit 79e792f

20 files changed

Lines changed: 996 additions & 70 deletions

messages/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"Site": {
33
"title": "Research Portal",
4-
"description": "The data in this demo is for illustration only and should not be used for research, analysis, or decision-making.",
4+
"description": "Data provided for demonstration purposes only.",
55
"homeTitle": "Home"
66
},
77
"Common": {

src/app/[locale]/page.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import DatasetCard from "@/components/package/dataset/DatasetCard";
2+
import HomeDataSignalCarousel from "@/components/home/HomeDataSignalCarousel";
23
import HomePromptChips from "@/components/home/HomePromptChips";
34
import ReportCard from "@/components/reports/ReportCard";
45
import SearchForm from "@/components/package/search/SearchForm";
@@ -43,8 +44,12 @@ export default async function Home() {
4344
"mailto:data@example.com?subject=Open%20Data%20Request";
4445
const suggestedPrompts = QUERYLESS_SUGGESTED_PROMPTS;
4546

46-
const [featuredDatasetsResult, statsResult, visualizationsResult, allGroups] =
47-
await Promise.all([
47+
const [
48+
featuredDatasetsResult,
49+
statsResult,
50+
visualizationsResult,
51+
allGroups,
52+
] = await Promise.all([
4853
searchDatasets({
4954
options: {
5055
limit: 4,
@@ -188,6 +193,8 @@ export default async function Home() {
188193
))}
189194
</div>
190195
</div>
196+
197+
<HomeDataSignalCarousel />
191198
</Container>
192199
</section>
193200

src/components/csv-explorer/DataProvider.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import React, {
1111
useRef,
1212
useState,
1313
} from "react";
14-
import Papa from "papaparse";
14+
import { parseCsvRows } from "@/lib/csv";
15+
import type { ParseMeta, ParseResult } from "papaparse";
1516
import { isValidDate } from "./utils";
1617

1718
export type SortConfig =
@@ -82,12 +83,15 @@ export const useResourceDataSafe = () => {
8283
return context;
8384
};
8485

85-
const parseData = (stringData: string): Papa.ParseResult<RowData> => {
86-
return Papa.parse<RowData>(stringData, {
87-
header: true,
88-
skipEmptyLines: true,
89-
dynamicTyping: true,
90-
});
86+
const parseData = (stringData: string): ParseResult<RowData> => {
87+
return {
88+
data: parseCsvRows<RowData>(stringData, {
89+
dynamicTyping: true,
90+
throwOnErrors: false,
91+
}),
92+
errors: [],
93+
meta: {} as ParseMeta,
94+
};
9195
};
9296

9397
export const DataStateProvider = ({

src/components/csv-explorer/SearchDataForm.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
"use client";
22

33
import { useCallback, useRef, useState } from "react";
4-
import Papa from "papaparse";
4+
import { parseCsvRows, unparseCsvRows } from "@/lib/csv";
55
import { useResourceData } from "./DataProvider";
66
import { Loader2 } from "lucide-react";
77
import { useTranslations } from "next-intl";
88

99
type CsvRow = Record<string, string | number | null | undefined>;
1010

1111
function parseCsv(data: string): CsvRow[] {
12-
const result = Papa.parse<CsvRow>(data, {
13-
header: true,
14-
skipEmptyLines: true,
15-
});
16-
17-
if (result.errors.length > 0) {
18-
throw new Error(
19-
`CSV parsing errors: ${result.errors.map((e) => e.message).join(", ")}`
20-
);
21-
}
22-
23-
return result.data;
12+
return parseCsvRows<CsvRow>(data);
2413
}
2514

2615
export default function SearchDataForm() {
@@ -68,7 +57,7 @@ export default function SearchDataForm() {
6857
)
6958
);
7059

71-
const csvString = Papa.unparse(matchingRows);
60+
const csvString = unparseCsvRows(matchingRows);
7261
context.setTableData(csvString);
7362
context.setCurrentPage(1);
7463
} catch (err) {
@@ -89,7 +78,7 @@ export default function SearchDataForm() {
8978
const baseRows = await ensureRows();
9079
if (!baseRows) return;
9180

92-
const csvString = Papa.unparse(baseRows);
81+
const csvString = unparseCsvRows(baseRows);
9382
context.setTableData(csvString);
9483
context.setCurrentPage(1);
9584
} catch (err) {
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { ResourceChart } from "@/components/resource-chart";
2+
import { getDatasetDetails } from "@/lib/ckan/dataset";
3+
import { getResourceByName } from "@/lib/ckan/resource";
4+
import { toPublicOrgSlug } from "@/lib/portal-name";
5+
import type { ResourceChartConfig } from "@/components/resource-chart";
6+
import HomeDataSignalCarouselClient, {
7+
type HomeDataSignalSlide,
8+
} from "./HomeDataSignalCarouselClient";
9+
10+
type SlideDefinition = HomeDataSignalSlide & {
11+
resourceName: string;
12+
config: ResourceChartConfig;
13+
};
14+
15+
type ResolvedSlide = SlideDefinition & {
16+
resourceId: string;
17+
resourceHref?: string;
18+
};
19+
20+
const SLIDES: SlideDefinition[] = [
21+
{
22+
id: "co2-concentration",
23+
resourceName: "co2-annmean-mlo",
24+
eyebrow: "Data signal",
25+
title: "Atmospheric CO2 concentration",
26+
description:
27+
"Annual mean carbon dioxide levels measured at Mauna Loa, showing the long-run trend in atmospheric concentration.",
28+
badges: ["CO2 concentration", "Annual series"],
29+
config: {
30+
type: "line",
31+
x: "Year",
32+
y: "Mean",
33+
aggregation: "avg",
34+
sort: "x-asc",
35+
limit: 90,
36+
xLabel: "Year",
37+
yLabel: "CO2 concentration (ppm)",
38+
height: 270,
39+
},
40+
},
41+
{
42+
id: "temperature-anomaly",
43+
resourceName: "annual",
44+
eyebrow: "Climate record",
45+
title: "Global temperature anomaly",
46+
description:
47+
"Annual global surface temperature anomaly, useful for seeing how measured temperatures diverge from the historical baseline.",
48+
badges: ["Temperature anomaly", "Annual series"],
49+
config: {
50+
type: "line",
51+
x: "Year",
52+
y: "Mean",
53+
filters: { Source: "GCAG" },
54+
aggregation: "avg",
55+
sort: "x-asc",
56+
limit: 180,
57+
xLabel: "Year",
58+
yLabel: "Temperature anomaly",
59+
height: 270,
60+
},
61+
},
62+
{
63+
id: "fossil-co2-emitters",
64+
resourceName: "top-emitters",
65+
eyebrow: "Country comparison",
66+
title: "Largest fossil CO2 emitters in 2020",
67+
description:
68+
"A ranked snapshot of country-level fossil-fuel and cement-related CO2 emissions for the latest year in this resource.",
69+
badges: ["Million tonnes CO2", "Top countries"],
70+
config: {
71+
type: "bar-h",
72+
x: "country",
73+
y: "total_mt",
74+
filters: { year: 2020 },
75+
aggregation: "sum",
76+
sort: "y-desc",
77+
limit: 8,
78+
xLabel: "Total emissions",
79+
yLabel: "Country",
80+
height: 330,
81+
},
82+
},
83+
];
84+
85+
function toClientSlide(slide: SlideDefinition): HomeDataSignalSlide {
86+
return {
87+
id: slide.id,
88+
eyebrow: slide.eyebrow,
89+
title: slide.title,
90+
description: slide.description,
91+
badges: slide.badges,
92+
resourceHref: slide.resourceHref,
93+
};
94+
}
95+
96+
export default async function HomeDataSignalCarousel() {
97+
const resources = await Promise.all(
98+
SLIDES.map((slide) => getResourceByName(slide.resourceName)),
99+
);
100+
const datasets = await Promise.all(
101+
resources.map((resource) =>
102+
resource?.package_id ? getDatasetDetails(resource.package_id).catch(() => null) : null,
103+
),
104+
);
105+
106+
const slides = SLIDES.reduce<ResolvedSlide[]>((resolvedSlides, slide, index) => {
107+
const resource = resources[index];
108+
const dataset = datasets[index];
109+
110+
if (!resource?.id) return resolvedSlides;
111+
112+
resolvedSlides.push({
113+
...slide,
114+
resourceId: resource.id,
115+
resourceHref:
116+
dataset?.name && dataset.organization?.name
117+
? `/@${toPublicOrgSlug(dataset.organization.name)}/${dataset.name}`
118+
: undefined,
119+
});
120+
121+
return resolvedSlides;
122+
}, []);
123+
124+
if (slides.length === 0) return null;
125+
126+
return (
127+
<HomeDataSignalCarouselClient
128+
slides={slides.map(toClientSlide)}
129+
>
130+
{slides.map((slide) => (
131+
<ResourceChart
132+
key={slide.id}
133+
resourceId={slide.resourceId}
134+
config={slide.config}
135+
/>
136+
))}
137+
</HomeDataSignalCarouselClient>
138+
);
139+
}

0 commit comments

Comments
 (0)