Summary Areas, Thumbnails, and Charting with geeViz¶
geeViz.getSummaryAreasLibprovides functions to retrieve common summary and study areaee.FeatureCollectionobjectsgeeViz.thumbLibgenerates automatic thumbnails from Earth Engine data with auto-visualization detectiongeeViz.chartingLibsummarizes and charts EE data with automatic thematic/continuous detectiongeeViz.reportLibcombines all three into styled HTML reports with LLM-generated narrativesEvery function accepts an
areaparameter (ee.Geometry,ee.Feature, oree.FeatureCollection) and returns spatially filtered resultsThis notebook demonstrates each module’s capabilities
Datasets used: LCMS, Annual NLCD, MTBS, and Sentinel-2 composites via
superSimpleGetS2
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.
# Boilerplate imports
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 geeViz.getSummaryAreasLib as sal
from geeViz.outputLib import thumbs as tl
from geeViz.outputLib import reports as rl
ee = geeView.ee
Map = geeView.Map
Map.clearMap()
print('Imports complete')
from IPython.display import HTML, display
Define a Study Area¶
We’ll use a broad area centered on the Wasatch Front in Utah
This area intersects national forests, urban areas, multiple counties, and protected lands
All
getSummaryAreasLibfunctions will filter to features intersecting this area
# Study area: point near Salt Lake City with an 80km buffer
study_area = ee.Geometry.Point([-111.9, 40.7]).buffer(80000)
print('Study area defined')
Load Datasets¶
We load several datasets that will be summarized across the different summary areas throughout the notebook
LCMS: Land cover, land use, and change (1985-2024)
Annual NLCD: Annual National Land Cover Database (1985-2024)
MTBS: Monitoring Trends in Burn Severity
Global Land Cover: GLC-FCS30D annual land cover
Sentinel-2: Cloud-free composites via
superSimpleGetS2
# LCMS
lcms = ee.ImageCollection('USFS/GTAC/LCMS/v2024-10')
# Annual NLCD (preferred over official NLCD releases — annual data from 1985-2024)
nlcd = ee.ImageCollection('projects/sat-io/open-datasets/USGS/ANNUAL_NLCD/LANDCOVER')
nlcd_viz_props = {
'LC_class_values': [11,12,21,22,23,24,31,41,42,43,52,71,81,82,90,95],
'LC_class_palette': ['466b9f','d1def8','dec5c5','d99282','eb0000','ab0000','b3ac9f','68ab5f','1c5f2c','b5c58f','ccb879','dfdfc2','dcd939','ab6c28','b8d9eb','6c9fb8'],
'LC_class_names': ['Open Water','Perennial Ice/Snow','Developed, Open Space','Developed, Low Intensity','Developed, Medium Intensity','Developed, High Intensity','Barren Land','Deciduous Forest','Evergreen Forest','Mixed Forest','Shrub/Scrub','Grassland/Herbaceous','Pasture/Hay','Cultivated Crops','Woody Wetlands','Emergent Herbaceous Wetlands'],
}
nlcd = nlcd.map(lambda img: img.rename('LC').set(nlcd_viz_props))
# MTBS burn severity (CONUS mosaics) - select by index and rename for consistent band naming
mtbs = ee.ImageCollection('USFS/GTAC/MTBS/annual_burn_severity_mosaics/v1').select([0], ['Severity'])
# Sentinel-2 unfiltered collection (will be filtered to study areas later)
s2 = gil.superSimpleGetS2(study_area, '2023-06-01', '2023-09-30')
print('Datasets loaded')
1. Countries (getAdminBoundaries(level=0))¶
Returns country boundaries from geoBoundaries v6 (default), FAO GAUL, or FieldMaps
getAdminBoundariessupports levels 0–4: countries, states/provinces, districts/counties, sub-districts, localitiesUse
getAdminNameProperty(level, source)to get the name column for any source
countries = sal.getAdminBoundaries(study_area, level=0)
name_prop = sal.getAdminNameProperty(level=0)
print('Countries intersecting study area:')
print(countries.aggregate_array(name_prop).getInfo())
# Also try GAUL source — different name property
countries_gaul = sal.getAdminBoundaries(study_area, level=0, source='gaul')
gaul_name = sal.getAdminNameProperty(level=0, source='gaul')
print(f'Countries (GAUL, prop={gaul_name}):', countries_gaul.aggregate_array(gaul_name).getInfo())
2. States/Provinces (getAdminBoundaries(level=1))¶
Returns state or province boundaries
Default source is geoBoundaries v6; also supports GAUL, GAUL 2024, and FieldMaps
Use
getAdminNamePropertyto get the correct name column for any source
states = sal.getAdminBoundaries(study_area, level=1)
name_prop = sal.getAdminNameProperty(level=1)
print(f'States/provinces (prop={name_prop}):', states.aggregate_array(name_prop).getInfo())
3. US States (getUSStates)¶
Returns US state boundaries from TIGER 2018
Includes
NAME,STUSPS(abbreviation),STATEFP(FIPS)
us_states = sal.getUSStates(study_area)
print('US States:', us_states.aggregate_array('NAME').getInfo())
4. US Counties (getUSCounties) + LCMS Land Use Charting¶
Returns US county boundaries with
FULL_NAME,STATEFP,STUSPSOptional
state_fipsorstate_abbrfiltersWe’ll use counties as summary zones for LCMS Land Use with area charting on the map
summarize_and_chartwithfeature_label+ee.ImageCollectionproduces per-feature time series subplotsAlso shows how to use getAdminBoundaries which is more versatile - works outside the US
# Get Utah counties in the study area
counties = sal.getUSCounties(study_area, state_abbr='UT')
print('Utah counties:', counties.aggregate_array('FULL_NAME').getInfo())
counties2 = sal.getAdminBoundaries(study_area, level = 2)
print('Utan counties using getAdminBoundaries:',counties2.aggregate_array('shapeName').getInfo())
# Map: LCMS Land Use with counties as selectable areas
Map.clearMap()
Map.addLayer(lcms.select(['Land_Use']), {
'autoViz': True,
'canAreaChart': True,
'areaChartParams': {'line': True, 'sankey': True}
}, 'LCMS Land Use')
Map.addLayer(lcms.select(['Land_Cover']), {
'autoViz': True,
'canAreaChart': True,
'areaChartParams': {'line': True}
}, 'LCMS Land Cover', False)
Map.addSelectLayer(counties, {
'strokeColor': '00F',
'selectLayerNameProperty': 'FULL_NAME'
}, 'Utah Counties')
Map.turnOnSelectionAreaCharting()
Map.centerObject(study_area,8)
Map.view()
# Inline: LCMS Land Use time series across counties (per-feature subplots)
# Passing an ImageCollection + feature_label produces one time series subplot per county
# Height is automatically scaled by number of features
top3_counties = counties.sort('Shape_Area', False).limit(3)
per_feature_dfs, fig = cl.summarize_and_chart(
lcms.select(['Land_Use']).filter(ee.Filter.calendarRange(2000, 2024, 'year')),
top3_counties,
feature_label='FULL_NAME',
title='LCMS Land Use Time Series by County (2000-2024)',
scale=60,
width=900,
)
fig.show()
# per_feature_dfs is a dict of {county_name: DataFrame}
for name, feat_df in per_feature_dfs.items():
print(f'\n--- {name} ---')
display(feat_df.tail(3))
# Sankey: LCMS Land Use transitions for the largest county
sankey_df, sankey_html, matrix_dict = cl.summarize_and_chart(
lcms.select(['Land_Use']).filter(ee.Filter.calendarRange(2000, 2024, 'year')),
top3_counties,
chart_type='sankey',
transition_periods=[2000, 2010, 2024],
sankey_band_name='Land_Use',
title='LCMS Land Use Transitions (2000-2024)',
min_percentage=0.5,
)
display(HTML(cl.sankey_iframe(sankey_html)))
for k,i in matrix_dict.items():
print(k)
display(i)
# Thumbnail: Multi-feature grid in Albers projection with topo basemap
# Note: rotated north arrow and projected extent box in the inset
result = tl.generate_thumbs(
lcms.select(['Land_Use']), top3_counties,
dimensions=300, max_features=3, columns=3,
crs='EPSG:5070', # Albers Equal Area projection
basemap='esri-topo', # topographic basemap
scalebar_units='imperial', # show miles instead of km
inset_map=True, # inset should show all 3 features
inset_basemap='esri-hillshade', # different basemap for inset
title='LCMS Land Use by County (Albers)',
)
display(HTML(result['html']))
# Same data as 2-column layout with geometry burn-in, no clip, no inset
result2 = tl.generate_thumbs(
lcms.select(['Land_Use']), top3_counties,
dimensions=250, max_features=3, columns=2, # 2 columns = taller grid
burn_in_geometry=True,
geometry_outline_color='yellow',
clip_to_geometry=False, # show data beyond boundary
inset_map=False, # no inset map
)
display(HTML(result2['html']))
5. US Urban Areas (getUSUrbanAreas) + Annual NLCD Charting¶
Returns TIGER 2024 urban area boundaries
We’ll summarize Annual NLCD land cover across urban areas
urban = sal.getUSUrbanAreas(study_area)
print('Urban areas:', urban.aggregate_array('NAME20').getInfo())
# Map: NLCD over urban areas
Map.clearMap()
Map.addLayer(nlcd, {
'autoViz': True,
'canAreaChart': True,
'areaChartParams': {'chartType': 'stacked-bar'}
}, 'Annual NLCD Land Cover')
Map.addSelectLayer(urban, {
'strokeColor': 'F0F',
'selectLayerNameProperty': 'NAME20'
}, 'Urban Areas')
Map.turnOnSelectionAreaCharting()
Map.centerObject(study_area)
Map.view()
# Inline: NLCD mode grouped bar across the 3 largest urban areas
top3_urban = urban.sort('ALAND20', False).limit(3)
per_feature_dfs, fig = cl.summarize_and_chart(
nlcd,
top3_urban,
feature_label='NAME20',
title='Annual NLCD Land Cover Mode - Top 3 Urban Areas',
scale=60,
width=800,
height=400*3,
)
fig.show()
# per_feature_dfs is a dict of {county_name: DataFrame}
for name, feat_df in per_feature_dfs.items():
print(f'\n--- {name} ---')
display(feat_df.tail(3))
# Thumbnail: Multi-feature grid with USGS imagery basemap
result = tl.generate_thumbs(
nlcd, top3_urban,
dimensions=300, max_features=3,
basemap='usgs-imagery-topo', # USGS imagery + topo hybrid
overlay_opacity=0.7, # semi-transparent EE overlay
clip_to_geometry=False, # show data beyond boundary
burn_in_geometry=True,
geometry_outline_color='lime',
)
display(HTML(result['html']))
# Thumbnail from a single ee.Feature (not a FeatureCollection)
single_urban = top3_urban.first() # ee.Feature
result2 = tl.generate_thumbs(
nlcd, single_urban, # ee.Feature works too
dimensions=400,
basemap='esri-street',
inset_map=True, inset_on_map=True, # inset overlaid on map corner
title='Largest Urban Area - NLCD',
)
display(HTML(result2['html']))
6. Admin Level 2 (getAdminBoundaries(level=2))¶
Returns admin-level-2 boundaries (districts/counties) from any supported source
Works globally — use
getAdminNamePropertyfor the name column
admin2 = sal.getAdminBoundaries(study_area, level=2)
name_prop = sal.getAdminNameProperty(level=2)
print(f'Admin-2 districts: {admin2.size().getInfo()} features')
print(f'Names (prop={name_prop}):', admin2.aggregate_array(name_prop).getInfo())
7. USFS National Forests (getUSFSForests) + LCMS Land Cover Charting¶
Returns USFS National Forest boundaries
Properties include
FORESTNAME,REGION,GIS_ACRESOptional
regionfilter
forests = sal.getUSFSForests(study_area)
print('National Forests:', forests.aggregate_array('FORESTNAME').getInfo())
# Map: LCMS Land Cover over national forests + Sentinel-2 basemap
Map.clearMap()
Map.addLayer(s2.filterBounds(forests.geometry().bounds(500)).median(), gil.vizParamsFalse10k, 'Sentinel-2 Summer 2023', True)
Map.addLayer(lcms.select(['Land_Cover']), {
'autoViz': True,
'canAreaChart': True,
'areaChartParams': {'line': True, 'sankey': True}
}, 'LCMS Land Cover')
Map.addLayer(lcms.select(['Change']), {
'autoViz': True,
'canAreaChart': True,
'areaChartParams': {'line': True}
}, 'LCMS Change', False)
Map.addSelectLayer(forests, {
'strokeColor': '0F0',
'selectLayerNameProperty': 'FORESTNAME'
}, 'National Forests')
Map.turnOnSelectionAreaCharting()
Map.centerObject(forests)
Map.view()
# Inline: LCMS Land Cover time series over the Uinta-Wasatch-Cache NF
uwc = forests.filter(ee.Filter.stringContains('FORESTNAME', 'Uinta'))
df, fig = cl.summarize_and_chart(
lcms.select(['Land_Cover']),
uwc,
title='LCMS Land Cover - Uinta-Wasatch-Cache NF',
stacked=False,
)
fig.show()
display(df.head())
# Sankey: LCMS Land Cover transitions for the same forest
sankey_df, sankey_html, matrix_dict = cl.summarize_and_chart(
lcms.select(['Land_Cover']),
uwc,
chart_type='sankey',
transition_periods=[1985, 2000, 2010, 2024],
sankey_band_name='Land_Cover',
title='LCMS Land Cover Transitions - Uinta-Wasatch-Cache NF',
min_percentage=0.5,
)
display(HTML(cl.sankey_iframe(sankey_html)))
for label, mdf in matrix_dict.items():
print(f'\n{label}')
display(mdf)
# Thumbnail: UTM projection with hillshade basemap and geometry fill
crs = gil.getUTMEpsg(ee.Geometry.Point([-111.5, 40.5]))
result = tl.generate_thumbs(
lcms.select(['Land_Cover']), uwc,
dimensions=400,
crs=crs, # UTM zone for this location
basemap='esri-hillshade', # hillshade basemap
burn_in_geometry=True,
geometry_outline_color='yellow',
geometry_fill_color='00000044', # semi-transparent black outside
inset_map=True,
inset_on_map=True, # overlay inset on the map corner
title='Uinta-Wasatch-Cache NF',
)
display(HTML(result['html']))
# Same area with NatGeo basemap, no clip, outline arrow style
result2 = tl.generate_thumbs(
lcms.select(['Land_Cover']), uwc,
dimensions=400,
basemap='esri-natgeo',
overlay_opacity=0.6,
north_arrow_style='outline', # outline arrow style
clip_to_geometry=False, # bounding box, not clipped
)
display(HTML(result2['html']))
8. USFS Ranger Districts (getUSFSDistricts) + MTBS Charting¶
Returns USFS Ranger District boundaries
Optional
forest_nameandregionfiltersWe’ll summarize MTBS burn severity across districts
districts = sal.getUSFSDistricts(study_area)
print('Ranger Districts:', districts.aggregate_array('DISTRICTNA').getInfo())
# Filter to a specific forest
uwc_districts = sal.getUSFSDistricts(study_area, forest_name='Uinta-Wasatch-Cache National Forest')
print('\nUinta-Wasatch-Cache districts:', uwc_districts.aggregate_array('DISTRICTNA').getInfo())
# Map: MTBS burn severity with ranger districts
Map.clearMap()
Map.addLayer(mtbs, {
'autoViz': True,
'canAreaChart': True,
'areaChartParams': {
'chartType': 'stacked-bar',
'visible': [False, True, True, True, True, True, False]
}
}, 'MTBS Burn Severity')
Map.addSelectLayer(uwc_districts, {
'strokeColor': 'FF0',
'selectLayerNameProperty': 'DISTRICTNA'
}, 'Ranger Districts')
Map.turnOnSelectionAreaCharting()
Map.centerObject(uwc_districts)
Map.view()
# Inline: MTBS time series across all ranger districts (per-feature subplots)
per_feature_dfs, fig = cl.summarize_and_chart(
mtbs.filter(ee.Filter.calendarRange(2000, 2024, 'year')),
uwc_districts.limit(3),
feature_label='DISTRICTNA',
title='MTBS Burn Severity by Ranger District (2000-2024)',
chart_type = 'stacked_bar',
class_visible={'Background':False,'Non-Mapping Area':False},
scale=60,
)
fig.show()
# Show years with fire activity for each district
for name, feat_df in per_feature_dfs.items():
fire_years = feat_df[feat_df.sum(axis=1) > 0]
if len(fire_years) > 0:
print(f'\n--- {name} (years with fire) ---')
display(fire_years)
# Thumbnail: Multi-feature with dark basemap and classic north arrow
result = tl.generate_thumbs(
mtbs.filter(ee.Filter.calendarRange(2000, 2024, 'year')),
uwc_districts.limit(3),
dimensions=300,
feature_label='DISTRICTNA',
basemap='esri-dark-gray', # dark gray canvas basemap
north_arrow_style='classic', # classic arrow style
burn_in_geometry=True,
geometry_outline_color='cyan',
inset_map=True,
title='MTBS Burn Severity by District',
)
display(HTML(result['html']))
# Same as single-column layout
result2 = tl.generate_thumbs(
mtbs.filter(ee.Filter.calendarRange(2000, 2024, 'year')),
uwc_districts.limit(2), columns=1, # vertical stack
feature_label='DISTRICTNA',
basemap='esri-satellite',
burn_in_geometry=True,
geometry_outline_color='cyan',
overlay_opacity=0.85,
)
display(HTML(result2['html']))
9. USFS Regions (getUSFSRegions)¶
Returns USFS administrative region boundaries
Properties include
REGION,REGIONNAME,REGIONHEAD
regions = sal.getUSFSRegions(study_area)
for feat in regions.getInfo()['features']:
p = feat['properties']
print(f"Region {p['REGION']}: {p['REGIONNAME']} (HQ: {p['REGIONHEAD']})")
10. US Census Tracts (getUSCensusTracts) + Sentinel-2 Spectral Charting¶
Returns TIGER 2020 census tracts
We’ll use a smaller area and chart mean Sentinel-2 spectral values across tracts
# Use a smaller area for census geographies
small_area = ee.Geometry.Point([-111.9, 40.7]).buffer(5000)
tracts = sal.getUSCensusTracts(small_area)
print(f'Census tracts: {tracts.size().getInfo()}')
block_groups = sal.getUSBlockGroups(small_area)
print(f'Block groups: {block_groups.size().getInfo()}')
# Map: Sentinel-2 composite with census tracts
Map.clearMap()
s2_small = gil.superSimpleGetS2(small_area, '2023-06-01', '2023-09-30').median().clip(small_area)
Map.addLayer(s2_small, {
'min': 0, 'max': 3000,
'bands': 'swir1,nir,red',
'canAreaChart': True,
'areaChartParams': {'bandNames': 'blue,green,red,nir,swir1,swir2', 'palette': '00D,0D0,D00,D0D,0DD,DD0'}
}, 'Sentinel-2 Summer 2023')
Map.addLayer(lcms.select(['Land_Cover']), {
'autoViz': True,
'canAreaChart': True
}, 'LCMS Land Cover', False)
Map.addSelectLayer(tracts, {
'strokeColor': '0FF',
'selectLayerNameProperty': 'NAMELSAD'
}, 'Census Tracts')
Map.turnOnSelectionAreaCharting()
Map.centerObject(small_area)
Map.view()
# Inline: Mean spectral values per tract (continuous, reduceRegions)
s2_bands = s2_small.select(['blue', 'green', 'red', 'nir', 'swir1', 'swir2'])
df, fig = cl.summarize_and_chart(
s2_bands,
tracts,
feature_label='NAMELSAD',
title='Mean S2 Spectral Values by Census Tract',
palette=['00D', '0D0', 'D00', 'D0D', '0DD', 'DD0'],
width=800,
height=400,
chart_type='stacked_bar'
)
fig.show()
display(df)
# Thumbnail: Sentinel-2 false color with custom viz params on Google Hybrid
result = tl.generate_thumbs(
s2_small, tracts,
viz_params={'bands': ['swir1', 'nir', 'red']},
dimensions=250, max_features=4,
basemap='google-hybrid', # Google hybrid basemap
burn_in_legend=False, # hide legend for continuous data
north_arrow=False, # no north arrow
scalebar_units='imperial',
)
display(HTML(result['html']))
# True color version with geometry burn-in
result2 = tl.generate_thumbs(
s2_small, tracts,
viz_params={'bands': ['red', 'green', 'blue']},
dimensions=250, max_features=4,
burn_in_geometry=True,
geometry_outline_color='red',
geometry_outline_weight=3, # thicker boundary
title='Census Tracts - True Color',
)
display(HTML(result2['html']))
11. US Census Blocks (getUSCensusBlocks) and Block Groups (getUSBlockGroups)¶
Census blocks are extremely numerous - use small areas only
Block groups are a coarser aggregation
tiny_area = ee.Geometry.Point([-111.9, 40.7]).buffer(1000)
blocks = sal.getUSCensusBlocks(tiny_area)
print(f'Census blocks in 1km radius: {blocks.size().getInfo()}')
12. Protected Areas (getProtectedAreas) + LCMS Change Charting¶
Returns WDPA protected area polygons
Optional
iucn_catanddesig_typefiltersWe’ll chart LCMS change within protected areas
protected = sal.getProtectedAreas(study_area).sort('REP_AREA', False).limit(5)
print(f'Protected areas: {protected.size().getInfo()}')
# Show a sample of names and designations
for feat in protected.getInfo()['features']:
p = feat['properties']
print(f" {p.get('NAME', 'N/A')} - {p.get('DESIG_ENG', 'N/A')} (IUCN: {p.get('IUCN_CAT', 'N/A')})")
# Map: LCMS Change with protected areas as selectable zones
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
}, 'LCMS Land Cover', False)
# Filter to larger protected areas for cleaner visualization
large_protected = protected.filter(ee.Filter.gt('REP_AREA', 10))
Map.addSelectLayer(large_protected, {
'strokeColor': '0F0',
'selectLayerNameProperty': 'NAME'
}, 'Protected Areas')
Map.turnOnSelectionAreaCharting()
Map.centerObject(study_area)
Map.view()
# Inline: LCMS Change time series over all protected areas combined
df, fig = cl.summarize_and_chart(
lcms.select(['Change']),
protected.limit(2),
class_visible={'Stable':False,'Non-Processing Area Mask':False},
title='LCMS Change - Protected Areas near Salt Lake City',
)
fig.show()
for title, d in df.items():
print(title)
display(d.head())
# Sankey: LCMS Change transitions over the protected areas
sankey_df, sankey_html, matrix_dict = cl.summarize_and_chart(
lcms.select(['Change']),
protected,
chart_type='sankey',
transition_periods=[1990, 2000, 2010, 2024],
sankey_band_name='Change',
title='LCMS Change Transitions - Protected Areas',
min_percentage=0.2,
scale=120 # Increase scale to avoid memory erros - 120m will include every 4th pixel in the stats
)
display(HTML(cl.sankey_iframe(sankey_html)))
for k,t in matrix_dict.items():
print(k)
display(t)
lcms_change_composite = lcms.select(['Change']).min().copyProperties(lcms.first())
# Thumbnail: LCMS Change in Albers with USA Topo basemap
result = tl.generate_thumbs(
lcms_change_composite, protected.limit(2),
dimensions=300, max_features=2,
crs='EPSG:5070', # Albers Equal Area
basemap='esri-usa-topo', # USA Topo basemap
north_arrow_style='outline',
inset_map=True,
inset_basemap='esri-natgeo', # different basemap for inset
title='LCMS Change Composite - Protected Areas',
)
display(HTML(result['html']))
# Single protected area as ee.Feature
result2 = tl.generate_thumbs(
lcms_change_composite, protected.first(), # ee.Feature
dimensions=400,
basemap='esri-hillshade-dark', # dark hillshade
burn_in_geometry=True,
geometry_outline_color='lime',
geometry_fill_color='00000066',
inset_map=True, inset_on_map=True,
)
display(HTML(result2['html']))
13. Roads (getRoads)¶
Returns road features from TIGER (US, 2012-2025) or GRIP4 (global)
TIGER properties include
FULLNAME,MTFCC(road type code),RTTYPGRIP4 properties include
GP_RTP(1=Highway, 2=Primary, 3=Secondary, 4=Tertiary, 5=Local)Roads are line features — useful for visualization and proximity analysis
# Use a small area - roads are very numerous
road_area = ee.Geometry.Point([-111.9, 40.7]).buffer(2000)
# TIGER 2024 roads (US, default)
roads = sal.getRoads(road_area)
print(f'TIGER 2024 roads in 2km radius: {roads.size().getInfo()}')
# Filter to primary/secondary roads only (MTFCC codes)
major_roads = roads.filter(ee.Filter.inList('MTFCC', ['S1100', 'S1200']))
print(f'Major roads: {major_roads.size().getInfo()}')
print('Road names:', major_roads.aggregate_array('FULLNAME').distinct().getInfo())
# GRIP4 global roads for the same area
grip_roads = sal.getRoads(road_area, source='grip')
print(f'\nGRIP4 roads: {grip_roads.size().getInfo()}')
highways = grip_roads.filter(ee.Filter.lte('GP_RTP', 2))
print(f'GRIP4 highways+primary: {highways.size().getInfo()}')
# Map: Roads overlaid on Sentinel-2
Map.clearMap()
s2_road = gil.superSimpleGetS2(road_area, '2023-06-01', '2023-09-30').median().clip(road_area)
Map.addLayer(s2_road, {'min': 0, 'max': 3000, 'bands': 'red,green,blue'}, 'Sentinel-2 True Color')
# All roads
Map.addLayer(roads, {'strokeColor': 'AAA'}, 'All Roads', False)
# Major roads highlighted
Map.addLayer(major_roads, {'strokeColor': 'F00'}, 'Major Roads')
Map.centerObject(road_area)
Map.view()
# Thumbnail: Satellite basemap with study area outline and title
result = tl.generate_thumbs(
s2_road, road_area,
viz_params=gil.vizParamsFalse10k,
dimensions=400,
basemap='esri-satellite',
burn_in_geometry=True,
geometry_outline_color='lime',
inset_map=True,
title='Roads Study Area - False Color',
)
display(HTML(result['html']))
# True color version on OSM basemap
result2 = tl.generate_thumbs(
s2_road, road_area,
viz_params=gil.vizParamsTrue10k,
dimensions=400,
basemap='osm', # OpenStreetMap
overlay_opacity=0.5, # very transparent to see OSM detail
scalebar=False,
)
display(HTML(result2['html']))
14. Buildings (getBuildings)¶
Returns building footprints from VIDA Combined (default), Microsoft, or Google Open Buildings
Automatically determines intersecting countries and merges per-country collections
Use small areas only - building collections are very large
bldg_area = ee.Geometry.Point([-111.9, 40.7]).buffer(500)
# VIDA Combined buildings
vida_bldgs = sal.getBuildings(bldg_area, source='vida')
print(f'VIDA buildings in 500m radius: {vida_bldgs.size().getInfo()}')
# Microsoft buildings
ms_bldgs = sal.getBuildings(bldg_area, source='ms')
print(f'MS buildings in 500m radius: {ms_bldgs.size().getInfo()}')
# Map: Buildings on Sentinel-2
Map.clearMap()
s2_bldg = gil.superSimpleGetS2(bldg_area, '2023-06-01', '2023-09-30').median().clip(bldg_area)
Map.addLayer(s2_bldg, {'min': 0, 'max': 3000, 'bands': 'red,green,blue'}, 'Sentinel-2 True Color')
Map.addLayer(vida_bldgs, {'strokeColor': '0FF', 'fillColor': '0FF4'}, 'VIDA Buildings')
Map.addLayer(ms_bldgs, {'strokeColor': 'F0F', 'fillColor': 'F0F4'}, 'MS Buildings', False)
Map.centerObject(bldg_area)
Map.view()
# Thumbnail: OSM basemap with transparent overlay (small area)
result = tl.generate_thumbs(
s2_bldg, bldg_area,
viz_params=gil.vizParamsFalse10k,
dimensions=400,
basemap='osm',
overlay_opacity=0.6, # transparent to see OSM roads
scalebar=False, # no scalebar at this zoom
title='Buildings Study Area',
)
display(HTML(result['html']))
# Light gray basemap version
result2 = tl.generate_thumbs(
s2_bldg, bldg_area,
viz_params=gil.vizParamsTrue10k,
dimensions=400,
basemap='esri-light-gray',
overlay_opacity=0.7,
burn_in_geometry=True,
geometry_outline_color='red',
)
display(HTML(result2['html']))
15. Full Summary: All Layers on One Map¶
This final example puts multiple summary area layers together on a single map
LCMS Land Cover, Land Use, and Change are all available with area charting
Multiple feature layers are available for selection-based charting
The Sentinel-2 composite provides a basemap
Map.clearMap()
# Raster layers with area charting
Map.addLayer(s2.filterBounds(counties).median(), {
'min': 0, 'max': 3000,
'bands': 'swir1,nir,red',
'canAreaChart': True,
'areaChartParams': {'bandNames': 'nir,swir1,swir2', 'palette': 'D0D,0DD,DD0'}
}, 'Sentinel-2 False Color', True)
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', False)
Map.addLayer(lcms.select(['Change']), {
'autoViz': True,
'canAreaChart': True,
'areaChartParams': {'line': True}
}, 'LCMS Change', False)
Map.addLayer(nlcd, {
'autoViz': True,
'canAreaChart': True
}, 'Annual NLCD Land Cover', False)
Map.addLayer(mtbs, {
'autoViz': True,
'canAreaChart': True,
'areaChartParams': {'chartType': 'stacked-bar', 'visible': [False, True, True, True, True, True, False]}
}, 'MTBS Burn Severity', False)
# Selectable summary area layers
Map.addSelectLayer(counties, {'strokeColor': '00F', 'selectLayerNameProperty': 'FULL_NAME'}, 'Counties')
Map.addSelectLayer(forests, {'strokeColor': '0F0', 'selectLayerNameProperty': 'FORESTNAME'}, 'National Forests')
Map.addSelectLayer(uwc_districts, {'strokeColor': 'FF0', 'selectLayerNameProperty': 'DISTRICTNA'}, 'Ranger Districts')
Map.addSelectLayer(urban, {'strokeColor': 'F0F', 'selectLayerNameProperty': 'NAME20'}, 'Urban Areas')
Map.turnOnSelectionAreaCharting()
Map.centerObject(study_area)
Map.view()
Inline Summary Across Multiple Summary Areas¶
summarize_and_chartwithfeature_label+ee.ImageCollectionproduces per-feature time series subplotsEach feature gets its own subplot, with shared x-axis and consistent legend
For
ee.Image+feature_label, a grouped bar chart is produced insteadThis section demonstrates both chart types across different summary areas
# LCMS Land Cover time series across ranger districts (per-feature subplots)
per_feature_dfs, fig = cl.summarize_and_chart(
lcms.select(['Land_Cover']).filter(ee.Filter.calendarRange(2000, 2024, 'year')),
uwc_districts.limit(3),
feature_label='DISTRICTNA',
title='LCMS Land Cover by Ranger District (2000-2024)',
scale=60,
width=900,
)
fig.show()
for name, feat_df in per_feature_dfs.items():
print(f'\n--- {name} ---')
display(feat_df.tail(3))
# LCMS Land Use Sankey over the full study area
sankey_df, sankey_html, matrix_dict = cl.summarize_and_chart(
lcms.select(['Land_Use']),
study_area,
chart_type='sankey',
transition_periods=[1985, 1990, 2000, 2005, 2015, 2020, 2022, 2024],
title='LCMS Land Use Transitions - Wasatch Front Region',
min_percentage=0.5,
)
display(HTML(cl.sankey_iframe(sankey_html)))
for label, mdf in matrix_dict.items():
print(f'\n{label}')
display(mdf)
# NLCD grouped bar across counties (ee.Image + feature_label = grouped bar chart)
# Using .mode() to reduce the ImageCollection to a single Image first
nlcd_mode = nlcd.mode().set(nlcd_viz_props)
df, fig = cl.summarize_and_chart(
nlcd_mode,
counties.limit(5),
feature_label='FULL_NAME',
title='Annual NLCD Land Cover Mode by County - Grouped Bar',
width=900,
height=450,
)
fig.show()
display(df)
# Stacked bar chart of LCMS Land Cover (2022-2024) for the first 3 counties
lcms_subset = lcms.select(['Land_Cover']).filter(ee.Filter.calendarRange(2022, 2024, 'year'))
county_subset = counties.limit(3)
stacked_bar_df, stacked_bar_fig = cl.summarize_and_chart(
nlcd_mode,
counties.limit(5),
feature_label='FULL_NAME',
chart_type='stacked_bar',
title='Annual NLCD Land Cover Mode by County - Stacked Bar',
width=900,
height=450
)
stacked_bar_fig.show()
display(stacked_bar_df)
Reports with reportLib¶
geeViz.outputLib.reportsbuilds styled HTML reports combining:Thumbnails (via
outputLib.thumbs) – auto-visualized satellite imageryCharts (via
outputLib.charts) – time series, bar charts, Sankey diagramsData tables – summary statistics from
summarize_and_chartLLM narratives (via Gemini) – AI-generated interpretation of each section
All EE data requests (charts + thumbnails) run in parallel using thread pools
Each section has toggle params:
generate_table,generate_chart(default True),thumb_format(default"png", also"gif","filmstrip", orNone)Key API:
Report(title, model, header_text)– create a reportreport.add_section(ee_obj, geometry, title, ...)– add data sectionsreport.generate(format="html", output_path=...)– generate the report
# Build a report with multiple sections
report = rl.Report(
title='Wasatch Front Land Assessment',
header_text='Automated analysis of land cover, land use, and burn severity trends.',
)
# Section 1: LCMS Land Cover in national forests
forests = sal.getUSFSForests(study_area)
report.add_section(
ee_obj=lcms.select(['Land_Cover']).filter(ee.Filter.calendarRange(2020, 2024, 'year')),
geometry=forests,
title='LCMS Land Cover in National Forests',
scale=60,
feature_label='FORESTNAME',
)
# Section 2: LCMS Land Use (no thumbnail, just chart + table)
report.add_section(
ee_obj=lcms.select(['Land_Use']).filter(ee.Filter.calendarRange(2020, 2024, 'year')),
geometry=study_area,
title='LCMS Land Use Trends',
scale=60,
thumb_format=None,
)
# Section 3: MTBS Burn Severity (no table, just chart + thumb)
report.add_section(
ee_obj=mtbs.filter(ee.Filter.calendarRange(2010, 2024, 'year')),
geometry=study_area,
title='MTBS Burn Severity',
scale=30,
generate_table=False,
)
print(f'Report: {report.title}')
print(f'Sections: {len(report._sections)}')
for i, sec in enumerate(report._sections):
print(f' {i+1}. {sec.title} (table={sec.generate_table}, chart={sec.generate_chart}, thumb_format={sec.thumb_format})')
# Generate the report (parallel EE requests + Gemini narratives)
import time
start = time.time()
html_path = report.generate(format='html', output_path='wasatch_report.html')
elapsed = time.time() - start
print(f'\nReport generated in {elapsed:.1f}s')
print(f'Saved to: {html_path}')
# Display the report inline in the notebook
from IPython.display import IFrame,HTML
with open('wasatch_report.html', 'r', encoding='utf-8') as f:
html = f.read()
display(HTML(html))
Available Summary Area Types¶
getSummaryAreasLibprovides a dictionary listing all available summary area functions
import pandas as pd
rows = [{'Type': k, 'Function': v['function'], 'Description': v['description']}
for k, v in sal.AVAILABLE_SUMMARY_AREAS.items()]
display(pd.DataFrame(rows))