Skip to content

Commit 001f740

Browse files
committed
Add climate chart and population map
1 parent a23e7e6 commit 001f740

File tree

9 files changed

+677
-59
lines changed

9 files changed

+677
-59
lines changed

app/src/components/domain/BaseMap/index.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type overrides = 'mapStyle' | 'mapOptions' | 'navControlShown' | 'navControlPosi
2626
type BaseMapProps = Omit<MapProps, overrides> & {
2727
baseLayers?: React.ReactNode;
2828
withDisclaimer?: boolean;
29+
withoutLabel?: boolean;
2930
} & Partial<Pick<MapProps, overrides>>;
3031

3132
const sourceOptions: mapboxgl.GeoJSONSourceRaw = {
@@ -66,6 +67,7 @@ function BaseMap(props: BaseMapProps) {
6667
navControlOptions,
6768
scaleControlShown,
6869
children,
70+
withoutLabel = false,
6971
...otherProps
7072
} = props;
7173

@@ -125,16 +127,18 @@ function BaseMap(props: BaseMapProps) {
125127
/>
126128
{baseLayers}
127129
</MapSource>
128-
<MapSource
129-
sourceKey="override-labels"
130-
sourceOptions={sourceOptions}
131-
geoJson={countryCentroidGeoJson}
132-
>
133-
<MapLayer
134-
layerKey="point-circle"
135-
layerOptions={adminLabelOverrideOptions}
136-
/>
137-
</MapSource>
130+
{!withoutLabel && (
131+
<MapSource
132+
sourceKey="override-labels"
133+
sourceOptions={sourceOptions}
134+
geoJson={countryCentroidGeoJson}
135+
>
136+
<MapLayer
137+
layerKey="point-circle"
138+
layerOptions={adminLabelOverrideOptions}
139+
/>
140+
</MapSource>
141+
)}
138142
{children}
139143
</Map>
140144
);
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import {
2+
useCallback,
3+
useMemo,
4+
useRef,
5+
} from 'react';
6+
import {
7+
ChartAxes,
8+
Container,
9+
TextOutput,
10+
Tooltip,
11+
} from '@ifrc-go/ui';
12+
import {
13+
maxSafe,
14+
minSafe,
15+
} from '@ifrc-go/ui/utils';
16+
import {
17+
bound,
18+
isNotDefined,
19+
listToMap,
20+
} from '@togglecorp/fujs';
21+
22+
import useTemporalChartData from '#hooks/useTemporalChartData';
23+
import { GoApiResponse } from '#utils/restRequest';
24+
25+
import styles from './styles.module.css';
26+
27+
const NUM_Y_AXIS_TICKS = 5;
28+
29+
interface Props {
30+
data: GoApiResponse<'/api/v2/country/{id}/databank/'>['key_climate'] | undefined;
31+
}
32+
33+
function ClimateChart(props: Props) {
34+
const {
35+
data,
36+
} = props;
37+
38+
const temperatureContainerRef = useRef<HTMLDivElement>(null);
39+
const precipitationContainerRef = useRef<HTMLDivElement>(null);
40+
41+
const temperatureBounds = useMemo(
42+
() => {
43+
if (isNotDefined(data) || data.length === 0) {
44+
return undefined;
45+
}
46+
47+
const minRaw = minSafe(data.map(({ min_temp }) => min_temp));
48+
const maxRaw = maxSafe(data.map(({ max_temp }) => max_temp));
49+
50+
if (isNotDefined(minRaw) || isNotDefined(maxRaw)) {
51+
return undefined;
52+
}
53+
54+
const min = Math.floor(minRaw);
55+
const max = Math.ceil(maxRaw);
56+
57+
return { min, max };
58+
},
59+
[data],
60+
);
61+
62+
const precipitationBounds = useMemo(
63+
() => {
64+
if (isNotDefined(data) || data.length === 0) {
65+
return undefined;
66+
}
67+
68+
const maxRaw = maxSafe(data.map(({ precipitation }) => precipitation));
69+
70+
if (isNotDefined(maxRaw)) {
71+
return undefined;
72+
}
73+
74+
const max = Math.ceil(maxRaw);
75+
76+
return { min: 0, max };
77+
},
78+
[data],
79+
);
80+
81+
const temperatureChartData = useTemporalChartData(
82+
data,
83+
{
84+
containerRef: temperatureContainerRef,
85+
yearlyChart: true,
86+
keySelector: ({ id }) => id,
87+
xValueSelector: ({ year, month }) => new Date(year, month - 1, 1),
88+
yValueSelector: ({ min_temp }) => min_temp,
89+
yDomain: temperatureBounds,
90+
numYAxisTicks: NUM_Y_AXIS_TICKS,
91+
},
92+
);
93+
94+
const precipitationChartData = useTemporalChartData(
95+
data,
96+
{
97+
containerRef: precipitationContainerRef,
98+
yearlyChart: true,
99+
keySelector: ({ id }) => id,
100+
xValueSelector: ({ year, month }) => new Date(year, month - 1, 1),
101+
yValueSelector: ({ precipitation }) => precipitation,
102+
yDomain: precipitationBounds,
103+
},
104+
);
105+
106+
const dataByMonth = useMemo(
107+
() => listToMap(data, ({ month }) => month - 1),
108+
[data],
109+
);
110+
111+
const temperatureTooltipSelector = useCallback(
112+
(key: string | number) => {
113+
const month = Number(key);
114+
if (Number.isNaN(month)) {
115+
return null;
116+
}
117+
118+
const currentData = dataByMonth?.[month];
119+
120+
if (isNotDefined(currentData)) {
121+
return null;
122+
}
123+
124+
return (
125+
<Tooltip
126+
title={currentData.month_display}
127+
description={(
128+
<>
129+
<TextOutput
130+
// FIXME: use translation
131+
label="Max"
132+
value={currentData.max_temp}
133+
valueType="number"
134+
/>
135+
<TextOutput
136+
// FIXME: use translation
137+
label="Average"
138+
value={currentData.avg_temp}
139+
valueType="number"
140+
/>
141+
<TextOutput
142+
// FIXME: use translation
143+
label="Min"
144+
value={currentData.min_temp}
145+
valueType="number"
146+
/>
147+
</>
148+
)}
149+
/>
150+
);
151+
},
152+
[dataByMonth],
153+
);
154+
155+
const precipitationTooltipSelector = useCallback(
156+
(key: string | number) => {
157+
const month = Number(key);
158+
if (Number.isNaN(month)) {
159+
return null;
160+
}
161+
162+
const currentData = dataByMonth?.[month];
163+
164+
if (isNotDefined(currentData)) {
165+
return null;
166+
}
167+
168+
return (
169+
<Tooltip
170+
title={currentData.month_display}
171+
description={(
172+
<TextOutput
173+
// FIXME: use translation
174+
label="Precipitation"
175+
value={currentData.precipitation}
176+
valueType="number"
177+
/>
178+
)}
179+
/>
180+
);
181+
},
182+
[dataByMonth],
183+
);
184+
185+
const barWidth = bound(
186+
(temperatureChartData.dataAreaSize.width - 24) / 12,
187+
4,
188+
24,
189+
);
190+
191+
return (
192+
<Container
193+
className={styles.climateChart}
194+
// FIXME: use translation
195+
heading="Climate chart"
196+
contentViewType="vertical"
197+
withHeaderBorder
198+
>
199+
<Container
200+
// FIXME: use translation
201+
heading="Temperature"
202+
headingLevel={5}
203+
>
204+
<div
205+
ref={temperatureContainerRef}
206+
className={styles.temperatureChartContainer}
207+
>
208+
<svg className={styles.svg}>
209+
<ChartAxes
210+
chartData={temperatureChartData}
211+
tooltipSelector={temperatureTooltipSelector}
212+
/>
213+
<g className={styles.temperature}>
214+
{temperatureChartData.chartPoints.map((chartPoint) => {
215+
const minY = chartPoint.y;
216+
const maxY = temperatureChartData.yScaleFn(
217+
chartPoint.originalData.max_temp,
218+
);
219+
220+
return (
221+
<rect
222+
key={chartPoint.key}
223+
className={styles.rect}
224+
x={chartPoint.x - barWidth / 2}
225+
y={maxY}
226+
width={barWidth}
227+
height={Math.abs(minY - maxY)}
228+
/>
229+
);
230+
})}
231+
</g>
232+
</svg>
233+
</div>
234+
</Container>
235+
<Container
236+
// FIXME: use translation
237+
heading="Precipitation"
238+
headingLevel={5}
239+
>
240+
<div
241+
ref={precipitationContainerRef}
242+
className={styles.precipitationChartContainer}
243+
>
244+
<svg className={styles.svg}>
245+
<ChartAxes
246+
chartData={precipitationChartData}
247+
tooltipSelector={precipitationTooltipSelector}
248+
/>
249+
<g className={styles.precipitation}>
250+
{precipitationChartData.chartPoints.map((chartPoint) => (
251+
<rect
252+
key={chartPoint.key}
253+
className={styles.rect}
254+
x={chartPoint.x - barWidth / 2}
255+
y={chartPoint.y}
256+
width={barWidth}
257+
height={(
258+
Math.max(
259+
precipitationChartData.dataAreaSize.height
260+
- chartPoint.y
261+
+ precipitationChartData.dataAreaOffset.top,
262+
0,
263+
)
264+
)}
265+
/>
266+
))}
267+
</g>
268+
</svg>
269+
</div>
270+
</Container>
271+
</Container>
272+
);
273+
}
274+
275+
export default ClimateChart;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.climate-chart {
2+
.precipitation-chart-container,
3+
.temperature-chart-container {
4+
height: 16rem;
5+
6+
.svg {
7+
width: 100%;
8+
height: 100%;
9+
10+
.rect {
11+
fill: currentColor;
12+
pointer-events: none;
13+
}
14+
15+
.temperature {
16+
color: var(--go-ui-color-red-50);
17+
}
18+
19+
.precipitation {
20+
color: var(--go-ui-color-blue);
21+
}
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)