Visualizing Area Summaries

  • It can be difficult to easily summarize GEE imageCollections and images over an area in an interactive environment

  • geeViz provides two complementary approaches for area charting:

    1. Map-based charting (canAreaChart in Map.addLayer) — interactive charts inside the geeView map UI

    2. Inline charting (geeViz.outputLib.charts) — Plotly charts rendered directly in a notebook cell

  • This notebook demonstrates both approaches side-by-side for each example

Copyright 2026 Ian Housman

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

github github

#Import modules
try:
    import  geeViz.geeView as geeView
except:
    !python -m pip install geeViz
    import  geeViz.geeView as geeView

import geeViz.getImagesLib as gil
from geeViz.outputLib import charts as cl
import pandas as pd
from IPython.display import display, HTML
ee = geeView.ee
Map = geeView.Map
Map.clearMap()
print("done")
  • There are many different formats of data geeViz’s area charting module can handle

  • The most common is charting the percent or area in hectares or acres of thematic classes of an image collection

  • If an imageCollection has class_values, class_names, class_palette properties for its images, all charts will automatically be populated with those names and colors

  • First, we will look at these properties for a couple available images and image collections

lcms = ee.ImageCollection("USFS/GTAC/LCMS/v2024-10")
nlcd = ee.ImageCollection("USGS/NLCD_RELEASES/2021_REL/NLCD").select(['landcover'])
def get_props_dataFrames(props,bandNames = None):
    props = {k:i for k,i in props.items() if k.find('_class_')>-1}

    if bandNames == None:
        bandNames = list(set([k.split('_class_')[0] for k in props.keys()]))
    out = {}
    for bn in bandNames:
        print(bn)
        df = pd.DataFrame({'Values':props[f'{bn}_class_values'],
                           'Names':props[f'{bn}_class_names'],
                           'Colors':props[f'{bn}_class_palette']})
        display(df)
        out[bn]=df
    # return out
lcms_props = lcms.first().toDictionary().getInfo()
nlcd_props = nlcd.first().toDictionary().getInfo()


lcms_thematic_bandNames = lcms.select(['Change','Land_Cover','Land_Use']).first().bandNames().getInfo()

nlcd_landcover_bandNames = ['landcover']

get_props_dataFrames(lcms_props,lcms_thematic_bandNames)
get_props_dataFrames(nlcd_props,nlcd_landcover_bandNames)


# Shorten NLCD class names for later use
nlcd_class_names_key = f'{nlcd_landcover_bandNames[0]}_class_names'
nlcd_class_names_shortened = [nm.split(':')[0] for nm in nlcd_props[nlcd_class_names_key]]
nlcd_props_shortened_names = nlcd_props
nlcd_props_shortened_names[nlcd_class_names_key] = nlcd_class_names_shortened

print('Shortened NLCD class names:')
get_props_dataFrames(nlcd_props_shortened_names,nlcd_landcover_bandNames)

Define a Study Area for Inline Charts

  • The map-based charting uses the current map extent (or a selected feature) as the summary area

  • For the inline outputLib.charts examples we need to define an explicit geometry

  • This study area is reused throughout the notebook

# Study area: a rectangle in the western US (Colorado front range area)
study_area = ee.Geometry.Polygon(
    [[[-106.0, 39.5], [-105.0, 39.5], [-105.0, 40.5], [-106.0, 40.5], [-106.0, 39.5]]]
)
print('Study area defined')

Basic Area Charting

  • This example will show the most basic method for adding area chart layers

  • By setting "canAreaChart":True, the layer will be added to area charting. Available properties (shown above) will be used to set up names and colors

  • There are many options for area charting. Most of these options are provided as a dictionary using the areaChartParams key. This example shows two different chartTypes

  • Using the Map.turnOnAutoAreaCharting() method will turn on autmatic area charting. This will use the map extent as the summary area.

  • Additional methods are Map.turnOnSelectionAreaCharting() and Map.turnOnUserDefinedAreaCharting() for turning on different methods of providing areas to summarize. You can also change the method being used in the geeView UI under Tools -> Area Tools

# Map-based approach
Map.clearMap()


Map.addLayer(lcms.select(['Land_Cover']),{'autoViz':True,'canAreaChart':True,'areaChartParams':{'chartType':'stacked-line'}},'LCMS Land Cover')
Map.addLayer(nlcd.select(['landcover']),{'autoViz':True,'canAreaChart':True,'areaChartParams':{'chartType':'stacked-bar'}},'NLCD Land Cover')



