"""
Pylab plotting utilities.
"""
__all__ = [
"config_matplotlib",
"auto_shift",
"coordinated_colors",
"dhsv",
"next_color",
"plot_quantiles",
"form_quantiles",
]
[docs]
def set_mplconfig(appdatadir):
r"""
Point the matplotlib config dir to %LOCALAPPDATA%\{appdatadir}\mplconfig.
"""
import os
import sys
if hasattr(sys, "frozen"):
if os.name == "nt":
mplconfigdir = os.path.join(os.environ["LOCALAPPDATA"], appdatadir, "mplconfig")
elif sys.platform == "darwin":
mplconfigdir = os.path.join(os.path.expanduser("~/Library/Caches"), appdatadir, "mplconfig")
else:
return # do nothing on linux
mplconfigdir = os.environ.setdefault("MPLCONFIGDIR", mplconfigdir)
if not os.path.exists(mplconfigdir):
os.makedirs(mplconfigdir)
[docs]
def config_matplotlib(backend=None):
"""
Setup matplotlib to use a particular backend.
The backend should be 'WXAgg' for interactive use, or 'Agg' for batch.
This distinction allows us to run in environments such as cluster computers
which do not have wx installed on the compute nodes.
This function must be called before any imports to matplotlib. To allow
this, modules should not import matplotlib at the module level, but instead
import it for each function/method that uses it. Exceptions can be made
for modules which are completely dedicated to plotting, but these modules
should never be imported at the module level.
"""
import os
import sys
import matplotlib as mpl
# When running from a frozen environment created by py2exe, we will not
# have a range of backends available, and must set the default to WXAgg.
# With a full matplotlib distribution we can use whatever the user prefers.
if hasattr(sys, "frozen"):
if "MPLCONFIGDIR" not in os.environ:
raise RuntimeError(r"MPLCONFIGDIR should be set to e.g., %LOCALAPPDATA%\YourApp\mplconfig")
if backend is None:
backend = "WXAgg"
## CRUFT: check that backend is valid, trying alternates if an import fails
# if backend is None:
# backend = os.environ.get('MPLBACKEND', mpl.rcParams['backend'])
# import importlib
# for name in (backend, 'MacOSX', 'Qt5Agg', 'Qt4Agg', 'Gtk3Agg', 'TkAgg', 'WXAgg'):
# path = 'matplotlib.backends.backend_' + name.lower()
# try:
# importlib.import_module(path)
# backend = name
# break
# except ImportError:
# backend = None
# Specify the backend to use for plotting and import backend dependent
# classes. This must be done before importing pyplot to have an
# effect. If no backend is given, let pyplot use the default.
if backend is not None:
mpl.use(backend)
# Disable interactive mode so that plots are only updated on show() or
# draw(). The interactive function must be called before importing pyplot,
# otherwise it will have no effect.
mpl.interactive(False)
# configure the plot style
line_width = 1
pad = 2
font_family = "Arial" if os.name == "nt" else "sans-serif"
font_size = 12
plot_style = {
"xtick.direction": "in",
"ytick.direction": "in",
"lines.linewidth": line_width,
"axes.linewidth": line_width,
"xtick.labelsize": font_size,
"ytick.labelsize": font_size,
"xtick.major.size": 5,
"ytick.major.size": 5,
"xtick.minor.size": 2.5,
"ytick.minor.size": 2.5,
"xtick.major.width": line_width,
"ytick.major.width": line_width,
"xtick.minor.width": line_width,
"ytick.minor.width": line_width,
"xtick.major.pad": pad,
"ytick.major.pad": pad,
"xtick.top": True,
"ytick.right": True,
"font.size": font_size,
"font.family": font_family,
"svg.fonttype": "none",
"savefig.dpi": 100,
}
mpl.rcParams.update(plot_style)
[docs]
def auto_shift(offset):
"""
Return a y-offset coordinate transform for the current axes.
Each call to auto_shift increases the y-offset for the next line by
the given number of points (with 72 points per inch).
Example::
from matplotlib import pyplot as plt
from bumps.plotutil import auto_shift
trans = auto_shift(plt.gca())
plot(x, y, trans=trans)
"""
from matplotlib.transforms import ScaledTranslation
import matplotlib.pyplot as plt
ax = plt.gca()
if ax.lines and hasattr(ax, "_auto_shift"):
ax._auto_shift += offset
else:
ax._auto_shift = 0
trans = plt.gca().transData
if ax._auto_shift:
trans += ScaledTranslation(0, ax._auto_shift / 72.0, plt.gcf().dpi_scale_trans)
return trans
# ======== Color functions ========
[docs]
def next_color(axes=None):
"""
Return the next color in the plot color cycle.
Example::
from matplotlib import pyplot as plt
from bumps.plotutil import next_color, dhsv
color = next_color()
plt.errorbar(x, y, yerr=dy, fmt='.', color=color)
# Draw the theory line with the same color as the data, but darker
plt.plot(x, y, '-', color=dhsv(color, dv=-0.2))
"""
import matplotlib.pyplot as plt
if axes is None:
axes = plt.gca()
lines = axes._get_lines
try:
base = lines.get_next_color()
except Exception:
try: # Cruft 1.7 - 3.7?
base = next(lines.prop_cycler)["color"]
except Exception:
try: # Cruft 1.4-1.6?
base = next(lines.color_cycle)
except Exception: # Cruft 1.3 and earlier
base = lines._get_next_cycle_color()
return base
[docs]
def coordinated_colors(base=None):
"""
Return a set of coordinated colors as c['base|light|dark'].
If *base* is not provided, use the next color in the color cycle as
the base. Light is bright and pale, dark is dull and saturated.
"""
if base is None:
base = next_color()
return dict(
base=base,
light=dhsv(base, dv=+0.3, ds=-0.2),
dark=dhsv(base, dv=-0.25, ds=+0.35),
)
[docs]
def dhsv(color, dh=0.0, ds=0.0, dv=0.0, da=0.0):
"""
Modify color on hsv scale.
*dv* change intensity, e.g., +0.1 to brighten, -0.1 to darken.
*dh* change hue
*ds* change saturation
*da* change transparency
Color can be any valid matplotlib color. The hsv scale is [0,1] in
each dimension. Saturation, value and alpha scales are clipped to [0,1]
after changing. The hue scale wraps between red to violet.
:Example:
Make sea green 10% darker:
>>> from bumps.plotutil import dhsv
>>> darker = dhsv('seagreen', dv=-0.1)
>>> print([int(v*255) for v in darker])
[37, 113, 71, 255]
"""
from matplotlib.colors import colorConverter
from colorsys import rgb_to_hsv, hsv_to_rgb
from numpy import clip, array, fmod
r, g, b, a = colorConverter.to_rgba(color)
# print "from color",r,g,b,a
h, s, v = rgb_to_hsv(r, g, b)
s, v, a = [clip(val, 0.0, 1.0) for val in (s + ds, v + dv, a + da)]
h = fmod(h + dh, 1.0)
r, g, b = hsv_to_rgb(h, s, v)
# print "to color",r,g,b,a
return array((r, g, b, a))
# ==== Specialized plotters =====
[docs]
def plot_quantiles(x, y, contours, color, alpha=None, axes=None):
"""
Plot quantile curves for a set of lines.
*x* is the x coordinates for all lines.
*y* is the y coordinates, one row for each line.
*contours* is a list of confidence intervals expressed as percents.
*color* is the color to use for the quantiles. Quantiles are draw as
a filled region with alpha transparency. Higher probability regions
will be covered with multiple contours, which will make them lighter
and more saturated.
*alpha* is the transparency level to use for all fill regions. The
default value, alpha=2./(#contours+1), works pretty well.
"""
_, q = form_quantiles(y, contours)
_plot_quantiles(x, q, color, alpha, axes=axes)
def _plot_quantiles(x, q, color, alpha, axes=None):
import matplotlib.pyplot as plt
# print "p",p
# print "q",q[:,:,0]
# print "y",y[:,0]
if axes is None:
axes = plt.gca()
if alpha is None:
alpha = 2.0 / (len(q) + 1)
edgecolor = dhsv(color, ds=-(1 - alpha), dv=(1 - alpha))
for lo, hi in q:
axes.fill_between(x, lo, hi, facecolor=color, edgecolor=edgecolor, alpha=alpha)
def _convert_contours_to_probabilities(contours):
"""
Given confidence intervals [a, b,...] as percents, return probability
in [0,1] for each interval as [a_low, a_high, b_low, b_high, ...].
"""
from numpy import hstack
# lower quantile for ci in percent = (100 - ci)/2
# upper quantile = 100 - lower quantile = 100 - (100-ci)/2 = (100 + ci)/2
# divide by an additional 100 to get proportion from 0 to 1
return hstack([(100.0 - p, 100.0 + p) for p in contours]) / 200.0