Interactive App Examples

Learn by example! This module provides complete, working app templates you can customize for your own projects. Each example demonstrates a different app pattern.

Learning objectives

  • Build an NDVI time series app with date selection.
  • Create a split-panel comparison viewer.
  • Implement a location search with point inspection.
  • Combine multiple widgets into a complete application.

Example 1: NDVI Time Series Explorer

Users can select a location and view NDVI trends over time:

// NDVI Time Series App
ui.root.clear();

// Create map
var map = ui.Map();
map.setCenter(-82.3, 29.6, 10);
map.style().set('cursor', 'crosshair');

// Create chart panel
var chartPanel = ui.Panel({style: {width: '400px'}});

// Instructions
var instructions = ui.Label(
  'Click anywhere on the map to view NDVI time series for that location.',
  {fontSize: '14px', margin: '10px'}
);

// Title
var title = ui.Label('NDVI Time Series Explorer', {
  fontWeight: 'bold',
  fontSize: '24px',
  margin: '10px'
});

// Year selector
var yearSelect = ui.Select({
  items: ['2020', '2021', '2022', '2023'],
  value: '2023',
  style: {margin: '10px'}
});

// Function to create chart
var createChart = function(point, year) {
  chartPanel.clear();
  chartPanel.add(ui.Label('Loading...'));
  
  var startDate = year + '-01-01';
  var endDate = year + '-12-31';
  
  // Get NDVI collection
  var collection = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
    .filterDate(startDate, endDate)
    .filterBounds(point)
    .filter(ee.Filter.lt('CLOUD_COVER', 30))
    .map(function(img) {
      var ndvi = img.multiply(0.0000275).add(-0.2)
        .normalizedDifference(['SR_B5', 'SR_B4'])
        .rename('NDVI');
      return ndvi.set('system:time_start', img.get('system:time_start'));
    });
  
  // Create chart
  var chart = ui.Chart.image.series({
    imageCollection: collection,
    region: point.buffer(30),
    reducer: ee.Reducer.mean(),
    scale: 30
  }).setOptions({
    title: 'NDVI Time Series - ' + year,
    vAxis: {title: 'NDVI', viewWindow: {min: 0, max: 1}},
    hAxis: {title: 'Date'},
    lineWidth: 2,
    pointSize: 4
  });
  
  chartPanel.clear();
  chartPanel.add(title);
  chartPanel.add(yearSelect);
  chartPanel.add(instructions);
  chartPanel.add(chart);
};

// Map click handler
map.onClick(function(coords) {
  var point = ee.Geometry.Point([coords.lon, coords.lat]);
  map.layers().reset();
  map.addLayer(point, {color: 'red'}, 'Selected Location');
  createChart(point, yearSelect.getValue());
});

// Year change handler
yearSelect.onChange(function(year) {
  var layers = map.layers();
  if (layers.length() > 0) {
    var layer = layers.get(0);
    var point = ee.Geometry(layer.getEeObject());
    createChart(point, year);
  }
});

// Initial panel content
chartPanel.add(title);
chartPanel.add(yearSelect);
chartPanel.add(instructions);

// Layout
ui.root.add(ui.SplitPanel(chartPanel, map));

What you should see

An app with a sidebar showing a year selector. Click anywhere on the map to generate an NDVI time series chart for that location.

Example 2: Before/After Comparison

Split-screen comparison with swipe functionality:

// Split-Map Comparison App
ui.root.clear();

// Create two maps
var leftMap = ui.Map();
var rightMap = ui.Map();

// Link the maps
var linker = ui.Map.Linker([leftMap, rightMap]);
leftMap.setCenter(-82.3, 29.6, 11);

// Load images for different years
var image2015 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2015-06-01', '2015-08-31')
  .filterBounds(ee.Geometry.Point([-82.3, 29.6]))
  .median()
  .multiply(0.0000275).add(-0.2);

var image2023 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2023-06-01', '2023-08-31')
  .filterBounds(ee.Geometry.Point([-82.3, 29.6]))
  .median()
  .multiply(0.0000275).add(-0.2);

// Visualization
var vis = {bands: ['SR_B4', 'SR_B3', 'SR_B2'], min: 0, max: 0.3};

// Add layers
leftMap.addLayer(image2015, vis, 'Summer 2015');
rightMap.addLayer(image2023, vis, 'Summer 2023');

// Add labels
leftMap.add(ui.Label('2015', {
  position: 'top-center',
  fontSize: '20px',
  fontWeight: 'bold',
  backgroundColor: 'rgba(255,255,255,0.8)'
}));

rightMap.add(ui.Label('2023', {
  position: 'top-center',
  fontSize: '20px',
  fontWeight: 'bold',
  backgroundColor: 'rgba(255,255,255,0.8)'
}));

// Create split panel with wipe
var splitPanel = ui.SplitPanel({
  firstPanel: leftMap,
  secondPanel: rightMap,
  orientation: 'horizontal',
  wipe: true  // Enable swipe!
});

ui.root.add(splitPanel);

What you should see

A swipeable split view comparing 2015 and 2023 imagery. Drag the divider left/right to compare the same location across time.

Example 3: Layer Toggle Dashboard

Multiple layers with visibility controls:

// Layer Toggle Dashboard
ui.root.clear();

var map = ui.Map();
map.setCenter(-82.3, 29.6, 10);

// Load and prepare data
var image = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2023-06-01', '2023-08-31')
  .filterBounds(ee.Geometry.Point([-82.3, 29.6]))
  .median()
  .multiply(0.0000275).add(-0.2);