Map.turnOnAutoAreaCharting()
Map.view()

Inline outputLib.charts equivalent

  • cl.summarize_and_chart auto-detects thematic data and uses frequencyHistogram

  • Returns both a pandas.DataFrame and a plotly.graph_objects.Figure

  • Use chart_type='stacked_bar' for a stacked bar chart

# Inline: Stacked time series of LCMS Land Cover
df, fig = cl.summarize_and_chart(
    lcms.select(['Land_Cover']),
    study_area,
    chart_type='stacked_line+markers',
    class_visible={'Non-Processing Area Mask':False},
    title='LCMS Land Cover'
)
fig.show()
display(df)
# Inline: Stacked bar chart of NLCD Land Cover
df, fig = cl.summarize_and_chart(
    nlcd.select(['landcover']),
    study_area,
    title='NLCD Land Cover',
    chart_type='stacked_bar',
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df)

Line and Sankey Charts

  • For thematic imageCollections, line and sankey charts are supported

  • You can specify one or both within the areaChartParams dictionary. By default, line is chosen. (e.g. "areaChartParams":{"sankey":True})

  • Note that sankey charts work well at showing transitions between classes. Since the number of transition classes is the number of classes2, if there are many classes, sankey charting will be quite slow and may error out.

  • For sankey charting, you can specify transition periods in the code using the sankeyTransitionPeriods key in areaChartParams (e.g. "sankeyTransitionPeriods":[[1985,1987],[2000,2002],[2020,2022]]), or leave it blank and geeViz will try to figure out a good set given the years of the provided imageCollection. Note that if you add imageCollections with different time extents, geeViz will take the intersection for the default years. You can add and change the periods in the geeView UI as well under Tools -> Area Tools -> Area Tools Parameters

# Map-based approach
Map.clearMap()

Map.addLayer(lcms.select(['Change']),{'autoViz':True,'canAreaChart':True,'areaChartParams':{'sankey':True}},'LCMS Change')

Map.addLayer(lcms.select(['Land_Cover']),{'autoViz':True,'canAreaChart':True,'areaChartParams':{'sankey':True}},'LCMS Land Cover')

Map.addLayer(lcms.select(['Land_Use']),{'autoViz':True,'canAreaChart':True,'areaChartParams':{'sankey':True}},'LCMS Land Use')


Map.turnOnAutoAreaCharting()
Map.view()

Inline outputLib.charts equivalent

  • Set chart_type='sankey' and provide transition_periods as a list of [start_year, end_year] pairs

  • min_percentage filters out small flows for readability

# Inline: Sankey diagram of LCMS Change transitions
sankey_df, sankey_fig, matrix_dict = cl.summarize_and_chart(
    lcms.select(['Change']),
    study_area,
    chart_type='sankey',
    transition_periods=[1985,1990,2000,2010,2024],
    title='LCMS Change Transitions',
    min_percentage=0.5,
)
display(sankey_fig)
display(sankey_df)
print('Transition matrices:')
for label, mdf in matrix_dict.items():
    print(f"\n{label}")
    display(mdf)
# Inline: Sankey diagram of LCMS Land Cover transitions
sankey_df, sankey_fig, matrix_dict = cl.summarize_and_chart(
    lcms.select(['Land_Cover']),
    study_area,
    chart_type='sankey',
    transition_periods=[1985, 1990, 2000, 2005, 2018, 2023],
    title='LCMS Land Cover Transitions',
    min_percentage=0.5,
)
display(HTML(sankey_fig.to_html(full_html=False, include_plotlyjs="cdn")))
print('Source-Target-Value table:')
display(sankey_df)
print('Transition matrices:')
for label, mdf in matrix_dict.items():
    print(f"\n{label}")
    display(mdf)
# Inline: Sankey diagram of LCMS Land Use transitions
sankey_df, sankey_fig, matrix_dict = cl.summarize_and_chart(
    lcms.select(['Land_Use']),
    study_area,
    chart_type='sankey',
    transition_periods=[1985, 1990, 2005, 2010, 2018, 2023],
    title='LCMS Land Use Transitions',
    min_percentage=0.5,
)
display(HTML(sankey_fig.to_html(full_html=False, include_plotlyjs="cdn")))
print('Source-Target-Value table:')
display(sankey_df)
print('Transition matrices:')
for label, mdf in matrix_dict.items():
    print(f"\n{label}")
    display(mdf)

  • Adding a layer to line and sankey charting is done as follows

  • Any time you specify 'line':True,'sankey':True, both line and sankey charts will be created

  • This can slow things down a lot if many layers are visible. Also, moving the map, pausing for a second or two, and then moving the map again, many times, can create a long queue and delay charting.

