Created
October 31, 2024 19:22
-
-
Save adamml/508030e9d3befd7c2165b6c9c85e2e96 to your computer and use it in GitHub Desktop.
Argo data animation builder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""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