-
Eva Schiffer authoredEva Schiffer authored
plotting_util.py 34.63 KiB
#!/usr/bin/env python
# encoding: utf-8
"""
plotting_util.py
Purpose: Utility methods for plotting, not linked to input data formatting.
Created by Eva Schiffer <eva.schiffer@ssec.wisc.edu> on 2017-04-05.
Copyright (c) 2017 University of Wisconsin Regents. All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from pylab import *
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib.ticker as mticker
import logging
import numpy
from numpy import ma
import cartopy
import cartopy.crs as crs
import cartopy.feature as cfeature
from distutils.version import StrictVersion
from constants import *
LOG = logging.getLogger(__name__)
# check whether we have an old cartopy or one that's new enough to handle the sweep direction correctly for GOES-R and later
have_new_cartopy = False
if StrictVersion(cartopy.__version__) >= StrictVersion("0.16.0") :
have_new_cartopy = True
else :
LOG.warning("Older version of cartopy (pre 0.16.0) detected. The sweep parameter can not be set "
"correctly in this version which may lead to slight navigational misalignment in the "
"resulting quicklooks images.")
# check if we have an old matplotlib
have_new_matplotlib = False
import matplotlib
if StrictVersion(matplotlib.__version__) >= StrictVersion("3.0.0") :
have_new_matplotlib = True
else :
LOG.warning("Older version of matplotlib (pre 3.0.0) detected. Some plots will be modified as "
"needed to avoid a display bug with the use of logrithmic scales.")
def _add_colorbar(axes_object, plot_object, place_str="right", pad_inches=0.05,
units=None, valid_range=None, data_shape=None, tick_positions=None, tick_labels=None,
extend_constant=None, ):
"""
Add a colorbar to our image next to the plot in axes_object.
:param axes_object: The axes for the plot we want a colorbar for.
:param place_str: Which side the colorbar should be on as a string.
:param pad_inches: How far the colorbar should be from the plot in axes_object.
:return:
"""
# if we were given a data shape, override the place_str and try to pick a place for the axes intelligently
which_side = place_str
if data_shape is not None :
if len(data_shape) == 1 :
which_side = "bottom"
elif data_shape[0] < data_shape[1] :
which_side = "bottom"
else :
which_side = "right"
# if we're on the bottom or left of the figure, fix the padding to miss the bottom labels
pad_to_use = pad_inches
if which_side == "left" or which_side == "bottom" :
pad_to_use += 0.25
# figure out the orientation
orientation_temp = 'horizontal'
if which_side == "right" or which_side == "left" :
orientation_temp = 'vertical'
# make the colorbar
if extend_constant is not None :
cbar = plt.colorbar(plot_object, ax=axes_object, orientation=orientation_temp, extend=extend_constant, )
else :
cbar = plt.colorbar(plot_object, ax=axes_object, orientation=orientation_temp, )
# set various tick mark related stuff on the colorbar
if ( (valid_range is not None and (valid_range[1] - valid_range[0] > 5000.0) and (orientation_temp == 'horizontal')) or
((tick_labels is not None) and (len(tick_labels) > 3)) or
(tick_labels is not None and orientation_temp == 'vertical')):
cbar.ax.tick_params(labelsize=6)
if tick_positions is not None :
cbar.set_ticks(tick_positions)
if tick_labels is not None :
cbar.set_ticklabels(tick_labels)
cbar.ax.tick_params(axis="both", which="both", length=0)
# add the units to the colorbar
if (units is not None) and (str.lower(str(units)) != "none") and (str.lower(str(units)) != "1"):
temp_units = "[" + units + "]"
# if we have a very long units text, make it smaller sized text so it will hopefully fit
if len(temp_units) > 20 :
cbar.set_label(temp_units, fontstyle='italic', fontsize=8,)
else :
cbar.set_label(temp_units, fontstyle='italic',)
return cbar
# Note: we are using this for our category data, but it only works because our category data
# is very limited in it's range and values
def _cmap_discretize(cmap, N):
"""Return a discrete colormap from the continuous colormap cmap.
cmap: colormap instance, eg. cm.jet.
N: number of colors.
Example
x = resize(arange(100), (5,100))
djet = cmap_discretize(cm.jet, 5)
imshow(x, cmap=djet)
Note: this function came from the scipy cookbook
http://scipy-cookbook.readthedocs.io/items/Matplotlib_ColormapTransformations.html
"""
if type(cmap) == str:
cmap = get_cmap(cmap)
colors_i = concatenate((linspace(0, 1., N), (0., 0., 0., 0.)))
colors_rgba = cmap(colors_i)
indices = linspace(0, 1., 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 range(N + 1)]
# Return colormap object.
return matplotlib.colors.LinearSegmentedColormap(cmap.name + "_%d" % N, cdict, 1024)
def _cap_extent (my_extent, projection_object, scene_name=None,) :
"""
Make sure my extent doesn't go beyond the available extent for the projection.
:param my_extent:
:param projection_object:
:return:
"""
global_bounds = projection_object.boundary.bounds
g_extent = (global_bounds[0], global_bounds[2], global_bounds[1], global_bounds[3])
"""
This margin exists because in the Full Disk case if you plug in the global bounds returned by the
projection_object.boundary.bounds as the extent the wind plots will only show a thin slice of the
middle of the globe.
If you slice even 1 off of the outer edges of the boundaries (and in the full disk case these are like
(-5434182.0342756845, 5434182.0342756845, -5412936.5763213765, 5412936.5763213765), so not small) then the final
plot has bounds as expected. This seems like a bug in cartopy, but for now, just work around it.
"""
extent_margin = 1 if scene_name == FULL_DISK_ID else 0
# we were previously using this, but it caused problems for full disks, because the longitude didn't select correctly
return [max(my_extent[0], g_extent[0] + extent_margin) ,
min(my_extent[1], g_extent[1] - extent_margin),
max(my_extent[2], g_extent[2] + extent_margin),
min(my_extent[3], g_extent[3] - extent_margin),]
def _position_category_labels(flag_values, ):
"""
Given a list of flag values that are an increasing list of integers that starts with 0,
position the labels so the first and last ticks are not at the ends, but rather in the
middle of their segments.
Given something like [0, 1, 2, 3,] we want to put each of the ticks in the "middle" of their
numbered segment. So there are 4 categories in 3 segments of space. The width of each segment is
(3 - 0) / 4 and we need to space the first and last labels in 1/2 of that amount so we'd end up
with [3/8, 3/8 + 3/4, 3/8 + 6/4, 3 - 3/8 or 3/8 + 9/4,]
:param flag_values: a list of flag values that are an increasing list of integers starting with 0
:return:
"""
# we don't strictly need to subtract the flag_values[0] here (since it should always be 0),
# but a good reminder if this ever becomes more general
category_size = float(flag_values[-1] - flag_values[0]) / float(len(flag_values))
half_cat_size = category_size / 2.0
# note: this is not a very general solution, if we ever need to plot more general sets it will need work
new_positions = [half_cat_size + (x * category_size) for x in flag_values]
return new_positions
def close_figure(figure_obj) :
plt.close(figure_obj)
def _setup_kwargs ( input_kwargs, masked_data, colormap, flag_values, range_to_use, scale_const, ) :
# get the min and max data values for convenience
data_min = numpy.min(masked_data)
data_max = numpy.max(masked_data)
# when a log scale is being used the range can't include zero, if it does, use a regular linear scale instead of log
if scale_const == LOG_SCALE and (range_to_use[0] <= 0.0) and (range_to_use[-1] >= 0.0) :
LOG.warning("Detected attempt to use log scale with a data range that includes zero. "
"Turning off log scaling for this variable.")
scale_const = REGULAR_SCALE
LOG.debug("Data min / max: " + str(data_min) + " / " + str(data_max))
# if we've got a color map, pass it to the list of things we want to tell the plotting function
if colormap is not None:
input_kwargs['cmap'] = colormap
if flag_values is not None:
cmap_temp = input_kwargs['cmap'] if 'cmap' in input_kwargs and input_kwargs['cmap'] is not None else cm.jet
input_kwargs['cmap'] = _cmap_discretize(cmap_temp, len(flag_values))
# add the overflow colors and display if needed
extend_const = None
if input_kwargs['cmap'] is not None and range_to_use is not None:
# if the low end of the range is not as far down as the smallest data
if range_to_use[0] > data_min :
# see also: https://github.com/matplotlib/matplotlib/issues/11486
# note, this bug was fixed in matplotlib 3.0.0
if (not have_new_matplotlib) and scale_const == SQ_ROOT_SCALE :
LOG.warning("Square root scaling is configured for this product and the product data overflows "
"below the lower limit of the range to be plotted. But older versions of matplotlib "
"(pre 3.0.0) cannot "
"display colorbars with an arrow indicating minimum overflow due to a bug. Therefore "
"the minimum overflow indication will not be displayed in the plot for this data.")
input_kwargs['cmap'].set_under(input_kwargs['cmap'](0.0)) # get the min color from the colormap for the min overflow
else :
extend_const = "min"
#input_kwargs['cmap'].set_under(input_kwargs['cmap'](0.0)) # get the min color from the colormap for the min overflow
# if the high end of the range is not as far up as the largest data
if range_to_use[-1] < data_max :
extend_const = "max" if extend_const is None else "both"
#input_kwargs['cmap'].set_over(input_kwargs['cmap'](1.0)) # get the max color from the colormap for the max overflow
input_kwargs['vmin'] = range_to_use[0]
input_kwargs['vmax'] = range_to_use[-1]
# get the scale normalization we want to use
norm_object = None
if scale_const is not None:
min_to_use = data_min if range_to_use is None else range_to_use[0]
max_to_use = data_max if range_to_use is None else range_to_use[-1]
if scale_const == LOG_SCALE:
norm_object = matplotlib.colors.LogNorm(vmin=min_to_use, vmax=max_to_use, )
elif scale_const == SQ_ROOT_SCALE:
norm_object = matplotlib.colors.PowerNorm(gamma=0.5, vmin=min_to_use, vmax=max_to_use, )
if matplotlib.__version__ >= '3.5.0' :
# we need to handle vmin and vmax and the norm_object differently because of:
# https://matplotlib.org/3.5.0/api/prev_api_changes/api_changes_3.5.0.html#stricter-validation-of-function-parameters
if norm_object is not None and 'vmin' in input_kwargs and 'vmax' in input_kwargs :
del input_kwargs['vmin']
del input_kwargs['vmax']
return extend_const, input_kwargs, norm_object
def _get_gridlines_config(scene_id,) :
"""
Figure out how the gridlines should be displayed based on the scene_id.
:param scene_id: The scene id that we are plotting. Should be MESO_ID, CONUS_ID, or FULL_DISK_ID.
:return: If we should draw gridline labels, and locators for the longitude (x) and latitude (y)
"""
# right now we're having some problems with some CONUS domains displaying labels wrong, so turn that off until
# this ticket is fixed: https://github.com/SciTools/cartopy/issues/1819
do_draw_labels = scene_id.lower() != CONUS_ID.lower()
# figure out our line spacing based on the scene id
step_size = 5 if scene_id.lower() == MESO_ID.lower() else 10 if scene_id.lower() == CONUS_ID.lower() else 20
lonLocator = mticker.FixedLocator(range(-180, 180, step_size,))
latLocator = mticker.FixedLocator(range( -80, 80, step_size)) # we want our 20 step to hit the equator, so just don't do the extremes
return do_draw_labels, lonLocator, latLocator
def _make_mapped_figure_and_draw_basic_features (expected_longitude, expected_sat_height, bounding_axes, scene, back_color, line_color, ) :
"""
Make a figure and associated projection and draw basic features like geopolitical boundaries on it.
:param expected_longitude:
:param expected_sat_height:
:param bounding_axes:
:param scene:
:param back_color:
:param line_color:
:return:
"""
# make our cartopy projection object Note: height is in meters, not km
# proj4_str = "+proj=geos +h=35786023.44 +lon_0=-75.0 +sweep=x" # this is right, but cartopy won't take it in this format
# note: the preliminary central longitude was -89.5 before Dec. 2017, then it moved to -75.0
if have_new_cartopy:
proj_object = crs.Geostationary(central_longitude=expected_longitude, satellite_height=expected_sat_height,
sweep_axis='x', )
else:
# in the old version of cartopy there is no way to set the sweep direction to x, and unfortunately the default is y
proj_object = crs.Geostationary(central_longitude=expected_longitude, satellite_height=expected_sat_height, )
# build the plot
figure = plt.figure()
axes = figure.add_subplot(111, projection=proj_object)
LOG.debug("plotting with bounding axes: " + str(bounding_axes))
# cap the extents so cartopy won't freak out about them and set the extent of the plot
extent_list = _cap_extent(bounding_axes, proj_object, scene_name=scene,)
# set the extents so we see the full area regardless of how much data we have
axes.set_extent(extent_list, crs=proj_object)
# figure out our configuration for the gridlines display
draw_gridlines_labels, xLocator, yLocator = _get_gridlines_config(scene, )
# draw some features on our map
# draw some background if we need to; note: cartopy refuses to let me change the image background any other way
if back_color is not None:
axes.add_feature(cartopy.feature.LAND, facecolor=back_color, )
axes.add_feature(cartopy.feature.OCEAN, facecolor=back_color, )
# draw some lat/lon gridlines
gl_obj = axes.gridlines(linestyle=':', zorder=2, edgecolor=line_color, draw_labels=draw_gridlines_labels, )
gl_obj.top_labels = False
gl_obj.right_labels = False
gl_obj.xlocator = xLocator
gl_obj.ylocator = yLocator
# FUTURE, right now cartopy can't label grid lines on a Geostationary plot
axes.coastlines(resolution='50m', zorder=3, color=line_color, )
axes.add_feature(cfeature.BORDERS, linewidth=0.5, zorder=4, edgecolor=line_color, ) # this makes country borders
axes.add_feature(cfeature.LAKES, linestyle=':', linewidth=0.5, facecolor='none', edgecolor=line_color, zorder=5, )
# add states and provinces
# Create a feature for States/Admin 1 regions at 1:50m from Natural Earth
states_provinces = cfeature.NaturalEarthFeature(
category='cultural',
name='admin_1_states_provinces_lines',
scale='50m',
facecolor='none')
axes.add_feature(states_provinces, edgecolor=line_color, linewidth=0.25, zorder=6, )
return figure, axes, proj_object, extent_list
def _draw_infotext (axes, version_txt=None, comment_txt=None, stride_txt=None, draw_no_data_txt=False,) :
"""
Draw various informational text on the plot in different places and styles.
:param version_txt: What version text should we display? (if None, nothing is drawn)
:param comment_txt: What comment text should we display? (if None, nothing is drawn)
:param stride_txt: What stride message text should we display? (if None, nothing is drawn)
:param draw_no_data_txt: Should we be drawing the "No Data Available" message? (if False, nothing is drawn)
:return:
"""
# add an explicit message saying we had no data if we were asked to
if draw_no_data_txt :
plt.text(0.5, 0.5, "No Data\nAvailable", size=40, color='red',
horizontalalignment='center', verticalalignment='center',
transform=axes.transAxes)
# add the comment message if we have one
if comment_txt is not None:
plt.figtext(.02, .05, comment_txt, size=12, color='r', )
# add the stride message if we have one
stride_txt_height = .05 if comment_txt is None else .08
if stride_txt is not None:
plt.figtext(.02, stride_txt_height, stride_txt, size=8, )
# add the version message if we have one
if version_txt is not None:
plt.figtext(.02, .02, version_txt, size=6, )
def create_mapped_figure(data, boundingAxes, title,
invalidMask=None, colorMap=None, backColor=None, lineColor=None,
dataRanges=None, units=None,
valid_range=None, range_to_use=None, scale_const=REGULAR_SCALE,
flag_values=None, flag_names=None, version_txt=None, comment_txt=None,
expected_longitude=None, expected_sat_height=None, scene=None, ):
"""
create a figure including our data on a map
:param data: The raw data to plot
:param boundingAxes: The bounding axes to select the plot view in terms of x and y
:param title: The plot title
:param invalidMask: A mask of any invalid or missing data
:param colorMap: The colormap to use for plotting the data
:param backColor: The background color to use where there isn't any data
:param lineColor: The color to draw geopolitical boundaries with
:param dataRanges: If the data has discrete ranges, the list of ranges to use
:param units: The units that will be displayed on the colorbar
:param valid_range: The potential valid range of the data as defined in the file
:param range_to_use: The range of the data that should be plotted, pinning overflows to the min and max as needed.
If passed as None, use the existing range of the data.
:param scale_const: The type of scale to use on the colorbar. Possible values are defined by constants in the constants module
:param flag_values: If this is a flag or category variable, what are the values we need to plot and label
:param flag_names: If this is a flag or category value, what are the display names of the values
:param version_txt: Informational text describing the versions used to make and plot the data to be displayed on the plot
:param comment_txt: Some informational comment text that needs to be displayed more prominently if it's not None.
:param expected_longitude: The central longitude for the projection this will be plotted in.
:param expected_sat_height: The satellite height for the projection this will be plotted in.
:param scene: An indicator of which scene we're plotting. Should be MESO_ID, CONUS_ID, or FULL_DISK_ID.
:return: The figure containing the data plotted on a map
"""
# make a clean version of data
dataClean = ma.array(data, mask=invalidMask)
# build extra info to go to the map plotting function
kwargs = {}
# if we have a category variable, override the range we use to make the colorbar look ok
temp_valid_range = range_to_use if flag_values is None else [flag_values[0] - 0.5, flag_values[-1] + 0.5, ]
# setup stuff like the colormap and range related kwargs and get the extend constant for the colorbar
extend_const, kwargs, norm_object = _setup_kwargs(kwargs, dataClean, colorMap, flag_values, range_to_use,
scale_const, )
# FUTURE: this is kind of a hack way to get the line colors right, we should probably actually pass this into the method instead
line_color = lineColor if (lineColor is not None) else 'black' if (backColor is None) or (
backColor == 'white') else 'violet'
# create our plot and projection and draw basic geopolitical figures on it
figure, axes, proj_object, extent_list = _make_mapped_figure_and_draw_basic_features(expected_longitude,
expected_sat_height,
boundingAxes, scene,
backColor, line_color,)
# set the interpolation type based on what scene we're displaying
interp_type = 'nearest' if scene != FULL_DISK_ID else 'bilinear'
# plot our data with a map
temp_plt = plt.imshow(dataClean,
transform=proj_object,
extent=extent_list,
origin='upper',
interpolation=interp_type,
norm=norm_object,
zorder=1, **kwargs)
# FUTURE, if we ever need to go back to one of these other plotting methods, here is how to call them
# Note: these could be called using the extent_list rather than lat/lon, but these calls would need revision
# plt.contourf(longitudeClean, latitudeClean, dataClean, transform=crs.PlateCarree(), zorder=1, **kwargs)
# plt.scatter(longitudeClean, latitudeClean, c=dataClean, s=0.2, transform=crs.PlateCarree(), zorder=1, **kwargs)
# plt.pcolormesh(longitudeClean, latitudeClean, dataClean, transform=crs.PlateCarree(), zorder=1, **kwargs)
_draw_infotext(axes, comment_txt=comment_txt, version_txt=version_txt,)
# set the title of the plot
axes.set_title(title)
# show a colorbar
if data is not None:
# if we have a category variable, override tick info we use to make the colorbar look ok.
# Note: right now we are relying on the fact that all of the variables we are plotting have categories that are
# increasing integers (so stuff like [0,1,] or [0,1,2,3,4,5,]). In FUTURE we should try to handle this in
# a more flexible manner (this strategy definitely wouldn't work for things like plotting the QA flags).
tick_positions = None if flag_values is None else _position_category_labels(flag_values)
tick_labels = None if flag_names is None else flag_names.split(" ")
cbar = _add_colorbar(axes, temp_plt, units=units, valid_range=temp_valid_range, data_shape=data.shape,
tick_positions=tick_positions, tick_labels=tick_labels, extend_constant=extend_const,)
return figure
def create_simple_figure(data, figureTitle, invalidMask=None, colorMap=None, backColor=None, units=None, drawColorbar=True,
valid_range=None, range_to_use=None, scale_const=REGULAR_SCALE,
flag_values=None, flag_names=None, version_txt=None, comment_txt=None,):
"""
create a simple figure showing the data given
:param data: The data to show
:param figureTitle: The title of the figure
:param invalidMask: A mask of any invalid or missing data in the data set
:param colorMap: The colormap to use when plotting the data (if None the default for imshow will be used)
:param backColor: The background color to use where there isn't any data
:param units: The units that will be displayed on the colorbar
:param drawColorbar: Whether or not to draw a colorbar at all
:param valid_range: The potential valid range of the data as defined in the file
:param range_to_use: The range of the data that should be plotted, pinning overflows to the min and max as needed.
If passed as None, use the existing range of the data.
:param scale_const: The type of scale to use on the colorbar. Possible values are defined by constants in the constants module
:param flag_values: If this is a flag or category variable, what are the values we need to plot and label
:param flag_names: If this is a flag or category value, what are the display names of the values
:param version_txt: Informational text describing the versions used to make and plot the data to be displayed on the plot
:param comment_txt:
:return: The figure containing the image of the data
"""
cleanData = ma.array(data, mask=invalidMask)
# build the plot
figure = plt.figure()
axes = figure.add_subplot(111, facecolor=backColor,)
# build extra info to go to the map plotting function
kwargs = {}
# setup stuff like the colormap and range related kwargs and get the extend constant for the colorbar
extend_const, kwargs, norm_object = _setup_kwargs(kwargs, cleanData, colorMap, flag_values, range_to_use, scale_const,)
if (data is not None) and (numpy.sum(invalidMask) < invalidMask.size):
# draw our data
im = imshow(cleanData, norm=norm_object, **kwargs)
if drawColorbar :
# if we have a category variable, override tick info we use to make the colorbar look ok.
# Note: right now we are relying on the fact that all of the variables we are plotting have categories that are
# increasing integers (so stuff like [0,1,] or [0,1,2,3,4,5,]). In FUTURE we should try to handle this in
# a more flexible manner (this strategy definitely wouldn't work for things like plotting the QA flags).
temp_valid_range = range_to_use if flag_values is None else [flag_values[0] - 0.5, flag_values[-1] + 0.5,]
tick_positions = None if flag_values is None else _position_category_labels(flag_values)
tick_labels = None if flag_names is None else flag_names.split(" ")
cbar = _add_colorbar(axes, im, units=units,
valid_range=temp_valid_range, data_shape=data.shape,
tick_positions=tick_positions, tick_labels=tick_labels,
extend_constant=extend_const, )
_draw_infotext(axes, version_txt=version_txt, comment_txt=comment_txt,)
# and some informational stuff
axes.set_title(figureTitle)
return figure
def create_winds_on_map_figure (xData, yData, uData, vData, colorData, boundingAxes, title,
colorMap=None, backColor=None, colorUnitsTxt=None,
valid_range=None,
version_txt=None, comment_txt=None, stride_txt=None,
expected_longitude=None, expected_sat_height=None,
scene=None, ) :
"""
create a image of the winds vectors on a map
Note: cartopy's documentation says that for wind barb and quiver plotting
"The vector components must be defined as grid eastward and grid northward."
:param xData: The x that corresponds to each winds vector.
:param yData: The y that corresponds to each winds vector.
:param uData: The U component of the winds vector.
:param vData: The V component of the winds vector.
:param colorData: The color data at the site of the winds vector (normally expect air pressure).
:param boundingAxes: The bounding axes to select the plot view in terms of x and y
:param title: The plot title
:param colorMap: The colormap to use for plotting the data
:param backColor: The background color to use where there isn't any data
:param colorUnitsTxt: Text describing the units of the color data (for use on the colorbar label)
:param valid_range: The potential valid range of the color data as defined in the file
:param version_txt: Informational text describing the versions used to make and plot the data to be displayed on the plot
:param comment_txt:
:param stride_txt:
:param expected_longitude: The central longitude for the projection this will be plotted in.
:param expected_sat_height: The satellite height for the projection this will be plotted in.
:param scene: An indicator of which scene we're plotting. Should be MESO_ID, CONUS_ID, or FULL_DISK_ID.
:return: The figure containing the winds vectors plotted on a map
"""
# build extra info to go to the map plotting function
kwargs = {}
if colorMap is not None:
kwargs['cmap'] = colorMap
if valid_range is not None :
#kwargs['norm'] = matplotlib.colors.LogNorm(vmin=valid_range[0] +0.0001, vmax=valid_range[1],) # LOG scaling for testing
kwargs['norm'] = Normalize(vmin=valid_range[0], vmax=valid_range[1],)
# FUTURE: this is kind of a hack way to get the line colors right, we should probably actually pass this into the method instead
line_color = 'black' if (backColor is None) or (backColor == 'white') else 'violet'
# create our plot and projection and draw basic geopolitical figures on it
figure, axes, proj_object, extent_list = _make_mapped_figure_and_draw_basic_features(expected_longitude,
expected_sat_height,
boundingAxes, scene,
backColor, line_color, )
# our x and y need to be multiplied by the height of the satellite
xData = xData * expected_sat_height
yData = yData * expected_sat_height
# put our winds on the map
temp_plt = plt.quiver(xData, yData, uData, vData, colorData,
transform=proj_object,
pivot='middle',
zorder=1,
# autoscaling was causing us weird consistency issues when we had different numbers of
# wind vectors in one file vs another, so manual scale for now
#scale_units="width",
#scale=900.0,
**kwargs
)
# for future reference, here is the basic method to plot wind barbs
#temp_plt = plt.barbs(xData, yData, uData, vData, colorData, length=5, zorder=1,
# sizes=dict(emptybarb=0.25, spacing=0.2, height=0.5),
# linewidth=0.95, transform=proj_object, **kwargs)
_draw_infotext(axes, version_txt=version_txt, comment_txt=comment_txt, stride_txt=stride_txt,)
# set the title of the plot
axes.set_title(title)
# show a colorbar
if colorData is not None:
cbar = _add_colorbar(axes, temp_plt, units=colorUnitsTxt,
valid_range=valid_range,
data_shape=colorData.shape, )
return figure
def create_nodata_figure(figureTitle, version_txt=None, comment_txt=None,):
"""
create a blank figure to convey that we had no data to plot
:param figureTitle: The title of the figure
:param version_txt:
:param comment_txt:
:return: The figure containing the title and an explicit message saying we had no data
"""
# build the plot
figure = plt.figure()
axes = figure.add_subplot(111)
_draw_infotext(axes, version_txt=version_txt, comment_txt=comment_txt, draw_no_data_txt=True, )
# set the title
axes.set_title(figureTitle)
return figure
def create_mapped_nodata_figure(boundingAxes, title,
backColor=None, lineColor=None,
version_txt=None, comment_txt=None,
expected_longitude=None, expected_sat_height=None, scene=None, ):
"""
create a no-data figure on a map
:param boundingAxes: The bounding axes to select the plot view in terms of x and y
:param title: The plot title
:param backColor: The background color to use
:param lineColor: The color to draw geopolitical boundaries with
:param version_txt: Informational text describing the versions used to make and plot the data to be displayed on the plot
:param comment_txt: Some informational comment text that needs to be displayed more prominently if it's not None.
:param expected_longitude: The central longitude for the projection this will be plotted in.
:param expected_sat_height: The satellite height for the projection this will be plotted in.
:param scene: An indicator of which scene we're plotting. Should be MESO_ID, CONUS_ID, or FULL_DISK_ID.
:return: The figure containing a map of the area where the data would be, labeled with
"No Data Available" in large red letters
"""
# FUTURE: this is kind of a hack way to get the line colors right, we should probably actually pass this into the method instead
line_color = lineColor if (lineColor is not None) else 'black' if (backColor is None) or (backColor == 'white') else 'violet'
# create our plot and projection and draw basic geopolitical figures on it
figure, axes, proj_object, extent_list = _make_mapped_figure_and_draw_basic_features(expected_longitude,
expected_sat_height,
boundingAxes, scene,
backColor, line_color, )
_draw_infotext(axes, version_txt=version_txt, comment_txt=comment_txt, draw_no_data_txt=True,)
# set the title of the plot
axes.set_title(title)
return figure