diff --git a/components/widgets/land-cover/natural-forest/index.js b/components/widgets/land-cover/natural-forest/index.js
new file mode 100644
index 0000000000..dcf21e60f7
--- /dev/null
+++ b/components/widgets/land-cover/natural-forest/index.js
@@ -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 global 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 global 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,
+};
diff --git a/components/widgets/land-cover/natural-forest/selectors.js b/components/widgets/land-cover/natural-forest/selectors.js
new file mode 100644
index 0000000000..a64ed8a599
--- /dev/null
+++ b/components/widgets/land-cover/natural-forest/selectors.js
@@ -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,
+});
diff --git a/components/widgets/land-cover/tree-cover-ranked/index.js b/components/widgets/land-cover/tree-cover-ranked/index.js
index abecb46cc8..3ac9b4bcaa 100644
--- a/components/widgets/land-cover/tree-cover-ranked/index.js
+++ b/components/widgets/land-cover/tree-cover-ranked/index.js
@@ -76,7 +76,7 @@ export default {
],
sortOrder: {
summary: 1,
- landCover: 1,
+ landCover: 1.1,
},
refetchKeys: ['threshold', 'extentYear', 'forestType', 'landCategory'],
settings: {
diff --git a/components/widgets/land-cover/tree-cover/index.js b/components/widgets/land-cover/tree-cover/index.js
index ad5415a613..5adb358259 100644
--- a/components/widgets/land-cover/tree-cover/index.js
+++ b/components/widgets/land-cover/tree-cover/index.js
@@ -107,7 +107,7 @@ export default {
],
sortOrder: {
summary: 4,
- landCover: 1,
+ landCover: 1.5,
},
refetchKeys: ['threshold', 'decile', 'extentYear', 'landCategory'],
pendingKeys: ['threshold', 'decile', 'extentYear'],
diff --git a/components/widgets/manifest.js b/components/widgets/manifest.js
index 7f257d94de..fe8b8c377b 100644
--- a/components/widgets/manifest.js
+++ b/components/widgets/manifest.js
@@ -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/';
@@ -103,6 +104,7 @@ export default {
treeCoverLocated,
rankedForestTypes,
treeCoverDensity,
+ naturalForest,
// climate
// emissions,
diff --git a/components/widgets/utils/config.js b/components/widgets/utils/config.js
index 9481fd6a56..9d5d4ea3b3 100644
--- a/components/widgets/utils/config.js
+++ b/components/widgets/utils/config.js
@@ -440,6 +440,10 @@ export const getStatements = ({
...(indicatorStatements || []),
]);
+ if (dataType === 'naturalForest') {
+ return [];
+ }
+
return statements;
};
diff --git a/data/datasets.js b/data/datasets.js
index ca9eea2d3d..a6db0e9fe5 100644
--- a/data/datasets.js
+++ b/data/datasets.js
@@ -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';
diff --git a/data/layers.js b/data/layers.js
index 28b73186fb..57a45c8811 100644
--- a/data/layers.js
+++ b/data/layers.js
@@ -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';
diff --git a/services/analysis-cached.js b/services/analysis-cached.js
index 39d76fc1f4..ad6c353114 100644
--- a/services/analysis-cached.js
+++ b/services/analysis-cached.js
@@ -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:
@@ -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 || {};