This workshop is under active development and is not complete.



Introduction

Google Earth Engine is a cloud-based platform that enables large-scale processing of satellite imagery to detect changes, map trends, and quantify differences on the Earth’s surface. This course covers the full range of topics in Earth Engine to give the participants practical skills to master the platform and implement their remote sensing projects.

View Presentation

View the Presentation ↗

Get the Workshop Materials

The course material and exercises are in the form of Earth Engine scripts shared via a code repository.

  1. Click this link to open Google Earth Engine code editor and add the repository to your account.
  2. If successful, you will have a new repository named users/ujavalgandhi/GEE-Charts in the Scripts tab in the Reader section.
  3. Verify that your code editor looks like below

If you do not see the repository in the Reader section, click Refresh repository cache button in your Scripts tab and it will show up.

Refresh repository cache

Refresh repository cache

1. Time-Series Charts

In this section, we will explore various built-in functions to create time-series charts from ImageCollections. We will also explore the customization options provided by Google Charts to make high-quality functional graphics.

1.1 Simple Time-Series

We start by using the time-series charting function ui.Chart.image.series() that allows you to create a time-series plot from an ImageCollection at a single location. You get one time-series per band of the input dataset. We take the TerraClimate dataset and select the bands for monthly maximum and minimum temperatures. The resulting chart is a Line Chart that can be further customized using the .setOptions() method.

Here are the customization applied to the default time-series chart:

  • lineWidth: Sets the thickness of the line
  • pointSize: Sets the size of the data point
  • title: Sets the chart title
  • vAxis: Sets the options for Y-Axis. Axis label is specified using the title option.
  • hAxis: Sets the options for X-Axis. Grid lines are specified using the gridlines option. Date format for tick labels is specified with format option.
  • series: Sets the options for each individual time-series. Series count starts from 0.
Time-Series Chart

Time-Series Chart

Open in Code Editor ↗

// Select a location
var geometry = ee.Geometry.Point([77.57738128916243, 12.964758918835752]);

// We use TerraClimate Dataset
var terraclimate = ee.ImageCollection('IDAHO_EPSCOR/TERRACLIMATE');

// Select the temerature bands
// 'tmmx' = Maximum temperature
// 'tmmn' (Minimum temperature)
var temp = terraclimate.select(['tmmx', 'tmmn']);

// The pixel values have a scale factor of 0.1
// We must multiply the pixel values with the scale factor
// to get the temperature values in °C
var tempScaled = temp.map(function(image) {
  return image.multiply(0.1)
    .copyProperties(image,['system:time_start']);
});

// Filter the collection
var startYear = 2022;
var endYear = 2022;
var startDate = ee.Date.fromYMD(startYear, 1, 1);
var endDate = ee.Date.fromYMD(endYear + 1, 1, 1);

var filtered = tempScaled
  .filter(ee.Filter.date(startDate, endDate));

// Create a time-series chart
var chart = ui.Chart.image.series({
  imageCollection: filtered.select(['tmmx']),
  region: geometry,
  reducer: ee.Reducer.mean(),
  scale: 4638.3
});

// Print the chart
print(chart);

// We can use .setOptions() to customize the chart
var chart = ui.Chart.image.series({
  imageCollection: filtered.select(['tmmx']),
  region: geometry,
  reducer: ee.Reducer.mean(),
  scale: 4638.3
});

var chart = ui.Chart.image.series({
  imageCollection: filtered.select(['tmmx']),
  region: geometry,
  reducer: ee.Reducer.mean(),
  scale: 4638.3
}).setOptions({
  lineWidth: 1,
  pointSize: 2,
  title: 'Monthly Temperature Time-Series',
  vAxis: {title: 'Temparature (°C)'},
  hAxis: {title: '', format: 'YYYY-MMM', gridlines: {count: 12}}
})
print(chart);    
    
// We can select multiple bands and get a time-series for each band
// Additionally, we can specify the 'series' options
// to specify styling options for each series.
var chart = ui.Chart.image.series({
  imageCollection: filtered.select(['tmmx', 'tmmn'], ['maximum', 'minimum']),
  region: geometry,
  reducer: ee.Reducer.mean(),
  scale: 4638.3
}).setOptions({
      lineWidth: 1,
      pointSize: 2,
      title: 'Monthly Temperature Time-Series',
      vAxis: {title: 'Temparature (°C)'},
      hAxis: {title: '', format: 'YYYY-MMM', gridlines: {count: 12}},
      series: {
        0: {color: 'red'},
        1: {color: 'blue'}
      },
    })

