Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FLAG-1155: natural forest widget #4874

Merged
merged 6 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading