Skip to content

Commit

Permalink
feat(natural-forest): add new natural forest widget
Browse files Browse the repository at this point in the history
  • Loading branch information
wri7tno committed Nov 10, 2024
1 parent f435a8c commit dc95e82
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 2 deletions.
111 changes: 111 additions & 0 deletions components/widgets/land-cover/natural-forest/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { getNaturalForest } from 'services/analysis-cached';
import { NATURAL_FOREST } from 'data/datasets';
import { NATURAL_FOREST_2020 } from 'data/layers';

import getWidgetProps from './selectors';

export default {
widget: 'naturalForest',
title: {
default: 'Natural forest in {location}',
global: 'Global natural forest',
},
sentence: {
default: {
global: `As of 2020, {naturalForestPercentage} of <b>global</b> land cover was natural forests and {nonNaturalForestPercentage} was non-natural tree cover.`,
region: `As of 2020, {naturalForestPercentage} of land cover in {location} was natural forests and {nonNaturalForestPercentage} was non-natural tree cover.`,
},
withIndicator: {
global: `As of 2020, {naturalForestPercentage} of <b>global</b> land cover in {indicator} was natural forests and {nonNaturalForestPercentage} was non-natural tree cover.`,
region: `As of 2020, {naturalForestPercentage} of land cover in {indicator} in {location} was natural forests and {nonNaturalForestPercentage} was non-natural tree cover.`,
},
},
metaKey: {
2000: 'sbtn_natural_forests_map',
2010: 'sbtn_natural_forests_map',
2020: 'sbtn_natural_forests_map',
},
chartType: 'pieChart',
large: false,
colors: 'extent',
source: 'gadm',
categories: ['land-cover', 'summary'],
types: ['global', 'country', 'geostore', 'aoi', 'wdpa', 'use'],
admins: ['global', 'adm0', 'adm1', 'adm2'],
visible: ['dashboard'],
datasets: [
{
dataset: NATURAL_FOREST,
layers: [NATURAL_FOREST_2020],
boundary: true,
},
],
dataType: 'naturalForest',
sortOrder: {
summary: 6,
landCover: 1,
},
refetchKeys: ['threshold', 'decile', 'extentYear', 'landCategory'],
pendingKeys: ['threshold', 'decile', 'extentYear'],
settings: {
threshold: 30,
decile: 30,
extentYear: 2000,
},
getSettingsConfig: () => {
return [
{
key: 'landCategory',
label: 'Land Category',
type: 'select',
placeholder: 'All categories',
clearable: true,
border: true,
},
];
},
getData: (params) => {
const { threshold, decile, ...filteredParams } = params;

return getNaturalForest({ ...filteredParams }).then((response) => {
const extent = response.data;

let totalNaturalForest = 0;
let totalNonNaturalTreeCover = 0;
let unknown = 0;

let data = {};
if (extent && extent.length) {
// Sum values
extent.forEach((item) => {
switch (item.sbtn_natural_forests__class) {
case 'Natural Forest':
totalNaturalForest += item.area__ha;
break;
case 'Non-Natural Forest':
totalNonNaturalTreeCover += item.area__ha;
break;
default:
// 'Unknown'
unknown += item.area__ha;
}
});

data = {
totalNaturalForest,
unknown,
totalNonNaturalTreeCover,
totalArea: totalNaturalForest + unknown + totalNonNaturalTreeCover,
};
}

return data;
});
},
getDataURL: async (params) => {
const response = await getNaturalForest({ ...params, download: true });

return [response];
},
getWidgetProps,
};
126 changes: 126 additions & 0 deletions components/widgets/land-cover/natural-forest/selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { createSelector, createStructuredSelector } from 'reselect';
import isEmpty from 'lodash/isEmpty';
import { formatNumber } from 'utils/format';

const getData = (state) => state.data;
const getSettings = (state) => state.settings;
const getIndicator = (state) => state.indicator;
const getWhitelist = (state) => state.polynamesWhitelist;
const getSentence = (state) => state.sentence;
const getTitle = (state) => state.title;
const getLocationName = (state) => state.locationLabel;
const getMetaKey = (state) => state.metaKey;
const getAdminLevel = (state) => state.adminLevel;

export const isoHasPlantations = createSelector(
[getWhitelist, getLocationName],
(whitelist, name) => {
const hasPlantations =
name === 'global'
? true
: whitelist &&
whitelist.annual &&
whitelist.annual.includes('plantations');
return hasPlantations;
}
);

export const parseData = createSelector([getData], (data) => {
if (isEmpty(data)) {
return null;
}

const { totalNaturalForest, unknown, totalNonNaturalTreeCover, totalArea } =
data;
const parsedData = [
{
label: 'Natural forests',
value: totalNaturalForest,
color: '#2C6639',
percentage: (totalNaturalForest / totalArea) * 100,
},
{
label: 'Non-natural tree cover',
value: totalNonNaturalTreeCover,
color: '#A8DDB5',
percentage: (totalNonNaturalTreeCover / totalArea) * 100,
},
{
label: 'Other land cover',
value: unknown,
color: '#D3D3D3',
percentage: (unknown / totalArea) * 100,
},
];

return parsedData;
});

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

