Skip to content

Commit 6a4ba53

Browse files
frozenheliumsamshara
authored andcommitted
Improve tooltip in risk seasonal map
1 parent a504761 commit 6a4ba53

File tree

3 files changed

+141
-102
lines changed

3 files changed

+141
-102
lines changed

src/components/MapPopup/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function MapPopup(props: Props) {
4747
// eslint-disable-next-line react/jsx-props-no-spreading
4848
{...containerProps}
4949
className={styles.container}
50-
ellipsizeHeading
50+
withoutWrapInHeading
5151
childrenContainerClassName={_cs(styles.content, childrenContainerClassName)}
5252
withHeaderBorder
5353
withInternalPadding

src/components/domain/RiskSeasonalMap/index.tsx

Lines changed: 128 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ import {
6464
COLOR_DARK_GREY,
6565
} from '#utils/constants';
6666
import BaseMap from '#components/domain/BaseMap';
67+
import Message from '#components/Message';
68+
import Tooltip from '#components/Tooltip';
6769

6870
import Filters, { type FilterValue } from './Filters';
6971
import i18n from './i18n.json';
@@ -78,9 +80,91 @@ const defaultFilterValue: FilterValue = {
7880
includeCopingCapacity: false,
7981
};
8082

81-
type BaseProps = {
82-
className?: string;
83-
bbox: LngLatBoundsLike | undefined;
83+
interface TooltipContentProps {
84+
selectedRiskMetric: RiskMetric,
85+
valueListByHazard: {
86+
value: number;
87+
riskCategory: number;
88+
hazard_type: HazardType;
89+
hazard_type_display: string;
90+
}[];
91+
}
92+
93+
function TooltipContent(props: TooltipContentProps) {
94+
const {
95+
selectedRiskMetric,
96+
valueListByHazard,
97+
} = props;
98+
99+
const strings = useTranslation(i18n);
100+
const riskCategoryToLabelMap: Record<number, string> = useMemo(
101+
() => ({
102+
[CATEGORY_RISK_VERY_LOW]: strings.riskCategoryVeryLow,
103+
[CATEGORY_RISK_LOW]: strings.riskCategoryLow,
104+
[CATEGORY_RISK_MEDIUM]: strings.riskCategoryMedium,
105+
[CATEGORY_RISK_HIGH]: strings.riskCategoryHigh,
106+
[CATEGORY_RISK_VERY_HIGH]: strings.riskCategoryVeryHigh,
107+
}),
108+
[
109+
strings.riskCategoryVeryLow,
110+
strings.riskCategoryLow,
111+
strings.riskCategoryMedium,
112+
strings.riskCategoryHigh,
113+
strings.riskCategoryVeryHigh,
114+
],
115+
);
116+
117+
const riskMetricLabelMap: Record<RiskMetric, string> = {
118+
riskScore: strings.riskScoreOptionLabel,
119+
displacement: strings.peopleAtRiskOptionLabel,
120+
exposure: strings.peopleExposedOptionLabel,
121+
};
122+
123+
return valueListByHazard.map(
124+
({
125+
hazard_type_display,
126+
hazard_type,
127+
riskCategory,
128+
value,
129+
}) => (
130+
<Container
131+
key={hazard_type}
132+
heading={hazard_type_display}
133+
headingLevel={5}
134+
spacing="condensed"
135+
icons={(
136+
<div className={styles.tooltipHazardIndicator}>
137+
<div
138+
className={styles.color}
139+
// eslint-disable-next-line max-len
140+
style={{ backgroundColor: hazardTypeToColorMap[hazard_type] }}
141+
/>
142+
</div>
143+
)}
144+
>
145+
<TextOutput
146+
label={strings.riskScoreOptionLabel}
147+
strongValue
148+
value={riskCategoryToLabelMap[riskCategory]}
149+
/>
150+
{selectedRiskMetric !== 'riskScore' && (
151+
<TextOutput
152+
label={riskMetricLabelMap[selectedRiskMetric]}
153+
strongValue
154+
value={formatNumber(Math.ceil(value), { maximumFractionDigits: 0 })}
155+
/>
156+
)}
157+
</Container>
158+
),
159+
);
160+
}
161+
162+
const RISK_LOW_COLOR = '#c7d3e0';
163+
const RISK_HIGH_COLOR = '#f5333f';
164+
165+
interface ClickedPoint {
166+
feature: GeoJSON.Feature<GeoJSON.Point, GeoJsonProps>;
167+
lngLat: mapboxgl.LngLatLike;
84168
}
85169

86170
interface GeoJsonProps {
@@ -96,6 +180,11 @@ interface GeoJsonProps {
96180
region_id: number;
97181
}
98182

183+
type BaseProps = {
184+
className?: string;
185+
bbox: LngLatBoundsLike | undefined;
186+
}
187+
99188
type Props = BaseProps & ({
100189
variant: 'global';
101190
regionId?: never;
@@ -104,14 +193,6 @@ type Props = BaseProps & ({
104193
regionId: number;
105194
});
106195

107-
interface ClickedPoint {
108-
feature: GeoJSON.Feature<GeoJSON.Point, GeoJsonProps>;
109-
lngLat: mapboxgl.LngLatLike;
110-
}
111-
112-
const RISK_LOW_COLOR = '#c7d3e0';
113-
const RISK_HIGH_COLOR = '#f5333f';
114-
115196
function RiskSeasonalMap(props: Props) {
116197
const {
117198
className,
@@ -164,8 +245,10 @@ function RiskSeasonalMap(props: Props) {
164245
apiType: 'risk',
165246
url: '/api/v1/risk-score/',
166247
query: variant === 'region'
167-
? { region: regionId }
168-
: undefined,
248+
? {
249+
region: regionId,
250+
limit: 9999,
251+
} : { limit: 9999 },
169252
});
170253

171254
// NOTE: We get single element as array in response
@@ -600,7 +683,7 @@ function RiskSeasonalMap(props: Props) {
600683
];
601684

602685
if (isDefined(riskCategory) && isDefined(populationFactor)) {
603-
riskCategory *= populationFactor;
686+
riskCategory = Math.ceil(riskCategory * populationFactor);
604687
}
605688
}
606689

@@ -610,7 +693,7 @@ function RiskSeasonalMap(props: Props) {
610693
];
611694

612695
if (isDefined(riskCategory) && isDefined(lccFactor)) {
613-
riskCategory *= lccFactor;
696+
riskCategory = Math.ceil(riskCategory * lccFactor);
614697
}
615698
}
616699

@@ -634,6 +717,9 @@ function RiskSeasonalMap(props: Props) {
634717
const maxRiskCategory = maxSafe(
635718
valueListByHazard.map(({ riskCategory }) => riskCategory),
636719
);
720+
const riskCategorySum = sumSafe(
721+
valueListByHazard.map(({ riskCategory }) => riskCategory),
722+
);
637723

638724
if (
639725
isNotDefined(maxValue)
@@ -658,6 +744,7 @@ function RiskSeasonalMap(props: Props) {
658744
totalValue,
659745
maxValue,
660746
riskCategory: maxRiskCategory,
747+
riskCategorySum,
661748
valueListByHazard: normalizedValueListByHazard,
662749
country_details: firstItem.country_details,
663750
};
@@ -675,7 +762,7 @@ function RiskSeasonalMap(props: Props) {
675762
normalizedValue: item.totalValue / maxValue,
676763
maxValue,
677764
}),
678-
).sort((a, b) => compareNumber(a.totalValue, b.totalValue, -1));
765+
).sort((a, b) => compareNumber(a.riskCategorySum, b.riskCategorySum, -1));
679766
}
680767

681768
if (filters.riskMetric === 'displacement') {
@@ -697,39 +784,6 @@ function RiskSeasonalMap(props: Props) {
697784
);
698785

699786
return transformedData;
700-
701-
/*
702-
return transformedData?.map(
703-
(item) => {
704-
let newNormalizedValue = item.normalizedValue;
705-
let newTotalValue = item.totalValue;
706-
707-
if (filters.normalizeByPopulation
708-
&& isDefined(maxPopulation)
709-
&& maxPopulation > 0
710-
) {
711-
const population = mappings?.population[item.iso3] ?? 0;
712-
newNormalizedValue *= (population / maxPopulation);
713-
newTotalValue *= (population / maxPopulation);
714-
}
715-
716-
if (filters.includeCopingCapacity
717-
&& isDefined(maxLackOfCopingCapacity)
718-
&& maxLackOfCopingCapacity > 0
719-
) {
720-
const lcc = mappings?.lcc[item.iso3] ?? 0;
721-
newNormalizedValue *= (lcc / maxLackOfCopingCapacity);
722-
newTotalValue *= (lcc / maxLackOfCopingCapacity);
723-
}
724-
725-
return {
726-
...item,
727-
totalValue: newTotalValue,
728-
normalizedValue: newNormalizedValue,
729-
};
730-
},
731-
).sort((a, b) => compareNumber(a.totalValue, b.totalValue, -1));
732-
*/
733787
}
734788

735789
return undefined;
@@ -762,7 +816,7 @@ function RiskSeasonalMap(props: Props) {
762816
item.country_details.iso3.toUpperCase(),
763817
[
764818
'interpolate',
765-
['exponential', 1],
819+
['linear'],
766820
['number', item.riskCategory],
767821
CATEGORY_RISK_VERY_LOW,
768822
COLOR_LIGHT_BLUE,
@@ -785,23 +839,6 @@ function RiskSeasonalMap(props: Props) {
785839
[filteredData],
786840
);
787841

788-
const riskCategoryToLabelMap: Record<number, string> = useMemo(
789-
() => ({
790-
[CATEGORY_RISK_VERY_LOW]: strings.riskCategoryVeryLow,
791-
[CATEGORY_RISK_LOW]: strings.riskCategoryLow,
792-
[CATEGORY_RISK_MEDIUM]: strings.riskCategoryMedium,
793-
[CATEGORY_RISK_HIGH]: strings.riskCategoryHigh,
794-
[CATEGORY_RISK_VERY_HIGH]: strings.riskCategoryVeryHigh,
795-
}),
796-
[
797-
strings.riskCategoryVeryLow,
798-
strings.riskCategoryLow,
799-
strings.riskCategoryMedium,
800-
strings.riskCategoryHigh,
801-
strings.riskCategoryVeryHigh,
802-
],
803-
);
804-
805842
const handleCountryClick = useCallback(
806843
(feature: mapboxgl.MapboxGeoJSONFeature, lngLat: mapboxgl.LngLat) => {
807844
setClickedPointProperties({
@@ -822,8 +859,8 @@ function RiskSeasonalMap(props: Props) {
822859

823860
const riskPopupValue = useMemo(() => (
824861
filteredData?.find(
825-
(filter) => filter.iso3
826-
=== clickedPointProperties?.feature.properties.iso3.toLowerCase(),
862+
(filter) => filter.iso3 === clickedPointProperties
863+
?.feature.properties.iso3.toLowerCase(),
827864
)
828865
), [filteredData, clickedPointProperties]);
829866

@@ -932,20 +969,10 @@ function RiskSeasonalMap(props: Props) {
932969
contentViewType="vertical"
933970
childrenContainerClassName={styles.popupContent}
934971
>
935-
{riskPopupValue.valueListByHazard.map((hazard) => (
936-
<Container
937-
key={hazard.hazard_type}
938-
heading={hazard.hazard_type_display}
939-
headingLevel={5}
940-
className={styles.popupAppeal}
941-
childrenContainerClassName={styles.popupAppealDetail}
942-
>
943-
<TextOutput
944-
value={riskCategoryToLabelMap[hazard.riskCategory]}
945-
label={strings.riskPopupLabel}
946-
/>
947-
</Container>
948-
))}
972+
<TooltipContent
973+
selectedRiskMetric={filters.riskMetric}
974+
valueListByHazard={riskPopupValue.valueListByHazard}
975+
/>
949976
</MapPopup>
950977
)}
951978
</BaseMap>
@@ -959,6 +986,12 @@ function RiskSeasonalMap(props: Props) {
959986
contentViewType="vertical"
960987
>
961988
{dataPending && <BlockLoading />}
989+
{!dataPending && (isNotDefined(filteredData) || filteredData?.length === 0) && (
990+
<Message
991+
// FIXME: add translations
992+
title="Data not available for selected filters"
993+
/>
994+
)}
962995
{/* FIXME: use List */}
963996
{!dataPending && filteredData?.map(
964997
(dataItem) => {
@@ -984,13 +1017,11 @@ function RiskSeasonalMap(props: Props) {
9841017
<div className={styles.track}>
9851018
{dataItem.valueListByHazard.map(
9861019
({
987-
normalizedValue,
988-
value,
9891020
hazard_type,
990-
hazard_type_display,
1021+
riskCategory,
9911022
}) => {
9921023
// eslint-disable-next-line max-len
993-
const percentage = 100 * normalizedValue * dataItem.normalizedValue;
1024+
const percentage = (100 * riskCategory) / (5 * filters.hazardTypes.length);
9941025

9951026
if (percentage < 1) {
9961027
return null;
@@ -999,7 +1030,6 @@ function RiskSeasonalMap(props: Props) {
9991030
return (
10001031
<div
10011032
className={styles.bar}
1002-
title={`${hazard_type_display}: ${filters.riskMetric === 'riskScore' ? riskCategoryToLabelMap[value] : formatNumber(value)}`}
10031033
key={hazard_type}
10041034
style={{
10051035
width: `${percentage}%`,
@@ -1012,6 +1042,16 @@ function RiskSeasonalMap(props: Props) {
10121042
},
10131043
)}
10141044
</div>
1045+
<Tooltip
1046+
title={dataItem.country_details.name}
1047+
className={styles.barChartTooltip}
1048+
description={(
1049+
<TooltipContent
1050+
selectedRiskMetric={filters.riskMetric}
1051+
valueListByHazard={dataItem.valueListByHazard}
1052+
/>
1053+
)}
1054+
/>
10151055
</div>
10161056
);
10171057
},

src/components/domain/RiskSeasonalMap/styles.module.css

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,18 @@
105105
}
106106
}
107107
}
108-
.popup-content {
109-
display: flex;
110-
flex-direction: column;
111-
gap: var(--go-ui-spacing-md);
112-
113-
.popup-appeal {
114-
gap: var(--go-ui-spacing-xs);
108+
}
115109

116-
.popup-appeal-detail {
117-
display: flex;
118-
flex-direction: column;
119-
font-size: var(--go-ui-font-size-sm);
120-
}
121-
}
110+
.tooltip-hazard-indicator {
111+
display: flex;
112+
align-items: center;
113+
width: 1rem;
114+
height: 1.2rem;
115+
116+
.color {
117+
flex-shrink: 0;
118+
border-radius: 0.3rem;
119+
width: 0.6rem;
120+
height: 0.6rem;
122121
}
123122
}

0 commit comments

Comments
 (0)