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

Add learning by sector,regions bar chart and Map for learning #1577

Merged
56 changes: 0 additions & 56 deletions app/src/hooks/domain/usePerComponent.ts

This file was deleted.

4 changes: 3 additions & 1 deletion app/src/views/OperationalLearning/Filters/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ import { type EntriesAsList } from '@togglecorp/toggle-form';

import CountryMultiSelectInput, { type CountryOption } from '#components/domain/CountryMultiSelectInput';
import RegionSelectInput, { type RegionOption } from '#components/domain/RegionSelectInput';
import { type PerComponents } from '#contexts/domain';
import { type components } from '#generated/types';
import { type DisasterType } from '#hooks/domain/useDisasterType';
import { type PerComponent } from '#hooks/domain/usePerComponent';
import { type SecondarySector } from '#hooks/domain/useSecondarySector';
import { getFormattedComponentName } from '#utils/domain/per';
import { type GoApiResponse } from '#utils/restRequest';

import i18n from './i18n.json';

export type PerComponent = NonNullable<PerComponents['results']>[number];

type OpsLearningOrganizationType = NonNullable<GoApiResponse<'/api/v2/ops-learning/organization-type/'>['results']>[number];
export type PerLearningType = components<'read'>['schemas']['PerLearningTypeEnum'];

