Google Earth Engine – How to Create Box Plots for NDVI

chart;google-earth-enginendvi

I am trying to create an NDVI chart with bix plots in Google Earth Engine. I drew an ordinary chart for NDVI and SAVI using LT5 images for 1992-2000. But when it comes to drawing charts with box plots, the following error occurs:
enter image description here

The code is the following:

var countries = ee.FeatureCollection("FAO/GAUL/2015/level2");

var uzbekistan = countries.filter(ee.Filter.eq('ADM0_NAME','Uzbekistan'));

var bostanliq =uzbekistan.filter(ee.Filter.eq('ADM2_NAME','Bostanlik district'));
Map.addLayer(bostanliq, {}, "Bostanliq");


                                 ////////////LANDSAT 5 TM////////////
var dataset_5 = ee.ImageCollection('LANDSAT/LT05/C02/T1_L2')
    .filterDate('1992-01-01', '2001-01-01')
    .filterBounds(bostanliq)
    .filter(ee.Filter.lt('CLOUD_COVER',15))
    .map(function(image){return image.clip(bostanliq)});

// Applies scaling factors.
function applyScaleFactors_5(image) {
  var opticalBands = image.select('SR_B.').multiply(0.0000275).add(-0.2);
  var thermalBand = image.select('ST_B6').multiply(0.00341802).add(149.0);
  return image.addBands(opticalBands, null, true)
              .addBands(thermalBand, null, true);
}

dataset_5 = dataset_5.map(applyScaleFactors_5);

var dataset_list5 = dataset_5.toList(dataset_5.size());

print ('Landsat 5 list',dataset_list5);

var visualization = {
  bands: ['SR_B3', 'SR_B2', 'SR_B1'],
  min: 0.0,
  max: 0.3,
};

Map.setCenter(70.223, 41.714, 8);

Map.addLayer(dataset_5, visualization, 'Landsat 5 Bostanaliq');

//NDVI
function addNDVI_LT5 (image) {
  
  var ndvi =image.normalizedDifference(['SR_B4', 'SR_B3']).rename('NDVI');
  return image.addBands(ndvi);
}

//SAVI
function addSAVI_LT5 (image) {
  
  var savi =image.expression(
  '((NIR - RED)/(NIR+RED +0.5))*(1+0.5)',{
  'NIR':image.select('SR_B4'),
  'RED':image.select('SR_B3'),}).rename('SAVI'); 
  return image.addBands(savi);
}


dataset_5 = dataset_5.map(addNDVI_LT5);

dataset_5 = dataset_5.map(addSAVI_LT5);

//Map.addLayer(dataset_5.select('NDVI'),{},"NDVI for Bostanliq");
var chart1 =ui.Chart.image.series({
  imageCollection: dataset_5.select(['NDVI','SAVI']),
  region:bostanliq,
  reducer:ee.Reducer.mean(),
  scale:90}).setChartType('LineChart')
  .setOptions({
    linewidth:4,
    title:'NDVI and SAVI Time Series for Bostanliq from 1992 to 2000',
    interpolateNulls: true,
    vAxis: {title: 'VI', titleTextStyle: {italic: false, bold: true}},
    pointSize:2,
    curveType: 'function',
    colors: ['green','brown'],
    hAxis: {title:'Date', format: 'YYYY-MM',titleTextStyle: {italic: false, bold: true} }
  });
print(chart1);

var values =dataset_5.map(function(image) {
  var ndvi = image.select('NDVI');
  
  var allReducers = ee.Reducer.median()
    .combine({reducer2: ee.Reducer.min(), sharedInputs: true} )
    .combine({reducer2: ee.Reducer.max(), sharedInputs: true} )
    .combine({reducer2: ee.Reducer.percentile([25]), sharedInputs: true} )
    .combine({reducer2: ee.Reducer.percentile([50]), sharedInputs: true} )
    .combine({reducer2: ee.Reducer.percentile([75]), sharedInputs: true} );
  
  var stats = ndvi.reduceRegion({
    reducer: allReducers,
    geometry: bostanliq,
    scale: 30});
  var date = image.date();
  var dateString = date.format('YYYY-MM-dd');

  var properties = {
    'date': dateString,
    'median': stats.get('ndvi_p50'), // median is 50th percentile
    'min': stats.get('ndvi_min'),
    'max': stats.get('ndvi_max'),
    'p25': stats.get('ndvi_p25'),
    'p50': stats.get('ndvi_p50'),
    'p75': stats.get('ndvi_p75'),
  };
  return ee.Feature(null, properties);
  
});

// Remove null values
var values = values.filter(ee.Filter.notNull(
  ['median', 'min', 'max', 'p25', 'p50', 'p75']));
// Format the results as a list of DataTable rows

// We need a list to map() over
var dateList = values.aggregate_array('date');

// Helper function to format dates as per DataTable requirements
// Converts date strings

function formatDate(date) {
  var year = ee.Date(date).get('year').format();
  var month = ee.Date(date).get('month').subtract(1).format();
  var day = ee.Date(date).get('day').format();
  return ee.String('Date(')
    .cat(year)
    .cat(', ')
    .cat(month)
    .cat(', ')
    .cat(day)
    .cat(ee.String(')'));
}