export const parseSentence = createSelector(
[
getData,
getSettings,
getLocationName,
getIndicator,
getSentence,
getAdminLevel,
],
(data, settings, locationName, indicator, sentences, admLevel) => {
if (!data || !sentences) return null;

const { extentYear, threshold, decile } = settings;

const isTropicalTreeCover = extentYear === 2020;
const decileThreshold = isTropicalTreeCover ? decile : threshold;
const withIndicator = !!indicator;
const sentenceKey = withIndicator ? 'withIndicator' : 'default';
const sentenceSubkey = admLevel === 'global' ? 'global' : 'region';
const sentence = sentences[sentenceKey][sentenceSubkey];

const { totalNaturalForest, totalNonNaturalTreeCover, totalArea } = data;
const percentNaturalForest = (100 * totalNaturalForest) / totalArea;
const percentNonNaturalForest =
(100 * totalNonNaturalTreeCover) / totalArea;

const formattedNaturalForestPercentage = formatNumber({
num: percentNaturalForest,
unit: '%',
});
const formattedNonNaturalForestPercentage = formatNumber({
num: percentNonNaturalForest,
unit: '%',
});

const thresholdLabel = `>${decileThreshold}%`;

const params = {
year: extentYear,
location: locationName,
naturalForestPercentage: formattedNaturalForestPercentage,
nonNaturalForestPercentage: formattedNonNaturalForestPercentage,
indicator: indicator?.label,
threshold: thresholdLabel,
};

return { sentence, params };
}
);

export const parseMetaKey = createSelector(
[getMetaKey, getSettings],
(metaKey, settings) => metaKey[settings.extentYear]
);

export default createStructuredSelector({
data: parseData,
sentence: parseSentence,
title: parseTitle,
metaKey: parseMetaKey,
});
2 changes: 1 addition & 1 deletion components/widgets/land-cover/tree-cover-ranked/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default {
],
sortOrder: {
summary: 1,
landCover: 1,
landCover: 1.1,
},
refetchKeys: ['threshold', 'extentYear', 'forestType', 'landCategory'],
settings: {
Expand Down
2 changes: 1 addition & 1 deletion components/widgets/land-cover/tree-cover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default {
],
sortOrder: {
summary: 4,
landCover: 1,
landCover: 1.5,
},
refetchKeys: ['threshold', 'decile', 'extentYear', 'landCategory'],
pendingKeys: ['threshold', 'decile', 'extentYear'],
Expand Down
2 changes: 2 additions & 0 deletions components/widgets/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import treeCoverLocated from 'components/widgets/land-cover/tree-cover-located';
import USLandCover from 'components/widgets/land-cover/us-land-cover';
import rankedForestTypes from 'components/widgets/land-cover/ranked-forest-types';
import treeCoverDensity from 'components/widgets/land-cover/tree-cover-density';
import naturalForest from 'components/widgets/land-cover/natural-forest';

// Climate
import woodyBiomass from 'components/widgets/climate/whrc-biomass/';
Expand Down Expand Up @@ -103,6 +104,7 @@ export default {
treeCoverLocated,
rankedForestTypes,
treeCoverDensity,
naturalForest,

// climate
// emissions,
Expand Down
4 changes: 4 additions & 0 deletions components/widgets/utils/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,10 @@ export const getStatements = ({
...(indicatorStatements || []),
]);

if (dataType === 'naturalForest') {
return [];
}

return statements;
};

Expand Down
1 change: 1 addition & 0 deletions data/datasets.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ export const PRIMARY_FOREST_DATASET = 'primary-forests';
export const MANGROVE_FORESTS_DATASET = 'mangrove-forests';
export const GFW_STORIES_DATASET = 'mongabay-stories';
export const TROPICAL_TREE_COVER_DATASET = 'tropical-tree-cover';
export const NATURAL_FOREST = 'natural-forests';
1 change: 1 addition & 0 deletions data/layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@ export const PRIMARY_FOREST = 'primary-forests-2001';
export const MANGROVE_FORESTS = 'mangrove-forests-1996';
export const TROPICAL_TREE_COVER_HECTARE = 'tropical-tree-cover-hectare';
export const TROPICAL_TREE_COVER_METERS = 'tropical-tree-cover-meters';
export const NATURAL_FOREST_2020 = 'natural-forests-2020';
33 changes: 33 additions & 0 deletions services/analysis-cached.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ const SQL_QUERIES = {
treeCoverOTFExtent: 'SELECT SUM(area__ha) FROM data&geostore_id={geostoreId}',
treeCoverGainSimpleOTF:
'SELECT SUM(area__ha) FROM data&geostore_id={geostoreId}',
naturalForest:
'SELECT {location}, sbtn_natural_forests__class, SUM(area__ha) AS area__ha FROM data {WHERE} GROUP BY {location}, sbtn_natural_forests__class',
netChangeIso:
'SELECT {select_location}, stable, loss, gain, disturb, net, change, gfw_area__ha FROM data {WHERE}',
netChange:
Expand Down Expand Up @@ -1022,6 +1024,37 @@ export const getTropicalExtentGrouped = (params) => {
}));
};

export const getNaturalForest = async (params) => {
const { download } = params || {};

const requestUrl = getRequestUrl({
...params,
dataset: 'annual',
datasetType: 'summary',
version: 'v20240815',
});

if (!requestUrl) {
return new Promise(() => {});
}

const url = encodeURI(
`${requestUrl}${SQL_QUERIES.naturalForest}`
.replace(/{location}/g, getLocationSelect({ ...params, cast: false }))
.replace(/{location}/g, getLocationSelect({ ...params }))
.replace('{WHERE}', getWHEREQuery({ ...params, dataset: 'annual' }))
);

if (download) {
return {
name: `natural_forest_2020__ha`,
url: getDownloadUrl(url),
};
}

return dataRequest.get(url);
};

export const getTreeCoverByLandCoverClass = (params) => {
const { forestType, download, extentYear, landCategory, ifl } = params || {};

Expand Down

0 comments on commit dc95e82

Please sign in to comment.