var ndvi = image.normalizedDifference(['SR_B5', 'SR_B4']);
var ndwi = image.normalizedDifference(['SR_B3', 'SR_B5']);

// Add layers (initially hidden except first)
map.addLayer(image, {bands: ['SR_B4','SR_B3','SR_B2'], min:0, max:0.3}, 'True Color', true);
map.addLayer(image, {bands: ['SR_B5','SR_B4','SR_B3'], min:0, max:0.4}, 'False Color', false);
map.addLayer(ndvi, {min: 0, max: 0.8, palette: ['brown','yellow','green']}, 'NDVI', false);
map.addLayer(ndwi, {min: -0.5, max: 0.5, palette: ['brown','white','blue']}, 'NDWI', false);

// Create layer controls
var makeCheckbox = function(label, index) {
  var isVisible = index === 0;
  var checkbox = ui.Checkbox(label, isVisible);
  checkbox.onChange(function(checked) {
    map.layers().get(index).setShown(checked);
  });
  return checkbox;
};

// Build control panel
var controlPanel = ui.Panel({
  widgets: [
    ui.Label('Landsat 8 Layer Dashboard', {fontWeight: 'bold', fontSize: '20px'}),
    ui.Label('Toggle layers on/off:', {fontSize: '14px'}),
    makeCheckbox('True Color', 0),
    makeCheckbox('False Color', 1),
    makeCheckbox('NDVI', 2),
    makeCheckbox('NDWI', 3),
    ui.Label(''),
    ui.Label('Tip: Compare NDVI and NDWI to distinguish vegetation from water.', 
      {fontSize: '12px', color: 'gray'})
  ],
  style: {width: '250px', padding: '15px'}
});

ui.root.add(ui.SplitPanel(controlPanel, map));

Example 4: Location Search with Inspector

Search by coordinates and inspect pixel values:

// Location Search + Inspector App
ui.root.clear();

var map = ui.Map();
map.style().set('cursor', 'crosshair');

// Load NDVI layer
var ndvi = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2023-06-01', '2023-08-31')
  .median()
  .multiply(0.0000275).add(-0.2)
  .normalizedDifference(['SR_B5', 'SR_B4'])
  .rename('NDVI');

map.addLayer(ndvi, {min: 0, max: 0.8, palette: ['brown','yellow','green']}, 'NDVI');
map.setCenter(-82.3, 29.6, 8);

// Create input fields
var lonInput = ui.Textbox({placeholder: 'Longitude (e.g., -82.3)'});
var latInput = ui.Textbox({placeholder: 'Latitude (e.g., 29.6)'});

// Result label
var resultLabel = ui.Label('Click map or enter coordinates to inspect NDVI');

// Go to location function
var goToLocation = function() {
  var lon = parseFloat(lonInput.getValue());
  var lat = parseFloat(latInput.getValue());
  if (!isNaN(lon) && !isNaN(lat)) {
    var point = ee.Geometry.Point([lon, lat]);
    map.centerObject(point, 12);
    inspectPoint(point, lon, lat);
  }
};

// Inspect point function
var inspectPoint = function(point, lon, lat) {
  resultLabel.setValue('Loading...');
  
  ndvi.reduceRegion({
    reducer: ee.Reducer.mean(),
    geometry: point.buffer(30),
    scale: 30
  }).get('NDVI').evaluate(function(value) {
    if (value !== null) {
      resultLabel.setValue(
        'Location: ' + lon.toFixed(4) + ', ' + lat.toFixed(4) + 
        '\nNDVI: ' + value.toFixed(4)
      );
    } else {
      resultLabel.setValue('No data at this location');
    }
  });
  
  // Add marker
  map.layers().set(1, ui.Map.Layer(point, {color: 'red'}, 'Selected'));
};

// Map click handler
map.onClick(function(coords) {
  var point = ee.Geometry.Point([coords.lon, coords.lat]);
  lonInput.setValue(coords.lon.toFixed(4));
  latInput.setValue(coords.lat.toFixed(4));
  inspectPoint(point, coords.lon, coords.lat);
});

// Build panel
var panel = ui.Panel({
  widgets: [
    ui.Label('NDVI Point Inspector', {fontWeight: 'bold', fontSize: '20px'}),
    ui.Label('Enter coordinates:'),
    lonInput,
    latInput,
    ui.Button('Go to Location', goToLocation),
    ui.Label(''),
    resultLabel
  ],
  style: {width: '280px', padding: '15px'}
});

ui.root.add(ui.SplitPanel(panel, map));

Pro tips

  • Start simple: Get core functionality working before adding features.
  • Use evaluate(): For displaying server values in UI, use .evaluate(callback).
  • Loading indicators: Show "Loading..." while async operations run.
  • Error handling: Check for null values in callbacks.
  • Mobile testing: Check layout on narrow screens before publishing.

Try it: Customize an example

  1. Take Example 3 (Layer Toggle) and add an NDBI (Built-up Index) layer.
  2. Modify Example 1 to show a different spectral index.
  3. Combine elements from multiple examples into your own app.

Common mistakes

  • Forgetting to use .evaluate() for async server values.
  • Not providing feedback when operations are loading.
  • Making panels too wide for comfortable map viewing.
  • Not testing the full workflow before publishing.

Quick self-check

  1. How do you create a split-screen comparison with swipe?
  2. What callback pattern gets server values into the UI?
  3. How do you show/hide map layers programmatically?

Next steps