Skip to content

Commit

Permalink
Feature: Adding plotting method calls for state variables (#480)
Browse files Browse the repository at this point in the history
* Feature: Adding plots for state variables

Added plotting ability for time dimentioned state variable that uses
flag_meanings for y axis labels instead of numerical scale.

Added plotting ability for 2D state varible indicating classification
using CF flag style.

Includding testing data for 2D state variable, testing for plots,
baseline plots, example file.

Fixed the clean method to also clean state variables in CF format but
not using string arrays for flag_meanings.

* Adding subplots_adjust to ensure the text is visible.

* Updated plots using correct method to save image from pytest.

* Adjusting the image tolerance. GitHub testing is producing a slightly
different image than development.

* Fixing spelling issue with keyword.

* Updating keyword call to corrected spelling
  • Loading branch information
kenkehoe authored Jun 20, 2022
1 parent 4682b79 commit 8e5f2a7
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 32 deletions.
23 changes: 18 additions & 5 deletions act/plotting/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ def assign_to_figure_axis(self, fig, ax):
self.fig = fig
self.axes = np.array([ax])

def add_colorbar(self, mappable, title=None, subplot_index=(0,)):
def add_colorbar(self, mappable, title=None, subplot_index=(0,), pad=None,
width=None, **kwargs):
"""
Adds a colorbar to the plot.
Expand All @@ -241,7 +242,13 @@ def add_colorbar(self, mappable, title=None, subplot_index=(0,)):
title : str
The title of the colorbar. Set to None to have no title.
subplot_index : 1 or 2D tuple, list, or array
The index of the subplot to set the x range of.
The index of the subplot to set the x range
pad : float
Padding to right of plot for placement of the colorbar
width : float
Width of the colorbar
**kwargs : keyword arguments
The keyword arguments for :func:`plt.colorbar`
Returns
-------
Expand All @@ -255,12 +262,18 @@ def add_colorbar(self, mappable, title=None, subplot_index=(0,)):
fig = self.fig
ax = self.axes[subplot_index]

if pad is None:
pad = 0.01

if width is None:
width = 0.01

# Give the colorbar it's own axis so the 2D plots line up with 1D
box = ax.get_position()
pad, width = 0.01, 0.01
cax = fig.add_axes([box.xmax + pad, box.ymin, width, box.height])
cbar = plt.colorbar(mappable, cax=cax)
cbar.ax.set_ylabel(title, rotation=270, fontsize=8, labelpad=3)
cbar = plt.colorbar(mappable, cax=cax, **kwargs)
if title is not None:
cbar.ax.set_ylabel(title, rotation=270, fontsize=8, labelpad=3)
cbar.ax.tick_params(labelsize=6)
self.cbs.append(cbar)

Expand Down
78 changes: 71 additions & 7 deletions act/plotting/timeseriesdisplay.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import warnings
from copy import deepcopy
from re import search, search as re_search
import textwrap

import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib import colors as mplcolors
import matplotlib as mpl
from mpl_toolkits.axes_grid1 import make_axes_locatable
from scipy.interpolate import NearestNDInterpolator

Expand Down Expand Up @@ -274,7 +276,10 @@ def plot(
force_line_plot=False,
labels=False,
cbar_label=None,
cbar_h_adjust=None,
secondary_y=False,
y_axis_flag_meanings=False,
colorbar_labels=None,
**kwargs,
):
"""
Expand Down Expand Up @@ -343,8 +348,26 @@ def plot(
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.
secondary_y : boolean
Option to plot on secondary y axis.
y_axis_flag_meanings : boolean or int
When set to True and plotting state variable with flag_values and
flag_meanings attribures 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'}}
**kwargs : keyword arguments
The keyword arguments for :func:`plt.plot` (1D timeseries) or
:func:`plt.pcolormesh` (2D timeseries).
Expand All @@ -364,6 +387,9 @@ def plot(
elif dsname is None:
dsname = list(self._obj.keys())[0]

if y_axis_flag_meanings:
kwargs['linestyle'] = ''

# Get data and dimensions
data = self._obj[dsname][field]
dim = list(self._obj[dsname][field].dims)
Expand Down Expand Up @@ -415,6 +441,20 @@ def plot(
else:
ax = self.axes[subplot_index].twinx()

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:
if day_night_background is True:
self.day_night_background(subplot_index=subplot_index, dsname=dsname)
Expand Down Expand Up @@ -492,6 +532,21 @@ def plot(
elif add_legend:
ax.legend()

# Change y axis to text from flag_meanings if requested.
if y_axis_flag_meanings:
flag_meanings = self._obj[dsname][field].attrs['flag_meanings']
flag_values = self._obj[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:
Expand Down Expand Up @@ -535,7 +590,8 @@ def plot(
ax.set_title(set_title)

# Set YTitle
ax.set_ylabel(ytitle)
if not y_axis_flag_meanings:
ax.set_ylabel(ytitle)

# Set X Limit - We want the same time axes for all subplots
if not hasattr(self, 'time_rng'):
Expand Down Expand Up @@ -610,13 +666,21 @@ def plot(

if ydata is not None:
if cbar_label is None:
self.add_colorbar(mesh, title=cbar_default, subplot_index=subplot_index)
cbar_title = cbar_default
else:
self.add_colorbar(
mesh,
title=''.join(['(', cbar_label, ')']),
subplot_index=subplot_index,
)
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

Expand Down
62 changes: 42 additions & 20 deletions act/qc/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ def clean_arm_state_variables(
override_cf_flag=True,
clean_units_string=True,
integer_flag=True,
replace_in_flag_meanings=None,
):
"""
Function to clean up state variables to use more CF style.
Expand All @@ -449,38 +450,59 @@ def clean_arm_state_variables(
udunits compliant '1'.
integer_flag : bool
Pass through keyword of 'flag' for get_attr_info().
replace_in_flag_meanings : None or string
Character string to search and replace in each flag meanings array value
to increase readability since the flag_meanings stored in netCDF file
is a single character array separated by space character. Alows for
replacing things like "_" with space character.
"""
if isinstance(variables, str):
variables = [variables]

for var in variables:
flag_info = self.get_attr_info(variable=var, flag=integer_flag)
if flag_info is None:
continue
if flag_info is not None:

# Add new attributes to variable
for attr in ['flag_values', 'flag_meanings', 'flag_masks']:
# Add new attributes to variable
for attr in ['flag_values', 'flag_meanings', 'flag_masks']:

if len(flag_info[attr]) > 0:
# Only add if attribute does not exist.
if attr in self._obj[var].attrs.keys() is False:
self._obj[var].attrs[attr] = copy.copy(flag_info[attr])
# If flag is set set attribure even if exists
elif override_cf_flag:
self._obj[var].attrs[attr] = copy.copy(flag_info[attr])
if len(flag_info[attr]) > 0:
# Only add if attribute does not exist.
if attr in self._obj[var].attrs.keys() is False:
self._obj[var].attrs[attr] = copy.copy(flag_info[attr])
# If flag is set, set attribure even if exists
elif override_cf_flag:
self._obj[var].attrs[attr] = copy.copy(flag_info[attr])

# Remove replaced attributes
arm_attributes = flag_info['arm_attributes']
for attr in arm_attributes:
try:
del self._obj[var].attrs[attr]
except KeyError:
pass
# Remove replaced attributes
arm_attributes = flag_info['arm_attributes']
for attr in arm_attributes:
try:
del self._obj[var].attrs[attr]
except KeyError:
pass

# Check if flag_meanings is string. If so convert to list.
try:
flag_meanings = copy.copy(self._obj[var].attrs['flag_meanings'])
if isinstance(flag_meanings, str):
flag_meanings = flag_meanings.split()
if replace_in_flag_meanings is not None:
for ii, flag_meaning in enumerate(flag_meanings):
flag_meaning = flag_meaning.replace(replace_in_flag_meanings, ' ')
flag_meanings[ii] = flag_meaning

self._obj[var].attrs['flag_meanings'] = flag_meanings
except KeyError:
pass

# Clean up units attribute from unitless to udunits '1'
if clean_units_string and self._obj[var].attrs['units'] == 'unitless':
self._obj[var].attrs['units'] = '1'
try:
if clean_units_string and self._obj[var].attrs['units'] == 'unitless':
self._obj[var].attrs['units'] = '1'
except KeyError:
pass

def correct_valid_minmax(self, qc_variable):
"""
Expand Down
Binary file added act/tests/baseline/test_colorbar_labels.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added act/tests/baseline/test_y_axis_flag_meanings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
1 change: 1 addition & 0 deletions act/tests/sample_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@
EXAMPLE_HK = os.path.join(
DATA_PATH, 'mosaossp2auxM1.00.20191217.010801.raw.20191216000000.hk')
EXAMPLE_MET_YAML = os.path.join(DATA_PATH, 'sgpmetE13.b1.yaml')
EXAMPLE_CLOUDPHASE = os.path.join(DATA_PATH, 'nsacloudphaseC1.c1.20180601.000000.nc')
36 changes: 36 additions & 0 deletions act/tests/test_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,3 +943,39 @@ def test_time_plot2():
display = TimeSeriesDisplay(obj)
display.plot('time')
return display.fig


@pytest.mark.mpl_image_compare(tolerance=30)
def test_y_axis_flag_meanings():
variable = 'detection_status'
obj = arm.read_netcdf(sample_files.EXAMPLE_CEIL1,
keep_variables=[variable, 'lat', 'lon', 'alt'])
obj.clean.clean_arm_state_variables(variable, override_cf_flag=True)

display = TimeSeriesDisplay(obj, figsize=(12, 8), subplot_shape=(1,))
display.plot(variable, subplot_index=(0, ), day_night_background=True, y_axis_flag_meanings=18)
display.fig.subplots_adjust(left=0.15, right=0.95, bottom=0.1, top=0.94)

return display.fig


@pytest.mark.mpl_image_compare(tolerance=35)
def test_colorbar_labels():
variable = 'cloud_phase_hsrl'
obj = arm.read_netcdf(sample_files.EXAMPLE_CLOUDPHASE)
obj.clean.clean_arm_state_variables(variable)

display = TimeSeriesDisplay(obj, figsize=(12, 8), subplot_shape=(1,))

y_axis_labels = {}
flag_colors = ['white', 'green', 'blue', 'red', 'cyan', 'orange', 'yellow', 'black', 'gray']
for value, meaning, color in zip(obj[variable].attrs['flag_values'],
obj[variable].attrs['flag_meanings'],
flag_colors):
y_axis_labels[value] = {'text': meaning, 'color': color}

display.plot(variable, subplot_index=(0, ), colorbar_labels=y_axis_labels,
cbar_h_adjust=0)
display.fig.subplots_adjust(left=0.08, right=0.88, bottom=0.1, top=0.94)

return display.fig
Loading

0 comments on commit 8e5f2a7

Please sign in to comment.