I am generating an NDVI Timeseries for masked land cover classes using the Copernicus landcover 2019 Global product in Google Earth Engine. I have masked the landcover classes but and I am computing the NDVI time-series for these masked landcover classes at the Pixel level. The code runs but in the chart I have multiple observations for each date. I understand this is a result of several images being available for the same date.

Is there a way I could reduce these NDVI for a single date into one mean value to have a less noisy (attached) NDVI time-series chart?

The red line is the trend line just for visualizing the NDVI trend over time.

The link to my code https://code.earthengine.google.com/?scriptPath=users%2Fwawerujohn%2FProject__Work%3ASamburu

The code:

var globcover = ee.Image("COPERNICUS/Landcover/100m/Proba-V-C3/Global/2019")

// Extract the landcover band
var landcover = globcover.select('discrete_classification');
// Clip the image to the polygon geometry
var landcover_roi = landcover.clip(geometry);
// Add a map layer of the landcover clipped to the polygon.
// Print out the frequency of landcover occurrence for the polygon.
var frequency = landcover.reduceRegion({

// Select the classes you are interested in analyzing
//Shrub land cover
//var LCclass20 = landcover_roi.eq(20);

//Vegetation land cover
var LCclass30 = landcover_roi.eq(30);

//Forest land cover
//var LCclass126 = landcover_roi.eq(126);

// Update mask to landcover_roi to stay only with the LCclass20 pixels
// Then transform it to ee.Feature
//var LCclass20Vec = landcover_roi.updateMask(LCclass20)
//  scale: 1200,
//  maxPixels: 1e11

// Update mask to landcover_roi to stay only with the LCclass30 pixels
// Then transform it to ee.Feature
var LCclass30Vec = landcover_roi.updateMask(LCclass30)
  scale: 1200,
  maxPixels: 1e20

// Make a union of all polygons into a single one
//LCclass20Vec = LCclass20Vec.union(1000);
LCclass30Vec = LCclass30Vec.union(1000000);
//LCclass126Vec = LCclass126Vec.union(10000);

//Vegetation land cover
var LCclass30 = landcover_roi.eq(30); 

//Forest land cover
//var LCclass126 = landcover_roi.eq(126)

//print('Shrub', LCclass20);
print('Vegetation', LCclass30);
//print('Forest', LCclass126);

//Map.addLayer(LCclass20, {}, "Shrub");
Map.addLayer(LCclass30, {}, "Vegetation");
//Map.addLayer(LCclass126, {}, "Forest");

// Function to remove cloud and snow pixels
function maskCloudAndShadows(image) {
  var cloudProb = image.select('MSK_CLDPRB');
  var snowProb = image.select('MSK_SNWPRB');
  var cloud = cloudProb.lt(5);
  var snow = snowProb.lt(5);
  var scl = image.select('SCL'); 
  var shadow = scl.eq(5); // 3 = cloud shadow
  var cirrus = scl.eq(5); // 10 = cirrus
  // Cloud probability less than 5% or cloud shadow classification
  var mask = (cloud.and(snow)).and(cirrus.neq(1)).and(shadow.neq(1));
  return image.updateMask(mask);

// Adding a NDVI band
function addNDVI(image) {
  var ndvi = image.normalizedDifference(['B8', 'B4']).rename('ndvi')
  return image.addBands([ndvi])

var startDate = '2018-01-01'
var endDate = '2021-05-31'

// Use Sentinel-2 L2A data - which has better cloud masking
var collection = ee.ImageCollection('COPERNICUS/S2_SR')
    .filterDate(startDate, endDate)

// View the median composite
var vizParams = {bands: ['B4', 'B3', 'B2'], min: 0, max: 2000}
Map.addLayer(collection.median(), vizParams, 'collection')

// Create palettes for display of NDVI
var visParams = {
  min: -0.1,
  max: 1.0,
  palette: [
    'FFBB22', 'FFFF4C', '648C00', 

// Make chart selecting the 'nd' band, indicate the desired reducer to apply over the complete polygon and the desired scale 
var chart = ui.Chart.image.series({
    imageCollection: collection.select('ndvi'),
    region: LCclass30Vec,
    reducer: ee.Reducer.median(),
    scale: 120
      trendlines: {0: {
        color: 'CC0000'
      interpolateNulls: true,
      lineWidth: 1,
      pointSize: 3,
      title: 'NDVI over Time at Pixel Level for Vegetation Landcover',
      vAxis: {title: 'NDVI'},
      hAxis: {title: 'Date', format: 'YYYY-MMM', gridlines: {count: 12}}

var dict = ee.Dictionary(frequency.get('discrete_classification'));
var sum = ee.Array(dict.values()).reduce(ee.Reducer.sum(),[0]).get([0]);
var new_dict = dict.map(function(k,v) {
  return ee.Number(v).divide(sum).multiply(100);
print('Land Cover (%)',new_dict);

print('landcover frequency', frequency.get('discrete_classification'));

Map.setCenter(37, 1, 8);

//Map.addLayer(globcover, {}, "Land Cover");

      image: landcover_roi,
      description: 'Landcover',
      scale: 100,
      region: geometry,
      fileFormat: 'GeoTIFF',
      formatOptions: {
        cloudOptimized: true

1 Answer 1


The solution is a little tricky because it involves two steps. First, you need to get a list that contains the unique dates of image acquisition. Afterwards, you will use this list to filter the collection by date and reduce the images that have the same date of acquisition. Finally, you need to create a new ee.ImageCollection from these images. You need to insert this code between the collection filtering process and setting the vizparameters and change the collection used for the image series chart to imgColNew.

//Step 1
// Create a function to extract the date of acquisition of the  images
var consult = function(image){
  var resul = ee.Dictionary({
    date: ee.Date(image.get('system:time_start')).format('y-M-d')
  return ee.Feature(null, resul);

// Map this function to the filtered collection
var dates = collection.map(consult);
// Aggregate the FeatureCollection over the 'date' property
dates = dates.aggregate_array('date');
// Get distinct values of dates
dates = dates.distinct();

//Step 2
// Create function to filter images by the dates stored in the previous dates object
var reduceImgs = function(date){
  // Create end date as 1 day after the date passed to the function
  var date2 = ee.Date(ee.String(date)).advance(1,'day');
  // Filter collection according to date and date2
  var dayCollection = collection.filterDate(ee.Date(date), date2);
  // Calculate mean of the filtered collection
  var resul = dayCollection.mean();
  // Return the mean image and set the system start property = date
  return ee.Image(resul).set('system:time_start', ee.Date(date));

// Map the previous function over the dates object
var imgColNew = dates.map(reduceImgs);
// Create a new image collection from the previous list of images
imgColNew = ee.ImageCollection.fromImages(imgColNew);
  • thank you so much for the well-guided explanation and also for the code snippet. That was a tricky solution and I had to take some time to understand the script before implementing it. Thank you for always taking your time to reach out and share your knowledge. I have learnt a lot from you.
    – Shiraz
    Jun 23, 2021 at 12:17
  • This is a great answer and helped with a similar challenge I was working on with the GPM IMERG Dataset. However, I noticed an issue with one of the functions. If you Map the new Image Collection <Map.addLayer(imgColNew);> before VizParams and Inspect the Layer, opening the Series Chart will bring up an <invalid date> error. I believe it's because the <system: time_start:> Property of the Layers becomes '<Date (YYYY-MM-DD hh:mm:ss)>' with sub-Properties instead of the numbers used in the original Image Collection. You can still create charts separately, but could there be a solution? Jan 13, 2023 at 13:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.