# Map-based approach: both line and sankey
Map.clearMap()

Map.addLayer(lcms.select(['Change']),{'autoViz':True,'canAreaChart':True,'areaChartParams':{'line':True,'sankey':True}},'LCMS Change')

Map.addLayer(lcms.select(['Land_Cover']),{'autoViz':True,'canAreaChart':True,'areaChartParams':{'line':True,'sankey':True}},'LCMS Land Cover')

Map.addLayer(lcms.select(['Land_Use']),{'autoViz':True,'canAreaChart':True,'areaChartParams':{'line':True,'sankey':True}},'LCMS Land Use')


Map.turnOnAutoAreaCharting()
Map.view()

Inline outputLib.charts equivalent — both line and sankey

  • With outputLib.charts, you simply make two separate calls — one for the time series and one for the sankey

# Inline: both time series and sankey for LCMS Land Cover

# Time series
df, fig = cl.summarize_and_chart(
    lcms.select(['Land_Cover']),
    study_area,
    title='LCMS Land Cover (time series)',
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))

# Sankey
sankey_df, sankey_fig, matrix_dict = cl.summarize_and_chart(
    lcms.select(['Land_Cover']),
    study_area,
    chart_type='sankey',
    transition_periods=[1985, 1990, 2005, 2010, 2018, 2023],
    title='LCMS Land Cover (sankey)',
    min_percentage=0.5,
)
display(HTML(sankey_fig.to_html(full_html=False, include_plotlyjs="cdn")))
print('Transition matrices:')
for label, mdf in matrix_dict.items():
    print(f"\n{label}")
    display(mdf)

Ways of adding area charting layers

  • There are two methods for adding area chart layers. The first is shown above, where when adding a layer using Map.addLayer, specify "canAreaChart":True. With this method, if a layer is visible, it will be included in area charting.

  • There are instances where you would like to summarize layers, but may not want them on the map or it is impossible to visualize all the thematic bands you’d like to chart. In this instance, you can use the Map.addAreaChartLayer method.

  • If you use the Map.addAreaChartLayer method, you will need to use the Map.populateAreaChartLayerSelect() method to instantiate a selection menu for choosing which area chart layers should be charted.

  • In this example, we will summarize all thematic classes in LCMS in a single graph. This cannot be displayed on a map, but is an interesting way to look at the summarized data in charts

  • Note that the dictionary of parameters is more or less the same as what you would put in the "areaChartParams" if you were to use the Map.addLayer method.

  • Note that while multi thematic band image collections can be charted in a single line chart, sankey charts can only support one band per chart. If a multi thematic band image collection is given with "sankey":True (as is the case in this example), separate sankey charts will be created for each band.

# Map-based approach
Map.clearMap()

Map.addLayer(lcms.select(['Change_Raw_Probability.*']),{'reducer':ee.Reducer.stdDev(),'min':0,'max':10},'LCMS Change Prob')

Map.addAreaChartLayer(lcms,{'line':True},'LCMS All Thematic Classes Line',False)

Map.addAreaChartLayer(lcms,{'sankey':True},'LCMS All Thematic Classes Sankey',True)

Map.populateAreaChartLayerSelect()
Map.turnOnAutoAreaCharting()

Map.view()

Inline outputLib.charts equivalent

  • outputLib.charts can chart all thematic bands at once — just select multiple bands

  • Column names are automatically prefixed with the band name when multiple bands are present

# Inline: All LCMS thematic bands in one chart
df, fig = cl.summarize_and_chart(
    lcms.select(['Change', 'Land_Cover', 'Land_Use']),
    study_area,
    title='LCMS All Thematic Classes',
    chart_type='line+markers',
    line_width=1,
    width=800,
    height=500,
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df.head())

