#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
plot: SpacePy plotting routines
This package aims to make getting publication ready plots easier.
It provides classes and functions for different types of plot
(e.g. Spectrogram, levelPlot), for helping make plots more cleanly
(e.g. set_target, dual_half_circle), and for making plots convey
information more cleanly, with less effort
(e.g. applySmartTimeTicks, style).
This plot module now provides style sheets. For most standard plotting
we recommend the *default* style sheet (aka *spacepy*). To apply the
default plot style, use the following::
import spacepy.plot as splot
splot.style()
.. versionchanged:: 0.5.0
Plot styles are no longer applied automatically.
Different plot types may not work well with this style, so we have provided
alternatives. For polar plots, spectrograms, or anything with larger blocks
of color, it may be better to use one of the alternatives::
import spacepy.plot as splot
splot.style('altgrid') # inverts background from default so it's white
splot.style('polar') # designed for filled polar plots
splot.revert_style() # put the style back to matplotlib defaults
Authors: Brian Larsen and Steve Morley
Institution: Los Alamos National Laboratory
Contact: balarsen@lanl.gov
Copyright 2011-2016 Los Alamos National Security, LLC.
Most of the functionality in the plot module is made available directly
through the *plot* namespace. However, the plot module does contain
several submodules listed below
"""
from collections.abc import Mapping
import os
import warnings
import numpy as np
import matplotlib as mpl
from matplotlib.patches import Wedge
from matplotlib import __version__ as mplv
import matplotlib.pyplot as plt
from spacepy import __path__ as basepath
from spacepy.datamodel import dmcopy
import spacepy.datamanager as dman
from .. import config
from .spectrogram import *
from .utils import *
from .carrington import *
[docs]
def plot(*args, **kwargs):
'''Convenience wrapper for matplotlib's plot function
As with matplotlib's plot function, *args* is a variable length
argument, allowing for multiple *x*, *y* pairs, each with optional
format string. For full details, see matplotlib.pyplot.plot
Other Parameters
----------------
smartTimeTicks : boolean
If True then use applySmartTimeTicks to set x-axis labeling
figsize : array-like, 2 elements
Set figure size directly on call to plot, (width, height)
**kwargs : other keywords
Other keywords to pass to matplotlib.pyplot.plot
'''
if 'smartTimeTicks' in kwargs:
sTT = kwargs['smartTimeTicks']
del kwargs['smartTimeTicks']
else:
sTT = False
if 'figsize' not in kwargs:
kwargs['figsize'] = (10,6)
fig = plt.figure(figsize=kwargs['figsize'])
del kwargs['figsize']
pobj = plt.plot(*args, **kwargs)
if sTT is True:
ax = plt.gca()
applySmartTimeTicks(ax, args[0], dolimit=True, dolabel=False)
return pobj
[docs]
def available(returnvals=False):
'''List the available plot styles provided by spacepy.plot
Note that some of the available styles have multiple aliases.
To apply an available style, use `spacepy.plot.style`.
'''
spacepystyle = os.path.join('{0}'.format(basepath[0]), 'data', 'spacepy.mplstyle')
spacepyaltstyle = os.path.join('{0}'.format(basepath[0]), 'data', 'spacepy_altgrid.mplstyle')
polarstyle = os.path.join('{0}'.format(basepath[0]), 'data', 'spacepy_polar.mplstyle')
lookdict = {'default': spacepystyle,
'spacepy': spacepystyle,
'spacepy_altgrid': spacepyaltstyle,
'altgrid': spacepyaltstyle,
'spacepy_polar': polarstyle,
'polar': polarstyle
}
if returnvals:
return lookdict
else:
return list(lookdict.keys())
[docs]
def style(look=None, cmap='plasma'):
'''
Apply SpacePy's matplotlib style settings from a known style sheet.
Parameters
----------
look : str, optional
Name of style. For a list of available style names, see
`spacepy.plot.available`. If not specified, will use default
``"spacepy"`` style.
'''
lookdict = available(returnvals=True)
usestyle = lookdict.get(look, lookdict['default'])
plt.style.use(usestyle)
mpl.rcParams['image.cmap'] = cmap
#save current rcParams before applying spacepy style
oldParams = {key: dmcopy(val) for key, val in mpl.rcParams.items()}
[docs]
def revert_style():
'''Revert plot style settings to those in use prior to importing spacepy.plot
'''
with warnings.catch_warnings():
warnings.simplefilter("ignore")
for key in oldParams:
try:
mpl.rcParams[key] = oldParams[key]
except ValueError:
pass
[docs]
def dual_half_circle(center=(0,0), radius=1.0,
sun_direction='right', ax=None, colors=('w','k'),
**kwargs):
"""
Plot two half circles to a plot with the specified face colors and
rotation. This is normal to use to denote the sun direction in
magnetospheric science plots.
Other Parameters
----------------
center : array-like, 2 elements
Center in data coordinates of the circles, default (0,0)
radius : float
Radius of the circles, defualt 1.0
sun_direction : string or float
The rotation direction of the first (white) circle. Options are ['down',
'down right', 'right', 'up left', 'up right', 'up', 'down left', 'left']
or an angle in degrees counter-clockwise from up. Default right.
ax : matplotlib.axes
Axis to plot the circles on.
colors : array-like, 2 elements
The two colors for the circle fill. The First number is the light and
second is the dark.
**kwargs : other keywords
Other keywords to pass to matplotlib.patches.Wedge
Returns
-------
out : tuple
Tuple of the two wedge objects
Examples
--------
>>> import spacepy.plot
>>> spacepy.plot.dual_half_circle()
"""
sun_dict = {'left':90,
'right':-90,
'up':0,
'down':180,
'up right':-45,
'up left':45,
'down right':45+180,
'down left':-45+180,
}
try:
angle = sun_dict[sun_direction]
except KeyError:
try:
angle = float(sun_direction)
except ValueError:
raise(ValueError("Sun_direction was not understood, must be a float or {0}".format(sun_dict.keys())))
theta1, theta2 = angle, angle + 180
if ax is None:
ax = plt.gca()
w1 = Wedge(center, radius, theta1, theta2, fc=colors[0],transform=ax.transData._b, **kwargs)
w2 = Wedge(center, radius, theta2, theta1, fc=colors[1], transform=ax.transData._b,**kwargs)
for wedge in [w1, w2]:
ax.add_artist(wedge)
return (w1, w2)
[docs]
def levelPlot(data, var=None, time=None, levels=(3, 5), target=None, colors=None, **kwargs):
"""
Draw a step-plot with up to 5 levels following a color cycle (e.g. Kp index "stoplight")
Parameters
----------
data : array-like, or dict-like
Data for plotting. If dict-like, the key providing an array-like
to plot must be given to var keyword argument.
Other Parameters
----------------
var : string
Name of key in dict-like input that contains data
time : array-like or string
Name of key in dict-like that contains time, or arraylike of datetimes
levels : array-like, up to 5 levels
Breaks between levels in data that should be shown as distinct colors
target : figure or axes
Target axes or figure window
colors : array-like
Colors to use for the color sequence (if insufficient colors, will use as a cycle)
**kwargs : other keywords
Other keywords to pass to spacepy.toolbox.binHisto
Returns
-------
binned : tuple
Tuple of the binned data and bins
Examples
--------
>>> import spacepy.plot as splot
>>> import spacepy.time as spt
>>> import spacepy.omni as om
>>> tt = spt.tickrange('2012/09/28','2012/10/2', 3/24.)
>>> omni = om.get_omni(tt)
>>> splot.levelPlot(omni, var='Kp', time='UTC', colors=['seagreen', 'orange', 'crimson'])
"""
#assume dict-like/key-access, before moving to array-like
if var is not None:
try:
usearr = data[var]
except KeyError:
raise KeyError('Key "{1}" not present in data'.format(var))
else:
#var is None, so make sure we don't have a dict-like
if not isinstance(data, Mapping):
usearr = np.asarray(data)
else:
raise TypeError('Data appears to be dict-like without a key being given')
tflag = False
if time is not None:
import scipy.stats
try:
times = data[time]
except (KeyError, ValueError, IndexError):
times = time
try:
times = matplotlib.dates.date2num(times)
tflag = True
except AttributeError:
#the x-data are a non-datetime
times = np.asarray(time)
#now add the end-point
try: # scipy 1.9 and later
stepsize, dum = scipy.stats.mode(np.diff(times), keepdims=False)
except TypeError:
stepsize, dum = scipy.stats.mode(np.diff(times), axis=None)
times = np.hstack([times, times[-1]+stepsize])
else:
times = np.asarray(range(0, len(usearr)+1))
if not colors:
if len(levels)<=3:
#traffic light colours that are distinct to protanopes and deuteranopes
colors = ['lime', 'yellow', 'crimson', 'saddlebrown']
else:
colors = matplotlib.rcParams['axes.color_cycle']
else:
try:
assert len(colors) > len(levels)
except AssertionError:
#cycle the given colors, if not enough are given
colors = list(colors)*int(1+len(levels)/len(colors))
if 'alpha' not in kwargs:
kwargs['alpha']=0.75
if 'legend' not in kwargs:
legend = False
else:
legend = kwargs['legend']
del kwargs['legend']
fig, ax = set_target(target)
subset = np.asarray(dmcopy(usearr))
def fill_between_steps(ax, x, y1, **kwargs):
y2 = np.zeros_like(y1)
stepsxx = x.repeat(2)[1:-1]
stepsyy = y1.repeat(2)
y2 = np.zeros_like(stepsyy)
ax.fill_between(stepsxx, stepsyy, y2, **kwargs)
#below threshold 1
idx = 0
inds = usearr>levels[0]
subset[inds] = np.nan
kwargs['label'] = u'≤{0}'.format(levels[idx])
fill_between_steps(ax, times, subset, color=colors[0], zorder=30, **kwargs)
#for each of the "between" thresholds
for idx in range(1,len(levels)):
subset = np.asarray(dmcopy(usearr))
inds = np.bitwise_or(usearr<=levels[idx-1], usearr>levels[idx])
subset[inds] = np.nan
kwargs['label'] = u'>{0},≤{1}'.format(levels[idx-1], levels[idx])
fill_between_steps(ax, times, subset, color=colors[idx], zorder=30-(idx*2), **kwargs)
#last
idx += 1
try:
inds = usearr<=levels[idx-1]
subset = np.asarray(dmcopy(usearr))
subset[inds] = np.nan
kwargs['label'] = '>{0}'.format(levels[-1])
fill_between_steps(ax, times, subset, color=colors[idx], zorder=30-(idx*2), **kwargs)
except:
pass
#if required, set x axis to times
if tflag:
try:
applySmartTimeTicks(ax, data[time])
except (IndexError, KeyError):
#using data array to index, so should just use time
applySmartTimeTicks(ax, time)
ax.grid(False, which='minor') #minor grid usually looks bad on these...
if legend:
ncols = len(levels)+1
if ncols > 3: ncols = ncols//2
ax.legend(loc='upper left', ncol=ncols)
return ax