Skip to content

Commit

Permalink
Feature/f 89 clickable countries in global view (#109)
Browse files Browse the repository at this point in the history
* feat: implement active / inactive countries for fcs mode

* feat: implement active / inactive countries for nutrition mode

* feat: implement active / inactive countries for ipc

* feat: implement inactive country overlay for rainfall mode

* feat: implement inactive country overlay for rainfall mode

* feat: refactor ipc and nutrition map data requests

* feat: cleanup data and props

* feat: implement disabled countries in search

* feat: small adjustments to new hooks

* feat: remove grey overlay in rainfall mode

* feat: remove exclamation mark of "No data!"

---------

Co-authored-by: Ahmed Farouk <[email protected]>
  • Loading branch information
Lukas0912 and ahmedfarouk2000 authored Dec 9, 2024
1 parent 4c65026 commit 862bd89
Show file tree
Hide file tree
Showing 14 changed files with 230 additions and 161 deletions.
2 changes: 1 addition & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default async function Home() {

return (
<>
<Sidebar countryMapData={countryMapData} />
<Sidebar countryMapData={countryMapData} fcsData={fcsData} />
<AlertsMenuWrapper />
<Chatbot />
<MapLoader countries={countryMapData} fcsData={fcsData} disputedAreas={disputedAreas} alertData={alertData} />
Expand Down
26 changes: 15 additions & 11 deletions src/components/Map/FcsChoropleth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTheme } from 'next-themes';
import React, { useEffect, useRef } from 'react';
import { GeoJSON } from 'react-leaflet';

import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts';
import { LayerWithFeature } from '@/domain/entities/map/LayerWithFeature.ts';
import FcsChoroplethProps from '@/domain/props/FcsChoroplethProps';
import FcsChoroplethOperations from '@/operations/map/FcsChoroplethOperations';
Expand All @@ -22,6 +23,7 @@ export default function FcsChoropleth({
countryData,
countryIso3Data,
selectedCountryName,
fcsData,
}: FcsChoroplethProps) {
const geoJsonRef = useRef<L.GeoJSON | null>(null);
const { theme } = useTheme();
Expand All @@ -36,7 +38,7 @@ export default function FcsChoropleth({
geoJsonRef.current.eachLayer((layer: LayerWithFeature) => {
if (!layer) return;
const feature = layer.feature as Feature;
if (feature.properties?.adm0_id !== selectedCountryId) {
if (FcsChoroplethOperations.checkIfActive(data.features[0] as CountryMapData, fcsData)) {
const tooltipContainer = MapboxMapOperations.createCountryNameTooltipElement(feature?.properties?.adm0_name);
layer.bindTooltip(tooltipContainer, { className: 'leaflet-tooltip', sticky: true });
} else {
Expand All @@ -47,16 +49,18 @@ export default function FcsChoropleth({

return (
<div>
<GeoJSON
ref={(instance) => {
geoJsonRef.current = instance;
}}
data={data}
style={FcsChoroplethOperations.countryStyle}
onEachFeature={(feature, layer) =>
FcsChoroplethOperations.onEachFeature(feature, layer, setSelectedCountryId, theme === 'dark')
}
/>
{countryId !== selectedCountryId && (
<GeoJSON
ref={(instance) => {
geoJsonRef.current = instance;
}}
data={data}
style={FcsChoroplethOperations.countryStyle(data.features[0], theme === 'dark', fcsData)}
onEachFeature={(feature, layer) =>
FcsChoroplethOperations.onEachFeature(feature, layer, setSelectedCountryId, theme === 'dark', fcsData)
}
/>
)}
{/* Animated GeoJSON layer for the selected country */}
{!regionData && selectedCountryId && (
<CountryLoadingLayer
Expand Down
3 changes: 1 addition & 2 deletions src/components/Map/IpcMap/IpcChoropleth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson';
import React from 'react';

import CountryLoadingLayer from '@/components/Map/CountryLoading';
import { CountryIpcData } from '@/domain/entities/country/CountryIpcData.ts';
import { useIpcQuery } from '@/domain/hooks/globalHooks';
import { IpcChoroplethProps } from '@/domain/props/IpcChoroplethProps';

Expand All @@ -27,7 +26,7 @@ function IpcChoropleth({
<>
{ipcData && (
<IpcGlobalChoropleth
ipcData={ipcData as CountryIpcData[]}
ipcData={ipcData}
countries={countries}
setSelectedCountryId={setSelectedCountryId}
selectedCountryId={selectedCountryId}
Expand Down
10 changes: 9 additions & 1 deletion src/components/Map/IpcMap/IpcGlobalChoropleth.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from 'geojson';
import { useTheme } from 'next-themes';
import React from 'react';
import { GeoJSON } from 'react-leaflet';

Expand All @@ -11,6 +12,8 @@ function IpcGlobalChoropleth({
setSelectedCountryId,
selectedCountryId,
}: IpcGlobalChoroplethProps) {
const { theme } = useTheme();

const ipcColorData = IpcChoroplethOperations.generateColorMap(ipcData, countries) as FeatureCollection<
Geometry,
GeoJsonProperties
Expand All @@ -28,7 +31,12 @@ function IpcGlobalChoropleth({
return (
<GeoJSON
key={selectedCountryId}
style={(feature) => IpcChoroplethOperations.ipcGlobalStyle(feature?.properties.adm0_id, ipcData)}
style={(feature) => {
if (!feature) {
return {};
}
return IpcChoroplethOperations.ipcGlobalStyle(feature, feature?.properties.adm0_id, ipcData, theme === 'dark');
}}
data={filteredIpcColorData}
onEachFeature={handleCountryFeature}
/>
Expand Down
56 changes: 26 additions & 30 deletions src/components/Map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,23 +111,21 @@ export default function Map({ countries, disputedAreas, fcsData, alertData }: Ma
{countries && <VectorTileLayer countries={countries} disputedAreas={disputedAreas} />}
{selectedMapType === GlobalInsight.FOOD &&
countries.features &&
countries.features
.filter((country) => country.properties.interactive)
.filter((country) => fcsData[country.properties.adm0_id]?.fcs)
.map((country) => (
<FcsChoropleth
key={country.properties.adm0_id}
countryId={country.properties.adm0_id}
data={{ type: 'FeatureCollection', features: [country as Feature<Geometry, GeoJsonProperties>] }}
selectedCountryId={selectedCountryId}
setSelectedCountryId={setSelectedCountryId}
loading={countryClickLoading}
countryData={countryData}
countryIso3Data={countryIso3Data}
regionData={regionData}
selectedCountryName={selectedCountryName}
/>
))}
countries.features.map((country) => (
<FcsChoropleth
key={country.properties.adm0_id}
countryId={country.properties.adm0_id}
data={{ type: 'FeatureCollection', features: [country as Feature<Geometry, GeoJsonProperties>] }}
selectedCountryId={selectedCountryId}
setSelectedCountryId={setSelectedCountryId}
loading={countryClickLoading}
countryData={countryData}
countryIso3Data={countryIso3Data}
regionData={regionData}
selectedCountryName={selectedCountryName}
fcsData={fcsData}
/>
))}

{selectedMapType === GlobalInsight.IPC && (
<IpcChoropleth
Expand All @@ -142,19 +140,17 @@ export default function Map({ countries, disputedAreas, fcsData, alertData }: Ma

{selectedMapType === GlobalInsight.NUTRITION &&
countries.features &&
countries.features
.filter((country) => country.properties.interactive)
.map((country) => (
<NutritionChoropleth
key={country.properties.adm0_id}
countryId={country.properties.adm0_id}
data={{ type: 'FeatureCollection', features: [country as Feature<Geometry, GeoJsonProperties>] }}
selectedCountryId={selectedCountryId}
setSelectedCountryId={setSelectedCountryId}
regionNutritionData={regionNutritionData}
selectedCountryName={selectedCountryName}
/>
))}
countries.features.map((country) => (
<NutritionChoropleth
key={country.properties.adm0_id}
countryId={country.properties.adm0_id}
data={{ type: 'FeatureCollection', features: [country as Feature<Geometry, GeoJsonProperties>] }}
selectedCountryId={selectedCountryId}
setSelectedCountryId={setSelectedCountryId}
regionNutritionData={regionNutritionData}
selectedCountryName={selectedCountryName}
/>
))}
<ZoomControl threshold={5} callback={onZoomThresholdReached} />
<BackToGlobalButton />
</MapContainer>
Expand Down
58 changes: 26 additions & 32 deletions src/components/Map/NutritionChoropleth.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Feature } from 'geojson';
import L from 'leaflet';
import React, { useEffect, useRef, useState } from 'react';
import { useTheme } from 'next-themes';
import React, { useEffect, useRef } from 'react';
import { GeoJSON } from 'react-leaflet';

import { CountryNutrition } from '@/domain/entities/country/CountryNutrition';
import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts';
import { LayerWithFeature } from '@/domain/entities/map/LayerWithFeature.ts';
import { useNutritionQuery } from '@/domain/hooks/globalHooks';
import NutritionChoroplethProps from '@/domain/props/NutritionChoroplethProps';
Expand All @@ -22,32 +23,17 @@ export default function NutritionChoropleth({
selectedCountryName,
}: NutritionChoroplethProps) {
const geoJsonRef = useRef<L.GeoJSON | null>(null);

const [countryStyles, setCountryStyles] = useState<{ [key: number]: L.PathOptions }>({});
const { theme } = useTheme();
const { data: nutritionData } = useNutritionQuery(true);

// given the `CountryNutrition` data -> parse the data to map country styling
useEffect(() => {
if (nutritionData) {
const parsedStyles = NutritionChoroplethOperations?.getCountryStyles(nutritionData);
setCountryStyles(parsedStyles);
}
}, [nutritionData]);

// adding the country name as a tooltip to each layer (on hover)
// the tooltip is not shown if the country is selected or there is no data available for the country
useEffect(() => {
if (!geoJsonRef.current || !nutritionData) return;
geoJsonRef.current.eachLayer((layer: LayerWithFeature) => {
if (!layer) return;
const feature = layer.feature as Feature;
if (
NutritionChoroplethOperations.allowCountryHover(
nutritionData as CountryNutrition,
feature.properties?.adm0_id,
selectedCountryId
)
) {
if (NutritionChoroplethOperations.checkIfActive(data.features[0] as CountryMapData, nutritionData)) {
const tooltipContainer = MapboxMapOperations.createCountryNameTooltipElement(feature?.properties?.adm0_name);
layer.bindTooltip(tooltipContainer, { className: 'leaflet-tooltip', sticky: true });
} else {
Expand All @@ -58,19 +44,27 @@ export default function NutritionChoropleth({

return (
<div>
<GeoJSON
ref={(instance) => {
geoJsonRef.current = instance;
}}
data={data}
style={(feature) => {
const featureStyle = countryStyles[feature?.properties?.adm0_id];
return featureStyle || NutritionChoroplethOperations.countryStyle;
}}
onEachFeature={(feature, layer) =>
NutritionChoroplethOperations.onEachFeature(feature, layer, setSelectedCountryId, countryStyles)
}
/>
{countryId !== selectedCountryId && nutritionData && (
<GeoJSON
ref={(instance) => {
geoJsonRef.current = instance;
}}
data={data}
style={NutritionChoroplethOperations.countryStyle(
data.features[0] as CountryMapData,
nutritionData,
theme === 'dark'
)}
onEachFeature={(feature, layer) =>
NutritionChoroplethOperations.onEachFeature(
feature as CountryMapData,
layer,
setSelectedCountryId,
nutritionData
)
}
/>
)}
{/* Animated GeoJSON layer for the selected country */}
{!regionNutritionData && selectedCountryId && (
<CountryLoadingLayer
Expand Down
28 changes: 24 additions & 4 deletions src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,23 @@ import { useSelectedMap } from '@/domain/contexts/SelectedMapContext';
import { useSidebar } from '@/domain/contexts/SidebarContext';
import { AlertsMenuVariant } from '@/domain/enums/AlertsMenuVariant';
import { GlobalInsight } from '@/domain/enums/GlobalInsight.ts';
import { useIpcQuery, useNutritionQuery } from '@/domain/hooks/globalHooks';
import { useIpcQuery, useNutritionQuery } from '@/domain/hooks/globalHooks.ts';
import SidebarProps from '@/domain/props/SidebarProps.ts';
import { SidebarOperations } from '@/operations/sidebar/SidebarOperations';
import { useMediaQuery } from '@/utils/resolution';

import PopupModal from '../PopupModal/PopupModal';
import Subscribe from '../Subscribe/Subscribe';

export function Sidebar({ countryMapData }: SidebarProps) {
export function Sidebar({ countryMapData, fcsData }: SidebarProps) {
const { isSidebarOpen, toggleSidebar, closeSidebar } = useSidebar();
const { selectedMapType, setSelectedMapType } = useSelectedMap();
const [isModalOpen, setIsModalOpen] = useState(false);
const isMobile = useMediaQuery('(max-width: 640px)');
const { selectedCountryId, setSelectedCountryId } = useSelectedCountryId();
const { clearAccordionModal } = useAccordionsModal();
const { isFetching: ipcDataIsFetching } = useIpcQuery(false);
const { isFetching: nutritionDataIsFetching } = useNutritionQuery(false);
const { isFetching: ipcDataIsFetching, data: ipcData } = useIpcQuery(false);
const { isFetching: nutritionDataIsFetching, data: nutritionData } = useNutritionQuery(false);

const mapDataFetching: Partial<Record<GlobalInsight, boolean>> = {
[GlobalInsight.IPC]: ipcDataIsFetching,
Expand Down Expand Up @@ -98,6 +98,26 @@ export function Sidebar({ countryMapData }: SidebarProps) {
className="transition-all hover:text-background dark:text-foreground"
key={country.properties.adm0_id.toString()}
aria-label={country.properties.adm0_name}
isDisabled={
!SidebarOperations.checkAvailabilityOfData(
country,
selectedMapType,
fcsData,
nutritionData,
ipcData
)
}
endContent={
!SidebarOperations.checkAvailabilityOfData(
country,
selectedMapType,
fcsData,
nutritionData,
ipcData
)
? 'No data'
: undefined
}
>
{country.properties.adm0_name}
</AutocompleteItem>
Expand Down
2 changes: 2 additions & 0 deletions src/domain/props/FcsChoroplethProps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson';

import { CountryData } from '@/domain/entities/country/CountryData.ts';
import { CountryFcsData } from '@/domain/entities/country/CountryFcsData.ts';
import { CountryIso3Data } from '@/domain/entities/country/CountryIso3Data.ts';

export default interface FcsChoroplethProps {
Expand All @@ -13,4 +14,5 @@ export default interface FcsChoroplethProps {
countryData?: CountryData;
countryIso3Data?: CountryIso3Data;
selectedCountryName?: string;
fcsData: Record<string, CountryFcsData>;
}
3 changes: 3 additions & 0 deletions src/domain/props/SidebarProps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { CountryFcsData } from '@/domain/entities/country/CountryFcsData.ts';

import { CountryMapDataWrapper } from '../entities/country/CountryMapData';

export default interface SidebarProps {
countryMapData: CountryMapDataWrapper;
fcsData: Record<string, CountryFcsData>;
}
Loading

0 comments on commit 862bd89

Please sign in to comment.