Charting Non-Thematic Data

  • You can chart continuous data as well. By default, a ee.Reducer.mean() will be used. You can use any reducer that returns a single value per image-band (e.g. ee.Reducer.min(), ee.Reducer.max(), ee.Reducer.stdDev(), ee.Reducer.mode(), and not ee.Reducer.percentile([0,50,100])).

  • You can specify this using "areaChartParams":{"reducer":ee.Reducer.mean()}

  • Optionally, you can provide a color palette to be used. Each band will be assigned to a color in the order given

  • Notice in the example, the reducer for what is shown on the map is different from the zonal summary reducer. In this example, on the map the standard deviation of the probability is shown to highlight likely change, while the average over the area is shown in the chart since that is a more appropriate representation of probability over an area.

# Map-based approach
Map.clearMap()

Map.addLayer(lcms.select(['Change_Raw_Probability.*']),
             {'reducer':ee.Reducer.stdDev(),'min':0,'max':10,'canAreaChart':True,'areaChartParams':{'palette':'f39268,d54309,00a398'}},'LCMS Change Prob')


Map.turnOnAutoAreaCharting()
Map.view()

Inline outputLib.charts equivalent

  • outputLib.charts auto-detects continuous data (no class properties) and defaults to ee.Reducer.mean()

  • For non-thematic data, chart_type='line+markers gives a multi-line chart

# Inline: Continuous data — LCMS change probability bands
df, fig = cl.summarize_and_chart(
    lcms.select(['Change_Raw_Probability_Slow_Loss',
                 'Change_Raw_Probability_Fast_Loss',
                 'Change_Raw_Probability_Gain']),
    study_area,
    title='LCMS Change Probabilities (Mean)',
    chart_type = 'line+markers',
    marker_size=5,
    line_width=2,
    palette=['f39268', 'd54309', '00a398'],
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df)

Charting Images

  • You can also chart images

  • It will behave in a similar fashion to imageCollections, but will show a bar chart

  • Placing names for bar charts can be challenging if the names are very long. geeViz will automatically change the bar chart to be a horizontal bar chart if long names are detected. This still does not ensure the bar charts are readable (as is the case in this example). Shortening class names is the easiest method to address this issue (shown by splitting the full NLCD landcover name with : and take the first part earlier in this notebook).

  • If using "autoViz":True, be sure to copy the _class_ properties back in

# Map-based approach
Map.clearMap()


Map.addLayer(lcms.select(['Land_Cover']),{'autoViz':True,'canAreaChart':True},'LCMS Land Cover')
Map.addLayer(lcms.select(['Land_Cover']).mode().set(lcms.first().toDictionary()),{'autoViz':True,'canAreaChart':True},'LCMS Land Cover Mode')

Map.addLayer(nlcd.select(['landcover']),{'autoViz':True,'canAreaChart':True},'NLCD Land Cover')
Map.addLayer(nlcd.select(['landcover']).mode().set(nlcd.first().toDictionary()),{'autoViz':True,'canAreaChart':True},'NLCD Land Cover Mode')

# Use the shortened class names to clean up chart
Map.addLayer(nlcd.select(['landcover']).mode().set(nlcd_props_shortened_names),{'autoViz':True,'canAreaChart':True},'NLCD Land Cover Mode Shortened Names')





Map.turnOnAutoAreaCharting()
Map.view()

Inline outputLib.charts equivalent

  • For a single ee.Image, outputLib.charts produces a bar chart

  • It auto-detects horizontal vs vertical orientation based on label length

# Inline: Bar chart for LCMS Land Cover mode image
lc_mode = lcms.select(['Land_Cover']).mode().set(lcms.first().toDictionary())
df, fig = cl.summarize_and_chart(
    lc_mode,
    study_area,
    title='LCMS Land Cover Mode',
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df)
# Inline: Bar chart for NLCD landcover mode with original (long) names
# Notice how the long class names make the chart hard to read — compare with the shortened version below
nlcd_mode_long = nlcd.select(['landcover']).mode().set(nlcd.first().toDictionary())
df, fig = cl.summarize_and_chart(
    nlcd_mode_long,
    study_area,
    title='NLCD Land Cover Mode (Original Names)',
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df)
# Inline: Bar chart for NLCD landcover mode with shortened names
nlcd_mode = nlcd.select(['landcover']).mode().set(nlcd_props_shortened_names)
df, fig = cl.summarize_and_chart(
    nlcd_mode,
    study_area,
    title='NLCD Land Cover Mode (Shortened Names)',
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df)

