|
| 1 | +--- |
| 2 | +title: "Animated polar plot with oceanographic data" |
| 3 | +date: 2020-06-12T09:56:36+02:00 |
| 4 | +draft: false |
| 5 | +description: "This post describes how to animate some oceanographic measurements in a tweaked polar plot" |
| 6 | +categories: ["tutorials"] |
| 7 | +displayInList: true |
| 8 | +author: Kevin Balem |
| 9 | +resources: |
| 10 | +- name: featuredImage |
| 11 | + src: "thumbnail.png" |
| 12 | + params: |
| 13 | + showOnTop: false |
| 14 | +--- |
| 15 | +The **ocean** is a key component of the Earth climate system. It thus needs a continuous real-time monitoring to help scientists better understand its dynamic and predict its evolution. All around the world, oceanographers have managed to join their efforts and set up a [Global Ocean Observing System](https://www.goosocean.org) among which [**Argo**](http://www.argo.ucsd.edu/) is a key component. Argo is a global network of nearly 4000 autonomous probes or floats measuring pressure, temperature and salinity from the surface to 2000m depth every 10 days. The localisation of these floats is nearly random between the 60th parallels (see live coverage [here](http://collab.umr-lops.fr/app/divaa/)). All data are collected by satellite in real-time, processed by several data centers and finally merged in a single dataset (collecting more than 2 millions of vertical profiles data) made freely available to anyone. |
| 16 | + |
| 17 | +In this particular case, we want to plot temperature (surface and 1000m deep) data measured by those floats, for the period 2010-2020 and for the Mediterranean sea. We want this plot to be circular and animated, now you start to get the title of this post : **Animated polar plot** |
| 18 | + |
| 19 | +First we need some data to work with. To retrieve our temperature values from argo float, we use [**Argopy**](https://argopy.readthedocs.io) wich is a python library that aims to ease Argo data access, manipulation and visualisation for standard users as well as Argo experts and operators. Argopy returns [xarray](http://xarray.pydata.org) dataset objects, wich make our analysis much easier. |
| 20 | +```python |
| 21 | +import pandas as pd |
| 22 | +import numpy as np |
| 23 | +from argopy import DataFetcher as ArgoDataFetcher |
| 24 | +argo_loader = ArgoDataFetcher(cache=True) |
| 25 | +# |
| 26 | +# Query surface and 1000m temp in Med sea with argopy |
| 27 | +df1 = argo_loader.region([-1.2,29.,28.,46.,0,10.,'2009-12','2020-01']).to_xarray() |
| 28 | +df2 = argo_loader.region([-1.2,29.,28.,46.,975.,1025.,'2009-12','2020-01']).to_xarray() |
| 29 | +# |
| 30 | +``` |
| 31 | + |
| 32 | +Here we create some arrays we'll use for plotting, we set up a date array and extract day of the year and year itself that will be usefull. Then to build our temperature array, we use xarray very usefull methods : `where()` and `mean()`. Then we build a pandas Dataframe, because it's prettier ! |
| 33 | +```python |
| 34 | +# Weekly date array |
| 35 | +daterange=np.arange('2010-01-01','2020-01-03',dtype='datetime64[7D]') |
| 36 | +dayoftheyear=pd.DatetimeIndex(np.array(daterange,dtype='datetime64[D]')+3).dayofyear # middle of the week |
| 37 | +activeyear=pd.DatetimeIndex(np.array(daterange,dtype='datetime64[D]')+3).year # extract year |
| 38 | + |
| 39 | +# Init final arrays |
| 40 | +tsurf=np.zeros(len(daterange)) |
| 41 | +t1000=np.zeros(len(daterange)) |
| 42 | + |
| 43 | +# Filling arrays |
| 44 | +for i in range(len(daterange)): |
| 45 | + i1=(df1['TIME']>=daterange[i])&(df1['TIME']<daterange[i]+7) |
| 46 | + i2=(df2['TIME']>=daterange[i])&(df2['TIME']<daterange[i]+7) |
| 47 | + tsurf[i]=df1.where(i1,drop=True)['TEMP'].mean().values |
| 48 | + t1000[i]=df2.where(i2,drop=True)['TEMP'].mean().values |
| 49 | + |
| 50 | +# Creating dataframe |
| 51 | +d = {'date': np.array(daterange,dtype='datetime64[D]'), 'tsurf': tsurf, 't1000': t1000} |
| 52 | +ndf = pd.DataFrame(data=d) |
| 53 | +ndf.head() |
| 54 | + |
| 55 | + date tsurf t1000 |
| 56 | +0 2009-12-31 15.725000 13.306133 |
| 57 | +1 2010-01-07 15.530414 13.315658 |
| 58 | +2 2010-01-14 15.307378 13.300347 |
| 59 | +3 2010-01-21 14.954195 13.300647 |
| 60 | +4 2010-01-28 14.708816 13.300274 |
| 61 | +``` |
| 62 | + |
| 63 | + |
| 64 | +Then it's time to plot, for that we first need to import what we need, and set some usefull variables. |
| 65 | +```python |
| 66 | +import matplotlib.pyplot as plt |
| 67 | +import matplotlib |
| 68 | +plt.rcParams['xtick.major.pad']='17' |
| 69 | +plt.rcParams["axes.axisbelow"] = False |
| 70 | +matplotlib.rc('axes',edgecolor='w') |
| 71 | +from matplotlib.lines import Line2D |
| 72 | +from matplotlib.animation import FuncAnimation |
| 73 | +from IPython.display import HTML |
| 74 | + |
| 75 | +big_angle= 360/12 # How we split our polar space |
| 76 | +date_angle=((360/365)*dayoftheyear)*np.pi/180 # For a day, a corresponding angle |
| 77 | +# inner and outer ring limit values |
| 78 | +inner=10 |
| 79 | +outer=30 |
| 80 | +# setting our color values |
| 81 | +ocean_color = ["#ff7f50","#004752"] |
| 82 | +``` |
| 83 | + |
| 84 | +Then we want to make our axes like we want, for that we build a function `dress_axes` that will be called during the animation process. Here we plot some bars with an offset (combination of `bottom` and `ylim` after). Those bars are actually our background, and the offset allows us to plot a legend in the middle of the plot. |
| 85 | +```python |
| 86 | +def dress_axes(ax): |
| 87 | + ax.set_facecolor('w') |
| 88 | + ax.set_theta_zero_location("N") |
| 89 | + ax.set_theta_direction(-1) |
| 90 | + # Here is how we position the months labels |
| 91 | + middles=np.arange(big_angle/2 ,360, big_angle)*np.pi/180 |
| 92 | + ax.set_xticks(middles) |
| 93 | + ax.set_xticklabels(['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August','September','October','November','December']) |
| 94 | + ax.set_yticks([15,20,25]) |
| 95 | + ax.set_yticklabels(['15°C','20°C','25°C']) |
| 96 | + # Changing radial ticks angle |
| 97 | + ax.set_rlabel_position(359) |
| 98 | + ax.tick_params(axis='both',color='w') |
| 99 | + plt.grid(None,axis='x') |
| 100 | + plt.grid(axis='y',color='w', linestyle=':', linewidth=1) |
| 101 | + # Here is the bar plot that we use as background |
| 102 | + bars = ax.bar(middles, outer, width=big_angle*np.pi/180, bottom=inner, color='lightgray', edgecolor='w',zorder=0) |
| 103 | + plt.ylim([2,outer]) |
| 104 | + # Custom legend |
| 105 | + legend_elements = [Line2D([0], [0], marker='o', color='w', label='Surface', markerfacecolor=ocean_color[0], markersize=15), |
| 106 | + Line2D([0], [0], marker='o', color='w', label='1000m', markerfacecolor=ocean_color[1], markersize=15), |
| 107 | + ] |
| 108 | + ax.legend(handles=legend_elements, loc='center', fontsize=13, frameon=False) |
| 109 | + # Main title for the figure |
| 110 | + plt.suptitle('Mediterranean temperature from Argo profiles',fontsize=16,horizontalalignment='center') |
| 111 | +``` |
| 112 | +From there we can plot the frame of our plot. |
| 113 | +```python |
| 114 | +fig = plt.figure(figsize=(10,10)) |
| 115 | +ax = fig.add_subplot(111, polar=True) |
| 116 | +dress_axes(ax) |
| 117 | +plt.show() |
| 118 | +``` |
| 119 | + |
| 120 | + |
| 121 | + |
| 122 | +Then it's finally time to plot our data. Since we want to animated the plot, we'll build a function that will be called in `FuncAnimation` later on. Since the state of the plot changes on every time stamp, we have to redress the axes for each frame, easy with our `dress_axes` function. Then we plot our temperature data using basic `plot()` : Thin lines for historical measurements and a thicker ones for the current year. |
| 123 | +```python |
| 124 | +def draw_data(i): |
| 125 | + # Clear |
| 126 | + ax.cla() |
| 127 | + # Redressing axes |
| 128 | + dress_axes(ax) |
| 129 | + # Limit between thin lines and thick line, this is current date minus 51 weeks basically. |
| 130 | + # why 51 and not 52 ? That create a small gap before the current date, wich is prettier |
| 131 | + i0=np.max([i-51,0]) |
| 132 | + |
| 133 | + ax.plot(date_angle[i0:i+1], ndf['tsurf'][i0:i+1],'-',color=ocean_color[0],alpha=1.0,linewidth=5) |
| 134 | + ax.plot(date_angle[0:i+1], ndf['tsurf'][0:i+1],'-',color=ocean_color[0],linewidth=0.7) |
| 135 | + |
| 136 | + ax.plot(date_angle[i0:i+1], ndf['t1000'][i0:i+1],'-',color=ocean_color[1],alpha=1.0,linewidth=5) |
| 137 | + ax.plot(date_angle[0:i+1], ndf['t1000'][0:i+1],'-',color=ocean_color[1],linewidth=0.7) |
| 138 | + |
| 139 | + # Plotting a line to spot the current date easily |
| 140 | + ax.plot([date_angle[i],date_angle[i]],[inner,outer],'k-',linewidth=0.5) |
| 141 | + # Display the current year as a title, just beneath the suptitle |
| 142 | + plt.title(str(activeyear[i]),fontsize=16,horizontalalignment='center') |
| 143 | + |
| 144 | +# Test it |
| 145 | +draw_data(322) |
| 146 | +plt.show() |
| 147 | +``` |
| 148 | + |
| 149 | + |
| 150 | + |
| 151 | +Finally it's time to animate, using `FuncAnimation`. Then we save it as a mp4 file or we display it in our notebook with `HTML(anim.to_html5_video())`. |
| 152 | +```python |
| 153 | +anim = FuncAnimation(fig, draw_data, interval=40, frames=len(daterange)-1, repeat=False) |
| 154 | +#anim.save('ArgopyUseCase_MedTempAnimation.mp4') |
| 155 | +HTML(anim.to_html5_video()) |
| 156 | +``` |
| 157 | + |
0 commit comments