From 042360c56180381142a2faa70c73995b81497578 Mon Sep 17 00:00:00 2001 From: Limber Mamani Vallejos Date: Mon, 20 Jan 2025 11:20:47 -0400 Subject: [PATCH 1/3] [TM-1575] adds dynamic description functionality --- .../MonitoredTab/components/DataCard.tsx | 229 +++++++++++++++++- src/utils/MonitoredIndicatorUtils.ts | 47 ++++ 2 files changed, 267 insertions(+), 9 deletions(-) create mode 100644 src/utils/MonitoredIndicatorUtils.ts diff --git a/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx b/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx index d9e7ed1ab..65bbb4193 100644 --- a/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx +++ b/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx @@ -38,6 +38,13 @@ import { parsePolygonsIndicatorDataForStrategies, parseTreeCoverData } from "@/utils/dashboardUtils"; +import { + calculatePercentage, + formatDescriptionIndicator, + getKeyValue, + getOrderTop3, + replaceTextWithParams +} from "@/utils/MonitoredIndicatorUtils"; import { downloadFileBlob } from "@/utils/network"; import { useMonitoredData } from "../hooks/useMonitoredData"; @@ -124,27 +131,37 @@ const DROPDOWN_OPTIONS = [ { title: "Tree Cover Loss", value: "1", - slug: "treeCoverLoss" + slug: "treeCoverLoss", + description: + "Tree cover loss and tree cover loss by fires gives an indication of any past deforestation events in the project area prior to the project start date. To ensure additionality of the portfolio, we aim to fund projects that have experienced minimal disturbances 10 years before the project start date.

From [year_start] to [year_end], the project area being restored today by [organization_name] lost [x_ha] ha of tree cover from fires and [xx_ha] from all other drivers of loss. The total tree cover loss presents [x_%]% of project area.[sites]

The following data layer is used for lookback analysis.

UMD tree cover loss (Global, 30m, annual, 2001-2020)

Shows year-by-year tree cover loss, defined as stand level replacement of vegetation greater than 5 meters, within the selected area. Note that “tree cover loss” is not the same as “deforestation” – tree cover loss includes change in both natural and planted forest and does not need to be human caused. The data from 2011 onward were produced with an updated methodology that may capture additional loss.

Tree cover loss due to fires (Global, 30m, annual, 2001-2020)

Identifies areas of tree cover loss due to fires compared to all other drivers of tree cover loss. This data is produced by the Global Land Analysis & Discovery (GLAD) lab at the University of Maryland and measures areas of tree cover loss due to fires compared to all other drivers across all global land (except Antarctica and other Arctic islands) at approximately 30 × 30- meter resolution. The data were generated using global Landsat-based annual change detection metrics for 2001-2020 as input data to a set of regionally calibrated classification tree ensemble models. The result of the mapping process can be viewed as a set of binary maps (tree cover loss due to fire vs. tree cover loss due to all other drivers)" }, { title: "Tree Cover Loss from Fire", value: "2", - slug: "treeCoverLossFires" + slug: "treeCoverLossFires", + description: + "Tree cover loss and tree cover loss by fires gives an indication of any past deforestation events in the project area prior to the project start date. To ensure additionality of the portfolio, we aim to fund projects that have experienced minimal disturbances 10 years before the project start date.

From [year_start] to [year_end], the project area being restored today by [organization_name] lost [x_ha] ha of tree cover from fires and [xx_ha] from all other drivers of loss. The total tree cover loss presents [x_%]% of project area.[sites]

The following data layer is used for lookback analysis.

UMD tree cover loss (Global, 30m, annual, 2001-2020)

Shows year-by-year tree cover loss, defined as stand level replacement of vegetation greater than 5 meters, within the selected area. Note that “tree cover loss” is not the same as “deforestation” – tree cover loss includes change in both natural and planted forest and does not need to be human caused. The data from 2011 onward were produced with an updated methodology that may capture additional loss.

Tree cover loss due to fires (Global, 30m, annual, 2001-2020)

Identifies areas of tree cover loss due to fires compared to all other drivers of tree cover loss. This data is produced by the Global Land Analysis & Discovery (GLAD) lab at the University of Maryland and measures areas of tree cover loss due to fires compared to all other drivers across all global land (except Antarctica and other Arctic islands) at approximately 30 × 30- meter resolution. The data were generated using global Landsat-based annual change detection metrics for 2001-2020 as input data to a set of regionally calibrated classification tree ensemble models. The result of the mapping process can be viewed as a set of binary maps (tree cover loss due to fire vs. tree cover loss due to all other drivers)" }, { title: "Hectares Under Restoration By WWF EcoRegion", value: "3", - slug: "restorationByEcoRegion" + slug: "restorationByEcoRegion", + description: + "According to the polygons approved for this project, [organization_name] is restoring [x_ha] hectares, [x_%]% of their [x_ha_goal] ha goal. [restoration_eco_region]

This analysis was last updated on [date_run] analysis using the WWF ecoregion dataset." }, { title: "Hectares Under Restoration By Strategy", value: "4", - slug: "restorationByStrategy" + slug: "restorationByStrategy", + description: + "According to the polygons approved for this project, [organization_name] is restoring [x_ha] hectares, [x_%]% of their [x_ha_goal] ha goal.

Within these hectares, the most prevalent restoration strategy used to restore land was [x_1a], present on [x_1b] ha. [other_restoration_strategies]

This analysis was last updated on [date_run] analysis and is calculated by the sum of hectares of all approved polygons and their relevant attribute data. You can learn more about restoration strategies and their definitions here." }, { title: "Hectares Under Restoration By Target Land Use System", value: "5", - slug: "restorationByLandUse" + slug: "restorationByLandUse", + description: + "According to the polygons approved for this project, [organization_name] is restoring [x_ha] hectares, [x_%]% of their [x_ha_goal] ha goal.

Within these hectares, the most prevalent target land use system is [x_1a] with [x_1b] ha. [other_target_land_use]

This analysis was last updated on [date_run] analysis and is calculated by the sum of hectares of all approved polygons and their relevant attribute data. You can learn more about restoration strategies and their definitions here." } ]; @@ -234,6 +251,39 @@ const noDataMap = ( ); +const sumValuesTreeCoverLoss = (data: any) => { + return data?.reduce((totalAcc: number, data: { [key: number | string]: number }) => { + const sum = Object.values(data?.data).reduce((acc: number, curr: number) => acc + curr, 0); + return totalAcc + sum; + }, 0); +}; + +const groupedBySiteUuidWithPolygons = (data: any[]) => { + return data.reduce((acc, polygon) => { + acc[polygon.site_name] = acc[polygon.site_name] || []; + acc[polygon.site_name].push(polygon); + return acc; + }, {} as Record); +}; + +const getSiteValues = (data: any[]) => { + const arrayValues: any[] = []; + Object.entries(data).forEach(([siteName, polygons]) => { + arrayValues.push({ [siteName]: polygons }); + }); + return arrayValues; +}; + +const getSiteTreeCoverLossSumValues = (data: any[]) => { + const arrayValues: any[] = []; + data.forEach((site: any) => { + Object.entries(site).forEach(([siteName, polygons]) => { + arrayValues.push({ [siteName]: sumValuesTreeCoverLoss(polygons) }); + }); + }); + return arrayValues; +}; + const DataCard = ({ type, ...rest @@ -264,6 +314,16 @@ const DataCard = ({ : treeCoverLossFiresData; const parsedData = parseTreeCoverData(filteredTreeCoverLossData, filteredTreeCoverLossFiresData); + + const sumTreeCoverData = parsedData.reduce( + (acc, data) => { + const treeCoverLoss = acc.treeCoverLoss + data.treeCoverLoss; + const treeCoverLossFires = acc.treeCoverLossFires + data.treeCoverLossFires; + return { treeCoverLoss, treeCoverLossFires }; + }, + { treeCoverLoss: 0, treeCoverLossFires: 0 } + ); + const { setSearchTerm, setIndicatorSlug, indicatorSlug, setSelectPolygonFromMap, selectPolygonFromMap } = useMonitoredDataContext(); const navigate = useNavigate(); @@ -597,6 +657,157 @@ const DataCard = ({ setSelectPolygonFromMap?.({ isOpen: false, uuid: "" }); } }, [selectPolygonFromMap]); + + const dateRunIndicator = polygonsIndicator?.[polygonsIndicator.length - 1] + ? format(new Date(polygonsIndicator?.[polygonsIndicator.length - 1]?.created_at!), "dd/MM/yyyy") + : ""; + const sitePolygonsIndicator = getSiteValues(groupedBySiteUuidWithPolygons(polygonsIndicator)); + const sortedTreeCoverSiteValues = getSiteTreeCoverLossSumValues(sitePolygonsIndicator).sort((a, b) => { + const valueA: any = Object.values(a)[0]; + const valueB: any = Object.values(b)[0]; + + return valueB - valueA; + }); + + const sumRestorationByValues = (data: any[], landUse: boolean) => { + return data?.reduce((acc, polygon) => { + if (landUse) return acc + (parseInt(polygon?.valueText?.match(/^(.*?)ha/)![1].trim(), 10) || 0); + return acc + (polygon.value || 0); + }, 0); + }; + + const valuesItemsTreecover = { + [getKeyValue(sortedTreeCoverSiteValues[0])?.name as string]: Math.round( + getKeyValue(sortedTreeCoverSiteValues[0])?.value as number + ), + [getKeyValue(sortedTreeCoverSiteValues[1])?.name as string]: Math.round( + getKeyValue(sortedTreeCoverSiteValues[1])?.value as number + ), + [getKeyValue(sortedTreeCoverSiteValues[2])?.name as string]: Math.round( + getKeyValue(sortedTreeCoverSiteValues[2])?.value as number + ) + }; + + const valuesItemsLandUse = { + [getOrderTop3(landUseData.graphicTargetLandUseTypes)?.[1]?.label as string]: Math.round( + Math.round(getOrderTop3(landUseData.graphicTargetLandUseTypes)?.[1]?.value) + ), + [getOrderTop3(landUseData.graphicTargetLandUseTypes)?.[2]?.label as string]: Math.round( + Math.round(getOrderTop3(landUseData.graphicTargetLandUseTypes)?.[2]?.value) + ) + }; + + const valuesItemsRestorationBy = { + [getOrderTop3(strategiesData)?.[1]?.label as string]: Math.round( + Math.round(getOrderTop3(strategiesData)?.[1]?.value) + ), + [getOrderTop3(strategiesData)?.[2]?.label as string]: Math.round( + Math.round(getOrderTop3(strategiesData)?.[2]?.value) + ) + }; + + const sitesMostDisturbancesText = + !record?.project && sortedTreeCoverSiteValues.length > 0 + ? `

The sites that had the most disturbances are ${formatDescriptionIndicator( + valuesItemsTreecover, + record.total_hectares_restored_sum, + true + )}` + : ""; + + const restorationEcoregionText = + ecoRegionData.chartData.length > 0 + ? `

Within these hectares, the project is located within ${ + ecoRegionData.chartData.length + } major ecoregion(s): ${calculatePercentage( + ecoRegionData.chartData[0].value, + record.total_hectares_restored_sum + )}% of the project takes place in the ${ecoRegionData.chartData[0].name} ecoregion ${ + ecoRegionData.chartData?.[1] + ? `[and ${calculatePercentage( + ecoRegionData.chartData[1].value, + record.total_hectares_restored_sum + )}% of the project takes place in the ${ecoRegionData.chartData[1].name} ecoregion].` + : "." + }` + : ""; + + const monitoredDescriptionParams: Record = { + treeCoverLoss: { + "[organization_name]": record?.organisation?.name, + "[year_start]": 2015, + "[year_end]": 2024, + "[x_ha]": Math.round(sumTreeCoverData.treeCoverLossFires), + "[xx_ha]": Math.round(sumTreeCoverData.treeCoverLoss), + "[x_%]": calculatePercentage( + sumTreeCoverData.treeCoverLossFires + sumTreeCoverData.treeCoverLoss, + record.total_hectares_restored_sum + ), + "[sites]": sitesMostDisturbancesText + }, + treeCoverLossFires: { + "[organization_name]": record?.organisation?.name, + "[year_start]": 2015, + "[year_end]": 2024, + "[x_ha]": Math.round(sumTreeCoverData.treeCoverLossFires), + "[xx_ha]": Math.round(sumTreeCoverData.treeCoverLoss), + "[x_%]": calculatePercentage( + sumTreeCoverData.treeCoverLossFires + sumTreeCoverData.treeCoverLoss, + record.total_hectares_restored_sum + ), + "[sites]": sitesMostDisturbancesText + }, + restorationByEcoRegion: { + "[organization_name]": record?.organisation?.name, + "[date_run]": dateRunIndicator, + "[x_ha]": Math.round(ecoRegionData.total), + "[x_%]": calculatePercentage(ecoRegionData.total, record.total_hectares_restored_sum), + "[x_ha_goal]": Math.round(record.total_hectares_restored_sum), + "[restoration_eco_region]": restorationEcoregionText + }, + restorationByStrategy: { + "[organization_name]": record?.organisation?.name, + "[date_run]": dateRunIndicator, + "[x_ha]": Math.round(sumRestorationByValues(strategiesData, false)), + "[x_%]": calculatePercentage(sumRestorationByValues(strategiesData, false), record.total_hectares_restored_sum), + "[x_ha_goal]": Math.round(record.total_hectares_restored_sum), + "[x_1a]": getOrderTop3(strategiesData)?.[0]?.label ?? "N/A", + "[x_1b]": Math.round(getOrderTop3(strategiesData)?.[0]?.value) ?? "N/A", + "[other_restoration_strategies]": formatDescriptionIndicator( + valuesItemsRestorationBy, + totalHectaresRestoredGoal, + false, + "The other restoration strategies used include" + ) + }, + restorationByLandUse: { + "[organization_name]": record?.organisation?.name, + "[date_run]": dateRunIndicator, + "[x_ha]": Math.round(sumRestorationByValues(landUseData.graphicTargetLandUseTypes, true)), + "[x_%]": calculatePercentage( + sumRestorationByValues(landUseData.graphicTargetLandUseTypes, true), + record.total_hectares_restored_sum + ), + "[x_ha_goal]": Math.round(record.total_hectares_restored_sum), + "[x_1a]": getOrderTop3(landUseData.graphicTargetLandUseTypes)?.[0]?.label ?? "N/A", + "[x_1b]": + Math.round( + parseInt( + getOrderTop3(landUseData.graphicTargetLandUseTypes)?.[0] + ?.valueText?.match(/^(.*?)ha/)[1] + .trim(), + 10 + ) + ) ?? "N/A", + "[other_target_land_use]": formatDescriptionIndicator( + valuesItemsLandUse, + record.total_hectares_restored_sum, + false, + "The other target land use systems used include" + ) + } + }; + return ( <>
@@ -665,10 +876,10 @@ const DataCard = ({
- {indicatorDescription1} - - - {indicatorDescription2} + {replaceTextWithParams( + monitoredDescriptionParams[indicatorSlug!], + DROPDOWN_OPTIONS.find(item => item.slug === indicatorSlug)?.description! + )}
diff --git a/src/utils/MonitoredIndicatorUtils.ts b/src/utils/MonitoredIndicatorUtils.ts new file mode 100644 index 000000000..6a0fa4d56 --- /dev/null +++ b/src/utils/MonitoredIndicatorUtils.ts @@ -0,0 +1,47 @@ +export const replaceTextWithParams = (params: Record, text: string): string => { + return Object.entries(params).reduce((result, [key, value]) => { + const escapedKey = key.replace(/([.*+?^=!:${}()|[\]/\\])/g, "\\$1"); + return result.replace(new RegExp(escapedKey, "g"), value?.toString() || ""); + }, text); +}; + +export const getOrderTop3 = (data: any[]) => { + return data.sort((a, b) => b.value - a.value).slice(0, 3); +}; + +export const getKeyValue = (data: { [key: string]: number }) => { + if (data) { + const name = Object?.keys(data!)?.[0]; + const value = data[name]; + return { name: name, value: value }; + } +}; + +export const calculatePercentage = (value: number, total: number): number => { + if (!total) return 0; + return Math.round((Math.round(value) / Math.round(total)) * 100); +}; + +export const formatDescriptionIndicator = ( + items: { [key: string]: number | undefined }, + totalHectares: number, + percentage?: boolean, + baseText?: string +) => { + const validItems = Object.entries(items) + .filter(([key, value]) => value != undefined && value != null && !Number.isNaN(value)) + .map( + ([key, value]) => + `${key} with ${value} ha ${percentage ? `(${calculatePercentage(value!, totalHectares)}%)` : ""}` + ); + + if (validItems.length == 0) return ""; + + const formattedItems = + validItems.length == 1 + ? validItems[0] + : validItems.slice(0, -1).join(", ") + " and " + validItems[validItems.length - 1]; + + if (baseText) return `${baseText} ${formattedItems}`; + return formattedItems; +}; From b4df835d35cc353fbb74c6959a505cb70e21faa9 Mon Sep 17 00:00:00 2001 From: Limber Mamani Vallejos Date: Mon, 20 Jan 2025 13:46:30 -0400 Subject: [PATCH 2/3] [TM-1575] change total values --- .../MonitoredTab/components/DataCard.tsx | 14 +++++++------- src/utils/MonitoredIndicatorUtils.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx b/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx index 65bbb4193..5dadb8df8 100644 --- a/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx +++ b/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx @@ -721,12 +721,12 @@ const DataCard = ({ ecoRegionData.chartData.length } major ecoregion(s): ${calculatePercentage( ecoRegionData.chartData[0].value, - record.total_hectares_restored_sum + ecoRegionData.total )}% of the project takes place in the ${ecoRegionData.chartData[0].name} ecoregion ${ ecoRegionData.chartData?.[1] ? `[and ${calculatePercentage( ecoRegionData.chartData[1].value, - record.total_hectares_restored_sum + ecoRegionData.total )}% of the project takes place in the ${ecoRegionData.chartData[1].name} ecoregion].` : "." }` @@ -760,9 +760,9 @@ const DataCard = ({ restorationByEcoRegion: { "[organization_name]": record?.organisation?.name, "[date_run]": dateRunIndicator, - "[x_ha]": Math.round(ecoRegionData.total), - "[x_%]": calculatePercentage(ecoRegionData.total, record.total_hectares_restored_sum), - "[x_ha_goal]": Math.round(record.total_hectares_restored_sum), + "[x_ha]": Math.round(sumRestorationByValues(ecoRegionData?.chartData, false)), + "[x_%]": calculatePercentage(sumRestorationByValues(ecoRegionData?.chartData, false), ecoRegionData.total), + "[x_ha_goal]": Math.round(ecoRegionData.total), "[restoration_eco_region]": restorationEcoregionText }, restorationByStrategy: { @@ -786,9 +786,9 @@ const DataCard = ({ "[x_ha]": Math.round(sumRestorationByValues(landUseData.graphicTargetLandUseTypes, true)), "[x_%]": calculatePercentage( sumRestorationByValues(landUseData.graphicTargetLandUseTypes, true), - record.total_hectares_restored_sum + landUseData.totalSection.totalHectaresRestored ), - "[x_ha_goal]": Math.round(record.total_hectares_restored_sum), + "[x_ha_goal]": Math.round(landUseData.totalSection.totalHectaresRestored), "[x_1a]": getOrderTop3(landUseData.graphicTargetLandUseTypes)?.[0]?.label ?? "N/A", "[x_1b]": Math.round( diff --git a/src/utils/MonitoredIndicatorUtils.ts b/src/utils/MonitoredIndicatorUtils.ts index 6a0fa4d56..34f494eba 100644 --- a/src/utils/MonitoredIndicatorUtils.ts +++ b/src/utils/MonitoredIndicatorUtils.ts @@ -19,7 +19,7 @@ export const getKeyValue = (data: { [key: string]: number }) => { export const calculatePercentage = (value: number, total: number): number => { if (!total) return 0; - return Math.round((Math.round(value) / Math.round(total)) * 100); + return Math.round((value / total) * 100); }; export const formatDescriptionIndicator = ( From 1172083e18cac95054c295b9ac2e387eae99a171 Mon Sep 17 00:00:00 2001 From: Limber Mamani Vallejos Date: Mon, 20 Jan 2025 14:00:00 -0400 Subject: [PATCH 3/3] [TM-1575] change total value to land use --- .../ResourceTabs/MonitoredTab/components/DataCard.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx b/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx index 5dadb8df8..dcfe408e6 100644 --- a/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx +++ b/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx @@ -786,10 +786,10 @@ const DataCard = ({ "[x_ha]": Math.round(sumRestorationByValues(landUseData.graphicTargetLandUseTypes, true)), "[x_%]": calculatePercentage( sumRestorationByValues(landUseData.graphicTargetLandUseTypes, true), - landUseData.totalSection.totalHectaresRestored + totalHectaresRestoredGoal ), - "[x_ha_goal]": Math.round(landUseData.totalSection.totalHectaresRestored), - "[x_1a]": getOrderTop3(landUseData.graphicTargetLandUseTypes)?.[0]?.label ?? "N/A", + "[x_ha_goal]": Math.round(totalHectaresRestoredGoal), + "[x_1a]": getOrderTop3(landUseData.graphicTargetLandUseTypes)?.[0]?.label ?? "NaN", "[x_1b]": Math.round( parseInt( @@ -798,10 +798,10 @@ const DataCard = ({ .trim(), 10 ) - ) ?? "N/A", + ) ?? "NaN", "[other_target_land_use]": formatDescriptionIndicator( valuesItemsLandUse, - record.total_hectares_restored_sum, + totalHectaresRestoredGoal, false, "The other target land use systems used include" )