Charting Images Without Color and Name Properties

  • Sometimes you will have an image you add to the map with a min, max, and palette in the viz params that are thematic or ordinal thematic data. If those images have no class_names, etc properties set, geeViz will try to render the chart properly using the given min, max, and palette if you specify "areaChartParams":{"reducer":ee.Reducer.frequencyHistogram()}

# Map-based approach
Map.clearMap()

def getMostRecentChange(c, code):
    def wrapper(img):
        yr = ee.Date(img.get("system:time_start")).get("year")
        return (
            ee.Image(yr)
            .int16()
            .rename(["year"])
            .updateMask(img.eq(code))
            .copyProperties(img, ["system:time_start"])
        )

    return c.map(wrapper)

mostRecentFastLossYear = getMostRecentChange(lcms.select(['Change']),3).max()

Map.addLayer(mostRecentFastLossYear, {'min':1985,'max':2023,'palette':["ffffe5", "fff7bc", "fee391", "fec44f", "fe9929", "ec7014", "cc4c02"],'canAreaChart':True,'areaChartParams':{'reducer':ee.Reducer.frequencyHistogram()}}, "Most Recent Fast Loss Year", True)

Map.turnOnAutoAreaCharting()
Map.view()

Inline outputLib.charts equivalent

  • Pass reducer=ee.Reducer.frequencyHistogram() to force thematic treatment

  • Without class properties, class values are used as labels

# Inline: Force frequencyHistogram for a non-class image
df, fig = cl.summarize_and_chart(
    mostRecentFastLossYear,
    study_area,
    reducer=ee.Reducer.frequencyHistogram(),
    title='Most Recent Fast Loss Year',
    palette=["ffffe5", "fff7bc", "fee391", "fec44f", "fe9929", "ec7014", "cc4c02"],
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df)

Charting Time Lapses

  • Time lapses also support area charting. All functionality is the same as the Map.addLayer methods.

  • Band names can be specified in the areaChartParams. If they are not specified, but bands is specified in the visualization parameters, those bands will be used instead. Otherwise, all bands will be shown.

# Map-based approach
Map.clearMap()

composites = ee.ImageCollection("projects/lcms-tcc-shared/assets/CONUS/Composites/Composite-Collection-yesL7")

years = list(range(1985,2024))

# Need to mosaic the tiled outputs for each year
composites = [composites.filter(ee.Filter.calendarRange(yr,yr,'year')).mosaic().set('system:time_start',ee.Date.fromYMD(yr,6,1).millis()) for yr in years]
composites = ee.ImageCollection(composites)

# Set up visualization parameters
viz = gil.vizParamsFalse10k
viz['canAreaChart']=True
viz['areaChartParams']={'bandNames':'blue,green,red,nir,swir1,swir2','palette':'00D,0D0,D00,D0D,0DD'}
Map.addTimeLapse(composites,gil.vizParamsFalse10k,'Composites')

Map.turnOnAutoAreaCharting()
Map.view()

Inline outputLib.charts equivalent

  • Use band_names to select specific bands for a continuous time series

# Inline: Composite time series of spectral bands
df, fig = cl.summarize_and_chart(
    composites,
    study_area,
    band_names=['blue', 'green', 'red', 'nir', 'swir1', 'swir2'],
    title='Composite Band Means Over Time',
    palette=['00D', '0D0', 'D00', 'D0D', '0DD', 'DD0'],
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df.head())

Charting Thematic Data without set properties

  • You can chart thematic datasets that lack values, names, and palette properties by specifying the ee.Reducer.frequencyHistogram() as the reducer

  • This is not the best method however. Charts will lack descriptive class names and colors

# Map-based approach: LCMAP example
Map.clearMap()


lcpri = ee.ImageCollection("projects/sat-io/open-datasets/LCMAP/LCPRI").select(['b1'],['LC'])


Map.addTimeLapse(lcpri,{'min':1,'max':9,'canAreaChart':True,"areaChartParams":{'reducer':ee.Reducer.frequencyHistogram()}},'LCMAP LC Primary')

Map.turnOnAutoAreaCharting()
Map.view()

Inline outputLib.charts equivalent

# Inline: LCMAP without class properties — just uses class values as labels
df, fig = cl.summarize_and_chart(
    lcpri.filterBounds(study_area),
    study_area,
    reducer=ee.Reducer.frequencyHistogram(),
    title='LCMAP LC Primary (no class properties)',
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df.head())

