Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/f 142 comparison portal #135

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
932af1d
feat: simple country selection
bohdangarchu Dec 6, 2024
2f0dd2d
feat: add chart and disabled countries
bohdangarchu Dec 6, 2024
bf5bd08
feat: use single selection component
bohdangarchu Dec 7, 2024
10b479c
feat: improved data fetching
bohdangarchu Dec 7, 2024
f8f9b4d
feat: add alert
bohdangarchu Dec 7, 2024
d5f75e4
feat: add basic bar chart & fix country selection
bohdangarchu Dec 8, 2024
991b7cb
Merge branch 'main' into feature/f-143-comparison-of-multiple-countri…
bohdangarchu Dec 8, 2024
eb0364d
feat: add comparison charts for import dependency, BOT, inflation
jschoedl Dec 9, 2024
b00582c
Merge branch 'main' into feature/f-143-comparison-of-multiple-countri…
bohdangarchu Dec 9, 2024
a1974d9
fix: disabled countries
bohdangarchu Dec 9, 2024
1845540
fix: country selection and number formatting
bohdangarchu Dec 9, 2024
b6a8f16
Merge branch 'main' into feature/f-143-comparison-of-multiple-countri…
bohdangarchu Dec 12, 2024
33538b7
feat: refactoring
bohdangarchu Dec 12, 2024
1bb0bdd
feat: remove alert
bohdangarchu Dec 12, 2024
afa9e8a
feat: add query param support
jschoedl Dec 13, 2024
7fd0c67
fix: accept undefined inflation graphs
jschoedl Dec 13, 2024
b855981
feat: show hint if not enough countries are selected
jschoedl Dec 13, 2024
70d523e
fix: remove redundant food security heading
jschoedl Dec 13, 2024
f88f233
fix: country selection update
bohdangarchu Dec 14, 2024
0844f79
feat: remove comments
bohdangarchu Dec 14, 2024
452b22f
Merge branch 'main' into feature/f-143-comparison-of-multiple-countri…
bohdangarchu Dec 14, 2024
a679b77
feat: add missing x-axis sliders for comparisons
jschoedl Dec 14, 2024
28f304b
feat: remove countries without data from comparison charts
jschoedl Dec 14, 2024
e272a16
fix: reset selectedXAxisRange if data changes
jschoedl Dec 14, 2024
3b68317
feat: add hint if no data available
jschoedl Dec 14, 2024
ffc7371
fix: downgrade @nextui-org/theme again
jschoedl Dec 15, 2024
883ab81
fix: do not show NoDataHint while loading
jschoedl Dec 15, 2024
bb7db3a
feat: handle importDependency = null
jschoedl Dec 15, 2024
13b6ca9
feat: add skeleton on load
jschoedl Dec 15, 2024
6191021
feat: add comparison accordion suspense
jschoedl Dec 15, 2024
14a447c
feat: add country select suspense & skeleton
jschoedl Dec 15, 2024
7a639e3
fix: return type
bohdangarchu Dec 15, 2024
56d6dbc
Merge branch 'main' into feature/f-143-comparison-of-multiple-countri…
bohdangarchu Dec 15, 2024
f3673df
feat: minor refactoring
bohdangarchu Dec 15, 2024
8f52fe2
feat: bar chart & fix react warnings
bohdangarchu Dec 15, 2024
bdc3b91
fix: layout
bohdangarchu Dec 15, 2024
e14d25c
fix: add removed code & add comments
bohdangarchu Dec 15, 2024
db478e7
feat: deselect error country
bohdangarchu Dec 15, 2024
0e566e8
fix: no scroll in initial page
bohdangarchu Dec 15, 2024
40e0a56
feat: deselect error country in url params
bohdangarchu Dec 16, 2024
2ad6266
fix: do not auto-expand comparison accordions
jschoedl Dec 16, 2024
6ac8710
feat: do not show 2 bar charts next to each other on mobile
jschoedl Dec 16, 2024
e39a284
fix: remove invalid country after selection
jschoedl Dec 16, 2024
8daa4e4
fix: remove unused code
jschoedl Dec 16, 2024
d17bb5f
fix: remove unused code
jschoedl Dec 16, 2024
6a529b3
fix: move suspense boundary up
jschoedl Dec 16, 2024
362a41f
fix: selectedCountries can be undefined
bohdangarchu Dec 16, 2024
57707cf
Merge remote-tracking branch 'origin/feature/f-143-comparison-of-mult…
jschoedl Dec 16, 2024
2d95015
fix: add query params hook to update query params and state simutaneo…
jschoedl Dec 16, 2024
dafc86c
feat: disable error country & remove countries without data from sele…
bohdangarchu Dec 16, 2024
5dce14b
feat: clear selection button
bohdangarchu Dec 16, 2024
619395c
feat: transparent bg
bohdangarchu Dec 16, 2024
6df6f8a
Merge branch 'main' into feature/f-143-comparison-of-multiple-countri…
bohdangarchu Dec 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@internationalized/date": "^3.6.0",
"@nextui-org/accordion": "^2.0.40",
"@nextui-org/alert": "^2.2.7",
"@nextui-org/button": "^2.0.38",
"@nextui-org/card": "^2.0.34",
"@nextui-org/chip": "^2.0.33",
Expand All @@ -37,7 +38,7 @@
"@nextui-org/switch": "^2.0.34",
"@nextui-org/system": "2.2.6",
"@nextui-org/table": "^2.0.40",
"@nextui-org/theme": "2.2.11",
"@nextui-org/theme": "^2.3.0",
"@nextui-org/tooltip": "^2.0.41",
"@react-aria/ssr": "3.9.4",
"@react-aria/visually-hidden": "3.8.12",
Expand Down
20 changes: 20 additions & 0 deletions src/app/comparison-portal/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Metadata } from 'next';

