NATURE / Analysis

Google Earth Engine NDVI Time Series: A Sentinel-2 Script with Cloud Score+

Copy-paste Google Earth Engine script for a Sentinel-2 NDVI time series with Cloud Score+ masking, monthly composites, a chart and CSV export.

climaRevolt
climaRevolt ExpertsWritten and checked by our specialists · 27 October 2026
8 min read
Google Earth Engine NDVI Time Series: A Sentinel-2 Script with Cloud Score+

In brief

  • This script charts NDVI for any area from 2019 to 2025 using Sentinel-2 surface reflectance in Google Earth Engine.
  • It masks clouds with Cloud Score+, Google’s current recommended cloud mask for Sentinel-2, instead of the older QA60 band most tutorials still use.
  • Monthly median composites remove most of the remaining noise, and the chart can be downloaded as a CSV from the Code Editor.
  • Earth Engine is free for non-commercial and research use once you register a Google Cloud project.

An NDVI time series shows how vegetation in one place changes through the seasons and across years: a crop cycle, a drought, a forest recovering after fire. Earth Engine is the quickest way to build one, because the whole Sentinel-2 archive is already there. The weak point in most tutorials is cloud masking. They use the QA60 band, which misses thin cloud and shadow, so the chart fills with false dips. The script below uses Cloud Score+ instead.

Before you start

You need a Google account and an Earth Engine-enabled Google Cloud project; registration for non-commercial use is free at code.earthengine.google.com. Paste each block into the Code Editor in order. Replace the area with your own: draw a polygon with the geometry tools, or edit the coordinates.

Step 1: the area and the cloud-masked collection

Sentinel-2 NDVI with Cloud Score+ masking · JavaScript
// Area of interest: a 500 m circle around a point (farmland near Seville, Spain).
// Replace with your own point or a drawn polygon.
var aoi = ee.Geometry.Point([-5.97, 37.38]).buffer(500);

var s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED');
var csPlus = ee.ImageCollection('GOOGLE/CLOUD_SCORE_PLUS/V1/S2_HARMONIZED');
var QA_BAND = 'cs_cdf';
var CLEAR_THRESHOLD = 0.60; // 0.50-0.65 works for most areas

var ndvi = s2
  .filterBounds(aoi)
  .filterDate('2019-01-01', '2026-01-01')
  .linkCollection(csPlus, [QA_BAND])
  .map(function (img) {
    var clear = img.updateMask(img.select(QA_BAND).gte(CLEAR_THRESHOLD));
    return clear.normalizedDifference(['B8', 'B4']).rename('NDVI')
      .set('system:time_start', img.get('system:time_start'));
  });

COPERNICUS/S2_SR_HARMONIZED already removes the 2022 radiometric offset, so you can use the bands directly. linkCollection joins each Sentinel-2 image to its Cloud Score+ image, and pixels with a clear-sky score below 0.6 are masked.

Step 2: monthly composites

One median NDVI image per month · JavaScript
var start = ee.Date('2019-01-01');
var nMonths = 7 * 12;

var monthly = ee.ImageCollection.fromImages(
  ee.List.sequence(0, nMonths - 1).map(function (m) {
    var from = start.advance(m, 'month');
    var subset = ndvi.filterDate(from, from.advance(1, 'month'));
    return subset.median()
      .set('system:time_start', from.millis())
      .set('n', subset.size());
  })
).filter(ee.Filter.gt('n', 0)); // drop months with no images

Step 3: chart it

Mean NDVI over the area, month by month · JavaScript
var chart = ui.Chart.image.series({
  imageCollection: monthly,
  region: aoi,
  reducer: ee.Reducer.mean(),
  scale: 10
}).setOptions({
  title: 'Monthly mean NDVI',
  vAxis: {title: 'NDVI', viewWindow: {min: 0, max: 1}},
  lineWidth: 1,
  pointSize: 3
});
print(chart);
Map.centerObject(aoi, 15);
Map.addLayer(monthly.filterDate('2025-06-01', '2025-07-01').first(),
  {min: 0, max: 0.9, palette: ['brown', 'yellow', 'green']}, 'NDVI June 2025');

Run the script. The chart appears in the Console. Click the arrow at its top right to open it full size, then Download CSV to take the numbers into Excel, R or Python.

Reading the chart

A regular wave each year is the growing season. A lower peak in one year can mean drought, a different crop or earlier harvest. A sudden, lasting drop suggests clearing, fire or building. Single sharp dips that recover the next month are often leftover cloud or haze; if you see many, raise the threshold to 0.65. For land under persistent cloud, Sentinel-1 radar is the alternative, because it sees through cloud.

Common problems and fixes
ProblemLikely causeFix
Chart is emptyArea outside your filter dates or too much cloudCheck the area and dates; lower the threshold slightly
“Too many pixels” or memory errorArea too large at 10 mUse scale: 20 or 30, or a smaller area
Many sharp dipsThin cloud or haze left inRaise CLEAR_THRESHOLD to 0.65
Values near zero all yearArea is water, urban or bareCheck the area on the map layer

Frequently asked questions

What is the best cloud mask for Sentinel-2 in Earth Engine?

Cloud Score+ (GOOGLE/CLOUD_SCORE_PLUS/V1/S2_HARMONIZED), linked to Sentinel-2 with linkCollection and a threshold of about 0.6 on the cs or cs_cdf band.

Do I need to remove the Sentinel-2 offset in Earth Engine?

Not with COPERNICUS/S2_SR_HARMONIZED, which already shifts newer scenes to match older ones.

How do I export an NDVI time series from Earth Engine?

Open the chart in the Console full size and click Download CSV, or use Export.table.toDrive with values from reduceRegion.

Is Google Earth Engine free?

It is free for non-commercial and research use with a registered Google Cloud project. Commercial use requires a paid licence.

THE CLIMAREVOLT WEEKLY

Get the climate stories that matter, explained, every Sunday.

One email a week: new explainers, data and free tools. No spam, unsubscribe any time.

ShareXLi