Setting properties for charting

  • The easiest way to chart thematic data where each class has a number, name, and color, but lack the preset properties, is to set them on-the-fly

  • These properties can then be used for charting and map rendering

# Map-based approach: LCMAP with manually set properties
Map.clearMap()

lcpri_palette = ['E60000','A87000','E3E3C2','1D6330','476BA1','BAD9EB','FFFFFF','B3B0A3','A201FF']
lc_names = ['Developed','Cropland','Grass/Shrub','Tree Cover','Water','Wetlands','Ice/Snow','Barren','Class Change']
lc_numbers = list(range(1,len(lcpri_palette)+1))

lcpri = ee.ImageCollection("projects/sat-io/open-datasets/LCMAP/LCPRI").select(['b1'],['LC'])

lcpri = lcpri.map(lambda img:img.set({'LC_class_values':lc_numbers,'LC_class_names':lc_names,'LC_class_palette':lcpri_palette}))


Map.addTimeLapse(lcpri,{'autoViz':True,'canAreaChart':True,'areaChartParams':{'line':True,'sankey':True}},'LCMAP LC Primary')

Map.turnOnAutoAreaCharting()
Map.view()

Inline outputLib.charts equivalent

# Inline: LCMAP with class properties — time series
df, fig = cl.summarize_and_chart(
    lcpri,
    study_area,
    title='LCMAP LC Primary',
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df.head())
# Inline: LCMAP sankey
sankey_df, sankey_fig, matrix_dict = cl.summarize_and_chart(
    lcpri,
    study_area,
    chart_type='sankey',
    transition_periods=[1985, 1990, 2000, 2005, 2018, 2021],
    title='LCMAP LC Primary Transitions',
    min_percentage=0.5,
)
display(HTML(sankey_fig.to_html(full_html=False, include_plotlyjs="cdn")))
print('Source-Target-Value table:')
display(sankey_df)
print('Transition matrices:')
for label, mdf in matrix_dict.items():
    print(f"\n{label}")
    display(mdf)

Comparing map output versions

  • One common task is to understand the differences between 2 or more model runs. This can be challenging.

  • This example will show three versions of LCMS products and how they relate

  • This approach makes comparing the maps and their respective class counts relatively easy

  • This idea could be adapted to comparing different thematic or continuous outputs

# Map-based approach
Map.clearMap()

# Bring in 3 different LCMS versions
lcms_2020 = ee.ImageCollection("USFS/GTAC/LCMS/v2020-5").filter('study_area=="CONUS"')
lcms_2021 = ee.ImageCollection("USFS/GTAC/LCMS/v2021-7").filter('study_area=="CONUS"')
lcms_2022 = ee.ImageCollection("USFS/GTAC/LCMS/v2022-8").filter('study_area=="CONUS"')
lcms_2023 = ee.ImageCollection("USFS/GTAC/LCMS/v2023-9").filter('study_area=="CONUS"')

# Choose a year to compare (any year 1985-2020)
year = 2010

# Filter off the image and set that image to the version year
lcms_2020 = lcms_2020.filter(ee.Filter.calendarRange(year,year,'year')).first().set({'year':2020,'system:time_start':ee.Date.fromYMD(2020,6,1).millis()})
lcms_2021 = lcms_2021.filter(ee.Filter.calendarRange(year,year,'year')).first().set({'year':2021,'system:time_start':ee.Date.fromYMD(2021,6,1).millis()})
lcms_2022 = lcms_2022.filter(ee.Filter.calendarRange(year,year,'year')).first().set({'year':2022,'system:time_start':ee.Date.fromYMD(2022,6,1).millis()})
lcms_2023 = lcms_2023.filter(ee.Filter.calendarRange(year,year,'year')).first().set({'year':2023,'system:time_start':ee.Date.fromYMD(2023,6,1).millis()})

# Construct the image collection
c = ee.ImageCollection([lcms_2020,lcms_2021,lcms_2022,lcms_2023])

# Add the collection as a timelapse
# Will need to specify the transition years as the years used for each image, otherwise geeViz will default to only showing the first and last year
# Note that if you specify the sankey transition periods, any periods you enter in the geeViz UI will not be used for that layer
for bn in ['Change','Land_Cover','Land_Use']:
  Map.addTimeLapse(c.select([bn]),{'autoViz':True,'canAreaChart':True,'areaChartParams':{'sankey':True,
                                                                            'sankeyTransitionPeriods':[[2020,2020],[2021,2021],[2022,2022],[2023,2023]]
                                                                            }}, f"LCMS {bn.replace('_',' ')} Comparison {year}")

