From 8ee38d7df765795774c648d951fd41f356908368 Mon Sep 17 00:00:00 2001 From: roshni73 Date: Thu, 12 Dec 2024 11:10:23 +0545 Subject: [PATCH] Add map for learning --- .../OperationalLearning/LearningMap/i18n.json | 6 + .../OperationalLearning/LearningMap/index.tsx | 194 ++++++++++++++++++ .../LearningMap/styles.module.css | 28 +++ .../OperationalLearning/LearningMap/utils.ts | 161 +++++++++++++++ app/src/views/OperationalLearning/index.tsx | 18 +- .../OperationalLearning/styles.module.css | 6 +- 6 files changed, 398 insertions(+), 15 deletions(-) create mode 100644 app/src/views/OperationalLearning/LearningMap/i18n.json create mode 100644 app/src/views/OperationalLearning/LearningMap/index.tsx create mode 100644 app/src/views/OperationalLearning/LearningMap/styles.module.css create mode 100644 app/src/views/OperationalLearning/LearningMap/utils.ts diff --git a/app/src/views/OperationalLearning/LearningMap/i18n.json b/app/src/views/OperationalLearning/LearningMap/i18n.json new file mode 100644 index 000000000..9ad4e3d80 --- /dev/null +++ b/app/src/views/OperationalLearning/LearningMap/i18n.json @@ -0,0 +1,6 @@ +{ + "namespace": "learningMap", + "strings": { + "learningDownloadMapTitle":"Operational learning" + } +} diff --git a/app/src/views/OperationalLearning/LearningMap/index.tsx b/app/src/views/OperationalLearning/LearningMap/index.tsx new file mode 100644 index 000000000..61379f48e --- /dev/null +++ b/app/src/views/OperationalLearning/LearningMap/index.tsx @@ -0,0 +1,194 @@ +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, + basePointLayerOptions, + outerCircleLayerOptionsForPersonnel, +} from './utils'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const sourceOptions: mapboxgl.GeoJSONSourceRaw = { + type: 'geojson', +}; + +interface CountryProperties { + country_id: number; + name: string; + units: number; +} + +interface ClickedPoint { + feature: GeoJSON.Feature; + lngLat: mapboxgl.LngLatLike; +} + +interface Props { + className?: string; +} + +function OperationalLearningMap(props: Props) { + const { className } = props; + + const strings = useTranslation(i18n); + const [ + clickedPointProperties, + setClickedPointProperties] = useState(); + + 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 countryResponse = useCountryRaw(); + + const countryCentroidGeoJson = useMemo( + (): GeoJSON.FeatureCollection => ({ + type: 'FeatureCollection', + features: countryResponse + ?.map((country) => { + if ( + (!country.independent && isNotDefined(country.record_type)) + || isNotDefined(country.centroid) + || isNotDefined(country.iso3) + ) { + return undefined; + } + + const learningList = learning_by_country.find( + (item) => item.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: { + id: country, + name: country.name, + units, + }, + }; + }) + .filter(isDefined) ?? [], + }), + [], + ); + + 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/LearningMap/styles.module.css b/app/src/views/OperationalLearning/LearningMap/styles.module.css new file mode 100644 index 000000000..82ccf43e9 --- /dev/null +++ b/app/src/views/OperationalLearning/LearningMap/styles.module.css @@ -0,0 +1,28 @@ +.learning-map { + .map-container { + height: 50rem; + } + + .footer { + display: flex; + align-items: flex-end; + flex-wrap: wrap; + background-color: var(--go-ui-color-background); + } +} + +.popup-content { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); + + .popup-item { + gap: var(--go-ui-spacing-xs); + + .popup-item-detail { + display: flex; + flex-direction: column; + font-size: var(--go-ui-font-size-sm); + } + } +} diff --git a/app/src/views/OperationalLearning/LearningMap/utils.ts b/app/src/views/OperationalLearning/LearningMap/utils.ts new file mode 100644 index 000000000..e623b648c --- /dev/null +++ b/app/src/views/OperationalLearning/LearningMap/utils.ts @@ -0,0 +1,161 @@ +import type { + CircleLayer, + CirclePaint, + FillLayer, +} from 'mapbox-gl'; + +import { + COLOR_BLACK, + COLOR_BLUE, + COLOR_DARK_GREY, + COLOR_LIGHT_GREY, + COLOR_RED, + COLOR_YELLOW, +} from '#utils/constants'; + +type i18nType = typeof import('./i18n.json'); + +const COLOR_ERU_AND_PERSONNEL = COLOR_BLUE; +const COLOR_ERU_ONLY = COLOR_RED; +const COLOR_PERSONNEL_ONLY = COLOR_YELLOW; +const COLOR_DEFAULT = COLOR_BLACK; + +const SURGE_TYPE_ERU = 0; +const SURGE_TYPE_PERSONNEL = 1; +const SURGE_TYPE_ERU_AND_PERSONNEL = 2; + +export const adminFillLayerOptions: Omit = { + type: 'fill', + paint: { + 'fill-color': [ + 'case', + ['boolean', ['feature-state', 'hovered'], false], + COLOR_DARK_GREY, + COLOR_LIGHT_GREY, + ], + }, +}; + +export function getLegendOptions(strings: i18nType['strings']) { + const legendOptions = [ + { + value: SURGE_TYPE_ERU_AND_PERSONNEL, + label: strings.eruAndPersonnel, + color: COLOR_ERU_AND_PERSONNEL, + }, + { + value: SURGE_TYPE_ERU, + label: strings.surgeEruOnly, + color: COLOR_ERU_ONLY, + }, + { + value: SURGE_TYPE_PERSONNEL, + label: strings.surgePersonnelOnly, + color: COLOR_PERSONNEL_ONLY, + }, + ]; + + return legendOptions; +} + +const circleColor: CirclePaint['circle-color'] = [ + 'case', + ['all', ['>', ['get', 'units'], 0], ['>', ['get', 'personnel'], 0]], + COLOR_ERU_AND_PERSONNEL, + ['>', ['get', 'units'], 0], + COLOR_ERU_ONLY, + ['>', ['get', 'personnel'], 0], + COLOR_PERSONNEL_ONLY, + COLOR_DEFAULT, +]; + +const basePointPaint: CirclePaint = { + 'circle-radius': 5, + 'circle-color': circleColor, + 'circle-opacity': 0.8, +}; + +export const basePointLayerOptions: Omit = { + type: 'circle', + paint: basePointPaint, +}; + +const baseOuterCirclePaint: CirclePaint = { + 'circle-color': circleColor, + 'circle-opacity': 0.4, +}; + +const outerCirclePaintForEru: CirclePaint = { + ...baseOuterCirclePaint, + 'circle-radius': [ + 'interpolate', + ['linear', 1], + ['get', 'units'], + 2, + 5, + 4, + 7, + 6, + 9, + 8, + 11, + 10, + 13, + 12, + 15, + ], +}; + +const outerCirclePaintForPersonnel: CirclePaint = { + ...baseOuterCirclePaint, + 'circle-radius': [ + 'interpolate', + ['linear', 1], + ['get', 'personnel'], + + 2, + 5, + 4, + 7, + 6, + 9, + 8, + 11, + 10, + 13, + 12, + 15, + ], +}; + +export const outerCircleLayerOptionsForEru: Omit = { + type: 'circle', + paint: outerCirclePaintForEru, +}; + +export const outerCircleLayerOptionsForPersonnel: Omit = { + type: 'circle', + paint: outerCirclePaintForPersonnel, +}; + +export interface ScaleOption { + label: string; + value: 'eru' | 'personnel'; +} + +export function getScaleOptions(strings: i18nType['strings']) { + const scaleOptions: ScaleOption[] = [ + { value: 'eru', label: strings.eruLabel }, + { value: 'personnel', label: strings.personnelLabel }, + ]; + + return scaleOptions; +} + +export function optionKeySelector(option: ScaleOption) { + return option.value; +} + +export function optionLabelSelector(option: ScaleOption) { + return option.label; +} diff --git a/app/src/views/OperationalLearning/index.tsx b/app/src/views/OperationalLearning/index.tsx index 06d615d45..d849cf035 100644 --- a/app/src/views/OperationalLearning/index.tsx +++ b/app/src/views/OperationalLearning/index.tsx @@ -63,6 +63,7 @@ import { import Filters, { type FilterValue } from './Filters'; import KeyInsights from './KeyInsights'; +import OperationalLearningMap from './LearningMap'; import Summary, { type Props as SummaryProps } from './Summary'; import i18n from './i18n.json'; @@ -109,14 +110,6 @@ const responseData = { { 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 }, - ], learning_by_sector: [ { id: 17, count: 1, title: 'health' }, { id: 18, count: 1, title: 'education' }, @@ -126,7 +119,7 @@ const responseData = { { id: 22, count: 1, title: 'Shelter' }, ], sources_overtime: { - DREF: [ + ' DREF': [ { year: 2023, count: 1 }, { year: 2024, count: 3 }, ], @@ -542,9 +535,10 @@ export function Component() { )} />
- + + + +