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 230 Comparison Portal | Compare Country Regions #193

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
571f329
feat: region comparison tabs and selection
bohdangarchu Jan 16, 2025
0522a63
feat: error handling & move skeleton to loading.tsx
bohdangarchu Jan 16, 2025
81b5189
feat: move props to separate file
bohdangarchu Jan 16, 2025
61fde90
fix: hydration error
jschoedl Jan 18, 2025
efcded6
fix: missing key
jschoedl Jan 18, 2025
f813a36
docs: RegionFcs.ts
jschoedl Jan 18, 2025
9d30580
feat: add region comparison accordion
jschoedl Jan 18, 2025
1e4ba88
feat: improve comparison accordion skeleton
jschoedl Jan 18, 2025
d79950a
feat: select none/all
jschoedl Jan 18, 2025
b643dd2
feat: improve region select render value
jschoedl Jan 18, 2025
f33cb87
feat: flex-wrap on mobile
jschoedl Jan 18, 2025
1d2bd0c
fix: remove redundant 'use client' directives
jschoedl Jan 18, 2025
2d56dad
fix: "Food Security" -> "Current Food Security" for consistency
jschoedl Jan 18, 2025
6368c9c
Merge branch 'main' into feature/f-230-comparison-portal-compare-coun…
jschoedl Jan 18, 2025
3763463
docs: add docs
jschoedl Jan 18, 2025
b590631
fix: error handling for regions without data
jschoedl Jan 18, 2025
81f0b1e
fix: build & format population numbers
bohdangarchu Jan 19, 2025
4f403e1
fix: deal with null values for region FCS / rCSI
jschoedl Jan 20, 2025
ec00059
fix: deal with undefined region ID
jschoedl Jan 20, 2025
7e45d4b
fix: remove console log
jschoedl Jan 20, 2025
8ae5bb1
feat: make chart titles consistent
jschoedl Jan 20, 2025
fbc1504
feat: show error margins for up to 5 countries
jschoedl Jan 20, 2025
0844777
fix: query params don't update on tab switch
bohdangarchu Jan 20, 2025
164e0f8
feat: move adm0data request to client
bohdangarchu Jan 20, 2025
4a55292
Revert "feat: move adm0data request to client"
bohdangarchu Jan 21, 2025
005ddec
feat: use window.history instead of next.js router
bohdangarchu Jan 21, 2025
96b3126
feat: span full width on tabs
bohdangarchu Jan 21, 2025
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
6 changes: 3 additions & 3 deletions src/app/comparison-portal/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ export const metadata: Metadata = {
template: `%s - ${siteConfig.name}`,
},
description:
'Compare real-time global hunger data across different countries and regions. Obtain food insecurity statistics from the WFP Hunger Map Comparison Portal, tailored to various time zones. A valuable resource for humanitarian efforts and research.',
'Compare real-time global hunger data across different countries and regions. A valuable resource for humanitarian efforts and research.',
keywords: siteConfig.keywords,
openGraph: {
title: `Comparison Portal - ${siteConfig.name}`,
description:
'Compare real-time global hunger data across different countries and regions. Obtain food insecurity statistics from the WFP Hunger Map Comparison Portal, tailored to various time zones. Essential for humanitarian aid and research.',
'Compare real-time global hunger data across different countries and regions. Essential for humanitarian aid and research.',
url: `${siteConfig.domain}/comparison-portal`,
images: [
{
Expand All @@ -31,7 +31,7 @@ export const metadata: Metadata = {
card: 'summary_large_image',
title: `Comparison Portal - ${siteConfig.name}`,
description:
'Access comparable global hunger data from the WFP Hunger Map Comparison Portal, tailored to different countries and time zones.',
'Access comparable global hunger data from the WFP Hunger Map Comparison Portal, tailored to different countries and regions.',
images: [
{
url: '/Images/Comparison-preview.png',
Expand Down
12 changes: 12 additions & 0 deletions src/app/comparison-portal/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ComparisonAccordionSkeleton from '@/components/ComparisonPortal/ComparisonAccordionSkeleton';
import SelectionSkeleton from '@/components/ComparisonPortal/CountrySelectSkeleton';

export default function Loading() {
return (
<>
<h1>Comparison Portal</h1>
<SelectionSkeleton />
<ComparisonAccordionSkeleton nItems={5} />
</>
);
}
23 changes: 5 additions & 18 deletions src/app/comparison-portal/page.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,15 @@
import { Suspense } from 'react';

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

export default async function ComparisonPortal() {
export default async function Page() {
const globalRepo = container.resolve<GlobalDataRepository>('GlobalDataRepository');
const countryMapData = await globalRepo.getMapDataForCountries();
const globalFcsData = await globalRepo.getFcsData();
return (
<div>
<>
<h1>Comparison Portal</h1>
<Suspense
fallback={
<>
<CountrySelectionSkeleton />
<ComparisonAccordionSkeleton />
</>
}
>
<CountryComparison countryMapData={countryMapData} globalFcsData={globalFcsData} />
</Suspense>
</div>
<ComparisonPortal countryMapData={countryMapData} globalFcsData={globalFcsData} />
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import { v4 as uuid } from 'uuid';

/**
* A skeleton component for the ComparisonAccordion component.
* @returns {JSX.Element} The ComparisonAccordionSkeleton component
* @param {number} nItems Number of accordion items for the skeleton.
*/
export default function ComparisonAccordionSkeleton(): JSX.Element {
const N_ITEMS = 5;
export default function ComparisonAccordionSkeleton({ nItems }: { nItems: number }): JSX.Element {
return (
<div className="overflow-x-auto rounded-lg shadow-none">
<div className="flex flex-col gap-2 mb-4">
{[...Array(N_ITEMS)].map(() => (
{[...Array(nItems)].map(() => (
<div
key={uuid()}
className="rounded-medium last:border-b-0 bg-content1 white:bg-white overflow-hidden shadow-md"
Expand Down
74 changes: 55 additions & 19 deletions src/components/ComparisonPortal/CountryComparison.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,75 @@
'use client';

import { Tab, Tabs } from '@nextui-org/react';
import { useState } from 'react';

import { useSelectedCountries } from '@/domain/hooks/queryParamsHooks.ts';
import CountryComparisonProps from '@/domain/props/CountryComparisonProps';
import RegionComparisonAccordion from '@/components/ComparisonPortal/RegionComparisonAccordion';
import { useSelectedCountries, useSelectedRegions, useSelectedTab } from '@/domain/hooks/queryParamsHooks.ts';
import CountryPortalProps from '@/domain/props/CountryComparisonProps';

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

/**
* The `CountryComparison` component is the parent component for the country comparison feature.
* It contains the state for the selected countries and disabled countries, and updates query parameters accordingly using the `useSelectedCountries` hook.
* @param {CountryComparisonProps} props Props for the CountryComparison component
* The `CountryComparison` component is the parent component for the country and region comparison features.
* It contains the state for the selected countries, regions as well as the current tab.
* As far as needed, those are synchronized with the query parameters using dedicated hooks.
*
* @param {CountryPortalProps} props Props for the CountryComparison component
* @param {CountryMapDataWrapper} props.countryMapData Country map data
* @param {GlobalFcsData} props.globalFcsData National FCS Data of all countries
* @returns {JSX.Element} The CountryComparison component
*/
export default function CountryComparison({ countryMapData, globalFcsData }: CountryComparisonProps): JSX.Element {
export default function ComparisonPortal({ countryMapData, globalFcsData }: CountryPortalProps): JSX.Element {
const [selectedCountries, setSelectedCountries] = useSelectedCountries(countryMapData);
const [disabledCountryIds, setDisabledCountryIds] = useState<string[]>([]);
const [selectedTab, setSelectedTab] = useSelectedTab();
const { selectedRegions, setSelectedRegions, selectedRegionComparisonCountry, setSelectedRegionComparisonCountry } =
useSelectedRegions();

return (
<div>
<CountrySelection
countryMapData={countryMapData}
globalFcsData={globalFcsData}
selectedCountries={selectedCountries}
setSelectedCountries={setSelectedCountries}
disabledCountryIds={disabledCountryIds}
/>
<CountryComparisonAccordion
selectedCountries={selectedCountries}
setSelectedCountries={setSelectedCountries}
setDisabledCountryIds={setDisabledCountryIds}
/>
<div className="flex w-full flex-col">
<Tabs
selectedKey={selectedTab}
onSelectionChange={(key) => setSelectedTab(key as string)}
size="md"
variant="underlined"
classNames={{
base: 'justify-center px-1',
cursor: 'w-full',
}}
fullWidth
>
<Tab key="country" title="Country Comparison">
<CountrySelection
countryMapData={countryMapData}
globalFcsData={globalFcsData}
selectedCountries={selectedCountries}
setSelectedCountries={setSelectedCountries}
disabledCountryIds={disabledCountryIds}
/>
<CountryComparisonAccordion
selectedCountries={selectedCountries}
setSelectedCountries={setSelectedCountries}
setDisabledCountryIds={setDisabledCountryIds}
/>
</Tab>
<Tab key="region" title="Region Comparison">
<RegionSelection
countryMapData={countryMapData}
globalFcsData={globalFcsData}
selectedRegionComparisonCountry={selectedRegionComparisonCountry}
setSelectedRegionComparisonCountry={setSelectedRegionComparisonCountry}
selectedRegions={selectedRegions}
setSelectedRegions={setSelectedRegions}
/>
<RegionComparisonAccordion
selectedRegionComparisonCountry={selectedRegionComparisonCountry}
selectedRegions={selectedRegions}
/>
</Tab>
</Tabs>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use client';

import { useMemo } from 'react';

import ComparisonAccordionSkeleton from '@/components/ComparisonPortal/ComparisonAccordionSkeleton';
Expand Down Expand Up @@ -68,7 +66,7 @@ export default function CountryComparisonAccordion({
return CountryComparisonOperations.getComparisonAccordionItems(chartData, selectedCountryNames, isLoading);
}, [countryDataList, countryIso3DataList, selectedCountries]);

if (!accordionItems || (countryDataList.length < 2 && isLoading)) return <ComparisonAccordionSkeleton />;
if (!accordionItems || (countryDataList.length < 2 && isLoading)) return <ComparisonAccordionSkeleton nItems={5} />;

if (countryDataList.length < 2) {
return (
Expand Down
8 changes: 4 additions & 4 deletions src/components/ComparisonPortal/CountrySelectSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { Skeleton } from '@nextui-org/skeleton';
import React from 'react';

/**
* A skeleton component for the CountrySelection component.
* @returns {JSX.Element} The CountrySelectionSkeleton component
* A skeleton for the Select component.
* @returns {JSX.Element}
*/
export default function CountrySelectionSkeleton(): JSX.Element {
export default function SelectionSkeleton(): JSX.Element {
return (
<div className="pb-4 space-y-6">
<div className="flex-1">
<div className="group flex flex-col w-full">
<div className="w-full flex flex-col">
<Skeleton aria-hidden="true" className="relative px-3 gap-3 w-full shadow-sm h-10 min-h-10 rounded-medium">
Expand Down
6 changes: 2 additions & 4 deletions src/components/ComparisonPortal/CountrySelection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use client';

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

Expand Down Expand Up @@ -29,6 +27,7 @@ export default function CountrySelection({
setSelectedCountries,
disabledCountryIds,
}: CountrySelectionProps): JSX.Element {
const COUNTRY_LIMIT = 5;
const selectedKeys = useMemo(
() => selectedCountries?.map((country) => country.properties.adm0_id.toString()),
[selectedCountries]
Expand All @@ -43,8 +42,7 @@ export default function CountrySelection({
return availableCountries
.filter(
(country) =>
// if there are already 5 selected countries, disable the rest
selectedCountries.length >= 5 &&
selectedCountries.length >= COUNTRY_LIMIT &&
!selectedCountries.find(
(selectedCountry) => selectedCountry.properties.adm0_id === country.properties.adm0_id
)
Expand Down
36 changes: 18 additions & 18 deletions src/components/ComparisonPortal/NoDataHint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,54 @@ import { isContinuousChartData } from '@/domain/entities/charts/ContinuousChartD
import { NoDataHintProps } from '@/domain/props/NoDataHintProps.ts';

/**
* Displays an alert when there is one or more selected countries that are not present in the chart.
* Displays an alert when there is one or more selected chart categories (i.e. countries or regions) that are not present in the chart.
* @param {NoDataHintProps} props Props for the NoDataHint component
* @param {ContinuousChartData | CategoricalChartData} props.chartData Chart data
* @param {string[]} props.selectedCountryNames Selected country names
* @param {string[]} props.requestedChartCategories Selected country names
* @param {boolean} props.isLoading Whether the data is loading
* @returns {JSX.Element | null} The NoDataHint component if there is missing data, otherwise null
*/
export default function NoDataHint({
chartData,
selectedCountryNames,
isLoading,
requestedChartCategories,
isLoading = false,
}: NoDataHintProps): JSX.Element | null {
const [formattedMissingCountryNames, setFormattedMissingCountryNames] = useState<string | null>(null);
const [formattedMissingCategories, setFormattedMissingCategories] = useState<string | null>(null);

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

const countryNamesInChart = isContinuousChartData(chartData)
const actualChartCategories = isContinuousChartData(chartData)
? chartData.lines.map((line) => line.name)
: chartData.categories.map((category) => category.name);
const missingCountryNames = selectedCountryNames.filter(
(countryName) => !countryNamesInChart.includes(countryName)
const missingChartCategories = requestedChartCategories.filter(
(category) => !actualChartCategories.includes(category)
);

// if there is no data for at least one country we do not show the warnings
// cause the chart components will display a "no data available" message
if (missingCountryNames.length === selectedCountryNames.length) {
setFormattedMissingCountryNames(null);
if (missingChartCategories.length === requestedChartCategories.length) {
setFormattedMissingCategories(null);
return;
}

switch (missingCountryNames.length) {
switch (missingChartCategories.length) {
case 0:
setFormattedMissingCountryNames(null);
setFormattedMissingCategories(null);
break;
case 1:
setFormattedMissingCountryNames(missingCountryNames[0]);
setFormattedMissingCategories(missingChartCategories[0]);
break;
default:
setFormattedMissingCountryNames(
`${missingCountryNames.slice(0, -1).join(', ')} and ${missingCountryNames.slice(-1)}`
setFormattedMissingCategories(
`${missingChartCategories.slice(0, -1).join(', ')} and ${missingChartCategories.slice(-1)}`
);
}
}, [isLoading, chartData, selectedCountryNames]);
}, [isLoading, chartData, requestedChartCategories]);

return formattedMissingCountryNames ? (
return formattedMissingCategories ? (
<Alert
description={`No data for ${formattedMissingCountryNames}.`}
description={`No data for ${formattedMissingCategories}.`}
classNames={{ mainWrapper: 'justify-center', iconWrapper: 'my-0.5' }}
/>
) : null;
Expand Down
44 changes: 44 additions & 0 deletions src/components/ComparisonPortal/RegionComparisonAccordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';

import { useMemo, useState } from 'react';

import ComparisonAccordionSkeleton from '@/components/ComparisonPortal/ComparisonAccordionSkeleton';
import { useRegionDataQuery } from '@/domain/hooks/countryHooks';
import { RegionComparisonAccordionProps } from '@/domain/props/RegionComparisonAccordionProps';
import { RegionComparisonOperations } from '@/operations/comparison-portal/RegionComparisonOperations';

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

/**
* The `CountryComparisonAccordion` component displays comparison accordion for selected regions.
* Once a country is selected, it fetches all its regions once using the respective hook.
* @param {string[] | 'all'} selectedRegions
* @param {string | undefined} selectedRegionComparisonCountry
*/
export default function RegionComparisonAccordion({
selectedRegions,
selectedRegionComparisonCountry,
}: RegionComparisonAccordionProps) {
const { data: regionData, isLoading } = useRegionDataQuery(Number(selectedRegionComparisonCountry));

// TODO (F-254): Toggle this within the chart options. If the pie chart is selected, switch to false and hide the toggle button.
const [showRelativeNumbers] = useState(false);

const accordionItems = useMemo(() => {
if (!regionData) return [];
const chartData = RegionComparisonOperations.getChartData(regionData, selectedRegions, showRelativeNumbers);
return RegionComparisonOperations.getComparisonAccordionItems(chartData, selectedRegions, regionData.features);
}, [regionData, selectedRegions, showRelativeNumbers]);

if (!accordionItems || isLoading) return <ComparisonAccordionSkeleton nItems={2} />;

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

return <AccordionContainer multipleSelectionMode loading={isLoading} items={accordionItems} />;
}
Loading
Loading