From b57bd38dabc4356b162d6b29e53ff2b09a41bf5e Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:43:10 +0300 Subject: [PATCH 01/18] Working choropleth Map --- .../src/components/ExplorePage/index.js | 34 +++++++++++++++++++ .../src/lib/hurumap/index.js | 4 +-- packages/hurumap-next/src/Map/Layers.js | 30 +++++++++++++--- packages/hurumap-next/src/Map/geoStyles.js | 14 +++++++- 4 files changed, 74 insertions(+), 8 deletions(-) diff --git a/apps/climatemappedafrica/src/components/ExplorePage/index.js b/apps/climatemappedafrica/src/components/ExplorePage/index.js index 73bab2ca7..c74a669f2 100644 --- a/apps/climatemappedafrica/src/components/ExplorePage/index.js +++ b/apps/climatemappedafrica/src/components/ExplorePage/index.js @@ -107,6 +107,39 @@ function ExplorePage({ panelProps, profile: profileProp, ...props }) { if (secondaryTags?.length) { tags.push(secondaryTags[secondaryTags.length - 1]); } + + const { locations, mapType = "choropleth" } = props; + + let choropleth = null; + if (mapType === "choropleth") { + const filteredLocations = locations.filter(({ count }) => count !== null); + const counts = filteredLocations.map(({ count }) => count); + const maxCount = Math.max(...counts); + const minCount = Math.min(...counts); + + const getClassification = (count) => { + const range = maxCount - minCount; + const veryLowThreshold = minCount + range * 0.2; + const lowThreshold = minCount + range * 0.4; + const moderateThreshold = minCount + range * 0.6; + const highThreshold = minCount + range * 0.8; + + if (count <= veryLowThreshold) return "very low"; + if (count <= lowThreshold) return "low"; + if (count <= moderateThreshold) return "moderate"; + if (count <= highThreshold) return "high"; + return "very high"; + }; + + choropleth = filteredLocations.map(({ code, count }) => { + return { + code, + count, + classification: getClassification(count), + }; + }); + } + return ( <> { - return { name, level, code: code.toLowerCase() }; + ({ name, code, level, count = null }) => { + return { name, level, code: code.toLowerCase(), count }; }, ); diff --git a/packages/hurumap-next/src/Map/Layers.js b/packages/hurumap-next/src/Map/Layers.js index 5cba19185..5cd19aa08 100644 --- a/packages/hurumap-next/src/Map/Layers.js +++ b/packages/hurumap-next/src/Map/Layers.js @@ -10,12 +10,14 @@ import { FeatureGroup, GeoJSON, useMap } from "react-leaflet"; import { defaultPrimaryGeoStyles, defaultSecondaryGeoStyles, + defaultChoroplethStyles, } from "./geoStyles"; function Layers({ PinnedLocationTagProps, PopUpLocationTagProps, geography, + choropleth, isPinOrCompare = false, locationCodes: locationCodesProp, onClick, @@ -63,11 +65,15 @@ function Layers({ const onEachFeature = useCallback( (feature, layer) => { + const count = choropleth?.find( + (c) => c.code.toLowerCase() === feature.properties.code.toLowerCase(), + ); + const choroplethColor = defaultChoroplethStyles[count?.classification]; 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 +124,10 @@ function Layers({ let style; if (feature?.properties?.selected) { style = geoStyles.selected.out; + style = { + ...style, + ...(choroplethColor && { fillColor: choroplethColor }), + }; } else if ( isPinOrCompare && feature.properties.code === secondaryGeography?.code @@ -126,21 +136,30 @@ function Layers({ } else { style = geoStyles.hoverOnly.out; } + layer.setStyle(style); 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 && { fillColor: choroplethColor }), + fillOpacity: 0.5, + weight: 2, + opacity: 1, + }); }); layer.on("mouseout", () => { geoStyles = isPinOrCompare ? secondaryGeoStyles : primaryGeoStyles; let outStyle; if (feature?.properties?.selected) { outStyle = geoStyles.selected.out; + outStyle = { + ...outStyle, + ...(choroplethColor && { fillColor: choroplethColor }), + }; } else if ( isPinOrCompare && feature.properties.code === secondaryGeography?.code @@ -163,6 +182,7 @@ function Layers({ } }, [ + choropleth, PopUpLocationTagProps, geography, isPinOrCompare, diff --git a/packages/hurumap-next/src/Map/geoStyles.js b/packages/hurumap-next/src/Map/geoStyles.js index 21c4d0892..90e6cac1b 100644 --- a/packages/hurumap-next/src/Map/geoStyles.js +++ b/packages/hurumap-next/src/Map/geoStyles.js @@ -77,4 +77,16 @@ const defaultSecondaryGeoStyles = { }, }; -export { defaultPrimaryGeoStyles, defaultSecondaryGeoStyles }; +const defaultChoroplethStyles = { + "very low": "yellow", + low: "orange", + moderate: "red", + high: "purple", + "very high": "black", +}; + +export { + defaultPrimaryGeoStyles, + defaultSecondaryGeoStyles, + defaultChoroplethStyles, +}; From a579e0cfcdb5e77963c8dd26c76ccab8e3ff55f6 Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:13:15 +0300 Subject: [PATCH 02/18] Cleanup code --- .../climatemappedafrica/src/components/ExplorePage/index.js | 2 +- apps/climatemappedafrica/src/lib/hurumap/index.js | 6 +++++- apps/climatemappedafrica/src/pages/explore/[[...slug]].js | 3 ++- packages/hurumap-next/src/Map/Layers.js | 1 - 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/climatemappedafrica/src/components/ExplorePage/index.js b/apps/climatemappedafrica/src/components/ExplorePage/index.js index c74a669f2..119e9645e 100644 --- a/apps/climatemappedafrica/src/components/ExplorePage/index.js +++ b/apps/climatemappedafrica/src/components/ExplorePage/index.js @@ -108,7 +108,7 @@ function ExplorePage({ panelProps, profile: profileProp, ...props }) { tags.push(secondaryTags[secondaryTags.length - 1]); } - const { locations, mapType = "choropleth" } = props; + const { locations, mapType = "default" } = props; let choropleth = null; if (mapType === "choropleth") { diff --git a/apps/climatemappedafrica/src/lib/hurumap/index.js b/apps/climatemappedafrica/src/lib/hurumap/index.js index 064aeca48..ff0ea8c82 100644 --- a/apps/climatemappedafrica/src/lib/hurumap/index.js +++ b/apps/climatemappedafrica/src/lib/hurumap/index.js @@ -16,7 +16,11 @@ export async function fetchProfile() { }, ); - return { locations, preferredChildren: configuration.preferred_children }; + return { + locations, + preferredChildren: configuration.preferred_children, + mapType: configuration?.map_type, + }; } function formatProfileGeographyData(data, parent) { diff --git a/apps/climatemappedafrica/src/pages/explore/[[...slug]].js b/apps/climatemappedafrica/src/pages/explore/[[...slug]].js index 07d0ab8af..a192ab30a 100644 --- a/apps/climatemappedafrica/src/pages/explore/[[...slug]].js +++ b/apps/climatemappedafrica/src/pages/explore/[[...slug]].js @@ -190,7 +190,7 @@ export async function getStaticProps({ params }) { ghostkitSR: "", }, }; - const { locations, preferredChildren } = await fetchProfile(); + const { locations, preferredChildren, mapType } = await fetchProfile(); const [originalCode] = params?.slug || [""]; const code = originalCode.trim().toLowerCase(); @@ -240,6 +240,7 @@ export async function getStaticProps({ params }) { ...props, blocks, 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 5cd19aa08..c29f041d1 100644 --- a/packages/hurumap-next/src/Map/Layers.js +++ b/packages/hurumap-next/src/Map/Layers.js @@ -136,7 +136,6 @@ function Layers({ } else { style = geoStyles.hoverOnly.out; } - layer.setStyle(style); layer.on("mouseover", () => { From 319484fbe1e69c33c6e8865b1d3d0851a8c04759 Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:24:12 +0300 Subject: [PATCH 03/18] Update default colors Signed-off-by: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> --- packages/hurumap-next/src/Map/geoStyles.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/hurumap-next/src/Map/geoStyles.js b/packages/hurumap-next/src/Map/geoStyles.js index 90e6cac1b..0156d877e 100644 --- a/packages/hurumap-next/src/Map/geoStyles.js +++ b/packages/hurumap-next/src/Map/geoStyles.js @@ -1,5 +1,14 @@ const CHART_PRIMARY_COLOR_SCHEME = ["#0B2AEA", "#7986D1", "#DFDFDF", "#666666"]; +// Range from deep blue to light blue +const CHOROPLETH_COLOR_SCHEME = [ + "#00008B", + "#0000CD", + "#4169E1", + "#87CEFA", + "#ADD8E6", +]; + const CHART_SECONDARY_COLOR_SCHEME = [ "#FC0D1B", "#F8A199", @@ -78,11 +87,11 @@ const defaultSecondaryGeoStyles = { }; const defaultChoroplethStyles = { - "very low": "yellow", - low: "orange", - moderate: "red", - high: "purple", - "very high": "black", + "very low": CHOROPLETH_COLOR_SCHEME[4], + low: CHOROPLETH_COLOR_SCHEME[3], + moderate: CHOROPLETH_COLOR_SCHEME[2], + high: CHOROPLETH_COLOR_SCHEME[1], + "very high": CHOROPLETH_COLOR_SCHEME[0], }; export { From 8b68515f4b0daea030fc900154a0e0aaa90d0f7c Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:36:54 +0300 Subject: [PATCH 04/18] Allow customization of choropleth colors --- apps/climatemappedafrica/src/lib/hurumap/index.js | 1 + apps/climatemappedafrica/src/pages/explore/[[...slug]].js | 4 +++- packages/hurumap-next/src/Map/Layers.js | 6 +++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/climatemappedafrica/src/lib/hurumap/index.js b/apps/climatemappedafrica/src/lib/hurumap/index.js index ff0ea8c82..6adb9382d 100644 --- a/apps/climatemappedafrica/src/lib/hurumap/index.js +++ b/apps/climatemappedafrica/src/lib/hurumap/index.js @@ -20,6 +20,7 @@ export async function fetchProfile() { locations, preferredChildren: configuration.preferred_children, mapType: configuration?.map_type, + choroplethColors: configuration?.choropleth, }; } diff --git a/apps/climatemappedafrica/src/pages/explore/[[...slug]].js b/apps/climatemappedafrica/src/pages/explore/[[...slug]].js index a192ab30a..4e9dc80f0 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, mapType } = await fetchProfile(); + const { locations, preferredChildren, mapType, choroplethColors } = + await fetchProfile(); const [originalCode] = params?.slug || [""]; const code = originalCode.trim().toLowerCase(); @@ -239,6 +240,7 @@ export async function getStaticProps({ params }) { props: { ...props, blocks, + choroplethColors, locations, mapType, profile, diff --git a/packages/hurumap-next/src/Map/Layers.js b/packages/hurumap-next/src/Map/Layers.js index c29f041d1..a5cbae610 100644 --- a/packages/hurumap-next/src/Map/Layers.js +++ b/packages/hurumap-next/src/Map/Layers.js @@ -18,6 +18,7 @@ function Layers({ PopUpLocationTagProps, geography, choropleth, + choroplethColors, isPinOrCompare = false, locationCodes: locationCodesProp, onClick, @@ -68,7 +69,9 @@ function Layers({ const count = choropleth?.find( (c) => c.code.toLowerCase() === feature.properties.code.toLowerCase(), ); - const choroplethColor = defaultChoroplethStyles[count?.classification]; + const choroplethColor = + (choroplethColors && choroplethColors[count?.classification]) || + defaultChoroplethStyles[count?.classification]; let geoStyles = isPinOrCompare && feature.properties.code === secondaryGeography?.code ? secondaryGeoStyles @@ -182,6 +185,7 @@ function Layers({ }, [ choropleth, + choroplethColors, PopUpLocationTagProps, geography, isPinOrCompare, From 5e52a85d524021afca3b4f7cdb237da72b898685 Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:43:14 +0300 Subject: [PATCH 05/18] Move computation to Map component Signed-off-by: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> --- .../src/components/ExplorePage/index.js | 34 ------------------- packages/hurumap-next/src/Map/LazyMap.js | 32 +++++++++++++++++ 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/apps/climatemappedafrica/src/components/ExplorePage/index.js b/apps/climatemappedafrica/src/components/ExplorePage/index.js index 119e9645e..73bab2ca7 100644 --- a/apps/climatemappedafrica/src/components/ExplorePage/index.js +++ b/apps/climatemappedafrica/src/components/ExplorePage/index.js @@ -107,39 +107,6 @@ function ExplorePage({ panelProps, profile: profileProp, ...props }) { if (secondaryTags?.length) { tags.push(secondaryTags[secondaryTags.length - 1]); } - - const { locations, mapType = "default" } = props; - - let choropleth = null; - if (mapType === "choropleth") { - const filteredLocations = locations.filter(({ count }) => count !== null); - const counts = filteredLocations.map(({ count }) => count); - const maxCount = Math.max(...counts); - const minCount = Math.min(...counts); - - const getClassification = (count) => { - const range = maxCount - minCount; - const veryLowThreshold = minCount + range * 0.2; - const lowThreshold = minCount + range * 0.4; - const moderateThreshold = minCount + range * 0.6; - const highThreshold = minCount + range * 0.8; - - if (count <= veryLowThreshold) return "very low"; - if (count <= lowThreshold) return "low"; - if (count <= moderateThreshold) return "moderate"; - if (count <= highThreshold) return "high"; - return "very high"; - }; - - choropleth = filteredLocations.map(({ code, count }) => { - return { - code, - count, - classification: getClassification(count), - }; - }); - } - return ( <> code); + + let choropleth = null; + if (mapType === "choropleth") { + const filteredLocations = locations.filter(({ count }) => count !== null); + const counts = filteredLocations.map(({ count }) => count); + const maxCount = Math.max(...counts); + const minCount = Math.min(...counts); + + const getClassification = (count) => { + const range = maxCount - minCount; + const veryLowThreshold = minCount + range * 0.2; + const lowThreshold = minCount + range * 0.4; + const moderateThreshold = minCount + range * 0.6; + const highThreshold = minCount + range * 0.8; + + if (count <= veryLowThreshold) return "very low"; + if (count <= lowThreshold) return "low"; + if (count <= moderateThreshold) return "moderate"; + if (count <= highThreshold) return "high"; + return "very high"; + }; + + choropleth = filteredLocations.map(({ code, count }) => { + return { + code, + count, + classification: getClassification(count), + }; + }); + } return ( Date: Tue, 20 Aug 2024 18:48:31 +0300 Subject: [PATCH 06/18] New working Map --- .../src/lib/hurumap/index.js | 4 +- packages/hurumap-next/src/Map/Layers.js | 79 ++++++++++++++++--- packages/hurumap-next/src/Map/LazyMap.js | 35 +------- packages/hurumap-next/src/Map/geoStyles.js | 19 ++--- 4 files changed, 74 insertions(+), 63 deletions(-) diff --git a/apps/climatemappedafrica/src/lib/hurumap/index.js b/apps/climatemappedafrica/src/lib/hurumap/index.js index 6adb9382d..26706fdfd 100644 --- a/apps/climatemappedafrica/src/lib/hurumap/index.js +++ b/apps/climatemappedafrica/src/lib/hurumap/index.js @@ -19,8 +19,8 @@ export async function fetchProfile() { return { locations, preferredChildren: configuration.preferred_children, - mapType: configuration?.map_type, - choroplethColors: configuration?.choropleth, + mapType: configuration?.map_type ?? "default", + choroplethColors: configuration?.choropleth ?? null, }; } diff --git a/packages/hurumap-next/src/Map/Layers.js b/packages/hurumap-next/src/Map/Layers.js index a5cbae610..3792d39e3 100644 --- a/packages/hurumap-next/src/Map/Layers.js +++ b/packages/hurumap-next/src/Map/Layers.js @@ -17,10 +17,10 @@ function Layers({ PinnedLocationTagProps, PopUpLocationTagProps, geography, - choropleth, choroplethColors, isPinOrCompare = false, - locationCodes: locationCodesProp, + locations, + mapType, onClick, onClickUnpin, parentsGeometries, @@ -66,19 +66,72 @@ function Layers({ const onEachFeature = useCallback( (feature, layer) => { - const count = choropleth?.find( + let choropleth = null; + if (mapType === "choropleth") { + const filteredLocations = locations.filter( + ({ count }) => count !== null, + ); + const counts = filteredLocations.map(({ count }) => count); + const maxCount = Math.max(...counts); + const minCount = Math.min(...counts); + + const negativeColorRange = + choroplethColors?.negative_color_range || + defaultChoroplethStyles.negative_color_range; + const positiveColorRange = + choroplethColors?.positive_color_range || + defaultChoroplethStyles.positive_color_range; + const zeroColor = + choroplethColors?.zero_color || defaultChoroplethStyles.zero_color; + const opacity = + choroplethColors?.opacity || defaultChoroplethStyles.opacity; + // const opacityHover = choroplethColors?.opacity_hover || defaultChoroplethStyles.opacity_hover; + + const getColor = (count) => { + if (count === 0) return zeroColor; + + const range = maxCount - minCount; + const negativeThresholds = negativeColorRange.map( + (_, index) => + minCount + range * ((index + 1) / negativeColorRange.length), + ); + const positiveThresholds = positiveColorRange.map( + (_, index) => + minCount + range * ((index + 1) / positiveColorRange.length), + ); + + if (count < 0) { + for (let i = 0; i < negativeThresholds.length; i += 1) { + if (count <= negativeThresholds[i]) return negativeColorRange[i]; + } + return negativeColorRange[negativeColorRange.length - 1]; + } + for (let i = 0; i < positiveThresholds.length; i += 1) { + if (count <= positiveThresholds[i]) return positiveColorRange[i]; + } + return positiveColorRange[positiveColorRange.length - 1]; + }; + + choropleth = filteredLocations.map(({ code, count }) => { + return { + code, + count, + fillColor: getColor(count), + opacity, + }; + }); + } + + const choroplethColor = choropleth?.find( (c) => c.code.toLowerCase() === feature.properties.code.toLowerCase(), ); - const choroplethColor = - (choroplethColors && choroplethColors[count?.classification]) || - defaultChoroplethStyles[count?.classification]; let geoStyles = isPinOrCompare && feature.properties.code === secondaryGeography?.code ? secondaryGeoStyles : primaryGeoStyles; // assume ISO 3166-1 codes so comparing uppercase should be good const locationCodes = - locationCodesProp?.map((c) => c.toUpperCase()) || []; + locations?.map(({ code }) => code)?.map((c) => c.toUpperCase()) || []; if (!locationCodes?.includes(feature.properties.code.toUpperCase())) { layer.setStyle(geoStyles.inactive); } else { @@ -129,7 +182,7 @@ function Layers({ style = geoStyles.selected.out; style = { ...style, - ...(choroplethColor && { fillColor: choroplethColor }), + ...(choroplethColor && { ...choroplethColor }), }; } else if ( isPinOrCompare && @@ -147,7 +200,7 @@ function Layers({ ...(feature?.properties?.selected ? geoStyles.selected.over : geoStyles.hoverOnly.over), - ...(choroplethColor && { fillColor: choroplethColor }), + ...(choroplethColor && { ...choroplethColor }), fillOpacity: 0.5, weight: 2, opacity: 1, @@ -160,7 +213,7 @@ function Layers({ outStyle = geoStyles.selected.out; outStyle = { ...outStyle, - ...(choroplethColor && { fillColor: choroplethColor }), + ...(choroplethColor && { ...choroplethColor }), }; } else if ( isPinOrCompare && @@ -184,12 +237,12 @@ function Layers({ } }, [ - choropleth, choroplethColors, PopUpLocationTagProps, geography, isPinOrCompare, - locationCodesProp, + locations, + mapType, onClick, primaryGeoStyles, secondaryGeoStyles, @@ -273,7 +326,7 @@ Layers.propTypes = { name: PropTypes.string, }), isPinOrCompare: PropTypes.bool, - locationCodes: PropTypes.arrayOf(PropTypes.string), + locations: PropTypes.arrayOf(PropTypes.shape({})), onClick: PropTypes.func, onClickUnpin: PropTypes.func, parentsGeometries: PropTypes.arrayOf(PropTypes.shape({})), diff --git a/packages/hurumap-next/src/Map/LazyMap.js b/packages/hurumap-next/src/Map/LazyMap.js index b7e345c7a..194d02ceb 100644 --- a/packages/hurumap-next/src/Map/LazyMap.js +++ b/packages/hurumap-next/src/Map/LazyMap.js @@ -15,7 +15,6 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) { geometries, isPinOrCompare, locations, - mapType = "default", preferredChildren, styles = { height: "100%", @@ -79,37 +78,6 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) { setSelectedBoundary(selectedBound); }, [geometries, geography, getSelectedBoundary]); - const locationCodes = locations?.map(({ code }) => code); - - let choropleth = null; - if (mapType === "choropleth") { - const filteredLocations = locations.filter(({ count }) => count !== null); - const counts = filteredLocations.map(({ count }) => count); - const maxCount = Math.max(...counts); - const minCount = Math.min(...counts); - - const getClassification = (count) => { - const range = maxCount - minCount; - const veryLowThreshold = minCount + range * 0.2; - const lowThreshold = minCount + range * 0.4; - const moderateThreshold = minCount + range * 0.6; - const highThreshold = minCount + range * 0.8; - - if (count <= veryLowThreshold) return "very low"; - if (count <= lowThreshold) return "low"; - if (count <= moderateThreshold) return "moderate"; - if (count <= highThreshold) return "high"; - return "very high"; - }; - - choropleth = filteredLocations.map(({ code, count }) => { - return { - code, - count, - classification: getClassification(count), - }; - }); - } return ( Date: Wed, 21 Aug 2024 10:42:00 +0300 Subject: [PATCH 07/18] use generateChoropleth function --- packages/hurumap-next/src/Map/Layers.js | 106 +++++++++++------------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/packages/hurumap-next/src/Map/Layers.js b/packages/hurumap-next/src/Map/Layers.js index 3792d39e3..105e6b66c 100644 --- a/packages/hurumap-next/src/Map/Layers.js +++ b/packages/hurumap-next/src/Map/Layers.js @@ -64,63 +64,61 @@ function Layers({ ), }); - const onEachFeature = useCallback( - (feature, layer) => { - let choropleth = null; - if (mapType === "choropleth") { - const filteredLocations = locations.filter( - ({ count }) => count !== null, - ); - const counts = filteredLocations.map(({ count }) => count); - const maxCount = Math.max(...counts); - const minCount = Math.min(...counts); + const generateChoropleth = useCallback(() => { + if (mapType !== "choropleth") return null; - const negativeColorRange = - choroplethColors?.negative_color_range || - defaultChoroplethStyles.negative_color_range; - const positiveColorRange = - choroplethColors?.positive_color_range || - defaultChoroplethStyles.positive_color_range; - const zeroColor = - choroplethColors?.zero_color || defaultChoroplethStyles.zero_color; - const opacity = - choroplethColors?.opacity || defaultChoroplethStyles.opacity; - // const opacityHover = choroplethColors?.opacity_hover || defaultChoroplethStyles.opacity_hover; + const filteredLocations = locations.filter(({ count }) => count !== null); + const counts = filteredLocations.map(({ count }) => count); + const maxCount = Math.max(...counts); + const minCount = Math.min(...counts); - const getColor = (count) => { - if (count === 0) return zeroColor; + const negativeColorRange = + choroplethColors?.negative_color_range || + defaultChoroplethStyles.negative_color_range; + const positiveColorRange = + choroplethColors?.positive_color_range || + defaultChoroplethStyles.positive_color_range; + const zeroColor = + choroplethColors?.zero_color || defaultChoroplethStyles.zero_color; + const opacity = + choroplethColors?.opacity || defaultChoroplethStyles.opacity; - const range = maxCount - minCount; - const negativeThresholds = negativeColorRange.map( - (_, index) => - minCount + range * ((index + 1) / negativeColorRange.length), - ); - const positiveThresholds = positiveColorRange.map( - (_, index) => - minCount + range * ((index + 1) / positiveColorRange.length), - ); + const getColor = (count) => { + if (count === 0) return zeroColor; - if (count < 0) { - for (let i = 0; i < negativeThresholds.length; i += 1) { - if (count <= negativeThresholds[i]) return negativeColorRange[i]; - } - return negativeColorRange[negativeColorRange.length - 1]; - } - for (let i = 0; i < positiveThresholds.length; i += 1) { - if (count <= positiveThresholds[i]) return positiveColorRange[i]; - } - return positiveColorRange[positiveColorRange.length - 1]; - }; + const range = maxCount - minCount; + const thresholds = (colorRange) => + colorRange.map( + (_, index) => minCount + range * ((index + 1) / colorRange.length), + ); - choropleth = filteredLocations.map(({ code, count }) => { - return { - code, - count, - fillColor: getColor(count), - opacity, - }; - }); + const negativeThresholds = thresholds(negativeColorRange); + const positiveThresholds = thresholds(positiveColorRange); + + if (count < 0) { + for (let i = 0; i < negativeThresholds.length; i += 1) { + if (count <= negativeThresholds[i]) return negativeColorRange[i]; + } + return negativeColorRange[negativeColorRange.length - 1]; + } + + for (let i = 0; i < positiveThresholds.length; i += 1) { + if (count <= positiveThresholds[i]) return positiveColorRange[i]; } + return positiveColorRange[positiveColorRange.length - 1]; + }; + + return filteredLocations.map(({ code, count }) => ({ + code, + count, + fillColor: getColor(count), + opacity, + })); + }, [choroplethColors, locations, mapType]); + + const onEachFeature = useCallback( + (feature, layer) => { + const choropleth = generateChoropleth(); const choroplethColor = choropleth?.find( (c) => c.code.toLowerCase() === feature.properties.code.toLowerCase(), @@ -201,9 +199,6 @@ function Layers({ ? geoStyles.selected.over : geoStyles.hoverOnly.over), ...(choroplethColor && { ...choroplethColor }), - fillOpacity: 0.5, - weight: 2, - opacity: 1, }); }); layer.on("mouseout", () => { @@ -237,12 +232,11 @@ function Layers({ } }, [ - choroplethColors, + generateChoropleth, PopUpLocationTagProps, geography, isPinOrCompare, locations, - mapType, onClick, primaryGeoStyles, secondaryGeoStyles, From a3bac0a725f9fb0bcc03b577f199757d7984bb12 Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:55:48 +0300 Subject: [PATCH 08/18] use generateChoropleth function Signed-off-by: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> --- packages/hurumap-next/src/Map/Layers.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/hurumap-next/src/Map/Layers.js b/packages/hurumap-next/src/Map/Layers.js index 105e6b66c..23578ec2b 100644 --- a/packages/hurumap-next/src/Map/Layers.js +++ b/packages/hurumap-next/src/Map/Layers.js @@ -68,7 +68,9 @@ function Layers({ if (mapType !== "choropleth") return null; const filteredLocations = locations.filter(({ count }) => count !== null); + const counts = filteredLocations.map(({ count }) => count); + const maxCount = Math.max(...counts); const minCount = Math.min(...counts); @@ -83,17 +85,19 @@ function Layers({ const opacity = choroplethColors?.opacity || defaultChoroplethStyles.opacity; - const getColor = (count) => { - if (count === 0) return zeroColor; - + // Calculate color thresholds based on count range + const calculateThresholds = (colorRange) => { const range = maxCount - minCount; - const thresholds = (colorRange) => - colorRange.map( - (_, index) => minCount + range * ((index + 1) / colorRange.length), - ); + return colorRange.map( + (_, index) => minCount + range * ((index + 1) / colorRange.length), + ); + }; + + const negativeThresholds = calculateThresholds(negativeColorRange); + const positiveThresholds = calculateThresholds(positiveColorRange); - const negativeThresholds = thresholds(negativeColorRange); - const positiveThresholds = thresholds(positiveColorRange); + const getColor = (count) => { + if (count === 0) return zeroColor; if (count < 0) { for (let i = 0; i < negativeThresholds.length; i += 1) { From a4b392b2a593192e94928c9689262e412ec2a834 Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:29:33 +0300 Subject: [PATCH 09/18] improve choropleth calculation --- packages/hurumap-next/src/Map/Layers.js | 45 ++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/hurumap-next/src/Map/Layers.js b/packages/hurumap-next/src/Map/Layers.js index 23578ec2b..88dfd297c 100644 --- a/packages/hurumap-next/src/Map/Layers.js +++ b/packages/hurumap-next/src/Map/Layers.js @@ -68,9 +68,7 @@ function Layers({ if (mapType !== "choropleth") return null; const filteredLocations = locations.filter(({ count }) => count !== null); - const counts = filteredLocations.map(({ count }) => count); - const maxCount = Math.max(...counts); const minCount = Math.min(...counts); @@ -89,13 +87,15 @@ function Layers({ const calculateThresholds = (colorRange) => { const range = maxCount - minCount; return colorRange.map( - (_, index) => minCount + range * ((index + 1) / colorRange.length), + (_, index) => minCount + range * (index / colorRange.length), ); }; const negativeThresholds = calculateThresholds(negativeColorRange); const positiveThresholds = calculateThresholds(positiveColorRange); + const roundToNearestHalf = (num) => Math.round(num * 2) / 2; + const getColor = (count) => { if (count === 0) return zeroColor; @@ -112,17 +112,52 @@ function Layers({ return positiveColorRange[positiveColorRange.length - 1]; }; - return filteredLocations.map(({ code, count }) => ({ + // Generate legend based on thresholds and color ranges + const generateLegend = () => { + const legend = []; + + // Check if there are any negative counts + const hasNegativeCounts = locations.some(({ count }) => count < 0); + + if (hasNegativeCounts) { + negativeThresholds.forEach((threshold, index) => { + legend.push({ + range: `${index === 0 ? roundToNearestHalf(minCount) : roundToNearestHalf(negativeThresholds[index - 1] + 0.5)} to ${roundToNearestHalf(threshold)}`, + color: negativeColorRange[index], + }); + }); + } + + legend.push({ + range: "0", + color: zeroColor, + }); + + positiveThresholds.forEach((threshold, index) => { + legend.push({ + range: `${index === 0 ? 0.5 : roundToNearestHalf(positiveThresholds[index - 1] + 0.5)} to ${roundToNearestHalf(threshold)}`, + color: positiveColorRange[index], + }); + }); + + return legend; + }; + + const legend = generateLegend(); + + const choroplethData = filteredLocations.map(({ code, count }) => ({ code, count, fillColor: getColor(count), opacity, })); + + return { choropleth: choroplethData, legend }; }, [choroplethColors, locations, mapType]); const onEachFeature = useCallback( (feature, layer) => { - const choropleth = generateChoropleth(); + const { choropleth } = generateChoropleth(); const choroplethColor = choropleth?.find( (c) => c.code.toLowerCase() === feature.properties.code.toLowerCase(), From ebde965d673df8def3bccda4e10b5da2ddd73d18 Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Wed, 21 Aug 2024 15:41:03 +0300 Subject: [PATCH 10/18] Calculate legend values --- packages/hurumap-next/src/Map/Layers.js | 77 ++++++++----------------- 1 file changed, 25 insertions(+), 52 deletions(-) diff --git a/packages/hurumap-next/src/Map/Layers.js b/packages/hurumap-next/src/Map/Layers.js index 88dfd297c..8570c271f 100644 --- a/packages/hurumap-next/src/Map/Layers.js +++ b/packages/hurumap-next/src/Map/Layers.js @@ -71,6 +71,9 @@ function Layers({ const counts = filteredLocations.map(({ count }) => count); const maxCount = Math.max(...counts); const minCount = Math.min(...counts); + const roundedMinCount = Math.floor(minCount); + const roundedMaxCount = Math.ceil(maxCount); + const range = roundedMaxCount - roundedMinCount; const negativeColorRange = choroplethColors?.negative_color_range || @@ -83,67 +86,37 @@ function Layers({ const opacity = choroplethColors?.opacity || defaultChoroplethStyles.opacity; - // Calculate color thresholds based on count range - const calculateThresholds = (colorRange) => { - const range = maxCount - minCount; - return colorRange.map( - (_, index) => minCount + range * (index / colorRange.length), + const calculateThresholds = (steps) => { + const stepSize = range / (steps - 1); + const thresholds = Array.from( + { length: steps }, + (_, i) => roundedMinCount + i * stepSize, ); + return thresholds; }; - const negativeThresholds = calculateThresholds(negativeColorRange); - const positiveThresholds = calculateThresholds(positiveColorRange); + const positiveThresholds = calculateThresholds(positiveColorRange.length); + const negativeThresholds = calculateThresholds(negativeColorRange.length); - const roundToNearestHalf = (num) => Math.round(num * 2) / 2; - - const getColor = (count) => { - if (count === 0) return zeroColor; - - if (count < 0) { - for (let i = 0; i < negativeThresholds.length; i += 1) { - if (count <= negativeThresholds[i]) return negativeColorRange[i]; - } - return negativeColorRange[negativeColorRange.length - 1]; - } - - for (let i = 0; i < positiveThresholds.length; i += 1) { - if (count <= positiveThresholds[i]) return positiveColorRange[i]; - } - return positiveColorRange[positiveColorRange.length - 1]; - }; - - // Generate legend based on thresholds and color ranges const generateLegend = () => { - const legend = []; - - // Check if there are any negative counts - const hasNegativeCounts = locations.some(({ count }) => count < 0); - - if (hasNegativeCounts) { - negativeThresholds.forEach((threshold, index) => { - legend.push({ - range: `${index === 0 ? roundToNearestHalf(minCount) : roundToNearestHalf(negativeThresholds[index - 1] + 0.5)} to ${roundToNearestHalf(threshold)}`, - color: negativeColorRange[index], - }); - }); - } - - legend.push({ - range: "0", - color: zeroColor, - }); - - positiveThresholds.forEach((threshold, index) => { - legend.push({ - range: `${index === 0 ? 0.5 : roundToNearestHalf(positiveThresholds[index - 1] + 0.5)} to ${roundToNearestHalf(threshold)}`, - color: positiveColorRange[index], - }); + const legend = {}; + const thresholds = positiveThresholds.concat(negativeThresholds); + const colorRange = positiveColorRange.concat(negativeColorRange); + thresholds.forEach((threshold, i) => { + legend[threshold] = colorRange[i]; }); - return legend; }; - const legend = generateLegend(); + const legend = generateLegend(positiveThresholds, positiveColorRange); + + 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); + return colorRange[index]; + }; const choroplethData = filteredLocations.map(({ code, count }) => ({ code, From 4c11900a69ccfe2a1f0a0d54837cab406529847e Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Wed, 21 Aug 2024 15:51:04 +0300 Subject: [PATCH 11/18] Add basic map legend Signed-off-by: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> --- packages/hurumap-next/src/Map/LazyMap.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/hurumap-next/src/Map/LazyMap.js b/packages/hurumap-next/src/Map/LazyMap.js index 194d02ceb..ea363359c 100644 --- a/packages/hurumap-next/src/Map/LazyMap.js +++ b/packages/hurumap-next/src/Map/LazyMap.js @@ -1,3 +1,4 @@ +import { Typography, Box } from "@mui/material"; import PropTypes from "prop-types"; import React, { useCallback, useEffect, useState } from "react"; import { MapContainer, Pane, TileLayer, ZoomControl } from "react-leaflet"; @@ -102,6 +103,23 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) { ))} + ({ + position: "absolute", + zIndex: 1000, + bottom: theme.spacing(1), + right: theme.spacing(12), + backgroundColor: theme.palette.background.paper, + padding: theme.spacing(3), + borderRadius: theme.shape.borderRadius, + boxShadow: theme.shadows[3], + ...sx, + })} + > + + Legend for the map goes here + + Date: Wed, 21 Aug 2024 16:14:09 +0300 Subject: [PATCH 12/18] Move calculations to individual file --- packages/hurumap-next/src/Map/Layers.js | 80 ++---------------------- packages/hurumap-next/src/Map/LazyMap.js | 14 ++++- packages/hurumap-next/src/Map/utils.js | 66 +++++++++++++++++++ 3 files changed, 85 insertions(+), 75 deletions(-) create mode 100644 packages/hurumap-next/src/Map/utils.js diff --git a/packages/hurumap-next/src/Map/Layers.js b/packages/hurumap-next/src/Map/Layers.js index 8570c271f..9d9a01f40 100644 --- a/packages/hurumap-next/src/Map/Layers.js +++ b/packages/hurumap-next/src/Map/Layers.js @@ -10,17 +10,15 @@ import { FeatureGroup, GeoJSON, useMap } from "react-leaflet"; import { defaultPrimaryGeoStyles, defaultSecondaryGeoStyles, - defaultChoroplethStyles, } from "./geoStyles"; function Layers({ PinnedLocationTagProps, PopUpLocationTagProps, + choropleth, geography, - choroplethColors, isPinOrCompare = false, - locations, - mapType, + locationCodes: locationCodesProp, onClick, onClickUnpin, parentsGeometries, @@ -64,74 +62,8 @@ function Layers({ ), }); - const generateChoropleth = useCallback(() => { - if (mapType !== "choropleth") return null; - - const filteredLocations = locations.filter(({ count }) => count !== null); - const counts = filteredLocations.map(({ count }) => count); - const maxCount = Math.max(...counts); - const minCount = Math.min(...counts); - const roundedMinCount = Math.floor(minCount); - const roundedMaxCount = Math.ceil(maxCount); - const range = roundedMaxCount - roundedMinCount; - - const negativeColorRange = - choroplethColors?.negative_color_range || - defaultChoroplethStyles.negative_color_range; - const positiveColorRange = - choroplethColors?.positive_color_range || - defaultChoroplethStyles.positive_color_range; - const zeroColor = - choroplethColors?.zero_color || defaultChoroplethStyles.zero_color; - const opacity = - choroplethColors?.opacity || defaultChoroplethStyles.opacity; - - const calculateThresholds = (steps) => { - const stepSize = range / (steps - 1); - const thresholds = Array.from( - { length: steps }, - (_, i) => roundedMinCount + i * stepSize, - ); - return thresholds; - }; - - const positiveThresholds = calculateThresholds(positiveColorRange.length); - const negativeThresholds = calculateThresholds(negativeColorRange.length); - - const generateLegend = () => { - const legend = {}; - const thresholds = positiveThresholds.concat(negativeThresholds); - const colorRange = positiveColorRange.concat(negativeColorRange); - thresholds.forEach((threshold, i) => { - legend[threshold] = colorRange[i]; - }); - return legend; - }; - - const legend = generateLegend(positiveThresholds, positiveColorRange); - - 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); - return colorRange[index]; - }; - - const choroplethData = filteredLocations.map(({ code, count }) => ({ - code, - count, - fillColor: getColor(count), - opacity, - })); - - return { choropleth: choroplethData, legend }; - }, [choroplethColors, locations, mapType]); - const onEachFeature = useCallback( (feature, layer) => { - const { choropleth } = generateChoropleth(); - const choroplethColor = choropleth?.find( (c) => c.code.toLowerCase() === feature.properties.code.toLowerCase(), ); @@ -141,7 +73,7 @@ function Layers({ : primaryGeoStyles; // assume ISO 3166-1 codes so comparing uppercase should be good const locationCodes = - locations?.map(({ code }) => code)?.map((c) => c.toUpperCase()) || []; + locationCodesProp?.map((c) => c.toUpperCase()) || []; if (!locationCodes?.includes(feature.properties.code.toUpperCase())) { layer.setStyle(geoStyles.inactive); } else { @@ -244,11 +176,11 @@ function Layers({ } }, [ - generateChoropleth, PopUpLocationTagProps, + choropleth, geography, isPinOrCompare, - locations, + locationCodesProp, onClick, primaryGeoStyles, secondaryGeoStyles, @@ -332,7 +264,7 @@ Layers.propTypes = { name: PropTypes.string, }), isPinOrCompare: PropTypes.bool, - locations: PropTypes.arrayOf(PropTypes.shape({})), + locationCodes: PropTypes.arrayOf(PropTypes.string), onClick: PropTypes.func, onClickUnpin: PropTypes.func, parentsGeometries: PropTypes.arrayOf(PropTypes.shape({})), diff --git a/packages/hurumap-next/src/Map/LazyMap.js b/packages/hurumap-next/src/Map/LazyMap.js index ea363359c..97df418c2 100644 --- a/packages/hurumap-next/src/Map/LazyMap.js +++ b/packages/hurumap-next/src/Map/LazyMap.js @@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { MapContainer, Pane, TileLayer, ZoomControl } from "react-leaflet"; import Layers from "./Layers"; +import { generateChoropleth } from "./utils"; import "leaflet/dist/leaflet.css"; @@ -12,10 +13,12 @@ import "leaflet/dist/leaflet.css"; const LazyMap = React.forwardRef(function LazyMap(props, ref) { const { center, + choroplethColors, geography, geometries, isPinOrCompare, locations, + mapType, preferredChildren, styles = { height: "100%", @@ -47,6 +50,12 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) { [preferredChildren, isPinOrCompare], ); + const { choropleth } = generateChoropleth( + choroplethColors, + locations, + mapType, + ); + useEffect(() => { let selectedBound = getSelectedBoundary(geography.level, geometries) ?? geometries.boundary; @@ -79,6 +88,8 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) { setSelectedBoundary(selectedBound); }, [geometries, geography, getSelectedBoundary]); + const locationCodes = locations?.map(({ code }) => code); + return ( { + if (mapType !== "choropleth") return null; + + const filteredLocations = locations.filter(({ count }) => count !== null); + const counts = filteredLocations.map(({ count }) => count); + const maxCount = Math.max(...counts); + const minCount = Math.min(...counts); + const roundedMinCount = Math.floor(minCount); + const roundedMaxCount = Math.ceil(maxCount); + const range = roundedMaxCount - roundedMinCount; + + const negativeColorRange = + choroplethColors?.negative_color_range || + defaultChoroplethStyles.negative_color_range; + const positiveColorRange = + choroplethColors?.positive_color_range || + defaultChoroplethStyles.positive_color_range; + const zeroColor = + choroplethColors?.zero_color || defaultChoroplethStyles.zero_color; + const opacity = choroplethColors?.opacity || defaultChoroplethStyles.opacity; + + const calculateThresholds = (steps) => { + const stepSize = range / (steps - 1); + const thresholds = Array.from( + { length: steps }, + (_, i) => roundedMinCount + i * stepSize, + ); + return thresholds; + }; + + const positiveThresholds = calculateThresholds(positiveColorRange.length); + const negativeThresholds = calculateThresholds(negativeColorRange.length); + + const generateLegend = () => { + const legend = {}; + const thresholds = positiveThresholds.concat(negativeThresholds); + const colorRange = positiveColorRange.concat(negativeColorRange); + thresholds.forEach((threshold, i) => { + legend[threshold] = colorRange[i]; + }); + return legend; + }; + + const legend = generateLegend(positiveThresholds, positiveColorRange); + + 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); + return colorRange[index]; + }; + + const choroplethData = filteredLocations.map(({ code, count }) => ({ + code, + count, + fillColor: getColor(count), + opacity, + })); + + return { choropleth: choroplethData, legend }; +}; From edec26a7038b940b5a725a63d068c43266ec3acb Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Wed, 21 Aug 2024 18:21:08 +0300 Subject: [PATCH 13/18] Add legend --- packages/hurumap-next/src/Map/LazyMap.js | 36 +++++-- packages/hurumap-next/src/Map/utils.js | 124 +++++++++++++++++------ 2 files changed, 120 insertions(+), 40 deletions(-) diff --git a/packages/hurumap-next/src/Map/LazyMap.js b/packages/hurumap-next/src/Map/LazyMap.js index 97df418c2..dd5dbdc0e 100644 --- a/packages/hurumap-next/src/Map/LazyMap.js +++ b/packages/hurumap-next/src/Map/LazyMap.js @@ -1,4 +1,4 @@ -import { Typography, Box } from "@mui/material"; +import { Typography, Grid } from "@mui/material"; import PropTypes from "prop-types"; import React, { useCallback, useEffect, useState } from "react"; import { MapContainer, Pane, TileLayer, ZoomControl } from "react-leaflet"; @@ -50,7 +50,7 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) { [preferredChildren, isPinOrCompare], ); - const { choropleth } = generateChoropleth( + const { choropleth, legend } = generateChoropleth( choroplethColors, locations, mapType, @@ -114,23 +114,39 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) { ))} - ({ position: "absolute", zIndex: 1000, + width: "fit-content", bottom: theme.spacing(1), - right: theme.spacing(12), + left: "50%", + transform: "translateX(-50%)", backgroundColor: theme.palette.background.paper, - padding: theme.spacing(3), - borderRadius: theme.shape.borderRadius, + padding: theme.spacing(1), boxShadow: theme.shadows[3], ...sx, })} > - - Legend for the map goes here - - + {legend?.map(({ min, max, color }) => ( + ({ + backgroundColor: color, + padding: theme.spacing(1), + })} + > + ({ color: theme.palette.text.secondary })} + > + {min} - {max} + + + ))} + { + const stepSize = (max - min) / steps; + const thresholds = []; + + for (let i = 0; i < steps; 1 + i) { + thresholds.push({ + min: min + i * stepSize, + max: 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 = (choroplethColors, 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 range = roundedMaxCount - roundedMinCount; const negativeColorRange = choroplethColors?.negative_color_range || @@ -23,44 +80,51 @@ export const generateChoropleth = (choroplethColors, locations, mapType) => { choroplethColors?.zero_color || defaultChoroplethStyles.zero_color; const opacity = choroplethColors?.opacity || defaultChoroplethStyles.opacity; - const calculateThresholds = (steps) => { - const stepSize = range / (steps - 1); - const thresholds = Array.from( - { length: steps }, - (_, i) => roundedMinCount + i * stepSize, - ); - return thresholds; - }; + const positiveThresholds = hasPositiveValues + ? calculateThresholds( + roundedMinCount, + roundedMaxCount, + positiveColorRange.length, + ) + : []; + const negativeThresholds = hasNegativeValues + ? calculateThresholds( + roundedMinCount, + roundedMaxCount, + negativeColorRange.length, + ) + : []; - const positiveThresholds = calculateThresholds(positiveColorRange.length); - const negativeThresholds = calculateThresholds(negativeColorRange.length); - - const generateLegend = () => { - const legend = {}; - const thresholds = positiveThresholds.concat(negativeThresholds); - const colorRange = positiveColorRange.concat(negativeColorRange); - thresholds.forEach((threshold, i) => { - legend[threshold] = colorRange[i]; - }); - return legend; - }; - - const legend = generateLegend(positiveThresholds, positiveColorRange); + 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); + const index = thresholds.findIndex( + (threshold) => count >= threshold.min && count < threshold.max, + ); return colorRange[index]; }; - const choroplethData = filteredLocations.map(({ code, count }) => ({ - code, - count, - fillColor: getColor(count), - opacity, - })); + const choroplethData = filteredLocations.map(({ code, count }) => { + const color = getColor(count); + + return { + code, + count, + fillColor: color, + opacity, + }; + }); return { choropleth: choroplethData, legend }; }; From 336ba96224864da824bb1f39558ca4d726696180 Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Wed, 21 Aug 2024 18:28:27 +0300 Subject: [PATCH 14/18] Fix loop bug --- packages/hurumap-next/src/Map/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hurumap-next/src/Map/utils.js b/packages/hurumap-next/src/Map/utils.js index 847a7547f..8193c1176 100644 --- a/packages/hurumap-next/src/Map/utils.js +++ b/packages/hurumap-next/src/Map/utils.js @@ -6,7 +6,7 @@ const calculateThresholds = (min, max, steps) => { const stepSize = (max - min) / steps; const thresholds = []; - for (let i = 0; i < steps; 1 + i) { + for (let i = 0; i < steps; i += 1) { thresholds.push({ min: min + i * stepSize, max: min + (i + 1) * stepSize, From d48565fd80094044b7347e70db4a52e7d5277516 Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Fri, 23 Aug 2024 10:11:12 +0300 Subject: [PATCH 15/18] Improve legend --- packages/hurumap-next/src/Map/LazyMap.js | 36 +-------------- packages/hurumap-next/src/Map/Legend.js | 58 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 34 deletions(-) create mode 100644 packages/hurumap-next/src/Map/Legend.js diff --git a/packages/hurumap-next/src/Map/LazyMap.js b/packages/hurumap-next/src/Map/LazyMap.js index dd5dbdc0e..753a4c913 100644 --- a/packages/hurumap-next/src/Map/LazyMap.js +++ b/packages/hurumap-next/src/Map/LazyMap.js @@ -1,9 +1,9 @@ -import { Typography, Grid } from "@mui/material"; import PropTypes from "prop-types"; 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"; @@ -114,39 +114,7 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) { ))} - ({ - position: "absolute", - zIndex: 1000, - width: "fit-content", - bottom: theme.spacing(1), - left: "50%", - transform: "translateX(-50%)", - backgroundColor: theme.palette.background.paper, - padding: theme.spacing(1), - boxShadow: theme.shadows[3], - ...sx, - })} - > - {legend?.map(({ min, max, color }) => ( - ({ - backgroundColor: color, - padding: theme.spacing(1), - })} - > - ({ color: theme.palette.text.secondary })} - > - {min} - {max} - - - ))} - + ({ + 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} + + + + ))} + + ); +} From b75d34a4fb63c597e40cc50f34dd9ca2e977cd7c Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:09:07 +0300 Subject: [PATCH 16/18] Update default choropleth colors --- packages/hurumap-next/src/Map/geoStyles.js | 20 ++++++++++++++++++-- packages/hurumap-next/src/Map/utils.js | 8 ++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/hurumap-next/src/Map/geoStyles.js b/packages/hurumap-next/src/Map/geoStyles.js index 06cffb430..aed496603 100644 --- a/packages/hurumap-next/src/Map/geoStyles.js +++ b/packages/hurumap-next/src/Map/geoStyles.js @@ -78,8 +78,24 @@ const defaultSecondaryGeoStyles = { }; const defaultChoroplethStyles = { - negative_color_range: CHART_SECONDARY_COLOR_SCHEME, - positive_color_range: CHART_PRIMARY_COLOR_SCHEME, + 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", diff --git a/packages/hurumap-next/src/Map/utils.js b/packages/hurumap-next/src/Map/utils.js index 8193c1176..fec110bd2 100644 --- a/packages/hurumap-next/src/Map/utils.js +++ b/packages/hurumap-next/src/Map/utils.js @@ -2,14 +2,18 @@ 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: min + i * stepSize, - max: min + (i + 1) * stepSize, + min: roundToNearestHalf(min + i * stepSize), + max: roundToNearestHalf(min + (i + 1) * stepSize), }); } From 704e9a38cd7a58918c25a698defdb7dda96d545e Mon Sep 17 00:00:00 2001 From: Kipruto <43873157+kelvinkipruto@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:20:27 +0300 Subject: [PATCH 17/18] Show legend conditionally --- packages/hurumap-next/src/Map/LazyMap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hurumap-next/src/Map/LazyMap.js b/packages/hurumap-next/src/Map/LazyMap.js index 753a4c913..d4959f9b2 100644 --- a/packages/hurumap-next/src/Map/LazyMap.js +++ b/packages/hurumap-next/src/Map/LazyMap.js @@ -114,7 +114,7 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) { ))} - + {mapType === "choropleth" && } Date: Fri, 23 Aug 2024 12:30:32 +0300 Subject: [PATCH 18/18] Rename choroplethColors to choropleth --- apps/climatemappedafrica/src/lib/hurumap/index.js | 2 +- .../src/pages/explore/[[...slug]].js | 4 ++-- packages/hurumap-next/src/Map/LazyMap.js | 4 ++-- packages/hurumap-next/src/Map/utils.js | 11 +++++------ 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/climatemappedafrica/src/lib/hurumap/index.js b/apps/climatemappedafrica/src/lib/hurumap/index.js index 26706fdfd..3332d79ca 100644 --- a/apps/climatemappedafrica/src/lib/hurumap/index.js +++ b/apps/climatemappedafrica/src/lib/hurumap/index.js @@ -20,7 +20,7 @@ export async function fetchProfile() { locations, preferredChildren: configuration.preferred_children, mapType: configuration?.map_type ?? "default", - choroplethColors: configuration?.choropleth ?? null, + choropleth: configuration?.choropleth ?? null, }; } diff --git a/apps/climatemappedafrica/src/pages/explore/[[...slug]].js b/apps/climatemappedafrica/src/pages/explore/[[...slug]].js index 4e9dc80f0..8bb73f377 100644 --- a/apps/climatemappedafrica/src/pages/explore/[[...slug]].js +++ b/apps/climatemappedafrica/src/pages/explore/[[...slug]].js @@ -190,7 +190,7 @@ export async function getStaticProps({ params }) { ghostkitSR: "", }, }; - const { locations, preferredChildren, mapType, choroplethColors } = + const { locations, preferredChildren, mapType, choropleth } = await fetchProfile(); const [originalCode] = params?.slug || [""]; const code = originalCode.trim().toLowerCase(); @@ -240,7 +240,7 @@ export async function getStaticProps({ params }) { props: { ...props, blocks, - choroplethColors, + choropleth, locations, mapType, profile, diff --git a/packages/hurumap-next/src/Map/LazyMap.js b/packages/hurumap-next/src/Map/LazyMap.js index d4959f9b2..25329f0a2 100644 --- a/packages/hurumap-next/src/Map/LazyMap.js +++ b/packages/hurumap-next/src/Map/LazyMap.js @@ -13,7 +13,7 @@ import "leaflet/dist/leaflet.css"; const LazyMap = React.forwardRef(function LazyMap(props, ref) { const { center, - choroplethColors, + choropleth: choroplethProps, geography, geometries, isPinOrCompare, @@ -51,7 +51,7 @@ const LazyMap = React.forwardRef(function LazyMap(props, ref) { ); const { choropleth, legend } = generateChoropleth( - choroplethColors, + choroplethProps, locations, mapType, ); diff --git a/packages/hurumap-next/src/Map/utils.js b/packages/hurumap-next/src/Map/utils.js index fec110bd2..21d347907 100644 --- a/packages/hurumap-next/src/Map/utils.js +++ b/packages/hurumap-next/src/Map/utils.js @@ -62,7 +62,7 @@ const generateLegend = ( return legend; }; -export const generateChoropleth = (choroplethColors, locations, mapType) => { +export const generateChoropleth = (colors, locations, mapType) => { if (mapType !== "choropleth") return null; const filteredLocations = locations.filter(({ count }) => count !== null); @@ -75,14 +75,13 @@ export const generateChoropleth = (choroplethColors, locations, mapType) => { const roundedMaxCount = Math.ceil(maxCount); const negativeColorRange = - choroplethColors?.negative_color_range || + colors?.negative_color_range || defaultChoroplethStyles.negative_color_range; const positiveColorRange = - choroplethColors?.positive_color_range || + colors?.positive_color_range || defaultChoroplethStyles.positive_color_range; - const zeroColor = - choroplethColors?.zero_color || defaultChoroplethStyles.zero_color; - const opacity = choroplethColors?.opacity || defaultChoroplethStyles.opacity; + const zeroColor = colors?.zero_color || defaultChoroplethStyles.zero_color; + const opacity = colors?.opacity || defaultChoroplethStyles.opacity; const positiveThresholds = hasPositiveValues ? calculateThresholds(