What You'll Learn
- Define sample points for different land cover types and create a labeled FeatureCollection
- Build a multi-year, cloud-masked Landsat 8 NDVI collection
- Create a single-point time series chart using
ui.Chart.image.series() - Compare regions with
ui.Chart.image.seriesByRegion() - Detect NDVI trends using
ee.Reducer.linearFit()and map the results
Why This Matters
A single satellite image tells you what the land looks like right now. But what if you want to know how it's been changing? Is this forest getting healthier or declining? Did that drought leave a mark?
Time series charts reveal seasonal patterns, drought impacts, and long-term trends that single images simply cannot show. Trend maps turn years of satellite data into actionable information about landscape health. Let's see how.
Before You Start
- Prerequisites: Image Collections module, Spectral Indices module, Time Series Charting module.
- Estimated time: 40 minutes
- You will need: GEE Code Editor access
Key Terms
- Time series
- A sequence of observations recorded at regular time intervals, used here for NDVI values from sequential satellite images.
- Linear fit
- A regression method that calculates the best straight line through data points, producing a slope (trend direction) and offset (starting value).
- Greening
- A positive NDVI trend over time, indicating increasing vegetation cover or health.
- Browning
- A negative NDVI trend over time, indicating vegetation decline, drought stress, or land cover change.
Lab Instructions
In this lab, we'll analyze NDVI dynamics near Gainesville, Florida. We'll compare forest, cropland, and urban areas, then create a spatial map of vegetation trends across the region. Let's get started.
Part 1 - Define sample points
First, we need three sample points representing different land cover types. We'll label each one so that our charts display meaningful legend entries.
// Define three sample points near Gainesville, FL
var forest = ee.Geometry.Point([-82.35, 29.65]);
var cropland = ee.Geometry.Point([-82.5, 29.5]);
var urban = ee.Geometry.Point([-82.32, 29.65]);
// Create a labeled FeatureCollection
var regions = ee.FeatureCollection([
ee.Feature(forest, {'label': 'Forest'}),
ee.Feature(cropland, {'label': 'Cropland'}),
ee.Feature(urban, {'label': 'Urban'})
]);
// Add points to the map for reference
Map.centerObject(regions, 10);
Map.addLayer(forest, {color: 'green'}, 'Forest Point');
Map.addLayer(cropland, {color: 'orange'}, 'Cropland Point');
Map.addLayer(urban, {color: 'red'}, 'Urban Point');
Understanding the code:
ee.Feature(geometry, properties)creates a feature with a location and metadata. The'label'property is what the chart will use as each series name.ee.FeatureCollection([])groups the three features into a collection that charting functions can iterate over.
What you should see
Three colored dots on the map near Gainesville, FL. Verify that each point falls on the correct land cover type by switching to satellite basemap.
Part 2 - Build a cloud-masked NDVI collection
Let's load four years of Landsat 8 imagery (2020 through 2023), apply cloud masking, and compute NDVI for every image. Four years gives us enough data to spot real trends.
// Cloud masking function for Landsat 8 SR
function maskL8sr(image) {
var qaMask = image.select('QA_PIXEL').bitwiseAnd(1 << 3).eq(0)
.and(image.select('QA_PIXEL').bitwiseAnd(1 << 4).eq(0));
return image.updateMask(qaMask)
.multiply(0.0000275).add(-0.2);
}
// Load Landsat 8 collection for 2020-2023
var collection = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
.filterDate('2020-01-01', '2023-12-31')
.filterBounds(regions)
.filter(ee.Filter.lt('CLOUD_COVER', 30))
.map(maskL8sr)
.map(function(img) {
var ndvi = img.normalizedDifference(['SR_B5', 'SR_B4']).rename('NDVI');
return img.addBands(ndvi);
});
print('Total images in collection:', collection.size());
Understanding the code:
- The
.map(maskL8sr)call applies cloud masking to every image in the collection. - The second
.map()adds an NDVI band to each image using.normalizedDifference(). - Four years of data provides enough observations for reliable trend detection.
What you should see
The console should print a number between 100 and 200, depending on how many Landsat 8 scenes cover this area.
Part 3 - Chart NDVI at a single point
Let's use ui.Chart.image.series() to plot NDVI over time at the forest point.
This chart will reveal the seasonal cycle and overall trajectory. Let's see it in action.
// Chart NDVI at the forest point
var forestChart = ui.Chart.image.series({
imageCollection: collection.select('NDVI'),
region: forest,
reducer: ee.Reducer.mean(),
scale: 30
}).setOptions({
title: 'NDVI Time Series - Forest (2020-2023)',
vAxis: {title: 'NDVI', minValue: 0, maxValue: 1},
hAxis: {title: 'Date'},
lineWidth: 1,
pointSize: 3,
series: {0: {color: 'green'}}
});
print(forestChart);
Understanding the code:
ui.Chart.image.series()extracts NDVI from every image at the specified point and plots it as a time series.ee.Reducer.mean()averages the pixel values within the region (for a single point, it simply returns the value at that pixel)..setOptions()customizes the chart with titles, axis labels, colors, and point sizes.
What you should see
A chart in the Console panel with NDVI values ranging roughly from 0.4 to 0.9. You should observe a seasonal pattern: higher values in summer (peak greenness) and lower values in winter. That rhythm is the forest's annual heartbeat.
Part 4 - Compare all three land cover types
Here's where it gets interesting. Let's plot all three land cover types on the same chart using
ui.Chart.image.seriesByRegion(). This reveals how each land cover
responds differently to seasonal cycles.
// Chart NDVI for all three regions
var comparisonChart = ui.Chart.image.seriesByRegion({
imageCollection: collection.select('NDVI'),
regions: regions,
reducer: ee.Reducer.mean(),
band: 'NDVI',
scale: 30,
seriesProperty: 'label'
}).setOptions({
title: 'NDVI by Land Cover Type (2020-2023)',
vAxis: {title: 'NDVI', minValue: 0, maxValue: 1},
hAxis: {title: 'Date'},
lineWidth: 1,
pointSize: 3,
series: {
0: {color: 'orange'},
1: {color: 'green'},
2: {color: 'gray'}
}
});
print(comparisonChart);
Understanding the code:
seriesByRegion()creates a separate line for each feature in the FeatureCollection.seriesProperty: 'label'uses the label property from each feature as the legend name.- The
seriesoption assigns colors to each line (the order matches the FeatureCollection order).
What you should see
Three colored lines on one chart. Forest (green) should show consistently high NDVI with gentle seasonal swings. Cropland (orange) should show dramatic peaks during the growing season and drops during fallow periods. Urban (gray) should remain low and relatively flat. Each land cover type has its own signature, like a fingerprint.
Part 5 - Detect NDVI trends with linear regression
Now let's get quantitative. We'll apply ee.Reducer.linearFit() to figure out whether
NDVI is increasing or decreasing over the four-year period. First, we need to add a time band
to each image.
// Add a time band to each image (fractional years since 2020)
var withTime = collection.map(function(img) {
var ndvi = img.select('NDVI');
var timeImage = ee.Image.constant(
img.date().difference('2020-01-01', 'year')
).float().rename('time');
return ndvi.addBands(timeImage);
});
// Apply linear regression (bands must be in order: time, NDVI)
var trend = withTime.select(['time', 'NDVI'])
.reduce(ee.Reducer.linearFit());
// Print trend values at each sample point
print('Forest trend:', trend.reduceRegion({
reducer: ee.Reducer.mean(),
geometry: forest, scale: 30, maxPixels: 1e9
}));
print('Cropland trend:', trend.reduceRegion({
reducer: ee.Reducer.mean(),
geometry: cropland, scale: 30, maxPixels: 1e9
}));
print('Urban trend:', trend.reduceRegion({
reducer: ee.Reducer.mean(),
geometry: urban, scale: 30, maxPixels: 1e9
}));
Understanding the code:
img.date().difference('2020-01-01', 'year')converts each image date to fractional years since January 1, 2020. This makes the slope interpretable as "NDVI change per year."ee.Reducer.linearFit()expects bands in order: independent variable (time) first, dependent variable (NDVI) second.- The result has two bands:
scale(the slope) andoffset(the intercept).
What you should see
Three printed dictionaries, each containing a scale (slope) and
offset value. A positive slope means greening (NDVI going up).
A negative slope means browning (NDVI going down). Values near zero?
That's a stable landscape.
Part 6 - Map the trend spatially
Let's apply the trend analysis to every pixel in the study area to create a spatial map of greening and browning. We'll use a diverging color palette so that greening pixels show up green and browning pixels show up red.
// Visualize the slope (trend) spatially
Map.addLayer(trend.select('scale'), {
min: -0.05,
max: 0.05,
palette: ['red', 'orange', 'lightyellow', 'lightgreen', 'darkgreen']
}, 'NDVI Trend (2020-2023)');
// Add reference points on top
Map.addLayer(regions, {color: 'blue'}, 'Sample Points');
// Add a true color basemap for context
var composite = collection.median();
Map.addLayer(composite, {
bands: ['SR_B4', 'SR_B3', 'SR_B2'],
min: 0, max: 0.3
}, 'True Color Composite', false);
Understanding the code:
trend.select('scale')extracts only the slope band from the linear fit result.- The diverging palette centers on light yellow (no change), with green for positive slopes and red for negative slopes.
- The
minandmaxof -0.05 to 0.05 represent a range of 0.05 NDVI units change per year. - The true color composite is added but turned off by default (
falseparameter). Toggle it on for context.
What you should see
A trend map centered on the Gainesville, FL area. Stable forested areas should appear light yellow or light green. Areas with new development or disturbance may appear red or orange. Agricultural areas may show variable patterns depending on crop rotation. This is a powerful way to see landscape change at a glance.
Check Your Understanding
- What does a slope of -0.02 mean in the context of an NDVI trend analysis?
- Why does cropland NDVI have more dramatic seasonal peaks than forest NDVI?
- What is the difference between the "scale" band and the "offset" band in the linearFit output?
- Why do we use fractional years instead of milliseconds as the time variable?
Troubleshooting
Solution: Check that your point coordinates are correct and fall within
the image collection's footprint. Print collection.size() to verify images exist.
Make sure you selected the 'NDVI' band in the chart call.
Solution: These are usually residual cloud contamination. Try lowering the CLOUD_COVER threshold from 30 to 15 for cleaner results, though you will have fewer data points.
Solution: The collection has too few images at that location. Widen the date range, relax the cloud filter, or choose a location with better Landsat coverage.
Solution: Adjust the min and max values in the
visualization parameters. Try min: -0.02, max: 0.02 for subtler trends.
Key Takeaways
ui.Chart.image.series()plots one band from an image collection over time at a single location.ui.Chart.image.seriesByRegion()compares multiple locations on the same chart.ee.Reducer.linearFit()fits a straight line through temporal data, returning slope and offset.- Positive slope indicates greening; negative slope indicates browning.
- Mapping the slope band spatially reveals landscape-scale vegetation trends.
Common Mistakes to Avoid
- Putting bands in the wrong order for
linearFit(): time must be first, NDVI second. - Forgetting to convert dates to fractional years, which makes slope values uninterpretable.
- Using too few images (less than 30) for reliable trend detection.
- Not specifying
scale: 30in chart calls, which defaults to a coarser resolution.
Pro Tips
- Click the small icon in the upper-right corner of any GEE chart to download it as a CSV file or SVG image for use in reports.
- Filter to summer months only (June through August) before running
linearFit()to remove seasonal noise and isolate true annual trends. - Experiment with the
pointSizeandlineWidthchart options to find the most readable combination for your data density. - Use the Inspector tool to click on individual pixels in the trend map and examine their slope values.
📋 Lab Submission
Subject: Lab 22 - Time Series Charting - [Your Name]
Include in your submission:
- Shareable GEE script link (click "Get Link" in the Code Editor)
- Screenshot of your multi-region comparison chart (Part 4)
- Screenshot of your NDVI trend map (Part 6)
- Interpretation paragraph (3-5 sentences): Describe the differences you observe between the three land cover types in the time series chart. Which land cover shows the strongest seasonal signal? Which is the most stable? Explain why these differences exist.
- Question 1: What is the slope value at your forest point? Is the forest greening or browning over the 2020-2023 period?
- Question 2: Identify one area on your trend map that shows a strong negative slope (browning). What do you think is causing the vegetation decline at that location? Use the true color composite to investigate.