# -*- coding: utf8 -*-
"""Classes for all plots"""
# Copyright or © or Copr. Actimar/IFREMER (2012-2018)
#
# This software is a computer program whose purpose is to provide
# utilities for handling oceanographic and atmospheric data,
# with the ultimate goal of validating the MARS model from IFREMER.
#
# This software is a computer program whose purpose is to [describe
# functionalities and technical features of your software].
#
# This software is governed by the CeCILL license under French law and
# abiding by the rules of distribution of free software. You can use,
# modify and/ or redistribute the software under the terms of the CeCILL
# license as circulated by CEA, CNRS and INRIA at the following URL
# "http://www.cecill.info".
#
# As a counterpart to the access to the source code and rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty and the software's author, the holder of the
# economic rights, and the successive licensors have only limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading, using, modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean that it is complicated to manipulate, and that also
# therefore means that it is reserved for developers and experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and, more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.
import os
import re
from operator import isNumberType
from warnings import warn
import matplotlib.dates
import matplotlib.pyplot as P
import matplotlib.transforms as mtransforms
import numpy as N, MV2, cdms2
from matplotlib.artist import Artist
from matplotlib.axes import Subplot, Axes
from matplotlib.axis import YAxis
from matplotlib.colors import Colormap, Normalize
from matplotlib.dates import (DateFormatter, MonthLocator, WeekdayLocator,
YearLocator,DayLocator, HourLocator, MinuteLocator, SecondLocator,
MONDAY, WEEKLY, YEARLY, MONTHLY,
AutoDateLocator, AutoDateFormatter, MO, DAILY, HOURLY, num2date,
MINUTELY, SECONDLY)
from matplotlib.figure import Figure
from matplotlib.patches import Patch
from matplotlib.path import Path
from matplotlib.patheffects import Normal
from matplotlib.text import Text
from matplotlib.ticker import (FormatStrFormatter, Formatter, Locator,
NullLocator, AutoMinorLocator, AutoLocator)
from matplotlib.transforms import offset_copy
from mpl_toolkits.basemap import Basemap, _setlatlab, _setlonlab
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.collections import PolyCollection, LineCollection
from ._ext_plot import (DropShadowFilter, FilteredArtistList, GrowFilter,
LightFilter)
from .misc import (kwfilter, dict_aliases, geo_scale, lonlab, latlab, deplab, cp_atts,
auto_scale, dict_check_defaults, basic_auto_scale, dict_copy_items,
dict_merge, phaselab, set_atts)
from .atime import mpl, strftime, is_numtime, numtime
from .axes import (check_axes, axis_type, set_order, merge_orders,
check_order, order_match, isaxis, get_axis_type)
from .color import (get_cmap, RGB, land,
RGBA, change_luminosity, change_saturation, pastelise,
CMAP_POSITIVE, CMAP_NEGATIVE, CMAP_SYMETRIC)
from .docstrings import docfiller
from .filters import generic2d
from .grid import get_axis, meshbounds, meshgrid
from .grid.masking import resol_mask
from .grid.regridding import shift1d
from .phys.units import deg2m, tometric, m2deg
from .remote import OutputWorkFile
from ..config import get_config_value
import color
MA = N.ma
__all__ = ['PlotError','Plot', 'Plot1D', 'Curve', 'Bar', 'Stick',
'Plot2D', 'Map', 'Hov', 'QuiverKey',
'ScalarMappable','AutoDateFormatter2', 'AutoDateLocator2',
'AutoDateMinorLocator', 'AutoDualDateFormatter', 'DualDateFormatter',
'MinuteLabel', 'Section', 'twinxy', 'DepthFormatter',
'AutoDegreesMinutesFormatter', 'AutoDegreesMinutesLocator']
#: Aliases for argisimage services hosted by the arcgis server
ARCGISIMAGE_ALIASES = dict(
esriimagery="ESRI_Imagery_World_2D",
esristreet="ESRI_StreetMap_World_2D",
esristreetmap="ESRI_StreetMap_World_2D",
natgeo="NatGeo_World_Map",
ngstopo="NGS_Topo_US_2D",
usatopo="USA_Topo_Maps",
ocean="Ocean_Basemap",
topo="World_Topo_Map",
shaded="World_Shaded_Relief",
physical="World_Physical_Map",
imagery="World_Imagery",
street="World_Street_Map",
streetmap="World_Street_Map",
terrain="World_Terrain_Base",
)
[docs]class PlotError(Exception):
pass
[docs]class Plot(object):
"""Base class for all plots
:Generic params:
- **load_data**, optional: Load data.
- **pre_plot**, optional: Initialize the plot (preprocessing).
- **plot**, optional: Plot data.
- **post_plot**, optional: Finalize plot.
- **Data loading**: See :meth:`load_data`, :meth:`_set_axes_`, :meth:`_check_order_` .
- **Plot initialisation**: see :meth:`pre_plot`.
- **Plot**: see :meth:`plot`
- **Plot finalization**: see :meth:`post_plot`
:Attribute params:
- **long_name**, optional: Force the :attr:`~vacumm.misc.core_plot.Plot.long_name`
attribute used in :attr:`~vacumm.misc.core_plot.Plot.title`.
See also param ``x/ylong_name``.
- **x/ylong_name**, optional: Same as :attr:`~vacumm.misc.core_plot.Plot.long_name`
but for X and Y axes (:attr:`~vacumm.misc.core_plot.Plot.xlong_name`
or :attr:`~vacumm.misc.core_plot.Plot.ylong_name`). It refers only to
axes for 2D plots, and potentially to data for 1D plots.
- **units**, optional: Force the :attr:`~vacumm.misc.core_plot.Plot.units`
attribute used in labels (:attr:`~vacumm.misc.core_plot.Plot.label`,
:attr:`~vacumm.misc.core_plot.Plot.xlabel` or
:attr:`~vacumm.misc.core_plot.Plot.ylabel`).
- **latex_units**, optional: Interpret units with latex after defining
the :attr:`~vacumm.misc.core_plot.Plot.latex_units` attribute.
Alternatively, you can simply specify units enclosed with ``"$"``.
- **x/yunits**, optional: Same as :attr:`~vacumm.misc.core_plot.Plot.units`
but for X and Y axes (:attr:`~vacumm.misc.core_plot.Plot.xunits`
or :attr:`~vacumm.misc.core_plot.Plot.yunits`). It refers only to
axes for 2D plots, and potentially to data for 1D plots.
See also param ``x/yunits``.
- **x/ymin/max**, optional: Force min and max along X and Y by setting
attributes :attr:`~vacumm.misc.core_plot.Plot.xmin`,
:attr:`~vacumm.misc.core_plot.Plot.xmax`,
:attr:`~vacumm.misc.core_plot.Plot.ymin` or
:attr:`~vacumm.misc.core_plot.Plot.ymax`.
- **vmin/max**, optional: Force min and max value for data by setting
attributes :attr:`~vacumm.misc.core_plot.Plot.vmin`,
:attr:`~vacumm.misc.core_plot.Plot.vmax`.
This may be equivalent to
to set X or Y extrema for 1D plots.
- **x/ylabel**, optional: Force label used for X and Y axes
by setting attributes :attr:`~vacumm.misc.core_plot.Plot.xlabel` or
:attr:`~vacumm.misc.core_plot.Plot.ylabel`.
- **title**, optional: Force the title of the plot by setting
:attr:`~vacumm.misc.core_plot.Plot.title`
- **x/ymasked**, optional: Force the plot to fit all data
positions along X and/or Y,
even if data is missing, by setting attributes
:attr:`~vacumm.misc.core_plot.Plot.xmasked`,
:attr:`~vacumm.misc.core_plot.Plot.ymasked`
or :attr:`~vacumm.misc.core_plot.Plot.xymasked`.
- **anim**, optional: Create an animation for the current figure.
Example:
>>> myplot = Plot(data, xmin=3.5, title='My plot')
>>> print myplot.xmin, myplot.title
3.5 Myplot
The followwing rules apply:
- :attr:`title` defaults to :attr:`long_name`,
which defaults to the ``long_name`` attribute of input data.
You can use templates with all other attributes as replacement
keys (example ``"%(long_name)s (max=%(vmax)g)"``) but
:attr:`xlabel` and :attr`ylabel`.
- :attr:`units` defaults to the ``units`` attribute of input data.
- :attr:`xlong_name` defaults to :attr:`long_name` if X
axis refers to data, otherwize to the ``long_name`` attribute
of input second axis.
- :attr:`ylong_name` defaults to :attr:`long_name` if Y
axis refers to data, otherwize to the ``long_name`` attribute
of input first axis.
- :attr:`xunits` defaults to :attr:`units` if X
axis refers to data, otherwize to the ``units`` attribute
of input second axis.
- :attr:`yunits` defaults to :attr:`units` if Y
axis refers to data, otherwize to the ``units`` attribute
of input first axis.
- :attr:`xlabel` is empty if X axis refers to a spatial or
temporal axis, else it defaults ``"%(xlong_name)s [%(xunits)s]"``.
You can use templates with all other attributes as replacement
keys (example ``"%(long_name)s (max=%(vmax)g)"``) but
:attr:`title` and :attr`ylabel`.
- :attr:`ylabel` is empty if Y axis refers to a spatial or
temporal axis, else it defaults ``"%(ylong_name)s [%(yunits)s]"``.
You can use templates with all other attributes as replacement
keys (example ``"%(long_name)s (max=%(vmax)g)"``) but
:attr:`title` and :attr`xlabel`.
- :attr:`vmin`/:attr:`vmax` defaults to the min/max of all plotted data.
- :attr:`xmin`/:attr:`xmax` defaults to :attr:`vmin`/:attr:`vmin`
if X axis refers to data, else to the min/max of
the second axis of input data.
- :attr:`ymin`/:attr:`ymax` defaults to :attr:`vmin`/:attr:`vmin`
if Y axis refers to data, else to the min/max of
the first axis of input data.
- :attr:`uvlat` and :attr:`uvscaler` are used to convert vectors
(like speed) used by quiver plots.
.. warning::
These attributes may be used for the plotting process but may not be
equal to their graphical counterpart. For instance, :attr:`xmin`
may not be equal to ``matplotlib.pyplot.xlim()[0]``.
:Generic tasks:
#. Call to :meth:`load_attributes`.
#. Call to :meth:`load_data` if ``load_data is True``.
#. Call to :meth:`pre_plot` if ``pre_plot is True``.
#. Call to :meth:`register`.
#. Call to :meth:`load_attributes` for remaining attributes.
#. Call to :meth:`plot` if ``plot is True``.
#. Call to :meth:`post_plot` if ``plot is True and post_plot is True``.
"""
rank = None
_order = None
_primary_attributes = ['long_name', 'xlong_name', 'ylong_name',
'units', 'xunits', 'yunits', 'vmin', 'xmin', 'ymin', 'vmax',
'xmax', 'ymax']
_secondary_attributes = ['xlabel', 'ylabel', 'title']
_special_attributes = ['uvlat', 'uvscaler', 'anim', 'xymasked',
'xmasked', 'ymasked']
_plot_status_none = 0
_plot_status_plot = 1
_plot_status_post = 2
masked = True
def __init__(self, data, load_data=True, pre_plot=True, plot=False, post_plot=False,
**kwargs):
# First inits
self._gobjs = {}
self.data = self.axes = None
# self._kwargs = kwargs
self._post_plotted = False
self._finalize = []
# Load data
if load_data :
self.load_data(data, **kwargs)
# Load attributes: for current instance
self.load_attributes(kwargs)
# Initialize plot
self.pre_plot(**kwargs)
# Register
self.register()
# Load attributes: stored in axes instance
self.load_attributes(kwargs)
# Plot
if plot:
self.plot(**kwargs)
# Post plot
if post_plot:
self.post_plot(**kwargs)
[docs] def plot_status(self):
"""Guess if current instance has a plot"""
if self.get_obj('plots') is None:
return self._plot_status_none
if self._post_plotted:
return self._plot_status_post
return self._plot_status_plot
# def update(self, status=None, glob=True):
# """Update what is needed of the plot"""
# # Guess the status
# if status is None: status = self.plot_status()
# if not status: return
#
# # Update
# if glob: # Global update (all plotters)
#
# for plotter in self.get_brothers():
# plotter.update(status=status, glob=False)
#
# else: # Update this plotter only
#
# if status>=self._plot_status_plot: # Re-plot
# self.plot(*self._pargs, **self._kwargs)
#
# if status==self._plot_status_post: # Re-post_plot
# self.post_plot(**self._kwargs)
[docs] def is_plotted(self):
return self.plot_status()>=self._plot_status_plot
[docs] def is_post_plotted(self):
return self.plot_status()>=self._plot_status_post
[docs] def load_attributes(self, items, select=None):
"""Load (selected) attributes from ``items``
Attributes not found are set to ``None``.
:Example:
>>> items = dict(xmin=4., title='%(long_name)s')
>>> myplot.load_attributes('xmin', 'units', units='degC', **items)
"""
# User attributes
for att in self._primary_attributes+self._secondary_attributes+self._special_attributes:
if select and att not in select: continue
value = items.get(att, None)
if value is not None:
setattr(self, att, value)
if self.isset(att): del items[att] # clean out
[docs] def register(self):
"""Register current instance into self.fig and self.axes"""
if self.axes is None:
raise PlotError('Cannot register plot instance since no MPL axes available')
if self.get_axobj('plotters') is None or self not in self.get_axobj('plotters'):
self.add_axobj('plotters', self)
self.fig._vacumm = self.get_axobj()
[docs] def load_data(self, data, **kwargs):
"""Load data and format data, and check rank.
It finally calls :meth:`_check_order_`.
:Params:
- **data**: A single :mod:`cdms2` variable or a tuple of them,
in the forms ``(m,)``, or ``(u,v)``, ``(m,u,v)``, where:
- ``u``: X component of a vector.
- ``v``: Y component of a vector.
- ``m``: A scalar variable. If ``u`` and ``v`` are set,
it defaults to their modulus.
- **order**, optional: See :meth:`_check_order_`
- **transpose**, optional: See :meth:`_check_order_`
- Keywords are passed to :meth:`_set_axes_` for
axis subtitutions.
:Attributes:
The following attributes are defined:
.. attribute:: data
A 1- to 3-element tuple of :class:`MV2.array`
in a form of similar to **data** above.
.. attribute:: x
The last axis of the first element of data,
or ``None`` if X axis refer to data.
.. attribute:: y
The first axis of the first element of data,
or ``None`` if Y axis refer to data.
.. attribute:: mask
The data mask.
:See also:
:meth:`_check_order_` :meth`_set_axes_`
"""
self.raw_data = data
self.data = None
# if data is None:
# self._check_order_()
# return
# Arrows
if data is not None:
N.seterr(over='ignore')
if isinstance(data, list):
data = tuple(data)
if not isinstance(data, tuple):
self.data = [MV2.array(data, copy=1),]
elif len(data) in [1,3]: # (mod,) or (mod,u,v)
self.data = [MV2.array(v, copy=1) for v in data]
elif len(data)==2:
# Get vectors and modulus (u,v) -> (mod,u,v)
vx,vy = [MV2.array(v, copy=1) for v in data]
self.data = [MV2.sqrt(vx**2+vy**2),vx,vy]
ln = []
for vv in vx,vy:
if hasattr(vv,'units'):
self.data[0].units = vv.units
break
if hasattr(vv,'long_name'):
ln.append(vv.long_name)
ln = ' and '.join(ln)
if len(ln) == 0:
ln = 'Modulus'
else:
ln = 'Modulus of '+ln
self.data[0].long_name = ln
self.data[0].setAxisList(self.data[2].getAxisList())
self.data[0].setGrid(self.data[2].getGrid())
self.data[0].id = 'modulus'
else:
raise PlotError('You must provide no more than 3 arrays as input for '+self.__class__.__name__)
# Check all variables and axes
units = [None]*self.rank
for ivar,var in enumerate(self.data):
# Check rank
if var.rank() != self.rank:
raise PlotError('Your variable must have a rank = %i (current rank is %i)' % (self.rank, var.rank()))
# Guess axis types
if '-' in var.getOrder():
check_axes(var)
# Make time axes comptatible with matplotlib
mpl(var, copy=True)
# Store axes in x and y
self._set_axes_(**kwargs)
# Homogeneous axis units and long_names
self._check_axis_atts_()
# Check order
self._check_order_(**kwargs)
# Store mask
#self.x = get_axis(self.data[0], -1) if self.order[1]!='d' else None
#self.y = get_axis(self.data[0], 0) if self.order[0]!='d' else None
#self.x = self.data[0].getAxis(-1) if self.order[1]!='d' else None
#self.y = self.data[0].getAxis(0) if self.order[0]!='d' else None
self.mask = (N.ma.getmaskarray(self.data[0]) if self.has_data() else
N.ma.nomask)
self.masked = self.mask.all()
if self.masked and self.has_data():
warn('All your data are masked')
return self.data
def _set_axes_(self, **kwargs):
pass
def _check_axis_atts_(self):
"""Check that X/Y units and long_name can be used if possible
If current axes (self.x and self.y) have no units or long_name,
it tries to get them from variables (self.data).
:attr`x` and :attr`y` must have been set _set_axes_ method.
"""
if not self.has_data():
return
for i, xy in enumerate(['y','x']):
axis = getattr(self, xy)
if axis is None: continue
for att in 'units', 'long_name':
if hasattr(axis, att): continue
for dat in self.data:
if self.rank==1:
ax = dat.getAxis(0)
else:
ax = get_axis(dat, i, strict=False)#, geo=False)
if hasattr(ax, att):
setattr(axis, att, getattr(ax, att))
break
def _check_order_(self, order=None, transpose=False, vertical=None, **kwargs):
"""Check the order of axes
:Params:
- **order**, optional: A string of length 2 that specify the physical type
of plot axes. The first char refers to the Y axis, and the second
to the X axis. It must contain one of the following characters:
- ``x``: longitude
- ``y``: latitude
- ``z``: level
- ``t``: time
- ``-``: any of the latters
- ``d``: data
If char is upper-cased, its type is mandatory and must be found
in input :mod:`cdms2` variables.
It defines the :attr:`~vacumm.misc.core_plot.Plot.order` attribute.
- **vertical**: Force data to be plotted along the vertical axis.
- **transpose**: Transpose the plot axes
(:attr:`~vacumm.misc.core_plot.Plot.order`).
:Attributes:
This methods defines the following attributes:
.. attribute:: order
Two-character string define the order of axis types.
It is the sum of the :attr:`xtype` and :attr:`ytype`.
Examples: ``"xt"``, ``z-``.
.. attribute:: xtype
Type of the X axis (see above).
.. attribute:: ytype
Type of the Y axis (see above).
"""
if self._order is None:
raise NotImplementedError
if not self.has_data():
if not isinstance(self._order, list):
self.order = self._order.lower()
else:
self.order = ('d' if self.y is None else
get_axis_type(self.y, checkaxis=False))
self.order = self.order + ('d' if self.x is None else
get_axis_type(self.x, checkaxis=False))
else:
# Allowed orders
if not isinstance(self._order, list): self._order = [self._order]
withd = 'd' in self._order[0]
if order is not None:
if not isinstance(order, list): order = [order]
for oo in order:
if len(oo) != 2 or (withd and not 'd' in oo):
raise PlotError('You order must be a 2-element string with "d" inside')
self._order = order
# Loop on variables
data_order = None
for i, var in enumerate(self.data):
self.data[i], this_order, _reordered = check_order(var,
self._order, reorder=True,
vertical=vertical, extended=True, getorder=True)
if data_order is not None:
assert order_match(data_order, this_order), \
"Axis order incompatible: %s and %s"%(last_order, this_order)
data_order, _ = merge_orders(data_order, this_order)
else:
data_order = this_order
if i==0:
reordered = _reordered
self.order = data_order
# Transpose axes when reordered
if reordered or (self.rank==1 and data_order[1]=='d'):
self._transpose_axes_()
# Transpose?
if transpose: self._transpose_()
self.ytype, self.xtype = self.order
return self.order
def _transpose_(self):
"""Transpose data and axes"""
# Variables
if self.rank==2:
for ivar, var in enumerate(self.data):
self.data[ivar] = MV2.transpose(var)
del var
# Axes
self._transpose_axes_()
# Order
self.order = self.order[::-1]
def _transpose_axes_(self):
for xy in 'x','y':
axis = getattr(self, xy)
if axis is None: continue
if len(axis.shape)==2:
axis = MV2.transpose(axis)
setattr(self, xy, axis)
self.x, self.y = self.y, self.x
[docs] def get(self, key, **kwargs):
return getattr(self, 'get_'+key)(**kwargs)
[docs] def get_xmask(self):
"""Get the data mask projected along X"""
if self.mask is None: return
if self.xtype == 'd': return self.mask
xdata = self.get_xdata(masked=False)
if xdata.ndim==self.mask.ndim==1:
return self.mask
return self.mask.min(axis=0)
[docs] def get_ymask(self):
"""Get the data mask projected along Y"""
if self.mask is None: return
if self.ytype == 'd': return self.mask
ydata = self.get_ydata(masked=False)
if ydata.ndim==self.mask.ndim==1:
return self.mask
return self.mask.min(axis=-1)
[docs] def get_data(self, scalar=False):
"""Get data as a tuple of :class:`~numpy.ma.core.MaskedArray`
:See also: :meth:`get_xdata` :meth:`get_ydata` :attr:`uvscaler`
"""
data = [var.asma() for var in self.data]
if len(data)==3:
uvscaler = self.uvscaler
if uvscaler is not None:
data = [data[0]]+list(uvscaler(data[1], data[2]))
if scalar=='uv':
data = data[1:]
if scalar is not False:
if scalar is True:
scalar = 0
if isinstance(scalar, int):
return data[scalar]
return tuple(data)
[docs] def get_xdata(self, scalar=True, masked=False, bounds=False):
"""Get the numerical data associated with the X axis
.. note::
It can come from a physical axis or data
depending on the axis type :attr:`xtype`.
:Params:
- **scalar**, optional: Set it to ``True`` to get data
as a scalar array in case X axis refers to a tuple of data.
If set to an int, it takes the element #scalar of this tuple.
- **masked**, optional: If it is an axis (not data), values are
masked with data mask.
- **bounds**, optional: The data bounds (valid only of X
is an axis).
:See also: :meth:`get_ydata` :meth:`get_data`
"""
# # Nothing
# if not self.has_data(): return
# Axis
if self.x is not None:
x = self.x[:] #.getValue()
if cdms2.isVariable(x): x = x.asma()
if N.ma.isMA(x): x = x.filled(x.max())
if masked is None:
masked = self.xmasked
if masked:
mask = self.get_xmask()
if mask.shape!=x.shape:
mask = N.resize(mask, x.shape)
x = N.ma.masked_array(x, mask=mask)
if bounds:
if not hasattr(self, '_xb'):
self._xb = meshcells(x)
return self._xb
return x
# Data to plot
if scalar is True:
scalar = max(0, len(self.data)-2)
return self.get_data(scalar)
[docs] def get_ydata(self, scalar=True, masked=False, bounds=False):
"""Get the numerical data associated with the Y axis
.. note::
It can come from a physical axis or data
depending on the axis type :attr:`ytype`.
:Params:
- **scalar**, optional: Set it to ``True`` to get data
as a scalar array in case Y axis refers to a tuple of data.
If set to an int, it takes the element #scalar of this tuple.
- **masked**, optional: If it is an axis (not data), values are
masked with data mask.
- **bounds**, optional: The data bounds (valid only of Y
is an axis).
:See also: :meth:`get_xdata` :meth:`get_data`
"""
# # Nothing
# if not self.has_data(): return
# Axis
if self.y is not None:
y = self.y[:] #.getValue()
if cdms2.isVariable(y): y = y.asma()
if N.ma.isMA(y): y = y.filled(y.max())
if masked is None:
masked = self.ymasked
if masked:
mask = self.get_ymask()
if mask.shape!=y.shape:
mask = N.resize(mask, y.shape[::-1]).T
y = N.ma.masked_array(y, mask=mask)
if bounds:
if not hasattr(self, '_yb'):
self._yb = meshcells(y)
return self._yb
return y
# Data to plot
if scalar is True:
scalar = max(0, len(self.data)-1)
return self.get_data(scalar)
[docs] def pre_plot(self, axes=None, figure=None, figsize=None, subplot=None, twin=None,
subplots_adjust=None, bgcolor=None, noframe=False, fullscreen=False,
verbose=False, axes_host=False, axes_xoffset=0, elev=None, azim=None,
**kwargs):
"""Initialize the plot
:Tasks:
#. Filter keyword parameters.
#. Create the :class:`~matplotlib.fig.Figure` instance and
store it into :attr:`fig`.
#. Create the :class:`~matplotlib.axes.Axes` instance and
store it into :attr:`axes`
:Params:
- **fig**, optional: Figure number.
- **figsize**, optional: Initialize the figure with this size.
- **axes**, optional: Use this axes object.
- **subplot**, optional: Call to :func:`~matplotlib.pyplot.subplot` to create axes.
- **subplots_adjust**, optional: Dictionary sent to :func:`~matplotlib.pyplot.subplots_adjust`.
You can also use keyparams 'left', 'right', 'top', 'bottom', 'wspace', 'hspace'!
- **top/bottom/left/right/wspace/hspace**, optional: Override ``subplots_adjust``.
- **sa**, optional: Alias for subplots_adjust.
- **twin**, optional: Use ``"x"`` or ``"y"`` or ``"xy"`` to make a copy of current
X or Y axes (see :func:`matplotlib.pyplot.twinx`).
You can also provide a dictionary : ``twin=dict(x=axes1, y=axes2)``.
- **bgcolor**, optional: Background axis color.
- **axes_rect**, optional: [left, bottom, width, height]
in normalized (0,1) units to create axes using :func:`~matplotlib.pyplot.axes`.
- **axes_<param>**, optional: <param> is passed to :func:`~matplotlib.pyplot.axes`.
- **noframe**, optional: Suppress plot frames.
- **fullscreen**, optional: Plot in full screen mode (thus, ``noframe==True``).
- **verbose**, optional: Informs about errors with axes.
:Attributes:
.. attribute:: fig
:class:`~matplotlib.fig.Figure` on which plots are drawn.
.. attribute:: axes
:class:`~matplotlib.axes.Axes` instance of the current plot.
"""
# Keywords management
figure = kwargs.pop('fig', figure)
kwfig = kwfilter(kwargs,'figure')
kwaxes = kwfilter(kwargs,'axes')
subplots_adjust = kwargs.pop('sa', subplots_adjust)
for adj in 'left', 'right', 'top', 'bottom', 'wspace', 'hspace':
if kwargs.has_key(adj):
if subplots_adjust is None: subplots_adjust = {}
subplots_adjust[adj] = kwargs.pop(adj)
if fullscreen:
noframe = True
subplot = None
subplots_adjust = None
default_axes_rect = [0,0,1,1]
else:
default_axes_rect = None
axes_rect = kwaxes.pop('rect', default_axes_rect)
if axes_rect is not None:
axes_rect = [axes_rect]
else:
axes_rect = []
if noframe:
kwargs['figure_frameon'] = False
kwargs['axes_frameon'] = False
# Figure
if figure == 'old':
figure = None
if isinstance(figure, Figure):
self.fig = figure
elif figure is True or figure == 'new':
self.fig = P.figure(**kwfig)
elif figure is not None:
self.fig = P.figure(figure, **kwfig)
else:
self.fig = P.gcf()
if figsize is not None:
if not isinstance(figsize, tuple):
figsize = (figsize, figsize)
else:
figsize = tuple(figsize)
self.fig.set_size_inches(figsize, forward=True)
if noframe:
self.fig.set_frameon(False)
if subplots_adjust is not None:
self.fig.subplots_adjust(**subplots_adjust)
# Axes
if (axes_host and axes is None and subplot is None and not axes_rect
and twin is None):
subplot = 111
if axes=="3d":
kwaxes['projection'] = "3d"
if subplot is None and not axes_rect:
subplot = 111
axes = None
if axes is not None:
self.axes = axes
if self.axes.get_figure() != self.fig:
if verbose: print 'Axes does not match figure'
self.fig = self.axes.get_figure()
elif subplot is not None:
if axes_host:
# from mpl_toolkits.axes_grid1 import host_subplot
from mpl_toolkits.axes_grid1.parasite_axes import host_subplot_class_factory
from mpl_toolkits.axisartist import Axes as AAxes
host_subplot_class = host_subplot_class_factory(AAxes)
if isinstance(subplot,(list,tuple)):
self.axes = host_subplot_class(self.fig, *subplot, **kwaxes)
else:
self.axes = host_subplot_class(self.fig, subplot, **kwaxes)
self.fig.add_subplot(self.axes)
else:
if isinstance(subplot,(list,tuple)):
self.axes = self.fig.add_subplot(*subplot,**kwaxes)
else:
self.axes = self.fig.add_subplot(subplot,**kwaxes)
elif axes_rect:
self.axes = self.fig.add_axes(*axes_rect,**kwaxes)
else:
if isinstance(twin, str):
self.axes = twinxy(twin, ax=self.axes)
else:
self.axes = P.gca()
try: # Fails with host
self.fig.sca(self.axes)
except:
pass
if axes_xoffset and hasattr(self.axes, 'get_grid_helper'):
new_fixed_axis = self.axes.get_grid_helper().new_fixed_axis
if axes_xoffset>0:
loc = 'right'
else:
loc = 'left'
offset = (axes_xoffset, 0)
self.axes.axis[loc] = new_fixed_axis(loc=loc, axes=self.axes, offset=offset)
self.axes.axis[loc].toggle(all=True)
self.is3d = isinstance(self.axes, Axes3D)
if self.is3d:
self.axes.view_init(elev=elev, azim=azim)
if noframe:
self.axes.set_frame_on(False)
if bgcolor is not None:
try:
self.axes.set_facecolor(bgcolor)
except ImportError:
self.axes.set_axis_bgcolor(bgcolor)
[docs] def plot(self, **kwargs):
"""The main plot"""
if self.plot_status()==2:
self.axes.cla()
[docs] def clear(self):
"""Clear axes from plotted objects"""
objs = self.get_obj('plotted')
if objs is None: return
for obj in objs:
self.remove(obj)
if self.axes is not None:
for container in self.axes.xaxis, self.axes.yaxis, self.axes:
if hasattr(container, '_vacumm'):
del container._vacumm
if hasattr(self, '_gobjs'):
del self._gobjs
[docs] def remove(self, objs):
"""Remove an graphical object from axes"""
if isinstance(objs, tuple):
objs = list(objs)
if not isinstance(objs, list):
objs = [objs]
for obj in objs:
for axobjs in self.axes.lines, self.axes.patches:
if obj in axobjs:
axobjs.remove(obj)
if self.axes is not None:
for container in self.axes.xaxis, self.axes.yaxis, self.axes:
if hasattr(container, '_vacumm'):
for key, val in container._vacumm.iteritems():
if val is obj:
del container._vacumm[key]
pp = self.get_obj('plotted')
if pp is not None and obj in pp:
pp.remove(obj)
[docs] def cla(self):
"""Clear axes of everything
See :func:`clear` and :func:`matplotlib.pyplot.cla`
"""
self.clear()
self.axes.cla()
[docs] def clf(self):
"""Clear figure of everything
See :func:`clear`, :func:`cla` and
:func:`matplotlib.pyplot.clf`
"""
self.cla()
self.fig.clf()
[docs] def close(self):
"""Close the current
See :func:`clear`, :func:`cla` and :func:`clf`
"""
self.clf()
try:
P.close(self.fig)
except:
pass
[docs] def get_brothers(self, notme=False, mefirst=True, filter=False):
"""Return all :class:`Plot` instances that belongs to current axes
:Params:
- **notme**, optional: Do not include current object in the list.
- **mefirst**, optional: Place me at the beginning of the list.
- **filter**, optional: If callable, use it to filter out brothers.
"""
brothers = self.get_axobj('plotters')
brothers = [] if brothers is None else list(brothers)
if self in brothers:
if notme:
brothers.remove(self)
elif mefirst:
brothers = [self]+brothers.pop(self)
if callable(filter):
brothers = [b for b in brothers if filter(b)]
return brothers
[docs] @classmethod
def get_current(cls, axes=None):
"""Retreive an instance of this class if found to be plotted in currents axes
:Params:
- **axes**, optional: Check this axes instance instead of the current one.
:Return: Last plotted instance, else ``None``
:Example:
>>> m = Map.get_current()
"""
if axes is None:
if P.get_fignums() and P.gcf().axes:
axes = P.gca()
else:
return
if not hasattr(axes, '_vacumm'): return
for p in axes._vacumm.get('plotters', [])[::-1]:
if isinstance(p, cls): return p
[docs] def add_obj(self, gtype, obj, single=False):
"""Add a graphic object to the bank of current instance
:Params:
- **gtype**: A list (or a single element) of string keys
to name the object.
- **obj**: The object it self (may be a list).
- **single**, optional: If ``True``, ``obj`` if store as is
(i.e is not appended to existing store objects having the same name).
:Return:
The object added.
:Example:
>>> text_object = myplot.add_obj(['plotted', 'text', myplot.axes.text(10, 20, 'text'))
>>> text_object = myplot.add_obj('colorbar', myplot.colorbar(), single=True)
:See also:
:meth:`set_obj` :meth:`get_obj`
"""
# Store the object
if not hasattr(self, '_gobjs'):
self._gobjs = {}
if not isinstance(gtype, list): gtype = [gtype]
for gt in gtype:
if single:
self._gobjs[gt] = obj
else:
if not self._gobjs.has_key(gt):
self._gobjs[gt] = []
if isinstance(obj, list):
self._gobjs[gt].extend(obj)
else:
self._gobjs[gt].append(obj)
return obj
[docs] def set_obj(self, gtype, obj):
"""Shortcut to :meth:`add_obj` called with ``single=True``"""
return self.add_obj(gtype, obj, single=True)
[docs] def get_obj(self, gtype):
"""Get a graphic object stored in the bank
:Example:
>>> myplot.get_obj('pcolor')[0].set_zorder(15)
>>> myplot.get_obj('key').set_color('red')
:Return:
The object or ``None`` if not found.
:See also:
:meth:`add_obj`
"""
if not hasattr(self, '_gobjs'):
self._gobjs = {}
# Dict key
if isinstance(gtype, str):
return self._gobjs.get(gtype, None)
# Check type
for oo in self._gobjs.values():
if isinstance(oo, list):
for o in oo:
if isinstance(oo, gtype):
return o
elif isinstance(oo, gtype):
return oo
[docs] def del_obj(self, gtype):
self._gobjs.pop(gtype, None)
def __getitem__(self, gtype):
return self.get_obj(gtype)
[docs] def add_axobj(self, gtype, obj, single=False, axis=None):
"""Add a object to the bank of current :class:`matplotlib.axes.Axes` instance
:Return:
The object added.
:Example:
>>> text_object = myplot.add_axobj('vmin', 24.5)
:See also:
:meth:`get_axobj`
"""
if self.axes is None: return
container = self.axes if axis is None else getattr(self.axes, axis+'axis')
if not hasattr(container, '_vacumm'):
container._vacumm = {}
if single:
container._vacumm[gtype] = obj
else:
if not container._vacumm.has_key(gtype):
container._vacumm[gtype] = []
container._vacumm[gtype].append(obj)
return obj
[docs] def set_axobj(self, gtype, obj, axis=None):
"""Shortcut to :meth:`add_axobj` called with ``single=True``"""
return self.add_axobj(gtype, obj, single=True, axis=axis)
[docs] def get_axobj(self, gtype=None, axis=None, axes=None):
"""Get an object stored in the bank of current
:class:`matplotlib.axes.Axes` instance
:Params:
- **gtype**, optional: Object type (name).
If not set, all objects are returned.
- **axis**, optional: If one of ``"x"`` or ``"y"``,
get objects stored in current
xaxis or yaxis instead if current axes instance.
- **axes**, optional: Target axes, which defaults to
#. attribute :attr:`axes`,
#. result from :func:`matplotlib.pyplot.gca`.
:Example:
>>> myplot.get_axobj()
>>> myplot.get_axobj('vmin')
>>> myplot.get_axobj('hlitvs', axis='x')
>>> Plot.get_axobj()
:Return:
The object or ``None`` if not found.
:See also:
:meth:`add_axobj`
"""
if axes is None and hasattr(self, 'axes'): axes = self.axes
if axes is None: return
#if not P.get_fignums() or not P.gcf().axes: return
#axes = P.gca()
container = axes if axis is None else getattr(axes, axis+'axis')
if container is None: return
if not hasattr(container, '_vacumm'):
container._vacumm = {}
if gtype is None: return container._vacumm
return container._vacumm.get(gtype, None)
[docs] def del_axobj(self, gtype, axis=None):
container = self.axes if axis is None else getattr(self.axes, axis+'axis')
if container is None: return
if container._vacumm.has_key(gtype):
del container._vacumm[gtype]
[docs] def isset(self, key):
"""Check if an attribute has been manually set different from ``None``
:Example:
>>> return myplot.iset('xmin')
:See also: :meth:`get_obj` :meth:`get_axobj`
"""
if key in ['units', 'long_name']:
return getattr(self, key) is not None
return self.get_axobj(key) is not None or self.get_obj(key) is not None
[docs] def post_plot(self, grid=True, figtext=None, show=True,
close=False, savefig=None, savefigs=None, title=None,
fullscreen=False, anchor=None, autoresize=2, finalize=None,
key=False, hlitvs=False, legend=False, tight_layout=False,
param_label=None, **kwargs):
"""Finish plotting stuff (plot size, grid, texts, saves, etc)
:Params:
- **title**: Title of the figure [defaults to var.long_name or '']
- **grid**: Plot the grid [default: True]
- **grid_<param>**: <param> is passed to :func:`~matplotlib.pyplot.grid`
- **hlitvs**: Add highlithing if time axis [default: False]
- **figtext**: figtext Add text at a specified position on the
figure. Example: figtext=[0,0,'text'] add a 'text' at the
lower left corner, or simply figtext='text'.
- **figtext_<param>**: <param> is passed to
:func:`~matplotlib.pyplot.figtext`
- **anchor**: Anchor of the axes (useful when resizing) in
['C', 'SW', 'S', 'SE', 'E', 'NE', 'N', 'NW', 'W'].
- **legend**, optional: Draw the legend using :func:`~matplotlib.pyplot.legend`.
- **legend_<param>**: <param> is passed to :func:`~matplotlib.pyplot.legend`
- **show**: Display the figure [default: True]
- **savefig**: Save the figure to this file.
- **savefig_<param>**: <param> is passed to method :meth:`savefig`
and finally to the matplotlib function :func:`~matplotlib.pyplot.savefig`.
- **savefigs**: Save the figure into multiple formats using
:func:`savefigs` and 'savefigs' as the prefix to the files.
- **savefigs_<param>**: <param> is passed to :func:`savefigs`
- **autoresize**: Auto resize the figure according axes (1 or True),
axes+margins (2). If 0 or False, not resized [default: False=2].
- **key**: Add a key (like 'a)') to the axes using add_key
if different from None [default: None]
- **key_<param>**: <param> is passed to :func:`add_key`
- **param_label**: Add a param label to the figure using
:meth:`add_param_label` if different from None [default: None]
- **param_label_<param>**: <param> is passed to :meth:`add_param_label`
- **close**: Close the figure at the end [default: False]
- **title_<param>**: <param> is passed to :func:`~matplotlib.pyplot.title`
- **logo_<param>**: <param> is passed to :func:`add_logo`
- **tight_layout**: To make a tight layout one everything is plotted.
"""
self._post_plotted = True
# Format X and Y axis
self.format_axes(**kwargs)
# Filter kewords
kw = {}
for kwtype in ['grid', 'title', 'hlitvs', 'hldays', 'dayhl', 'finalize',
'figtext', 'key', 'savefig', 'savefigs', 'show', 'legend',
'tight_layout', 'param_label', 'autoresize']:
kw[kwtype] = kwfilter(kwargs, kwtype+'_')
if (kwtype in self._primary_attributes+self._secondary_attributes+
self._special_attributes and kw[kwtype].has_key(kwtype)):
del kw[kwtype][kwtype]
kwanim = kwfilter(kwargs, 'anim_', keep=True)
kw['show'].update(**kwanim)
kw['hlitvs'].update(kw['dayhl']) # compat
kw['hlitvs'].update(kw['hldays']) # compat
# Resize plot
autoresized = self.autoresize(autoresize, **kw['autoresize'])
# Anchor
if anchor is None and autoresized:
anchor='C'
if anchor is not None:
self.axes.set_anchor(anchor)
# Grid
self.grid(grid, **kw['grid'])
# Highlight intervals
if kwargs.pop('hldays', hlitvs):
if kwargs.pop('hldays', False):
kw['hlitvs'].setdefault['units'] = 'day'
self.hlitvs(**kw['hlitvs'])
# Title
self.ptitle(title, **kw['title'])
# Fig text
self.figtext(figtext, **kw['figtext'])
# Key of axes
self.add_key(key, **kw['key'])
# Params
if param_label:
self.add_param_label(param_label, **kw['param_label'])
# Legend
if legend:
self.legend(**kw['legend'])
# Tight layout
if tight_layout:
self.fig.tight_layout(**kw['tight_layout'])
# User finalization
if callable(finalize):
dict_check_defaults(kw['finalize'], fig=self.fig, ax=self.axes)
finalize(**kw['finalize'])
# Save it
self.savefig(savefig, **kw['savefig'])
self.savefigs(savefigs, **kw['savefigs'])
# Show or close
if show:
self.show(**kw['show'])
if close:
self.close()
def _xyhide_(self, xy, hide):
if not isinstance(hide, basestring):
return hide
hide = hide.lower()
if hide=='auto' or hide=='subplot':
if not isinstance(self.axes, Subplot): return False
if xy=='x':
return not self.axes.is_last_row()
return not self.axes.is_first_col()
return False
[docs] def autoresize(self, autoresize=True, minaspect=None):
"""Resize figure or axes to fit to data axes"""
if (autoresize and self.axes.get_aspect() != 'auto' and
isinstance(self.axes, Subplot) and
self.axes.is_first_col() and self.axes.is_first_row() and
self.axes.is_last_col() and self.axes.is_last_row()):
r = self.axes.get_aspect() # dy/dx
if r=='equal':
r = 1.
r *= self.axes.get_data_ratio()
# rect = self.axes.get_position(True)
# if autoresize == 2: r *= rect.width/rect.height
sp = self.fig.subplotpars
x0 = sp.left
x1 = sp.right
y0 = sp.bottom
y1 = sp.top
Dx = x1 - x0
Dy = y1 - y0
R = r * Dx / Dy
if minaspect is not None:
R = N.clip(R, minaspect, 1/minaspect)
w, h = self.fig.get_size_inches()
if autoresize=='x': # Resize x only, change the surface
H = h
W = H / R
elif autoresize=='y': # Resize y only, change the surface
W = w
H = R * W
else: # Resize both x and y without changing the surface
a = 1.*w*h
W = N.sqrt(a / R)
H = r * W
self.fig.set_size_inches(W, H ,forward=True)
return True
return False
def _transform_(self, transform, default=None):
return _transform_(transform, default, ax=self.axes, fig=self.fig)
[docs] def get_xy(self, x, y, transform=None, xyscaler=None,
default_transform=None, atleast_1d=False):
"""Convert (x,y) in data coordinates
:Params:
- **x/y**: Coordinates referenced to data, axes or figure.
- **transform**, optional: Transform applied to coordinates.
This either a :class:`matplotlib.transforms.Transform` or a string:
``"data"``, ``"axes"``, ``"figure"``.
- **xyscaler**, optional: Converter of coordinates used when input
coordinates are in data coordinates. It must be a callable,
and it defaults to attribute :attr:`xyscaler` if existing.
It converts for instance from degrees to meters for :class:`Map`
instances. If equal to False, no conversion is performed.
"""
transform = self._transform_(transform, default_transform)
# From figure or axes coordinates
if transform is self.fig.transFigure or transform is self.axes.transAxes:
return tuple((transform-self.axes.transData).transform_point((x, y)))
# From data coordinates (check xyscaler only)
if (transform is self.axes.transData or transform in self.axes.transData._parents.values() \
or str(self.axes.transData) in str(transform)) and xyscaler is not False: # Add transform from ie degrees to m
if xyscaler is None and hasattr(self, 'xyscaler'): xyscaler = self.xyscaler
x = _asnum_(x)
y = _asnum_(y)
if xyscaler:
x, y = xyscaler(x, y)
return x, y
[docs] def get_transoffset(self, x, y, units='points', transform='data'):
"""Return a translation :class:`~maplotlib.transforms.Transform`
It can be used for instance to plot an object with an
offset with respect to its specified position.
:Params:
- **x/y**: Relative position.
- **units**, optional: Units ("points", "inches", "pixels", ...)
- **transform**, optional: Base transform for reference position.
Choose for instance "data" or "axes".
:Example:
>>> o = Plot2D(data)
>>> o.add_point(-4, 43)
>>> t = o.get_transoffset(0, 10)
>>> o.text(-4, 43, transform=t)
:See also: :func:`~maplotlib.transforms.offset_copy`
"""
transform = self._transform_(transform, 'data')
return offset_copy(transform, fig=self.fig, x=x, y=y, units=units)
[docs] def grid(self, b=True, **kwargs):
"""Add a grid to axes using :func:`~matplotlib.pyplot.grid`
:Example:
>>> myplot.grid(color='r')
>>> myplot.grid(False)
"""
# grid = self.get_axobj('grid')
# print 'grid',grid
# if grid is None:
self.axes.grid(b=b, **kwargs)
# print grid
# self.set_axobj('grid', grid)
# print self.get_axobj('grid')
# print 'done'
# return grid
[docs] def add_figtext(self, *args, **kwargs):
"""Add text to the current figure using :func:`~matplotlib.pyplot.figtext`
:Defaults:
- Position: defaults to the top center.
- Alignement: ``ha="center", va="top"``
:Example:
>>> myplot.figtext('Group of plots')
>>> myplot.figtext(0.2, 0.92, 'My plots', color='b', ha='left', va='center')
"""
# Arguments
if len(args)==0: return
if isinstance(args[0], (list, tuple)):
args = args[0]
if len(args) == 1:
text = args[0]
if text is None: return
if not isinstance(text, dict):
x = .5
y = .98
else:
x = 0
y = 0
else:
x, y, text = args
# Keywords
if isinstance(text, dict):
text = ' '.join('%s=%s'%(key, text[key]) for key in sorted(text.keys()))
defaults = dict(ha='left', va='bottom', size=9, color='.4', family='monospace')
else:
defaults = dict(ha='center', va='center', size='12')
for key, val in defaults.items():
kwargs.setdefault(key, val)
# Plot
return self.add_obj('figtext', self.fig.text(x, y, text, **kwargs))
figtext = add_figtext # backward compat
[docs] def add_text(self, x, y, text, transform='axes', shadow=False, glow=False,
xyscaler=None, strip=True, **kwargs):
"""Add text to the plot axes
:Params:
- **x,y**: Coordinates of the text.
- **text**: Text to plot.
- **transform**, optional: Type of coordinates
(like ``"axes"`` or ``"data"``).
- **shadow**, optional: Add a droped shadow below the text
(see :func:`add_shadow`).
- **shadow_<param>**, optional: ``<param>`` is passed to :func:`add_shadow`.
- **glow**, optional: Add a glow effect the text
(see :func:`add_glow`).
- **glow_<param>**, optional: ``<param>`` is passed to :func:`add_glow`.
- Other keywords are passed to :func:`matplotlib.pyplot.text`.
"""
# Keywords
kwsh = kwfilter(kwargs, 'shadow')
kwgl = kwfilter(kwargs, 'glow')
# Coordinates transform
transform = self._transform_(transform, 'axes')
if transform not in [self.axes.transAxes, self.fig.transFigure]:
x, y = self.get_xy(x, y, transform, xyscaler=xyscaler)
# Plot
if strip: text = text.strip()
obj = self.add_axobj('text', self.axes.text(x, y, text, transform=transform, **kwargs))
self.register_obj(obj, **kwargs)
# Path effects
if shadow: self.register_obj(self.add_shadow(obj, **kwsh), **kwargs)
if glow: self.register_obj(self.add_glow(obj, **kwgl), **kwargs)
return obj
text = add_text # backward compat
[docs] def add_water_mark(self, text, x=.5, y=.5, ha='center', va='center', size=20,
color='k', alpha=.7, zorder=0, transform='axes', **kwargs):
"""Add a background text to the plot
All arguments are passed to :meth:`add_text`.
"""
return self.add_text(x, y, text, ha=ha, va=va, size=size, color=color,
zorder=zorder, transform=transform, **kwargs)
[docs] def add_time_label(self, x, y, mytime, fmt="%Y/%m/%d %H:%M", **kwargs):
"""Add a time label
See :meth:`add_text` for other keywords
"""
text = strftime(fmt, mytime)
kwargs.setdefault('family', 'monospace')
return self.add_text(x, y, text, **kwargs)
[docs] def add_lon_label(self, x, y, mylon, fmt='%5g', **kwargs):
"""Add a longitude label
See :meth:`add_text` for other keywords
"""
text = lonlab(mylon, fmt=fmt)
return self.add_text(x, y, text, **kwargs)
[docs] def add_lat_label(self, x, y, mylat, fmt='%5g', **kwargs):
"""Add a longitude label
See :meth:`add_text` for other keywords
"""
text = latlab(mylat, fmt=fmt)
return self.add_text(x, y, text, **kwargs)
[docs] def add_left_label(self, text, **kwargs):
"""Add a text label to the left of the plot
:See also: :func:`~vacumm.misc.plot.add_left_label`
"""
kwargs['ax'] = self.axes
return add_left_label(text, **kwargs)
[docs] def add_right_label(self, text, **kwargs):
"""Add a text label to the right of the plot
:See also: :func:`~vacumm.misc.plot.add_right_label`
"""
kwargs['ax'] = self.axes
return add_right_label(text, **kwargs)
[docs] def add_top_label(self, text, **kwargs):
"""Add a text label to the top of the plot
:See also: :func:`~vacumm.misc.plot.add_top_label`
"""
kwargs['ax'] = self.axes
return add_top_label(text, **kwargs)
[docs] def add_bottom_label(self, text, **kwargs):
"""Add a text label to the bottom of the plot
:See also: :func:`~vacumm.misc.plot.add_bottom_label`
"""
kwargs['ax'] = self.axes
return add_bottom_label(text, **kwargs)
[docs] def add_key(self, key, **kwargs):
"""Add a key to specify the plot number
See :func:`~vacumm.misc.plot.add_key` for more information.
"""
if key is False: return
from vacumm.misc.plot import add_key
kwargs.update(fig=self.fig, axes=self.axes)
return self.set_axobj('key', add_key(key, **kwargs))
[docs] def add_param_label(self, text, **kwargs):
"""Add parameters description to the bottom/left of the figure using
:func:`~vacumm.misc.plot.add_param_label`
:Example:
>>> c = curve2(sst, show=False)
>>> c.add_param_label(dict(max=.23, kz=0.25), color='r')
:Params:
- **text**: Either a string or a dictionary.
- See :func:`~vacumm.misc.plot.add_param_label` for other parameters
"""
kwargs['fig'] = self.fig
return add_param_label(text, **kwargs)
[docs] def add_annotation(self, x, y, xtext, ytext, text='', xycoords='data',
textcoords='offset points', arrowprops='->',
shadow=False, glow=False,
xyscaler=None, strip=True, **kwargs):
"""Add an annotation to the plot axes using :func:`matplotlib.pyplot.annotate`
:Params:
- **x,y**: Coordinates of the text.
- **text**: Text to plot.
- **xycoords/transform**, optional: Type of coordinates of point
(like ``"axes"`` or ``"data"``).
- **textcoords**, optional: Type of coordinates of text
(like ``"axes"`` or ``"data"``).
- **arrowprops**, optional: Dictionary of arrow properties or
string thet defines the arrow style.
- **shadow**, optional: Add a droped shadow below the text
(see :func:`add_shadow`).
- **shadow_<param>**, optional: ``<param>`` is passed to :func:`add_shadow`.
- **glow**, optional: Add a glow effect the text
(see :func:`add_glow`).
- **glow_<param>**, optional: ``<param>`` is passed to :func:`add_glow`.
- Other keywords are passed to :func:`matplotlib.pyplot.annotate`.
"""
# Keywords
kwsh = kwfilter(kwargs, 'shadow')
kwgl = kwfilter(kwargs, 'glow')
# Coordinates transform
xycoords = kwargs.pop('transform', xycoords)
xycoords = self._transform_(xycoords, 'data')
if not isinstance(xycoords, basestring) and \
xycoords not in [self.axes.transAxes, self.fig.transFigure]:
x, y = self.get_xy(x, y, xycoords, xyscaler=xyscaler)
if textcoords is None:
textcoords = xycoords
else:
textcoords = self._transform_(textcoords, 'offset points')
if not isinstance(textcoords, basestring) and \
textcoords not in [self.axes.transAxes, self.fig.transFigure]:
xtext, ytext = self.get_xy(xtext, ytext, textcoords, xyscaler=xyscaler)
# Arrow properties
if isinstance(arrowprops, basestring):
arrowprops = dict(arrowstyle=arrowprops)
elif not isinstance(arrowprops, dict):
arrowprops = {}
arrowprops.setdefault('arrowstyle', '->')
# Plot
if strip: text = text.strip()
obj = self.axes.annotate(xy=(x, y), xytext=(xtext, ytext), s=text,
xycoords=xycoords, textcoords=textcoords, arrowprops=arrowprops, **kwargs)
obj = self.add_axobj('text', obj)
self.register_obj(obj, **kwargs)
# Path effects
if shadow: self.register_obj(self.add_shadow(obj, **kwsh), **kwargs)
if glow: self.register_obj(self.add_glow(obj, **kwgl), **kwargs)
return obj
[docs] def hlitvs(self, **kwargs):
"""Highlight intervals with grey/white background alternance
See :func:`~vacumm.misc.plot.hlitvs` for more information.
"""
ret = []
for i, xy in enumerate('yx'):
if self.order[i] == 't':
cached = self.get_axobj('hlitvs', axis=xy)
if cached is not None:
self.remove(cached)
kwargs['axis'] = getattr(self.axes, xy+'axis')
obj = self.set_axobj('hlitvs', hlitvs(**kwargs), axis=xy)
self.register_obj(obj, **kwargs)
return obj
[docs] def hldays(self, **kwargs):
"""Alias for :
>>> myplot.hlitv(units='day')
"""
kwargs.setdefault('units', 'day')
return self.hlitvs(**kwargs)
[docs] def ptitle(self, title=None, force=None, **kwargs):
"""Add a title to the plot
.. note::
No title is added to the plot if a title already exists
and the specified title is guessed (not hard set).
:Params:
- **title**: Title to add to plot.
- A string: directly used.
- ``True`` or ``None``: the :attr:`title` attribute is used.
- ``False``: not title is plotted.
- **force**, optional: If the title is already plotted,
it is not overwritten, except if ``force is True``.
- Other keywords are passed to :func:`matplotlib.pyplot.title`.
"""
if title is None:
title = self.title
isset = self.isset('title')
else:
isset = True
if force is None: force = isset
if title is False: return
if title is not None and (force or self.axes.get_title()==''):
return self.set_axobj(title, self.axes.set_title(title, **kwargs))
[docs] def legend(self, *args, **kwargs):
"""A simple call to the :meth:`matplotlib.axes.Axes.legend` method
Arguments and keywords are passed to :meth:`~matplotlib.axes.Axes.legend`.
Defaults values :
- **loc**: ``"best"``
- **shadow**: ``False``
- **fancybox**: ``True``
- **alpha** (applied to legend patch): ``0.5``
"""
kwargs.setdefault('loc', 'best')
# kwargs.setdefault('shadow', False)
# kwargs.setdefault('fancybox', True)
alpha = kwargs.pop('alpha', .5)
zorder = kwargs.pop('zorder', None)
# zorder = kwargs.pop('framealpha', alpha)
leg = self.axes.legend(*args, **kwargs)
if leg is not None:
leg.legendPatch.set_alpha(alpha)
self.set_axobj('legend', leg)
if zorder:
leg.set_zorder(zorder)
return leg
def _get_xykeys_(self, xy):
"""Get possible keys for interval selections along X or Y"""
keys = [xy]
ixy = 'yx'.index(xy)
if self.order[ixy]=='x':
keys.extend(['lon', 'longitude'])
elif self.order[ixy]=='y':
keys.extend(['lat', 'latitude'])
elif self.order[ixy]=='z':
keys.extend(['level', 'dep', 'depth'])
elif self.order[ixy]=='t':
keys.extend(['time'])
elif self.order[ixy]=='d':
keys.extend(['data', 'value'])
return keys
def _get_boxminmax_(self, box):
"""Get ``xmin,ymin,xmax,ymax`` from box specs
Two cases:
- ``box=xmin,ymin,xmax,ymax``
- ``box=dict(x=(xmin,xmax),y=(xmin,xmax))`` or with for instance
``lon``, ``lat``, ``time`` depending on axis type.
"""
if isinstance(box, dict):
# Along X
for key in self._get_xykeys_('x'):
if key in box:
xmin, xmax = box[key][:2]
break
else:
xmin, xmax = self.xmin, self.xmax
# Along Y
for key in self._get_xykeys_('y'):
if key in box:
ymin, ymax = box[key][:2]
break
else:
ymin, ymax = self.ymin, self.ymax
box = xmin, ymin, xmax, ymax
return box
[docs] def add_box(self, box, zorder=150, shadow=False, glow=False, color='r',
npts=10, xyscaler=None, fill=False, **kwargs):
"""Add a box to the plot using :meth:`matplotlib.pyplots.plot`
:Params:
- **box**: Box limits in the forms ``[xmin,ymin,xmax,ymax]``
``dict(x=(xmin,xmax),y=(xmin,xmax)``.
- **color**, optional: Line color of the box.
- **npts**, optional: Number of points per side
(useful with special map projections).
- **shadow**, optional: Add a droped shadow below the box
(see :func:`add_shadow`).
- **shadow_<param>**, optional: ``<param>`` is passed to :func:`add_shadow`.
- **glow**, optional: Add a glow effect the box
(see :func:`add_glow`).
- **glow_<param>**, optional: ``<param>`` is passed to :func:`add_glow`.
- Other keywords are passed to :func:`matplotlib.pyplot.plot`.
:See also: :func:`matplotlib.pyplot.plot`
"""
# Limits
try:
xmin, ymin, xmax, ymax = self._get_boxminmax_(box)
except:
raise PlotError('Box limits should be a list in the form [xmin,ymin,xmax,ymax]'
' or a dictionary in the form dict(x=(xmin,xmax),y=xmin,xmax): %s'%box)
xmin = _asnum_(xmin)
xmax = _asnum_(xmax)
ymin = _asnum_(ymin)
ymax = _asnum_(ymax)
if xyscaler is None and hasattr(self, 'xyscaler'): xyscaler = self.xyscaler
if xyscaler:
xmin, ymin = xyscaler(xmin, ymin)
xmax, ymax = xyscaler(xmax, ymax)
# Params
kwargs.update(ec=color, zorder=zorder)
if not fill:
kwargs.update(fc='none')
kwsh = kwfilter(kwargs, 'shadow')
kwgl = kwfilter(kwargs, 'glow')
kwsh.setdefault('zorder', zorder)
kwgl.setdefault('zorder', zorder)
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
# Sides
X = N.linspace(xmin, xmax, npts),
X += N.linspace(xmax, xmax, npts)[1:],
X += N.linspace(xmax, xmin, npts)[1:],
X += N.linspace(xmin, xmin, npts),
Y = N.linspace(ymin, ymin, npts),
Y += N.linspace(ymin, ymax, npts)[1:],
Y += N.linspace(ymax, ymax, npts)[1:],
Y += N.linspace(ymax, ymin, npts),
# Plot
o = self.axes.fill(N.concatenate(X), N.concatenate(Y), **kwargs)
self.register_obj(o, **kwargs)
# Effects
if shadow: self.add_shadow(o,**kwsh)
if glow: self.add_glow(o, **kwgl)
return o
[docs] def add_arrow(self, x, y, udata, vdata, zorder=150, polar=False, degrees=True,
shadow=False, glow=False, quiverkey=False, xyscaler=None,
color=False, **kwargs):
"""Add an arrow to the map using :func:`matplotlib.pyplot.quiver`
:Params:
- **x,y**: Coordinates of the position of the tail
- **udata**: X or radial component of arrows.
- **vdata**: Y or directional component of arrows.
- **polar**, optional: Consider polar coordinates: ``(u, v) -> (rho, theta)``
- **degrees**, optional: If True (default), trat ``theta`` as degrees, else radians.
- **quiver_<param>**, optional: ``<param>`` is passed to :func:`matplotlib.pyplot.quiver`.
- Other keywords are passed to :func:`matplotlib.pyplot.plot`.
:See also: :func:`matplotlib.pyplot.scatter`
"""
# Data for arrows
udata = MV2.asarray(udata)
vdata = MV2.asarray(vdata)
if polar:
u, v = udata,vdata
m = u
angle = (v*N.pi/180.) if degrees else v
u = m * MV2.cos(angle)
v = m * MV2.sin(angle)
del angle
if hasattr(m, 'units'):
u.units = v.units = m.units
if hasattr(m, 'long_name'):
u.long_name = 'X component of '+m.long_name
v.long_name = 'Y component of '+m.long_name
# Data
udata, vdata = u,v
# Coordinates
x = _asnum_(x, atleast_1d=True)
y = _asnum_(y, atleast_1d=True)
if xyscaler is None and hasattr(self, 'xyscaler'): xyscaler = self.xyscaler
if callable(xyscaler):
x, y = xyscaler(x, y)
# Params
kwargs.update(zorder=zorder)
kwqv = kwfilter(kwargs,'quiver')
kwqvk = kwfilter(kwargs,'quiverkey')
#dict_copy_items(kwargs, [kwqv],'anim')
kwargs = dict_merge(kwargs, kwqv)
args = []
# Color
if color is True:
color = N.ma.sqrt(u**2+v**2)
if isinstance(color, (N.ndarray, list)):
args.append(color)
elif color is not False and color is not None:
kwargs['color'] = color
# Plot
o = self.axes.quiver(x, y, udata, vdata, *args, **kwargs)
self.register_obj(o, **kwargs)
if quiverkey:
self.quiverkey(o, **kwqvk)
return o
[docs] def quiverkey(self, qv, value, pos=(0.,1.02), text='%(value)g %(units)s',
units=None, latex_units=None, value_mode=80, **kwargs):
"""Add a quiver key to the plot
:Params:
- **qv**: Results of :func:`~matplotlib.pyplot.quiver`.
- **value**: Numeric value for key (used by text).
- **pos**, optional: Position of key for arrow .
- **text**, optional: Text or format with variables 'value' and 'units'.
- **units**, optional: Units for key (used by text).
- **latex_units**, optional: Interpret units using latex.
- Extra keywords are passed to :func:`~matplotlib.pyplot.quiverkey`.
"""
# Value
value = get_quiverkey_value(value, mode=value_mode)
# Text
if units is None:
units = self.quiverkey_units
elif cdms2.isVariable(units) and hasattr(units,'units'):
units = units.units
elif not isinstance(units, basestring):
units = ''
latex_units = kwargs.pop('tex', None)
if latex_units is None: latex_units = self.latex_units
if units is None:
units = ''
if latex_units and not self.is_latex(units):
units = '$%s$'%units
try:
text = text % vars()
except:
text = '%(value)g' % value
# Plot
pos = kwargs.pop('loc', pos)
qvk = self.axes.quiverkey(qv, pos[0], pos[1], value, text, **kwargs)
return self.register_obj(qvk, 'quiverkey', **kwargs)
[docs] def get_quiverkey_units(self):
"""Get :attr:`quiverkey_units`"""
units = self.get_obj('quiverkey_units')
if units is None and isinstance(self, Plot1D) and self.isset('units'):
units = self.units
if units is None:
units = self.get_units(idata=[-2, -1])
return units
[docs] def set_quiverkey_units(self, value):
"""Set :attr:`quiverkey_units`"""
self.set_axobj('quiverkey_units', value)
[docs] def del_quiverkey_units(self):
"""Del :attr:`quiverkey_units`"""
self.del_axobj('quiverkey_units')
quiverkey_units = property(get_quiverkey_units, set_quiverkey_units,
del_quiverkey_units, doc="Units used for quiverkey")
[docs] def add_line(self, extents, zorder=150, shadow=False, glow=False, color='r',
npts=10, xyscaler=None, **kwargs):
"""Add a line to the plot using :meth:`matplotlib.pyplots.plot`
:Params:
- **extents**: Extents in the forms ``[xmin,ymin,xmax,ymax]``
``dict(x=(xmin,xmax),y=xmin,xmax)``.
- **color**, optional: Line color of the line.
- **npts**, optional: Number of points per side
(useful with special map projections).
- **shadow**, optional: Add a droped shadow below the box
(see :func:`add_shadow`).
- **shadow_<param>**, optional: ``<param>`` is passed to :func:`add_shadow`.
- **glow**, optional: Add a glow effect the box
(see :func:`add_glow`).
- **glow_<param>**, optional: ``<param>`` is passed to :func:`add_glow`.
- Other keywords are passed to :func:`matplotlib.pyplot.plot`.
:See also: :func:`matplotlib.pyplot.plot`
"""
# Positions
try:
xmin, ymin, xmax, ymax = self._get_boxminmax_(extents)
except:
raise PlotError('Extents should be a list in the form [xmin,ymin,xmax,ymax]'
' or a dictionary in the form dict(x=(xmin,xmax),y=xmin,xmax): %s'%extents)
xmin = _asnum_(xmin)
xmax = _asnum_(xmax)
ymin = _asnum_(ymin)
ymax = _asnum_(ymax)
if xyscaler is None and hasattr(self, 'xyscaler'): xyscaler = self.xyscaler
if xyscaler:
xmin, ymin = xyscaler(xmin, ymin)
xmax, ymax = xyscaler(xmax, ymax)
# Params
kwargs.update(color=color, zorder=zorder)
kwsh = kwfilter(kwargs, 'shadow')
kwgl = kwfilter(kwargs, 'glow')
kwsh.setdefault('zorder', zorder)
kwgl.setdefault('zorder', zorder)
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
# Sides
X = N.linspace(xmin, xmax, npts)
Y = N.linspace(ymin, ymax, npts)
# Plot
o = self.axes.plot(X, Y, **kwargs)
self.register_obj(o, **kwargs)
# Effects
if shadow: self.add_shadow(o, **kwsh)
if glow: self.add_glow(o, **kwgl)
return o
[docs] def add_point(self, x, y, zorder=150, shadow=False, glow=False,
color='r', size=20, xyscaler=None, **kwargs):
"""Add a point to the map using :meth:`matplotlib.pyplots.plot`
:Params:
- **x,y**: Coordinates.
- **color**, optional: Line color of the point.
- **shadow**, optional: Add a droped shadow below the box
(see :func:`add_shadow`).
- **shadow_<param>**, optional: ``<param>`` is passed
to :func:`add_shadow`.
- **glow**, optional: Add a glow effect the box
(see :func:`add_glow`).
- **glow_<param>**, optional: ``<param>`` is passed to :func:`add_glow`.
- Other keywords are passed to :func:`matplotlib.pyplot.plot`.
:See also: :func:`matplotlib.pyplot.scatter`
"""
# Coordinates
x = _asnum_(x, atleast_1d=True)
y = _asnum_(y, atleast_1d=True)
if xyscaler is None and hasattr(self, 'xyscaler'): xyscaler = self.xyscaler
if callable(xyscaler):
x, y = xyscaler(x, y)
# Params
kwargs.update(zorder=zorder)
kwsh = kwfilter(kwargs, 'shadow')
kwgl = kwfilter(kwargs, 'glow')
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
size = kwargs.pop('s', size)
if size is not None:
kwargs['s'] = size
color = kwargs.pop('c', color)
if color is not None:
kwargs['c'] = color
#kwsh.setdefault('zorder', zorder-0.01)
#kwgl.setdefault('zorder', zorder-0.01)
# Plot
o = self.axes.scatter(x, y, **kwargs)
self.register_obj(o, **kwargs)
# Effects
if shadow: self.add_shadow(o, **kwsh)
if glow: self.add_glow(o, **kwgl)
return o
[docs] def add_place(self, x, y, text, zorder=150, color='k',
shadow=False, glow=False,
text_offset=(0, 10), ha='center', va='center', **kwargs):
"""Place a point using :meth:`add_point` and a label using :meth:`add_text`
:Examples:
>>> m = map2(sst, show=False)
>>> m.add_place(-5, 44, 'Buoy 654', text_offset=(20,0), text_ha='left',
text_color='b', point_size=100, shadow=True)
:Params:
- **x/y**: Coordinates of the place in data units.
- **text**: Name of the place.
- **text_offset**, optional: Offset of the text in points with relative to
coordinates.
- **point_<param>**, optional: ``<param>`` is passed to :meth:`add_point`.
- **text_<param>**, optional: ``<param>`` is passed to :meth:`add_text`.
"""
kwpoint = kwfilter(kwargs, 'point_')
kwtext = kwfilter(kwargs, 'text_')
kwcom = dict(zorder=zorder, shadow=shadow, glow=glow, color=color)
dict_check_defaults(kwpoint, **kwcom)
dict_check_defaults(kwtext, ha=ha, va=va, weight='bold', **kwcom)
tha = kwtext['ha']
tva = kwtext['va']
if tha=='auto':
if text_offset[0]==0:
tha = 'center'
elif text_offset[0]>0:
tha = 'left'
else:
tha = 'right'
if tva=='auto':
if text_offset[1]==0:
tva = 'center'
elif text_offset[1]>0:
tva = 'bottom'
else:
tva = 'top'
kwtext['ha'] = tha
kwtext['va'] = tva
p = self.add_point(x, y, **kwpoint)
kwtext.setdefault('transform', self.get_transoffset(*text_offset))
t = self.add_text(x, y, text, **kwtext)
return p, t
[docs] def add_lines(self, xx, yy, zorder=150, shadow=False, glow=False, color='r',
xyscaler=None, closed=False, **kwargs):
"""Add lines to the plot using :meth:`matplotlib.axes.Axes.plot`
:Params:
- **xx/yy**: Coordinates (in degrees).
- **color**, optional: Line color of the line.
- **closed**, optional: Close the lines to form a polygon.
- **shadow**, optional: Add a droped shadow below the box
(see :func:`add_shadow`).
- **shadow_<param>**, optional: ``<param>`` is passed to :func:`add_shadow`.
- **glow**, optional: Add a glow effect the box
(see :func:`add_glow`).
- **glow_<param>**, optional: ``<param>`` is passed to :func:`add_glow`.
- Other keywords are passed to :func:`matplotlib.pyplot.plot`.
:See also: :func:`matplotlib.pyplot.plot`
"""
# Positions
xx = N.ma.ravel(_asnum_(xx, atleast_1d=True))
yy = N.ma.ravel(_asnum_(yy, atleast_1d=True))
if xx.size!=yy.size:
raise PlotError('xx and yy must have the same number of elements (%i!=%i)'%(xx.size,yy.size))
if xyscaler is None and hasattr(self, 'xyscaler'): xyscaler = self.xyscaler
if xyscaler:
xx, yy = xyscaler(xx, yy)
# Params
kwargs.update(color=color, zorder=zorder)
kwsh = kwfilter(kwargs, 'shadow')
kwgl = kwfilter(kwargs, 'glow')
kwsh.setdefault('zorder', zorder)
kwgl.setdefault('zorder', zorder)
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
# Plot
o = self.axes.plot(xx, yy, **kwargs)
self.register_obj(o, **kwargs)
# Effects
if shadow: self.add_shadow(o, **kwsh)
if glow: self.add_glow(o, **kwgl)
return o
[docs] def add_glow(self, objs, gtypes=None, **kwargs):
"""Add glow effect to objects
:See: :func:`add_glow`
"""
kwargs['ax'] = self.axes
anim = kwargs.pop('anim', None)
return self.register_obj(add_glow(objs, **kwargs), gtypes, anim=anim)
[docs] def add_shadow(self, objs, gtypes=None, **kwargs):
"""Add shadow to objects
:See: :func:`add_shadow`
"""
kwargs['ax'] = self.axes
anim = kwargs.pop('anim', None)
return self.register_obj(add_shadow(objs, **kwargs), gtypes, anim=anim)
[docs] def savefig(self, figfile, verbose=False, mkdir=True, **kwargs):
"""Save the figure to a file
:Params:
- **figfile**: Figure file name. Also accepts
:class:`~vacumm.misc.remote.OutputWorkFile`.
- **verbose**, optional: Informs about file name when written.
- **mkdir**, optional: Make figure directory if it does not exists.
- Other keywords are passed to :func:`matplotlib.pyplot.savefig`.
"""
if not figfile:
return
# List of files
if isinstance(figfile, (list, tuple)):
oo = []
for ff in figfile:
oo.append(self.savefig(ff, verbose=verbose, mkdir=mkdir, **kwargs))
return oo
# Remote output file
rem = figfile if isinstance(figfile, OutputWorkFile) else False
if rem: figfile = figfile.local_file
# Extension
backend = P.get_backend().lower()
ext_standalone = ['ps','pdf','svg']
ext_others = ['png','gif','jpg','jpeg','bmp']
basename, ext = os.path.splitext(figfile)
if ext == '':
ext = 'png'
figfile += '.png'
elif ext == ".py":
ext = 'png'
figfile = basename+'.png'
else:
ext = ext[1:]
if not rem and ext.lower() not in ext_others:
if backend not in ext_standalone and ext.lower() not in ext_standalone:
figfile += '.png'
elif backend in ext_standalone and not ext.lower().endwith(backend):
figfile += '.'+backend
# Directory
figdir = os.path.dirname(figfile)
if mkdir and figdir and not os.path.exists(figdir):
os.makedirs(figdir)
# Save
self.fig.savefig(figfile, **kwargs)
if verbose: print 'Saved plot to '+figfile
self._last_figfile = figfile
# Transfer
if rem: rem.put()
return figfile
[docs] def savefigs(self, figfile, **kwargs):
"""Save a figure to png (and optionaly) pdf files using :func:`~vacumm.misc.plot.savefigs`"""
if figfile is None: return
from vacumm.misc.plot import savefigs
savefigs(figfile, fig=self.fig, **kwargs)
[docs] def show(self, **kwargs):
"""Show the current figure
If the backend does not allow showing the figure using
:func:`matplotlib.pyplot.show`, it uses an external viewer
after saving the figure to a temporary file using
:func:`matplotlib.pyplot.savefig`
"""
kwanim = kwfilter(kwargs, 'anim_')
viewers = {
'pdf':['/usr/bin/kghostview',
'/usr/bin/evince',
'/usr/bin/xpdf',
'/usr/local/bin/acroread'],
'ps':['/usr/bin/kghostview',
'/usr/bin/evince',
'/usr/bin/ghostview'],
'svg':['/usr/bin/svgdisplay',
'/usr/bin/konqueror']}
backend = P.get_backend().lower()
if backend in viewers.keys():
if savefig is None:
tmpfig = mktemp(suffix='.'+backend)
self.fig.savefig(tmpfig, **kwargs)
else:
tmpfig = savefig
for viewer in viewers[backend]:
if os.path.exists(viewer):
cmd = '%s %s' % (viewer,tmpfig)
try:
os.system(cmd)
except:
raise PlotError('Unable to view file with command: '+str(cmd))
if savefig is None: os.remove(tmpfig)
return
else:
# Animation
if self.anim is not False:
dict_check_defaults(kwanim, interval=50, repeat_delay=3000, blit=True)
self.animation = self.animator.make_animation(**kwanim)
# Show
P.show()
[docs] def has_data(self):
"""Guess if object has data"""
return self.data is not None
[docs] def has_valid_data(self):
"""Guess if object has unmasked data"""
return self.data is not None and not self.masked
# Some properties
[docs] def get_anim(self):
"""Get the :attr:`anim` attribute`"""
return getattr(self, '_anim', False)
[docs] def set_anim(self, anim):
"""Set the :attr:`anim` attribute`"""
self._anim = anim
anim = property(get_anim, set_anim, doc="""Is each plot saved for final animation?""")
[docs] def get_animator(self):
"""Get the current :class:`Animator` instance or None"""
if self.anim is False: return
if not hasattr(self, '_animator'):
if not hasattr(self.fig, '_vacumm_animator'):
self.fig._vacumm_animator = Animator(self.fig)
self._animator = self.fig._vacumm_animator
return self._animator
[docs] def set_animator(self, animator):
"""Set the current :class:`Animator` instance"""
self._animator = animator
animator = property(get_animator, set_animator, doc="""Current :class:`Animator` instance or None""")
[docs] def animator_append(self, obj, anim=None):
"""Append an object to current :class:`Animator`"""
if self.fig is None: return
if anim is None: anim = self.anim
if anim is False: return
anim = self.animator.append(obj, anim)
if self.anim is True:
self.anim = anim
return anim
[docs] def register_obj(self, obj, gtypes=None, anim=None, **kwargs):
"""Register an object with :meth:`add_obj` and :meth:`animator_append`"""
if gtypes is None:
gtypes = []
elif not isinstance(gtypes, list):
gtypes = [gtypes]
if 'plotted' not in gtypes:
gtypes.append('plotted')
self.add_obj(gtypes, obj)
self.animator_append(obj, anim=anim)
return obj
[docs] def get_uvlat(self, default=45.):
"""Get :attr:`uvlat`
If a latitude axis is available on X or Y plot axis,
its mean value is used, else it defaults to ``default``
"""
lat = self.get_obj('uvlat')
if self.has_data() and 'y' in self.order:
return [self.y[:], self.x[:]][self.order.index('y')].mean()
return lat if lat is not None else default
[docs] def set_uvlat(self, value):
"""Set :attr:`uvlat`"""
self.set_obj('lat', value)
[docs] def del_uvlat(self):
"""Del :attr:`uvlat`"""
self.del_obj('uvlat')
uvlat = property(get_uvlat, set_uvlat, del_uvlat,
doc="Latitude used for guessing :attr:`uvscaler`")
[docs] def get_uvscaler(self, guess=True, lat=None, raw=False):
"""Get :attr:`uvscaler`
:Params:
- **guess**, optional: Guess scaler from axis types
and data units if not specified.
- **lat**, optional: Latitude value passed to
:meth:`get_metric_scale` to guess plot axis metric scale.
"""
uvscaler = self.get_obj('uvscaler')
if uvscaler is not None:
return self._uvscaler_(uvscaler, raw=raw)
if not guess: return
# Skip data mode
if 'd' in self.order: return
# Guess
if uvscaler is None:
# Latitude
if lat is not None:
self.uvlat = lat
else:
lat = self.uvlat
# Along X
xmscale = self.get_metric_scale('x', lat=lat)
if xmscale is None: return
uunits = getattr(self.data[1], 'units', None)
if uunits is None: return
umscale = tometric(uunits, munits='m/s')
if umscale is None: return
# Along Y
ymscale = self.get_metric_scale('y', lat=lat)
if ymscale is None: return
vunits = getattr(self.data[2], 'units', None)
if vunits is None: return
vmscale = tometric(vunits, munits='m/s')
if vmscale is None: return
# Scale
uvscaler = (vmscale*xmscale*self.x[:].ptp())/(umscale*ymscale*self.y[:].ptp())
# Parse
self.uvscaler = uvscaler
return self._uvscaler_(uvscaler, raw=raw)
@staticmethod
def _uvscaler_(uvscaler, raw=False):
if callable(uvscaler): return uvscaler
if isinstance(uvscaler, (tuple,list)):
_ = 1., uvscaler[1]/uvscaler[0]
if raw: return uvscaler
uvscaler = _
if N.isscalar(uvscaler):
if raw: return uvscaler
return lambda u, v: (u, uvscaler*v)
raise TypeError("uvscaler must be a callable, a scalar or a"+
" 2-element tuple or list")
[docs] def set_uvscaler(self, uvscaler):
"""Set :attr:`uvscaler`"""
self._uvscaler_(uvscaler)
self.set_obj('uvscaler', uvscaler)
[docs] def del_uvscaler(self):
"""Del :attr:`uvscaler`"""
uvscaler = property(get_uvscaler, set_uvscaler,
del_uvscaler, doc="""Function to rescale U anv V data along X and Y axes.
``None`` is returned if no scaling is possible.
:Example:
>>> uvscaler = myplot.uvscaler
>>> if uvscaler is not None:
... u2,v2 = myplot.uvscaler(u, v)
:Return: A callable function or ``None`` if no scaling is possible
""")
[docs] def get_metric_scale(self, xy, lat=None):
"""Get units of X or Y plot axis as meters if possible
Longitude and latitude coordinates are converted using
:func:`~vacumm.phys.units.deg2m`.
else :func:`~vacumm.misc.phys.units.tometric` is used
using axis units.
:Params:
- **xy**: Plot axis type (``"x"``, ``"y"``...).
- **lat**, optional: Latitude for degrees to meter conversion
of longitude coordinates. It default to :attr:`uvlat`.
:Return:
- ``None`` if conversion of possible, else a float value.
"""
# X or Y plot axis
if xy not in ['x', 'y']: return
# From order type
axtype = getattr(self, xy+'type')
if axtype=='x':
if lat is not None:
self.uvlat = lat
else:
lat = self.uvlat
return deg2m(1., lat=lat)
if axtype=='y':
return deg2m(1.)
units = getattr(self, xy+'units', None)
if units is None:
if axtype!='z': return
units = 'm'
#if units.startswith('deg'):
#if getattr(self, xy+'type')=='x':
#return deg2m(1., lat=lat)
#if getattr(self, xy+'type')=='y':
#return deg2m(1.)
return tometric(units, munits='m')
[docs] def get_vmin(self, index=0, glob=False):
"""Get :attr:`vmin`"""
gmin = self.get_obj('vmin')
if gmin is not None: return gmin
# if not self.has_data(): return
if index=='all': # All components (m,u,v)
if not self.has_valid_data():
vmins = [None]*3
else:
vmins = [var.min() for var in self.data]
if glob: # Of all plotters
for b in self.get_brothers(notme=True):
for i, v in enumerate(b.get_min(index='all')):
if v is not None:
vmins[i] = min(vmins[i], v) if vmins[i] is not None else v
else: # Selected component
vmins = [self.data[index].min()] if self.has_valid_data() else []
if glob: # Of all plotters
vmins = vmins+[b.get_min(index=index) for b in self.get_brothers(notme=True)]
vmins = [v for v in vmins if v is not None]
vmins = min(vmins) if len(vmins) else None
return vmins
[docs] def set_vmin(self, value):
"""Set :attr:`vmin`"""
self.set_obj('vmin', value)
[docs] def del_vmin(self):
"""Del :attr:`vmin`"""
self.del_obj('vmin')
vmin = property(get_vmin, set_vmin, del_vmin, doc="Data min to use for plot")
[docs] def get_vmax(self, index=0, glob=False):
"""Get :attr:`vmax`"""
gmax = self.get_obj('vmax')
if gmax is not None: return gmax
# if not self.has_data(): return
if index=='all': # All components (m,u,v)
if not self.has_data() or self.masked:
maxs = [None]*3
else:
maxs = [var.max() for var in self.data]
if glob: # Of all plotters
for b in self.get_brothers(notme=True):
for i, v in enumerate(b.get_max(index='all')):
if v is not None:
vmaxs[i] = max(vmaxs[i], v) if vmaxs[i] is not None else v
else: # Selected component
vmaxs = [self.data[index].max()] if self.has_valid_data() else []
if glob: # Of all plotters
vmaxs = vmaxs+[b.get_max(index=index) for b in self.get_brothers(notme=True)]
vmaxs = [v for v in vmaxs if v is not None]
vmaxs = max(vmaxs) if len(vmaxs) else None
return vmaxs
[docs] def set_vmax(self, value):
"""Set :attr:`vmax`"""
self.set_obj('vmax', value)
[docs] def del_vmax(self):
"""Del :attr:`vmax`"""
self.del_obj('vmax')
vmax = property(get_vmax, set_vmax, del_vmax, doc="Data max to use for plot")
# X/Y MASKED
[docs] def get_xymasked(self):
"""Get :attr:`xymasked`"""
gmasked = self.get_obj('xymasked')
return gmasked is None and True or gmasked
[docs] def set_xymasked(self, value):
"""Set :attr:`xymasked`"""
self.set_obj('xymasked', value)
[docs] def del_xymasked(self):
"""Del :attr:`xymasked`"""
self.del_obj('xymasked')
xymasked = property(get_xymasked, set_xymasked, del_xymasked, doc="Whether X and Y data are not considered if no data at these coordinates")
[docs] def get_xmasked(self):
"""Get :attr:`xmasked`"""
gmasked = self.get_obj('xmasked')
if gmasked is None: gmasked = self.xymasked
return gmasked is None and True or gmasked
[docs] def set_xmasked(self, value):
"""Set :attr:`xmasked`"""
self.set_obj('xmasked', value)
[docs] def del_xmasked(self):
"""Del :attr:`xmasked`"""
self.del_obj('xmasked')
xmasked = property(get_xmasked, set_xmasked, del_xmasked, doc="Whether X data are not considered if no data at these coordinates")
[docs] def get_ymasked(self):
"""Get :attr:`ymasked`"""
gmasked = self.get_obj('ymasked')
if gmasked is None: gmasked = self.xymasked
return gmasked is None and True or gmasked
[docs] def set_ymasked(self, value):
"""Set :attr:`ymasked`"""
self.set_obj('ymasked', value)
[docs] def del_ymasked(self):
"""Del :attr:`ymasked`"""
self.del_obj('ymasked')
ymasked = property(get_ymasked, set_ymasked, del_ymasked, doc="Whether X data are not considered if no data at these coordinates")
# X MIN/MAX
[docs] def get_xmin(self, glob=True, masked=None):
"""Get :attr:`xmin`"""
gmin = self.get_axobj('xmin')
if gmin is not None: return gmin
if self.masked: masked = False
if masked is None: masked = self.xmasked
# if not self.has_data(): return
if self.order[1]=='d':
index = max(0, len(self.data)-2)
xmin = self.get_vmin(index=index)
else:
if not self.has_data():
xmin = None
elif masked:
if self.masked:
xmin = None
else:
xmin = self.get_xdata(masked=masked).min()
else:
xmin = self.get_xdata(masked=False).min()
if glob:
xmins = [] if xmin is None else [xmin]
xmins += [b.get_xmin(glob=False, masked=masked)
for b in self.get_brothers(notme=True)]
xmins = [v for v in xmins if v is not None]
xmin = min(xmins) if len(xmins) else None
return xmin
[docs] def set_xmin(self, value):
"""Set :attr:`xmin`"""
self.set_axobj('xmin', value)
[docs] def del_xmin(self):
"""Del :attr:`xmin`"""
self.del_axobj('xmin')
xmin = property(get_xmin, set_xmin, del_xmin, doc="Min of X axis to use for plot")
[docs] def get_xmax(self, glob=True, masked=None):
"""Get :attr:`xmax`"""
gmax = self.get_axobj('xmax')
if gmax is not None: return gmax
if self.masked: masked = False
if masked is None: masked = self.xmasked
# if not self.has_data(): return
if self.order[1]=='d':
index = max(0, len(self.data)-2)
xmax = self.get_vmax(index=index)
else:
if not self.has_data():
xmax = None
elif masked:
if self.masked:
xmax = None
else:
xmax = self.get_xdata(masked=masked).max()
else:
xmax = self.get_xdata(masked=False).max()
if glob:
xmaxs = [] if xmax is None else [xmax]
xmaxs += [b.get_xmax(glob=False, masked=masked)
for b in self.get_brothers(notme=True)]
xmaxs = [v for v in xmaxs if v is not None]
xmax = max(xmaxs) if len(xmaxs) else None
return xmax
[docs] def set_xmax(self, value):
"""Set :attr:`xmax`"""
self.set_axobj('xmax', value)
[docs] def del_xmax(self):
"""Del :attr:`xmax`"""
self.del_axobj('xmax')
xmax = property(get_xmax, set_xmax, del_xmax, doc="Max of X axis to use for plot")
# Y MIN/XMAX
[docs] def get_ymin(self, glob=True, masked=None):
"""Get :attr:`ymin`"""
gmin = self.get_axobj('ymin')
if gmin is not None: return gmin
if self.masked: masked = False
if masked is None: masked = self.ymasked
# if not self.has_data(): return
if self.order[0]=='d':
index = min(0, len(self.data)-2)
ymin = self.get_vmin(index=index)
else:
if not self.has_data():
ymin = None
elif masked:
if self.masked:
ymin = None
else:
ymin = self.get_ydata(masked=masked).min()
else:
ymin = self.get_ydata(masked=False).min()
if glob:
ymins = [] if ymin is None else [ymin]
ymins += [b.get_ymin(glob=False, masked=masked)
for b in self.get_brothers(notme=True,)]
ymins = [v for v in ymins if v is not None]
ymin = min(ymins) if len(ymins) else None
return ymin
[docs] def set_ymin(self, value):
"""Set :attr:`ymin`"""
self.set_axobj('ymin', value)
[docs] def del_ymin(self):
"""Del :attr:`ymin`"""
self.del_axobj('ymin')
ymin = property(get_ymin, set_ymin, del_ymin, doc="Min of Y axis to use for plot")
[docs] def get_ymax(self, glob=True, masked=None):
"""Get :attr:`ymax`"""
gmax = self.get_axobj('ymax')
if gmax is not None: return gmax
if masked is None: masked = self.ymasked
if self.masked: masked = False
# if not self.has_data(): return
if self.order[0]=='d':
index = max(0, len(self.data)-2)
ymax = self.get_vmax(index=index)
else:
if not self.has_data():
ymax = None
elif masked:
if self.masked:
ymax = None
else:
ymax = self.get_ydata(masked=masked).max()
else:
ymax = self.get_ydata(masked=False).max()
if glob:
ymaxs = [] if ymax is None else [ymax]
ymaxs += [b.get_ymax(glob=False)
for b in self.get_brothers(notme=True)]
ymaxs = [v for v in ymaxs if v is not None]
ymax = max(ymaxs) if len(ymaxs) else None
ymaxs = [ymax]+[b.get_ymax(glob=False, masked=masked)
for b in self.get_brothers(notme=True)]
ymaxs = [v for v in ymaxs if v is not None]
ymax = max(ymaxs) if ymaxs else None
return ymax
[docs] def set_ymax(self, value):
"""Set :attr:`ymax`"""
self.set_axobj('ymax', value)
[docs] def del_ymax(self):
"""Del :attr:`ymax`"""
self.del_axobj('ymax')
ymax = property(get_ymax, set_ymax, del_ymax, doc="Max of Y axis to use for plot")
# X/Y TICKS
[docs] def get_xticks(self, raw):
"""Get X ticks"""
return self.axes.get_xticks()
[docs] def set_xticks(self, ticks):
"""Set X ticks"""
if ticks is False:
ticks = []
elif ticks=='auto':
if not self.has_data():return
if self.order[1]=='t':
ticks = AutoDateLocator2()
ticks = self.auto_scale(self.xmin, self.xmax)
if isinstance(ticks, Locator):
self.axes.xaxis.set_major_locator(ticks)
elif ticks is not None:
self.axes.set_xticks(ticks)
[docs] def del_xticks(self):
"""Del X ticks"""
self.xticks = False
xticks = property(get_xticks, set_xticks, del_xticks, doc="X ticks to use plot")
[docs] def get_yticks(self, glob=False):
"""Get Y ticks"""
return self.axes.get_yticks()
[docs] def set_yticks(self, ticks):
"""Set Y ticks"""
if ticks is False:
ticks = []
elif ticks=='auto':
if not self.has_data():return
if self.order[1]=='t':
ticks = AutoDateLocator2()
ticks = self.auto_scale(self.xmin, self.xmax)
if isinstance(ticks, Locator):
self.axes.xaxis.set_major_locator(ticks)
elif ticks is not None:
self.axes.set_xticks(ticks)
[docs] def del_yticks(self):
"""Del Y ticks"""
self.yticks = False
yticks = property(get_yticks, set_yticks,
del_yticks, doc="X ticks to use for plot")
# Generic attribute management
def _get_xyattr_(self, xy, att, idata=None):
"""Get an attribute of an X/Y axis or current data"""
# # Nothing
# if not self.has_data(): return
# From a variable
if xy=='d' or getattr(self, xy+'type')=='d':
if not self.has_data(): return
if idata is None:
idata = range(len(self.data))
elif not isinstance(idata, (list, tuple)):
idata = [idata]
for i in idata:
if i<0:
i = len(self.data)+i
if len(self.data)>i and hasattr(self.data[i], att):
return getattr(self.data[i], att)
return
# From an axis
return getattr(getattr(self, xy), att, None)
def _set_xyattr_(self, xy, att, value, idata=0):
"""Set an attribute to an X/Y axis or current data"""
# # Nothing
# if not self.has_data(): return
# Del
if value is False:
self._del_xyattr_(xy, att)
# Variable or axis?
if xy=='d' or getattr(self, xy+'type')=='d':
if not self.has_data():
return
target = self.data[idata]
else:
target = getattr(self, xy)
# setattr(self.data[idata], att, value)
# Set
setattr(target, att, value)
def _del_xyattr_(self, xy, att, idata=0):
"""Del an attribute from an X/Y axis or current data"""
# Nothing
if not self.has_data(): return
# To a variable
if xy=='d' or getattr(self, xy+'type')=='d':
if not self.has_data():
return
target = self.data[idata]
else:
target = getattr(self, xy)
# Del
if hasattr(target, att):
delattr(target, att)
# ID / SHORT NAME
[docs] def get_id(self, idata=None):
"""Get :attr:`id`"""
return self._get_xyattr_('d', 'id', idata=idata)
[docs] def set_id(self, id=None):
"""Set :attr:`id`"""
self._set_xyattr_('d', 'id', id)
[docs] def del_id(self):
"""Del :attr:`id`"""
self._del_xyattr_('d', 'id')
id = property(get_id, set_id,
del_id, 'Current data id')
[docs] def get_xid(self):
"""Get :attr:`xid`"""
return self._get_xyattr_('x', 'id')
[docs] def set_xid(self, id=None):
"""Set :attr:`xid`"""
self._set_xyattr_('x', 'id', id)
[docs] def del_xid(self):
"""Del :attr:`xid`"""
self._del_xyattr_('x', 'id')
xid = property(get_xid, set_xid,
del_xid, 'Current id of X axis')
[docs] def get_yid(self):
"""Get :attr:`yid`"""
return self._get_xyattr_('y', 'id')
[docs] def set_yid(self, id=None):
"""Set :attr:`yid`"""
self._set_xyattr_('y', 'id', id)
[docs] def del_yid(self):
"""Del :attr:`yid`"""
self._del_xyattr_('y', 'id')
yid = property(get_yid, set_yid,
del_yid, 'Current id of Y axis')
# LONG_NAME
[docs] def get_long_name(self, idata=None):
"""Get :attr:`long_name`"""
long_name = self._get_xyattr_('d', 'long_name', idata=idata)
id = self.get_id(idata=idata)
if long_name is None and id:
long_name = id.title().replace('_', ' ')
return long_name
[docs] def set_long_name(self, long_name=None):
"""Set :attr:`long_name`"""
self._set_xyattr_('d', 'long_name', long_name)
[docs] def del_long_name(self):
"""Del :attr:`long_name`"""
self._del_xyattr_('d', 'long_name')
long_name = property(get_long_name, set_long_name,
del_long_name, 'Current long name')
[docs] def get_xlong_name(self):
"""Get :attr:`xlong_name`"""
long_name = self._get_xyattr_('x', 'long_name')
if long_name is None:
long_name = self.get_xid().title().replace('_', ' ')
return long_name
[docs] def set_xlong_name(self, long_name=None):
"""Set :attr:`xlong_name`"""
self._set_xyattr_('x', 'long_name', long_name)
[docs] def del_xlong_name(self):
"""Del :attr:`xlong_name`"""
self._del_xyattr_('x', 'long_name')
xlong_name = property(get_xlong_name, set_xlong_name,
del_xlong_name, 'Current long_name of X axis')
[docs] def get_ylong_name(self):
"""Get :attr:`ylong_name`"""
long_name = self._get_xyattr_('y', 'long_name')
if long_name is None:
long_name = self.get_yid().title().replace('_', ' ')
return long_name
[docs] def set_ylong_name(self, long_name=None):
"""Set :attr:`ylong_name`"""
self._set_xyattr_('y', 'long_name', long_name)
[docs] def del_ylong_name(self):
"""Del :attr:`ylong_name`"""
self._del_xyattr_('y', 'long_name')
ylong_name = property(get_ylong_name, set_ylong_name,
del_ylong_name, 'Current long_name of Y axis')
# UNITS
[docs] def get_units(self, idata=None):
"""Get :attr:`units`"""
units = self._get_xyattr_('d', 'units', idata)
if not isinstance(units, basestring):
return
if units[0]=='$' and units[-1]=='$':
units = units[1:-1]
if not self.isset('latex_units'):
self.latex_units = True
return units
[docs] def set_units(self, units=None):
"""Set :attr:`units`"""
self._set_xyattr_('d', 'units', units)
[docs] def del_units(self):
"""Del :attr:`units`"""
self._del_xyattr_('d', 'units')
units = property(get_units, set_units, del_units, 'Current units')
[docs] def get_xunits(self):
"""Get :attr:`xunits`"""
return self._get_xyattr_('x', 'units')
[docs] def set_xunits(self, units=None):
"""Set :attr:`xunits`"""
self._set_xyattr_('x', 'units', units)
[docs] def del_xunits(self):
"""Del :attr:`xunits`"""
self._del_xyattr_('x', 'units')
xunits = property(get_xunits, set_xunits,
del_xunits, 'Current units of X axis')
[docs] def get_yunits(self):
"""Get :attr:`yunits`"""
return self._get_xyattr_('y', 'units')
[docs] def set_yunits(self, units=None):
"""Set :attr:`yunits`"""
self._set_xyattr_('y', 'units', units)
[docs] def del_yunits(self):
"""Del :attr:`yunits`"""
self._del_xyattr_('y', 'units')
yunits = property(get_yunits, set_yunits,
del_yunits, 'Current units of Y axis')
# INTERPRET UNITS WITH LATEX?
[docs] def get_latex_units(self):
"""Get :attr:`latex_units`"""
return self.get_obj('latex_units') or False
[docs] def set_latex_units(self, value):
"""Set :attr:`units`"""
self.set_obj('latex_units', value)
[docs] def del_latex_units(self):
"""Del :attr:`units`"""
self.del_obj('latex_units')
latex_units = property(get_latex_units, set_latex_units, del_latex_units, 'Interpret units with latex')
re_latex_match = re.compile(r'\$.+\$').match
[docs] def is_latex(self, text):
"""Is this text formatted as latex formula ("$...$")?"""
return self.re_latex_match(text)
# TITLE OF THE PLOT
def _strfill_(self, strpat, name='pattern'):
"""Try to fill strpat with self.sndict() or return strpat with not filling"""
try:
return strpat % self.sndict()
except:
# warn("Error when filling %(name)s (skipping): %(strpat)s"%locals())
return strpat
[docs] def get_title(self):
"""Get :attr:`title`"""
title = self.get_axobj('title')
if title is False: return False
if title == '_auto_': title = True
if isinstance(title, basestring):
return self._strfill_(title, 'title pattern')
return self.long_name
[docs] def set_title(self, title=None):
"""Set attr:`title`
.. note:: If set to ``False``, the title is not plotted.
"""
self.set_axobj('title', title)
[docs] def del_title(self):
"""Del attr:`title`"""
self.del_axobj('title')
title = property(get_title, set_title,
del_title, 'Preformed title to use for the plot. '
'It may be formed as a template using other attributes '
'like :attr:`long_name`, :attr:`units`, :attr:`xmin`, etc.')
# LABELS
[docs] def get_label(self):
"""Get :attr:`label`"""
label = self.get_axobj('label')
if label is False: return
if label is not None:
return self._strfill_(label, 'label pattern')
return self.long_name
[docs] def set_label(self, label=None):
"""Set :attr:`label`
.. note:: If set to ``False``, the label is not plotted.
"""
self.set_axobj('label', label)
[docs] def del_label(self):
"""Del :attr:`label`"""
self.del_axobj('label')
label = property(get_label, set_label,
del_label, 'Preformed label to use for the plot. '
'It may be formed as a template using other attributes '
'like :attr:`long_name`, :attr:`units`, :attr:`xmin`, etc.')
[docs] def get_xlabel(self):
"""Get :attr::`xlabel`"""
label = self.get_axobj('xlabel')
# label = getattr(self, '_xlabel', None)
if label is False: return ''
if label is None or label is True:
if self.order[1]=='d':
label = self.get_fmt_lnu(long_name=False)
elif label is True or self.order[1]=='-':
label = self.get_fmt_lnu(prefix='x')
else:
label = ''
return self._strfill_(label, 'xlabel pattern')
[docs] def set_xlabel(self, label):
"""set :attr::`xlabel`"""
self.set_axobj('xlabel', label)
# self._xlabel = label
[docs] def del_xlabel(self):
"""Del :attr::`xlabel`"""
self.del_axobj('xlabel')
# if hasattr(self, '_xlabel'): del self._xlabel
xlabel = property(get_xlabel, set_xlabel,
del_xlabel, 'Label of X axis. '
'It may be formed as a template using other attributes '
'like :attr:`long_name`, :attr:`units`, :attr:`xmin`, etc.')
[docs] def get_ylabel(self):
"""Get :attr::`ylabel`"""
label = self.get_axobj('ylabel')
# label = getattr(self, '_ylabel', None)
if label is False: return ''
if label is None or label is True:
if self.order[0]=='d':
label = self.get_fmt_lnu(long_name=False)
elif label is True or self.order[0]=='-':
label = self.get_fmt_lnu(prefix='y')
else:
label = ''
return self._strfill_(label, 'ylabel pattern')
[docs] def set_ylabel(self, label):
"""Set :attr::`ylabel`"""
self.set_axobj('ylabel', label)
# self._ylabel = label
[docs] def del_ylabel(self):
"""Del :attr::`ylabel`"""
self.del_axobj('ylabel')
# if hasattr(self, '_ylabel'): del self._ylabel
ylabel = property(get_ylabel, set_ylabel,
del_ylabel, 'Label of Y axis. '
'It may be formed as a template using other attributes '
'like :attr:`long_name`, :attr:`units`, :attr:`xmin`, etc.')
[docs] def get_fmt_lnu(self, prefix='', fmtln='%(long_name)s', fmtu='%(units)s',
fmtlnu='%(long_name)s [%(units)s]', long_name=True, units=True):
"""Format long_name and units as string according to their availability
:Params:
- **prefix**, optional: Prefix of the attributes
:Example:
>>> myplot.get_fmt_lnu()
'%(long_name)s [%(units)s]'
>>> myplot.get_fmt_lnu(prefix='x', fmtlnu='%(long_name)s [%(units)s]')
'%(xlong_name)s [%(xunits)s]'
>>> myplot.get_fmt_lnu(long_name=False)
'%(xunits)s'
"""
if getattr(self, prefix+'long_name') is None: long_name = False
if getattr(self, prefix+'units') is None: units = False
if long_name and units:
fmt = fmtlnu
elif long_name:
fmt = fmtln
elif units:
fmt = fmtu
else:
fmt=''
long_name = '%%(%slong_name)s'%prefix
units = '%%(%sunits)s'%prefix
return fmt%locals()
# DICTIONARY OF ATTRIBUTES
[docs] def dict(self, *keys, **items):
"""Get attributes as a dictionary
.. note::
:attr:`units` is treated in a special way.
If :attr:`latex_units` is ``True``, it is formatted
as ``$<units>$``.
"""
if len(keys)==0:
keys = list(self._primary_attributes)
dd = {}
for key in keys:
dd[key] = getattr(self, key)
items = dict([(key, val) for (key, val) in items.items() if val is not None])
dd.update(items)
if self.latex_units and 'units' in dd and not self.is_latex(dd['units']):
dd['units'] = '$%(units)s$'%dd
return dd
[docs] def sndict(self, *keys, **items):
"""Get attributes as a dictionary of strings or numbers"""
dd = self.dict(*keys, **items)
for key, val in dd.items():
if val is None or val is False or val is True:
dd[key] = ''
return dd
[docs] def sdict(self, *keys, **items):
"""Get attributes as a dictionary of strings"""
dd = self.sndict(*keys, **items)
for key, val in dd.items():
val = str(val)
return dd
# ADVANCED GRAPHICAL METHODS
[docs]class Plot1D(Plot):
_order = ['zd', 'd-', '-d']
rank = 1
def _check_order_(self, vertical=None, **kwargs):
"""Check order of data
:Params:
- **vertical**, optional: Plot vertically.
:Sea also: :meth:`Plot._check_order_`
"""
# Force vertical plot
if vertical is True:
self._order = [o for o in self._order if o.startswith('d') ]
elif vertical is False:
self._order = [o for o in self._order if not o.startswith('d')]
# Old stuff
return Plot._check_order_(self, **kwargs)
# check_order.__doc__ = Plot.check_order.__doc__
def _set_axes_(self, axis=None, axisatts=None, **kwargs):
"""Get/set used axes
:Params:
- **axis**, optional: Change plot axis.
"""
# Get axis and adjust order
if axis is not None:
if not isaxis(axis):
axis = cdms2.createAxis(axis)
if self.has_data():
cp_atts(self.data[0].getAxis(0), axis, overwrite=False)
elif self.has_data(): # adjust order
orders = [var.getOrder() for var in self.data]
for ivar, var in enumerate(self.data):
c = getattr(axis, 'axis', '-').lower()
if c!='-' or orders[ivar][0]=='-':
set_order(var, c)
elif self.has_data():
axis = self.data[0].getAxis(0)
else:
raise PlotError('No axis data available for this plot')
# Change attributes
if axisatts is not None:
set_atts(axis, axisatts)
# Set X axis by default
self.x = axis
self.y = None
# _set_axes_.__doc__ = Plot._set_axes_.__doc__
# Restore order
[docs] def get_axis_data(self, **kwargs):
"""Return data of the axis"""
if not self.has_data(): return
if self.xtype=='d': return self.get_ydata(**kwargs)
return self.get_xdata(**kwargs)
[docs]class ScalarMappable:
"""Abstract class for adding scalar mappable utilities
:Attribute params:
- **levels**, optional: Levels to use for contours or colorbar ticks.
"They can be specified as a single value, a list or array, or "
"as a tuple used as argument to :func:`numpy.arange`.
It sets the attribute :attr:`~vacumm.misc.core_plot.ScalarMappable.levels`.
- **nmax_levels**, optional: Maximal number of levels when
:attr:`~vacumm.misc.core_plot.ScalarMappable.levels` are computed automatically.
It sets the attribute :attr:`~vacumm.misc.core_plot.ScalarMappable.nmax_levels`.
- **nmax**, optional: Same as **nmax_levels**.
It sets the attribute :attr:`~vacumm.misc.core_plot.ScalarMappable.keepminmax`.
- **cmap**, optional: Colormap name (see :func:`vacumm.misc.color.get_cmap`).
If not specified, it is taken from
config section ``[vacumm.misc.plot]`` and config option ``cmap``, as a string
that defaults to ``magic``.
- **levels_mode**, optional: Mode of computing levels if needed.
It can be specified at initialisation with
attribute :attr:`~vacumm.misc.core_plot.ScalarMappable.levels_mode`.
If not specified, it it taken from the config section
``[vacumm.misc.plot]`` and config option ``levels_mode``.
- ``"symetric"``: Min and max are set opposite.
- ``"positive"``: Min is set to 0.
- ``"negative"``: Max is set to 0.
- ``"auto"``: If abs(min) and abs(max) are close,
``"symetric"`` is assumed. If min and max are > 0,
``"positive"`` is assumed, and the reverse for
``"negative"``.
- **keepminmax**, optional:
It can be specified at initialisation with
attribute :attr:`~vacumm.misc.core_plot.ScalarMappable.keepminmax`.
If not specified, it it taken from the config section
``[vacumm.misc.plot]`` and config option ``keepminmax``.
If False or 0, adjust
:attr:`~vacumm.misc.core_plot.ScalarMappable.vmin` and
:attr:`~vacumm.misc.core_plot.ScalarMappable.vmax`
to first and last values of :attr:`~vacumm.misc.core_plot.ScalarMappable.levels`;
if 1, do not change :attr:`~vacumm.misc.core_plot.ScalarMappable.vmin`,
:attr:`~vacumm.misc.core_plot.ScalarMappable.vmax` and :attr:`levels`
if 2, adjust first and last values of :attr:`levels` or
:attr:`~vacumm.misc.core_plot.ScalarMappable.vmin`,
:attr:`~vacumm.misc.core_plot.ScalarMappable.vmax`.
It sets the attribute :attr:`~vacumm.misc.core_plot.ScalarMappable.cmap`.
- **cblabel**, optional: Preformed label of the colorbar.
It may be formed as a template using other attributes
like :attr:`long_name`, :attr:`~vacumm.misc.core_plot.Plot.units`,
:attr:`xmin`, etc. Example: ``"%(long_name)s [%(units)s]"``.
"""
_primary_attributes = Plot._primary_attributes + ['nmax', 'nmax_levels',
'levels_mode', 'levels', 'cmap', 'keepminmax']
_secondary_attributes = Plot._secondary_attributes + ['cblabel']
[docs] def get_nmax_levels(self):
"""Get :attr:`nmax_levels`"""
nmax = self.get_obj('nmax_levels')
if nmax is not None: return nmax
return int(get_config_value('vacumm.misc.plot', 'nmax_levels'))
[docs] def set_nmax_levels(self, value):
"""Set :attr:`nmax_levels`"""
self.set_obj('nmax_levels', value)
[docs] def del_nmax_levels(self):
"""Del :attr:`nmax_levels`"""
self.del_obj('nmax_levels')
nmax_levels = nmax = property(get_nmax_levels, set_nmax_levels,
del_nmax_levels, doc="Max number of :attr:`levels` for contours and colorbar ticks.")
[docs] def get_levels(self, mode=None, keepminmax=None, nocache=False,
autoscaling='normal', **kwargs):
"""Get :attr:`levels` for contours and colorbar ticks
:Params:
- **mode**, optional: Mode of computing levels if needed.
It can be specified at initialisation with
attribute :attr:`levels_mode`.
If not specified, it it taken from the config section
``[vacumm.misc.plot]`` and config option ``levels_mode``.
- ``"normal"``: Min and max are not preprocessed.
- ``"symetric"``: Min and max are set opposite.
- ``"positive"``: Min is set to 0.
- ``"negative"``: Max is set to 0.
- ``"auto"``: If abs(min) and abs(max) are close,
``"symetric"``is assumed. If min and max are > 0,
``"positive"`` is assumed, and the reverse for
``"negative"``.
- **keepminmax**, optional:
It can be specified at initialisation with
attribute :attr:`keepminmax`.
If not specified, it it taken from the config section
``[vacumm.misc.plot]`` and config option ``keepminmax``.
If False or 0, adjust
:attr:`vmin` and :attr:`vmax` to first and last values of :attr:`levels`;
if 1, do not change :attr:`vmin`, :attr:`vmax` and :attr:`levels`
if 2, adjust first and last values of :attr:`levels` or
:attr:`vmin`, :attr:`vmax`.
- **nocache**, optional: Once levels are computed, they are stored
in cache. If ``nocache is True``, first check cache before
trying to compute levels.
- **autoscaling**, optional: Autoscaling mode.
- ``"normal"``: Use :func:`~vacumm.misc.misc.auto_scale`.
- ``"degrees"``: Use :func:`~vacumm.misc.misc.geo_scale`.
- `A callable: Use it to auto scale. It should accept
the follwing keywords: vmin, vmax, nmax, keepminmax.
"""
# Cache
levels = self.get_obj('levels')
if levels is not None and not isinstance(levels, str):
if not hasattr(levels, '__len__'):
levels = N.asarray([levels])
elif isinstance(levels, tuple):
levels = N.arange(*levels[:3]).astype('d')
self._levels = levels
return levels
if hasattr(self, '_levels') and not nocache: return self._levels
# Inits
if isinstance(levels, str):
mode = levels
if mode is not None:
self.levels_mode = mode
else:
mode = self.levels_mode
if not self.has_data(): return
if keepminmax is not None:
self.keepminmax = keepminmax
keepminmax = self.keepminmax
# Min and max
for key in 'positive', 'negative', 'symetric', 'anomaly':
if kwargs.has_key(key) and kwargs[key]:
mode = key
vmin = self.vmin if self.isset('vmin') else None
vmax = self.vmax if self.isset('vmax') else None
if mode == 'auto':
mode = self.minmax2levelsmode(self.vmin, self.vmax)
self.levels_mode = mode
if mode=='positive':
vmin = 0.
elif mode=='negative':
vmax = 0.
elif mode=='symetric':
vmax = max(N.abs(self.vmin), N.abs(self.vmax))
vmin = -vmax
# Compute base levels
assert autoscaling in ['normal', 'degrees'] or callable(autoscaling), 'Wrong autoscaling parameter'
if autoscaling=='normal':
autoscaling = auto_scale
elif autoscaling=='degrees':
autoscaling = geo_scale
levels = autoscaling((self.vmin, self.vmax), vmin=vmin, vmax=vmax,
nmax=self.nmax_levels, keepminmax=keepminmax==2)
# Change min and max
if not keepminmax:
del self.vmin, self.vmax
# Cache
self._levels = levels
return levels
[docs] def minmax2levelsmode(self, vmin=None, vmax=None):
"""Get auto levels mode from min and max value
:Return: normal, symetric, positive or negative"""
if vmin is None:
vmin = self.vmin
if vmax is None:
vmax = self.vmax
if self.masked:
mode = 'normal'
elif (vmin>=0 and vmax > 0) and (vmin<=vmax/3.):
mode = 'positive'
elif (vmin<0 and vmax<=0) and (vmax>=vmin/3.):
mode = 'negative'
elif (vmin+vmax)< 0.05 * (vmax-vmin):
mode = 'symetric'
else:
mode = 'normal'
return mode
[docs] def set_levels(self, value):
"""Set :attr:`levels`"""
self.set_obj('levels', value)
[docs] def del_levels(self):
"""Del :attr:`levels`"""
self.del_obj('levels')
levels = property(get_levels, set_levels,
del_levels, doc="Levels to use for contours or colorbar ticks. "
"They can be specified as a single value, a list or array, or "
"as a tuple used as argument to :func:`numpy.arange`.")
[docs] def get_keepminmax(self):
"""Get :attr:`keepminmax`"""
keepminmax = self.get_obj('keepminmax')
if keepminmax is None:
keepminmax = get_config_value('vacumm.misc.plot', 'keepminmax')
try:
keepminmax = int(keepminmax)
except:
raise PlotError('Error with keepminmax: %s'%keepminmax)
return keepminmax
[docs] def set_keepminmax(self, value):
"""Set :attr:`keepminmax`"""
self.set_obj('keepminmax', value)
[docs] def del_keepminmax(self):
"""Del :attr:`keepminmax`"""
self.del_obj('keepminmax')
keepminmax = property(get_keepminmax, set_keepminmax,
del_keepminmax, doc="Do not adjust :attr:`vmin` and :attr:`vmax` when setting :attr:`levels`. If 0, :attr:`vmin` and :attr:`vmax` are set to first and last :attr:`levels`; if 1, they are not adjested to :attr:`levels` ; if 2, first and last :attr:`levels` are adjusted to :attr:`vmin` and :attr:`vmax`.")
[docs] def get_levels_mode(self):
"""Get :attr:`levels_mode`"""
levels_mode = self.get_obj('levels_mode')
if levels_mode is None:
levels_mode = get_config_value('vacumm.misc.plot', 'levels_mode')
levels_mode = self._check_levels_mode_(levels_mode)
return levels_mode
[docs] def set_levels_mode(self, mode):
"""Set :attr:`levels_mode`"""
mode = self._check_levels_mode_(mode)
self.set_obj('levels_mode', mode)
[docs] def del_levels_mode(self):
"""Del :attr:`levels_mode`"""
self.del_obj('levels_mode')
levels_mode = property(get_levels_mode, set_levels_mode,
del_levels_mode, doc="The way :attr:`levels` are estimated from "
":attr:`vmin` and :attr:`vmax`: 'positive'/'negative' means levels "
"starting/ending from 0, 'anomaly' or 'symetric' means symetric "
"levels, 'auto' or 'smart' means that mode is estimated "
"from min and max, and 'normal' means no special treatment.")
def _check_levels_mode_(self, mode):
"""Check values and aliases, and fallback to config value"""
mode = str(mode).lower()
if mode not in ['auto', 'smart', 'normal', 'basic', 'positive',
'negative', 'symetric', 'anomaly']:
oldmode = mode
mode = get_config_value('vacumm.misc.plot', 'levels_mode', user=False)
warn('Bad value for config value [vacumm.misc.plot] levels_mode: %s. Switched to default: %s'%(oldmode, mode))
if mode=='anomaly':
mode = 'symetric'
elif mode=='smart':
mode = 'auto'
elif mode=='basic':
mode = 'normal'
return mode
[docs] def get_vmin(self, index=0, glob=False):
"""Get :attr:`vmin`"""
gmin = self.get_obj('vmin')
if gmin is not None: return gmin
if not self.has_data(): return
if index==0 and hasattr(self, '_levels'):
mins = self._levels[0]
if glob: # Of all plotters
mins = max([mins]+[b.min for b in self.get_brothers(notme=True)])
return mins
return Plot.get_vmin(self, index=index, glob=glob)
vmin = property(get_vmin, Plot.set_vmin,
Plot.del_vmin, doc="Data min to use for plot")
[docs] def get_vmax(self, index=0, glob=False):
"""Get :attr:`vmax`"""
gmax = self.get_obj('vmax')
if gmax is not None: return gmax
if not self.has_data(): return
if index==0 and hasattr(self, '_levels'):
maxs = self._levels[-1]
if glob: # Of all plotters
maxs = max([maxs]+[b.max for b in self.get_brothers(notme=True)])
return maxs
return Plot.get_vmax(self, index=index, glob=glob)
vmax = property(get_vmax, Plot.set_vmax,
Plot.del_vmax, doc="Data max to use for plot")
@staticmethod
def _get_config_cmap_(key='cmap'):
cmap = get_config_value('vacumm.misc.plot', key)
if cmap is None:
cmap = get_config_value('vacumm.misc.plot', key, user=False)
if cmap is None or cmap.lower() in ['none', 'mpl', 'default',
'normal', 'true', 'false']:
cmap = None # default from matplotlib
return cmap
[docs] def get_cmap(self, cmap=None, nocache=False, tint=None, lum=None, sat=None,
pastel=False, **kwargs):
"""Get :attr:`cmap`
:Params:
- **cmap**, optional: Colormap name (see :func:`vacumm.misc.color.get_cmap`).
It defaults to ``"magic"``.
- **nocache**, optional: Once cmap is computed, it is stored
in cache. If ``nocache is True``, first check cache before
trying to compute cmap.
- **lum**, optional: Change luminosity, between -1 and 1.
- **sat**, optional: Change saturation.
"""
# From cache
cmap = self.get_obj('cmap')
if (cmap is None and not nocache and hasattr(self, '_cmap') and
tint is None and lum is None):
cmap = self._cmap
# From config
if cmap is None or cmap is True :
cmap = self._get_config_cmap_()
# Aliases
if cmap == 'mpl':
cmap = False
elif cmap=='mg':
cmap = 'magic'
elif cmap=='rb':
cmap = 'rainbow'
elif cmap == 'normal' or cmap is True or cmap is False:
cmap = None
# Adaptative choices
cmap_levels = ['positive', 'negative', 'symetric', 'anomaly']
if cmap in ['auto', 'magic', 'rainbow'] + cmap_levels:
# Levels mode
if cmap in cmap_levels:
levels_mode = self._check_levels_mode_(cmap)
cmap = 'auto'
else:
levels_mode = self.levels_mode
if levels_mode == 'auto':
levels_mode = self.minmax2levelsmode(self.vmin, self.vmax)
if cmap=='auto':
if levels_mode == 'normal':
cmap = None
elif levels_mode == 'symetric':
cmap = self._get_config_cmap_('cmap_symetric')
if cmap is None:
cmap = CMAP_SYMETRIC
elif levels_mode == 'positive':
cmap = self._get_config_cmap_('cmap_positive')
if cmap is None:
cmap = CMAP_POSITIVE
else:
cmap = self._get_config_cmap_('cmap_negative')
if cmap is None:
cmap = CMAP_NEGATIVE
cmap = get_cmap(cmap, errmode='warn')
elif cmap=='magic' or cmap=='rainbow':
if cmap=='magic':
kwargs.setdefault('mode', levels_mode)
kwargs.setdefault('stretcher', 'reduced_green')
if getattr(self, 'fill_method', None)=='contourf' or getattr(self, 'fill', '')=='contourf':
dict_check_defaults(kwargs, stretch=0, lstretch=0, rstretch=0)
cmap = getattr(color, 'cmap_'+cmap)(self.levels, **kwargs)
elif not isinstance(cmap, Colormap):
cmap = get_cmap(cmap, errmode='warn', **kwargs)
# Luminosity
if lum is None:
lum = .5
if tint is not None:
lum = tint*.5 + .5
if lum != 0.5:
cmap = change_luminosity(cmap, lum)
# Saturation
if sat is not None:
cmap = change_saturation(cmap, sat)
# Pastelisation
if pastel:
cmap = pastelise(cmap)
self._cmap = cmap
return cmap
[docs] def set_cmap(self, cmap):
"""Set :attr:`cmap`"""
self.set_obj('cmap', cmap)
[docs] def del_cmap(self, cmap):
"""Del :attr:`cmap`"""
self.del_obj('cmap')
cmap = property(get_cmap, set_cmap,
del_cmap, doc="Colomap to use for filled plots and colorbar.")
[docs] def get_cblabel(self):
"""Get :attr:`cblabel`"""
label = getattr(self, '_cblabel', None)
if label is None or label is True:
label = self.get_fmt_lnu(long_name=False)
if not isinstance(label, basestring): return ''
return self._strfill_(label, 'cblabel pattern')
[docs] def set_cblabel(self, label):
"""Set :attr:`cblabel`"""
self._cblabel = label
[docs] def del_cblabel(self):
"""Del :attr:`cblabel`"""
if hasattr(self, '_cblabel'): del self._cblabel
cblabel = property(get_cblabel, set_cblabel,
del_cblabel, doc='Preformed label of the colorbar. '
'It may be formed as a template using other attributes '
'like :attr:`long_name`, :attr:`units`, :attr:`xmin`, etc.')
[docs] def get_scalar_mappable(self):
"""Get the current scalar mappable or ``None``
It is useful for instance for :meth:`colorbar`.
.. note:
The scalar mappable is search for in current instance only.
"""
return self.get_obj('scalar_mappable')
get_sm = get_scalar_mappable
[docs] def get_colorbar(self):
"""Get the current colorbar object """
cb = self.get_obj('colorbar')
if cb is not None:
return cb
# def get_colorbars(self):
# """Get the colorbar of all brothers"""
# return [b.get_colorbar()
# for b in self.get_brothers(notme=False, mefirst=True)
# if cb is not None]
[docs] def colorbar(self, cax=None, fit=False, ticklabels_nmax=12, **kwargs):
"""Add a colorbar
The colorbar is drawn only if :meth:`get_scalar_mappable` returns a valid
scalar mappable.
:Params:
- **cax**, optional: Axes for the colorbar.
- **label_<param>**, optional: <param> is passed to
:meth:`~matplotlib.colorbar.Colorbar.set_label`.
- **ticklabels_<param>**, optional: <param> is set as a property
of tick labels.
- Other keywords are passed to the :meth:`matplotlib.figure.Figure.colorbar`
method.
:See also: :meth:`get_colorbar` :meth:`get_scalar_mappable`
"""
# Get scalar mappable
sm = self.get_scalar_mappable()
if sm is None: return
# Check if already plotted
cb = self.get_colorbar()
kwcmap = kwfilter(kwargs, 'cmap')
if cb is not None: # Update
cb.set_cmap(sm.get_cmap(**kwcmap))
cb.set_clim(sm.get_clim())
cb.update_normal(sm)
if self.levels is not None:
cb.set_ticks(self.levels)
return cb
# Plot
kwtl = kwfilter(kwargs, 'ticklabels')
kwl = kwfilter(kwargs, 'label')
# - levels
levels = self.levels
if levels is not None:
kwargs.setdefault('ticks', levels)
# - extend
if kwargs.get('extend', None) is None:
kwargs['extend'] = self._get_extend_(sm.get_clim())
# - axes
if isinstance(cax, list):
cax = self.fig.add_axes(cax)
# - plot it
# print kwargs
# xxx
cb = self.fig.colorbar(sm, ax=self.axes, cax=cax, **kwargs)
# - fit to axes limits
if fit:
axbbox = self.axes.get_position()
caxbbox = cb.ax.get_position()
print 'avant', axbbox, caxbbox
if cb.orientation=='horizontal':
cb.ax.set_position([axbbox.x0, caxbbox.y0, axbbox.width, caxbbox.height])
else:
cb.ax.set_position([caxbbox.x0, axbbox.y0, caxbbox.width, axbbox.height])
print 'apres', cb.ax.get_position()
# - ticks
if levels is not None and 'format' not in kwargs:
samp = len(levels)/ticklabels_nmax+1
if samp>1:
levels = list(levels)
for i in xrange(len(levels)):
if i%samp:
levels[i] = ''
labels = cb.set_ticklabels(levels)
if kwtl:
axis = getattr(cb.ax, ('x' if cb.orientation=='horizontal' else 'y')+'axis')
P.setp(axis.get_ticklabels(), **kwtl)
# - label
label = self.cblabel
if label is not None:
cb.set_label(label, **kwl)
return self.set_obj('colorbar', cb)
def _get_extend_(self, clim):
"""clim: sm.get_clim() or self.levels"""
cmin = min(clim)
cmax = max(clim)
data = self.get_data(scalar=True)
vmin = data.min()
vmax = data.max()
if cmin>vmin and cmax<vmax:
return 'both'
if cmin>vmin:
return 'min'
if cmax<vmax:
return 'max'
return 'neither'
[docs] def post_plot(self, colorbar=True, savefig=None, savefigs=None, show=True, close=False, **kwargs):
"""
:Params:
- **colorbar**, optional: Plot the colorbar.
- **colorbar_<param>**, optional: ``<param>`` is passed to
:meth:`~vacumm.misc.core_plot.ScalarMappable.colorbar`.
"""
# Keywords
kw = {}
for kwtype in 'savefig', 'savefigs', 'show', 'colorbar':
kw[kwtype] = kwfilter(kwargs, kwtype+'_')
# Base stuff
kwargs.update(savefig=None, savefigs=None, show=False)
Plot.post_plot(self, **kwargs)
# Colorbar
if 'extend' in kwargs:
kw['colorbar'].setdefault('extend', kwargs.get('extend'))
if colorbar: self.colorbar(**kw['colorbar'])
# Save it
self.savefig(savefig, **kw['savefig'])
self.savefigs(savefigs, **kw['savefigs'])
# Show or close
if show:
self.show(**kw['show'])
elif close:
P.close(self.fig)
[docs]class Curve(Plot1D):
"""Class for plotting simple curve
:Params:
- **data**: Data to plot. It may be a single variable or tuple.
It a tuple is passed, here are the possible forms:
- ``(M,)``: Simple scalar.
- ``(U,V)``: Vector coordinates and the modulus is plotted.
- ``(M,U,V)``: Modulus and vector coordinates and
only the modulus is used and plotted.
- **parg**, optional: Argument passed to :func:`~matplotlib.pyplot.plot`
after the data.
- **Specific plot params**: See :meth:`plot`.
- **Other generic params**: See :class:`Plot`.
:Example:
>>> c = Curve(mydata, xmin=3, plot=False)
>>> c.ymax = 5.
>>> c.plot('-r')
>>> c.post_plot(savefig='toto.png')
"""
[docs] def load_data(self, *args, **kwargs):
"""Check order of data
:Params:
- **data**: A :mod:`MV2` 1D array.
:See also: :meth:`Plot.load_data`
"""
return Plot.load_data(self, *args, **kwargs)
[docs] def plot(self, parg=None, nosingle=False, label=None, err=None, fill_between=False,
shadow=False, glow=False, log=False, **kwargs):
"""Plot of data as a curve
:Params:
- **parg**, optional: Argument passed to :func:`~matplotlib.pyplot.plot`
after the data.
- **nosingle**, optional: Single point with missing data around
are not plotted.
- **fill_between**, optional: Plot curve using
:func:`~matplotlib.pyplot.fill_between`.
Reference value defaults to 0. and may be given
provided by the parameter.
- **fill_between_<param>**, optional: ``<param>`` is passed to
:func:`~matplotlib.pyplot.fill_between`.
- **err**, optional: Errors as ``(2,nx)`` array to add to the plot
using :func:`~matplotlib.pyplot.errorbar`.
- **err_<param>**, optional: ``<param>`` is passed to
:func:`~matplotlib.pyplot.errorbar`.
- **label**, optional: Alternative label for the plot
(see also :attr:`~vacumm.misc.core_plot.Plot.label`).
- **shadow**, optional: A shadow is plotted below the line and points.
- **shadow_<param>**, optional: ``<param>`` is passed to
:meth:`~vacumm.misc.core_plot.Plot.add_shadow`.
- **glow**, optional: A glow effect is plotted below the line and points.
- **glow_<param>**, optional: ``<param>`` is passed to
:meth:`~vacumm.misc.core_plot.Plot.add_glow`.
"""
if not self.has_valid_data(): return
# Data
xx = self.get_xdata(scalar=0)
yy = self.get_ydata(scalar=0)
if err is not None:
if cdms2.isVariable(err):
err = err.asma()
else:
err = N.ma.asarray(err)
egood = ~N.ma.getmaskarray(err)
if egood.ndim==2:
if egood.shape[0]==2:
egood = egood[0]&egood[1]
else:
err = err.ravel()
egood = egood.ravel()
if not egood.any():
err = None
elif err.shape[-1]!=len(xx):
raise PlotError('Error array must be 1D and of size %i'%xx.size)
# Plot keywords
kwsh = kwfilter(kwargs, 'shadow')
kwgl = kwfilter(kwargs, 'glow')
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
kwerr = kwfilter(kwargs, 'err')
kwfb = kwfilter(kwargs, 'fill_between')
kwline = {}
line_keys = ['color','linewidth','linestyle','markerwidth','drawstyle']
marker_keys = ['markeredgecolor','markeredgesize','markersize','markerfacecolor','marker','zorder','alpha']
line_keys.extend(marker_keys)
for key in line_keys:
if key in kwargs and kwargs[key] is not None:
kwline[key] = kwargs[key]
if parg is None:
parg = []
elif not isinstance(parg, list):
parg = [parg]
kwline['label'] = self.label if label is None else label
# Plot func
if log:
if self.order[0]=='d':
pfunc = self.axes.semilogy
else:
pfunc = self.axes.semilogx
else:
pfunc = self.axes.plot
# Plot
# - main
ll = pfunc(xx, yy, *parg, **kwline)
self.register_obj(ll, ['curve', 'lines', 'plot'], **kwargs)
# - fill_between
if fill_between is not False:
kwfb.setdefault('zorder', ll[0].get_zorder()-0.01)
kwfb.setdefault('linewidth', 0)
kwfb.setdefault('interpolate', True)
if 'color' in kwline:
kwfb.setdefault('color', kwline['color'])
b0 = yy
if fill_between is True or fill_between is None:
b1 = 0.
elif N.asarray(fill_between).ndim == 2:
b0, b1 = fill_between
if self.order[0]=='d':
ff = self.axes.fill_between(xx, b0, b1, **kwfb)
else:
ff = self.axes.fill_betweenx(yy, b0, b1, **kwfb)
self.add_obj('fill_between', ff)
self.register_obj(ff, 'fill_between', **kwargs)
# - error
if err is not None:
kwerr.setdefault('ecolor', ll[0].get_color())
kwerr.setdefault('elinewidth', ll[0].get_linewidth())
if kwerr.get('zorder', None) and kwerr['zorder']<0:
kwerr['zorder'] = ll[0].get_zorder() - kwerr['zorder']
ee = self.axes.errorbar(xx.compress(egood), yy.compress(egood),
err.compress(egood, axis=-1), fmt='none', **kwerr)
self.register_obj(ee, 'error', **kwargs)
# - filters
if shadow:
self.add_shadow(ll, 'lines_shadow', **kwsh) # 'lines_shadow'
if err is not None:
self.add_shadow(ee, 'error_shadow', **kwsh) # 'error_shadow'
if glow:
self.add_glow(ll, 'lines_glow', **kwgl) # 'lines_glow'
if err is not None:
self.add_glow(ee, 'error_glow', **kwgl) # 'error_glow'
# - single dots
if not nosingle and len(xx) > 2:
mask = self.mask.copy()
for m in [self.mask[:-2],self.mask[2:]]:
mask[1:-1] = mask[1:-1] | 1-m
mask[0] = mask[0] or not self.mask[1]
mask[-1] = mask[-1] or not self.mask[-2]
if N.sometrue(1-mask):
kwmark = {}
for key in marker_keys:
if kwargs.has_key(key):
kwmark[key] = kwargs[key]
if kwargs.has_key('zorder'): kwmark['zorder'] = kwargs['zorder']
if self.xtype!='d':
xx = N.ma.array(xx,mask=mask)
else:
yy = N.ma.array(yy,mask=mask)
kwmark.update(label='',color=ll[0].get_color())
dots = self.axes.plot(xx, yy, '.', **kwmark)
self.register_obj(dots, ['lines_dots', 'plot'], **kwargs)
if shadow: self.add_shadow(dots, 'lines_dots_shadow', **kwsh) # 'lines_dots_shadow'
if glow: self.add_glow(dots, 'lines_dots_glow', **kwsh) # 'lines_dots_glow'
[docs]class Bar(Plot1D):
"""Class for plotting simple curve
:Params:
- **data**: 1D array.
- **Specific plot params**: See :meth:`plot`.
- **Other generic params**: See :class:`Plot`.
:Example:
>>> c = Bar(mydata, xwidth=.8, plot=False, order='zd')
>>> c.ymax = 5.
>>> c.plot()
>>> c.post_plot(savefig='rain.png')
"""
[docs] def plot(self, width=1., lag=0, align='center', shadow=False, glow=False, offset=None,
label=None, **kwargs):
"""Plot data as bar plot
:Params:
- **width**, optional: Relative width of the bars(``0<width<1``).
a width of ``1`` means that successive are bars are joined.
- **lag**, optional: Relative lag to apply to the position
- **align**, optional: Alignment relative to coordinates.
- **shadow**, optional: Add a shadow to the bars.
- **shadow_<param>**, optional: ``<param>`` is passed to :meth:`add_shadow`.
- **glow**, optional: Add a glow effect to the bars.
- **glow_<param>**, optional: ``<param>`` is passed to :meth:`add_shadow`.
- **offset**, optional: Bars start at ``offset``.
- **label**, optional: Alternative label for the plot
(see also :attr:`~Plot.label`).
- **bar_<param>**, optional: ``param`` is passed to :func:`matplotlib.pyplot.bar` (or :func:`matplotlib.pyplot.barh`).
:Example:
>>> Bar(rain1).plot(width=.45, align='left', color='cyan')
>>> Bar(rain2).plot(width=.45, lag=.5, align='left', color='b')
"""
if not self.has_valid_data(): return
# Data
data = self.get_data()[0]
axis = self.get_axis_data().astype('d')
bounds = meshbounds(axis)
widths = N.diff(bounds)
axis += widths*(lag if lag else 0.)
widths *= width
if cdms2.isVariable(offset): offset = offset.asma()
# Plot keywords
kwsh = kwfilter(kwargs,'shadow_')
kwgl = kwfilter(kwargs,'glow_')
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
kwbar = kwfilter(kwargs, 'bar_')
bar_keys = ['color','linewidth','linestyle','markerwidth', 'edgecolor', 'zorder', 'alpha', 'log']
for key in bar_keys:
if kwargs.has_key(key):
kwbar[key] = kwargs[key]
kwbar['label'] = self.label if label is None else label
# Log data
if kwbar.get('log', False) and (data<=0.).all(): return
# Plot
# - main
func_name = 'bar'+('h' if self.xtype=='d' else '')
plot_func = getattr(self.axes, func_name)
pp = plot_func(axis, data, widths, offset, align=align, **kwbar)
self.register_obj(pp, ['patches', func_name], **kwargs)
# - filters
if shadow: self.add_shadow(pp, **kwsh)
if glow: self.add_glow(pp, **kwgl)
[docs]class QuiverKey:
[docs] def quiverkey(self, qv, value=None, value_mode=80, **kwargs):
"""Add a quiver key to the plot
See :meth:`Plot.quiverkey` for arguments.
:Params:
- **qv**: Results of :func:`~matplotlib.pyplot.quiver`.
- **pos**, optional: Position of key for arrow .
- **text**, optional: Text or format with variables 'value' and 'units'.
- **value**, optional: Numeric value for key (used by text).
- **units**, optional: Units for key (used by text).
- **latex_units**, optional: Interpret units using latex.
- Extra keywords are passed to :func:`~matplotlib.pyplot.quiverkey`.
"""
# Value
if value is None:
m,u,v = self.get_data()
value = get_quiverkey_value((u, v), mode=value_mode)
del m, u, v
return Plot.quiverkey(self, qv, value, **kwargs)
[docs]class Stick(QuiverKey, ScalarMappable, Curve):
"""Class for makeing a stick plot (vectors on a line)
:Params:
- **udata**: 1D array of intensities along X.
- **vdata**: 1D array of intensities along V.
- **Specific data loading params**: See :meth:`load_data`.
- **Specific plot params**: See :meth:`plot`.
- **Specific plot finalization params**: See :meth:`~vacumm.misc.core_plot.ScalarMappable.post_plot`.
- **Generic params**: See :class:`Plot`.
:Example:
>>> c = Stick(u10, v10, savefig='cart.png')
>>> c = Stick(r, theta, polar=True,width=.8, plot=False, order='zd')
>>> c.plot()
>>> c.post_plot(savefig='polar.png')
"""
# _primary_attributes = Plot._primary_attributes + ['levels', 'nmax', 'nmax_levels', 'cmap']
# _secondary_attributes = Plot._secondary_attributes + ['cblabel']
def __init__(self, udata, vdata, polar=False, degrees=True, **kwargs):
Curve.__init__(self, (udata, vdata), polar=polar, degrees=degrees, **kwargs)
# Polar case
[docs] def load_data(self, data, **kwargs):
"""Load variables
:Special params:
- **udata**: X or radial component of arrows.
- **vdata**: Y or directional component of arrows.
- **polar**, optional: Consider polar coordinates: ``(u, v) -> (rho, theta)``
- **degrees**, optional: If True (default), trat ``theta`` as degrees, else radians.
:Tasks:
#. Calls :meth:`Plot.load_data`.
#. Deals with polar case, if keyword ``polar==True``.
"""
polar = kwargs.pop('polar')
degrees = kwargs.pop('degrees')
Curve.load_data(self, data, **kwargs)
if polar:
m, u, v = self.data
m = u
angle = (v*N.pi/180.) if degrees else v
u = m * MV2.cos(angle)
v = m * MV2.sin(angle)
del angle
if hasattr(m, 'units'):
u.units = v.units = m.units
if hasattr(m, 'long_name'):
u.long_name = 'X component of '+m.long_name
v.long_name = 'Y component of '+m.long_name
self.data[:] = m, u, v
# Read vector data and not modulus
[docs] def get_xdata(self, scalar=1, masked=False, bounds=False):
return Curve.get_xdata(self, scalar=scalar, masked=masked, bounds=bounds)
get_xdata.__doc__ = Curve.get_xdata.__doc__
[docs] def get_ydata(self, scalar=2, masked=False, bounds=False):
return Curve.get_ydata(self, scalar=scalar, masked=masked, bounds=bounds)
get_ydata.__doc__ = Curve.get_ydata.__doc__
[docs] def plot(self, mod=False, pos=None, line=False, color='k', alpha=1, quiverkey=True,
headwidth=None, headlength=None, headaxislength=None, width=None, scale=None,
minshaft=None, minlength=None,
shadow=False, glow=False, cmap=None, levels=None, label=None,
anomaly=False, **kwargs):
"""Main plot
:Params:
- **pos**, optional: Position of the arrow tails.
It defaults to the middle of the appropriate axis.
- **mod**, optional: Plot the curve of the modulus.
- **mod_<param>***, optional: ``<param>`` is passed to
:meth:`Curve.plot` when plotting the modulus.
- **color**: Can be either
- ``"mod"``: The color is function of the modulus.
- A normal color argument for :func:`~matplotlib.pyplot.quiver`
(single or list/array).
- **line**, optional: Add a transversal line along the arrow tails.
- **alpha**, optional: Opacity.
- **scale**, optional: Scale of arrows
(see :func:`~matplotlib.pyplot.quiver`).
- **headwidth**, optional: Head width of arrows
(see :func:`~matplotlib.pyplot.quiver`).
- **headlength**, optional: Head length of arrows
(see :func:`~matplotlib.pyplot.quiver`).
- **headaxislength**, optional: Length of arrow head on axis
(see :func:`~matplotlib.pyplot.quiver`).
- **minlength**, optional: See :func:`~matplotlib.pyplot.quiver`.
- **minshaft**, optional: See :func:`~matplotlib.pyplot.quiver`.
- **quiverkey**, optional: Add key to scale arrows.
- **quiverkey_<param>**, optional: ``<param>`` is passed
to :meth:`~vacumm.misc.core_plot.QuiverKey.quiverkey`.
- **shadow**, optional: Add a drop shadow.
- **shadow_<param>**, optional: ``<param>`` is passed to :meth:`add_shadow`
- **glow**, optional: Add a drop glow effect.
- **glow_<param>**, optional: ``<param>`` is passed to :meth:`add_glow`
- **cmap**, optional: Colormap to use when ``color="mod"``.
- **cmap_<param>**, optional: Passed to
meth:`~vacumm.misc.core_plot.ScalarMappable.get_cmap` to tune
colormap.
- **levels**, optional: Levels of values to use when ``color="mod"``.
- **levels_<param>**, optional: Passed to
meth:`~vacumm.misc.core_plot.ScalarMappable.get_levels` to tune levels.
"""
if not self.has_valid_data(): return
# Keywords
kwmod = kwfilter(kwargs, 'mod_')
kwqv = kwfilter(kwargs, 'quiver_')
kwqvkey = kwfilter(kwargs, 'quiverkey_')
kwline = kwfilter(kwargs, 'line_')
kwcmap = kwfilter(kwargs, 'cmap_')
kwlevels = kwfilter(kwargs, 'levels_', defaults=dict(anomaly=anomaly))
kwsh = kwfilter(kwargs, 'shadow_')
kwgl = kwfilter(kwargs, 'glow_')
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
kwqv['label'] = self.label if label is None else label
# Cmap and levels
numcolors = color=='mod' or color is True or \
(isinstance(color, N.ndarray) and color.dtype.char!='S')
if numcolors:
if levels is None:
levels = self.get_levels(**kwlevels)
else:
self.levels = levels
# color = None
if cmap is not None:
self.cmap = cmap
cmap = self.get_cmap(cmap, **kwcmap)
# Add modulus
if mod:
for att in 'color', 'alpha', 'shadow', 'glow':
value = eval(att)
if value is not None:
kwmod.setdefault(att, eval(att))
# if color is not None and isinstance(kwmod['color'], (list, N.ndarray)):
if kwmod['color'] is not None and isinstance(kwmod['color'], (list, N.ndarray)):
kwmod['color'] = kwmod['color'][0]
Curve.plot(self, **kwmod)
# Data
mm, uu, vv = self.get_data(scalar=False)
# Coordinates
pos = kwargs.pop('ycenter', pos) # compat
if self.ytype=='d':
if pos is None: pos = (self.axes.get_ylim()[0]+self.axes.get_ylim()[1])/2.
xx = self.get_xdata()
yy = N.zeros(len(xx))+pos
else:
if pos is None: pos = (self.axes.get_xlim()[0]+self.axes.get_xlim()[1])/2.
yy = self.get_ydata()
xx = N.zeros(len(yy))+pos
# Plot
for att in ['color', 'headwidth', 'headlength', 'alpha', 'headwidth',
'headlength', 'headaxislength', 'minshaft', 'minlength', 'width', 'scale']:
value = eval(att)
if value is not None:
kwqv.setdefault(att, eval(att))
args = [xx, yy, uu, vv]
if color is None or color is True:
# kwqv['vmin'] = self.vmin
# kwqv['vmax'] = self.vmax
args.append(mm)
kwqv.pop('color')
elif numcolors: # numerical values for colors
args.append(color)
kwqv.pop('color')
elif isinstance(color, (list, N.ndarray)):
kwqv.setdefault('color', color)
qv = self.register_obj(self.axes.quiver(*args, **kwqv), ['quiver', 'patches'], **kwargs)
if numcolors:
kwqv['cmap'] = cmap
qv.set_clim(self.vmin, self.vmax)
self.set_obj('scalar_mappable', qv)
sel.axes._sci(av)
qv.set_alpha(alpha)
# Filters
if shadow: self.add_shadow([qv], 'quiver_shadow', **kwsh)
if glow: self.add_glow([qv], 'quiver_glow', **kwgl)
# Zorder of modulus
if mod and not kwmod.has_key('zorder'):
oo = self.get_obj('lines')
if self.get_obj('lines_shadow') is not None:
oo.extend(self.get_obj('lines_shadow'))
if self.get_obj('lines_dot') is not None:
oo.extend(self.get_obj('lines_dot'))
if self.get_obj('lines_dot_shadow') is not None:
oo.extend(self.get_obj('lines_dot_shadow'))
dz = qv.get_zorder()-oo[0].get_zorder()
if dz<=0:
for o in oo:
o.set_zorder(o.get_zorder()+dz-.2)
# Quiver key
if quiverkey:
self.quiverkey(qv, **kwqvkey)
# Base line
if line:
hv, pp = ('h', yy[0]) if self.ytype=='d' else ('v', xx[0])
funcname = 'ax%sline'%hv
func = getattr(self.axes, funcname)
self.register_obj(func(pp, **kwline), [funcname], **kwargs)
return qv
[docs]class Plot2D(ScalarMappable, QuiverKey, Plot):
_order = ['--']
_primary_attributes = ScalarMappable._primary_attributes + ['fill']
_secondary_attributes = ScalarMappable._secondary_attributes + ['cblabel']
rank = 2
_plotter = None
def _set_axes_(self, xaxis=None, yaxis=None, xatts=None, yatts=None,
x2d=None, y2d=None, x2db=None, y2db=None, **kwargs):
"""Change axes and their attributes of the variables
The main goal is to deal 2D plots on special grids where the grid
cannot be properly stored internally in variables (like a section grid).
:Params:
- **x/yaxis**, optional: 1D or 2D array or axes to use for axis,
instead of internal axes of input variables.
- **x/y2d**, optional: (ny,nx) array of cell center coordinates.
- **x/y2db**, optional: (ny+1,nx+1) array of cell bounds coordinates
used in pcolor like plots.
- **x/yatts**, optional: Dictionnary of alter given ``x/yaxis``.
"""
# Get axes and adjust order
if (xaxis is not None or yaxis is not None) and self.has_data():
# Adjust order
for ivar, var in enumerate(self.data):
order = var.getOrder()
#self.data[ivar] = var2d(var, xaxis, yaxis, xatts=xatts, yatts=yatts)
for i, axis in enumerate([yaxis, xaxis]):
c = getattr(axis, 'axis', '-').lower()
if order[i]=='-' and c!='-': order[i] = c
set_order(self.data[ivar], order)
self.data[ivar]._nogridorder = True
if self.has_data():
xax = get_axis(self.data[0], -1, strict=True)
yax = get_axis(self.data[0], -2, strict=True)
if xaxis is None:
if self.has_data():
xaxis = xax
else:
raise PlotError('No xaxis data available')
elif not isaxis(xaxis):
xaxis = MV2.array(xaxis, copy=0)
if self.has_data():
cp_atts(xax, xaxis, overwrite=False)
if yaxis is None:
if self.has_data():
yaxis = yax
else:
raise PlotError('No yaxis data available')
elif not isaxis(yaxis):
yaxis = MV2.array(yaxis, copy=0)
if self.has_data():
cp_atts(yax, yaxis, overwrite=False)
# Change attributes
if xatts is not None:
set_atts(xaxis, xatts)
if yatts is not None:
set_atts(yaxis, yatts)
# Set axes
self.x = xaxis
self.y = yaxis
# Mesh form
xdata = self.get_xdata(masked=False)
ydata = self.get_ydata(masked=False)
# - centers
if x2d is None or y2d is None:
self.x2d, self.y2d = meshgrid(xdata, ydata)
if x2d is not None:
self.x2d = x2d
if y2d is not None:
self.y2d = y2d
self.x2dr,self.y2dr = self.x2d, self.y2d # save raw values
# - bounds
if x2db is None or y2db is None:
self.x2db, self.y2db = meshbounds(xdata, ydata)
if x2db is not None:
if x2db.ndim==1:
self.x2db = N.resize(x2db, (self.x2d.shape[0]+1, x2db.shape[0]))
elif x2db.shape[0] == self.x2d.shape[0]:
self.x2db = N.zeros((self.x2d.shape[0]+1, self.x2d.shape[1]+1))
self.x2db[1:] = shift1d(x2db, 1, axis=0, mode='same')
self.x2db[:-1] += shift1d(x2db, -1, axis=0, mode='same')
self.x2db[1:-1] *= 0.5
else:
self.x2db = x2db
if y2db is not None:
if y2db.ndim==1:
self.y2db = N.resize(y2db, (self.y2d.shape[1]+1, y2db.shape[0])).T
elif y2db.shape[1] == self.y2d.shape[1]:
self.y2db = N.zeros((self.y2d.shape[0]+1, self.y2d.shape[1]+1))
self.y2db[:, 1:] = shift1d(y2db, 1, axis=1, mode='same')
self.y2db[:, :-1] += shift1d(y2db, -1, axis=1, mode='same')
self.y2db[:, 1:-1] *= 0.5
else:
self.y2db = y2db
self.x2dbr,self.y2dbr = self.x2db, self.y2db # save raw values
[docs] def load_data(self, data, **kwargs):
"""Load data and axes
:Tasks:
#. Call to :meth:`Plot.load_data`.
#. Compute :attr:`x2d`, :attr:`y2d`, :attr:`x2db`, :attr:`y2db`
:Params:
- **data**: A single or a tuple or 2D :mod:`MV2` arrays.
- Single variable: Plot 2D scalar field with contours, filled contours, pcolor or image.
- 2-tuple ``(U,V)``: Plot arrows.
- 3-tuple ``(M,U,V)``: Plot both scalar field ``M`` and arrows.
:Specific attributes:
.. attribute:: x2d
2D version of the X axis data.
.. attribute:: y2d
2D version of the Y axis data.
.. attribute:: x2db
2D bounds coordinates of the X axis data.
.. attribute:: y2db
2D bounds coordinates of the Y axis data.
"""
# # Check
# if data is None: return
# Normal load
Plot.load_data(self, data, **kwargs)
def _transpose_axes_(self):
# Normal transpose
Plot._transpose_axes_(self)
# Mesh form
# - centers
self.x2d, self.y2d = self.y2d.T, self.x2d.T
self.x2dr, self.y2dr = self.y2dr.T, self.x2dr.T
# - bounds
self.x2db, self.y2db = self.y2db.T, self.x2db.T
self.x2dbr, self.y2dbr = self.y2dbr.T, self.x2dbr.T
[docs] def get_xyscaler(self, guess=True):
"""Get :attr:`xyscaler`"""
return self.get_obj('xyscaler')
[docs] def set_xyscaler(self, scaler):
"""Set :attr:`xyscaler`"""
self.set_obj('xyscaler', scaler)
[docs] def del_xyscaler(self):
"""Del :attr:`xyscaler`"""
xyscaler = property(get_xyscaler, set_xyscaler,
del_xyscaler, doc="""Function to rescale X and Y coordinates.
``None`` is returned if no scaler is available.
A typical scaler is a :class:`mpl_toolkits.basemap.Basemap` or
a :class:`mpl_toolkits.pyproj.Proj` instance that converts
positions from degrees to meters using a geographic
projection.
This scaler is used by :meth:`plot_quiver` for undersampling
based on resolution.
:Example:
>>> xyscaler = myplot.xyscaler
>>> if xyscaler is not None:
... x,y = myplot.xyscaler(x, y)
:Return: A callable function or ``None`` if no scaling is possible
""")
@staticmethod
def _fill_method_(fill='pcolormesh', pcolor=None, nofill=None, **kwargs):
if fill is None:
fill = get_config_value('vacumm.misc.plot', 'fill')
if fill is not None:
if fill.isdigit():
fill = int(fill)
elif fill in ['True', 'None', 'False']:
fill = eval(fill)
if fill is True or str(fill).startswith('pcolor'):
fill = 'pcolormesh'
if nofill:
fill = 0
elif pcolor:
fill = 'pcolormesh'
elif pcolor in (False, 0, 2):
fill = 'contourf'
elif pcolor == 3:
fill = 'imshow'
if fill == 'nofill' or fill is False:
fill = 'no'
elif fill == 'contour':
fill = 'contourf'
elif isinstance(fill, int):
fill = N.clip(fill, 0, 4)
fill = ['no', 'pcolor', 'contourf', 'imshow', 'scatter'][fill]
return fill
[docs] def get_fill(self, fill=None, pcolor=None, nofill=None, **kwargs):
"""Get the :attr:`fill` attribute"""
if fill is None:
fill = self.get_obj('fill')
return self._fill_method_(fill=fill, pcolor=pcolor, nofill=nofill)
[docs] def set_fill(self, fill):
"""Set the :attr:`fill` attribute"""
self.set_obj('fill', fill)
[docs] def del_fill(self):
"""Del the :attr:`fill` attribute"""
self.del_obj('fill')
fill = property(get_fill, set_fill, del_fill,
doc="Fill method for 2D plots: 'pcolor', None, True, False, 'contourf', 'imshow', 'pcolormesh'")
[docs] def plot_fill(self, norm=None, shading='flat', alpha=1, extend=None,
zorder=None, shadow=False, glow=False, **kwargs):
"""Plot filled stuff
:Params:
- **fill**, optional: Type filling.
It defaults to ``fill`` option of the ``[vacumm.misc.plot]`` section
of the configuration (see :func:`~vacumm.config.edit_config`).
- ``0`` or ``"no"`` or ``"nofill"`` or ``False``: No filling.
- ``1`` or ``"pcolor"`` or ``"pcolormesh"``: Fill using :func:`~matplotlib.pyplot.pcolor`
or :func:`~matplotlib.pyplot.pcolormesh`.
- ``2`` or ``"imshow"``: Fill using :func:`~matplotlib.pyplot.imshow`.
- ``3`` or ``"contourf"``: Fill using :func:`~matplotlib.pyplot.contourf`.
- **fill_<param>**: ``<param>`` is passed to the corresponding plot function.
- **nofill**, optional: Implies ``fill=0``.
- **cmap**, optional: Colormap (defaults to ``"magic"``).
- **cmap_<param>**, optional: Passed to
:meth:`~vacumm.misc.core_plot.ScalarMappable.get_cmap` to tune
colormap.
- **norm**, optional: :class:`~matplotlib.colors.Normalize` instance.
- **alpha**, optional: Opacity.
- **shading**, optional: Shading with :func:`~matplotlib.pyplot.pcolor`.
- **extend**, optional: Let's :func:`~matplotlib.pyplot.contourf` add
contours to cover all data range.
- **zorder**, optional: Plot order.
:Tasks:
#. Guess the fill method.
#. Get the colormap with :meth:`~ScalarMappable.get_cmap`.
#. Get the scalar data with :meth:`~Plot.get_data`.
#. Calls :func:`~matplotlib.pyplot.imshow` or
:func:`~matplotlib.pyplot.pcolor` or
:func:`~matplotlib.pyplot.pcolormesh` or
:func:`~matplotlib.pyplot.contourf`.
#. Calls :func:`~matplotlib.pyplot.clabel`
.. note::
:meth:`~ScalarMappable.get_levels` must have been previously called.
"""
if not self.has_valid_data(): return
# Keywords
kwfill = kwfilter(kwargs, 'fill_')
kwnorm = kwfilter(kwargs, 'norm')
kwcmap = kwfilter(kwargs, 'cmap', copy=True)
kwlevels = kwfilter(kwargs, 'levels_')
if zorder is not None:
kwfill.setdefault('zorder', zorder)
kwsh = kwfilter(kwargs, 'shadow')
kwgl = kwfilter(kwargs, 'glow')
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
# Fill method
fill = self.fill_method = self.get_fill(**kwargs)
if fill == 'no': return
# Levels
levels = self.get_levels(**kwlevels)
# Norm
# from matplotlib.colors import Normalize
if norm is None:
from color import StepsNorm
norm = StepsNorm(levels, **kwnorm)
elif norm is True:
norm = Normalize(min(levels), max(levels))
elif not isinstance(norm, Normalize):
norm = None
# Color map
cmap = self.get_cmap(**kwcmap)
# Data
data = self.get_data(scalar=True)
# Plots
if fill == 'imshow':
kwfill.setdefault('origin', 'lower')
pp = self._plotter.imshow(data, cmap=cmap, alpha=alpha,
norm=norm, vmin=self.vmin, vmax=self.vmax,
extent=[self.x2db.min(), self.x2db.max(),
self.y2db.min(), self.y2db.max()], **kwfill)
if alpha != 1:
pp.set_alpha(alpha)
self.set_obj('imshow', pp)
elif fill.startswith('pcolor'):
if fill=='pcolormesh': # self.mask is MV2.nomask or
func = self._plotter.pcolormesh
kwfill.setdefault('edgecolors', 'None')
else:
kwfill.setdefault('edgecolors', 'None')
kwfill.setdefault('linewidth', 0)
func = self._plotter.pcolor
for att, val in dict(cmap=cmap,alpha=alpha, norm=norm,
vmin=self.vmin, vmax=self.vmax, edgecolors='none').items():
kwfill.setdefault(att, val)
pp = func(self.x2db, self.y2db, data, **kwfill)
if alpha != 1:
pp.set_alpha(alpha)
pp.set_linewidth(0.)
self.set_obj('pcolor', pp)
elif fill == 'contourf':
if extend is None:
extend = self._get_extend_(levels)
dict_check_defaults(kwfill, cmap=cmap,
levels=levels, alpha=alpha, norm=norm,
extend=extend, edgecolors='none')
pp = self._plotter.contourf(self.x2d, self.y2d, data, copy=0, **kwfill)
self.set_obj('contourf', pp)
if alpha != 1:
for col in pp.collections:
try:
col.set_alpha(alpha)
except:
pass
elif fill == 'scatter':
pp = self._plotter.scatter(self.x2d.ravel(), self.y2d.ravel(),
c=data.ravel(), cmap=cmap, alpha=alpha,
norm=norm, vmin=self.vmin, vmax=self.vmax,
**kwfill)
if alpha != 1:
pp.set_alpha(alpha)
self.set_obj('scatter', pp)
else:
raise PlotError('Wrong fill method')
# Register
self.set_obj(['fill', 'scalar_mappable'], pp)
self.register_obj(pp, 'patches', **kwargs)
self.axes._sci(pp)
# Filters
if shadow or glow:
obj = pp.collections if hasattr(pp, 'collections') else pp
if shadow: self.add_shadow(obj, 'fill_shadow', **kwsh)
if glow: self.add_glow(obj, 'fill_glow', **kwgl)
# Hacks
# if zorder is not None: pp.set_zorder(zorder)
for key in 'edgecolors', 'linewidths':
# if hasatt(pp, 'set_'+key):
# getattr(pp, 'set_'+key)(kwfill[key])
if not kwfill.has_key(key): continue
if hasattr(pp, 'collections'):
for p in pp.collections:
getattr(p, 'set_'+key)(kwfill[key])
# if kwfill.has_key('edgecolor'):
# for p in pp.collections:
# p.set_edgecolor(kwfill['edgecolor'])
# if kwfill.has_key('linewidth'):
# for p in pp.collections:
# p.set_linewidth(kwfill['linewidth'])
[docs] def plot_contour(self, zorder=None, alpha=1, clabel=None, linewidths=None,
colors='k', shadow=False, glow=False, **kwargs):
"""Plot contour lines
:Params:
- **contour_<param>**, optional: ``<param>`` is passed to
:func:`~matplotlib.pyplot.contour`.
- **zorder**, optional: Plot order.
- **alpha**, optional: Opacity.
- **linewidths**, optional: Contour linewidths.
- **clabel**, optional: Add contour labels with
:func:`~matplotlib.pyplot.clabel`. If not specified, it is taken from
config section ``[vacumm.misc.plot]`` and config option ``clabel``.
- **clabel_<param>**, optional: ``<param>`` is passed to
:func:`~matplotlib.pyplot.clabel`..
- **contour_<param>**, optional: ``<param>`` is passed to
:func:`~matplotlib.pyplot.contour`.
:Tasks:
#. Get the scalar data with :meth:`~Plot.get_data`.
#. Calls :func:`~matplotlib.pyplot.contour`.
#. Calls :func:`~matplotlib.pyplot.clabel`
"""
if not self.has_valid_data():
return
# Keywords
if clabel is None:
try:
clabel = eval(get_config_value('vacumm.misc.plot', 'clabel'))
except:
clabel = eval(get_config_value('vacumm.misc.plot', 'clabel', user=False))
kw = kwfilter(kwargs, 'contour', defaults=dict(levels=self.levels))
kwcl = kwfilter(kwargs, 'clabel')
kwsh = kwfilter(kwargs, 'shadow')
kwsh.update(kwfilter(kw, 'shadow_'))
kwgl = kwfilter(kwargs, 'glow')
kwgl.update(kwfilter(kw, 'glow_'))
kwcmap = kwfilter(kwargs, 'cmap', copy=True)
# kwcmap.update(kwfilter(kw, 'cmap_'))
shadow = kw.get('shadow', shadow)
glow = kw.get('glow', glow)
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
# Setup
if kwargs.has_key('fmt'):
kwcl['fmt'] = kwargs['fmt']
kwcl.setdefault('fmt', '%g')
kw.setdefault('alpha', .5 if self.fill_method.startswith('pcolor') or
self.fill_method.startswith('imshow') else alpha)
kwcl.setdefault('alpha', alpha)
if 'linewidth' in kw:
linewidths = kw.pop('linewidth')
if linewidths is None and 'linewidth' in kwargs:
linewidths = kwargs.pop('linewidth')
if linewidths is not None:
kw.setdefault('linewidths', linewidths)
if zorder is not None:
kw.setdefault('zorder', zorder)
# Colors
if not 'cmap' in kw:
cmap = None
else:
cmap = self.get_cmap(kw.get('cmap', None))
if colors is None and 'color' in kwargs:
colors = kwargs.pop('color')
colors = kw.get('colors', colors)
if cmap is not None:
colors = None
else:
cmap = self.get_cmap(**kwcmap)
if colors is not None:
cmap = None
kw['cmap'] = cmap
kw['colors'] = colors
# Data
data = self.get_data(scalar=True)
# Contours
cc = self._plotter.contour(self.x2d, self.y2d, data, **kw)
self.set_obj('contour', cc)
self.register_obj(cc, 'lines', **kwargs)
if shadow: self.add_shadow(cc.collections, 'contour_shadow', **kwsh)
if glow: self.add_glow(cc.collections, 'contour_glow', **kwgl)
# Labels
if len(cc.collections) and not kwcl.pop('hide', not clabel):
kwcl.setdefault('zorder', cc.collections[0].get_zorder())
clshadow = kwcl.pop('shadow', shadow)
kwclsh = kwfilter(kwcl, 'shadow')
clglow = kwcl.pop('glow', glow)
kwclgl = kwfilter(kwcl, 'glow')
dict_copy_items(kwargs, [kwclsh, kwclgl], 'anim')
cl = self.axes.clabel(cc, **kwcl)
self.set_obj('clabel', cl)
self.register_obj(cl, 'text', **kwargs)
for l in cl:
l.set_alpha(kwcl['alpha'])
l.set_zorder(kwcl['zorder'])
if clshadow: self.add_shadow(cl, **kwclsh)
if clglow: self.add_glow(cl, **kwclgl)
[docs] def plot_quiver(self, zorder=None, quiverkey=True, barbs=False,
shadow=False, glow=False, quiver_cmap=None,
quiver_vmin=None, quiver_vmax=None,
quiver_samp=None, quiver_xsamp=None, quiver_ysamp=None, quiver_res=None,
quiver_relres=None, quiver_xres=None, quiver_xrelres=None, quiver_yres=None,
quiver_yrelres=None, quiver_res_scaler=None, quiver_nauto=None, **kwargs):
"""Plot arrows
You can undersample arrows using direct undersampling (parameters
with "samp") or undersampling based on resolution (using
:func:`~vacumm.misc.grid.masking.resol_mask`).
Resolution undersampling may be in input (like degrees) or
transformed (like meters) coordinates. Transformed
coordinates are deduced from input coordinates using
``quiver_res_scaler``, which defaults to :attr:`xyscaler`.
.. note::
Direct unsampling is incompatible with resolution undersampling:
the former prevails against the latter.
:Params:
- **quiver_norm**,optional: Normalize/colorize arrows
- ``0`` or ``None``: No normalization, no colorization (default)
- ``1``: Normalization, no colorization.
- ``2``: Normalization, colorization.
- ``3``: No normalization, colorization.
- **quiver_<param>**, optional: ``<param>`` is passed to
:func:`~matplotlib.pyplot.quiver`.
- **barbs**, optional: Plot wind barbs instead of arrows
(see :func:`~matplotlib.pyplot.barbs`).
- **zorder**, optional: Plot order.
- **alpha**, optional: Opacity.
- **shadow**, optional: Add shadow below arrows.
- **glow**, optional: Add glow effect to arrows.
- **quiverkey**, optional: Add key to scale arrows.
- **quiverkey_<param>**, optional: ``<param>`` is passed
to :meth:`~vacumm.misc.core_plot.QuiverKey.quiverkey`.
- **cmap**, optional: Colormap to use when ``color="mod"`` (defaults to
``"magic"``).
- **quiver_samp**, optional: Horizontal sampling of arrows (in both directions) [default: 1]
- **quiver_x/ysamp**, optional: Sampling along X/Y [default: quiver_samp]
- **quiver_res**, optional: Horizontal resolution of arrows (in both directions)
for undersampling [default: None]
If ``'auto'``, resolution is computed so as to have at max ``quiver_nauto``
arrow in along an axis. If it is a :class:`complex` type, its imaginary part
set the ``quiver_nauto`` parameter and ``quiver_res`` is set to ``'auto'``.
- **quiver_x/yres**, optional: Same along X/Y [default: quiver_res]
- **quiver_relres**, optional: Relative resolution (in both directions).
- If > 0, = ``mean(res)*relres``.
- If < -1, = ``min(res)*abs(relres)``.
- If < 0 and > -1, = ``max(res)*abs(relres)``
- **quiver_x/yrelres**, optional: Same along X/Y [default: quiver_relres]
:Tasks:
#. Get the scalar data with :meth:`~Plot.get_data`.
#. Calls :func:`~matplotlib.pyplot.quiver`.
#. Calls :func:`~matplotlib.pyplot.quiverkey`
"""
if not self.has_valid_data(): return
# Get data
if len(self.data)!=3: return
x2d, y2d = self.x2d, self.y2d
x2dr, y2dr = self.x2dr, self.y2dr
mm, uu, vv = self.get_data(scalar=False)
del mm
mm = N.ma.sqrt(uu**2+vv**2)
# Keywords
kwqv = kwfilter(kwargs, 'quiver')#, defaults=dict(angles='uv'))
kwqvkey = kwfilter(kwargs, 'quiverkey')
if zorder is not None:
kwqv['zorder'] = zorder
# if kwqv.has_key('width') and kwqv['width'] > .01:
# kwqv['width'] *= 0.001
shadow = kwqv.pop('shadow', shadow)
glow = kwqv.pop('glow', glow)
kwsh = kwfilter(kwqv, 'shadow')
kwgl = kwfilter(kwqv, 'glow')
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
kwcmap = kwfilter(kwargs, 'cmap', copy=True) if not self.get_scalar_mappable() else {}
kwcmap.update(kwfilter(kwqv, 'cmap'))
# Undersampling
# - setup
if quiver_samp is not None:
if quiver_xsamp is None: quiver_xsamp = quiver_samp
if quiver_ysamp is None: quiver_ysamp = quiver_samp
if quiver_xsamp is not None:
quiver_xres = quiver_xrelres = False
if quiver_ysamp is not None:
quiver_yres = quiver_yrelres = False
# - indirect
rmask = resol_mask((x2dr, y2dr), res=quiver_res, xres=quiver_xres, yres=quiver_yres,
relres=quiver_relres, xrelres=quiver_xrelres, yrelres=quiver_yrelres,
scaler = self.xyscaler, compact=True, nauto=quiver_nauto)
# - apply
if rmask is not False:
uu = uu.copy()
vv = vv.copy()
mm = mm.copy()
uu[rmask] = MV2.masked
vv[rmask] = MV2.masked
mm[rmask] = MV2.masked
if quiver_xsamp or quiver_ysamp:
xslice = slice(None, None, quiver_xsamp)
yslice = slice(None, None, quiver_ysamp)
x2d = x2d[yslice, xslice]
y2d = y2d[yslice, xslice]
uu = uu[yslice, xslice]
vv = vv[yslice, xslice]
mm = mm[yslice, xslice]
# Norm
quiver_norm = kwqv.pop('norm', None)
if quiver_norm in [1, 2] :
quiverkey = False
good = mm!=0.
uu = N.ma.where(good, uu/mm, uu)
vv = N.ma.where(good, vv/mm, vv)
del good
qvargs = [x2d, y2d, uu, vv]
if quiver_norm == 2:
qvargs.append(mm)
else:
qvargs = [x2d, y2d, uu, vv]
if quiver_norm == 3:
qvargs.append(mm)
if quiver_norm >=2:
if quiver_cmap is None:
quiver_cmap = self.get_cmap(**kwcmap)
kwqv['cmap'] = quiver_cmap
vmin = quiver_vmin if quiver_vmin is not None else self.vmin
vmax = quiver_vmax if quiver_vmax is not None else self.vmax
kwqv['norm'] = Normalize(vmin=vmin, vmax=vmax)
mask = N.ma.asarray(uu).mask
if mask is not MV2.nomask:
good = ~mask.ravel()
qvargs = [N.compress(good, qa.ravel()) for qa in qvargs] ; del qa
# Arrows
if barbs:
quiver_func = self._plotter.barbs
quiverkey = False
else:
quiver_func = self._plotter.quiver
angles = 'xy' if self.uvscaler is not None else 'uv'
kwqv.setdefault('angles', angles)
qv = quiver_func(*qvargs, **kwqv)
if isinstance(qv, tuple): qv = list(qv)
self.set_obj('quiver', qv)
self.register_obj(qv, 'patches')
if quiver_norm>=2 and self.get_scalar_mappable() is None:
self.set_obj('scalar_mappable', qv)
# Filters
if shadow: self.add_shadow(qv, 'quiver_shadow', **kwsh)
if glow: self.add_glow(qv, 'quiver_glow', **kwgl)
# Quiver key
if quiverkey:
self.quiverkey(qv, **kwqvkey)
return qv
[docs] def plot_streamplot(self, zorder=None,
shadow=False, glow=False, streamplot_color=None, streamplot_linewidth=None,
streamplot_lwmodmin=.5, streamplot_lwmodmax=3,
streamplot_cmap=None, streamplot_norm=None,
streamplot_vmin=None, streamplot_vmax=None,
**kwargs):
"""Plot stream lines with :func:`~matplotlib.pyplot.streamplot`
:Params:
- **streamplot_color**,optional:
- ``None`` or `"default"`: Default colorization.
- ``"modulus"``: Colorization as a function of the modulus.
- Else, passed directly to :func:`~matplotlib.pyplot.streamplot`
- **streamplot_linewidth**,optional:
- ``None`` or ``"default"``: Default linewidth.
- ``"modulus"``: Linewidth proportional to the modulus with a maximum
linewidth of ``streamplot_lwmod``.
- Else, passed directly to :func:`~matplotlib.pyplot.streamplot`
- **streamplot_lwmodmin**, optional: Min linewidth used when ``streamplot_linewidth``
is set to ``"modulus"``.
- **streamplot_lwmodmax**, optional: Max linewidth used when ``streamplot_linewidth``
is set to ``"modulus"``.
- **streamplot_<param>**, optional: ``<param>`` is passed to
:func:`~matplotlib.pyplot.streamplot`.
- **zorder**, optional: Plot order.
- **alpha**, optional: Opacity.
- **shadow**, optional: Add shadow below arrows.
- **glow**, optional: Add glow effect to arrows.
- **streamplotkey**, optional: Add key to scale arrows.
- **cmap**, optional: Colormap to use when ``color="modulus"`` (defaults to
``"magic"``).
:Tasks:
#. Get the scalar data with :meth:`~Plot.get_data`.
#. Calls :func:`~matplotlib.pyplot.streamplot`.
"""
if not self.has_valid_data(): return
# Get data
if len(self.data)!=3: return
x2d, y2d = self.x2d, self.y2d
mm, uu, vv = self.get_data(scalar=False)
del mm
mm = N.ma.sqrt(uu**2+vv**2)
# Keywords
kwsp = kwfilter(kwargs, 'streamplot')#, defaults=dict(angles='uv'))
if zorder is not None:
kwsp['zorder'] = zorder
if kwsp.has_key('width') and kwsp['width'] > .01:
kwsp['width'] *= 0.001
shadow = kwsp.pop('shadow', shadow)
glow = kwsp.pop('glow', glow)
kwsh = kwfilter(kwsp, 'shadow')
kwgl = kwfilter(kwsp, 'glow')
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
kwcmap = kwfilter(kwargs, 'cmap', copy=True) if not self.get_scalar_mappable() else {}
kwcmap.update(kwfilter(kwsp, 'cmap'))
# Color
vmax = streamplot_vmax if streamplot_vmax is not None else self.vmax
vmin = streamplot_vmin if streamplot_vmin is not None else self.vmin
if streamplot_color in [None, 'default']:
streamplot_color = None
elif streamplot_color=='modulus':
streamplot_color = mm
if streamplot_cmap is None:
streamplot_cmap = self.get_cmap(**kwcmap)
kwsp['cmap'] = streamplot_cmap
if streamplot_norm is None:
streamplot_norm = Normalize(vmin=vmin, vmax=vmax)
kwsp['norm'] = streamplot_norm
kwsp['color'] = streamplot_color
# Linewidth
streamplot_linewidth = kwsp.pop('lw', streamplot_linewidth)
streamplot_lwmodmax = kwsp.pop('lwmod', streamplot_lwmodmax)
if streamplot_linewidth in [None, 'default']:
streamplot_linewidth = None
elif streamplot_linewidth=='modulus':
streamplot_linewidth = (streamplot_lwmodmin+
(streamplot_lwmodmax-streamplot_lwmodmin)*(mm-vmin)/(vmax-vmin))
kwsp['linewidth'] = streamplot_linewidth
# Lines
sp = self._plotter.streamplot(x2d, y2d, uu, vv, **kwsp)
self.set_obj('streamplot', sp)
self.register_obj(sp.arrows, 'patches')
self.register_obj(sp.lines, 'lines')
if isinstance(streamplot_color, N.ndarray) and self.get_scalar_mappable() is None:
sp.arrows.set_array(streamplot_color)
sp.arrows.set_clim(vmin, vmax)
sp.arrows.set_norm(kwsp['norm'])
sp.arrows.set_cmap(kwsp['cmap'])
self.set_obj('scalar_mappable', sp.arrows)
return sp
[docs] def plot(self, levels=None, contour=True, anomaly=False, streamplot=False, **kwargs):
"""Plot filled and/or contoured content
:Params:
- **levels**, optional: Levels for contouring and/or colormap.
- **levels_<param>**, optional: Passed to
:meth:`~vacumm.misc.core_plot.ScalarMappable.get_levels` to tune levels.
- **contour**, optional: Plot line contours.
- **streamplot**, optional: Plot stream lines instead of arrows.
- Other arguments are passed to :meth:`plot_fill` and :meth:`plot_contour`.
:Tasks:
#. Get :attr:`~ScalarMappable.levels`.
#. Calls :meth:`plot_fill`.
#. Calls :meth:`plot_contour`.
#. Calls :meth:`plot_quiver` or :meth:`plot_streamplot`.
"""
if not self.has_valid_data(): return
# Get levels
kwlevels = kwfilter(kwargs, 'levels', defaults=dict(anomaly=anomaly))
if levels is not None:
self.levels = levels
levels = self.get_levels(**kwlevels)
# Plotter
if self._plotter is None:
self._plotter = self.axes
# Fill
self.plot_fill(**kwargs)
# Contours
if contour: self.plot_contour(**kwargs)
# Quiver or stream lines
if not streamplot:
self.plot_quiver(**kwargs)
else:
self.plot_streamplot(**kwargs)
[docs] def add_grid(self, **kwargs):
"""Add the coordinates as a grid with centers and corners
See :func:`~vacumm.misc.plot.add_grid` for parameters.
"""
from .plot import add_grid
kwargs.update(xcorners=self.x2db, ycorners=self.y2db)
return self.register_obj(add_grid((self.x2d, self.y2d), **kwargs), 'grid')
[docs]class Hov(Plot2D):
_order = ['zT', 'Tx', 'yT', 'T-', '-T']
[docs]class Section(Plot2D):
_order = ['Zx', 'Zy']
[docs]class Map(Plot2D):
"""This class is used for plotting a map with or without data
It uses a :class:`mpl_toolkits.basemap.Basemap` instance, stored in attribute :attr:`map`.
Some other attributes: :attr:`lon`, :attr:`lat`, :attr:`map_update`.
.. attribute:: map
A :class:`~mpl_toolkits.basemap.Basemap` initialized in :meth:`pre_plot`.
"""
_order = 'YX'
map = None
def _set_axes_(self, xaxis=None, yaxis=None, xatts=None, yatts=None, **kwargs):
Plot2D._set_axes_(self, xaxis=xaxis, yaxis=yaxis, xatts=xatts, yatts=yatts, **kwargs)
if self.has_data():
if xaxis is not None:
for var in self.data:
var.getAxis(1).designateLongitude()
if yaxis is not None:
for var in self.data:
var.getAxis(0).designateLatitude()
_set_axes_.__doc__ = Plot2D._set_axes_.__doc__
[docs] def mapproj(self, x, y, inverse=False):
"""Convert from degrees to meters (or inverse)
It uses attribute :attr:`map` if set to a
:class:`~mpl_toolkits.basemap.Basemap` instance with
a projection different from "cyl".
If no valid projection is found, et uses functions
:func:`~vacumm.misc.phys.units.deg2m` and
:func:`~vacumm.misc.phys.units.m2deg`.
:Params:
- **x/y**: Coordinates in degrees or meters
- **inverse**, optional: Reverse projection (meters->degrees)
- **current**, optional: If True,
"""
if getattr(self, 'map', None) is None or self.map.projection=='cyl':
if inverse:
return m2deg(x, y), m2deg(x)
return deg2m(x, y), deg2m(x)
return self.map(x, y, inverse=inverse)
[docs] def get_xyscaler(self):
"""Get :attr:`xyscaler`"""
xyscaler = Plot2D.get_xyscaler(self)
if xyscaler is None:
if getattr(self, 'map', None) is None:
return lambda x,y,inverse=True: x,y
return self.map
return xyscaler
xyscaler = property(get_xyscaler, Plot2D.set_xyscaler,
Plot2D.del_xyscaler, Plot2D.xyscaler.__doc__)
[docs] def get_uvscaler(self, guess=False):
"""Get :attr:`uvscaler`"""
uvscaler = self.get_obj('uvscaler')
if uvscaler is not None: return uvscaler
if guess: return Plot.get_uvscaler(self, guess=True)
uvscaler = self.uvscaler = lambda u, v: self.map.rotate_vector(u, v, self.x.getValue(), self.y.getValue(), returnxy=False)
return uvscaler
uvscaler = property(get_uvscaler, Plot.set_uvscaler,
Plot.del_uvscaler, Plot.uvscaler.__doc__)
[docs] def load_data(self, data=None, lon=None, lat=None, **kwargs):
"""Data loading
It performs the following tasks:
#. Call to the generic :meth:`Plot2D.load_data` method.
#. Set attributes :attr:`lon` :attr:`lat`.
:Params:
- **data**, optional: See :meth:`~vacumm.misc.core_plot.Plot2D.load_data`.
- **lon**, optional: Longitude interval.
- **lat**, optional: Latitude interval.
.. attribute:: lon
Longitudes as a two-elements tuple or and array.
.. attribute:: lat
Latitudes as a two-elements tuple or and array.
"""
# Default bounds
default_lon = N.array([-180., 180.])
default_lat = N.array([-90., 90.])
if lon is not None:
lon = N.asarray(lon)
if lat is not None:
lat = N.asarray(lat)
# Get some keys
self.lon = lon
self.lat = lat
self.order = 'yx'
# Minimal setup
if data is None:
if 'xaxis' not in kwargs:
kwargs['xaxis'] = lon if lon is not None else default_lon
if 'yaxis' not in kwargs:
kwargs['yaxis'] = lat if lat is not None else default_lat
# Basic load
Plot2D.load_data(self, data, **kwargs)
# Longitudes and latitudes
if not self.has_data():
if self.lon is None:
self.lon = default_lon
if self.lat is None:
self.lat = default_lat
else:
# masked = False if self.masked else None
self.lon = self.get_xdata(masked=True) if self.lon is None else self.lon
self.lat = self.get_ydata(masked=True) if self.lat is None else self.lat
[docs] def pre_plot(self, map=None, projection='cyl', resolution='auto',
epsg=None, overlay=False, fullscreen=False, map_update=None,
lon_min=None, lon_max=None, lat_min=None, lat_max=None,
lon_center=None, lat_center=None, lat_ts=None,
nocache=False, cache_dir=None, zoom=None, **kwargs):
"""Plot initialisation.
:Tasks:
#. Call to the generic :meth:`Plot.pre_plot` method.
#. Setup a :class:`~mpl_toolkits.basemap.Basemap` instance
and store it in :attr:`map`.
#. Project :attr:`~Plot2D.x2d`, :attr:`~Plot2D.y2d`,
:attr:`~Plot2D.x2db` and :attr:`~Plot2D.y2db`
using :attr:`map`.
:Generic params: See :meth:`Plot.pre_plot`.
:Special params:
- **projection**: Map projection, like "merc".
See :mod:`~mpl_toolkits.basemap.Basemap` for a list of possible projections.
- **resolution**: GSHHS resolution of shoreline or 's' for Histolitt (SHOM).
- ``"c"``: Crude.
- ``"l"``: Low.
- ``"i"``: Intermediate.
- ``"h"``: High.
- ``"f"``: Full.
- ``"s"``: Histolittfor the french coast
(from a shapefile file automatically
downloaded once the license is accepted).
- **nocache**: Management of cached maps.
.. only:: html and epub
- ``0``: Read and write cached maps.
- ``1``: Only write cached maps.
- ``2``: Caching is disabled.
.. only:: latex
``0``: read and write cached maps.;
``1``: Only write cached maps;
``2``: Caching is disabled.
- **map_update**: Force the update of the map latter by :meth:`post_plot`,
setting :attr:`map_update`.
- **zoom**: Zoom on map bounds before creating it.
.. attribute:: map_update
An attribute to know if current map must be updated by :meth:`post_plot`.
It is also a parameter to method.
"""
# Aliases
self.map = kwargs.pop('m', map)
# Common init
Plot.pre_plot(self, **kwargs)
# Check if map exists
if isinstance(self.map, Plot):
self.map = getattr(self.map, 'map', None)
if self.map is None and self.get_axobj('map'):
self.map = self.get_axobj('map')
if self.map is not None:
if self.map.ax is not self.axes:
self.map.ax = self.axes
map_update = 2
elif map_update is None:
map_update = 0
if map_update is not None:
self.map_update = int(map_update)
else:
self.map_update = 2
# Create the map
if self.map is None:
kwmap = kwfilter(kwargs, 'basemap')
kwmap.update(kwfilter(kwargs, 'map_'))
lon = self.lon if self.xmasked else N.ma.getdata(self.lon)
lat = self.lat if self.ymasked else N.ma.getdata(self.lat)
if lon_min is None:
lon_min = lon.min()
if lon_max is None:
lon_max = lon.max()
if lat_min is None:
lat_min = lat.min()
if lat_max is None:
lat_max = lat.max()
projection = kwargs.pop('proj', projection)
from vacumm.misc.grid.basemap import create_map
resolution = kwargs.pop('res', resolution)
self.map = create_map(lon_min, lon_max, lat_min, lat_max, projection=projection,
resolution=resolution, overlay=overlay, fullscreen=fullscreen,
lon_center=lon_center, lat_center=lat_center, lat_ts=lat_ts, epsg=epsg,
nocache=nocache, cache_dir=cache_dir, zoom=zoom, ax=self.axes, **kwmap)
# Projection of coordinates
if self.has_data():
self.x2d, self.y2d = self.xyscaler(self.x2d, self.y2d)
self.x2db, self.y2db = self.xyscaler(self.x2db, self.y2db)
if not hasattr(self.map, 'res'): self.map.res = self.map.resolution
self.set_axobj('map', self.map)
#self._plotter = self.map
self._plotter = self.axes
def _check_map_(self):
if self.map is None: raise PlotError('Map still no set')
def __call__(self, lon, lat, inverse=False):
"""Convert coordinates with a geographic projection
using the current :meth:`~mpl_toolkits.basemap.Basemap` instance (:attr:`map`).
See :meth:`mpl_toolkits.basemap.Basemap.__call__`
"""
self._check_map_()
return self.map(lon, lat, inverse=inverse)
[docs] def plot(self, **kwargs):
"""Main plot
It performs the following tasks:
#. Call to generic :meth:`~Plot2D.plot` method.
"""
# Generic 2D plot
Plot2D.plot(self, **kwargs)
# Update map limits
self.map.set_axes_limits(ax=self.axes)
[docs] def plot_quiver(self, quiver_res_scaler=None, quiver_angles='uv', **kwargs):
if quiver_res_scaler is None:
quiver_res_scaler = self.mapproj
return Plot2D.plot_quiver(self, quiver_res_scaler=quiver_res_scaler, **kwargs)
plot_quiver.__doc__ = Plot2D.plot_quiver.__doc__
[docs] def add_lowhighs(self, size=40, weight='normal', lowtext='L', hightext='H',
shadow=False, glow=False, smooth=False, va='center', ha='center', **kwargs):
"""Mark position of mins and maxs using letters.
It is typically used for adding L and H to depressions and anticyclones.
"""
if not self.has_valid_data(): return
# Data
data = self.get_data(scalar=True)
if data.shape[0]<=2 or data.shape[1]<=2: return
# Smooth
if smooth:
data[:] = generic2d(data, smooth)
# Gradients
ndata = data.filled()
dx = N.sign(N.diff(data[1:-1], axis=1))
dy = N.sign(N.diff(data[:, 1:-1], axis=0))
# High and low fields
lows = dx[:, :-1]<0
lows &= dx[:, 1:]>0
lows &= dy[:-1]<0
lows &= dy[1:]>0
highs = dx[:, :-1]>0
highs &= dx[:, 1:]<0
highs &= dy[:-1]>0
highs &= dy[1:]<0
# Masking
if self.mask.any():
mask = self.mask[:-1] | self.mask[1:]
mask |= self.mask[:, -1] | self.mask[:, 1:]
for hl in lows, highs:
var[mask] = False
# Positions
xlows = self.x2d[1:-1, 1:-1][lows]
ylows = self.y2d[1:-1, 1:-1][lows]
xhighs = self.x2d[1:-1, 1:-1][highs]
yhighs = self.y2d[1:-1, 1:-1][highs]
# Plot text
kwsh = kwfilter(kwargs, 'shadow')
kwgl = kwfilter(kwargs, 'glow')
dict_copy_items(kwargs, [kwsh, kwgl], 'anim')
for key in 'size', 'va', 'ha', 'weight':
kwargs.setdefault(key, eval(key))
tts = []
for (xx, yy), text in (((xlows, ylows), lowtext), ((xhighs, yhighs), hightext)):
for x, y in zip(xx, yy):
tt = self.register_obj(self.axes.text(x, y, text, **kwargs), 'lowhighs', **kwargs)
tts.append(tt)
if shadow: self.add_shadow(tt, 'lowhighs_shadow', **kwsh)
if glow: self.add_glow(tt, 'lowhighs_glow', **kwgl)
del xlows, ylows, xhighs, yhighs
return tts
[docs] def add_arcgisimage(self, service, **kwargs):
"""Add an Arcgis image
Available service aliases (see :attr:`ARCGISIMAGE_ALIASES`): {}
"""
# Alias
if not service:
return
if service is True:
service = "esriimagery"
if isinstance(service, basestring):
service = ARCGISIMAGE_ALIASES.get(service, service)
# Pixels
axext = self.axes.bbox.extents
fact = 1.
xpixels = (axext[2]-axext[0])*fact
# ypixels = (axext[3]-axext[1])*fact
dpi = self.fig.dpi
# Call the basemap method
dict_check_defaults(kwargs, service=service,
xpixels=xpixels,
# ypixels=ypixels,
dpi=dpi)
try:
return self.map.arcgisimage(**kwargs)
except Exception, e:
warn('Error when plotting arcgisimage: {}.\nMessage: {}'.format(
service, e.message))
add_arcgisimage.__doc__.format(', '.join(ARCGISIMAGE_ALIASES.keys()))
def _get_posposref_(self, pos=None, posref=None, xrel=0.1, yrel=0.1, transform='axes',
xpad=30, ypad=None):
"""Get position of point and reference points in data coordinates (meters)
:Params:
- **pos**: Position in data (lon, lat), axes or figure coordinates for placement.
- **posref**: Position in data (lon, lat) coordinates of reference point.
- **x/yrel**: Default placement position relative to longitude and latitude ranges.
- **transform**: Coordinates transform for ``pos`` which defaults to "data".
Use a valid transform, or "data", "axes" or "figure".
- **x/ypad**: Padding if dots for placement when given as a string like "upper left".
Return: ``(x,y),(xref,yref)`` in meters
"""
# Placement point
offset = None
if xrel<0: xrel += 1
if yrel<0: yrel += 1
if pos is None:
lonmin = self.map.lonmin
lonmax = self.map.lonmax
latmin = self.map.latmin
latmax = self.map.latmax
dlon = lonmax-lonmin
dlat = latmax-latmin
lon0 = lonmin+xrel*dlon
lat0 = latmin+yrel*dlat
pos = tuple(self(lon0, lat0))
else:
if pos in _locations:
if xpad is not False and ypad is not False:
offset = loc2offset(pos, xpad=xpad, ypad=ypad, margin=0.01)
pos = loc2tuple(pos, xpad=0, ypad=0)
else:
pos = loc2tuple(pos, xpad=xrel, ypad=yrel)
transform = self.axes.transAxes
else:
transform = self._transform_(transform)
if transform is self.axes.transAxes or transform is self.fig.transFigure:
pos = tuple((transform-self.axes.transData).transform_point(pos))
else:
pos = tuple(self(*pos))
# Reference point
if posref is None:
posref = pos
else:
posref = self(*posref)
return pos, posref, offset
[docs] def add_mapscale(self, pos='lower left', scale=None, posref=None, barstyle='simple', transform=None,
xrel=0.1, yrel=0.1, getpos=False, posonly=False, shadow=False, zorder=10, **kwargs):
"""Add a map scale using :meth:`mpl_toolkits.basemap.Basemap.drawmapscale`
:Params:
- **pos**, optional: ``(lon,lat)`` where to draw it.
- **posref**, optional: ``(lon,lat)`` of reference position where the scale
is estimated.
- **scale**, optional: Length of the scale in projection coordinates (m).
- **barstyle**, optional: Bar style: 'simple' or 'fancy'.
- **transform**, optional: Coordinates transform for ``pos`` which defaults to "data".
Use a valid transform, or "data", "axes" or "figure".
- **x/yrel**, optional: Default placement position relative to longitude and latitude ranges.
- **x/ypad**, optional: Padding if dots for placement when given as a string like "upper left".
- **shadow**, optional: Add a shadow.
- **shadow_<param>**, optional: ``<param>`` is passed to
:meth:`~vacumm.misc.core_plot.Plot.add_shadow`.
- Othe keywords are passed to :meth:`~mpl_toolkits.basemap.Basemap.drawmapscale`.
:Example:
>>> mymap.add_mapscale((.2,.2), transform='axes')
>>> mymap.add_mapscale('upper right', posref=(-2,45), fontsize=10)
"""
if self.map is None: return
kwsh = kwfilter(kwargs, 'shadow_')
# Positions
pos, posref, offset = self._get_posposref_(pos, posref, transform=transform,
xrel=xrel if xrel<.5 else (1-xrel), yrel=yrel if yrel<.5 else (1-yrel), xpad=False)
pos = self(pos[0], pos[1], inverse=True)
if getpos and posonly: return pos
posref = self(posref[0], posref[1], inverse=True)
# Scale
if scale is None:
scales = basic_auto_scale(self.map.xmin, self.map.xmax, nmax=10)
scale = (scales[1]-scales[0])*0.001
# Draw it
kwargs['barstyle'] = barstyle
ms = self.map.drawmapscale(pos[0], pos[1], posref[0], posref[1], scale, **kwargs)
self.add_obj('mapscale', ms)
# Finalize
for o in ms:
o.set_clip_on(False)
o.set_zorder(zorder)
if shadow:
self.add_shadow(o, **kwsh)
if getpos: return ms, pos
return ms
[docs] def add_compass(self, pos='lower right', size=40, posref=None, style='simple', transform=None,
xpad=None, ypad=None, xrel=0.9, yrel=0.9, getpos=False, shadow=False, zorder=10, **kwargs):
"""Add a compass to the map using :func:`~vacumm.misc.plot.add_compass`
See :func:`~vacumm.misc.plot.add_compass` all options.
:Params:
- **pos**, optional: ``(lon,lat)`` where to draw it.
- **size**, optional: Size in pixels.
- **posref**, optional: ``(lon,lat)`` of reference position where the north
is estimated.
- **style**, optional: Compas style: 'simple' or 'fancy'.
- **transform**: Coordinates transform for ``pos`` which defaults to "data".
Use a valid transform, or "data", "axes" or "figure".
- **x/yrel**: Default placement position relative to longitude and latitude ranges.
- **x/ypad**: Padding in dots for placement when given as a string like "upper left".
- **shadow**, optional: Add a shadow.
- **shadow_<param>**, optional: ``<param>`` is passed to
:meth:`~vacumm.misc.core_plot.Plot.add_shadow`.
- Othe keywords are passed to :func:`~vacumm.misc.plot.add_compass`.
:Example:
>>> map2(data, compass=True)
>>> mymap.add_compass((-4,45), 40, facecolor1='r', text_weight='bold')
>>> mymap.add_compass(pos="upper right", style="fancy", xpad=100)
"""
if self.map is None: return
kwsh = kwfilter(kwargs, 'shadow_')
# Transform
transform = self._transform_(transform, 'axes')
# Positions
if xpad is None: xpad = int(size/2)+20
pos, posref, poffset = self._get_posposref_(pos, posref, transform=transform,
xrel=xrel, yrel=yrel, xpad=xpad, ypad=ypad)
# Departure angle from 90 for north
pr = self(posref[0], posref[1], inverse=True)
uo, vo = self.map.rotate_vector(N.array([0]),N.array([1.]),
N.array(pr[:1]),N.array(pr[1:]))
angle = N.degrees(N.arctan2(vo[0], uo[0]))-90.
# Draw it
x, y = pos
kwargs['posref'] = posref
dict_check_defaults(kwargs, offset=poffset, text_zorder=zorder)
kwfont = kwfilter(kwargs, 'font', short=True)
if 'size' in kwfont: kwargs['text_size'] = kwfont['size']
if 'color' in kwfont: kwargs['text_color'] = kwfont['color']
cp = add_compass(x, y, size=size, ax=self.axes, angle=angle, anglemode='data',
transform=self.axes.transData, zorder=zorder, **kwargs)
self.add_obj('compass', cp)
# Finalize
if shadow:
for o in cp[0]+cp[1]:
self.add_shadow(o, **kwsh)
if getpos: return cp, pos
return cp
[docs] def add_mscp(self, pos=None, posref=None, compass_size=40, transform=None,
shadow=False, color='k', zorder=10, **kwargs):
"""Plot a mapscale and a compass above
:Params:
- **pos**: Position of the mapscale (see :meth:`add_mapscale`).
The compass is drawn just above.
- **posref**, optional: Position of reference point for both mapscale and
compass.
- **mapscale_<param>**, optional: ``<param>`` is passed to
:meth:`~vacumm.misc.core_plot.Map.add_mapscale`.
- **scompass_<param>**, optional: ``<param>`` is passed to
:meth:`~vacumm.misc.core_plot.Map.add_compass`.
Example:
>>> map2(data, mscp=True, mscp_pos=(-2, 48))
>>> mymap.add_mscp('lower left')
>>> mymap.add_mscp((-4,45), mapscale_scale=100, mapscale_barstyle='fancy',
compass_size=50, compass_style='simple')
>>> mymap.add_mscp((.9,.1), transform='axes')
"""
kwms = kwfilter(kwargs, 'mapscale')
kwms['transform'] = transform
kwcp = kwfilter(kwargs, 'compass')
dict_check_defaults(kwcp, facecolor1=color, edgecolor=color, text_color=color,
zorder=zorder)
dict_check_defaults(kwms, fontcolor=color, fillcolor2=color,
zorder=zorder)
msoffset = kwms.get('offset', 0.02*(self.map.ymax-self.map.ymin))
cptextpad = kwcp.get('text_pad', 5)
cpoffset = (0, int(compass_size)/2)
# Mapscale position at top
if pos in _locations and 'top' in loc2align(pos)['va']:
pos = self.add_mapscale(pos=pos, posref=posref, getpos=True, posonly=True, **kwms)
mpos = self(*pos)
ppos = self.axes.transData.transform_point(mpos)
mpos = self.axes.transData.inverted().transform_point(
(ppos[0], ppos[1]-cpoffset[1]-cptextpad*2))
pos = self(mpos[0], mpos[1]-3*msoffset, inverse=True)
del kwms['transform']
# Mapscale
kwms.setdefault('pos', pos)
ms, pos = self.add_mapscale(posref=posref, getpos=True, **kwms)
# Compass above mapscale
pos = self(*pos)
pos = self(pos[0], pos[1]+msoffset*3, inverse=True)
kwcp.update(transform = 'data', offset=cpoffset)
kwcp.setdefault('pos', pos)
cp = self.add_compass(posref=posref, size=compass_size, **kwcp)
return ms+list(cp)
[docs] def post_plot(self, drawrivers=False, fillcontinents=True,
meridional_labels=True, zonal_labels=True,
drawcoastlines=True, drawmapboundary=True,
meridians=None, parallels=None,
land_color=None, ticklabel_size=None, refine=0,
no_seconds=False, fullscreen=False,
minutes=True, mapscale=False, compass=False, mscp=False,
bfdeg=None, lowhighs=False, arcgisimage=None,
**kwargs):
"""Post-processing of the plot
:Tasks:
#. Update the map if attribute :attr:`map_update` is True:
#) Treat continents if attribute :attr:`map_update` = 2 and
attribute :attr:`map` has a coastline resolution not ``None``:
draw coastlines, fill continents and draw rivers.
#) Draw parallels and associated labels.
#. Call to generic post processing of 2D plots (:meth:`Plot2D.post_plot`).
:Params:
- **drawrivers**: Draw rivers on the map using method
:meth:`~mpl_toolkits.basemap.Basemap.drawrivers`.
- **drawrivers_<param>**: Pass ``<param>`` to the method.
- **fillcontinents**: Fill continents with color ``land_color``
using method :meth:`~mpl_toolkits.basemap.Basemap.fillcontinents`.
- **land_color**: Fill color.
- **fillcontinents_<param>**: Pass ``<param>`` to the method.
- **drawparallels**: Display or hide parallels and associated labels using
method :meth:`~mpl_toolkits.basemap.Basemap.drawparallels`.
- **drawparallels_<param>**: Pass ``<param>`` to the method.
- **drawparallels_fmt**: Default to :class:`MinuteLabel`.
- **drawparallels_gs_<param>**: Passed to :func:`~vacumm.misc.misc.geo_scale`.
- **parallels**: Parallels to plot.
- **drawmeridians**: Display or hide medidians and associated labels using
method :meth:`~mpl_toolkits.basemap.Basemap.drawmeridians`.
- **drawmeridians_<param>**: Pass ``<param>`` to the method.
- **drawmeridians_fmt**: Default to :class:`MinuteLabel`.
- **drawmeridians_gs_<param>**: Passed to :func:`~vacumm.misc.misc.geo_scale`.
- **meridians**: Meridians to plot.
- **meridional/zonal_labels**: Display or hide meridional/zonal labels.
``meridional/zonal_labels=False`` is equivalent to ``y/xhide=True``.
- **no_seconds**: Do not display seconds in labels (if applicable).
- **minutes**: Do not use decimal degrees for labels (if applicable).
- **bfdeg**: Degrees are in bold (latex text only, if applicable).
- **x/y/ticklabels_<param>**: Pass ``<param>`` to
:meth:`~mpl_toolkits.basemap.Basemap.drawmeridians` and
:meth:`~mpl_toolkits.basemap.Basemap.drawparallels` to change text properties.
- **fullscreen**: Full screen mode -> no colorbar and no labels.
- **mapscale**: Add a map scale using
:meth:`~vacumm.misc.core_plot.Map.add_mapscale`.
- **mapscale_<param>**: Pass ``<param>`` to the
:meth:`~vacumm.misc.core_plot.Map.add_mapscale` method.
- **compass**: Add a compass using
:meth:`~vacumm.misc.core_plot.Map.add_compass`.
- **compass_<param>**: Pass ``<param>`` to the
:meth:`~vacumm.misc.core_plot.Map.add_compass` method.
- **mscp**: Add a mapscale AND a compass using
:meth:`~vacumm.misc.core_plot.Map.add_mscp`.
- **mscp_<param>**: Pass ``<param>`` to the
:meth:`~vacumm.misc.core_plot.Map.add_mscp` method.
"""
if self.map_update:
# Full screen
if fullscreen:
kwargs.setdefault('colorbar', False)
default_drawparallels = default_drawmeridians = False
else:
default_drawparallels = default_drawmeridians = True
drawparallels = kwargs.pop('drawparallels', default_drawparallels)
drawmeridians = kwargs.pop('drawmeridians', default_drawmeridians)
# Map boundaries
if drawmapboundary and not self.is3d:
try:
self.set_axobj('drawmapboundary', self.map.drawmapboundary(**kwfilter(kwargs,'drawmapboundary')))
except:
print 'Problem with Basemap.drawmapboundary'
# Continents
if self.map.res is not None and self.map_update>=2:
if land_color is None: land_color = land
kwdrawcoastlines = kwfilter(kwargs, 'drawcoastlines_')
kwdrawcoastlines.setdefault('linewidth', 0.2)
kwfillcontinents = kwfilter(kwargs, 'fillcontinents_', defaults={'color':land_color})
if self.map.resolution is not None:# GSHHS
# Draw coastlines
if drawcoastlines and not self.get_axobj('drawcoastlines'):
o = self.map.drawcoastlines(**kwdrawcoastlines)
self.set_axobj('drawcoastlines', o)
if self.is3d:
self.axes.add_collection3d(o)
# Fill continents
fcsh = kwfillcontinents.pop('shadow', False)
kwfcsh = kwfilter(kwfillcontinents, 'shadow_')
if fillcontinents and not self.get_axobj('fillcontinents'):
if not self.is3d:
try:
self.set_axobj('fillcontinents', self.map.fillcontinents(**kwfillcontinents))
except:
pass
if self.get_axobj('fillcontinents') is not None and fcsh:
add_shadow(self.get_axobj('fillcontinents'), **kwfcsh)
else:
polys = []
for polygon in self.map.landpolygons:
polys.append(polygon.get_coords())
kwfillcontinents['facecolor'] = kwfillcontinents.pop('color')
kwfillcontinents.pop('lake_color', None)
lc = PolyCollection(polys, linewidth=0, closed=False,
**kwfillcontinents)
del polys
self.axes.add_collection3d(lc)
# Draw rivers
if drawrivers and not self.get_axobj('drawrivers'):
o = self.map.drawrivers(**kwfilter(kwargs,'drawrivers'))
self.set_axobj('drawrivers', o)
if self.is3d:
self.axes.add_collection3d(o)
#elif m.res == 's': # SHOM
else:
from io import Shapes
if self.map.res=='s':
from vacumm.bathy.shorelines import Histolitt
shoreline = Histolitt(m=self.map, proj=True)
else:
shoreline = self.map.res
if isinstance(shoreline, Shapes):
try:
kwshoreline = {}
if not drawcoastlines:
kwshoreline['linewidth'] = 0
else:
kwshoreline.update(kwdrawcoastlines)
if not fillcontinents:
kwshoreline['fill'] = False
else:
kwshoreline['fillcolor'] = kwfillcontinents.pop('color')
kwshoreline.update(kwfillcontinents)
kwshoreline.update(show = False, axes=self.axes)
shoreline.plot(**kwshoreline)
except Exception, e:
import traceback
print '*** Error in shaprefile shoreline'
print traceback.format_exc()
# Tick labels and lines
kwpm_def = dict(linewidth=0.5)
kwpm_def.update(kwfilter(kwargs, 'ticklabels_'))
if ticklabel_size is not None: kwpm_def.update(fontsize=ticklabel_size)
kwp = kwpm_def.copy() ; kwm = kwpm_def.copy()
if kwargs.get('grid', True) is False:
kwp['linewidth'] = kwm['linewidth'] = 0
# - parallels
if drawparallels:
if self._xyhide_('y', kwargs.get('yhide', False)) or self.is3d:
meridional_labels = 0
kwp.setdefault('labels', [int(meridional_labels),0,0,0])
# kwp.update(kwpm_def)
kwp.update(kwfilter(kwargs, 'yticklabels_'))
kwp = kwfilter(kwargs,'drawparallels',defaults=kwp)
kwgs = kwfilter(kwp,'gs',
defaults={'vmin':self.map.llcrnrlat, 'vmax':self.map.urcrnrlat,
'minutes':minutes})
if parallels is None:
parallels = geo_scale(**kwgs)
parallels = filter(lambda _:
_>= self.map.llcrnrlat and _<= self.map.urcrnrlat, parallels)
if minutes: kwp.setdefault('fmt',
MinuteLabel(parallels, zonal=False, tex=None,
no_seconds=no_seconds, bfdeg=bfdeg))
# self.set_axobj('drawparallels', self.map.drawparallels(parallels,**kwp))
self.parallels = parallels
else:
self.parallels = None
# - meridians
if drawmeridians:
if self._xyhide_('x', kwargs.get('xhide', False)) or self.is3d:
zonal_labels = 0
kwm.setdefault('labels', [0,0,0,int(zonal_labels)])
# kwm.update(kwpm_def)
kwm.update(kwfilter(kwargs, 'xticklabels_'))
kwm = kwfilter(kwargs,'drawmeridians',defaults=kwm)
kwgs = kwfilter(kwm,'gs',
defaults={'vmin':self.map.llcrnrlon, 'vmax':self.map.urcrnrlon,
'minutes':minutes})
if meridians is None:
meridians = geo_scale(**kwgs)
if (meridians[-1]-meridians[0])>=360.: # to prevent overlaping
meridians = meridians[meridians<meridians[0]+360.]
crnlon = self.map.llcrnrlon,
meridians = filter(lambda _:
_>= self.map.llcrnrlon and _<= self.map.urcrnrlon, meridians)
if minutes:
kwm.setdefault('fmt',
MinuteLabel(meridians, zonal=True, tex=None,
no_seconds=no_seconds, bfdeg=bfdeg))
self.meridians = meridians
else:
self.meridians = None
if self.is3d:
def addto3d(what, fmt='%g', labstyle=None, **kwargs):
"""Handle lines ans labels for 3D plots"""
coords = []
for value, par in self.get_axobj(what).items():
for line in par[0]:
xy = line.get_xydata()
coords.append(xy)
x = xy[0, 0]
y = xy[0, 1]
if what=='drawmeridians':
lab = _setlatlab(fmt, value, labstyle)
zdir = 'y'
ha = 'left'
else:
lab = _setlonlab(fmt, value, labstyle)
zdir = 'x'
ha = 'right'
self.axes.text(x, y, 0, lab, ha=ha, va='center',
zdir=zdir)
if coords:
self.axes.add_collection3d(LineCollection(coords,
linewidths=[line.get_linewidth()],
linestyles=[line.get_linestyle()],
colors=[line.get_color()],
))
# - bfdeg (bold face degrees) homogeneisation
if minutes:
if (drawparallels and isinstance(kwp['fmt'], MinuteLabel) and
drawmeridians and isinstance(kwm['fmt'], MinuteLabel) and
bfdeg is None and
(kwp['fmt'].kwargs['bfdeg'] or kwm['fmt'].kwargs['bfdeg'])):
kwp['fmt'].kwargs['bfdeg'] = kwm['fmt'].kwargs['bfdeg'] = True
# - draw
if drawparallels:
self.set_axobj('drawparallels', self.map.drawparallels(parallels,**kwp))
if self.is3d:
addto3d('drawparallels', **kwp)
if drawmeridians:
# Draw
lonlabs = self.set_axobj('drawmeridians', self.map.drawmeridians(meridians,**kwm))
if self.is3d:
addto3d('drawmeridians', **kwm)
# Remove duplicated labels
llfound = []
for ll in lonlabs.keys()[:]:
llm = ll%360.
istart = 1-int(llm in llfound)
for lab in lonlabs[ll][1][1:]:
lab.set_visible(False)
llfound.append(llm)
# Plot lows and highs
lowhighs = kwargs.pop('lowhigh', lowhighs)
if lowhighs:
self.add_lowhighs(**kwfilter(kwargs, 'lowhighs'))
# Plot arcgis image
arcgisimage = kwargs.pop('arcgisimage', arcgisimage)
if arcgisimage:
self.add_arcgisimage(arcgisimage, **kwfilter(kwargs, 'arcgisimage'))
# Map scale and compass
if mscp:
kw = kwfilter(kwargs, 'mscp')
kw.update(kwfilter(kwargs, 'mapscale_', keep=True))
kw.update(kwfilter(kwargs, 'compass_', keep=True))
self.add_mscp(**kw)
else:
# Map scale
if mapscale:
self.add_mapscale(**kwfilter(kwargs, 'mapscale_'))
# Compass
if compass:
self.add_compass(**kwfilter(kwargs, 'compass_'))
if fullscreen: self.axes.set_frame_on(False)
# Basic
Plot2D.post_plot(self, **kwargs)
[docs] def get_best_loc(self, onland=True, **kwargs):
"""Best location on the plot for an object according to land/sea mask"""
return best_loc_map(self, onland=onland, **kwargs)
############################################################
## Utilities
def get_axis_scale(axis, type=None):
"""Get an axis scale relative standard units
It searches for the units of the axis,
and guess conversion factor.
:Params:
- **axis**: A :mod:`cdms2` axis.
- **type**, optional: Its type.
If ``None`` it is guessed.
See :func:`~vacumm.misc.axes.axis_type`.
:Return: A scalar which defaults to ``1.``
"""
if type is None:
type = axis_type(axis).lower()
units = getattr(axis, 'units', '')
if type=='-' or not units: return 1.
if type in ['x', 'y']:
units
#NOT FINISHED
[docs]def get_quiverkey_value(data, mode=80):
"""Get a decent value for a quiver key"""
# Scalar
if N.isscalar(data):
return data
# From components
if isinstance(data, tuple):
data = N.ma.sqrt(data[0]**2+data[1]**2)
# Reference value
if mode=='max':
vref = N.ma.max(data)
elif mode=='mean':
vref = N.ma.mean(data)
else:
if mode=='median':
mode = .5
elif not isinstance(mode, (int, float)):
raise PlotError('Unkown quiverkey value mode: {}'.format(mode))
vref = N.percentile(data, mode)
del data
# Quiverkey value
v10 = N.ma.log10(vref)
if ((v10+1) % 1) > (N.log10(.5) % 1):
v10 = N.ma.ceil(v10)
else:
v10 = N.ma.floor(v10)
return 10.**v10
############################################################
## Locators
############################################################
[docs]class AutoDateLocator2(AutoDateLocator):
"""A clever :class:`matplotlib.dates.AutoDateLocator`"""
def __init__(self, *args, **kwargs):
noweek = kwargs.pop('noweek', 2)
try:
AutoDateLocator.__init__(self, *args, **kwargs)
except:
kwargs.pop('maxticks', None)
AutoDateLocator.__init__(self, *args, **kwargs)
self._oldlocator = None
self._oldlims = None
if noweek:
self.intervald[DAILY] = [1, 2, 3, 7, 14, 21][:2+int(noweek)]
self.minticks = 3
[docs] def get_locator(self, *args, **kwargs):
# Check cache
if self._oldlocator is not None and \
self._oldlims == self.axis.get_view_interval().tolist()+self.axis.get_data_interval().tolist():
return self._oldlocator
# Get normal locator
locator = AutoDateLocator.get_locator(self, *args, **kwargs)
# Rectifications
try: # sampling interval
sampling = locator.rule._rrule._interval
except:
sampling = locator.base._base
update = False
if self._freq == DAILY and sampling%7 == 0:
# Fix daily/7 to weekly/monday locator (start on monday)
locator.rule.set(byweekday=MO)
locator.rule.set(interval=1)
update = True
elif self._freq == HOURLY and sampling > 1:
# Fix hourly locator to start at midnight
good = [2, 3, 4, 6, 8, 12]
sampling = good[min(N.searchsorted(good, sampling), len(good)-1)]
for i in xrange(sampling):
try:
locator.rule.set(byhour=range(i, 24, sampling))
except:
pass
locator.rule.set(interval=1)
update = True
if update:
locator.set_axis(self.axis)
locator.set_view_interval(*self.axis.get_view_interval())
locator.set_data_interval(*self.axis.get_data_interval())
self._oldlocator = locator
self._oldlims = self.axis.get_view_interval().tolist()+self.axis.get_data_interval().tolist()
return locator
[docs]class AutoDateMinorLocator(AutoDateLocator2):
"""An extension to the :class:`AutoDateLocator2` for minor locators"""
def __init__(self, *args, **kwargs):
kwargs.setdefault('minticks', 3)
kwargs.setdefault('maxticks', 11)
AutoDateLocator2.__init__(self, *args, **kwargs)
[docs] def viewlim_to_dt(self):
major_ticks = self.axis.get_majorticklocs()[:2]
return num2date(major_ticks[0], self.tz), num2date(major_ticks[-1], self.tz)
[docs] def datalim_to_dt(self):
return self.get_view_interval()
############################################################
## Formatters
############################################################
def _get_split_locator_(locator, kwargs):
ls = locator.split('/')
lb = locator.split(':')
if len(ls)>1 and ls[1].isdigit():
kwargs.setdefault('interval', int(ls[1]))
locator = ls[0]
elif len(lb)>1:
locator = lb[0]
by = [int(v) for v in lb[1].split(',') if v.isdigit()]
if by:
kwargs.setdefault('by'+locator.lower(), by)
return locator
def setup_time_axis(axis, auto=True, formatter=None, rotation=None,
locator=None, minor_locator=True, minor_formatter=None, nominor=False,
maxticks=None, tz=None, interval_multiples=False, **kwargs):
"""Setup tick positions and labels of an time axis
:Params:
- **axis**: :class:`~matplotlib.axis.Axis` instance (like ``P.gca().xaxis``)
- **tz**, optional: Time zone.
- **auto**, optional: Auto Scaling [default: True]
- **rotation**, optional: Rotation angle of tick labels.
If None, automatic [default: None]
- **formatter**, optional: Date format:
- 'mpl' or 0 : use the internal Matplotlib auto date locator.
- "simple" or 1 or "auto": :class:`AutoDateFormatter2`
- "dual", 2, None, tuple or dict: :class:`AutoDualDateFormatter`
(default)
- String: :class:`DateFormatter`
- else a :class:`matplotlib.ticker.Formatter` instance.
- **locator**, optional: Major locator. Can be within
['year','month','Weekday','day','hour','minute','second'],
be like :class:`matplotlib.dates.MonthLocator`.
or have a special value:
- None or 'auto' or 'vacumm': use the :class:`AutoDateLocator`
(default)
- 'mpl': use the internal Matplotlib auto date locator.
- **minor_locator**, optional: Minor locator.
- **nominor**, optional: Do not try to add minor ticks [default: False]
- **locator_<keyword>**, optional: <keyword> is passed to locator if
locator is a string. If locator = 'month',
locator = MonthLocator(locator_<keyword>=<value>).
- **minor_locator_<keyword>**, optional: Same with minor_locator.
- **maxticks**, optional: Maximal number of major ticks when
default auto locator is used.
"""
if isinstance(axis, int):
axis = 'xy'[axis]
if isinstance(axis, str):
axis = getattr(P.gca(), xy+'axis')
axes = axis.axes
iaxis = 1-int(axis.axes.xaxis is axis)
xy = 'xy'[iaxis]
yx = 'yx'[iaxis]
# Base
try:
axis.axis_date()
except:
import datetime
axis.update_units(datetime.date(2009,1,1))
# Scale axes
if auto:
axes.autoscale_view(**{'scale'+xy:True,'scale'+yx:False})
# Guess locators
major_locator = locator or kwargs.get('major_locator', None)
if nominor: minor_locator = False
locs = ['year','month','weekday','day','hour','minute','second']
locs.extend([(loc+'s') for loc in locs])
kwmjl = kwfilter(kwargs, 'major_locator', defaults=kwfilter(kwargs, 'locator'))
kwmnl = kwfilter(kwargs, 'minor_locator')
# - major
if major_locator is None:
major_locator = 'auto'
if isinstance(major_locator, basestring):
if major_locator.lower()=='mpl':
major_locator = None
else:
major_locator = _get_split_locator_(major_locator, kwmjl)
if (major_locator.lower().startswith('auto') or
major_locator.lower()=='vacumm'):
maxticks = kwargs.get('nmax_ticks', maxticks) # compat
kwmjl.setdefault('maxticks', maxticks)
major_locator = AutoDateLocator2(**kwmjl)
elif major_locator.lower() in locs:
major_locator = eval(major_locator.lower().title()+'Locator(**kwmjl)')
if major_locator:
axis.set_major_locator(major_locator)
else:
major_locator = axis.get_major_locator()
major_locator.set_axis(axis)
if hasattr(major_locator, 'interval_multiples'):
major_locator.interval_multiples = interval_multiples
# - minor
if minor_locator is 1 or minor_locator is True:
minor_locator = 'auto'
if isinstance(minor_locator, str):
minor_locator = _get_split_locator_(minor_locator, kwmnl)
if minor_locator.lower() in locs:
minor_locator = eval(minor_locator.lower().title()+'Locator(**kwmnl)')
elif minor_locator.lower().startswith('auto'):
minor_locator = AutoDateMinorLocator(**kwmnl)
else:
minor_locator = None
if minor_locator:
axis.set_minor_locator(minor_locator)
else:
minor_locator = axis.get_minor_locator()
minor_locator.set_axis(axis)
if hasattr(minor_locator, 'interval_multiples'):
minor_locator.interval_multiples = True
#print 'minor_locator', minor_locator
#print 'major_locator', major_locator
# Tick format
# - major
fmt = formatter or kwargs.get('fmt', None) or kwargs.get('major_formatter', None)
if fmt is None: fmt = 'dual' # default
if fmt == 'mpl' or fmt==0: # matplotlib
fmt = None
elif fmt == 'simple' or fmt == 1 or fmt=="auto":
fmt = AutoDateFormatter2(major_locator)
elif fmt == 'dual' or fmt is True or fmt == 2:
fmt = AutoDualDateFormatter(major_locator)
elif isinstance(fmt, basestring):
fmt = DateFormatter(fmt)
elif isinstance(fmt, list):
fmt = AutoDualDateFormatter(major_locator, *fmt)
elif isinstance(fmt, dict):
fmt = AutoDualDateFormatter(major_locator, **fmt)
elif isinstance(fmt, tuple):
if len(fmt) == 1:
fmt += ({}, )
elif not isinstance(fmt[1], dict):
fmt = (fmt[0], {})
fmt = DualDateFormatter(fmt[0], **fmt[1])
if fmt:
axis.set_major_formatter(fmt)
if rotation is not None and rotation!=0 \
and not isinstance(fmt, (DualDateFormatter, AutoDualDateFormatter)):
P.setp(axis.get_majorticklabels(), "rotation", rotation)
# - minor
if not nominor and not isinstance(minor_locator, NullLocator):
if minor_formatter is True:
minor_formatter = AutoDateFormatter2()
elif isinstance(minor_formatter, basestring):
minor_formatter = DateFormatter(minor_formatter)
if minor_formatter:
axis.set_minor_formatter(minor_formatter)
[docs]def add_agg_filter(objs, filter, zorder=None, ax=None, add=True):
"""Add a filtered version of objects to plot
:Params:
- **objs**: :class:`matplotlib.artist.Artist` instances.
- **filter**: :class:`vacumm.misc._ext_plot.BaseFilter` instance.
- **zorder**, optional: zorder (else guess from ``objs``).
- **ax**, optional: class:`matplotlib.axes.Axes` instance.
Inspired from http://matplotlib.sourceforge.net/examples/pylab_examples/demo_agg_filter.html .
"""
# Input
if not isinstance(objs, (list, tuple)):
objs = [objs]
elif len(objs)==0:
return []
# Filter
if ax is None: ax = P.gca()
shadows = FilteredArtistList(objs, filter)
if hasattr(add, 'add_artist'):
add.add_artist(shadows)
elif add:
ax.add_artist(shadows)
# Text
for t in objs:
if isinstance(t, Text):
t.set_path_effects([Normal()])
# Adjust zorder
if zorder is None or zorder is True:
same = zorder is True
if hasattr(objs, 'get_zorder'):
zorder = objs.get_zorder()
else:
zorder = objs[0].get_zorder()
if not same:
zorder -= 0.1
if zorder is not False:
shadows.set_zorder(zorder)
return shadows
[docs]def add_shadow(objs, width=3, xoffset=2, yoffset=-2, alpha=0.5, color='k',
zorder=None, ax=None, add=True):
"""Add a drop-shadow to objects
:Params:
- **objs**: :class:`matplotlib.artist.Artist` instances.
- **width**, optional: Width of the gaussian filter in points.
- **xoffset**, optional: Shadow offset along X in points.
- **yoffset**, optional: Shadow offset along Y in points.
- **color**, optional: Color of the shadow.
- **zorder**, optional: zorder (else guess from ``objs``).
- **ax**, optional: class:`matplotlib.axes.Axes` instance.
Inspired from http://matplotlib.sourceforge.net/examples/pylab_examples/demo_agg_filter.html .
"""
if color is not None: color = RGB(color)
try:
gauss = DropShadowFilter(width, offsets=(xoffset, yoffset), alpha=alpha, color=color)
return add_agg_filter(objs, gauss, zorder=zorder, ax=ax, add=add)
except:
warn('Cannot plot shadows using agg filters')
[docs]def add_glow(objs, width=3, zorder=None, color='w', ax=None, alpha=1., add=True):
"""Add a glow effect to text
:Params:
- **objs**: Plotted objects.
- **width**, optional: Width of the gaussian filter in points.
- **color**, optional: Color of the shadow.
- **zorder**, optional: zorder (else guess from ``objs``).
- **ax**, optional: class:`matplotlib.axes.Axes` instance.
Inspired from http://matplotlib.sourceforge.net/examples/pylab_examples/demo_agg_filter.html .
"""
if color is not None: color = RGB(color)
try:
white_glows = GrowFilter(width, color=color, alpha=alpha)
return add_agg_filter(objs, white_glows, zorder=zorder, ax=ax, add=add)
except:
warn('Cannot add glow effect using agg filters')
[docs]def add_lightshading(objs, width=7, fraction=0.5, zorder=None, ax=None, add=True,
**kwargs):
"""Add a light shading effect to objects
:Params:
- **objs**: Plotted objects.
- **width**, optional: Width of the gaussian filter in points.
- **fraction**, optional: Unknown.
- **zorder**, optional: zorder (else guess from ``objs``).
- **ax**, optional: class:`matplotlib.axes.Axes` instance.
- Extra keywords are passed to :class:`matplotlib.colors.LightSource`
Inspired from http://matplotlib.sourceforge.net/examples/pylab_examples/demo_agg_filter.html .
"""
if zorder is None: zorder = True
try:
lf = LightFilter(width, fraction=fraction, **kwargs)
return add_agg_filter(objs, lf, zorder=zorder, add=add, ax=ax)
except:
warn('Cannot add light shading effect using agg filters')
[docs]class MinuteLabel:
def __init__(self, m, zonal=True, **kwargs):
bfdeg = kwargs.pop('bfdeg', None)
if not isinstance(m, Basemap) and (bfdeg is None or bfdeg=='auto'):
bfdeg = False if N.size(m)==0 else (N.asarray(m)%1).ptp()!=0
if zonal:
if isinstance(m, Basemap):
auto_minutes = int(m.urcrnrlon) != int(m.llcrnrlon)
else:
auto_minutes = int(min(m)) != int(max(m))
self.func = lonlab
else:
if isinstance(m, Basemap):
auto_minutes = int(m.urcrnrlat) != int(m.llcrnrlat)
else:
auto_minutes = int(min(m)) != int(max(m))
self.func = latlab
kwargs['bfdeg'] = bfdeg
kwargs['decimal'] = False
kwargs['no_zeros'] = True
kwargs['auto_minutes'] = auto_minutes
self.kwargs = kwargs
def __call__(self, deg):
return self.func(deg, **self.kwargs)
[docs]class AutoDegreesMinutesLocator(AutoLocator):
[docs] def bin_boundaries(self, vmin, vmax):
steps = None
mn = 1/60.
if (vmax-vmin) > 50.:
steps = [1, 2, 3, 6, 9, 10]
elif (vmax-vmin) > 3.:
steps = [1, 2, 2.5, 3, 5, 10]
elif vmax//mn != vmin//mn:
nmn = int(N.ceil((vmax-vmin)*60.))+1
nmax = 10
if nmn > nmax:
steps = [1, 10/6.,15/6.,20/6.,30/6.,10]
self.set_params(steps=steps)
return AutoLocator.bin_boundaries(self, vmin, vmax)
[docs]def twinxy(xy, ax=None, fig=None):
"""Create an :func:`~matplotlib.axes.Axes` instance based on existing one(s)*
This is an fusion and extension of :func:`matplotlib.pyplot.twinx` and
:func:`matplotlib.pyplot.twiny`
:Params:
- **xy**: A string containing ``"x"`` and/or ``"y"``
- **ax**, optional: Consider this axes instance instead of the current one.
- **fig**, optional: Consider this figure instead of the current one.
"""
if ax is None:
if fig is None:
ax = P.gca()
else:
ax = fig.gca()
if not isinstance(xy, str): return ax
fig = ax.figure
twinx = 'x' in xy
twiny = 'y' is xy
if not twinx and not twiny:
return ax
kw = dict(frameon=False)
if twinx and not twiny:
kw.update(sharex=ax)
if twiny and not twinx:
kw.update(sharex=ax)
nax = fig.add_axes(ax.get_position(True), **kw)
if twinx:
nax.yaxis.tick_right()
nax.yaxis.set_label_position('right')
ax.yaxis.tick_left()
if not twiny: nax.xaxis.set_visible(False)
if twiny:
nax.xaxis.tick_right()
nax.xaxis.set_label_position('right')
ax.xaxis.tick_left()
if not twinx: nax.yaxis.set_visible(False)
return nax
def _add_label_(text, ax0, ax1, ay0, ay1, ax, va, ha, rotation, shadow, glow, **kwargs):
if ax is None: ax = P.gca()
fig = ax.figure
b = ax.get_position()
x = ax0*b.x0 + ax1*b.x1
y = ay0*b.y0 + ay1*b.y1
kwsh = kwfilter(kwargs, 'shadow')
kwgl = kwfilter(kwargs, 'glow')
o = fig.text(x, y, text, va=va, ha=ha, rotation=rotation, **kwargs)
if shadow:
add_shadow(o, ax=ax, **kwsh)
if glow:
add_glow(o, ax=ax, **kwgl)
return o
[docs]def add_left_label(text, pos=.1, ax=None, va='center', ha='left',
rotation=90., shadow=False, glow=False, **kwargs):
"""Add a text label to the left of a plot"""
return _add_label_(text, 1-pos, pos, .5, .5, ax, va, ha, rotation, shadow, glow, **kwargs)
[docs]def add_right_label(text, pos=.1, ax=None, va='center', ha='right',
rotation=-90., shadow=False, glow=False, **kwargs):
"""Add a text label to the left of a plot"""
return _add_label_(text, pos, 1-pos, .5, .5, ax, va, ha, rotation, shadow, glow, **kwargs)
[docs]def add_top_label(text, pos=.1, ax=None, va='top', ha='center',
rotation=0., shadow=False, glow=False, **kwargs):
"""Add a text label to the left of a plot"""
return _add_label_(text, .5, .5, pos, 1-pos, ax, va, ha, rotation, shadow, glow, **kwargs)
[docs]def add_bottom_label(text, pos=.1, ax=None, va='top', ha='center',
rotation=0., shadow=False, glow=False, **kwargs):
"""Add a text label to the left of a plot"""
return _add_label_(text, .5, .5, 1-pos, pos, ax, va, ha, rotation, shadow, glow, **kwargs)
_locations = ['upper right',
'upper left',
'lower left',
'lower right',
'center left',
'center right',
'lower center',
'upper center',
'center center',
'center']
def loc2tuple(loc, xpad=0.02, ypad=0.02):
"""Location as tuple of (x,y) in relative coordinates"""
if isinstance(loc, tuple): return loc
if isinstance(loc, basestring):
if len(loc.split())==1:
loc = loc+' '+loc
assert loc in _locations, 'loc must be a tuple or a string (%s)'% (", ".join(_locations))
sy, sx = loc.split()
xpad = abs(xpad)
if xpad>.5: xpad = 1-xpad
ypad = abs(ypad)
if ypad>.5: ypad = 1-ypad
x = {"left":xpad, 'center':.5, 'right':1-ypad}[sx]
y = {"lower":xpad, 'center':.5, 'upper':1-ypad}[sy]
return x, y
def loc2align(loc, ha=None, va=None, margin=1/3.):
"""From location to dict of ha and va
:Params:
- **loc**: (x,y) or string like "upper right.
- **ha/va**, optional: alignments.
:Return: dict(ha=ha, va=va)
"""
x, y = loc2tuple(loc, xpad=0, ypad=0)
if ha is None:
if x<margin:
ha = 'left'
elif x>(1-margin):
ha = 'right'
else:
ha = 'center'
if va is None:
if y<margin:
va = 'bottom'
elif y>(1-margin):
va = 'top'
else:
va = 'center'
return dict(ha=ha, va=va)
def loc2offset(loc, xpad, ypad=None, margin=1/3.):
"""From location to (xoffset, ypoffset) where x/yoffset = +/- x/ypad
:Return: (xoffset,yoffset)
"""
align = loc2align(loc, margin=margin)
xoffset = yoffset = 0
if ypad is None: ypad = xpad
if align['ha'] == 'left':
xoffset = xpad
elif align['ha'] == 'right':
xoffset = -xpad
if align['va'] == 'top':
yoffset = -ypad
elif align['va'] == 'bottom':
yoffset = ypad
return xoffset, yoffset
def best_loc_map(m, onland=True, allowed=_locations):
"""Find the best location on a plot map according to the land/ocean repartition"""
from collections import OrderedDict
if isinstance(m, Map): m = m.map
if not hasattr(m, 'coastpolygons'):
sloc = allowed[0]
else:
from grid.masking import polygons
from grid import bounds2d
from _geoslib import Polygon
fractions = N.zeros((3, 3))
polys = polygons(m)
x = m.xmin + (m.xmax-m.xmin)*(0.5+N.arange(3))/3.
y = m.ymin + (m.ymax-m.ymin)*(0.5+N.arange(3))/3.
xxb, yyb = bounds2d(x, y)
for j in xrange(3):
for i in xrange(3):
pcell = Polygon(N.array([xxb[j, i], yyb[j, i]]).T)
for pcoast in polys:
if pcell.within(pcoast):
fractions[j, i] = 1.
break
if not pcell.intersects(pcoast):
continue
for p in pcell.intersection(pcoast):
fractions[j, i] += p.area()
if not onland:
fractions = 1-fractions
jmax, imax = N.unravel_index(fractions.argmax())
sloc = '%s %s'%(['left', 'center', 'right'][imax], ['bottom', 'center', 'top'][jmax])
return _loc2tuple_(sloc)
[docs]def add_param_label(text, loc=(0.01, 0.01), color='0.4', size=8, family='monospace', fig=None, **kwargs):
"""Add parameters description to the bottom/left of the figure
:Example:
>>> c = curve2(sst, show=False)
>>> c.add_param_label(dict(max=.23, kz=0.25))
:Params:
- **text**: Either a string or a dictionary.
"""
# Convert dict to text
if isinstance(text, dict):
clsname = text.__class__.__name__
tt = []
for item in text.items():
tt.append('%s=%s'%item)
text = ', '.join(tt)
if fig is None: fig = P.gcf()
loc = loc2tuple(loc, xpad=0.01, ypad=0.01)
return fig.text(loc[0], loc[1], str(text), color=color, size=size, family=family, **kwargs)
class Animator(object):
"""Animate objects on a figure
:Example:
>>> anim = Animator()
>>> anim.append(P.plot([5,6])
>>> anim.append(P.plot([4,8])
>>> anim.make_animation()
"""
def __init__(self, fig=None):
self.objs = []
if fig is None: fig = P.gcf()
self.fig = fig
def append(self, obj, frame=None):
# Set frame index
nobjs = len(self.objs)
if frame == 'last':
frame == -1
elif frame=='new':
frame = None
if nobjs==0:
frame = None
if isinstance(frame, int):
if frame<0:
frame = max(0, nobjs-frame)
elif frame>nobjs-1:
frame = None
else:
frame = min(nobjs-1, frame)
# Get layer
if frame is None: # New layer
frameobjs = []
self.objs.append(frameobjs)
frame = len(self.objs)-1
else: # Old layer
frameobjs = self.objs[frame]
# Append data
if isinstance(obj, list):
if hasattr(obj[0], 'get_figure'):
if self.fig is None and obj:
self.fig = obj[0].get_figure()
else:
assert self.fig == obj[0].get_figure()
frameobjs.extend(obj)
else:
if hasattr(obj, 'get_figure'):
if self.fig is None:
self.fig = obj.get_figure()
else:
assert self.fig == obj.get_figure()
frameobjs.append(obj)
return frame
def make_animation(self, **kwargs):
"""Create the animation object"""
if self.fig is None: return
if not self.objs: return
from matplotlib.animation import ArtistAnimation
self.animation = ArtistAnimation(self.fig, self.objs, **kwargs)
return self.animation
[docs]def hlitvs(color='.95', axis='x', units='ticks', axes=None, maxticks=10, **kwargs):
"""Highlight intervals between ticks
:Params:
- *color*: Background color.
- *axis*: Matplotlib axis or 'x'/'y'.
- **units**, optional: Interval types
- `"ticks"`: Use axis ticks.
- A list or array: use it as ticks.
- `"auto"`: Estimate ticks using :class:`~matplotlib.ticker.AutoLocator`.
- `"date"` or `"time"`: Same but using :class:`AutoDateLocator2`.
- A time string like `"day"` for using :class:`~matplotlib.dates.DayLocator`.
- A locator.
- Other keyparam are passed to :func:`~matplotlib.pyplot.axhspan`
or :func:`~matplotlib.pyplot.axvspan`.
"""
# Get axis
axes = kwargs.pop('ax', axes)
if isinstance(axis, Axes):
axes = axis
axis = 'x'
if isinstance(axis, basestring):
if units.endswith('s'): units = units[:-1]
if axes is None: axes = P.gca()
axis = getattr(axes, axis+'axis')
xy = 'xy'[isinstance(axis, YAxis)]
axes = axis.axes
# Locators and ticks
kwloc = kwfilter(kwargs, 'locator_')
if units=='ticks':
ticks = axis.get_ticklocs()
elif isinstance(units, (list, N.ndarray)):
ticks = units
else: # locator
if units == 'auto':
locator = AutoLocator(**kwloc)
elif units=='date' or units=='time':
locator = AutoDateLocator2(**kwloc)
elif isinstance(units, basestring):
locator = getattr(matplotlib.dates, units.title()+'Locator')(**kwloc)
locator.set_axis(axis)
ticks = locator()
tmin, tmax = axis.get_view_interval()
if isinstance(ticks, N.ndarray):
ticks = ticks.tolist()
else:
ticks = list(ticks)
if tmin!=ticks[0]: ticks.insert(0, tmin)
if tmax!=ticks[-1]: ticks.append(tmax)
# Patch
alpha = RGBA(color)[-1]
kwargs.setdefault('zorder', 0)
kwargs.update(facecolor=color)
kwargs.setdefault('linewidth', 0)
kwargs.setdefault('label', '_nolegend_')
kwargs.setdefault('alpha', alpha)
objs = []
axspan = getattr(axes, 'ax%sspan'%'vh'[xy=='y'])
for i in xrange(0, len(ticks)-1, 2):
objs.append(axspan(ticks[i], ticks[i+1], **kwargs))
# objs[-1].set_zorder(0)
return objs
class _PPatch(Patch):
"""
Base class for patches with size in pixels.
You must define your coordinates in attribute _path_coords
between -5 and 5.
"""
def __str__(self):
return "Compass(%s,%s;%s)" % (self.center[0], self.center[1], self.size)
def __init__(self, xy, size, angle=0, anglemode='pixels', posref=None, offset=None,
**kwargs):
"""
*xy*
center of compass
*size*
size of compass
"""
Patch.__init__(self, **kwargs)
self.center = xy
self.size = size
self.angle = angle
self.anglemode = anglemode
self.posref = posref
self.offset = offset
path_coords = N.array(self._path_coords, 'f')
path_coords *= 0.1
self._path = Path(path_coords, closed=False)
self._patch_transform = mtransforms.IdentityTransform()
def _update_transform_(self):
self._patch_transform = self._get_pixrot_transform_()
def _get_pixrot_transform_(self, size=None):
if size is None: size=self.size
transform = Artist.get_transform(self) if self.is_transform_set() else None
return _transform_pixrot_(self.center, size, self.angle, self.anglemode,
ax = self.axes, posref=self.posref, transform=transform)
def get_patch_transform(self):
self._update_transform_()
return self._patch_transform
def get_path(self):
return self._path
def get_transform(self):
t = Patch.get_transform(self)
if self.offset is not None:
t = t+mtransforms.Affine2D().translate(*self.offset)
return t
def draw(self, renderer):
self._update_transform_()
Patch.draw(self, renderer)
class QuarterRightCompass(_PPatch):
_path_coords = [[0, 0], [1, 1], [0, 5]]
# _path_coords = [[0, 0], [1, 0], [1, 1], [0, 1]]
class QuarterLeftCompass(_PPatch):
_path_coords = [[0, 0], [-1, 1], [0, 5]]
class ArrowCompass(_PPatch):
_path_coords = [[-.75, -5], [-.25, 3.5], [-1, 3], [0, 5], [1, 3], [.25, 3.5],
[.75, -5], [0, -4]]
class PPText(Text):
def __init__(self, *args, **kwargs):
self._vc_offset = kwargs.pop('offset', 0)
self._vc_ppatch = kwargs.pop('ppatch', 0)
Text.__init__(self, *args, **kwargs)
def get_transform(self):
t = self._vc_ppatch._get_pixrot_transform_(self._vc_ppatch.size+self._vc_offset)+ \
Artist.get_transform(self._vc_ppatch)
if self._vc_ppatch.offset is not None:
t = t + mtransforms.Affine2D().translate(*self._vc_ppatch.offset)
return t
def _transform_(transform, default=None, ax=None, fig=None):
if ax is None: ax = P.gca()
if default is None: default = ax.transData
if transform is None: return default
if isinstance(transform, basestring):
if transform.lower()=='figure':
if fig is None:
fig = P.gcf() if ax is None else ax.fig
return fig.transFigure
if hasattr(ax, 'trans'+transform.title()):
return getattr(ax, 'trans'+transform.title())
return transform
def _transform_pixrot_(center, size, angle, anglemode, transform=None, posref=None, ax=None):
"""Special transform: size in pixel, rotation, translation in data
:Params:
- **trandform**: Transform for the center location.
- **posref**: Reference point for the angle if anglemode == 'data'.
It default to the center. Values must be in data coordinates.
"""
if ax is None: ax = P.gca()
transform = _transform_(transform, 'data', ax=ax)
# center
xp0, yp0 = transform.transform(center)
# scales
xp1, yp1 = xp0 + size, yp0 + size
xcorner, ycorner = transform.inverted().transform_point((xp1, yp1))
xscale = xcorner-center[0]
yscale = ycorner-center[1]
# angle
anglemode = str(anglemode)
if anglemode is None or anglemode.startswith('dot'): anglemode = 'pixels'
if not anglemode.startswith('pix') and not anglemode.startswith('dat'):
raise PlotError('Unkown anglemode: %s'%anglemode)
if anglemode.startswith('dat'): # from data to pixels
ar = N.radians(angle)
if posref is None:
posref = center
if transform is not ax.transData:
posref = (transform-ax.transData).transform_point(posref)
if posref[0]>1.e20 or posref[1]>1e20:
raise PlotError('Coordinates of reference seems wrong: %s,%s'%tuple(posref))
xlen = ylen = min(posref)*0.01
xr0, yr0 = ax.transData.transform_point(posref)
xr1, yr1 = ax.transData.transform_point((posref[0]+xlen*N.cos(ar),
posref[1]+ylen*N.sin(ar)))
angle = N.degrees(N.arctan2(yr1-yr0, xr1-xr0))
# Transform
t = mtransforms.Affine2D() \
.rotate_deg(angle) \
.scale(xscale, yscale) \
.translate(*center)
return t
def _add_text_(ax, text, **kwtext):
ax._set_artist_props(text)
text.update(kwtext)
ax.texts.append(text)
return text
[docs]def add_compass(x, y, size=40, facecolor1='k', facecolor2='w', edgecolor='k',
text_color='k', text_size='medium', linewidth=0.5, alpha=1, text_pad=5,
style='simple',
angle=0, ax=None, m=None, anglemode='pixels', **kwpatch):
"""Add a compass to the current plot
:Params:
- **x/y**: Position in data coordinates.
- **size**, optional: size in opixels.
- **style**, optional: "simple", "fancy".
- **angle**, optional: Angle of the north (trigo).
- **anglemode**, optional: "pixels" or "data".
- **facecolor1**, optional: Dark face color.
- **facecolor2**, optional: Light face color.
- **edgecolor**, optional: Edges color.
- **text_color**, optional: Text labels color.
- **text_size**, optional: Text labels size.
- **text_<param>**, optional: ``<param>`` is passed to
:class:`~matplotlib.text.Text` when adding labels.
- **fontsize/color**, optional: Same as ``text_size/color``.
"""
kwtext = kwfilter(kwpatch, 'text_')
kwtext.update(color=text_color, size=text_size, alpha=alpha)
if ax is None:
if m:
ax = m.ax or m._check_ax
else:
ax = P.gca()
if m:
x, y = m(x, y)
if style is None: style = 'arrow'
else: style = str(style)
# Loop on quarters
dict_check_defaults(kwpatch, linewidth=linewidth, alpha=alpha, clip_on=False,
edgecolor=edgecolor)
patches = []
texts = []
if style.startswith('a') or style.startswith('s'):
# Patch
kwp = kwpatch.copy()
kwp['facecolor'] = facecolor1
patches.append(ax.add_patch(ArrowCompass((x, y), size, angle=angle, anglemode=anglemode, **kwp)))
# Label (north only)
text = PPText(0, .5, 'N', offset=text_pad, ppatch=patches[-1], ha='center', va='bottom')
texts.append(_add_text_(ax, text, **kwtext))
elif style.startswith('f'):
for iq, (label, ha, va) in enumerate([
('N', 'center', 'bottom'),
('W', 'right', 'center'),
('S', 'center', 'top'),
('E', 'left', 'center')]):
aa = iq*90.
# Loop on back/white patches
for (C, fc) in [
(QuarterRightCompass, facecolor1),
(QuarterLeftCompass, facecolor2),
][:]:
kwp = kwpatch.copy()
kwp['facecolor'] = fc
patches.append(ax.add_patch(C((x, y), size, angle=aa+angle,
anglemode=anglemode, **kwp)))
# Add text labels
text = PPText(0, .5, label, offset=text_pad, ppatch=patches[-1], ha=ha, va=va)
texts.append(_add_text_(ax, text))
else:
raise PlotError('Unknown compass style. Please use one of : '+
', '.join(['simple', 'fancy']))
return patches, texts
def _asnum_(xy, atleast_1d=False):
"""Get xy as a number
If it is a number or a numpy array, it not converted,
else it is converted with :func:`numtime`.
xy can also be a list, a list of lists, etc.
"""
if isinstance(xy, N.ndarray):
return xy
single = not isinstance(xy, list)
xys = xy if not single else [xy]
out = []
for xy in xys:
if is_numtime(xy):
out.append(xy)
continue
if isinstance(xy, list):
out.extend(xy)
continue
xy = numtime(xy)
out.append(xy)
if atleast_1d:
return N.ma.atleast_1d(out)
if single:
return out[0]
return out
docfiller.scan(Plot, Plot.format_axes, Plot.load_data, Plot._check_order_,
Plot.pre_plot, Plot.post_plot,
Plot1D._set_axes_, Plot1D._check_order_,
Curve.plot, Curve.load_data,
Bar.plot,
Stick.load_data, Stick.plot,
ScalarMappable, ScalarMappable.colorbar, ScalarMappable.post_plot,
ScalarMappable.get_cmap, ScalarMappable.get_levels,
Plot2D.load_data,
Plot2D.plot, Plot2D.plot_contour, Plot2D.plot_fill,
Plot2D.plot_quiver, Plot2D._set_axes_,
Map.load_data, Map.pre_plot, Map.post_plot,
quiverkey_=QuiverKey.quiverkey,
savefig_=Plot.savefig)