import HungerMapChatbot from '@/components/Chatbot/Chatbot';
import { Topbar } from '@/components/Topbar/Topbar';

export const metadata: Metadata = {
title: 'Comparison Portal',
};
bohdangarchu marked this conversation as resolved.
Show resolved Hide resolved

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen flex flex-col">
<div className="mb-20">
<Topbar />
<HungerMapChatbot />
</div>
<main className="flex flex-col gap-6 lg:gap-10 p-5 lg:p-10 text-content w-full">{children}</main>
</div>
);
}
28 changes: 28 additions & 0 deletions src/app/comparison-portal/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Suspense } from 'react';

import ComparisonAccordionSkeleton from '@/components/ComparisonPortal/ComparisonAccordionSkeleton';
import CountryComparison from '@/components/ComparisonPortal/CountryComparison';
import CountrySelectionSkeleton from '@/components/ComparisonPortal/CountrySelectSkeleton';
import container from '@/container';
import { GlobalDataRepository } from '@/domain/repositories/GlobalDataRepository';

export default async function ComparisonPortal() {
const globalRepo = container.resolve<GlobalDataRepository>('GlobalDataRepository');
const countryMapData = await globalRepo.getMapDataForCountries();
const globalFcsData = await globalRepo.getFcsData();
bohdangarchu marked this conversation as resolved.
Show resolved Hide resolved
return (
<div>
<h1>Comparison Portal</h1>
<Suspense
fallback={
<>
<CountrySelectionSkeleton />
<ComparisonAccordionSkeleton />
</>
}
>
<CountryComparison countryMapData={countryMapData} globalFcsData={globalFcsData} />
</Suspense>
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/Charts/CategoricalChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function CategoricalChart({
// handling the bar and pie chart switch and the theme switch;
useEffect(() => {
setChartOptions(CategoricalChartOperations.getHighChartOptions(data, showPieChart));
}, [showPieChart, theme]);
}, [showPieChart, theme, data]);

const alternativeSwitchButtonProps = disablePieChartSwitch
? undefined
Expand Down
16 changes: 11 additions & 5 deletions src/components/Charts/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Highcharts from 'highcharts';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';

import { ChartContainer } from '@/components/Charts/helpers/ChartContainer';
import { LineChartData } from '@/domain/entities/charts/LineChartData';
Expand Down Expand Up @@ -54,9 +54,15 @@ export function LineChart({
const lineChartOptions: Highcharts.Options = LineChartOperations.getHighChartOptions(lineChartData);

// the `selectedXAxisRange` saves the to be rendered x-axis range of the chart
// can be changed using the `LinkeChartXAxisSlider` if the param `xAxisSlider==true`
const xAxisLength: number = LineChartOperations.getDistinctXAxisValues(lineChartData).length;
const [selectedXAxisRange, setSelectedXAxisRange] = useState([0, xAxisLength - 1]);
// can be changed using the `LineChartXAxisSlider` if the param `xAxisSlider==true`
const [selectedXAxisRange, setSelectedXAxisRange] = useState([0, 0]);
const xAxisLength = useMemo(() => {
return LineChartOperations.getDistinctXAxisValues(lineChartData).length;
}, [lineChartData]);

useEffect(() => {
setSelectedXAxisRange([0, xAxisLength - 1]);
}, [xAxisLength]);

// controlling if a line or bar chart is rendered; line chart is the default
const [showBarChart, setShowBarChart] = useState(false);
Expand All @@ -75,7 +81,7 @@ export function LineChart({
LineChartOperations.getHighChartOptions(lineChartData, selectedXAxisRange[0], selectedXAxisRange[1])
);
}
}, [showBarChart, theme, selectedXAxisRange]);
}, [showBarChart, theme, selectedXAxisRange, lineChartData]);

