Skip to content

Commit

Permalink
Merge pull request #4911 from wri/develop
Browse files Browse the repository at this point in the history
PROD Deploy 2025-01-09 11:55am EST
  • Loading branch information
wri7tno authored Jan 9, 2025
2 parents cb73d00 + a5403ac commit 6c333d6
Show file tree
Hide file tree
Showing 15 changed files with 671 additions and 98 deletions.
7 changes: 6 additions & 1 deletion components/analysis/components/show-analysis/component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ class ShowAnalysis extends PureComponent {
} = this.props;
const hasWidgets = widgetLayers && !!widgetLayers.length;

// NOTE: this is a horrible code smell but it was the only workaround
// I was able to find to avoid showing the Natural Forest data without widget in the map.
const filteredData = data?.filter((d) => d.label !== 'Natural forests');

return (
<div className="c-show-analysis">
<div className="show-analysis-body">
Expand Down Expand Up @@ -208,7 +212,8 @@ class ShowAnalysis extends PureComponent {
{(hasLayers || hasWidgets) && !loading && !error && (
<Fragment>
<ul className="draw-stats">
{data && data.map((d) => this.renderStatItem(d))}
{filteredData &&
filteredData.map((d) => this.renderStatItem(d))}
</ul>
<Widgets simple analysis />
<div className="disclaimers">
Expand Down
70 changes: 33 additions & 37 deletions components/widgets/forest-change/tree-loss-plantations/index.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import { all, spread } from 'axios';
import { getLoss } from 'services/analysis-cached';
import { getLossNaturalForest } from 'services/analysis-cached';
import { getYearsRangeFromMinMax } from 'components/widgets/utils/data';

import {
POLITICAL_BOUNDARIES_DATASET,
FOREST_LOSS_DATASET,
TREE_PLANTATIONS_DATASET,
NATURAL_FOREST,
} from 'data/datasets';
import {
DISPUTED_POLITICAL_BOUNDARIES,
POLITICAL_BOUNDARIES,
FOREST_LOSS,
TREE_PLANTATIONS,
NATURAL_FOREST_2020,
} from 'data/layers';

import getWidgetProps from './selectors';

const MIN_YEAR = 2013;
const MIN_YEAR = 2021;
const MAX_YEAR = 2023;

export default {
widget: 'treeLossPlantations',
title: 'Forest loss in natural forest in {location}',
title: {
default: 'Forest loss in natural forest in {location}',
global: 'Forest loss in natural forest',
},
large: true,
categories: ['forest-change'],
subcategories: ['forest-loss'],
types: ['country', 'aoi', 'wdpa'],
admins: ['adm0', 'adm1', 'adm2'],
types: ['global', 'country', 'aoi', 'wdpa'],
admins: ['global', 'adm0', 'adm1', 'adm2'],
alerts: [
{
text: 'Not all natural forest area can be monitored with existing data on tree cover loss. See the metadata for more information.',
visible: ['global', 'country', 'geostore', 'aoi', 'wdpa', 'use'],
},
],
settingsConfig: [
{
key: 'years',
Expand All @@ -36,12 +45,6 @@ export default {
type: 'range-select',
border: true,
},
{
key: 'threshold',
label: 'canopy density',
type: 'mini-select',
metaKey: 'widget_canopy_density',
},
],
refetchKeys: ['threshold'],
chartType: 'composedChart',
Expand All @@ -53,25 +56,27 @@ export default {
layers: [DISPUTED_POLITICAL_BOUNDARIES, POLITICAL_BOUNDARIES],
boundary: true,
},
// natural forest
{
// global plantations
dataset: TREE_PLANTATIONS_DATASET,
layers: [TREE_PLANTATIONS],
dataset: NATURAL_FOREST,
layers: [NATURAL_FOREST_2020],
boundary: true,
},
// loss
{
dataset: FOREST_LOSS_DATASET,
layers: [FOREST_LOSS],
},
],
dataType: 'naturalForest',
sortOrder: {
forestChange: 2,
},
sentence:
'From {startYear} to {endYear}, {percentage} of tree cover loss in {location} occurred within {lossPhrase}. The total loss within natural forest was equivalent to {value} of CO\u2082e emissions.',
whitelists: {
indicators: ['plantations'],
checkStatus: true,
sentence: {
global:
'From {startYear} to {endYear}, {percentage} of tree cover loss <b>globally</b> occurred within {lossPhrase}. The total loss within natural forest was {totalLoss}, equivalent to {value} of CO\u2082e emissions.',
region:
'From {startYear} to {endYear}, {percentage} of tree cover loss in {location} occurred within {lossPhrase}. The total loss within natural forest was {totalLoss}, equivalent to {value} of CO\u2082e emissions.',
},
settings: {
threshold: 30,
Expand All @@ -80,23 +85,12 @@ export default {
extentYear: 2010,
},
getData: (params) =>
all([
getLoss({ ...params, forestType: 'plantations' }),
getLoss({ ...params, forestType: '' }),
]).then(
spread((plantationsloss, gadmLoss) => {
all([getLossNaturalForest(params)]).then(
spread((gadmLoss) => {
let data = {};
const lossPlantations =
plantationsloss.data && plantationsloss.data.data;
const totalLoss = gadmLoss.data && gadmLoss.data.data;
if (
lossPlantations &&
totalLoss &&
lossPlantations.length &&
totalLoss.length
) {
if (totalLoss && totalLoss.length) {
data = {
lossPlantations,
totalLoss,
};
}
Expand All @@ -118,8 +112,10 @@ export default {
})
),
getDataURL: (params) => [
getLoss({ ...params, forestType: 'plantations', download: true }),
getLoss({ ...params, forestType: '', download: true }),
getLossNaturalForest({
...params,
download: true,
}),
],
getWidgetProps,
};
111 changes: 67 additions & 44 deletions components/widgets/forest-change/tree-loss-plantations/selectors.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { createSelector, createStructuredSelector } from 'reselect';
import sumBy from 'lodash/sumBy';
import groupBy from 'lodash/groupBy';
import uniqBy from 'lodash/uniqBy';
import { formatNumber } from 'utils/format';
import { getColorPalette } from 'components/widgets/utils/colors';
import { zeroFillYears } from 'components/widgets/utils/data';
import { zeroFillYearsFilter } from 'components/widgets/utils/data';

// get list data
const getLossPlantations = (state) => state.data && state.data.lossPlantations;
const getTotalLoss = (state) => state.data && state.data.totalLoss;
const getSettings = (state) => state.settings;
const getLocationName = (state) => state.locationLabel;
const getTitle = (state) => state.title;
const getColors = (state) => state.colors;
const getSentence = (state) => state.sentence;
const getAdminLevel = (state) => state.adminLevel;

// get lists selected
export const parseData = createSelector(
[getLossPlantations, getTotalLoss, getSettings],
(lossPlantations, totalLoss, settings) => {
if (!lossPlantations || !totalLoss) return null;
[getTotalLoss, getSettings],
(totalLoss, settings) => {
if (!totalLoss) return null;
const { startYear, endYear, yearsRange } = settings;
const years = yearsRange && yearsRange.map((yearObj) => yearObj.value);
const fillObj = {
Expand All @@ -28,44 +28,68 @@ export const parseData = createSelector(
emissions: 0,
percentage: 0,
};
const zeroFilledData = zeroFillYears(
lossPlantations,
const zeroFilledData = zeroFillYearsFilter(
totalLoss,
startYear,
endYear,
years,
fillObj
);
const totalLossByYear = groupBy(totalLoss, 'year');
const parsedData = uniqBy(
zeroFilledData
.filter((d) => d.year >= startYear && d.year <= endYear)
.map((d) => {
const groupedPlantations = groupBy(lossPlantations, 'year')[d.year];
const summedPlatationsLoss =
(groupedPlantations && sumBy(groupedPlantations, 'area')) || 0;
const summedPlatationsEmissions =
(groupedPlantations && sumBy(groupedPlantations, 'emissions')) || 0;
const totalLossForYear =
(totalLossByYear[d.year] && totalLossByYear[d.year][0]) || {};

const returnData = {
...d,
outsideAreaLoss: totalLossForYear.area - summedPlatationsLoss,
areaLoss: summedPlatationsLoss || 0,
totalLoss: totalLossForYear.area || 0,
outsideCo2Loss:
totalLossByYear[d.year]?.[0]?.emissions -
summedPlatationsEmissions,
co2Loss: summedPlatationsEmissions || 0,
};
return returnData;
}),
'year'
);
const mappedData = zeroFilledData.map((list) => {
const naturalForestList = list.filter(
(item) => item.sbtn_natural_forests__class === 'Natural Forest'
);

const nonNaturalForestList = list.filter(
(item) => item.sbtn_natural_forests__class === 'Non-Natural Forest'
);
// eslint-disable-next-line no-unused-vars
const unknownList = list.filter(
(item) => item.sbtn_natural_forests__class === 'Unknown'
);

const naturalForestArea = naturalForestList?.reduce(
(acc, curr) => acc + curr.area,
0
);
const naturalForestEmissions = naturalForestList?.reduce(
(acc, curr) => acc + curr.emissions,
0
);
const nonNaturalForestArea = nonNaturalForestList?.reduce(
(acc, curr) => acc + curr.area,
0
);
const nonNaturalForestEmissions = nonNaturalForestList?.reduce(
(acc, curr) => acc + curr.emissions,
0
);

return {
iso: nonNaturalForestList[0]?.iso || '',
outsideAreaLoss: naturalForestArea || 0,
outsideCo2Loss: naturalForestEmissions || 0,
areaLoss: nonNaturalForestArea || 0,
co2Loss: nonNaturalForestEmissions || 0,
totalLoss: (nonNaturalForestArea || 0) + (naturalForestArea || 0),
year: nonNaturalForestList[0]?.year || '',
};
});

const parsedData = uniqBy(mappedData, 'year');

return parsedData;
}
);

export const parseTitle = createSelector(
[getTitle, getLocationName],
(title, name) => {
return name === 'global' ? title.global : title.default;
}
);

export const parseConfig = createSelector([getColors], (colors) => {
const colorRange = getColorPalette(colors.ramp, 2);
return {
Expand Down Expand Up @@ -103,7 +127,7 @@ export const parseConfig = createSelector([getColors], (colors) => {
},
{
key: 'areaLoss',
label: 'Plantations',
label: 'Non-natural tree cover',
color: colorRange[0],
unitFormat: (value) =>
formatNumber({ num: value, unit: 'ha', spaceUnit: true }),
Expand All @@ -113,21 +137,18 @@ export const parseConfig = createSelector([getColors], (colors) => {
});

export const parseSentence = createSelector(
[parseData, getSettings, getLocationName, getSentence],
(data, settings, locationName, sentence) => {
[parseData, getSettings, getLocationName, getSentence, getAdminLevel],
(data, settings, locationName, sentences, admLevel) => {
if (!data) return null;
const { startYear, endYear } = settings;
const plantationsLoss = sumBy(data, 'areaLoss') || 0;
const totalLoss = sumBy(data, 'totalLoss') || 0;
const outsideLoss = sumBy(data, 'outsideAreaLoss') || 0;
const outsideEmissions = sumBy(data, 'outsideCo2Loss') || 0;
const sentenceSubkey = admLevel === 'global' ? 'global' : 'region';
const sentence = sentences[sentenceSubkey];

const lossPhrase =
plantationsLoss > outsideLoss ? 'plantations' : 'natural forest';
const percentage =
plantationsLoss > outsideLoss
? (100 * plantationsLoss) / totalLoss
: (100 * outsideLoss) / totalLoss;
const lossPhrase = 'natural forest';
const percentage = (100 * outsideLoss) / totalLoss;
const params = {
location: locationName,
startYear,
Expand All @@ -139,6 +160,7 @@ export const parseSentence = createSelector(
spaceUnit: true,
}),
percentage: formatNumber({ num: percentage, unit: '%' }),
totalLoss: formatNumber({ num: outsideLoss, unit: 'ha' }), // using outsideLoss (natural forest) value based on Michelle's feedback
};

return {
Expand All @@ -152,4 +174,5 @@ export default createStructuredSelector({
data: parseData,
config: parseConfig,
sentence: parseSentence,
title: parseTitle,
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const getSummedByYearsData = createSelector(
const mappedData = regions.map((region) => {
const isoLoss = Math.round(sumBy(groupedByRegion[region], 'loss')) || 0;
const regionExtent = extent.find((e) => {
return e[regionKey].toString() === region.toString(); // iso is string while adm1 and 2 are numbers
return e[regionKey]?.toString() === region.toString(); // iso is string while adm1 and 2 are numbers
});
const isoExtent = (regionExtent && regionExtent.extent) || 0;
const percentageLoss =
Expand Down
Loading

0 comments on commit 6c333d6

Please sign in to comment.