#!/bin/env python
# -*coding: UTF-8 -*-
#
# We try to import dependencies and catch missing module errors in order to avoid to load argopy just because
# Matplotlib is not installed.
#
# Decorator warnUnless is mandatory
#
import numpy as np
import pandas as pd
import warnings
from contextlib import contextmanager
from argopy.errors import InvalidDashboard
from argopy.utilities import warnUnless, check_wmo
try:
with_matplotlib = True
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import matplotlib.cm as cm
import matplotlib.colors as mcolors
except ModuleNotFoundError:
warnings.warn("argopy requires matplotlib installed for any plotting functionality")
with_matplotlib = False
try:
with_cartopy = True
import cartopy
import cartopy.crs as ccrs
import cartopy.feature as cfeature
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
land_feature = cfeature.NaturalEarthFeature(
category="physical", name="land", scale="50m", facecolor=[0.4, 0.6, 0.7]
)
except ModuleNotFoundError:
with_cartopy = False
# Default styles:
STYLE = {"axes": "whitegrid", "palette": "Set1"}
try:
import seaborn as sns
STYLE["axes"] = "dark"
with_seaborn = True
except ModuleNotFoundError:
with_seaborn = False
@contextmanager
def axes_style(style: str = STYLE['axes']):
""" Provide a context for plots
The point is to handle the availability of :mod:`seaborn` or not and to be able to use::
with axes_style(style):
fig, ax = plt.subplots()
in all situations.
"""
if with_seaborn: # Execute within a seaborn context:
with sns.axes_style(style):
yield
else: # Otherwise do nothing
yield
[docs]def open_dashboard(wmo=None, cyc=None, width="100%", height=1000, url=None, type="ea"):
""" Insert the Euro-Argo dashboard page in a notebook cell
Parameters
----------
wmo: int
The float WMO to display. By default, this is set to None and will insert the general dashboard.
Returns
-------
:class:`IPython.lib.display.IFrame`
"""
if type not in ["ea", "eric", "coriolis"]:
raise InvalidDashboard("Invalid dashboard type")
from IPython.display import IFrame
if url is None:
if type == "ea" or type == "eric": # Open Euro-Argo dashboard
if wmo is None:
url = "https://fleetmonitoring.euro-argo.eu"
else:
wmo = check_wmo(wmo)
url = "https://fleetmonitoring.euro-argo.eu/float/{}".format(str(wmo[0]))
elif type == 'coriolis': # Open Coriolis dashboard
if wmo is not None:
wmo = check_wmo(wmo)
url = ("https://co-insitucharts.ifremer.fr/platform/{}/charts").format(
str(wmo[0])
)
# return open_dashboard(url=("https://co-insitucharts.ifremer.fr/platform/{}/charts").format(str(self.WMO[0])), **kw)
# # Note that argovis doesn't allow X-Frame insertion !
# elif type == 'argovis':
# if cyc is None:
# url = "https://argovis.colorado.edu/catalog/platforms/{}/page".format(str(wmo))
# else:
# url = "https://argovis.colorado.edu/catalog/profiles/{}_{}/page".format(str(wmo),str(cyc))
return IFrame(url, width=width, height=height)
[docs]def open_sat_altim_report(WMO=None, embed='dropdown'):
""" Insert the CLS Satellite Altimeter Report figure in notebook cell
This is the method called when using the facade fetcher methods ``plot``:
>>> DataFetcher().float(6902745).plot('qc_altimetry')
Parameters
----------
WMO: int or list
The float WMO to display. By default, this is set to None and will insert the general dashboard.
embed: {'list', 'slide', 'dropdown'}, default: 'dropdown'
Set the embedding method. If set to None, simply return the list of urls to figures.
"""
if embed in ['list', 'slide', 'dropdown']:
from IPython.display import Image
if embed in ['list']:
from IPython.display import display
if embed in ['slide', 'dropdown']:
import ipywidgets as wg
WMOs = check_wmo(WMO)
urls = []
urls_dict = {}
for this_wmo in WMOs:
url = "https://data-argo.ifremer.fr/etc/argo-ast9-item13-AltimeterComparison/figures/%i.png" % this_wmo
if embed == 'list':
urls.append(Image(url, embed=True))
else:
urls.append(url)
urls_dict[this_wmo] = url
if embed == 'list':
return display(*urls)
elif embed == 'slide':
def f(Float):
return Image(url=urls[Float])
return wg.interact(f, Float=wg.IntSlider(min=0, max=len(urls) - 1, step=1))
elif embed == 'dropdown':
def f(Float):
return Image(url=urls_dict[int(Float)])
return wg.interact(f, Float=[str(wmo) for wmo in WMOs])
else:
return urls_dict
class discrete_coloring:
""" Handy class to manage discrete coloring and the associated colorbar
Example
-------
This class can be used like this::
year_range = np.arange(2002,2010)
dc = discrete_coloring(name='Spectral', N=len(year_range) )
plt.scatter(this['LONGITUDE'], this['LATITUDE'], c=this['TIME.year'],
cmap=dc.cmap, vmin=year_range[0], vmax=year_range[-1])
dc.cbar(ticklabels=yr_range, fraction=0.03, label='Years')
"""
def __init__(self, name="Set1", N=12):
"""
Parameters
----------
name: str
Name if the colormap to use. Default: 'Set1'
N: int
Number of colors to reduce the colormap to. Default: 12
"""
self.name = name
self.Ncolors = N
@property
def cmap(self):
"""Return a discrete colormap from a quantitative or continuous colormap name
Returns
-------
:class:`matplotlib.colors.LinearSegmentedColormap`
"""
name = self.name
K = self.Ncolors
if name in [
"Set1",
"Set2",
"Set3",
"Pastel1",
"Pastel2",
"Paired",
"Dark2",
"Accent",
]:
# Segmented (or quantitative) colormap:
N_ref = {
"Set1": 9,
"Set2": 8,
"Set3": 12,
"Pastel1": 9,
"Pastel2": 8,
"Paired": 12,
"Dark2": 8,
"Accent": 8,
}
N = N_ref[name]
cmap = plt.get_cmap(name=name)
colors_i = np.concatenate(
(np.linspace(0, 1.0, N), (0.0, 0.0, 0.0, 0.0)), axis=0
)
cmap = cmap(colors_i) # N x 4
n = np.arange(0, N)
new_n = n.copy()
if K > N:
for k in range(N, K):
r = np.roll(n, -k)[0][np.newaxis]
new_n = np.concatenate((new_n, r), axis=0)
new_cmap = cmap.copy()
new_cmap = cmap[new_n, :]
new_cmap = mcolors.LinearSegmentedColormap.from_list(
name + "_%d" % K, colors=new_cmap, N=K
)
elif name == "Month":
clist = [
"darkslateblue",
"skyblue",
"powderblue",
"honeydew",
"lemonchiffon",
"pink",
"salmon",
"deeppink",
"gold",
"chocolate",
"darkolivegreen",
"cadetblue",
]
cmap = mcolors.LinearSegmentedColormap.from_list("my_colormap", clist)
N = 12
colors_i = np.concatenate((np.linspace(0, 1.0, N), (0.0, 0.0, 0.0, 0.0)))
colors_rgba = cmap(colors_i)
indices = np.linspace(0, 1.0, N + 1)
cdict = {}
for ki, key in enumerate(("red", "green", "blue")):
cdict[key] = [
(indices[i], colors_rgba[i - 1, ki], colors_rgba[i, ki])
for i in np.arange(N + 1)
]
new_cmap = mcolors.LinearSegmentedColormap("month_%d" % N, cdict, N)
else:
# Continuous colormap:
N = K
cmap = plt.get_cmap(name=name)
colors_i = np.concatenate((np.linspace(0, 1.0, N), (0.0, 0.0, 0.0, 0.0)))
colors_rgba = cmap(colors_i) # N x 4
indices = np.linspace(0, 1.0, N + 1)
cdict = {}
for ki, key in enumerate(("red", "green", "blue")):
cdict[key] = [
(indices[i], colors_rgba[i - 1, ki], colors_rgba[i, ki])
for i in np.arange(N + 1)
]
# Return colormap object.
new_cmap = mcolors.LinearSegmentedColormap(cmap.name + "_%d" % N, cdict, N)
self._colormap = new_cmap
return new_cmap
def cbar(self, ticklabels=None, **kwargs):
"""Return a colorbar with adjusted tick labels
Returns
-------
:class:`matplotlib.pyplot.colorbar`
"""
cmap = self.cmap
ncolors = self.Ncolors
mappable = cm.ScalarMappable(cmap=cmap)
mappable.set_array([])
mappable.set_clim(-0.5, ncolors + 0.5)
colorbar = plt.colorbar(mappable, **kwargs)
colorbar.set_ticks(np.linspace(0, ncolors, ncolors))
colorbar.set_ticklabels(ticklabels)
self._colorbar = colorbar
return colorbar
def to_rgba(self, range, value):
""" Return the RGBA color for a given value of the colormap and a range """
norm = mpl.colors.Normalize(vmin=range[0], vmax=range[-1])
scalarMap = cm.ScalarMappable(norm=norm, cmap=self.cmap)
return scalarMap.to_rgba(value)
def latlongrid(ax, dx="auto", dy="auto", fontsize="auto", **kwargs):
""" Add latitude/longitude grid line and labels to a cartopy geoaxes
Parameters
----------
ax: cartopy.mpl.geoaxes.GeoAxesSubplot
Cartopy axes to add the lat/lon grid to
dx: 'auto' or float
Grid spacing along longitude
dy: 'auto' or float
Grid spacing along latitude
fontsize: 'auto' or int
Grid label font size
Returns
-------
class:`cartopy.mpl.geoaxes.GeoAxesSubplot.gridlines`
"""
if not isinstance(ax, cartopy.mpl.geoaxes.GeoAxesSubplot):
raise ValueError("Please provide a cartopy.mpl.geoaxes.GeoAxesSubplot instance")
defaults = {"linewidth": 0.5, "color": "gray", "alpha": 0.5, "linestyle": ":"}
gl = ax.gridlines(crs=ax.projection, draw_labels=True, **{**defaults, **kwargs})
if dx != "auto":
gl.xlocator = mticker.FixedLocator(np.arange(-180, 180 + 1, dx))
if dy != "auto":
gl.ylocator = mticker.FixedLocator(np.arange(-90, 90 + 1, dy))
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER
# Cartopy <= 0.18:
# gl.xlabels_top = False
# gl.ylabels_right = False
# Cartopy >= 0.18:
gl.top_labels = False
gl.right_labels = False
if fontsize != "auto":
gl.xlabel_style = {"fontsize": fontsize}
gl.ylabel_style = {"fontsize": fontsize}
return gl
@warnUnless(with_matplotlib, "requires matplotlib installed")
def plot_trajectory(
df: pd.core.frame.DataFrame,
style: str = STYLE["axes"],
add_legend: bool = True,
palette: str = STYLE["palette"],
set_global: bool = False,
with_cartopy: bool = with_cartopy,
with_seaborn: bool = with_seaborn,
**kwargs
):
""" Plot trajectories for an Argo index dataframe
This function is called by the Data and Index fetchers method 'plot' with the 'trajectory' option::
from argopy import IndexFetcher as ArgoIndexFetcher
from argopy import DataFetcher as ArgoDataFetcher
obj = ArgoIndexFetcher().float([6902766, 6902772, 6902914, 6902746])
# OR
obj = ArgoDataFetcher().float([6902766, 6902772, 6902914, 6902746])
fig, ax = obj.plot('trajectory')
Parameters
----------
df: Pandas DataFrame
Input data with columns: 'wmo','longitude','latitude'.
style: str
Define the axes style: 'white', 'darkgrid', 'whitegrid', 'dark', 'ticks'. Only used if Seaborn is available.
add_legend: bool
Add a box legend with list of floats. True by default for a maximum of 15 floats, otherwise no legend.
palette: str
Define colors to be used for floats: 'Set1' (default) or any other matplotlib colormap or name of
a Seaborn palette (deep, muted, bright, pastel, dark, colorblind).
set_global: bool
Plot trajectories on a global world map or not. False by default.
Returns
-------
fig: :class:`matplotlib.figure.Figure`
ax: :class:`matplotlib.axes.Axes`
"""
with axes_style(style):
# Set-up the figure and axis:
defaults = {"figsize": (10, 6), "dpi": 90}
if with_cartopy:
subplot_kw = {"projection": ccrs.PlateCarree()}
fig, ax = plt.subplots(**{**defaults, **kwargs}, subplot_kw=subplot_kw)
ax.add_feature(land_feature, edgecolor="black")
else:
fig, ax = plt.subplots(**{**defaults, **kwargs})
# How many float in this dataset ?
nfloat = len(df.groupby("wmo").first())
# Let's do the plot:
if with_seaborn:
mypal = sns.color_palette(palette, nfloat)
sns.lineplot(
x="longitude",
y="latitude",
hue="wmo",
data=df,
sort=False,
palette=mypal,
legend=False,
)
sns.scatterplot(
x="longitude", y="latitude", hue="wmo", data=df, palette=mypal
)
else:
mypal = discrete_coloring(palette, N=nfloat).cmap
for k, [name, group] in enumerate(df.groupby("wmo")):
group.plot.line(
x="longitude",
y="latitude",
ax=ax,
color=mypal(k),
legend=False,
label="_nolegend_",
)
group.plot.scatter(
x="longitude", y="latitude", ax=ax, color=mypal(k), label=name
)
if with_cartopy:
if set_global:
ax.set_global()
latlongrid(ax, dx="auto", dy="auto", fontsize="auto")
if not with_seaborn:
ax.get_yaxis().set_visible(False)
else:
if set_global:
ax.set_xlim(-180, 180)
ax.set_ylim(-90, 90)
ax.grid(b=True, linewidth=1, color="gray", alpha=0.7, linestyle=":")
if add_legend and nfloat <= 15:
handles, labels = ax.get_legend_handles_labels()
# if with_seaborn:
# handles, labels = handles[1:], labels[1:]
plt.legend(
handles,
labels,
loc="upper right",
bbox_to_anchor=(1.25, 1),
title="Floats WMO",
)
else:
ax.get_legend().remove()
return fig, ax
def plot_dac(idx):
""" Histogram of DAC for an index dataframe """
warnings.warn(
"plot_dac(idx) is deprecated; use bar_plot(idx, by='institution') instead.",
category=DeprecationWarning,
stacklevel=2,
)
def plot_profilerType(idx):
""" Histogram of profile types for an index dataframe """
warnings.warn(
"plot_profilerType(idx) is deprecated; use bar_plot(idx, by='profiler') instead.",
category=DeprecationWarning,
stacklevel=2,
)
@warnUnless(with_matplotlib, "requires matplotlib installed")
def bar_plot(
df: pd.core.frame.DataFrame,
by: str = "institution",
style: str = STYLE["axes"],
with_seaborn: bool = with_seaborn,
**kwargs
):
""" Create a bar plot for an Argo index dataframe
Parameters
----------
df: Pandas DataFrame
As returned by a fetcher index property
by: str
The profile property to plot. Default is 'institution'
style: str
Define the axes style: 'white', 'darkgrid', 'whitegrid', 'dark', 'ticks'. Only used if Seaborn is available.
Returns
-------
fig: :class:`matplotlib.figure.Figure`
ax: :class:`matplotlib.axes.Axes`
"""
if by not in df:
raise ValueError("'%s' is not a valid field for a bar plot" % by)
with axes_style(style):
defaults = {"figsize": (10, 6), "dpi": 90}
fig, ax = plt.subplots(**{**defaults, **kwargs})
if with_seaborn:
mind = df.groupby(by).size().sort_values(ascending=False).index
sns.countplot(y=by, data=df, order=mind)
else:
df.groupby(by).size().sort_values(ascending=True).plot.barh(ax)
ax.set_xlabel("Number of profiles")
ax.set_ylabel("")
return fig, ax