diff --git a/app/src/views/OperationalLearning/OperationalLearningMap/i18n.json b/app/src/views/OperationalLearning/OperationalLearningMap/i18n.json new file mode 100644 index 000000000..5179a5653 --- /dev/null +++ b/app/src/views/OperationalLearning/OperationalLearningMap/i18n.json @@ -0,0 +1,6 @@ +{ + "namespace": "OperationalLearningMap", + "strings": { + "downloadMapTitle": "Operational learning map" + } +} diff --git a/app/src/views/OperationalLearning/OperationalLearningMap/index.tsx b/app/src/views/OperationalLearning/OperationalLearningMap/index.tsx new file mode 100644 index 000000000..7d9a78ac7 --- /dev/null +++ b/app/src/views/OperationalLearning/OperationalLearningMap/index.tsx @@ -0,0 +1,201 @@ +import { + useCallback, + useMemo, + useState, +} from 'react'; +import { + Container, + TextOutput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + _cs, + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; +import { + MapLayer, + MapSource, +} from '@togglecorp/re-map'; + +import BaseMap from '#components/domain/BaseMap'; +import Link from '#components/Link'; +import MapContainerWithDisclaimer from '#components/MapContainerWithDisclaimer'; +import MapPopup from '#components/MapPopup'; +import useCountryRaw from '#hooks/domain/useCountryRaw'; +import { adminFillLayerOptions } from '#utils/map'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const sourceOptions: mapboxgl.GeoJSONSourceRaw = { + type: 'geojson', +}; + +interface CountryProperties { + country_id: number; + name: string; + operation_count: number; +} + +interface ClickedPoint { + feature: GeoJSON.Feature; + lngLat: mapboxgl.LngLatLike; +} + +interface Props { + className?: string; +} + +function OperationalLearningMap(props: Props) { + const strings = useTranslation(i18n); + const { + className, + } = props; + + const [ + clickedPointProperties, + setClickedPointProperties, + ] = useState(); + + const countryResponse = useCountryRaw(); + + const countryCentroidGeoJson = useMemo( + (): GeoJSON.FeatureCollection => { + const learning_by_country = [ + { + country_name: 'Afghanistan', + country_id: 14, + operation_count: 4, + }, + { + country_name: 'Albania', + country_id: 15, + operation_count: 1, + }, + { + country_name: 'Argentina', + country_id: 20, + operation_count: 1, + }, + { + country_name: 'Australia', + country_id: 22, + operation_count: 1, + }, + { + country_name: 'Belgium', + country_id: 30, + operation_count: 1, + }, + { + country_name: 'Canada', + country_id: 42, + operation_count: 1, + }, + ]; + + const features = countryResponse + ?.map((country) => { + const learningList = learning_by_country.find( + (item) => item.country_id === country.id, + ); + if (isNotDefined(learningList)) { + return undefined; + } + + const units = learningList.operation_count ?? 0; + + return { + type: 'Feature' as const, + geometry: country.centroid as { + type: 'Point', + coordinates: [number, number], + }, + properties: { + country_id: country.id, + name: country.name, + operation_count: units, + }, + }; + }) + .filter(isDefined) ?? []; + + return { + type: 'FeatureCollection', + features, + }; + }, + [countryResponse], + ); + + const handleCountryClick = useCallback( + (feature: mapboxgl.MapboxGeoJSONFeature, lngLat: mapboxgl.LngLatLike) => { + setClickedPointProperties({ + feature: feature as unknown as ClickedPoint['feature'], + lngLat, + }); + return false; + }, + [], + ); + + const handlePointClose = useCallback(() => { + setClickedPointProperties(undefined); + }, []); + + return ( + + + )} + > + + + {clickedPointProperties?.lngLat && ( + + {clickedPointProperties.feature.properties.name} + + )} + childrenContainerClassName={styles.popupContent} + > + + + + + )} + + + ); +} + +export default OperationalLearningMap; diff --git a/app/src/views/OperationalLearning/OperationalLearningMap/styles.module.css b/app/src/views/OperationalLearning/OperationalLearningMap/styles.module.css new file mode 100644 index 000000000..fa393c112 --- /dev/null +++ b/app/src/views/OperationalLearning/OperationalLearningMap/styles.module.css @@ -0,0 +1,22 @@ +.learning-map { + + .map-container { + height: 40rem; + } +} + +.popup-content { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); + + .popup-appeal { + gap: var(--go-ui-spacing-xs); + + .popup-appeal-detail { + display: flex; + flex-direction: column; + font-size: var(--go-ui-font-size-sm); + } + } +} diff --git a/app/src/views/OperationalLearning/index.tsx b/app/src/views/OperationalLearning/index.tsx index 1330314ac..b02c9f71c 100644 --- a/app/src/views/OperationalLearning/index.tsx +++ b/app/src/views/OperationalLearning/index.tsx @@ -64,6 +64,7 @@ import { import Filters, { type FilterValue } from './Filters'; import KeyInsights from './KeyInsights'; +import OperationalLearningMap from './OperationalLearningMap'; import Summary, { type Props as SummaryProps } from './Summary'; import i18n from './i18n.json'; @@ -102,29 +103,59 @@ const perComponentKeySelector = (option: PerComponent) => option.id; const disasterTypeKeySelector = (type: DisasterType) => type.id; const disasterTypeLabelSelector = (type: DisasterType) => type.name ?? '?'; -/** @knipignore */ - const responseData = { + operations_included: 9, + learning_extracts: 6, + sectors_covered: 6, + sources_used: 8, learning_by_region: [ - { region_name: 'Americas', region_id: 1, count: 2 }, - { region_name: 'Asia Pacific', region_id: 2, count: 5 }, - { region_name: 'Europe', region_id: 3, count: 2 }, - ], - learning_by_country: [ - { country_name: 'Afghanistan', country_id: 14, operation_count: 4 }, - { country_name: 'Albania', country_id: 15, operation_count: 1 }, - { country_name: 'Argentina', country_id: 20, operation_count: 1 }, - { country_name: 'Australia', country_id: 22, operation_count: 1 }, - { country_name: 'Belgium', country_id: 30, operation_count: 1 }, - { country_name: 'Canada', country_id: 42, operation_count: 1 }, + { + region_name: 'Americas', + region_id: 1, + count: 2, + }, + { + region_name: 'Asia Pacific', + region_id: 2, + count: 5, + }, + { + region_name: 'Europe', + region_id: 3, + count: 2, + }, ], learning_by_sector: [ - { id: 17, count: 1, title: 'health' }, - { id: 18, count: 1, title: 'education' }, - { id: 19, count: 3, title: 'Livelihoods and basic needs' }, - { id: 20, count: 4, title: 'Migration' }, - { id: 21, count: 1, title: 'WASH' }, - { id: 22, count: 1, title: 'Shelter' }, + { + id: 17, + count: 1, + title: 'health', + }, + { + id: 18, + count: 1, + title: 'education', + }, + { + id: 19, + count: 3, + title: 'Livelihoods and basic needs', + }, + { + id: 20, + count: 4, + title: 'Migration', + }, + { + id: 21, + count: 1, + title: 'WASH', + }, + { + id: 22, + count: 1, + title: 'Shelter', + }, ], sources_overtime: { DREF: [ @@ -145,18 +176,6 @@ const responseData = { }, }; -const mockSectorData = responseData.learning_by_sector.map((sector) => ({ - key: sector.id, - value: sector.count, - label: sector.title, -})); - -const mockRegionData = responseData.learning_by_region.map((region) => ({ - key: region.region_id, - value: region.count, - label: region.region_name, -})); - const timeSeriesDataKeys = Object.entries( responseData.sources_overtime, ).flatMap(([source, entries]) => entries.map((entry) => ({ @@ -175,12 +194,17 @@ const xAxisFormatter = (date: Date) => date.toLocaleString( navigator.language, { month: 'short' }, ); -const keySelector = (datum: { key: number; value: number; label: string }) => datum.key; - -const valueSelector = (d: { value: number }) => d.value; -const labelSelector = (d: { label: string }) => d.label; const dateSelector = (d: { date: string }) => d.date; +const sectorsKeySelector = (datum: { id: number }) => datum.id; +const sectorsValueSelector = (datum: { count: number }) => datum.count; +const sectorsLabelSelector = (datum: { title: string }) => datum.title; + +const regionsKeySelector = (datum: { region_id: number }) => datum.region_id; +const regionValueSelector = (datum: { count: number }) => datum.count; +const regionLabelSelector = (datum: { region_name: string }) => datum.region_name; + +/** @knipignore */ // eslint-disable-next-line import/prefer-default-export export function Component() { const strings = useTranslation(i18n); @@ -373,13 +397,15 @@ export function Component() { [], ); const timeSeriesValueSelector = useCallback( - (_: string, date: Date) => timeSeriesDataKeys?.find( - (source) => ( - getFormattedDateKey(source.date) === getFormattedDateKey(date) - ), - ) ?? null, + (_: string, date: Date) => { + const entry = timeSeriesDataKeys?.find( + (source) => getFormattedDateKey(source.date) === getFormattedDateKey(date), + ); + return entry ? entry.value : undefined; + }, [], ); + return (
-
- +