diff --git a/apps/climatemappedafrica/src/lib/hurumap/index.js b/apps/climatemappedafrica/src/lib/hurumap/index.js
index bb03b451a..3332d79ca 100644
--- a/apps/climatemappedafrica/src/lib/hurumap/index.js
+++ b/apps/climatemappedafrica/src/lib/hurumap/index.js
@@ -11,12 +11,17 @@ export async function fetchProfile() {
);
const locations = configuration?.featured_locations?.map(
- ({ name, code, level }) => {
- return { name, level, code: code.toLowerCase() };
+ ({ name, code, level, count = null }) => {
+ return { name, level, code: code.toLowerCase(), count };
},
);
- return { locations, preferredChildren: configuration.preferred_children };
+ return {
+ locations,
+ preferredChildren: configuration.preferred_children,
+ mapType: configuration?.map_type ?? "default",
+ choropleth: configuration?.choropleth ?? null,
+ };
}
function formatProfileGeographyData(data, parent) {
diff --git a/apps/climatemappedafrica/src/pages/explore/[[...slug]].js b/apps/climatemappedafrica/src/pages/explore/[[...slug]].js
index 07d0ab8af..8bb73f377 100644
--- a/apps/climatemappedafrica/src/pages/explore/[[...slug]].js
+++ b/apps/climatemappedafrica/src/pages/explore/[[...slug]].js
@@ -190,7 +190,8 @@ export async function getStaticProps({ params }) {
ghostkitSR: "",
},
};
- const { locations, preferredChildren } = await fetchProfile();
+ const { locations, preferredChildren, mapType, choropleth } =
+ await fetchProfile();
const [originalCode] = params?.slug || [""];
const code = originalCode.trim().toLowerCase();
@@ -239,7 +240,9 @@ export async function getStaticProps({ params }) {
props: {
...props,
blocks,
+ choropleth,
locations,
+ mapType,
profile,
variant: "explore",
preferredChildren,
diff --git a/packages/hurumap-next/src/Map/Layers.js b/packages/hurumap-next/src/Map/Layers.js
index 5cba19185..9d9a01f40 100644
--- a/packages/hurumap-next/src/Map/Layers.js
+++ b/packages/hurumap-next/src/Map/Layers.js
@@ -15,6 +15,7 @@ import {
function Layers({
PinnedLocationTagProps,
PopUpLocationTagProps,
+ choropleth,
geography,
isPinOrCompare = false,
locationCodes: locationCodesProp,
@@ -63,11 +64,14 @@ function Layers({
const onEachFeature = useCallback(
(feature, layer) => {
+ const choroplethColor = choropleth?.find(
+ (c) => c.code.toLowerCase() === feature.properties.code.toLowerCase(),
+ );
let geoStyles =
isPinOrCompare && feature.properties.code === secondaryGeography?.code
? secondaryGeoStyles
: primaryGeoStyles;
- // assume ISO 3166-1 codes so comparing uppercase should be ggood
+ // assume ISO 3166-1 codes so comparing uppercase should be good
const locationCodes =
locationCodesProp?.map((c) => c.toUpperCase()) || [];
if (!locationCodes?.includes(feature.properties.code.toUpperCase())) {
@@ -118,6 +122,10 @@ function Layers({
let style;
if (feature?.properties?.selected) {
style = geoStyles.selected.out;
+ style = {
+ ...style,
+ ...(choroplethColor && { ...choroplethColor }),
+ };
} else if (
isPinOrCompare &&
feature.properties.code === secondaryGeography?.code
@@ -130,17 +138,22 @@ function Layers({
layer.on("mouseover", () => {
geoStyles = isPinOrCompare ? secondaryGeoStyles : primaryGeoStyles;
- layer.setStyle(
- feature?.properties?.selected
+ layer.setStyle({
+ ...(feature?.properties?.selected
? geoStyles.selected.over
- : geoStyles.hoverOnly.over,
- );
+ : geoStyles.hoverOnly.over),
+ ...(choroplethColor && { ...choroplethColor }),
+ });
});
layer.on("mouseout", () => {
geoStyles = isPinOrCompare ? secondaryGeoStyles : primaryGeoStyles;
let outStyle;
if (feature?.properties?.selected) {
outStyle = geoStyles.selected.out;
+ outStyle = {
+ ...outStyle,
+ ...(choroplethColor && { ...choroplethColor }),
+ };
} else if (
isPinOrCompare &&
feature.properties.code === secondaryGeography?.code
@@ -164,6 +177,7 @@ function Layers({
},
[
PopUpLocationTagProps,
+ choropleth,
geography,
isPinOrCompare,
locationCodesProp,
diff --git a/packages/hurumap-next/src/Map/LazyMap.js b/packages/hurumap-next/src/Map/LazyMap.js
index 8ebae97c8..25329f0a2 100644
--- a/packages/hurumap-next/src/Map/LazyMap.js
+++ b/packages/hurumap-next/src/Map/LazyMap.js
@@ -3,6 +3,8 @@ import React, { useCallback, useEffect, useState } from "react";
import { MapContainer, Pane, TileLayer, ZoomControl } from "react-leaflet";
import Layers from "./Layers";
+import Legend from "./Legend";
+import { generateChoropleth } from "./utils";
import "leaflet/dist/leaflet.css";
@@ -11,10 +13,12 @@ import "leaflet/dist/leaflet.css";
const LazyMap = React.forwardRef(function LazyMap(props, ref) {
const {
center,
+ choropleth: choroplethProps,
geography,
geometries,
isPinOrCompare,
locations,
+ mapType,
preferredChildren,
styles = {
height: "100%",
@@ -46,6 +50,12 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) {
[preferredChildren, isPinOrCompare],
);
+ const { choropleth, legend } = generateChoropleth(
+ choroplethProps,
+ locations,
+ mapType,
+ );
+
useEffect(() => {
let selectedBound =
getSelectedBoundary(geography.level, geometries) ?? geometries.boundary;
@@ -79,6 +89,7 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) {
}, [geometries, geography, getSelectedBoundary]);
const locationCodes = locations?.map(({ code }) => code);
+
return (
))}
+ {mapType === "choropleth" && }
({
+ position: "absolute",
+ zIndex: 1000,
+ width: "fit-content",
+ bottom: theme.spacing(30),
+ right: theme.spacing(20),
+ backgroundColor: theme.palette.background.paper,
+ padding: theme.spacing(1),
+ boxShadow: theme.shadows[3],
+ ...sx,
+ })}
+ >
+ ({
+ color: theme.palette.text.primary,
+ marginBottom: theme.spacing(2),
+ })}
+ >
+ {title}
+
+ {legend?.map(({ min, max, color }) => (
+ ({
+ padding: theme.spacing(1),
+ })}
+ >
+ ({
+ backgroundColor: color,
+ width: theme.spacing(4),
+ height: theme.spacing(4),
+ })}
+ />
+
+ ({ color: theme.palette.text.primary })}
+ >
+ {min} - {max}
+
+
+
+ ))}
+
+ );
+}
diff --git a/packages/hurumap-next/src/Map/geoStyles.js b/packages/hurumap-next/src/Map/geoStyles.js
index 21c4d0892..aed496603 100644
--- a/packages/hurumap-next/src/Map/geoStyles.js
+++ b/packages/hurumap-next/src/Map/geoStyles.js
@@ -77,4 +77,32 @@ const defaultSecondaryGeoStyles = {
},
};
-export { defaultPrimaryGeoStyles, defaultSecondaryGeoStyles };
+const defaultChoroplethStyles = {
+ negative_color_range: [
+ "#FEA502",
+ "#FFAA54",
+ "#FD928E",
+ "#DFB494",
+ "#9BFAFA",
+ "#64F9F9",
+ "#01F8F8",
+ ],
+ positive_color_range: [
+ "#021AFE",
+ "#5455FF",
+ "#928EFD",
+ "#B494DF",
+ "#FA9B9B",
+ "#F96264",
+ "#F80701",
+ ],
+ opacity: 0.7,
+ opacity_hover: 1,
+ zero_color: "white",
+};
+
+export {
+ defaultPrimaryGeoStyles,
+ defaultSecondaryGeoStyles,
+ defaultChoroplethStyles,
+};
diff --git a/packages/hurumap-next/src/Map/utils.js b/packages/hurumap-next/src/Map/utils.js
new file mode 100644
index 000000000..21d347907
--- /dev/null
+++ b/packages/hurumap-next/src/Map/utils.js
@@ -0,0 +1,133 @@
+/* eslint-disable import/prefer-default-export */
+
+import { defaultChoroplethStyles } from "./geoStyles";
+
+const roundToNearestHalf = (num) => {
+ return Math.round(num * 2) / 2;
+};
+
+const calculateThresholds = (min, max, steps) => {
+ const stepSize = (max - min) / steps;
+ const thresholds = [];
+
+ for (let i = 0; i < steps; i += 1) {
+ thresholds.push({
+ min: roundToNearestHalf(min + i * stepSize),
+ max: roundToNearestHalf(min + (i + 1) * stepSize),
+ });
+ }
+
+ return thresholds;
+};
+
+const generateLegend = (
+ min,
+ max,
+ positiveThresholds,
+ positiveColorRange,
+ negativeThresholds,
+ negativeColorRange,
+ zeroColor,
+) => {
+ const legend = [];
+
+ if (negativeThresholds.length) {
+ negativeThresholds.forEach((threshold, index) => {
+ legend.push({
+ min: threshold.min,
+ max: threshold.max,
+ color: negativeColorRange[index],
+ });
+ });
+ }
+
+ if (min <= 0 && max >= 0) {
+ legend.push({
+ min: 0,
+ max: 0,
+ color: zeroColor,
+ });
+ }
+
+ if (positiveThresholds.length) {
+ positiveThresholds.forEach((threshold, index) => {
+ legend.push({
+ min: threshold.min,
+ max: threshold.max,
+ color: positiveColorRange[index],
+ });
+ });
+ }
+
+ return legend;
+};
+
+export const generateChoropleth = (colors, locations, mapType) => {
+ if (mapType !== "choropleth") return null;
+
+ const filteredLocations = locations.filter(({ count }) => count !== null);
+ const counts = filteredLocations.map(({ count }) => count);
+ const hasNegativeValues = counts.some((count) => count < 0);
+ const hasPositiveValues = counts.some((count) => count > 0);
+ const maxCount = Math.max(...counts);
+ const minCount = Math.min(...counts);
+ const roundedMinCount = Math.floor(minCount);
+ const roundedMaxCount = Math.ceil(maxCount);
+
+ const negativeColorRange =
+ colors?.negative_color_range ||
+ defaultChoroplethStyles.negative_color_range;
+ const positiveColorRange =
+ colors?.positive_color_range ||
+ defaultChoroplethStyles.positive_color_range;
+ const zeroColor = colors?.zero_color || defaultChoroplethStyles.zero_color;
+ const opacity = colors?.opacity || defaultChoroplethStyles.opacity;
+
+ const positiveThresholds = hasPositiveValues
+ ? calculateThresholds(
+ roundedMinCount,
+ roundedMaxCount,
+ positiveColorRange.length,
+ )
+ : [];
+ const negativeThresholds = hasNegativeValues
+ ? calculateThresholds(
+ roundedMinCount,
+ roundedMaxCount,
+ negativeColorRange.length,
+ )
+ : [];
+
+ const legend = generateLegend(
+ roundedMinCount,
+ roundedMaxCount,
+ positiveThresholds,
+ positiveColorRange,
+ negativeThresholds,
+ negativeColorRange,
+ zeroColor,
+ );
+
+ const getColor = (count) => {
+ if (count === 0) return zeroColor;
+ const colorRange = count > 0 ? positiveColorRange : negativeColorRange;
+ const thresholds = count > 0 ? positiveThresholds : negativeThresholds;
+ const index = thresholds.findIndex(
+ (threshold) => count >= threshold.min && count < threshold.max,
+ );
+ return colorRange[index];
+ };
+
+ const choroplethData = filteredLocations.map(({ code, count }) => {
+ const color = getColor(count);
+
+ return {
+ code,
+ count,
+ fillColor: color,
+ opacity,
+ };
+ });
+
+ return { choropleth: choroplethData, legend };
+};