import logging
import warnings
from packaging.version import Version
from functools import wraps
import numpy as np
import matplotlib.units as units
import matplotlib.dates as dates
from matplotlib.legend_handler import HandlerBase
from matplotlib.patches import Rectangle
from matplotlib import __version__
# We avoid importing matplotlib up here otherwise Matplotlib and therefore Qt
# get imported as soon as glue.utils is imported.
from glue.utils.misc import DeferredMethod
MATPLOTLIB_GE_30 = Version(__version__) >= Version('3')
MATPLOTLIB_GE_36 = Version(__version__) >= Version('3.6')
__all__ = ['all_artists', 'new_artists', 'remove_artists',
'get_extent', 'view_cascade', 'fast_limits', 'defer_draw',
'color2rgb', 'point_contour',
'datetime64_to_mpl', 'mpl_to_datetime64', 'color2hex']
[docs]def all_artists(fig):
"""
Build a set of all Matplotlib artists in a Figure
Parameters
----------
fig : :func:`matplotlib.pyplot.figure`
Matplotlib figure.
"""
return set(item
for axes in fig.axes
for container in [axes.collections, axes.patches, axes.lines,
axes.texts, axes.artists, axes.images]
for item in container)
[docs]def new_artists(fig, old_artists):
"""
Find the newly-added artists in a figure.
Parameters
----------
fig : :func:`matplotlib.pyplot.figure`
Matplotlib figure.
old_artists : set
Return value from :func:`all_artists`.
Returns
-------
set
All artists added since ``all_artists`` was called.
"""
return all_artists(fig) - old_artists
[docs]def remove_artists(artists):
"""
Remove a collection of matplotlib artists from a scene.
Parameters
----------
artists : sequence
Container of artists.
"""
for a in artists:
try:
a.remove()
except ValueError: # already removed
pass
[docs]def get_extent(view, transpose=False):
sy, sx = [s for s in view if isinstance(s, slice)]
if transpose:
return (sy.start, sy.stop, sx.start, sx.stop)
return (sx.start, sx.stop, sy.start, sy.stop)
[docs]def view_cascade(data, view):
"""
Return a set of views progressively zoomed out of input at roughly constant
pixel count.
Parameters
----------
data : array-like
The array to view.
view : iterable
The original view into the data.
"""
shp = data.shape
v2 = list(view)
logging.debug("image shape: %s, view: %s", shp, view)
# choose stride length that roughly samples entire image
# at roughly the same pixel count
step = max(shp[i - 1] * v.step // max(v.stop - v.start, 1)
for i, v in enumerate(view) if isinstance(v, slice))
step = max(step, 1)
for i, v in enumerate(v2):
if not isinstance(v, slice):
continue
v2[i] = slice(0, shp[i - 1], step)
return tuple(v2), view
def _scoreatpercentile(values, percentile, limit=None):
# Avoid using the scipy version since it is available in Numpy
if limit is not None:
values = values[(values >= limit[0]) & (values <= limit[1])]
return np.percentile(values, percentile)
[docs]def fast_limits(data, plo, phi):
"""
Quickly estimate percentiles in an array, using a downsampled version.
Parameters
----------
data : `~numpy.ndarray`
The array to estimate the percentiles for.
plo, phi : float
The percentiles.
Returns
-------
lo, hi : float
The percentile values.
"""
shp = data.shape
view = tuple([slice(None, None, np.intp(max(s / 50, 1))) for s in shp])
values = np.asarray(data)[view]
if ~np.isfinite(values).any():
return (0.0, 1.0)
limits = (-np.inf, np.inf)
lo = _scoreatpercentile(values.flat, plo, limit=limits)
hi = _scoreatpercentile(values.flat, phi, limit=limits)
return lo, hi
# We don't know in advance what backends are going to be used for Matplotlib
# so we can set up a list here and use it in defer_draw below, and each front-
# end is responsible for adding their backend here.
DEFER_DRAW_BACKENDS = []
[docs]def defer_draw(func):
"""
Decorator that globally defers all Agg canvas draws until
function exit.
If a Canvas instance's draw method is invoked multiple times,
it will only be called once after the wrapped function returns.
"""
@wraps(func)
def wrapper(*args, **kwargs):
if len(DEFER_DRAW_BACKENDS) == 0:
return func(*args, **kwargs)
# Don't recursively defer draws. We just check the first draw_idle
# method since all should be modified in sync.
if isinstance(DEFER_DRAW_BACKENDS[0].draw_idle, DeferredMethod):
return func(*args, **kwargs)
try:
for backend in DEFER_DRAW_BACKENDS:
backend.draw_idle = DeferredMethod(backend.draw_idle)
result = func(*args, **kwargs)
finally:
for backend in DEFER_DRAW_BACKENDS:
# We need to use another try...finally block here in case the
# executed deferred draw calls fail for any reason
try:
try:
backend.draw_idle.execute_deferred_calls()
except RuntimeError: # For C/C++ errors with Qt
pass
finally:
backend.draw_idle = backend.draw_idle.original_method
return result
wrapper._is_deferred = True
return wrapper
[docs]def color2rgb(color):
from matplotlib.colors import ColorConverter
result = ColorConverter().to_rgb(color)
return result
[docs]def color2hex(color):
try:
from matplotlib.colors import to_hex
result = to_hex(color)
except ImportError: # MPL 1.5
from matplotlib.colors import ColorConverter, rgb2hex
result = rgb2hex(ColorConverter().to_rgb(color))
return result
[docs]def point_contour(x, y, data):
"""Calculate the contour that passes through (x,y) in data
Parameters
----------
data : `~numpy.ndarray`
A 2D image.
x : int
Index of `x` location.
y : int
Index of `y` location.
Returns
-------
contour : `~numpy.ndarray`
A shape ``(N, 2)`` numpy array giving the `x` and `y` locations
of the `N` contour vertices.
"""
try:
from scipy.ndimage import label, binary_fill_holes
from skimage.measure import find_contours
except ImportError:
raise ImportError("Image processing in Glue requires SciPy and scikit-image")
# Find the intensity of the selected pixel
inten = data[y, x]
# Find all 'islands' above this intensity
labeled, nr_objects = label(data >= inten)
# Pick the object we clicked on
z = (labeled == labeled[y, x])
# Fill holes inside it so we don't get 'inner' contours
z = binary_fill_holes(z).astype(float)
# Pad the resulting array so that for contours that go to the edge we get
# one continuous contour
z = np.pad(z, 1, mode='constant')
# Finally find the contours around the island
xy = find_contours(z, 0.5, fully_connected='high')
if not xy:
return None
if len(xy) > 1:
warnings.warn("Too many contours found, picking the first one")
# We need to flip the array to get (x, y), and subtract one to account for
# the padding
return xy[0][:, ::-1] - 1
class AxesResizer(object):
def __init__(self, ax, margins):
self.ax = ax
self.margins = margins
@property
def margins(self):
return self._margins
@margins.setter
def margins(self, margins):
self._margins = margins
def on_resize(self, event):
fig_width = self.ax.figure.get_figwidth()
fig_height = self.ax.figure.get_figheight()
x0 = self.margins[0] / fig_width
x1 = 1 - self.margins[1] / fig_width
y0 = self.margins[2] / fig_height
y1 = 1 - self.margins[3] / fig_height
dx = max(0.01, x1 - x0)
dy = max(0.01, y1 - y0)
self.ax.set_position([x0, y0, dx, dy])
self.ax.figure.canvas.draw_idle()
def freeze_margins(axes, margins=[1, 1, 1, 1]):
"""
Make sure margins of axes stay fixed.
Parameters
----------
ax_class : :class:`matplotlib.axes.Axes`
The axes class for which to fix the margins
margins : iterable
The margins, in inches. The order of the margins is
``[left, right, bottom, top]``
Notes
-----
The object that controls the resizing is stored as the resizer attribute of
the Axes. This can be used to then change the margins:
>> ax.resizer.margins = [0.5, 0.5, 0.5, 0.5]
"""
axes.resizer = AxesResizer(axes, margins)
axes.figure.canvas.mpl_connect('resize_event', axes.resizer.on_resize)
class ColormapPatchHandler(HandlerBase):
def __init__(self, cmap, nb_subpatch=10, xpad=0.0, ypad=0.0):
"""
A custom legend handler to represent 2D dataset coded in colormaps
Parameters
----------
cmap : :class:`matplotlib.colors.colormap`
The matplotlib colormap to use
nb_subpatch : int, optional
The number of stripes to use to represent the colormap.
The default is 10.
xpad : float, optional
Padding in the x direction. The default is 0.0.
ypad : float, optional
Padding in the y direction. The default is 0.0.
"""
super().__init__(xpad, ypad)
self.nb_subpatch = nb_subpatch
self.cmap = cmap
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height, fontsize,
trans):
collection = []
for i in range(self.nb_subpatch):
width_sub = width / self.nb_subpatch
x = xdescent + i * width_sub
collection.append(
Rectangle((x, ydescent), width_sub, height, transform=trans,
facecolor=self.cmap(i / (self.nb_subpatch - 1)),
edgecolor="none"))
return collection
# In Matplotlib < 2.2, there is no datetime64 support, so we register a converter
# here to deal with it with older versions.
class Datetime64Converter(units.ConversionInterface):
@staticmethod
def convert(value, unit, axis):
value = np.asarray(value)
if value.dtype.kind == 'M':
return datetime64_to_mpl(value)
else:
return value
@staticmethod
def axisinfo(unit, axis):
majloc = dates.AutoDateLocator()
majfmt = dates.AutoDateFormatter(majloc)
return units.AxisInfo(majloc=majloc,
majfmt=majfmt)
@staticmethod
def default_units(x, axis):
return None
units.registry[np.datetime64] = Datetime64Converter()
# The following code is copied from the developer version of Matplotlib
# for compatibility with older versions. The following is the license
# agreement for Matplotlib:
#
# 1. This LICENSE AGREEMENT is between the Matplotlib Development Team
# ("MDT"), and the Individual or Organization ("Licensee") accessing and
# otherwise using matplotlib software in source or binary form and its
# associated documentation.
#
# 2. Subject to the terms and conditions of this License Agreement, MDT
# hereby grants Licensee a nonexclusive, royalty-free, world-wide license
# to reproduce, analyze, test, perform and/or display publicly, prepare
# derivative works, distribute, and otherwise use matplotlib
# alone or in any derivative version, provided, however, that MDT's
# License Agreement and MDT's notice of copyright, i.e., "Copyright (c)
# 2012- Matplotlib Development Team; All Rights Reserved" are retained in
# matplotlib alone or in any derivative version prepared by
# Licensee.
#
# 3. In the event Licensee prepares a derivative work that is based on or
# incorporates matplotlib or any part thereof, and wants to
# make the derivative work available to others as provided herein, then
# Licensee hereby agrees to include in any such work a brief summary of
# the changes made to matplotlib .
#
# 4. MDT is making matplotlib available to Licensee on an "AS
# IS" basis. MDT MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, MDT MAKES NO AND
# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB
# WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
#
# 5. MDT SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB
# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR
# LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING
# MATPLOTLIB , OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF
# THE POSSIBILITY THEREOF.
#
# 6. This License Agreement will automatically terminate upon a material
# breach of its terms and conditions.
#
# 7. Nothing in this License Agreement shall be deemed to create any
# relationship of agency, partnership, or joint venture between MDT and
# Licensee. This License Agreement does not grant permission to use MDT
# trademarks or trade name in a trademark sense to endorse or promote
# products or services of Licensee, or any third party.
#
# 8. By copying, installing or otherwise using matplotlib ,
# Licensee agrees to be bound by the terms and conditions of this License
# Agreement.
HOURS_PER_DAY = 24.
MIN_PER_HOUR = 60.
SEC_PER_MIN = 60.
SEC_PER_HOUR = SEC_PER_MIN * MIN_PER_HOUR
SEC_PER_DAY = SEC_PER_HOUR * HOURS_PER_DAY
T0 = np.datetime64('0001-01-01T00:00:00').astype('datetime64[s]')
[docs]def datetime64_to_mpl(d):
"""
Convert `numpy.datetime64` or an ndarray of those types to Gregorian
date as UTC float. The precision is limited to float64 precision. Practically:
microseconds for dates between 290301 BC, 294241 AD, milliseconds for
larger dates (see `numpy.datetime64`). Nanoseconds aren't possible
because we do times compared to ``0001-01-01T00:00:00`` (plus one day).
"""
# the "extra" ensures that we at least allow the dynamic range out to
# seconds. That should get out to +/-2e11 years.
extra = d - d.astype('datetime64[s]')
extra = extra.astype('timedelta64[ns]')
dt = (d.astype('datetime64[s]') - T0).astype(np.float64)
dt += extra.astype(np.float64) / 1.0e9
dt = dt / SEC_PER_DAY + 1.0
return dt
[docs]def mpl_to_datetime64(dt):
dt = np.asarray(dt, np.float64)
dt = (dt - 1.0) * SEC_PER_DAY
dt_s = dt.astype(np.int64) + T0.astype(np.int64)
dt_ns = ((dt % 1) * 1e9).astype(np.int64)
dt_s = np.array(dt_s, dtype='datetime64[s]')
dt_ns = np.array(dt_ns, dtype='timedelta64[ns]')
return dt_s + dt_ns