Skip to content

Instantly share code, notes, and snippets.

@adamml
Created October 31, 2024 19:22
Show Gist options
  • Save adamml/508030e9d3befd7c2165b6c9c85e2e96 to your computer and use it in GitHub Desktop.
Save adamml/508030e9d3befd7c2165b6c9c85e2e96 to your computer and use it in GitHub Desktop.
Argo data animation builder
"""A Python script to pull Argo float location data through time from a JSON
file and add it to a Matplotlib animation with colour coding for a dependent
parameter.
Data can be obtained from
https://erddap.ifremer.fr/erddap/tabledap/ArgoFloats.html
The full URL used for this version is:
https://erddap.ifremer.fr/erddap/tabledap/ArgoFloats.json?
time%2Clatitude%2Clongitude%2Cpres%2Ctemp&
time%3E=1997-07-28T20%3A26%3A20Z&
time%3C=2024-11-01&
position_qc=%221%22&
pres%3E0&
pres%3C=3&
pres_qc=%221%22&
temp_qc=%221%22
Also uses the land polygons from:
https://raw.githubusercontent.com/martynafford/natural-earth-geojson/refs/
heads/master/110m/physical/ne_110m_land.json
@author: Adam Leadbetter (@adamml)
@date: 2024-10-29
@version: 1.0"""
import cmocean
from datetime import datetime
import json
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation, FFMpegWriter
from matplotlib.collections import PolyCollection
from matplotlib.colors import ListedColormap
import urllib.request
import urllib.response
FILE = "ArgoFloats_9719_0b8f_adb1.json"
"""Erddap results file to process"""
MIN_YEAR = 2005
"""The first calendar year to process"""
MAX_YEAR = 2024
"""The final calendar year to process"""
SECONDS_PER_YEAR = 6
"""This is a parameter to control key framing of objects in animation. It
describes the number of seconds to display a given calendar year for in the
final rendering"""
FRAMES_PER_SECOND = 30
"""This is a parameter to control the frames per second calculation for the
animation"""
FRAMES_TO_HIDE = 5
"""This parameter determines how many frames it takes to hide the float"""
PALETTE_URL = ("https://raw.githubusercontent.com/kthyng/cmocean-odv/refs/" +
"heads/master/pal/thermal.pal")
"""The URL from which to read the CMOCEAN colour palette to use in the
final visualisation. Reading direct from the URL means not having to work
with importing the package in Blender, which can sometimes be awkward."""
GEOJSON_URL = "https://raw.githubusercontent.com/martynafford/natural-earth-geojson/refs/heads/master/110m/physical/ne_110m_land.json"
frame_data = [[[],[],[]] for x in range(0,((MAX_YEAR+1)-MIN_YEAR)*(
SECONDS_PER_YEAR*FRAMES_PER_SECOND))]
def pointProcessor(x: list, min_year:int, max_year: int,
seconds_per_year: int, frames_per_second: int,
frames_to_hide: int):
"""Process an Erddap results row and plot it"""
global frame_data
try:
if (datetime.strptime(x[0], "%Y-%m-%dT%H:%M:%SZ").year >= min_year and
datetime.strptime(x[0], "%Y-%m-%dT%H:%M:%SZ").year <= max_year):
key_frame: int = int((datetime.strptime(x[0],
"%Y-%m-%dT%H:%M:%SZ")-
datetime.strptime(f"{min_year}-1-1",
"%Y-%m-%d")).days *
((seconds_per_year * frames_per_second)/365))
for f in range(key_frame, key_frame+frames_to_hide):
frame_data[f][0].append(x[2])
frame_data[f][1].append(x[1])
frame_data[f][2].append(x[4])
except TypeError:
pass
#
# Initial plot setup
#
fig = plt.figure()
fig.patch.set_facecolor('#f1e9d2')
gs = fig.add_gridspec(16, 16)
#
# This is going to be our global map
#
ax0 = fig.add_subplot(gs[0:12, :])
ax0.patch.set_facecolor('#f1e9d2')
ax0.set_xlim([-180, 180])
ax0.set_ylim([-90, 90])
ax0.axis('off')
#
# Read and parse the Natural Earth data as GeoJSON from the GitHub repo
#
with urllib.request.urlopen(GEOJSON_URL) as r:
g = json.load(r)["features"]
for i, f in enumerate(g):
c = f["geometry"]["coordinates"][0]
xs = [x[0] for x in c]
ys = [x[1] for x in c]
poly = PolyCollection([list(zip(xs, ys))])
poly.set_color('#8B4513')
ax0.add_collection(poly)
#
# Read the data from a downloaded results file. The data is expected to be in
# a JSON file to the format exported from an Erddap query result
#
with open(FILE, "r") as f:
d = json.load(f)
any(pointProcessor(x, MIN_YEAR, MAX_YEAR, SECONDS_PER_YEAR,
FRAMES_PER_SECOND, FRAMES_TO_HIDE
) for x in d["table"]["rows"])
#
# Set up the profile data plots
#
argo_points= ax0.scatter([],[],
c=[],
s=1,
vmax=40,vmin=0,
cmap=cmocean.cm.thermal)
#
# This is going to be our timeline
#
ax1 = fig.add_subplot(gs[13:14, :])
ax1.patch.set_facecolor('#f1e9d2')
ax1.set_ylim([-1, 1])
ax1.plot([MIN_YEAR,MAX_YEAR+1], [0,0], color="#8B4513", zorder=20)
for y in range(MIN_YEAR,MAX_YEAR+2):
ax1.plot([y,y], [-1,0], color="#8B4513", zorder=20)
ax1.axis('off')
timer0 = ax1.scatter([],[],color="#8B4513", s=[100], zorder=0)
timer1 = ax1.scatter([],[],color="#f1e9d2", s=[70], zorder=10)
def animate(i):
try:
timer0.set_offsets([MIN_YEAR + (i/(FRAMES_PER_SECOND*SECONDS_PER_YEAR)),0])
timer1.set_offsets([MIN_YEAR + (i/(FRAMES_PER_SECOND*SECONDS_PER_YEAR)),0])
argo_points.set_offsets([[frame_data[i][0][ii], frame_data[i][1][ii]] for ii, x in enumerate(frame_data[i][0])])
argo_points.set_array(frame_data[i][2])
except IndexError:
pass
#
# This is going to be our colour scale
#
ax2 = fig.add_subplot(gs[15:16, :])
ax2.set_xlim([0, 1])
ax2.set_ylim([0, 1])
ax2.axis('off')
ax2.imshow([[0,1], [0,1]],
cmap=cmocean.cm.thermal,
interpolation="bicubic", aspect="auto")
anim = FuncAnimation(
fig,
animate,
frames = range(0,((MAX_YEAR+1)-MIN_YEAR)*(
FRAMES_PER_SECOND*SECONDS_PER_YEAR)+1),
interval = 1)
anim.save(f"Argo_SeaSurfaceTemp_{MIN_YEAR}_to_{MAX_YEAR}.MP4",
writer=FFMpegWriter(fps=FRAMES_PER_SECOND))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment