Interactive Viewer for GLORYS12V1 Ocean Potential Temperature and Other Variables#
For an interactive version of this page please visit the Google Colab:
Open in Google Colab
(To open link in new tab press Ctrl + click)
Alternatively this notebook can be opened with Binder by following the link: Interactive Viewer for GLORYS12V1 Ocean Potential Temperature and Other Variables
Purpose
This notebook provides an interactive environment for exploring, visualizing and extracting subsets of ocean reanalysis data from the GLORYS12V1 model through the OCEAN ICE backend data tool.
Users can:
Browse available variables, dates and depths.
Select an area of interest by clicking on a map or manually entering coordinates.
Retrieve and visualize selected data layers.
Compute summary statistics for the chosen area.
Download the subsetted dataset as a CSV file for offline analysis.
Data sources
The data used in this notebook come from GLORYS12V1, a high-resolution Global Ocean Reanalysis product developed by the Copernicus Marine Environment Monitoring Service (CMEMS).
GLORYS12V1 is built by assimilating multiple streams of ocean observations into a global ocean general circulation model. These inputs include:
In situ measurements from Argo floats, moorings and research cruises.
Satellite observations of sea surface temperature and sea level anomaly.
Ice concentration and drift data from polar observation systems.
The result is a consistent, 3D representation of the global ocean over time, providing key physical variables such as:
Potential temperature (thetao) — a temperature field corrected for pressure effects, allowing comparison between different depths.
**Practical salinity (so) **— a measure of dissolved salt concentration.
Ocean current components (uo and vo) — describing the east–west and north–south velocity.
Spatial and temporal coverage:
Global coverage at 1/12° horizontal resolution (≈ 8 km).
Up to 50 vertical levels, from the surface to the seafloor.
Daily temporal resolution, spanning several decades (historical reanalysis and near-real-time updates).
The data used in this Notebook are available at: https://er1.s4oceanice.eu/erddap/griddap/GLORYS12V1_sea_floor_potential_temp.html
Beyond ERDAPP, two other services are additionaly used in this notebook:
WMS (Web Map Service) — for visualizing data layers on interactive maps.
griddap — for downloading raw numerical data in CSV format.
The combination of these services enables both visual exploration and quantitative extraction of ocean state variables for any time, depth and region of interest.
Instructions for using this Notebook
To interact with the notebook, run each code cell sequentially, You can do this by clicking the Play button (▶️) on the left side of each grey code block. Executing the cells in order ensure that all features and visualizations work properly.
Explaining the code
1. Importing required libraries & defining data sources
This section imports all the Python libraries and Jupyter widgets required for interactive data visualization and map functionality.
It also defines the ERDDAP WMS and griddap (CSV) data access URLs for the GLORYS12V1 potential temperature dataset.
The following libraries are used in this notebook:
Data requests: xml.etree.ElementTree, requests
Data handling: pandas, io.BytesIO
Visualization: ipyleaflet,
Widget-based interactivity: ipywidgets, traitlets
# @title
import requests
import xml.etree.ElementTree as ET
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO
from traitlets import Unicode
from ipyleaflet import (
Map,
Marker,
basemaps,
projections,
WMSLayer,
Popup,
Icon,
AwesomeIcon,
ImageOverlay,
)
from ipywidgets import (
IntSlider,
HBox,
Layout,
Output,
VBox,
HTML,
Label,
Dropdown,
Button,
FloatText
)
from IPython.display import display, clear_output
ERRDAP_SLA_URL = 'https://er1.s4oceanice.eu/erddap/wms/GLORYS12V1_sea_floor_potential_temp/request?service=WMS&request=GetCapabilities&version=1.3.0'
BASE_DATA_URL = 'https://er1.s4oceanice.eu/erddap/griddap/GLORYS12V1_sea_floor_potential_temp.csv?'
2. Retrieving WMS capabilities and parsing available layers
This code block queries the ERDDAP WMS GetCapabilities
endpoint to retrieve metadata about the dataset, inlcuding:
Time dimension - available dates doe the dataset.
Elevation dimension - depth levels.
Available data layers - potential temperature, salinity and ocean currents components.
The metadata elements are then stored for later use in the notebook’s interactive visualization tools.
# @title
get_capabilities = requests.get(ERRDAP_SLA_URL)
if get_capabilities.status_code == 200:
root = ET.fromstring(get_capabilities.content)
for elem in root.findall(".//{http://www.opengis.net/wms}Dimension"):
if 'name' in elem.attrib and elem.attrib['name'] == 'time':
obs_dates = elem.text.strip().split(',')
elif 'name' in elem.attrib and elem.attrib['name'] == 'elevation':
obs_elev = elem.text.strip().split(',')
layers = {}
layers_to_display = []
layers_to_call = []
for name_tag in root.findall(".//{http://www.opengis.net/wms}Name"):
if name_tag.text and name_tag.text.startswith("GLORYS12V1"):
if name_tag.text.endswith("thetao"):
layers[name_tag.text] = 'Temperature'
elif name_tag.text.endswith("so"):
layers[name_tag.text] = 'Salinity'
elif name_tag.text.endswith("uo"):
layers[name_tag.text] = 'U-Velocity'
elif name_tag.text.endswith("vo"):
layers[name_tag.text] = 'V-Velocity'
layers_to_display.append(name_tag.text)
# Populate layers_to_call with the part after the last colon
if ':' in name_tag.text:
layers_to_call.append(name_tag.text.split(':')[-1])
else:
layers_to_call.append(name_tag.text)
else:
print('ERROR: ', get_capabilities.status_code)
3. Utility function for extracting layer type
This function extracts the variable name (layer type) from a full WMS layer identifier.
WMS layers are often formatted as datasetID:variableName
.
The function isoates the text that follows the colon (:
), which is the used in subsequent API calls.
# @title
def get_layer_type(selected_layer):
if selected_layer:
stop_char = ':'
res = ''
for c in reversed(selected_layer):
if c == stop_char:
break
res = c + res
return res
return None
4. Interactive map controls: time, depth and data layer selection
This section creates interactive widgets that allow users to control the map visualization dynamically:
Time slider - selects the observation date from available time steps.
Depth slider - selects the vertical level (elevation in meters)
Layer dropdown - selects the oceanographic variable to display (e.g., potential temperature, salinity, eastward velocity, northward velocity).
Whenever the user changes one of these selections, the function update_wms_layer()
is called. This function:
Removes the previously displayed WMS layer from the map (if exists).
Construct a new WMS request URL using the selected time, depth and variable.
Adds the uploaded WMS layer to the interactive map.
Calls
update_area_coordinates()
to refresh spatial information.
This ensures that the displayed map always reflects the current user-selected parameters.
# @title
# Function to update the WMS layer
def update_wms_layer():
global m
global selected_time
global selected_elev
global selected_layer
if not selected_time or not selected_elev or not selected_layer:
print("Cannot update WMS layer: time, elevation, or layer not selected.")
return
# Update the WMS layer on the map using the original selected_elev
new_wms_url = f'https://er1.s4oceanice.eu/erddap/wms/GLORYS12V1_sea_floor_potential_temp/request?&elevation={selected_elev}&time={selected_time}'
new_wms_layer_name = selected_layer
# Find and remove the existing WMS layer based on URL and layers
existing_wms_layer = None
# Iterate over a copy of the list to avoid issues during removal
for layer in list(m.layers):
if isinstance(layer, WMSLayer):
# Check if the URL contains the base WMS service URL
if layer.url.startswith('https://er1.s4oceanice.eu/erddap/wms/GLORYS12V1_sea_floor_potential_temp/request?'):
existing_wms_layer = layer
break
if existing_wms_layer:
m.remove_layer(existing_wms_layer)
# print("Removed existing WMS layer.") # Debugging print
# Add the new WMS layer
new_wms_layer = WMSLayer(
url=new_wms_url,
layers=f'{new_wms_layer_name},Coastlines,Nations,States',
format='image/png',
transparent=True,
crs={'name': 'EPSG:4326'},
)
m.add_layer(new_wms_layer)
# Call update_area_coordinates after updating the WMS layer
update_area_coordinates()
# Time Slider
selected_time = obs_dates[0] if obs_dates else None
time_label = Label(value=str(selected_time))
def updating_time_values(change):
global selected_time
# Ensure the index is within bounds
if obs_dates and 0 <= (change.new)-1 < len(obs_dates):
selected_time = obs_dates[(change.new)-1]
time_label.value = str(selected_time)
update_wms_layer() # Update WMS layer and data when time changes
else:
selected_time = None
time_label.value = "Invalid Index or No Data"
time_slider = IntSlider(min=1, max=len(obs_dates), readout = False, continuous_update=False) if obs_dates else Label("No time data available")
if obs_dates:
time_slider.observe(updating_time_values, names='value')
display(HBox([Label('Date: '), time_slider, time_label]))
else:
display(time_slider)
# Elevation Slider
selected_elev = obs_elev[0] if obs_elev else None
elev_label = Label(value=str(selected_elev))
def updating_elev_values(change):
global selected_elev
# Ensure the index is within bounds
if obs_elev and 0 <= (change.new)-1 < len(obs_elev):
selected_elev = obs_elev[(change.new)-1]
elev_label.value = str(selected_elev)
update_wms_layer() # Update WMS layer and data when elevation changes
else:
selected_elev = None
elev_label.value = "Invalid Index or No Data"
elev_slider = IntSlider(min=1, max=len(obs_elev), readout = False, continuous_update=False) if obs_elev else Label("No depth data available")
if obs_elev:
elev_slider.observe(updating_elev_values, names='value')
display(HBox([Label('Depth (m): '), elev_slider, elev_label]))
else:
display(elev_slider)
# Layer Dropdown
layer_dropdown = Dropdown(
options=layers.values(),
description='Select Layer:',
disabled=False,
)
selected_layer = list(layers.keys())[list(layers.values()).index(layer_dropdown.value)] if layers else None
def update_selected_layer(change):
global selected_layer
selected_value = change['new']
# Find the key corresponding to the selected value
selected_layer = list(layers.keys())[list(layers.values()).index(selected_value)]
update_wms_layer() # Update WMS layer and data when layer changes
layer_dropdown.observe(update_selected_layer, names='value')
# Display the dropdown menu
display(VBox([Label('Choose a layer to display on the map:'), layer_dropdown]))
5. Interactive map & area selection
TThis section generates an interactive world map, showing the selected WMS layer (based on time, depth and variable). User can define an area of interest in two ways:
By clicking on the map - up to 4 markers can be placed, defining the corners of a polygonal area.
By manually entering coordinates - users can specify minimum and maximum latitude/longitude values to define a rectangular area.
Once four points are defined, the notebook automatically:
Calculates mean, maximum and minimum values for the selected vairable within the area.
Displays these values below the map.
Generated a download button to export the full dataset for that area.
Additional controls provided:
Clear button - removes all markers and resets the input fields, re-enabling map clicking.
Manual input fields - allow users to enter latitude/longitude ranges directly.
Upload button - applies the manually entered coordinates.
This ensure both flexible, user-friendly area selection and reproducible data extraction.
Note: please be patient. Placing the fourth marker may take longer to appear, especially for larger areas, due to Colab’s processing limitations. Similarly, changing a value on the sliders or dropdowns may take a short time to update the displayed values and the download button.
# @title
# Map Generator
m = Map(
center=(0, 0),
zoom=1,
basemap = basemaps.Esri,
layout=Layout(width='80%', height='500px', margin='10px 0 0 0'),
crs = projections.EPSG4326,
)
# Output widget to display coordinates
output = Output()
# Output widget specifically for the DataFrame and plot
data_output = Output()
plot_output = Output(layout=Layout(width='80%', height='500px', margin='10px 0 0 0'))
coordinates_list = [] # List to store coordinates of up to 4 markers
markers_list = [] # List to store the marker objects
manual_input_mode = False # Flag to indicate if manual input is active
def handle_map_click(**kwargs):
global coordinates_list
global markers_list
global manual_input_mode
if not manual_input_mode and kwargs.get('type') == 'click':
new_coordinates = kwargs.get('coordinates')
if len(coordinates_list) < 4:
coordinates_list.append(new_coordinates)
with output:
clear_output()
print(f"Selected coordinates ({len(coordinates_list)}/4): {new_coordinates}")
update_area_coordinates() # Update area coordinates after adding a marker
# Add a new marker at the clicked location
new_marker = Marker(location=new_coordinates)
m.add_layer(new_marker)
markers_list.append(new_marker)
# If this is the first marker, update the data display
if len(coordinates_list) == 1:
global coordinates # Keep the global coordinates variable for now, linked to the first marker
coordinates = coordinates_list[0]
# If 4 markers have been placed, detach the click handler
if len(coordinates_list) == 4:
m.on_interaction(handle_map_click, remove=True)
with output:
print("Maximum number of markers reached. Map clicking is disabled.")
else:
with output:
clear_output()
print("Maximum of 4 locations already selected.")
update_area_coordinates() # Update area coordinates even if max markers reached (to show the message)
m.on_interaction(handle_map_click)
# This initial layer will be replaced when update_data is called by interacting with widgets/map
if layers and obs_dates and obs_elev:
initial_wms = WMSLayer(
url=f'https://er1.s4oceanice.eu/erddap/wms/GLORYS12V1_sea_floor_potential_temp/request?&elevation={selected_elev}&time={selected_time}',
layers=f'{selected_layer},Coastlines,Nations,States',
format='image/png',
transparent=True,
crs={'name': 'EPSG:4326'},
)
m.add(initial_wms)
# Function to clear markers and manual inputs
def clear_markers(b=None): # Added default argument for button click
global coordinates_list
global markers_list
global manual_input_mode
for marker in markers_list:
m.remove_layer(marker)
markers_list = []
coordinates_list = []
# Also clear manual input fields
min_lat_input.value = 0.0
max_lat_input.value = 0.0
min_lon_input.value = 0.0
max_lon_input.value = 0.0
with output:
clear_output()
print("All markers and manual inputs cleared.")
update_area_coordinates() # Update area coordinates after clearing markers
# Re-attach the click handler after clearing markers
m.on_interaction(handle_map_click)
with output:
print("Map clicking is re-enabled.")
# Optionally clear data output and plot when markers are cleared
with data_output:
clear_output(wait=True)
print("") # Empty the values list under the map
with plot_output:
clear_output(wait=True)
manual_input_mode = False # Reset manual input mode
clear_button = Button(description='Clear inputs')
clear_button.on_click(clear_markers)
# Create input textboxes for manual coordinates (min/max lat/lon)
min_lat_input = FloatText(description='Min Lat:')
max_lat_input = FloatText(description='Max Lat:')
min_lon_input = FloatText(description='Min Lon:')
max_lon_input = FloatText(description='Max Lon:')
# Button to use manual coordinates
use_manual_button = Button(description='Upload coordinates')
def use_manual_coordinates(b):
global coordinates_list
global markers_list
global manual_input_mode
manual_input_mode = True # Set manual input mode to True
# Clear existing markers from map clicks
for marker in markers_list:
m.remove_layer(marker)
markers_list = []
coordinates_list = [] # Clear coordinates from map clicks
try:
# Get coordinates from input fields
min_lat = min_lat_input.value
max_lat = max_lat_input.value
min_lon = min_lon_input.value
max_lon = max_lon_input.value
# Validate coordinates (basic validation: check if not None and within a reasonable range)
if isinstance(min_lat, (int, float)) and isinstance(max_lat, (int, float)) and isinstance(min_lon, (int, float)) and isinstance(max_lon, (int, float)) and \
-90 <= min_lat <= 90 and -90 <= max_lat <= 90 and -180 <= min_lon <= 180 and -180 <= max_lon <= 180 and \
min_lat <= max_lat and min_lon <= max_lon:
# Define the 4 corner coordinates based on min/max lat/lon
coordinates_list = [
(min_lat, min_lon),
(min_lat, max_lon),
(max_lat, max_lon),
(max_lat, min_lon)
]
with output:
clear_output()
print("Using manually entered coordinates for the area:")
print(f" Min Lat: {min_lat}, Max Lat: {max_lat}, Min Lon: {min_lon}, Max Lon: {max_lon}")
# Add markers for manually entered coordinates (optional, but good for visualization)
for lat, lon in coordinates_list:
new_marker = Marker(location=(lat, lon))
m.add_layer(new_marker)
markers_list.append(new_marker)
update_area_coordinates() # Update area coordinates using manual input
else:
with output:
clear_output()
print("Invalid coordinate input. Please enter valid numbers between -90 and 90 for latitude and -180 and 180 for longitude, ensuring Min Lat <= Max Lat and Min Lon <= Max Lon.")
manual_input_mode = False # Reset manual input mode on invalid input
except Exception as e:
with output:
clear_output()
print(f"Error processing manual input: {e}")
manual_input_mode = False # Reset manual input mode on error
use_manual_button.on_click(use_manual_coordinates)
# Create a download button widget, initially hidden
download_button = HTML(value='', layout=Layout(visibility='hidden'))
# Arrange the clear button, output, map, data output, and download button vertically in a VBox
display(VBox([clear_button,
VBox([Label("Enter area coordinates manually or click on the map to automatically select coordinates:"),
HBox([min_lat_input, max_lat_input]),
HBox([min_lon_input, max_lon_input]),
HBox([use_manual_button, Label('Use this button once you entered coordinates manually')])]),
output,
m,
data_output,
download_button]))
6. Extracting, summarizing and downloading data for the selected area
This section processes the spatial selection made by the user in the section above (either via map clicks or manual coordinate input) and after determining the bounding box (minimum/maximum latitude and longitude), builds an ERDDAP API request URL to extract the relevant dataset subset based on:
Selected variable (e.g., potential temperature, salinity, velocity).
Chosen depth level.
Chosen date/time.
Selected geographic bounds.
After this, it fetches and processes the data to compute key descriptive statistics (i.e. Mean, Maximum, Minimum, Standard deviation) and displays them along with the variable descriptions.
Finally, it generates a clickable link for downloading the subsetted data in CSV format.
# @title
# The download_button is now created and displayed in cell 1d8a8d54
# This cell will only update its content and visibility
def update_area_coordinates():
if len(coordinates_list) == 4:
# Extract latitudes and longitudes
latitudes = [coord[0] for coord in coordinates_list]
longitudes = [coord[1] for coord in coordinates_list]
# Find min and max latitude and longitude
min_latitude = min(latitudes)
max_latitude = max(latitudes)
min_longitude = min(longitudes)
max_longitude = max(longitudes)
with output: # Display in the output widget
clear_output()
print(f"Area coordinates:")
print(f" Min Latitude: {min_latitude}")
print(f" Max Latitude: {max_latitude}")
print(f" Min Longitude: {max_longitude}")
print(f" Max Longitude: {min_longitude}")
layer_type = get_layer_type(selected_layer)
try:
elev_for_data_api = abs(float(selected_elev))
except ValueError:
with data_output:
clear_output(wait=True)
print(f"Error: Could not convert elevation '{selected_elev}' to a number for the data API call.")
return # Exit the function if conversion fails
api_url = f'{BASE_DATA_URL}{layer_type}%5B({selected_time}):1:({selected_time})%5D%5B({elev_for_data_api}):1:({elev_for_data_api})%5D%5B({min_latitude}):1:({max_latitude})%5D%5B({min_longitude}):1:({max_longitude})%5D'
#print(api_url) # Optional: print API URL for debugging
try:
# Read the CSV including the first row to get the description
full_df = pd.read_csv(api_url)
# Get the description from the first row, last column
variable_description = full_df.iloc[0, -1]
# Read the CSV again, skipping the first row for data calculation
df = pd.read_csv(api_url, skiprows=[1])
with data_output:
clear_output(wait=True)
if not df.empty:
# Assuming the last column contains the variable of interest
variable_data = df.iloc[:, -1]
mean_value = variable_data.mean()
max_value = variable_data.max()
min_value = variable_data.min()
std_value = variable_data.std()
# Display the statistics as a simple list with the description after the value
print(f"Statistics for the selected area:")
print(f" Mean: {mean_value:.2f} ({variable_description})")
print(f" Max: {max_value:.2f} ({variable_description})")
print(f" Min: {min_value:.2f} ({variable_description})")
print(f" Standard Deviation: {std_value:.2f} ({variable_description})") # Display standard deviation
else:
print("No data available for the selected area and parameters.")
download_button.value = f'<a href="{api_url}" download="data.csv" style="display: inline-block; padding: 6px 12px; margin-bottom: 0; font-size: 14px; font-weight: normal; line-height: 1.42857143; text-align: center; white-space: nowrap; vertical-align: middle; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; background-image: none; border: 1px solid transparent; border-radius: 4px; color: #333; background-color: #fff; border-color: #ccc;">Click here to download data (CSV)</a>'
download_button.layout.visibility = 'visible'
except Exception as e:
with output:
print(f'ERROR fetching data: {e}')
# Hide the download button if there's an error
download_button.layout.visibility = 'hidden'
with data_output:
clear_output(wait=True)
else:
with output: # Display in the output widget
clear_output()
print(f"Please place exactly 4 markers on the map to define the area.")
# Hide the download button if not 4 markers are placed
# Ensure the download_button variable is accessible
download_button.layout.visibility = 'hidden'
with data_output:
clear_output(wait=True)