// chart slider props - to manipulate the shown x-axis range
const sliderProps = disableXAxisSlider
Expand Down
23 changes: 23 additions & 0 deletions src/components/ComparisonPortal/ComparisonAccordionSkeleton.tsx
jschoedl marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Skeleton } from '@nextui-org/skeleton';
import React from 'react';
import { v4 as uuid } from 'uuid';

export default function ComparisonAccordionSkeleton() {
const N_ITEMS = 5;
return (
<div className="overflow-x-auto rounded-lg shadow-none">
<div className="flex flex-col gap-2 mb-4">
{[...Array(N_ITEMS)].map(() => (
<div
key={uuid()}
className="rounded-medium last:border-b-0 bg-content1 white:bg-white overflow-hidden shadow-md"
>
<Skeleton className="rounded-lg bg-content1 dark:bg-content1">
<div className="h-[69px]" />
</Skeleton>
</div>
))}
</div>
</div>
);
}
31 changes: 31 additions & 0 deletions src/components/ComparisonPortal/CountryComparison.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';

import { useState } from 'react';

import { useSelectedCountries } from '@/domain/hooks/queryParamsHooks.ts';
import CountryComparisonProps from '@/domain/props/CountryComparisonProps';

import CountryComparisonAccordion from './CountryComparisonAccordion';
import CountrySelection from './CountrySelection';

export default function CountryComparison({ countryMapData, globalFcsData }: CountryComparisonProps) {
const [selectedCountries, setSelectedCountries] = useSelectedCountries(countryMapData);
const [disabledCountryIds, setDisabledCountryIds] = useState<string[]>([]);

return (
<div>
<CountrySelection
countryMapData={countryMapData}
globalFcsData={globalFcsData}
selectedCountries={selectedCountries}
setSelectedCountries={setSelectedCountries}
disabledCountryIds={disabledCountryIds}
/>
<CountryComparisonAccordion
selectedCountries={selectedCountries}
setSelectedCountries={setSelectedCountries}
setDisabledCountryIds={setDisabledCountryIds}
/>
</div>
);
}
74 changes: 74 additions & 0 deletions src/components/ComparisonPortal/CountryComparisonAccordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client';

import { useMemo } from 'react';

import ComparisonAccordionSkeleton from '@/components/ComparisonPortal/ComparisonAccordionSkeleton';
import { useSnackbar } from '@/domain/contexts/SnackbarContext';
import { useCountryDataListQuery, useCountryIso3DataListQuery } from '@/domain/hooks/countryHooks';
import CountryComparisonAccordionProps from '@/domain/props/CountryComparisonAccordionProps';
import { CountryComparisonOperations } from '@/operations/comparison-portal/CountryComparisonOperations';

import AccordionContainer from '../Accordions/AccordionContainer';