Map.turnOnAutoAreaCharting()
Map.view()

Inline outputLib.charts equivalent

# Inline: Version comparison — time series + sankey
for bn in ['Change', 'Land_Cover', 'Land_Use']:
    # Time series
    df, fig = cl.summarize_and_chart(
        c.select([bn]),
        study_area,
        title=f'LCMS {bn.replace("_", " ")} Comparison {year}',
    )
    display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))

    # Sankey
    sankey_df, sankey_fig, matrix_dict = cl.summarize_and_chart(
        c.select([bn]),
        study_area,
        chart_type='sankey',
        transition_periods=[2020, 2021, 2022, 2023],
        title=f'LCMS {bn.replace("_", " ")} Version Transitions {year}',
        min_percentage=0.5,
    )
    display(HTML(sankey_fig.to_html(full_html=False, include_plotlyjs="cdn")))
    print(f'Transition matrices for {bn}:')
    for label, mdf in matrix_dict.items():
        print(f"\n{label}")
        display(mdf)

Other charting summary zone selection methods

  • All examples have simply used the map extent as the zone to chart

  • There are other methods available

  • This example will show how to add a featureCollection to interactively select areas to summarize

  • All area selection will happen in the geeViz UI, under Tools -> Area Tools -> Select an Area on Map

  • This example also demonstrates the use of the stacked bar chartType and turning off the first and last MTBS burn severity classes in the chart

# Map-based approach
Map.clearMap()


for bn in ['Change','Land_Cover','Land_Use']:
  Map.addLayer(lcms.select([bn]),{'autoViz':True,'canAreaChart':True}, f"LCMS {bn.replace('_',' ')}  ")


# Bring in MTBS burn boundaries
mtbsBoundaries = ee.FeatureCollection("USFS/GTAC/MTBS/burned_area_boundaries/v1")
mtbsBoundaries = mtbsBoundaries.map(
    lambda f: f.set("system:time_start", f.get("Ig_Date"))
)
# Bring in MTBS burn severity mosaics
mtbsSeverity = ee.ImageCollection("USFS/GTAC/MTBS/annual_burn_severity_mosaics/v1").filter(ee.Filter.stringContains('system:index','_CONUS_')).select([0],['Severity'])
Map.addLayer(mtbsSeverity,{'autoViz':True,'canAreaChart':True,'areaChartParams':{'chartType':'stacked-bar',"visible": [False, True, True, True, True, True, False],}}, "MTBS Burn Severity")

# For area charting you can select areas to chart a number of ways. One method is using a map layer that is selectable by clicking on each feature.
Map.addSelectLayer(
    mtbsBoundaries,
    {
        "strokeColor": "00F",
        "selectLayerNameProperty": "Incid_Name",
    },
    "MTBS Fire Boundaries"
)

Map.turnOnSelectionAreaCharting()
Map.view()

Inline outputLib.charts equivalent

  • With outputLib.charts you can pass any ee.FeatureCollection as the geometry

  • For a single-feature FC (e.g. a state boundary), it behaves like a single geometry

  • This is useful for scripted, reproducible analyses where you want to specify the exact zone

  • Below we chart all LCMS thematic bands and MTBS Burn Severity over Colorado

# Inline: LCMS thematic bands summarized over Colorado
co = ee.FeatureCollection('TIGER/2018/States').filter(ee.Filter.eq('NAME', 'Colorado'))

for bn in ['Change', 'Land_Cover', 'Land_Use']:
    df, fig = cl.summarize_and_chart(
        lcms.select([bn]),
        co,
        title=f'LCMS {bn.replace("_", " ")} \u2014 Colorado',
    )
    display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
# Inline: MTBS Burn Severity stacked bar chart over Colorado
mtbsSeverity = ee.ImageCollection("USFS/GTAC/MTBS/annual_burn_severity_mosaics/v1").filter(ee.Filter.stringContains('system:index', '_CONUS_')).select([0],['Severity'])

df, fig = cl.summarize_and_chart(
    mtbsSeverity,
    co,
    title='MTBS Burn Severity \u2014 Colorado',
    chart_type='bar',
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df.head())

