blob: c03329d7c3267b1e846654f4051660d4aa2851f2 [file] [log] [blame]
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Module that handles the generation of data plots"""
# Import Statements
from math import floor, log
from matplotlib import pyplot as plt
from mpl_toolkits.basemap import Basemap
import matplotlib as mpl
import numpy as np
import os
def pow_round(x):
'''
Function to round x to the nearest power of 10
'''
return 10 ** (floor(log(x, 10) - log(0.5, 10)))
def calc_nice_color_bar_values(mymin, mymax, target_nlevs):
'''
Function to help make nicer plots.
Calculates an appropriate min, max and number of intervals to use in a color bar
such that the labels come out as round numbers.
i.e. often, the color bar labels will come out as 0.1234 0.2343 0.35747 0.57546
when in fact you just want 0.1, 0.2, 0.3, 0.4, 0.5 etc
Method::
Adjusts the max,min and nlevels slightly so as to provide nice round numbers.
Input::
mymin - minimum of data range (or first guess at minimum color bar value)
mymax - maximum of data range (or first guess at maximum color bar value)
target_nlevs - approximate number of levels/color bar intervals you would like to have
Output::
newmin - minimum value of color bar to use
newmax - maximum value of color bar to use
new_nlevs - number of intervals in color bar to use
* when all of the above are used, the color bar should have nice round number labels.
'''
myrange = mymax - mymin
# Find target color bar label interval, given target number of levels.
# NB. this is likely to be not a nice rounded number.
target_interval = myrange / float(target_nlevs)
# Find power of 10 that the target interval lies in
nearest_ten = pow_round(target_interval)
# Possible interval levels,
# i.e. labels of 1,2,3,4,5 etc are OK,
# labels of 2,4,6,8,10 etc are OK too
# labels of 3,6,9,12 etc are NOT OK (as defined below)
# NB. this is also true for any multiple of 10 of these values
# i.e. 0.01,0.02,0.03,0.04 etc are OK too.
pos_interval_levels = np.array([1, 2, 5])
# Find possible intervals to use within this power of 10 range
candidate_intervals = (pos_interval_levels * nearest_ten)
# Find which of the candidate levels is closest to the target level
absdiff = abs(target_interval - candidate_intervals)
rounded_interval = candidate_intervals[np.where(absdiff == min(absdiff))]
# Define actual nlevels to use in colorbar
nlevels = myrange / rounded_interval
# Define the color bar labels
newmin = mymin - mymin % rounded_interval
all_labels = np.arange(newmin, mymax + rounded_interval, rounded_interval)
newmin = all_labels.min()
newmax = all_labels.max()
new_nlevs = int(len(all_labels)) - 1
return newmin, newmax, new_nlevs
def draw_cntr_map_single(pVar, lats, lons, mnLvl, mxLvl, pTitle, pName, pType = 'png', cMap = None):
'''
Purpose::
Plots a filled contour map.
Input::
pVar - 2d array of the field to be plotted with shape (nLon, nLat)
lon - array of longitudes
lat - array of latitudes
mnLvl - an integer specifying the minimum contour level
mxLvl - an integer specifying the maximum contour level
pTitle - a string specifying plot title
pName - a string specifying the filename of the plot
pType - an optional string specifying the filetype, default is .png
cMap - an optional matplotlib.LinearSegmentedColormap object denoting the colormap,
default is matplotlib.pyplot.cm.jet
TODO: Let user specify map projection, whether to mask bodies of water??
'''
if cMap is None:
cMap = plt.cm.jet
# Set up the figure
fig = plt.figure()
ax = fig.gca()
# Determine the map boundaries and construct a Basemap object
lonMin = lons.min()
lonMax = lons.max()
latMin = lats.min()
latMax = lats.max()
m = Basemap(projection = 'cyl', llcrnrlat = latMin, urcrnrlat = latMax,
llcrnrlon = lonMin, urcrnrlon = lonMax, resolution = 'l', ax = ax)
# Draw the borders for coastlines and countries
m.drawcoastlines(linewidth = 1)
m.drawcountries(linewidth = .75)
# Draw 6 parallels / meridians.
m.drawmeridians(np.linspace(lonMin, lonMax, 5), labels = [0, 0, 0, 1])
m.drawparallels(np.linspace(latMin, latMax, 5), labels = [1, 0, 0, 1])
# Convert lats and lons to projection coordinates
if lats.ndim == 1 and lons.ndim == 1:
lons, lats = np.meshgrid(lons, lats)
x, y = m(lons, lats)
# Plot data with filled contours
nsteps = 24
mnLvl, mxLvl, nsteps = calc_nice_color_bar_values(mnLvl, mxLvl, nsteps)
spLvl = (mxLvl - mnLvl) / nsteps
clevs = np.arange(mnLvl, mxLvl, spLvl)
cs = m.contourf(x, y, pVar, cmap = cMap)
# Add a colorbar and save the figure
cbar = m.colorbar(cs, ax = ax, pad = .05)
plt.title(pTitle)
fig.savefig('%s.%s' %(pName, pType))
def draw_time_series_plot(data, times, myfilename, myworkdir, data2='', mytitle='', ytitle='Y', xtitle='time', year_labels=True):
'''
Purpose::
Function to draw a time series plot
Input::
data - a masked numpy array of data masked by missing values
times - a list of python datetime objects
myfilename - stub of png file created e.g. 'myfile' -> myfile.png
myworkdir - directory to save images in
data2 - (optional) second data line to plot assumes same time values)
mytitle - (optional) chart title
xtitle - (optional) y-axis title
ytitle - (optional) y-axis title
Output::
no data returned from function
Image file produced with name {filename}.png
'''
print 'Producing time series plot'
fig = plt.figure()
ax = fig.gca()
if year_labels == False:
xfmt = mpl.dates.DateFormatter('%b')
ax.xaxis.set_major_formatter(xfmt)
# x-axis title
plt.xlabel(xtitle)
# y-axis title
plt.ylabel(ytitle)
# Main title
fig.suptitle(mytitle, fontsize=12)
# Set y-range to sensible values
# NB. if plotting two lines, then make sure range appropriate for both datasets
ymin = data.min()
ymax = data.max()
# If data2 has been passed in, then set plot range to fit both lines.
# NB. if data2 has been passed in, then it is an array, otherwise it defaults to an empty string
if type(data2) != str:
ymin = min(data.min(), data2.min())
ymax = max(data.max(), data2.max())
# add a bit of padding so lines don't touch bottom and top of the plot
ymin = ymin - ((ymax - ymin) * 0.1)
ymax = ymax + ((ymax - ymin) * 0.1)
# Set y-axis range
plt.ylim((ymin, ymax))
# Make plot, specifying marker style ('x'), linestyle ('-'), linewidth and line color
line1 = ax.plot_date(times, data, 'bo-', markersize=6, linewidth=2, color='#AAAAFF')
# Make second line, if data2 has been passed in.
# TODO: Handle the optional second dataset better. Maybe set the Default to None instead
# of an empty string
if type(data2) != str:
line2 = ax.plot_date(times, data2, 'rx-', markersize=6, linewidth=2, color='#FFAAAA')
lines = []
lines.extend(line1)
lines.extend(line2)
fig.legend((lines), ('model', 'obs'), loc='upper right')
fig.savefig(myworkdir + '/' + myfilename + '.png')