// Print the chart
print(chart);

Exercise

Open in Code Editor ↗

// Exercise

// a) Delete the 'geometry' and add a new point at your chosen location
// b) Modify the chart options display the series with dashed lines
// c) Print the chart.

// See reference:
// https://developers.google.com/chart/interactive/docs/lines#dashed 

1.2 Time-Series with Trendlines

Google Charts can dynamically compute and display Trendlines on the chart. You can choose from linear, polynomial or exponential trendlines. The linear trendline fit a least-square regression model on the dataset. Here we take a time-series of precipitation data, aggregate it to yearly precipitation and then display a linear trendline to indicate whether we see an increasing or decreasing rainfall in the region.

Here are the styling options applied to the time-series chart:

  • vAxis.ticks: Sets the tick positions for Y-Axis. We manually specify the exact tick marks we want.
  • gridlines.color: Sets the color of the grid lines.
  • legend: Sets the position of the legend. The in options makes the legend appear inside the chart.
  • series.visibleInLegend: Sets whether a particular series label is visible in the legend.
  • trendlines: Sets the option for trendlines. We override the default label using labelInLegend option.
Time-Series Chart with Trendline

Time-Series Chart with Trendline

Open in Code Editor ↗

// Select a region
var geometry = ee.Geometry.Point([77.6045, 12.8992]);

// We use the CHIRPS Rainfall Dataset
var chirps = ee.ImageCollection('UCSB-CHG/CHIRPS/PENTAD');

// We will compute the trend of total annual precipitation
var createAnnualImage = function(year) {
  var startDate = ee.Date.fromYMD(year, 1, 1);
  var endDate = startDate.advance(1, 'year');
  var seasonFiltered = chirps
    .filter(ee.Filter.date(startDate, endDate));
  // Calculate total precipitation
  var total = seasonFiltered.reduce(ee.Reducer.sum()).rename('Precipitation');
  return total.set({
    'system:time_start': startDate.millis(),
    'system:time_end': endDate.millis(),
    'year': year,
  });
};

// Aggregate Precipitation Data over 40 years
var years = ee.List.sequence(1981, 2020);
var yearlyImages = years.map(createAnnualImage);
var yearlyCol = ee.ImageCollection.fromImages(yearlyImages);

// Create a time-series with sens slope trend
var chart = ui.Chart.image.series({
  imageCollection: yearlyCol,
  region: geometry,
  reducer: ee.Reducer.mean(),
  scale: 5566,
}).setOptions({
    title: 'Annual Total Precipitation',
    color: 'blue',
    pointSize: 3,
    lineWidth: 1,
    vAxis: {
      title: 'Rainfall (mm)',
      ticks: [0, 200, 400, 600, 800, 1000, 1200, 1400], 
      gridlines: {color: '#f0f0f0'}
    },
    hAxis: {
      title: 'Year',
      gridlines: {
        color: '#f0f0f0',
        units: {years: { format: 'YYYY'}}
      }
    },

    legend: {
      position: 'in'
    },
    series: {
      0: {
        visibleInLegend: false
      }
    },
    trendlines: {
      0: {
        type: 'linear', 
        color: 'black', 
        lineWidth: 1,
        pointSize: 0,
        visibleInLegend: true,
        labelInLegend: 'Precipitation Trend',
      }
    },
});
print(chart);

Exercise

Open in Code Editor ↗

// Exercise

// a) Delete the 'geometry' and add a new point at your chosen location
// b) Modify the chart options to remove the legend from the chart.
// c) Print the chart.

// Hint: Use legend 'position' option
// See reference:
// https://developers.google.com/chart/interactive/docs/gallery/linechart

1.3 Time-Series at Multiple Locations

So far, we have learnt how to display time-series of one or more variables at a single location using the ui.Chart.image.series() function. If you wanted to plot time-series of multiple locations in a single chart, you can use the ui.Chart.image.series.byRegion() function. This function takes a FeatureCollection with one or more locations and extract the time-series at each geometry.

Here we take the Global Forecast System (GFS) dataset and create a chart of 16-day temperature-forecasts at 2 cities.

Time-Series Chart at Multiple Locations

Time-Series Chart at Multiple Locations

Open in Code Editor ↗

// Select the locations
var geometry1 = ee.Geometry.Point([72.57, 23.04]);
var geometry2 = ee.Geometry.Point([77.58, 12.97]);
    
// We use the NOAA GFS dataset
var gfs = ee.ImageCollection('NOAA/GFS0P25');

// Select the temperature band
var forecast = gfs.select('temperature_2m_above_ground');