Expand Down
2 changes: 1 addition & 1 deletion app/src/views/OperationalLearning/KeyInsights/i18n.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"namespace": "operationalLearning",
"strings": {
"opsLearningSummariesHeading": "Summary of learnings",
"opsLearningSummariesHeading": "Summary of learning",
"keyInsightsDisclaimer": "These summaries were generated using AI and Large Language Models. They represent {numOfExtractsUsed} prioritised extracts out of {totalNumberOfExtracts} from the DREF and EA documents between {appealsFromDate} - {appealsToDate}. An initial automatic assessment of the quality of the summaries resulted in around 78% performance in terms of relevancy, coherence, consistency and fluency. To see the methodology behind the prioritisation {methodologyLink}.",
"methodologyLinkLabel": "click here",
"keyInsightsReportIssue": "Report an issue",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"namespace": "operationalLearning",
"strings": {
"downloadMapTitle": "Operational learning map",
"learningCount": "Learning count"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import {
useCallback,
useMemo,
useState,
} from 'react';
import {
Container,
NumberOutput,
TextOutput,
} from '@ifrc-go/ui';
import { useTranslation } from '@ifrc-go/ui/hooks';
import { maxSafe } from '@ifrc-go/ui/utils';
import {
_cs,
isDefined,
isNotDefined,
listToMap,
} from '@togglecorp/fujs';
import {
MapBounds,
MapLayer,
MapSource,
} from '@togglecorp/re-map';
import { type CirclePaint } from 'mapbox-gl';

import GlobalMap from '#components/domain/GlobalMap';
import Link from '#components/Link';
import MapContainerWithDisclaimer from '#components/MapContainerWithDisclaimer';
import MapPopup from '#components/MapPopup';
import useCountry from '#hooks/domain/useCountry';
import {
DEFAULT_MAP_PADDING,
DURATION_MAP_ZOOM,
} from '#utils/constants';
import { getCountryListBoundingBox } from '#utils/map';
import { type GoApiResponse } from '#utils/restRequest';

import i18n from './i18n.json';
import styles from './styles.module.css';

type OperationLearningStatsResponse = GoApiResponse<'/api/v2/ops-learning/stats/'>;
const sourceOptions: mapboxgl.GeoJSONSourceRaw = {
type: 'geojson',
};

interface CountryProperties {
countryId: number;
name: string;
learningCount: number;
}
interface ClickedPoint {
feature: GeoJSON.Feature<GeoJSON.Point, CountryProperties>;
lngLat: mapboxgl.LngLatLike;
}

const LEARNING_COUNT_LOW_COLOR = '#AEB7C2';
const LEARNING_COUNT_HIGH_COLOR = '#011E41';

interface Props {
className?: string;
learningByCountry: OperationLearningStatsResponse['learning_by_country'] | undefined;
}

function OperationalLearningMap(props: Props) {
const strings = useTranslation(i18n);
const {
className,
learningByCountry,
} = props;

const [
clickedPointProperties,
setClickedPointProperties,
] = useState<ClickedPoint | undefined>();

const countries = useCountry();

const countriesMap = useMemo(() => (
listToMap(countries, (country) => country.id)
), [countries]);

const learningCountGeoJSON = useMemo(
(): GeoJSON.FeatureCollection<GeoJSON.Geometry> | undefined => {
if ((countries?.length ?? 0) < 1 || (learningByCountry?.length ?? 0) < 1) {
return undefined;
}

const features = learningByCountry
?.map((value) => {
Comment on lines +88 to +89
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const features = learningByCountry
?.map((value) => {
const features = learningByCountry
.map((value) => {

const country = countriesMap?.[value.country_id];
if (isNotDefined(country)) {
return undefined;
}
return {
type: 'Feature' as const,
geometry: country.centroid as {
type: 'Point',
coordinates: [number, number],
},
properties: {
countryId: country.id,
name: country.name,
learningCount: value.count,
},
};
})
.filter(isDefined) ?? [];

return {
type: 'FeatureCollection',
features,
};
},
[learningByCountry, countriesMap, countries],
);

const bluePointHaloCirclePaint: CirclePaint = useMemo(() => {
const countriesWithLearning = learningByCountry?.filter((value) => value.count > 0);

const maxScaleValue = countriesWithLearning && countriesWithLearning.length > 0
? Math.max(
...(countriesWithLearning
.map((country) => country.count)),
)
: 0;

return {
'circle-opacity': 0.9,
'circle-color': [
'interpolate',
['linear'],
['number', ['get', 'learningCount']],
0,
LEARNING_COUNT_LOW_COLOR,
maxScaleValue,
LEARNING_COUNT_HIGH_COLOR,
],
'circle-radius': [
'interpolate',
['linear'],
['zoom'],
3, 10,
8, 15,
],
};
}, [learningByCountry]);

const handlePointClose = useCallback(() => {
setClickedPointProperties(undefined);
}, []);

const handlePointClick = useCallback(
(feature: mapboxgl.MapboxGeoJSONFeature, lngLat: mapboxgl.LngLatLike) => {
setClickedPointProperties({
feature: feature as unknown as ClickedPoint['feature'],
lngLat,
});
return true;
},
[setClickedPointProperties],
);

const maxLearning = useMemo(() => (
maxSafe(
learningByCountry?.map((value) => value.count),
)
), [learningByCountry]);

const bounds = useMemo(
() => {
if (isNotDefined(learningByCountry)) {
return undefined;
}
const countryList = learningByCountry
.map((d) => countriesMap?.[d.country_id])
.filter(isDefined);
return getCountryListBoundingBox(countryList);
},
[countriesMap, learningByCountry],
);

return (
<Container
className={_cs(styles.operationLearningMap, className)}
footerClassName={styles.footer}
footerContent={(
<div className={styles.legend}>
<div className={styles.legendLabel}>{strings.learningCount}</div>
<div className={styles.legendContent}>
<div
className={styles.gradient}
style={{ background: `linear-gradient(90deg, ${LEARNING_COUNT_LOW_COLOR}, ${LEARNING_COUNT_HIGH_COLOR})` }}
/>
<div className={styles.labelList}>
<NumberOutput
value={0}
/>
<NumberOutput
value={maxLearning}
/>
</div>
</div>
</div>
)}
childrenContainerClassName={styles.mainContent}
>
<GlobalMap>
<MapContainerWithDisclaimer
className={styles.mapContainer}
title={strings.downloadMapTitle}
/>
{isDefined(learningCountGeoJSON) && (
<MapSource
sourceKey="points"
sourceOptions={sourceOptions}
geoJson={learningCountGeoJSON}
>
<MapLayer
layerKey="points-halo-circle"
onClick={handlePointClick}
layerOptions={{
type: 'circle',
paint: bluePointHaloCirclePaint,
}}
/>
</MapSource>
)}
{clickedPointProperties?.lngLat && (
<MapPopup
onCloseButtonClick={handlePointClose}
coordinates={clickedPointProperties.lngLat}
heading={(
<Link
to="countriesLayout"
urlParams={{
countryId: clickedPointProperties.feature.properties.countryId,
}}
>
{clickedPointProperties.feature.properties.name}
</Link>
)}
childrenContainerClassName={styles.popupContent}
>
<Container
headingLevel={5}
>
<TextOutput
value={clickedPointProperties.feature.properties.learningCount}
label={strings.learningCount}
valueType="number"
/>
</Container>
</MapPopup>
)}
{isDefined(bounds) && (
<MapBounds
duration={DURATION_MAP_ZOOM}
bounds={bounds}
padding={DEFAULT_MAP_PADDING}
/>
)}
</GlobalMap>
</Container>
);
}

export default OperationalLearningMap;
Loading
Loading