From 27ab96cc5b46c6afe16435bfe106d7b519643a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Fri, 2 May 2025 10:58:05 +0200 Subject: [PATCH] feat: dynamic world data --- .../explore/landcover/Landcover.jsx | 52 +++++- .../explore/landcover/charts/dynamicWorld.js | 160 ++++++++++++++++++ src/hooks/useEarthEngineTimeSeries.js | 4 + src/utils/ee-utils.js | 43 +++-- 4 files changed, 247 insertions(+), 12 deletions(-) create mode 100644 src/components/explore/landcover/charts/dynamicWorld.js diff --git a/src/components/explore/landcover/Landcover.jsx b/src/components/explore/landcover/Landcover.jsx index d57ac182..c4731503 100644 --- a/src/components/explore/landcover/Landcover.jsx +++ b/src/components/explore/landcover/Landcover.jsx @@ -4,6 +4,7 @@ import DataLoader from '../../shared/DataLoader.jsx' import Chart from '../Chart.jsx' import getChartConfig from './charts/landcover.js' import getAllConfig from './charts/landcoverAll.js' +import getDynamicWorldConfig from './charts/dynamicWorld.js' import LandcoverSelect from './LandcoverSelect.jsx' const dataset = { @@ -17,16 +18,50 @@ const period = { endTime: '2024', } +const dynamicWorld = { + datasetId: 'GOOGLE/DYNAMICWORLD/V1', + band: [ + 'water', + 'trees', + 'grass', + 'flooded_vegetation', + 'crops', + 'shrub_and_scrub', + 'built', + 'bare', + 'snow_and_ice', + ], // 'label' + // reducer: 'frequencyHistogram', + // reducer: 'count', + // aggregationPeriod: MONTHLY, +} + const LandCover = () => { const orgUnit = exploreStore((state) => state.orgUnit) const type = exploreStore((state) => state.landcoverType) - const data = useEarthEngineTimeSeries(dataset, period, orgUnit) + // const dynamicWorldPeriod = exploreStore((state) => state.monthlyPeriod) + const dynamicWorldPeriod = { + startTime: '2024-01', + endTime: '2024-06', + } + + // const data = useEarthEngineTimeSeries(dataset, period, orgUnit) + let data - if (!data) { + const dynamicWorldData = useEarthEngineTimeSeries({ + dataset: dynamicWorld, + period: dynamicWorldPeriod, + feature: orgUnit, + }) + + // console.log('dynamicWorldData', dynamicWorldData, dynamicWorldPeriod) + + if (!data && !dynamicWorldData) { return } + /* return ( <> @@ -36,6 +71,19 @@ const LandCover = () => { /> ) + */ + + return ( + <> + + + ) } export default LandCover diff --git a/src/components/explore/landcover/charts/dynamicWorld.js b/src/components/explore/landcover/charts/dynamicWorld.js new file mode 100644 index 00000000..e4a5a164 --- /dev/null +++ b/src/components/explore/landcover/charts/dynamicWorld.js @@ -0,0 +1,160 @@ +import i18n from '@dhis2/d2-i18n' +import { colors } from '@dhis2/ui' +import { roundOneDecimal } from '../../../../utils/calc.js' +import { animation, landCoverCredits } from '../../../../utils/chart.js' +import { landcoverTypes } from '../LandcoverSelect.jsx' + +const band = 'LC_Type1' + +const getChartConfig = (name, data, bands) => { + console.log(name, data, bands) + + // const band = 'trees' + + const filteredData = data.filter((d) => d['trees'] !== undefined) + + const series = bands.map((band) => ({ + type: 'line', + data: filteredData.map((d) => ({ + x: d.startTime, + y: d[band], + })), + name: band, + color: colors.green600, + lineWidth: 3, + zIndex: 2, + })) + + return { + /* + title: { + text: i18n.t('{{name}}: {{band}} vegetation index {{period}}', { + name, + band, + period: getDailyPeriod(data), + nsSeparator: ';', + }), + }, + subtitle: isFacility && { + text: i18n.t( + 'Value is only for 250 x 250 m where the facility is located' + ), + }, + */ + // credits: vegetationCredits, + tooltip: { + crosshairs: true, + shared: true, + valueSuffix: '°C', + }, + xAxis: { + type: 'datetime', + tickInterval: 2592000000, + labels: { + format: '{value: %b}', + }, + }, + yAxis: { + title: false, + min: 0, + max: 1, + }, + chart: { + height: 480, + marginBottom: 75, + }, + plotOptions: { + series: { + animation, + }, + /* + column: { + borderColor: null, + pointPadding: 0, + groupPadding: 0, + }, + */ + }, + series, + } + + /* + const years = data.map((d) => d.id.slice(0, 4)).map(Number) + + const total = Object.values(data[0][band]).reduce( + (acc, cur) => acc + cur, + 0 + ) + + const keys = [ + ...new Set(data.map((d) => Object.keys(d[band]).map(Number)).flat()), + ] + + // console.log("Keys", keys); + + const series = keys.map((key) => ({ + key, + name: landcoverTypes.find((c) => c.value === key).name, + color: landcoverTypes.find((c) => c.value === key).color, + data: data.map((d) => + roundOneDecimal(((d[band][key] || 0) / total) * 100) + ), + })) + + series.sort((a, b) => { + const aValue = data[0][band][a.key] || 0 + const bValue = data[0][band][b.key] || 0 + return aValue - bValue + }) + */ + + /* + return { + chart: { + type: 'column', + height: 580, + }, + title: { + text: i18n.t('{{name}}: Land cover changes {{years}}', { + name, + years: `${years[0]}-${years[years.length - 1]}`, + nsSeparator: ';', + }), + }, + credits: landCoverCredits, + xAxis: { + categories: years, + }, + yAxis: { + min: 0, + max: 100, + labels: { + format: '{value}%', + }, + title: { + enabled: false, + }, + }, + tooltip: { + shared: true, + headerFormat: + '{point.key}
', + valueSuffix: '%', + }, + plotOptions: { + series: { + animation, + }, + column: { + stacking: 'normal', + borderColor: null, + // pointPadding: 0, + groupPadding: 0, + }, + }, + series, + } + */ +} + +export default getChartConfig diff --git a/src/hooks/useEarthEngineTimeSeries.js b/src/hooks/useEarthEngineTimeSeries.js index 693f0c59..621c9a36 100644 --- a/src/hooks/useEarthEngineTimeSeries.js +++ b/src/hooks/useEarthEngineTimeSeries.js @@ -26,6 +26,8 @@ const useEarthEngineTimeSeries = ({ dataset, period, feature, filter }) => { useEffect(() => { let canceled = false + // console.log('A', dataset, feature) + if (dataset && feature) { const key = getCacheKey({ dataset, period, feature, filter }) const { geometry } = feature @@ -42,6 +44,8 @@ const useEarthEngineTimeSeries = ({ dataset, period, feature, filter }) => { } } + // console.log('B') + setData() eePromise.then((ee) => { cachedPromise[key] = period diff --git a/src/utils/ee-utils.js b/src/utils/ee-utils.js index 15d32fe2..265766f0 100644 --- a/src/utils/ee-utils.js +++ b/src/utils/ee-utils.js @@ -384,7 +384,17 @@ export const getTimeSeriesData = async ({ }) => { const { datasetId, band, aggregationPeriod } = dataset - let collection = ee.ImageCollection(datasetId).select(band) + const { type, coordinates } = geometry + const eeGeometry = ee.Geometry[type](coordinates) + + // console.log('getTimeSeriesData', dataset) + + // let collection = ee.ImageCollection(datasetId).select(band) + let collection = ee + .ImageCollection(datasetId) + .select(band) + .filter(ee.Filter.bounds(eeGeometry)) + // .reduce(ee.Reducer.mode()) if (Array.isArray(filter)) { filter.forEach((f) => { @@ -410,17 +420,23 @@ export const getTimeSeriesData = async ({ const months = ee.List.sequence(0, monthCount.subtract(1)) const dates = months.map((month) => startMonth.advance(month, 'month')) + // console.log('#########') + const byMonth = ee.ImageCollection.fromImages( dates.map((date) => { const startDate = ee.Date(date) const endDate = startDate.advance(1, 'month') - return collection - .filter(ee.Filter.date(startDate, endDate)) - .mean() // Use mean to avoid extremes on monthly chart - .set('system:index', startDate.format('YYYYMM')) - .set('system:time_start', startDate.millis()) - .set('system:time_end', endDate.millis()) + return ( + collection + .filter(ee.Filter.date(startDate, endDate)) + .mean() // Use mean to avoid extremes on monthly chart + // .mode() + // .reduce(ee.Reducer.mode()) + .set('system:index', startDate.format('YYYYMM')) + .set('system:time_start', startDate.millis()) + .set('system:time_end', endDate.millis()) + ) }) ) @@ -437,7 +453,7 @@ export const getTimeSeriesData = async ({ let eeScale = getScale(collection.first()) - const { type, coordinates } = geometry + // const { type, coordinates } = geometry if (type.includes('Polygon')) { // unweighted reducer may fail if the features are smaller than the pixel area @@ -449,7 +465,9 @@ export const getTimeSeriesData = async ({ } } - const eeGeometry = ee.Geometry[type](coordinates) + // const eeGeometry = ee.Geometry[type](coordinates) + + const maxPixels = 1e13 // Returns a time series array of objects return getInfo( @@ -458,7 +476,12 @@ export const getTimeSeriesData = async ({ ee .Feature( null, - image.reduceRegion(eeReducer, eeGeometry, eeScale) + image.reduceRegion({ + reducer: eeReducer, + geometry: eeGeometry, + scale: eeScale, + maxPixels: maxPixels, + }) ) .set('system:index', image.get('system:index')) // .set("id", image.get("system:index"))