// Get the forecasts for today
var now = Date.now();
var now = ee.Date(now).advance(-1, 'day');

var filtered = forecast
  .filterDate(now, now.advance(1, 'day'));

// All forecast images have a timestamp of the current day
// As we want a time-series of forecasts, we update the
// timestamp to the date the image is forecasting.
var filtered = filtered.select('temperature_2m_above_ground')
  .map(function(image) {
    var forecastTime = image.get('forecast_time');
    return image.set('system:time_start', forecastTime);
  });

// Create a chart of forecast at a single location
var chart = ui.Chart.image.series({
  imageCollection: filtered,
  region: geometry1,
  reducer: ee.Reducer.first(),
  scale: 27830}).setOptions({
    lineWidth: 1,
    pointSize: 2,
    title: 'Temperature Forecast at a Single Location',
    vAxis: {title: 'Temparature (°C)'},
    hAxis: {title: '', format: 'YYYY-MM-dd'},
    series: {
      0: {color: '#fc8d62'},
    },
    legend: {
      position: 'none'
    }
  });
print(chart);


// For plotting multiple locations, we need a FeatureCollection
var locations = ee.FeatureCollection([
  ee.Feature(geometry1, {'name': 'Ahmedabad'}),
  ee.Feature(geometry2, {'name': 'Bengaluru'})
  ]);
  
// Create a chart of forecasted temperatures

var chart = ui.Chart.image.seriesByRegion({
  imageCollection: filtered,
  regions: locations,
  reducer: ee.Reducer.first(),
  scale: 27830,
  seriesProperty: 'name'
}).setOptions({
      lineWidth: 1,
      pointSize: 2,
      title: 'Temperature Forecast at Multiple Locations',
      vAxis: {title: 'Temparature (°C)'},
      hAxis: {title: '', format: 'YYYY-MM-dd'},
      series: {
        0: {color: '#fc8d62'},
        1: {color: '#8da0cb'}
      },
      legend: {
        position: 'top'
      }
    });
print(chart);

Exercise

Open in Code Editor ↗

// Exercise

// a) Replace the 'geometry1' and 'geometry2' points with your chosen locations.
// b) Modify the chart options to limit the Y-Axis range to the 
//    actual range of temperatures at your chosen locations (i.e. between 20-45 degrees)
// c) Print the chart.

1.4 Multi-Year Time-Series

