"""
Stores the class for TimeSeriesDisplay.
"""
import datetime as dt
import textwrap
import warnings
from copy import deepcopy
from re import search, search as re_search
import numpy as np
import pandas as pd
from scipy import stats
import matplotlib as mpl
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from matplotlib.collections import PatchCollection
from matplotlib import colors as mplcolors
from mpl_toolkits.axes_grid1 import make_axes_locatable
from scipy.interpolate import NearestNDInterpolator
from ..qc.qcfilter import parse_bit
from ..utils import data_utils, datetime_utils as dt_utils
from ..utils.datetime_utils import determine_time_delta, reduce_time_ranges
from ..utils.geo_utils import get_sunrise_sunset_noon
from . import common
from .plot import Display
[docs]class TimeSeriesDisplay(Display):
"""
This subclass contains routines that are specific to plotting
time series plots from data. It is inherited from Display and therefore
contains all of Display's attributes and methods.
Examples
--------
To create a TimeSeriesDisplay with 3 rows, simply do:
.. code-block:: python
ds = act.io.read_arm_netcdf(the_file)
disp = act.plotting.TimeSeriesDisplay(ds, subplot_shape=(3,), figsize=(15, 5))
The TimeSeriesDisplay constructor takes in the same keyword arguments as
plt.subplots. For more information on the plt.subplots keyword arguments,
see the `matplotlib documentation
<https://matplotlib.org/api/_as_gen/matplotlib.pyplot.subplots.html>`_.
If no subplot_shape is provided, then no figure or axis will be created
until add_subplots or plots is called.
"""
def __init__(self, ds, subplot_shape=(1,), ds_name=None, **kwargs):
super().__init__(ds, subplot_shape, ds_name, **kwargs)
[docs] def day_night_background(self, dsname=None, subplot_index=(0,)):
"""
Colorcodes the background according to sunrise/sunset.
Parameters
----------
dsname : None or str
If there is more than one datastream in the display object the
name of the datastream needs to be specified. If set to None and
there is only one datastream then ACT will use the sole datastream
in the object.
subplot_index : 1 or 2D tuple, list, or array
The index to the subplot to place the day and night background in.
"""
if dsname is None and len(self._ds.keys()) > 1:
raise ValueError(
'You must choose a datastream to derive the '
+ 'information needed for the day and night '
+ 'background when 2 or more datasets are in '
+ 'the display object.'
)
elif dsname is None:
dsname = list(self._ds.keys())[0]
# Get File Dates
try:
file_dates = self._ds[dsname].attrs['_file_dates']
except KeyError:
file_dates = []
if len(file_dates) == 0:
sdate = dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[0])
edate = dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[-1])
file_dates = [sdate, edate]
all_dates = dt_utils.dates_between(file_dates[0], file_dates[-1])
if self.axes is None:
raise RuntimeError('day_night_background requires the plot to ' 'be displayed.')
ax = self.axes[subplot_index]
# Find variable names for latitude and longitude
variables = list(self._ds[dsname].data_vars)
lat_name = [var for var in ['lat', 'latitude'] if var in variables]
lon_name = [var for var in ['lon', 'longitude'] if var in variables]
if len(lat_name) == 0:
lat_name = None
else:
lat_name = lat_name[0]
if len(lon_name) == 0:
lon_name = None
else:
lon_name = lon_name[0]
# Variable name does not match, look for standard_name declaration
if lat_name is None or lon_name is None:
for var in variables:
try:
if self._ds[dsname][var].attrs['standard_name'] == 'latitude':
lat_name = var
except KeyError:
pass
try:
if self._ds[dsname][var].attrs['standard_name'] == 'longitude':
lon_name = var
except KeyError:
pass
if lat_name is not None and lon_name is not None:
break
if lat_name is None or lon_name is None:
return
# Extract latitude and longitude scalar from variable. If variable is a vector look
# for first non-Nan value.
lat_lon_list = [np.nan, np.nan]
for ii, var_name in enumerate([lat_name, lon_name]):
try:
values = self._ds[dsname][var_name].values
if values.size == 1:
lat_lon_list[ii] = float(values)
else:
# Look for non-NaN values to use for latitude locaiton. If not found use first value.
index = np.where(np.isfinite(values))[0]
if index.size == 0:
lat_lon_list[ii] = float(values[0])
else:
lat_lon_list[ii] = float(values[index[0]])
except AttributeError:
pass
for value, name in zip(lat_lon_list, ['Latitude', 'Longitude']):
if not np.isfinite(value):
warnings.warn(
f"{name} value in dataset equal to '{value}' is not finite. ", RuntimeWarning
)
return
lat = lat_lon_list[0]
lon = lat_lon_list[1]
lat_range = [-90, 90]
if not (lat_range[0] <= lat <= lat_range[1]):
warnings.warn(
f"Latitude value in dataset of '{lat}' not within acceptable "
f'range of {lat_range[0]} <= latitude <= {lat_range[1]}. ',
RuntimeWarning,
)
return
lon_range = [-180, 180]
if not (lon_range[0] <= lon <= lon_range[1]):
warnings.warn(
f"Longitude value in dataset of '{lon}' not within acceptable "
f'range of {lon_range[0]} <= longitude <= {lon_range[1]}. ',
RuntimeWarning,
)
return
# Initialize the plot to a gray background for total darkness
rect = ax.patch
rect.set_facecolor('0.85')
# Get date ranges to plot
plot_dates = []
for f in all_dates:
for ii in [-1, 0, 1]:
plot_dates.append(f + dt.timedelta(days=ii))
# Get sunrise, sunset and noon times
sunrise, sunset, noon = get_sunrise_sunset_noon(lat, lon, plot_dates)
# Plot daylight
for ii in range(0, len(sunrise)):
ax.axvspan(sunrise[ii], sunset[ii], facecolor='#FFFFCC', zorder=0)
# Plot noon line
for ii in noon:
ax.axvline(x=ii, linestyle='--', color='y', zorder=1)
[docs] def set_xrng(self, xrng, subplot_index=(0,)):
"""
Sets the x range of the plot.
Parameters
----------
xrng : 2 number array
The x limits of the plot.
subplot_index : 1 or 2D tuple, list, or array
The index of the subplot to set the x range of.
"""
if self.axes is None:
raise RuntimeError('set_xrng requires the plot to be displayed.')
# If the xlim is set to the same value for range it will throw a warning
# This is to catch that and expand the range so we avoid the warning.
if xrng[0] == xrng[1]:
if isinstance(xrng[0], np.datetime64):
print(
f'\nAttempting to set xlim range to single value {xrng[0]}. '
'Expanding range by 2 seconds.\n'
)
xrng[0] -= np.timedelta64(1, 's')
xrng[1] += np.timedelta64(1, 's')
elif isinstance(xrng[0], dt.datetime):
print(
f'\nAttempting to set xlim range to single value {xrng[0]}. '
'Expanding range by 2 seconds.\n'
)
xrng[0] -= dt.timedelta(seconds=1)
xrng[1] += dt.timedelta(seconds=1)
self.axes[subplot_index].set_xlim(xrng)
# Make sure that the xrng value is a numpy array not pandas
if isinstance(xrng[0], pd.Timestamp):
xrng = [x.to_numpy() for x in xrng if isinstance(x, pd.Timestamp)]
# Make sure that the xrng value is a numpy array not datetime.datetime
if isinstance(xrng[0], dt.datetime):
xrng = [np.datetime64(x) for x in xrng if isinstance(x, dt.datetime)]
if len(subplot_index) < 2:
self.xrng[subplot_index, 0] = xrng[0].astype('datetime64[D]').astype(float)
self.xrng[subplot_index, 1] = xrng[1].astype('datetime64[D]').astype(float)
else:
self.xrng[subplot_index][0] = xrng[0].astype('datetime64[D]').astype(float)
self.xrng[subplot_index][1] = xrng[1].astype('datetime64[D]').astype(float)
[docs] def set_yrng(self, yrng, subplot_index=(0,), match_axes_ylimits=False):
"""
Sets the y range of the plot.
Parameters
----------
yrng : 2 number array
The y limits of the plot.
subplot_index : 1 or 2D tuple, list, or array
The index of the subplot to set the y range of. This is
ignored if match_axes_ylimits is True.
match_axes_ylimits : boolean
If True, all axes in the display object will have matching
provided ylims. Default is False. This is especially useful
when utilizing a groupby display with many axes.
"""
if self.axes is None:
raise RuntimeError('set_yrng requires the plot to be displayed.')
if not hasattr(self, 'yrng') and len(self.axes.shape) == 2:
self.yrng = np.zeros((self.axes.shape[0], self.axes.shape[1], 2))
elif not hasattr(self, 'yrng') and len(self.axes.shape) == 1:
self.yrng = np.zeros((self.axes.shape[0], 2))
if yrng[0] == yrng[1]:
yrng[1] = yrng[1] + 1
# Sets all axes ylims to the same values.
if match_axes_ylimits:
for i in range(self.axes.shape[0]):
for j in range(self.axes.shape[1]):
self.axes[i, j].set_ylim(yrng)
else:
self.axes[subplot_index].set_ylim(yrng)
try:
self.yrng[subplot_index, :] = yrng
except IndexError:
self.yrng[subplot_index] = yrng
[docs] def plot(
self,
field,
dsname=None,
subplot_index=(0,),
cmap=None,
set_title=None,
add_nan=False,
day_night_background=False,
invert_y_axis=False,
abs_limits=(None, None),
time_rng=None,
y_rng=None,
use_var_for_y=None,
set_shading='auto',
assessment_overplot=False,
overplot_marker='.',
overplot_behind=False,
overplot_markersize=6,
assessment_overplot_category={
'Incorrect': ['Bad', 'Incorrect'],
'Suspect': ['Indeterminate', 'Suspect'],
},
assessment_overplot_category_color={'Incorrect': 'red', 'Suspect': 'orange'},
force_line_plot=False,
labels=False,
cbar_label=None,
cbar_h_adjust=None,
y_axis_flag_meanings=False,
colorbar_labels=None,
cvd_friendly=False,
match_line_label_color=False,
**kwargs,
):
"""
Makes a timeseries plot. If subplots have not been added yet, an axis
will be created assuming that there is only going to be one plot.
If plotting a high data volume 2D dataset, it may take some time to plot.
In order to speed up your plot creation, please resample your data to a
lower resolution dataset.
Parameters
----------
field : str
The name of the field to plot.
dsname : None or str
If there is more than one datastream in the display object the
name of the datastream needs to be specified. If set to None and
there is only one datastream ACT will use the sole datastream
in the object.
subplot_index : 1 or 2D tuple, list, or array
The index of the subplot to set the x range of.
cmap : matplotlib colormap
The colormap to use.
set_title : str
The title for the plot.
add_nan : bool
Set to True to fill in data gaps with NaNs.
day_night_background : bool
Set to True to fill in a color coded background.
according to the time of day.
abs_limits : tuple or list
Sets the bounds on plot limits even if data values exceed
those limits. Set to (ymin,ymax). Use None if only setting
minimum or maximum limit, i.e. (22., None).
time_rng : tuple or list
List or tuple with (min, max) values to set the x-axis range
limits.
y_rng : tuple or list
List or tuple with (min, max) values to set the y-axis range
use_var_for_y : str
Set this to the name of a data variable in the Dataset to use as
the y-axis variable instead of the default dimension. Useful for
instances where data has an index-based dimension instead of a
height-based dimension. If shapes of arrays do not match it will
automatically revert back to the original ydata.
set_shading : string
Option to to set the matplotlib.pcolormesh shading parameter.
Default to 'auto'
assessment_overplot : boolean
Option to overplot quality control colored symbols over plotted
data using flag_assessment categories.
overplot_marker : str
Marker to use for overplot symbol.
overplot_behind : bool
Place the overplot marker behind the data point.
overplot_markersize : float or int
Size of overplot marker. If overplot_behind or force_line_plot
are set the marker size will be double overplot_markersize so
the color is visible.
assessment_overplot_category : dict
Lookup to categorize assessments into groups. This allows using
multiple terms for the same quality control level of failure.
Also allows adding more to the defaults.
assessment_overplot_category_color : dict
Lookup to match overplot category color to assessment grouping.
force_line_plot : boolean
Option to plot 2D data as 1D line plots.
labels : boolean or list
Option to overwrite the legend labels. Must have same dimensions as
number of lines plotted.
cbar_label : str
Option to overwrite default colorbar label.
cbar_h_adjust : float
Option to adjust location of colorbar horizontally. Positive values
move to right negative values move to left.
y_axis_flag_meanings : boolean or int
When set to True and plotting state variable with flag_values and
flag_meanings attributes will replace y axis numerical values
with flag_meanings value. Set to a positive number larger than 1
to indicate maximum word length to use. If text is longer that the
value and has space characters will split text over multiple lines.
colorbar_labels : dict
A dictionary containing values for plotting a 2D array of state variables.
The dictionary uses data values as keys and a dictionary containing keys
'text' and 'color' for each data value to plot.
Example:
{0: {'text': 'Clear sky', 'color': 'white'},
1: {'text': 'Liquid', 'color': 'green'},
2: {'text': 'Ice', 'color': 'blue'},
3: {'text': 'Mixed phase', 'color': 'purple'}}
cvd_friendly : boolean
Set to true if you want to use the integrated color vision deficiency (CVD) friendly
colors for green/red based on the Homeyer colormap.
match_line_label_color : boolean
Will set the y label to match the line color in the plot. This
will only work if the time series plot is a line plot.
**kwargs : keyword arguments
The keyword arguments for :func:`plt.plot` (1D timeseries) or
:func:`plt.pcolormesh` (2D timeseries).
Returns
-------
ax : matplotlib axis handle
The matplotlib axis handle of the plot.
"""
if dsname is None and len(self._ds.keys()) > 1:
raise ValueError(
'You must choose a datastream when there are 2 '
'or more datasets in the TimeSeriesDisplay '
'object.'
)
elif dsname is None:
dsname = list(self._ds.keys())[0]
if y_axis_flag_meanings:
kwargs['linestyle'] = ''
if cvd_friendly:
cmap = 'HomeyerRainbow'
assessment_overplot_category_color['Bad'] = (
0.9285714285714286,
0.7130901016453677,
0.7130901016453677,
)
assessment_overplot_category_color['Incorrect'] = (
0.9285714285714286,
0.7130901016453677,
0.7130901016453677,
)
assessment_overplot_category_color['Not Failing'] = (
(0.0, 0.4240129715562796, 0.4240129715562796),
)
assessment_overplot_category_color['Acceptable'] = (
(0.0, 0.4240129715562796, 0.4240129715562796),
)
# Get data and dimensions
data = self._ds[dsname][field]
dim = list(self._ds[dsname][field].dims)
xdata = self._ds[dsname][dim[0]]
if 'units' in data.attrs:
ytitle = ''.join(['(', data.attrs['units'], ')'])
else:
ytitle = field
if cbar_label is None:
cbar_default = ytitle
if len(dim) > 1:
if use_var_for_y is None:
ydata = self._ds[dsname][dim[1]]
else:
ydata = self._ds[dsname][use_var_for_y]
ydata_dim1 = self._ds[dsname][dim[1]]
if np.shape(ydata) != np.shape(ydata_dim1):
ydata = ydata_dim1
units = ytitle
if 'units' in ydata.attrs.keys():
units = ydata.attrs['units']
ytitle = ''.join(['(', units, ')'])
else:
units = ''
ytitle = dim[1]
# Create labels if 2d as 1d
if force_line_plot is True:
if labels is True:
labels = [' '.join([str(d), units]) for d in ydata.values]
if 'units' in data.attrs.keys():
units = data.attrs['units']
ytitle = ''.join(['(', units, ')'])
else:
units = ''
ytitle = dim[1]
ydata = None
else:
ydata = None
# Get the current plotting axis
if self.fig is None:
self.fig = plt.figure()
if self.axes is None:
self.axes = np.array([plt.axes()])
self.fig.add_axes(self.axes[0])
ax = self.axes[subplot_index]
if colorbar_labels is not None:
flag_values = list(colorbar_labels.keys())
flag_meanings = [value['text'] for key, value in colorbar_labels.items()]
cbar_colors = [value['color'] for key, value in colorbar_labels.items()]
cmap = mpl.colors.ListedColormap(cbar_colors)
for ii, flag_meaning in enumerate(flag_meanings):
if len(flag_meaning) > 20:
flag_meaning = textwrap.fill(flag_meaning, width=20)
flag_meanings[ii] = flag_meaning
else:
flag_values = None
flag_meanings = None
cbar_colors = None
if ydata is None:
# Add in nans to ensure the data does not connect the line.
if add_nan is True:
xdata, data = data_utils.add_in_nan(xdata, data)
if day_night_background is True:
self.day_night_background(subplot_index=subplot_index, dsname=dsname)
# If limiting data being plotted use masked arrays
# Need to do it this way because of autoscale() method
if abs_limits[0] is not None and abs_limits[1] is not None:
data = np.ma.masked_outside(data, abs_limits[0], abs_limits[1])
elif abs_limits[0] is not None and abs_limits[1] is None:
data = np.ma.masked_less_equal(data, abs_limits[0])
elif abs_limits[0] is None and abs_limits[1] is not None:
data = np.ma.masked_greater_equal(data, abs_limits[1])
# Plot the data
if 'marker' not in kwargs.keys():
kwargs['marker'] = '.'
lines = ax.plot(xdata, data, **kwargs)
# Check if we need to call legend method after plotting. This is only
# called when no assessment overplot is called.
add_legend = False
if 'label' in kwargs.keys():
add_legend = True
# Overplot failing data if requested
if assessment_overplot:
# If we are doing forced line plot from 2D data need to manage
# legend lables. Will make arrays to hold labels of QC failing
# because not set when labels not set.
if not isinstance(labels, list) and add_legend is False:
labels = []
lines = []
# For forced line plot need to plot QC behind point instead of
# on top of point.
zorder = None
if force_line_plot or overplot_behind:
zorder = 0
overplot_markersize *= 2.0
for assessment, categories in assessment_overplot_category.items():
flag_data = self._ds[dsname].qcfilter.get_masked_data(
field, rm_assessments=categories, return_inverse=True
)
if np.invert(flag_data.mask).any() and np.isfinite(flag_data).any():
try:
flag_data.mask = np.logical_or(data.mask, flag_data.mask)
except AttributeError:
pass
qc_ax = ax.plot(
xdata,
flag_data,
marker=overplot_marker,
linestyle='',
markersize=overplot_markersize,
color=assessment_overplot_category_color[assessment],
label=assessment,
zorder=zorder,
)
# If labels keyword is set need to add labels for calling legend
if isinstance(labels, list):
# If plotting forced_line_plot need to subset the Line2D object
# so we don't have more than one added to legend.
if len(qc_ax) > 1:
lines.extend(qc_ax[:1])
else:
lines.extend(qc_ax)
labels.append(assessment)
add_legend = True
# Add legend if labels are available
if isinstance(labels, list):
ax.legend(lines, labels)
elif add_legend:
ax.legend()
# Change y axis to text from flag_meanings if requested.
if y_axis_flag_meanings:
flag_meanings = self._ds[dsname][field].attrs['flag_meanings']
flag_values = self._ds[dsname][field].attrs['flag_values']
# If keyword is larger than 1 assume this is the maximum character length
# desired and insert returns to wrap text.
if y_axis_flag_meanings > 1:
for ii, flag_meaning in enumerate(flag_meanings):
if len(flag_meaning) > y_axis_flag_meanings:
flag_meaning = textwrap.fill(flag_meaning, width=y_axis_flag_meanings)
flag_meanings[ii] = flag_meaning
ax.set_yticks(flag_values)
ax.set_yticklabels(flag_meanings)
else:
# Add in nans to ensure the data are not streaking
if add_nan is True:
xdata, data = data_utils.add_in_nan(xdata, data)
# Sets shading parameter to auto. Matplotlib will check deminsions.
# If X,Y and C are same deminsions shading is set to nearest.
# If X and Y deminsions are 1 greater than C shading is set to flat.
if 'edgecolors' not in kwargs.keys():
kwargs['edgecolors'] = 'face'
mesh = ax.pcolormesh(
np.asarray(xdata),
ydata,
data.transpose(),
shading=set_shading,
cmap=cmap,
**kwargs,
)
# Set Title
if set_title is None:
if isinstance(self._ds[dsname].time.values[0], np.datetime64):
set_title = ' '.join(
[
dsname,
field,
'on',
dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[0]),
]
)
else:
date_result = search(r'\d{4}-\d{1,2}-\d{1,2}', self._ds[dsname].time.attrs['units'])
if date_result is not None:
set_title = ' '.join([dsname, field, 'on', date_result.group(0)])
else:
set_title = ' '.join([dsname, field])
ax.set_title(set_title)
# Set YTitle
if not y_axis_flag_meanings:
if match_line_label_color and len(ax.get_lines()) > 0:
ax.set_ylabel(ytitle, color=ax.get_lines()[0].get_color())
else:
ax.set_ylabel(ytitle)
# Set X Limit - We want the same time axes for all subplots
if not hasattr(self, 'time_rng'):
if time_rng is not None:
self.time_rng = list(time_rng)
else:
self.time_rng = [xdata.min().values, xdata.max().values]
self.set_xrng(self.time_rng, subplot_index)
# Set Y Limit
if y_rng is not None:
self.set_yrng(y_rng)
if hasattr(self, 'yrng'):
# Make sure that the yrng is not just the default
if ydata is None:
if abs_limits[0] is not None or abs_limits[1] is not None:
our_data = data
else:
our_data = data.values
else:
our_data = ydata
finite = np.isfinite(our_data)
# If finite is returned as DataArray or Dask array extract values.
try:
finite = finite.values
except AttributeError:
pass
if finite.any():
our_data = our_data[finite]
if invert_y_axis is False:
yrng = [np.min(our_data), np.max(our_data)]
else:
yrng = [np.max(our_data), np.min(our_data)]
else:
yrng = [0, 1]
# Check if current range is outside of new range an only set
# values that work for all data plotted.
if isinstance(yrng[0], np.datetime64):
yrng = mdates.datestr2num([str(yrng[0]), str(yrng[1])])
current_yrng = ax.get_ylim()
if invert_y_axis is False:
if yrng[0] > current_yrng[0]:
yrng[0] = current_yrng[0]
if yrng[1] < current_yrng[1]:
yrng[1] = current_yrng[1]
else:
if yrng[0] < current_yrng[0]:
yrng[0] = current_yrng[0]
if yrng[1] > current_yrng[1]:
yrng[1] = current_yrng[1]
self.set_yrng(yrng, subplot_index)
# Set X Format
if len(subplot_index) == 1:
days = self.xrng[subplot_index, 1] - self.xrng[subplot_index, 0]
else:
days = self.xrng[subplot_index][1] - self.xrng[subplot_index][0]
myFmt = common.get_date_format(days)
ax.xaxis.set_major_formatter(myFmt)
# Set X format - We want the same time axes for all subplots
if not hasattr(self, 'time_fmt'):
self.time_fmt = myFmt
# Put on an xlabel, but only if we are making the bottom-most plot
if subplot_index[0] == self.axes.shape[0] - 1:
ax.set_xlabel('Time [UTC]')
if ydata is not None:
if cbar_label is None:
cbar_title = cbar_default
else:
cbar_title = ''.join(['(', cbar_label, ')'])
if colorbar_labels is not None:
cbar_title = None
cbar = self.add_colorbar(
mesh,
title=cbar_title,
subplot_index=subplot_index,
values=flag_values,
pad=cbar_h_adjust,
)
cbar.set_ticks(flag_values)
cbar.set_ticklabels(flag_meanings)
cbar.ax.tick_params(labelsize=10)
else:
self.add_colorbar(
mesh, title=cbar_title, subplot_index=subplot_index, pad=cbar_h_adjust
)
return ax
[docs] def plot_barbs_from_spd_dir(
self, speed_field, direction_field, pres_field=None, dsname=None, **kwargs
):
"""
This procedure will make a wind barb plot timeseries.
If a pressure field is given and the wind fields are 1D, which, for
example, would occur if one wants to plot a timeseries of
rawinsonde data, then a time-height cross section of
winds will be made.
Note: This procedure calls plot_barbs_from_u_v and will take in the
same keyword arguments as that procedure.
Parameters
----------
speed_field : str
The name of the field specifying the wind speed in m/s.
direction_field : str
The name of the field specifying the wind direction in degrees.
0 degrees is defined to be north and increases clockwise like
what is used in standard meteorological notation.
pres_field : str
The name of the field specifying pressure or height. If using
height coordinates, then we recommend setting invert_y_axis
to False.
dsname : str
The name of the datastream to plot. Setting to None will make
ACT attempt to autodetect this.
kwargs : dict
Any additional keyword arguments will be passed into
:func:`act.plotting.TimeSeriesDisplay.plot_barbs_from_u_and_v`.
Returns
-------
the_ax : matplotlib axis handle
The handle to the axis where the plot was made on.
Examples
--------
..code-block :: python
sonde_ds = act.io.arm.read_arm_netcdf(
act.tests.sample_files.EXAMPLE_TWP_SONDE_WILDCARD)
BarbDisplay = act.plotting.TimeSeriesDisplay(
{'sonde_darwin': sonde_ds}, figsize=(10,5))
BarbDisplay.plot_barbs_from_spd_dir('deg', 'wspd', 'pres',
num_barbs_x=20)
"""
if dsname is None and len(self._ds.keys()) > 1:
raise ValueError(
'You must choose a datastream when there are 2 '
'or more datasets in the TimeSeriesDisplay '
'object.'
)
elif dsname is None:
dsname = list(self._ds.keys())[0]
# Make temporary field called tempu, tempv
spd = self._ds[dsname][speed_field]
dir = self._ds[dsname][direction_field]
tempu = -np.sin(np.deg2rad(dir)) * spd
tempv = -np.cos(np.deg2rad(dir)) * spd
self._ds[dsname]['temp_u'] = deepcopy(self._ds[dsname][speed_field])
self._ds[dsname]['temp_v'] = deepcopy(self._ds[dsname][speed_field])
self._ds[dsname]['temp_u'].values = tempu
self._ds[dsname]['temp_v'].values = tempv
the_ax = self.plot_barbs_from_u_v('temp_u', 'temp_v', pres_field, dsname, **kwargs)
del self._ds[dsname]['temp_u'], self._ds[dsname]['temp_v']
return the_ax
[docs] def plot_barbs_from_u_v(
self,
u_field,
v_field,
pres_field=None,
dsname=None,
subplot_index=(0,),
set_title=None,
day_night_background=False,
invert_y_axis=True,
num_barbs_x=20,
num_barbs_y=20,
barb_step_x=None,
barb_step_y=None,
use_var_for_y=None,
**kwargs,
):
"""
This function will plot a wind barb timeseries from u and v wind
data. If pres_field is given, a time-height series will be plotted
from 1-D wind data.
Parameters
----------
u_field : str
The name of the field containing the U component of the wind.
v_field : str
The name of the field containing the V component of the wind.
pres_field : str or None
The name of the field containing the pressure or height. Set
to None to not use this.
dsname : str or None
The name of the datastream to plot. Setting to None will make
ACT automatically try to determine this.
subplot_index : 2-tuple
The index of the subplot to make the plot on.
set_title : str or None
The title of the plot.
day_night_background : bool
Set to True to plot a day/night background.
invert_y_axis : bool
Set to True to invert the y axis (i.e. for plotting pressure as
the height coordinate).
num_barbs_x : int
The number of wind barbs to plot in the x axis.
num_barbs_y : int
The number of wind barbs to plot in the y axis.
barb_step_x : int
Step between each wind barb to plot. If set, will override
values given for num_barbs_x
barb_step_y : int
Step between each wind barb to plot. If set, will override
values given for num_barbs_y
cmap : matplotlib.colors.LinearSegmentedColormap
A color map to use with wind barbs. If this is set the plt.barbs
routine will be passed the C parameter scaled as sqrt of sum of the
squares and used with the passed in color map. A colorbar will also
be added. Setting the limits of the colorbar can be done with 'clim'.
Setting this changes the wind barbs from black to colors.
use_var_for_y : str
Set this to the name of a data variable in the Dataset to use as the
y-axis variable instead of the default dimension. Useful for instances
where data has an index-based dimension instead of a height-based
dimension. If shapes of arrays do not match it will automatically
revert back to the original ydata.
**kwargs : keyword arguments
Additional keyword arguments will be passed into plt.barbs.
Returns
-------
ax : matplotlib axis handle
The axis handle that contains the reference to the
constructed plot.
"""
if dsname is None and len(self._ds.keys()) > 1:
raise ValueError(
'You must choose a datastream when there are 2 '
'or more datasets in the TimeSeriesDisplay '
'object.'
)
elif dsname is None:
dsname = list(self._ds.keys())[0]
# Get data and dimensions
u = self._ds[dsname][u_field].values
v = self._ds[dsname][v_field].values
dim = list(self._ds[dsname][u_field].dims)
xdata = self._ds[dsname][dim[0]].values
if barb_step_x is None:
num_x = xdata.shape[-1]
barb_step_x = round(num_x / num_barbs_x)
if barb_step_x == 0:
barb_step_x = 1
if len(dim) > 1 and pres_field is None:
if use_var_for_y is None:
ydata = self._ds[dsname][dim[1]]
else:
ydata = self._ds[dsname][use_var_for_y]
ydata_dim1 = self._ds[dsname][dim[1]]
if np.shape(ydata) != np.shape(ydata_dim1):
ydata = ydata_dim1
if 'units' in ydata.attrs:
units = ydata.attrs['units']
else:
units = ''
ytitle = ''.join(['(', units, ')'])
if barb_step_y is None:
num_y = ydata.shape[0]
barb_step_y = round(num_y / num_barbs_y)
if barb_step_y == 0:
barb_step_y = 1
xdata, ydata = np.meshgrid(xdata, ydata, indexing='ij')
elif pres_field is not None:
# What we will do here is do a nearest-neighbor interpolation
# for each member of the series. Coordinates are time, pressure
pres = self._ds[dsname][pres_field]
u_interp = NearestNDInterpolator((xdata, pres.values), u, rescale=True)
v_interp = NearestNDInterpolator((xdata, pres.values), v, rescale=True)
barb_step_x = 1
barb_step_y = 1
x_times = pd.date_range(xdata.min(), xdata.max(), periods=num_barbs_x)
if num_barbs_y == 1:
y_levels = pres.mean()
else:
y_levels = np.linspace(np.nanmin(pres), np.nanmax(pres), num_barbs_y)
xdata, ydata = np.meshgrid(x_times, y_levels, indexing='ij')
u = u_interp(xdata, ydata)
v = v_interp(xdata, ydata)
if 'units' in pres.attrs:
units = pres.attrs['units']
else:
units = ''
ytitle = ''.join(['(', units, ')'])
else:
ydata = None
# Get the current plotting axis, add day/night background and plot data
if self.fig is None:
self.fig = plt.figure()
# Set up or get current axes
if self.axes is None:
self.axes = np.array([plt.axes()])
self.fig.add_axes(self.axes[0])
ax = self.axes[subplot_index]
if ydata is None:
ydata = np.ones(xdata.shape)
if 'cmap' in kwargs.keys():
map_color = np.sqrt(np.power(u[::barb_step_x], 2) + np.power(v[::barb_step_x], 2))
map_color[np.isnan(map_color)] = 0
barbs = ax.barbs(
xdata[::barb_step_x],
ydata[::barb_step_x],
u[::barb_step_x],
v[::barb_step_x],
map_color,
**kwargs,
)
plt.colorbar(
barbs,
ax=[ax],
label='Wind Speed (' + self._ds[dsname][u_field].attrs['units'] + ')',
)
else:
ax.barbs(
xdata[::barb_step_x],
ydata[::barb_step_x],
u[::barb_step_x],
v[::barb_step_x],
**kwargs,
)
ax.set_yticks([])
else:
if 'cmap' in kwargs.keys():
map_color = np.sqrt(
np.power(u[::barb_step_x, ::barb_step_y], 2)
+ np.power(v[::barb_step_x, ::barb_step_y], 2)
)
map_color[np.isnan(map_color)] = 0
barbs = ax.barbs(
xdata[::barb_step_x, ::barb_step_y],
ydata[::barb_step_x, ::barb_step_y],
u[::barb_step_x, ::barb_step_y],
v[::barb_step_x, ::barb_step_y],
map_color,
**kwargs,
)
plt.colorbar(
barbs,
ax=[ax],
label='Wind Speed (' + self._ds[dsname][u_field].attrs['units'] + ')',
)
else:
barbs = ax.barbs(
xdata[::barb_step_x, ::barb_step_y],
ydata[::barb_step_x, ::barb_step_y],
u[::barb_step_x, ::barb_step_y],
v[::barb_step_x, ::barb_step_y],
**kwargs,
)
if day_night_background is True:
self.day_night_background(subplot_index=subplot_index, dsname=dsname)
# Set Title
if set_title is None:
set_title = ' '.join(
[
dsname,
'on',
dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[0]),
]
)
ax.set_title(set_title)
# Set YTitle
if 'ytitle' in locals():
ax.set_ylabel(ytitle)
# Set X Limit - We want the same time axes for all subplots
time_rng = [xdata.min(), xdata.max()]
self.set_xrng(time_rng, subplot_index)
# Set Y Limit
if hasattr(self, 'yrng'):
# Make sure that the yrng is not just the default
if not np.all(self.yrng[subplot_index] == 0):
self.set_yrng(self.yrng[subplot_index], subplot_index)
else:
if ydata is None:
our_data = xdata
else:
our_data = ydata
if np.isfinite(our_data).any():
if invert_y_axis is False:
yrng = [np.nanmin(our_data), np.nanmax(our_data)]
else:
yrng = [np.nanmax(our_data), np.nanmin(our_data)]
else:
yrng = [0, 1]
self.set_yrng(yrng, subplot_index)
# Set X Format
if len(subplot_index) == 1:
days = self.xrng[subplot_index, 1] - self.xrng[subplot_index, 0]
else:
days = (
self.xrng[subplot_index[0], subplot_index[1], 1]
- self.xrng[subplot_index[0], subplot_index[1], 0]
)
# Put on an xlabel, but only if we are making the bottom-most plot
if subplot_index[0] == self.axes.shape[0] - 1:
ax.set_xlabel('Time [UTC]')
myFmt = common.get_date_format(days)
ax.xaxis.set_major_formatter(myFmt)
self.axes[subplot_index] = ax
return self.axes[subplot_index]
[docs] def plot_time_height_xsection_from_1d_data(
self,
data_field,
pres_field,
dsname=None,
subplot_index=(0,),
set_title=None,
day_night_background=False,
num_time_periods=20,
num_y_levels=20,
invert_y_axis=True,
cbar_label=None,
set_shading='auto',
**kwargs,
):
"""
This will plot a time-height cross section from 1D datasets using
nearest neighbor interpolation on a regular time by height grid.
All that is needed are a data variable and a height variable.
Parameters
----------
data_field : str
The name of the field to plot.
pres_field : str
The name of the height or pressure field to plot.
dsname : str or None
The name of the datastream to plot
subplot_index : 2-tuple
The index of the subplot to create the plot on.
set_title : str or None
The title of the plot.
day_night_background : bool
Set to true to plot the day/night background.
num_time_periods : int
Set to determine how many time periods. Setting to None
will do one time period per day.
num_y_levels : int
The number of levels in the y axis to use.
invert_y_axis : bool
Set to true to invert the y-axis (recommended for
pressure coordinates).
cbar_label : str
Option to overwrite default colorbar label.
set_shading : string
Option to to set the matplotlib.pcolormesh shading parameter.
Default to 'auto'
**kwargs : keyword arguments
Additional keyword arguments will be passed
into :func:`plt.pcolormesh`
Returns
-------
ax : matplotlib axis handle
The matplotlib axis handle pointing to the plot.
"""
if dsname is None and len(self._ds.keys()) > 1:
raise ValueError(
'You must choose a datastream when there are 2'
'or more datasets in the TimeSeriesDisplay'
'object.'
)
elif dsname is None:
dsname = list(self._ds.keys())[0]
dim = list(self._ds[dsname][data_field].dims)
if len(dim) > 1:
raise ValueError(
'plot_time_height_xsection_from_1d_data only '
'supports 1-D datasets. For datasets with 2 or '
'more dimensions use plot().'
)
# Get data and dimensions
data = self._ds[dsname][data_field].values
xdata = self._ds[dsname][dim[0]].values
# What we will do here is do a nearest-neighbor interpolation for each
# member of the series. Coordinates are time, pressure
pres = self._ds[dsname][pres_field]
u_interp = NearestNDInterpolator((xdata, pres.values), data, rescale=True)
# Mask points where we have no data
# Count number of unique days
x_times = pd.date_range(xdata.min(), xdata.max(), periods=num_time_periods)
y_levels = np.linspace(np.nanmin(pres), np.nanmax(pres), num_y_levels)
tdata, ydata = np.meshgrid(x_times, y_levels, indexing='ij')
data = u_interp(tdata, ydata)
ytitle = ''.join(['(', pres.attrs['units'], ')'])
units = data_field + ' (' + self._ds[dsname][data_field].attrs['units'] + ')'
# Get the current plotting axis, add day/night background and plot data
if self.fig is None:
self.fig = plt.figure()
# Set up or get current axes
if self.axes is None:
self.axes = np.array([plt.axes()])
self.fig.add_axes(self.axes[0])
ax = self.axes[subplot_index]
mesh = ax.pcolormesh(x_times, y_levels, np.transpose(data), shading=set_shading, **kwargs)
if day_night_background is True:
self.day_night_background(subplot_index=subplot_index, dsname=dsname)
# Set Title
if set_title is None:
set_title = ' '.join(
[
dsname,
'on',
dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[0]),
]
)
ax.set_title(set_title)
# Set YTitle
if 'ytitle' in locals():
ax.set_ylabel(ytitle)
# Set X Limit - We want the same time axes for all subplots
time_rng = [x_times[0], x_times[-1]]
self.set_xrng(time_rng, subplot_index)
# Set Y Limit
if hasattr(self, 'yrng'):
# Make sure that the yrng is not just the default
if not np.all(self.yrng[subplot_index] == 0):
self.set_yrng(self.yrng[subplot_index], subplot_index)
else:
if ydata is None:
our_data = data.values
else:
our_data = ydata
if np.isfinite(our_data).any():
if invert_y_axis is False:
yrng = [np.nanmin(our_data), np.nanmax(our_data)]
else:
yrng = [np.nanmax(our_data), np.nanmin(our_data)]
else:
yrng = [0, 1]
self.set_yrng(yrng, subplot_index)
# Set X Format
if len(subplot_index) == 1:
days = self.xrng[subplot_index, 1] - self.xrng[subplot_index, 0]
else:
days = (
self.xrng[subplot_index[0], subplot_index[1], 1]
- self.xrng[subplot_index[0], subplot_index[1], 0]
)
# Put on an xlabel, but only if we are making the bottom-most plot
if subplot_index[0] == self.axes.shape[0] - 1:
ax.set_xlabel('Time [UTC]')
if ydata is not None:
if cbar_label is None:
self.add_colorbar(mesh, title=units, subplot_index=subplot_index)
else:
self.add_colorbar(mesh, title=cbar_label, subplot_index=subplot_index)
myFmt = common.get_date_format(days)
ax.xaxis.set_major_formatter(myFmt)
return self.axes[subplot_index]
[docs] def time_height_scatter(
self,
data_field=None,
alt_field='alt',
dsname=None,
cmap='rainbow',
alt_label=None,
cb_label=None,
subplot_index=(0,),
plot_alt_field=False,
cvd_friendly=False,
day_night_background=False,
set_title=None,
**kwargs,
):
"""
Create a time series plot of altitude and data variable with
color also indicating value with a color bar. The Color bar is
positioned to serve both as the indicator of the color intensity
and the second y-axis.
Parameters
----------
data_field : str
Name of data field in the dataset to plot on second y-axis.
alt_field : str
Variable to use for y-axis.
dsname : str or None
The name of the datastream to plot.
cmap : str
Colorbar color map to use.
alt_label : str
Altitude first y-axis label to use. If None, will try to use
long_name and units.
cb_label : str
Colorbar label to use. If not set will try to use
long_name and units.
subplot_index : 1 or 2D tuple, list, or array
The index of the subplot to set the x range of.
plot_alt_field : boolean
Set to true to plot the altitude field on the secondary y-axis
cvd_friendly : boolean
If set to True will use the Homeyer colormap
day_night_background : boolean
If set to True will plot the day_night_background
set_title : str
Title to set on the plot
**kwargs : keyword arguments
Any other keyword arguments that will be passed
into TimeSeriesDisplay.plot module when the figure
is made.
"""
if dsname is None and len(self._ds.keys()) > 1:
raise ValueError(
'You must choose a datastream when there are 2 '
'or more datasets in the TimeSeriesDisplay '
'object.'
)
elif dsname is None:
dsname = list(self._ds.keys())[0]
# Set up or get current plot figure
if self.fig is None:
self.fig = plt.figure()
# Set up or get current axes
if self.axes is None:
self.axes = np.array([plt.axes()])
self.fig.add_axes(self.axes[0])
if cvd_friendly:
cmap = 'HomeyerRainbow'
ax = self.axes[subplot_index]
# Get data and dimensions
data = self._ds[dsname][data_field]
altitude = self._ds[dsname][alt_field]
dim = list(self._ds[dsname][data_field].dims)
xdata = self._ds[dsname][dim[0]]
if alt_label is None:
try:
alt_label = altitude.attrs['long_name'] + ''.join(
[' (', altitude.attrs['units'], ')']
)
except KeyError:
alt_label = alt_field
if cb_label is None:
try:
cb_label = data.attrs['long_name'] + ''.join([' (', data.attrs['units'], ')'])
except KeyError:
cb_label = data_field
if 'units' in data.attrs:
ytitle = ''.join(['(', data.attrs['units'], ')'])
else:
ytitle = data_field
# Set Title
if set_title is None:
if isinstance(self._ds[dsname].time.values[0], np.datetime64):
set_title = ' '.join(
[
dsname,
data_field,
'on',
dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[0]),
]
)
else:
date_result = search(r'\d{4}-\d{1,2}-\d{1,2}', self._ds[dsname].time.attrs['units'])
if date_result is not None:
set_title = ' '.join([dsname, data_field, 'on', date_result.group(0)])
else:
set_title = ' '.join([dsname, data_field])
# Plot scatter data
sc = ax.scatter(xdata.values, data.values, c=data.values, cmap=cmap, **kwargs)
ax.set_title(set_title)
if plot_alt_field:
self.fig.subplots_adjust(left=0.1, right=0.8, bottom=0.15, top=0.925)
pad = 0.02 + (0.02 * len(str(int(np.nanmax(altitude.values)))))
cbar = self.fig.colorbar(sc, pad=pad, cmap=cmap)
ax2 = ax.twinx()
ax2.set_ylabel(alt_label)
ax2.scatter(xdata.values, altitude.values, color='black')
else:
cbar = self.fig.colorbar(sc, cmap=cmap)
if day_night_background is True:
self.day_night_background(subplot_index=subplot_index, dsname=dsname)
cbar.ax.set_ylabel(cb_label)
# Set X Limit - We want the same time axes for all subplots
self.time_rng = [xdata.min().values, xdata.max().values]
self.set_xrng(self.time_rng, subplot_index)
# Set X Format
if len(subplot_index) == 1:
days = self.xrng[subplot_index, 1] - self.xrng[subplot_index, 0]
else:
days = (
self.xrng[subplot_index[0], subplot_index[1], 1]
- self.xrng[subplot_index[0], subplot_index[1], 0]
)
myFmt = common.get_date_format(days)
ax.xaxis.set_major_formatter(myFmt)
ax.set_xlabel('Time (UTC)')
ax.set_ylabel(ytitle)
self.axes[subplot_index] = ax
return self.axes[subplot_index]
[docs] def qc_flag_block_plot(
self,
data_field=None,
dsname=None,
subplot_index=(0,),
time_rng=None,
assessment_color=None,
edgecolor='face',
set_shading='auto',
cvd_friendly=False,
**kwargs,
):
"""
Create a time series plot of embedded quality control values
using broken barh plotting.
Parameters
----------
data_field : str
Name of data field in the dataset to plot corresponding quality
control.
dsname : None or str
If there is more than one datastream in the display object the
name of the datastream needs to be specified. If set to None and
there is only one datastream ACT will use the sole datastream
in the object.
subplot_index : 1 or 2D tuple, list, or array
The index of the subplot to set the x range of.
time_rng : tuple or list
List or tuple with (min, max) values to set the x-axis range limits.
assessment_color : dict
Dictionary lookup to override default assessment to color. Make sure
assessment work is correctly set with case syntax.
edgecolor : str or list
Color name, list of color names or 'face' as defined in matplotlib.axes.Axes.broken_barh
set_shading : string
Option to to set the matplotlib.pcolormesh shading parameter.
Default to 'auto'
cvd_friendly : boolean
Set to true if you want to use the integrated color vision deficiency (CVD) friendly
colors for green/red based on the Homeyer colormap
**kwargs : keyword arguments
The keyword arguments for :func:`plt.broken_barh`.
"""
# Color to plot associated with assessment.
color_lookup = {
'Bad': 'red',
'Incorrect': 'red',
'Indeterminate': 'orange',
'Suspect': 'orange',
'Missing': 'darkgray',
'Not Failing': 'green',
'Acceptable': 'green',
}
if cvd_friendly:
color_lookup['Bad'] = (0.9285714285714286, 0.7130901016453677, 0.7130901016453677)
color_lookup['Incorrect'] = (0.9285714285714286, 0.7130901016453677, 0.7130901016453677)
color_lookup['Not Failing'] = (0.0, 0.4240129715562796, 0.4240129715562796)
color_lookup['Acceptable'] = (0.0, 0.4240129715562796, 0.4240129715562796)
color_lookup['Indeterminate'] = (1.0, 0.6470588235294118, 0.0)
color_lookup['Suspect'] = (1.0, 0.6470588235294118, 0.0)
color_lookup['Missing'] = (0.6627450980392157, 0.6627450980392157, 0.6627450980392157)
if assessment_color is not None:
for asses, color in assessment_color.items():
color_lookup[asses] = color
if asses == 'Incorrect':
color_lookup['Bad'] = color
if asses == 'Suspect':
color_lookup['Indeterminate'] = color
# Set up list of test names to use for missing values
missing_val_long_names = [
'Value equal to missing_value*',
'Value set to missing_value*',
'Value is equal to missing_value*',
'Value is set to missing_value*',
]
if dsname is None and len(self._ds.keys()) > 1:
raise ValueError(
'You must choose a datastream when there are 2 '
'or more datasets in the TimeSeriesDisplay '
'object.'
)
elif dsname is None:
dsname = list(self._ds.keys())[0]
# Set up or get current plot figure
if self.fig is None:
self.fig = plt.figure()
# Set up or get current axes
if self.axes is None:
self.axes = np.array([plt.axes()])
self.fig.add_axes(self.axes[0])
ax = self.axes[subplot_index]
# Set X Limit - We want the same time axes for all subplots
data = self._ds[dsname][data_field]
dim = list(self._ds[dsname][data_field].dims)
xdata = self._ds[dsname][dim[0]]
# Get data and attributes
qc_data_field = self._ds[dsname].qcfilter.check_for_ancillary_qc(
data_field, add_if_missing=False, cleanup=False
)
if qc_data_field is None:
raise ValueError(f'No quality control ancillary variable in Dataset for {data_field}')
flag_masks = self._ds[dsname][qc_data_field].attrs['flag_masks']
flag_meanings = self._ds[dsname][qc_data_field].attrs['flag_meanings']
flag_assessments = self._ds[dsname][qc_data_field].attrs['flag_assessments']
# Get time ranges for green blocks
time_delta = determine_time_delta(xdata.values)
barh_list_green = reduce_time_ranges(xdata.values, time_delta=time_delta, broken_barh=True)
# Set background to gray indicating not available data
ax.set_facecolor('dimgray')
# Check if plotting 2D data vs 1D data. 2D data will be summarized by
# assessment category instead of showing each test.
data_shape = self._ds[dsname][qc_data_field].shape
if len(data_shape) > 1:
cur_assessments = list(set(flag_assessments))
cur_assessments.sort()
cur_assessments.reverse()
qc_data = np.full(data_shape, -1, dtype=np.int16)
plot_colors = []
tick_names = []
index = self._ds[dsname][qc_data_field].values == 0
if index.any():
qc_data[index] = 0
plot_colors.append(color_lookup['Not Failing'])
tick_names.append('Not Failing')
for ii, assess in enumerate(cur_assessments):
if assess not in color_lookup:
color_lookup[assess] = list(mplcolors.CSS4_COLORS.keys())[ii]
ii += 1
assess_data = self._ds[dsname].qcfilter.get_masked_data(
data_field, rm_assessments=assess
)
if assess_data.mask.any():
qc_data[assess_data.mask] = ii
plot_colors.append(color_lookup[assess])
tick_names.append(assess)
# Overwrite missing data. Not sure if we want to do this because VAPs set
# the value to missing but the test is set to Bad. This tries to overcome that
# by looking for correct test description that would only indicate the values
# are missing not that they are set to missing by a test... most likely.
missing_test_nums = []
for ii, flag_meaning in enumerate(flag_meanings):
# Check if the bit set is indicating missing data.
for val in missing_val_long_names:
if re_search(val, flag_meaning):
test_num = parse_bit(flag_masks[ii])[0]
missing_test_nums.append(test_num)
assess_data = self._ds[dsname].qcfilter.get_masked_data(
data_field, rm_tests=missing_test_nums
)
if assess_data.mask.any():
qc_data[assess_data.mask] = -1
plot_colors.append(color_lookup['Missing'])
tick_names.append('Missing')
# Create a masked array to allow not plotting where values are missing
qc_data = np.ma.masked_equal(qc_data, -1)
dims = self._ds[dsname][qc_data_field].dims
xvalues = self._ds[dsname][dims[0]].values
yvalues = self._ds[dsname][dims[1]].values
cMap = mplcolors.ListedColormap(plot_colors)
mesh = ax.pcolormesh(
xvalues,
yvalues,
np.transpose(qc_data),
cmap=cMap,
vmin=0,
shading=set_shading,
)
divider = make_axes_locatable(ax)
# Determine correct placement of words on colorbar
tick_nums = (
np.arange(0, len(tick_names) * 2 + 1) / (len(tick_names) * 2) * np.nanmax(qc_data)
)[1::2]
cax = divider.append_axes('bottom', size='5%', pad=0.3)
cbar = self.fig.colorbar(
mesh,
cax=cax,
orientation='horizontal',
spacing='uniform',
ticks=tick_nums,
shrink=0.5,
)
cbar.ax.set_xticklabels(tick_names)
# Set YTitle
dim_name = list(set(self._ds[dsname][qc_data_field].dims) - {'time'})
try:
ytitle = f"{dim_name[0]} ({self._ds[dsname][dim_name[0]].attrs['units']})"
ax.set_ylabel(ytitle)
except KeyError:
pass
# Add which tests were set as text to the plot
unique_values = []
for ii in np.unique(self._ds[dsname][qc_data_field].values):
unique_values.extend(parse_bit(ii))
if len(unique_values) > 0:
unique_values = list(set(unique_values))
unique_values.sort()
unique_values = [str(ii) for ii in unique_values]
self.fig.text(
0.5,
-0.35,
f"QC Tests Tripped: {', '.join(unique_values)}",
transform=ax.transAxes,
horizontalalignment='center',
verticalalignment='center',
fontweight='bold',
)
else:
test_nums = []
for ii, assess in enumerate(flag_assessments):
if assess not in color_lookup:
color_lookup[assess] = list(mplcolors.CSS4_COLORS.keys())[ii]
# Plot green data first.
ax.broken_barh(
barh_list_green,
(ii, ii + 1),
facecolors=color_lookup['Not Failing'],
edgecolor=edgecolor,
**kwargs,
)
# Get test number from flag_mask bitpacked number
test_nums.append(parse_bit(flag_masks[ii]))
# Get masked array data to use mask for finding if/where test is set
data = self._ds[dsname].qcfilter.get_masked_data(data_field, rm_tests=test_nums[-1])
if np.any(data.mask):
# Get time ranges from time and masked data
barh_list = reduce_time_ranges(
xdata.values[data.mask], time_delta=time_delta, broken_barh=True
)
# Check if the bit set is indicating missing data. If so change
# to different plotting color than what is in flag_assessments.
for val in missing_val_long_names:
if re_search(val, flag_meanings[ii]):
assess = 'Missing'
break
# Lay down blocks of tripped tests using correct color
ax.broken_barh(
barh_list,
(ii, ii + 1),
facecolors=color_lookup[assess],
edgecolor=edgecolor,
**kwargs,
)
# Add test description to plot.
ax.text(xdata.values[0], ii + 0.5, ' ' + flag_meanings[ii], va='center')
# Change y ticks to test number
plt.yticks(
[ii + 0.5 for ii in range(0, len(test_nums))],
labels=['Test ' + str(ii[0]) for ii in test_nums],
)
# Set ylimit to number of tests plotted
ax.set_ylim(0, len(flag_assessments))
# Set X Limit - We want the same time axes for all subplots
if not hasattr(self, 'time_rng'):
if time_rng is not None:
self.time_rng = list(time_rng)
else:
self.time_rng = [xdata.min().values, xdata.max().values]
self.set_xrng(self.time_rng, subplot_index)
# Get X format - We want the same time axes for all subplots
if hasattr(self, 'time_fmt'):
ax.xaxis.set_major_formatter(self.time_fmt)
else:
# Set X Format
if len(subplot_index) == 1:
days = self.xrng[subplot_index, 1] - self.xrng[subplot_index, 0]
else:
days = (
self.xrng[subplot_index[0], subplot_index[1], 1]
- self.xrng[subplot_index[0], subplot_index[1], 0]
)
myFmt = common.get_date_format(days)
ax.xaxis.set_major_formatter(myFmt)
self.time_fmt = myFmt
return self.axes[subplot_index]
[docs] def fill_between(
self,
field,
dsname=None,
subplot_index=(0,),
set_title=None,
**kwargs,
):
"""
Makes a fill_between plot, based on matplotlib
Parameters
----------
field : str
The name of the field to plot.
dsname : None or str
If there is more than one datastream in the display object the
name of the datastream needs to be specified. If set to None and
there is only one datastream ACT will use the sole datastream
in the object.
subplot_index : 1 or 2D tuple, list, or array
The index of the subplot to set the x range of.
set_title : str
The title for the plot.
**kwargs : keyword arguments
The keyword arguments for :func:`plt.plot` (1D timeseries) or
:func:`plt.pcolormesh` (2D timeseries).
Returns
-------
ax : matplotlib axis handle
The matplotlib axis handle of the plot.
"""
if dsname is None and len(self._ds.keys()) > 1:
raise ValueError(
'You must choose a datastream when there are 2 '
'or more datasets in the TimeSeriesDisplay '
'object.'
)
elif dsname is None:
dsname = list(self._ds.keys())[0]
# Get data and dimensions
data = self._ds[dsname][field]
dim = list(self._ds[dsname][field].dims)
xdata = self._ds[dsname][dim[0]]
if 'units' in data.attrs:
ytitle = ''.join(['(', data.attrs['units'], ')'])
else:
ytitle = field
# Get the current plotting axis, add day/night background and plot data
if self.fig is None:
self.fig = plt.figure()
if self.axes is None:
self.axes = np.array([plt.axes()])
self.fig.add_axes(self.axes[0])
# Set ax to appropriate axis
ax = self.axes[subplot_index]
ax.fill_between(xdata.values, data, **kwargs)
# Set X Format
if len(subplot_index) == 1:
days = self.xrng[subplot_index, 1] - self.xrng[subplot_index, 0]
else:
days = (
self.xrng[subplot_index[0], subplot_index[1], 1]
- self.xrng[subplot_index[0], subplot_index[1], 0]
)
myFmt = common.get_date_format(days)
ax.xaxis.set_major_formatter(myFmt)
# Set X format - We want the same time axes for all subplots
if not hasattr(self, 'time_fmt'):
self.time_fmt = myFmt
# Put on an xlabel, but only if we are making the bottom-most plot
if subplot_index[0] == self.axes.shape[0] - 1:
ax.set_xlabel('Time [UTC]')
# Set YTitle
ax.set_ylabel(ytitle)
# Set Title
if set_title is None:
set_title = ' '.join(
[
dsname,
field,
'on',
dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[0]),
]
)
ax.set_title(set_title)
self.axes[subplot_index] = ax
return self.axes[subplot_index]
[docs] def plot_stripes(
self,
field,
dsname=None,
subplot_index=(0,),
set_title=None,
reference_period=None,
cmap='bwr',
cbar_label=None,
colorbar=True,
**kwargs,
):
"""
Makes a climate stripe plot with or without a baseline period specified
Parameters
----------
field : str
The name of the field to plot.
dsname : None or str
If there is more than one datastream in the display object the
name of the datastream needs to be specified. If set to None and
there is only one datastream ACT will use the sole datastream
in the object.
subplot_index : 1 or 2D tuple, list, or array
The index of the subplot to set the x range of.
set_title : str
The title for the plot.
reference_period : list
List of a start and end date for a reference period ['2020-01-01', '2020-04-01']
If this is set, the plot will subtract the mean of the reference period from the
field to create an anomaly calculation.
cmap : string
Colormap to use for plotting. Defaults to bwr
cbar_label : str
Option to overwrite default colorbar label.
colorbar : boolean
Option to not plot the colorbar. Default is to plot it
**kwargs : keyword arguments
The keyword arguments for :func:`plt.plot` (1D timeseries) or
:func:`plt.pcolormesh` (2D timeseries).
Returns
-------
ax : matplotlib axis handle
The matplotlib axis handle of the plot.
"""
if dsname is None and len(self._ds.keys()) > 1:
raise ValueError(
'You must choose a datastream when there are 2 '
'or more datasets in the TimeSeriesDisplay '
'object.'
)
elif dsname is None:
dsname = list(self._ds.keys())[0]
# Get data and dimensions
data = self._ds[dsname][field]
dim = list(self._ds[dsname][field].dims)
xdata = self._ds[dsname][dim[0]]
start = int(mdates.date2num(xdata.values[0]))
end = int(mdates.date2num(xdata.values[-1]))
delta = stats.mode(xdata.diff('time').values)[0] / np.timedelta64(1, 'D')
# Calculate mean for reference period and subtract from the data
if reference_period is not None:
reference = data.sel(time=slice(reference_period[0], reference_period[1])).mean('time')
data.values = data.values - reference.values
# Get the current plotting axis, add day/night background and plot data
if self.fig is None:
self.fig = plt.figure()
if self.axes is None:
self.axes = np.array([plt.axes()])
self.fig.add_axes(self.axes[0])
# Set ax to appropriate axis
ax = self.axes[subplot_index]
# Plot up data using rectangles
col = PatchCollection(
[Rectangle((y, 0), delta, 1) for y in np.arange(start, end + 1, delta)]
)
col.set_array(data)
col.set_cmap(cmap)
col.set_clim(np.nanmin(data), np.nanmax(data))
ax.add_collection(col)
locator = mdates.AutoDateLocator(minticks=3)
formatter = mdates.AutoDateFormatter(locator)
ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(formatter)
ax.set_ylim(0, 1)
ax.set_yticks([])
ax.set_xlim(start, end + 1)
# Set Title
if set_title is None:
set_title = ' '.join(
[
dsname,
field,
'Stripes on',
dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[0]),
]
)
ax.set_title(set_title)
# Set Colorbar
if colorbar:
if 'units' in data.attrs:
ytitle = ''.join(['(', data.attrs['units'], ')'])
else:
ytitle = field
if cbar_label is None:
cbar_title = ytitle
else:
cbar_title = ''.join(['(', cbar_label, ')'])
self.add_colorbar(col, title=cbar_title, subplot_index=subplot_index)
self.axes[subplot_index] = ax
return self.axes[subplot_index]