export default function CountryComparisonAccordion({
selectedCountries,
setSelectedCountries,
setDisabledCountryIds,
}: CountryComparisonAccordionProps) {
const { showSnackBar } = useSnackbar();

const countryDataQuery = useCountryDataListQuery(
bohdangarchu marked this conversation as resolved.
Show resolved Hide resolved
// selected country ids
selectedCountries?.map((country) => country.properties.adm0_id),
// callback to show snackbar if data not found
(invalidCountryId) => {
if (!selectedCountries) return;
const invalidCountryName = CountryComparisonOperations.getCountryNameById(invalidCountryId, selectedCountries);
CountryComparisonOperations.showDataNotFoundSnackBar(showSnackBar, invalidCountryName);
setSelectedCountries(selectedCountries.filter((country) => country.properties.adm0_id !== invalidCountryId));
setDisabledCountryIds((prevDisabledCountryIds) => [...prevDisabledCountryIds, invalidCountryId.toString()]);
}
);

const countryIso3DataQuery = useCountryIso3DataListQuery(
bohdangarchu marked this conversation as resolved.
Show resolved Hide resolved
// selected country iso3 codes
selectedCountries?.map((country) => country.properties.iso3),
// callback to show snackbar if data not found
(countryCode) => {
if (!selectedCountries) return;
const countryName = CountryComparisonOperations.getCountryNameByIso3(countryCode, selectedCountries);
CountryComparisonOperations.showDataNotFoundSnackBar(showSnackBar, countryName);
}
);

const isLoading = useMemo(() => {
return (
countryDataQuery.some((result) => result.isLoading) || countryIso3DataQuery.some((result) => result.isLoading)
);
}, [countryDataQuery, countryIso3DataQuery]);

const { countryDataList, countryIso3DataList } = useMemo(
() => CountryComparisonOperations.getFilteredCountryData(countryDataQuery, countryIso3DataQuery),
[countryDataQuery, countryIso3DataQuery]
);

const accordionItems = useMemo(() => {
if (!selectedCountries) return undefined;
const chartData = CountryComparisonOperations.getChartData(countryDataList, countryIso3DataList, selectedCountries);
const selectedCountryNames = selectedCountries.map((country) => country.properties.adm0_name);
return CountryComparisonOperations.getComparisonAccordionItems(chartData, selectedCountryNames, isLoading);
}, [countryDataList, countryIso3DataList, selectedCountries]);

if (!accordionItems || (countryDataList.length < 2 && isLoading)) return <ComparisonAccordionSkeleton />;

if (countryDataList.length < 2) {
return (
<p className="pb-4">
Select {countryDataList.length === 1 ? 'one additional country' : 'two or more countries'} to start a
comparison.
</p>
);
}

return <AccordionContainer multipleSelectionMode loading={isLoading} items={accordionItems} />;
}
16 changes: 16 additions & 0 deletions src/components/ComparisonPortal/CountrySelectSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Skeleton } from '@nextui-org/skeleton';
import React from 'react';

export default function CountrySelectionSkeleton() {
return (
<div className="pb-4 space-y-6">
<div className="group flex flex-col w-full">
<div className="w-full flex flex-col">
<Skeleton className="relative px-3 gap-3 w-full shadow-sm h-10 min-h-10 rounded-medium">
<div className="inline-flex h-full w-[calc(100%_-_theme(spacing.6))] min-h-4 items-center gap-1.5 box-border" />
</Skeleton>
</div>
</div>
</div>
);
}
80 changes: 80 additions & 0 deletions src/components/ComparisonPortal/CountrySelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';

import { Select, SelectItem } from '@nextui-org/react';
import { useMemo } from 'react';

import { CountrySelectionProps } from '@/domain/props/CountrySelectionProps';
import { CountrySelectionOperations } from '@/operations/comparison-portal/CountrySelectionOperations';
import FcsChoroplethOperations from '@/operations/map/FcsChoroplethOperations';

import { CustomButton } from '../Buttons/CustomButton';