Multi-Feature reduceRegions (outputLib.charts only)

  • When you pass a multi-feature ee.FeatureCollection as the geometry and set feature_label to the name of a feature property, outputLib.charts uses reduceRegions to compute per-feature statistics in a single server call

  • The result is a grouped bar chart where each group is a feature (fire, state, watershed, etc.) and each bar within the group is a class

  • This is far more efficient than looping over features individually

  • In this example we find the 5 largest MTBS fires and summarize both LCMS Land Cover and Annual NLCD over each fire perimeter

# Get the 5 largest MTBS fire perimeters by burned area (BurnBndAc)
mtbs_perims = ee.FeatureCollection("USFS/GTAC/MTBS/burned_area_boundaries/v1")
top5_fires = mtbs_perims.filter(ee.Filter.notNull(["BurnBndAc", "Ig_Year", "Incid_Name"])).sort("BurnBndAc", False).limit(5)

# Shorten fire names for readability
top5_fires = top5_fires.map(
    lambda f: f.set("Fire", ee.String(f.get("Incid_Name")).cat(" (").cat(ee.Number(f.get("Ig_Year")).format("%d")).cat(")")))

print("5 largest MTBS fires:")
for feat in top5_fires.getInfo()["features"]:
    p = feat["properties"]
    fire_name = p.get("Fire", p.get("Incid_Name"))
    acres = p["BurnBndAc"]
    print(f"  {fire_name}: {acres:,.0f} acres")
# LCMS Land Cover mode over the 5 largest fires (reduceRegions)
lc_mode = lcms.select(["Land_Cover"]).mode().set(lcms.first().toDictionary())

df, fig = cl.summarize_and_chart(
    lc_mode,
    top5_fires,
    feature_label="Fire",
    title="LCMS Land Cover Mode — 5 Largest MTBS Fires",
    width=800,
    height=450,
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df)
# Annual NLCD landcover mode over the 5 largest fires (reduceRegions)
nlcd_mode_fires = nlcd.select(["landcover"]).mode().set(nlcd_props_shortened_names)

df, fig = cl.summarize_and_chart(
    nlcd_mode_fires,
    top5_fires,
    feature_label="Fire",
    title="NLCD Land Cover Mode — 5 Largest MTBS Fires",
    width=800,
    height=450,
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))
display(df)

Area Format Options (outputLib.charts only)

  • By default, thematic charts show percentages

  • You can switch to hectares, acres, or raw pixel counts using the area_format parameter

# Inline: Same data in hectares
df_ha, fig_ha = cl.summarize_and_chart(
    lcms.select(['Land_Cover']),
    study_area,
    area_format='Hectares',
    title='LCMS Land Cover (Hectares)'
)
display(HTML(fig_ha.to_html(full_html=False, include_plotlyjs="cdn")))

# And in acres
df_ac, fig_ac = cl.summarize_and_chart(
    lcms.select(['Land_Cover']),
    study_area,
    area_format='Acres',
    title='LCMS Land Cover (Acres)'
)
display(HTML(fig_ac.to_html(full_html=False, include_plotlyjs="cdn")))

Using the Lower-Level Functions (outputLib.charts only)

  • summarize_and_chart is a convenience wrapper

  • You can also call the lower-level functions directly for more control:

    • cl.get_obj_info() — inspect the object type and class metadata

    • cl.zonal_stats() — get just the DataFrame (no chart)

    • cl.chart_time_series() / cl.chart_bar() / cl.chart_sankey() — build charts from DataFrames

# Step-by-step: inspect, compute stats, then chart
lc = lcms.select(['Land_Cover'])

# 1. Inspect the object
info = cl.get_obj_info(lc)
print('Object info:')
for k, v in info.items():
    if k != 'class_info':
        print(f'  {k}: {v}')
print(f'  class_info bands: {list(info["class_info"].keys())}')

# 2. Compute zonal stats (returns just the DataFrame)
df = cl.zonal_stats(lc, study_area, area_format='Percentage')
print(f'\nDataFrame shape: {df.shape}')
display(df.head())

# 3. Build chart from the DataFrame
colors = info['class_info']['Land_Cover']['class_palette']
fig = cl.chart_time_series(
    df,
    colors=colors,
    title='LCMS Land Cover (step-by-step)',
    y_label='% Area'
)
display(HTML(fig.to_html(full_html=False, include_plotlyjs="cdn")))