var rowList = dateList.map(function(date) {
  var f = values.filter(ee.Filter.eq('date', date)).first();
  var x = formatDate(date);
  var median = f.get('median');
  var min = f.get('min');
  var max = f.get('max');
  var p25 = f.get('p25');
  var p50 = f.get('p50');
  var p75 = f.get('p75');
  var rowDict = {
    c: [{v: x}, {v: median}, {v: min}, {v: max},
        {v: p25}, {v: p50}, {v: p75}]
  };
  return rowDict;
});

print('Rows', rowList);
// We need to convert the server-side rowList object
// to client-side javascript object
// use evaluate()
rowList.evaluate(function(rowListClient) {
  var dataTable = {
    cols: [
      {id: 'x', type: 'date'},
      {id: 'median', type: 'number'},
      {id: 'min', type: 'number', role: 'interval'},
      {id: 'max', type: 'number', role: 'interval'},
      {id: 'firstQuartile', type: 'number', role: 'interval'},
      {id: 'median', type: 'number', role: 'interval'},
      {id: 'thirdQuartile', type:'number', role: 'interval'}
    ],
    rows: rowListClient
  };

  var options = {
    title:'NDVI Time-Series Box Plot',
    vAxis: {
      title: 'NDVI',
      gridlines: {
        color: '#d9d9d9'
      },
      minorGridlines: {
        color: 'transparent'
      }
    },
    hAxis: {
      title: '',
      format: 'YYYY-MM',
      viewWindow: {
        min: new Date(1992, 0),
        max: new Date(2000, 0)
      },
      gridlines: {
        color: '#d9d9d9'
      },
      minorGridlines: {
        color: 'transparent'
      }
    },
    legend: {position: 'none'},
    lineWidth: 1,
    series: [{'color': '#D3362D'}],
    interpolateNulls: true,
    intervals: {
      barWidth: 2,
      boxWidth: 4,
      lineWidth: 1,
      style: 'boxes'
    },
    interval: {
      min: {
        style: 'bars',
        fillOpacity: 1,
        color: '#777777'
      },
      max: {
        style: 'bars',
        fillOpacity: 1,
        color: '#777777'
      }
    },
    chartArea: {left:100, right:100}
  };
    
  var chart = ui.Chart(dataTable, 'LineChart', options);
  print(chart);
});



Best Answer

Following error:

List (Error) Collection.reduceColumns: Error in map(ID=LT05_152031_19920326): Dictionary.get: Dictionary does not contain key: ndvi_p25.

is produced because your statement as follows is not working for removing null values in dictionary of properties variable.

// Remove null values
var values = values.filter(ee.Filter.notNull(
  ['median', 'min', 'max', 'p25', 'p50', 'p75']));

The issue can be solved in two ways but, I chose the one whose procedure allows to preserve the remaining of your code. So, your variable values looks now as follows:

var values = dataset_5.toList(dataset_5.size()).map(function(image) {
  
  var ndvi = ee.Image(image).select('NDVI');
  
  var allReducers = ee.Reducer.median()
    .combine({reducer2: ee.Reducer.min(), sharedInputs: true} )
    .combine({reducer2: ee.Reducer.max(), sharedInputs: true} )
    .combine({reducer2: ee.Reducer.percentile([25]), sharedInputs: true} )
    .combine({reducer2: ee.Reducer.percentile([50]), sharedInputs: true} )
    .combine({reducer2: ee.Reducer.percentile([75]), sharedInputs: true} );
  
  var stats = ndvi.reduceRegion({
    reducer: allReducers,
    geometry: bostanliq,
    scale: 30});
  
  var date = ee.Image(image).date();
  var dateString = date.format('YYYY-MM-dd');
  
  var values = stats.values();
  
  return ee.Algorithms.If(values.get(0), 
                          ee.Feature(null).set({'date': dateString,
                                                'NDVI_max': values.get(0),
                                                'NDVI_median': values.get(1),
                                                'NDVI_min': values.get(2),
                                                'NDVI_p25': values.get(3),
                                                'NDVI_p50': values.get(4),
                                                'NDVI_p75': values.get(5)}), 0);
  
});

print(values);

values = ee.FeatureCollection(values.removeAll([0]));

Complete code can be founded here. After running it at GEE code editor, it can be obtained the result of following picture. Now, it is printed your rowList variable.

enter image description here

However, it can be observed in above image that function is not working properly (see dictionary values inside red rectangle) avoiding to print box plot chart. You should fix it.

Editing Note:

For fixing the issue reported in your below comment, it is necessary to obtain a dictionary with real values for calculated parameters, not nulls values. For fixing it you need to modify your rowList variable as follows:

var rowList = dateList.map(function(date) {
  var f = values.filter(ee.Filter.eq('date', date)).first();
  var x = formatDate(date);
  var median = f.get('NDVI_median');
  var min = f.get('NDVI_min');
  var max = f.get('NDVI_max');
  var p25 = f.get('NDVI_p25');
  var p50 = f.get('NDVI_p50');
  var p75 = f.get('NDVI_p75');
  var rowDict = {
    c: [{v: x}, {v: median}, {v: min}, {v: max},
        {v: p25}, {v: p50}, {v: p75}]
  };
  return rowDict;
});

print('Rows', rowList);

After that, I run modified complete code in GEE code editor and, I got the desired box plot chart as it can be observed in following picture:

enter image description here

Observe inside red rectangle that now there are not null values for rowList variable.