export default function CountrySelection({
countryMapData,
globalFcsData,
selectedCountries,
setSelectedCountries,
disabledCountryIds,
}: CountrySelectionProps) {
const selectedKeys = useMemo(
() => selectedCountries?.map((country) => country.properties.adm0_id.toString()),
[selectedCountries]
);

const availableCountries = useMemo(() => {
return countryMapData.features.filter((country) => FcsChoroplethOperations.checkIfActive(country, globalFcsData));
}, [countryMapData, globalFcsData]);

const disabledKeys = useMemo(() => {
if (!selectedCountries) return [];
return availableCountries
.filter(
(country) =>
// if there are already 5 selected countries, disable the rest
selectedCountries.length >= 5 &&
!selectedCountries.find(
(selectedCountry) => selectedCountry.properties.adm0_id === country.properties.adm0_id
)
)
.map((country) => country.properties.adm0_id.toString())
.concat(disabledCountryIds);
}, [selectedCountries, availableCountries, disabledCountryIds]);

return (
<div className="pb-4 flex items-center gap-4">
<Select
placeholder="Select up to 5 countries"
aria-label="Select countries for comparison"
selectionMode="multiple"
onSelectionChange={(keys) =>
CountrySelectionOperations.onSelectionChange(keys, setSelectedCountries, countryMapData)
}
defaultSelectedKeys={selectedKeys}
selectedKeys={selectedKeys}
disabledKeys={disabledKeys}
className="w-full"
variant="faded"
color="primary"
>
{availableCountries
.sort((a, b) => a.properties.adm0_name.localeCompare(b.properties.adm0_name))
.map((country) => (
<SelectItem
key={country.properties.adm0_id.toString()}
aria-label={country.properties.adm0_name}
className="transition-all hover:text-background dark:text-foreground"
>
{country.properties.adm0_name}
</SelectItem>
))}
</Select>
<CustomButton
variant="bordered"
onClick={() => setSelectedCountries([])}
isDisabled={selectedCountries === undefined || selectedCountries.length === 0}
>
Clear
</CustomButton>
</div>
);
}
39 changes: 39 additions & 0 deletions src/components/ComparisonPortal/NoDataHint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Alert } from '@nextui-org/alert';
import { useEffect, useState } from 'react';

import { isLineChartData } from '@/domain/entities/charts/LineChartData';
import { NoDataHintProps } from '@/domain/props/NoDataHintProps.ts';

export default function NoDataHint({ chartData, selectedCountryNames, isLoading }: NoDataHintProps) {
const [formattedMissingCountryNames, setFormattedMissingCountryNames] = useState<string | null>(null);

useEffect(() => {
if (isLoading) return;

const countryNamesInChart = isLineChartData(chartData)
? chartData.lines.map((line) => line.name)
: chartData.categories.map((category) => category.name);
const missingCountryNames = selectedCountryNames.filter(
(countryName) => !countryNamesInChart.includes(countryName)
);
switch (missingCountryNames.length) {
case 0:
setFormattedMissingCountryNames(null);
break;
case 1:
setFormattedMissingCountryNames(missingCountryNames[0]);
break;
default:
setFormattedMissingCountryNames(
`${missingCountryNames.slice(0, -1).join(', ')} and ${missingCountryNames.slice(-1)}`
);
}
}, [isLoading, chartData, selectedCountryNames]);

return formattedMissingCountryNames ? (
<Alert
description={`No data for ${formattedMissingCountryNames}.`}
classNames={{ mainWrapper: 'justify-center', iconWrapper: 'my-0.5' }}
/>
) : null;
}
5 changes: 5 additions & 0 deletions src/domain/entities/ApiError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type ApiError = [{ Error: string }, number];

export function isApiError(error: unknown): error is ApiError {
return error instanceof Array && error.length === 2 && typeof error[1] === 'number';
}
4 changes: 2 additions & 2 deletions src/domain/entities/charts/InflationGraphs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { ChartData } from '../common/ChartData';
export interface InflationGraphs {
type: LineChartDataType.INFLATION_CHARTS;
headline: {
data: ChartData[];
data: ChartData[] | undefined;
};
food: {
data: ChartData[];
data: ChartData[] | undefined;
};
}
Loading
Loading