Another useful function to plot time-series is `ui.Chart.image.doySeriesByYear() that extracts and plots values from an image band at different Day-Of-Year (DOY) over many years. This type of chart is helpful visualize both inter-annual and inter-annual variations in a single chart.

Here we take the MODIS 16-day Vegetation Indices (VI) dataset and create a chart of NDVI Time-Series over 4 years.

Here are the styling options applied to the time-series chart:

  • interpolateNulls: Sets whether to fill missing (i.e masked) time-series values
  • curveType: Apply smoothing on the time-series by fitting a function.
DOY Time-Series Chart

DOY Time-Series Chart

Open in Code Editor ↗

// Select a location
var geometry = ee.Geometry.Point([81.73099978484261, 27.371459793533507]);

// We use the MODIS 16-day Vegetation Indicies dataset
var modis = ee.ImageCollection('MODIS/061/MOD13Q1');

// Filter the collection
var startYear = 2019;
var endYear = 2022;

var startDate = ee.Date.fromYMD(startYear, 1, 1);
var endDate = ee.Date.fromYMD(endYear + 1, 1, 1);

var filtered = modis
  .filter(ee.Filter.date(startDate, endDate))
  
// Pre-Processing: Cloud Masking and Scaling

// Function for Cloud Masking
var bitwiseExtract = function(input, fromBit, toBit) {
  var maskSize = ee.Number(1).add(toBit).subtract(fromBit)
  var mask = ee.Number(1).leftShift(maskSize).subtract(1)
  return input.rightShift(fromBit).bitwiseAnd(mask)
}

var maskSnowAndClouds = function(image) {
  var summaryQa = image.select('SummaryQA')
  // Select pixels which are less than or equals to 1 (0 or 1)
  var qaMask = bitwiseExtract(summaryQa, 0, 1).lte(1)
  var maskedImage = image.updateMask(qaMask)
  return maskedImage.copyProperties(
    image, ['system:index', 'system:time_start'])
}

// Function for Scaling Pixel Values
// MODIS NDVI values come as NDVI x 10000
// that need to be scaled by 0.0001
var ndviScaled = function(image) {
  var scaled = image.divide(10000)
  return scaled.copyProperties(
    image, ['system:index', 'system:time_start'])
};

// Apply the functions and select the 'NDVI' band
var processedCol = filtered
  .map(maskSnowAndClouds)
  .map(ndviScaled)
  .select('NDVI');

// Plot a time-series
var chart = ui.Chart.image.series({
  imageCollection: processedCol,
  region: geometry,
  reducer: ee.Reducer.mean(),
  scale: 250
}).setOptions({
    interpolateNulls: true,
    lineWidth: 1,
    pointSize: 2,
    title: 'NDVI Time-Series',
    vAxis: {title: 'NDVI'},
})
print(chart);

// We can plot a yearly time-series 
// that allows us to compare changes over time
var chart = ui.Chart.image.doySeriesByYear({
  imageCollection: processedCol,
  region: geometry,
  regionReducer: ee.Reducer.mean(),
  scale: 250,
  bandName: 'NDVI'
}).setOptions({
      interpolateNulls: true,
      curveType: 'function',
      lineWidth: 1,
      pointSize: 2,
      title: 'Multi-Year NDVI Time-Series',
      vAxis: {title: 'NDVI'},
      hAxis: {title: 'Day-of-Year'},
      legend: {position: 'top'}
    })
print(chart)

Exercise

Exercise 04c

Exercise 04c

Open in Code Editor ↗

// Exercise

// a) Replace the 'geometry' with your chosen location.
// b) Modify the chart to specify custom colors for each year.
//    Use color codes from https://colorbrewer2.org/
// c) Modify the chart to plot only the time-series
//    with lines without any points.
// c) Print the chart.

2. Image Charts

This section covers charting functions and techniques to plot values from an image. We will also learn how to deal with limitations of the charting API and create plots by extracting data from large regions.

2.1 Image Histogram

A histogram plot is a bar chart showing count of pixel values. Typically the pixel values are grouped into range of values called buckets on the X-Axis and the total count of pixels is shown on the Y-Axis.

Here we take the Harmonized Night Time Lights dataset that contains images from both DMSP and VIIRS sensors.

Below is the list of styling options applied to the histogram:

  • hAxis.ticks: Sets the tick labels on the X-Axis.
  • bar.gap: Sets the gap between each histogram bar
Image Histogram

Image Histogram

Open in Code Editor ↗

// We use the Harmonized Global Night Time Lights (1992-2020) dataset
var dmsp = ee.ImageCollection('projects/sat-io/open-datasets/Harmonized_NTL/dmsp');
var viirs = ee.ImageCollection('projects/sat-io/open-datasets/Harmonized_NTL/viirs');

// Merge both collections to create a single Night Lights Collection
var ntlCol = dmsp.merge(viirs);

// Using LSIB for country boundaries
var lsib = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');
    
var country = 'Japan';
var selected = lsib.filter(ee.Filter.eq('country_na', country));
var geometry = selected.geometry();

var year = 2009;
var startDate = ee.Date.fromYMD(year, 1, 1);
var endDate = startDate.advance(1, 'year')

// We filter for the selected year
var filtered = ntlCol
  .filter(ee.Filter.date(startDate, endDate))

// Extract the image and set the masked pixels to 0
var ntlImage = ee.Image(filtered.first()).unmask(0);

var palette =['#253494','#2c7fb8','#41b6c4','#a1dab4','#ffffcc' ];
var ntlVis = {min:0, max: 63,  palette: palette}

Map.centerObject(geometry, 6);
Map.addLayer(ntlImage.clip(geometry), ntlVis, 'Night Time Lights ' + year);

// Extract the native resolution of the image
var resolution = ntlImage.projection().nominalScale();

// NTL images have DN values from 0-63
// We can create a histogram to show pixel counts
// for each DN value
var chart = ui.Chart.image.histogram({
  image: ntlImage,
  region: geometry,
  scale: resolution,
  maxBuckets: 63,
  minBucketWidth: 1})

print(chart);

// Add options to add labels and ticks
var chart = ui.Chart.image.histogram({
  image: ntlImage,
  region: geometry,
  scale: resolution,
  maxBuckets: 63,
  minBucketWidth: 1
}).setOptions({
    title: 'Night Time Lights Distribution for ' + country + ' ' + year,
    vAxis: {
      title: 'Number of Grids', 
      gridlines: {color: 'transparent'}
    },
    hAxis: {
      title: 'Level of Observed Nighttime Lights',
      ticks: [0, 6, 13, 21, 29, 37, 45, 53, 61],
      gridlines: {color: 'transparent'}
    },
    bar: { gap: 1 },
    legend: { position: 'none' },
    colors: ['#525252']
  })
  
print(chart);  

Exercise

Exercise 01c

Exercise 01c

Open in Code Editor ↗

// Exercise

// The code now has a function createChart that creates a chart
// for the given year

// a) Change the name of the country to your chosen country
// b) Call the function to create histograms for the year 2010 and 2020
// c) Print the charts.

2.2 Image Scatter Chart

A scatter plot is useful to explore the relationship between 2 variables. In Earth Engine, you can extract the pixel values from an image using any of the sampling functions such as sample() or stratifiedSample() to get a FeatureCollection with pixel values for a random subset of the pixels. We can then plot the results using the built-in charting functions for FeatureCollections.

Here we use the Sentinel-2 Surface Reflectance dataset along with Global Surface Water Yearly Water History dataset to get reflectance values of water and non-water pixels within the chosen region. We then use the ui.Chart.feature.groups() function to plot the results. Note that you can explicitly set the desired chart type using the setChartType() function.

Below is the list of new styling options applied to the scatter plot:

  • titleTextStyle: Sets the style of the title text.
  • dataOpacity: Sets the transparency for the data points. Useful when you have overlapping data points.
  • pointShape: Sets the shape of the marker from the available marker shapes.
Scatter Plot

Scatter Plot

Open in Code Editor ↗

// We want to plot the relationship between
// 2 spectral bands for different classes

// Select a region
var geometry =   ee.Geometry.Polygon([[
  [76.816, 13.006],[76.816, 12.901],
  [76.899, 12.901],[76.899, 13.006]
]]);

// We use the Sentinel-2 SR data
var s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED');

// Add function for cloud masking
function maskS2clouds(image) {
  var qa = image.select('QA60');
  var cloudBitMask = 1 << 10;
  var cirrusBitMask = 1 << 11;
  var mask = qa.bitwiseAnd(cloudBitMask).eq(0).and(
             qa.bitwiseAnd(cirrusBitMask).eq(0));
  return image.updateMask(mask)
      .select('B.*')
      .multiply(0.0001)
      .copyProperties(image, ['system:time_start']);
}

// Filter and apply cloud mask
var filtered = s2
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 30))
  .filter(ee.Filter.date('2020-01-01', '2021-01-01'))
  .filter(ee.Filter.bounds(geometry))
  .map(maskS2clouds)
  .select('B.*');

// Create a composite
var composite = filtered.median();

var rgbVis = {bands: ['B4', 'B3', 'B2'], min: 0, max: 0.3, gamma: 1.2};
Map.centerObject(geometry, 12);
Map.addLayer(composite.clip(geometry), rgbVis, 'RGB');

// Use the Global Surface Water Yearly dataset
var gswYearly = ee.ImageCollection('JRC/GSW1_4/YearlyHistory');

// Extract the image for the chosen year
var filtered = gswYearly.filter(ee.Filter.eq('year', 2020));
var gsw2020 = ee.Image(filtered.first());

// Select permanent or seasonal water
var water = gsw2020.eq(3).or(gsw2020.eq(2)).rename('water');
var waterVis = {min:0, max:1, palette: ['white','blue']};
Map.addLayer(water.clip(geometry).selfMask(), waterVis, 'Water', false);

// We want to splot the relationship between 
// 'NIR' (B8) and 'GREEN' (B3) band reflectance
// for water and non-water pixels

// Select the bands
var bands = composite.select(['B8', 'B3']);

// Extract samples for both classes
var samples = bands.addBands(water).stratifiedSample({
  numPoints: 50,
  classBand: 'water',
  region: geometry,
  scale: 10})

print(samples.first());

// Create a chart and set the chart type
var chart = ui.Chart.feature.groups({
  features: samples,
  xProperty: 'B3',
  yProperty: 'B8',
  seriesProperty: 'water'
}).setChartType('ScatterChart');
  
print(chart);

// Customize the style
var chart = ui.Chart.feature.groups({
  features: samples,
  xProperty: 'B3',
  yProperty: 'B8',
  seriesProperty: 'water'
}).setChartType('ScatterChart')
  .setOptions({
    title: 'Relationship Among Spectral Values ' +
      'for Water and Non-Water Pixels',
    titleTextStyle: {bold: true},
    dataOpacity: 0.8,
    hAxis: {
      'title': 'Green reflectance',
      titleTextStyle: {italic: true},
    },
    vAxis: {
      'title': 'NIR Reflectance',
      titleTextStyle: {italic: true},

    },
    series: {
      0: {
        pointShape: 'triangle',
        pointSize: 4,
        color: '#2c7bb6',
        labelInLegend: 'Water',
        },
      1: {
        pointShape: 'triangle',
        pointSize: 4,
        color: '#f46d43',
        labelInLegend: 'Non-Water'
      }
    },
    legend: {position: 'in'}
    });
print(chart);

Exercise

Open in Code Editor ↗

// Exercise
// The code now contains a function createChart() that creates a scatter plot
// between the chosen bands

// a) Delete the 'geometry' and add a new polygon at your chosen location
// b) Create a chart for B3 and B11
// c) Print the chart.

2.3 Image Class Areas (Table)

Many analysts would want to create a chart or a table showing areas of different landcover classes in an image. The EE API has a dedicated function ui.Chart.Image.byClass() that can tabulate image pixel values by class.

Here we use the ESA Landcover 2021 dataset and create a Table chart of areas within the buffer zone of a location. Note that we are using setChartType() function with the option Table to create a table. Such tables are useful when you are creating apps and want to display a formatted table. The Global Population Explorer is a good example where you can switch between a Bar Chart and a Table to display the results.

Table Chart

Table Chart

Open in Code Editor ↗

// Select a region
var geometry = ee.Geometry.Point([77.6045, 12.8992]);

// We use the ESA WorldCover 2021 dataset
var worldcover = ee.ImageCollection('ESA/WorldCover/v200').first();

// The image has 11 classes
// Remap the class values to have continuous values
// from 0 to 10
var classified = worldcover.remap(
  [10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100],
  [0,  1 , 2,  3,  4,  5,  6,  7,  8,  9,  10]).rename('classification');

// Define a list of class names
var worldCoverClassNames= [
  'Tree Cover', 'Shrubland', 'Grassland', 'Cropland', 'Built-up',
  'Bare / sparse Vegetation', 'Snow and Ice', 
  'Permanent Water Bodies', 'Herbaceous Wetland', 
  'Mangroves', 'Moss and Lichen'];
// Define a list of class colors
var worldCoverPalette = [
  '006400', 'ffbb22', 'ffff4c', 'f096ff', 'fa0000',
  'b4b4b4', 'f0f0f0', '0064c8', '0096a0', '00cf75',
  'fae6a0'];
  
var visParams = {min:0, max:10, palette: worldCoverPalette};
Map.addLayer(classified, visParams, 'Landcover');

// We want to compute the class areas in a buffer zone
var bufferDistance = 1000;
var buffer = geometry.buffer(bufferDistance);
Map.centerObject(buffer, 12);
Map.addLayer(buffer, {color: 'gray'}, 'Buffer Zone');

// Create an area image and convert to Hectares
var areaImage = ee.Image.pixelArea().divide(1e5);

// Add the band containing classes
var areaImageWithClass = areaImage.addBands(classified);

// Create a chart
var chart = ui.Chart.image.byClass({
  image: areaImageWithClass,
  classBand: 'classification',
  region: buffer,
  reducer: ee.Reducer.sum(),
  scale: 10,
});
print(chart);

// Set the chart type and add styling options
var chart = ui.Chart.image.byClass({
  image: areaImageWithClass,
  classBand: 'classification',
  region: buffer,
  reducer: ee.Reducer.sum(),
  scale: 10,
  classLabels: worldCoverClassNames,
  xLabels: ['Area (Hectares)']
}).setChartType('Table');

print(chart);

Exercise

Open in Code Editor ↗

// Exercise

// a) Delete the 'geometry' and add a new point at your chosen location
// b) Change the buffer distance to 10km and Area units to Square Kilometers
// c) Print the chart.

3. FeatureCollection Charts

3.1 Image Class Areas (Pie Chart)

One of the biggest limitations of the GEE Charting API is that it cannot create charts from more than 10000000 pixels. While this may seem like a big number, you can easily run into this limit when working with images that cover large areas. If you try creating charts for large regions, you may run into an error such as below:

Image.reduceRegion: Too many pixels in the region. Found 159578190, but maxPixels allows only 10000000. Ensure that you are not aggregating at a higher resolution than you intended; that is a frequent cause of this error. If not, then you may set the ‘maxPixels’ argument to a limit suitable for your computation; set ‘bestEffort’ to true to aggregate at whatever scale results in ‘maxPixels’ total pixels; or both.

Chart Error

Chart Error

Fortunately, there is a way around it. Earth Engine allows you to aggregate values from very large regions using reducers that have an option to specify a maxPixels parameter. You will need to use the appropriate reducer and create a FeatureCollection with the results. The resulting value can then be plotted easily using the charting functions.

Here we take the same dataset as the previous section, but try to summarize the area by class over a much larger region. We use a Grouped Reducer to compute the class areas and post-process the result into a FeatureCollection. If you find the code hard to understand, please review our article on Calculating Area in Google Earth Engine for explanation.

We set the chart type to PieChart and plot the percentage of area of each class in the region.

Below is the list of new styling options applied to the pie chart:

  • pieSliceBorderColor: Sets the edge color of each pie slice.
  • pieSliceTextStyle: Sets the text style of pie slice labels.
  • pieSliceText: Sets the format of the text.
  • sliceVisibilityThreshold: Sets the threshold below which to group small slices into others category.
Pie Chart

Pie Chart

Open in Code Editor ↗

// Select a region
var geometry = ee.Geometry.Point([77.6045, 12.8992]);

// We use the ESA WorldCover 2021 dataset
var worldcover = ee.ImageCollection('ESA/WorldCover/v200').first();

// The image has 11 classes
// Remap the class values to have continuous values
// from 0 to 10
var classified = worldcover.remap(
  [10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100],
  [0,  1 , 2,  3,  4,  5,  6,  7,  8,  9,  10]).rename('classification');

// Define a list of class names
var worldCoverClassNames= [
  'Tree Cover', 'Shrubland', 'Grassland', 'Cropland', 'Built-up',
  'Bare / sparse Vegetation', 'Snow and Ice', 
  'Permanent Water Bodies', 'Herbaceous Wetland', 
  'Mangroves', 'Moss and Lichen'];
// Define a list of class colors
var worldCoverPalette = [
  '006400', 'ffbb22', 'ffff4c', 'f096ff', 'fa0000',
  'b4b4b4', 'f0f0f0', '0064c8', '0096a0', '00cf75',
  'fae6a0'];
// We define a dictionary with class names
var classNames = ee.Dictionary.fromLists(
  ['0','1','2','3','4','5','6','7','8','9', '10'],
  worldCoverClassNames
);
// We define a dictionary with class colors
var classColors = ee.Dictionary.fromLists(
  ['0','1','2','3','4','5','6','7','8','9', '10'],
  worldCoverPalette
);
var visParams = {min:0, max:10, palette: worldCoverPalette};
Map.addLayer(classified, visParams, 'Landcover');

// We want to compute the class areas in a buffer zone
var bufferDistance = 50000;
var buffer = geometry.buffer(bufferDistance);
Map.centerObject(buffer, 12);
Map.addLayer(buffer, {color: 'gray'}, 'Buffer Zone');

// Create an area image and convert to Hectares
var areaImage = ee.Image.pixelArea().divide(1e5);

// Add the band containing classes
var areaImageWithClass = areaImage.addBands(classified);

// As charting functions do not work on more than
// 10000000 pixels, we need to extract the areas using
// a reducer and create a FeatureCollection first

// Use a Grouped Reducer to calculate areas
var areas = areaImageWithClass.reduceRegion({
      reducer: ee.Reducer.sum().group({
      groupField: 1,
      groupName: 'classification',
    }),
    geometry: buffer,
    scale: 10,
    maxPixels: 1e10
    }); 
 
var classAreas = ee.List(areas.get('groups'))

// Process results to extract the areas and
// create a FeatureCollection
var classAreasList = classAreas.map(function(item) {
  var areaDict = ee.Dictionary(item)
  var classNumber = ee.Number(areaDict.get('classification')).format();
  var area = ee.Number(
    areaDict.get('sum'))
  return ee.List([classNumber, area])
})

var classAreasDict = ee.Dictionary(classAreasList.flatten());

var classList = classAreasDict.keys();
var classAreaFc = ee.FeatureCollection(classList.map(function(classNumber) {
  var classArea = classAreasDict.get(classNumber);
  var className = classNames.get(classNumber);
  var classColor = classColors.get(classNumber);
  return ee.Feature(null, {
    'class': classNumber,
    'class_name': className,
    'Area': classArea,
    'color': classColor
  });
}));
print('Class Area (FeatureCollection)', classAreaFc)

// We can now chart the resulting FeatureCollection
// If your area is large, it is advisable to first Export
// the featurecolleciton as an Asset and import it once
// the export is finished.
var chart = ui.Chart.feature.byFeature({
  features: classAreaFc,
  xProperty: 'class_name',
  yProperties: ['Area']
}).setChartType('PieChart')
.setOptions({
  title: 'Area by class',
})

print(chart);

// The pie colors do not match the class colors
// We need to create a list of colors for
// all the classes present in the FeatureCollection
var colors = classAreaFc.aggregate_array('color');
print(colors);

// The variable 'colors' is a server-side object
// Use evaluate() to convert it to client-side
// and use the results in the chart
colors.evaluate(function(colorlist) {
  // Let's create a Pie Chart
  var areaChart = ui.Chart.feature.byFeature({
    features: classAreaFc,
    xProperty: 'class_name',
    yProperties: ['Area']
  }).setChartType('PieChart')
    .setOptions({
      title: 'Area by class',
      colors: colorlist,
      pieSliceBorderColor: '#fafafa',
      pieSliceTextStyle: {'color': '#252525'}, 
      pieSliceText: 'percentage',
      sliceVisibilityThreshold: .10
  });
  print(areaChart); 
})

Exercise

Open in Code Editor ↗

/ Exercise

// a) Delete the 'geometry' and add a new point at your chosen location
// b) Modify the chart options to show one of the slices separated from the pie.
// c) Print the chart.

// Hint: Use the 'offset' property 
// https://developers.google.com/chart/interactive/docs/gallery/piechart#exploding-a-slice
Exercise 04c

Exercise 04c

3.2 FeatureCollection Column Chart

In the previous example, we used the ui.Chart.feature.byFeature() function to create a plot from the properties of each feature. There are fewer built-in functions to create different plots from FeatureCollections, but we can always use the GEE API to process our data and create a FeatureCollection to meet our requirements.

Here we take the WRI Global Power Plant Database and create a plot showing total installed capacity by fuel type for the chosen country. The FeatureCollection has one feature for each power plant, so we first need to process the collection to create one feature for each fuel type having a property with the total capacity. We use the a Grouped Reducer with the reduceColumns() function to calculate group statistics on a FeatureCollection.

We then use the ui.Chart.feature.byFeature() function to create a Bar Chart.

Google Charts uses the term Column Chart for a vertical bar chart, while the term Bar Chart is used for a horizonal bar chart.

Below is the list of new styling options applied to the column chart:

  • backgroundColor: Sets the background color for the whole chart.
Column Chart

Column Chart

Open in Code Editor ↗

// Use the WRI Global Power Plant Database
var table = ee.FeatureCollection('projects/sat-io/open-datasets/global_power_plant_DB_1-3');

// Select features for a country
var country = 'Germany';
var filtered = table
  .filter(ee.Filter.eq('country_long', country));
print(filtered.first());

// We want to calculate total installed capacity
// by each fuel type
// We use a Grouped Reducer to sum 'capacity_mw'
// values grouped by 'primary_fuel'
var stats = filtered.reduceColumns({
  selectors: ['capacity_mw', 'primary_fuel'],
  reducer: ee.Reducer.sum().setOutputs(['capacity_mw']).group({
      groupField: 1,
      groupName: 'primary_fuel',
    })
});

// Post-process the result into a FeatureCollection
var groupStats = ee.List(stats.get('groups'));
var groupFc = ee.FeatureCollection(groupStats.map(function(item) {
  return ee.Feature(null, item);
}));

// Create a chart
var chart = ui.Chart.feature.byFeature({
  features: groupFc,
  xProperty: 'primary_fuel',
  yProperties: ['capacity_mw']
}).setChartType('ColumnChart')
  .setOptions({
    title: 'Installed Power Generation Capacity by Fuel Type for ' + country,
    vAxis: {
      title: 'Capacity (MW)',
      format: 'short' 
    },
    hAxis: {
      title: 'Type of Fuel'},
    backgroundColor: '#feedde',
    colors: ['#d94801'],
    legend: { position: 'none' },
  });
print(chart);

Exercise

Open in Code Editor ↗

// a) Change the country name to your chosen country
// b) Sort the groupFc by 'capacity_mw' property so the bars are plotted
//    from largest to smallest values
// c) Print the chart

// Hint: Use the .sort() function
Exercise 02c

Exercise 02c

References

License

The workshop material (text, images, presentation, videos) is licensed under a Creative Commons Attribution 4.0 International License.

The code (scripts, Jupyter notebooks) is licensed under the MIT License. For a copy, see https://opensource.org/licenses/MIT

You are free to re-use and adapt the material but are required to give appropriate credit to the original author as below:

Copyright © 2023 Ujaval Gandhi www.spatialthoughts.com

Citing and Referencing

You can cite the course materials as follows