Skip to content

Commit

Permalink
Merge 1118bf6 into e423f45
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Feb 18, 2016
2 parents e423f45 + 1118bf6 commit 915bb56
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 55 deletions.
11 changes: 11 additions & 0 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,17 @@ def sanitize(self, name, valid_fn):
label_sanitizer = sanitize_identifier_fn.instance()
dimension_sanitizer = sanitize_identifier_fn.instance(capitalize=False)


def isnumeric(val):
if isinstance(val, (basestring, bool, np.bool_)):
return False
try:
float(val)
return True
except:
return False


def find_minmax(lims, olims):
"""
Takes (a1, a2) and (b1, b2) as input and returns
Expand Down
2 changes: 1 addition & 1 deletion holoviews/plotting/mpl/chart3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Plot3D(ColorbarPlot):
bgcolor = param.String(default='white', doc="""
Background color of the axis.""")

projection = param.ObjectSelector(default='3d', doc="""
projection = param.ObjectSelector(default='3d', objects=['3d'], doc="""
The projection of the matplotlib axis.""")

show_frame = param.Boolean(default=False, doc="""
Expand Down
36 changes: 24 additions & 12 deletions holoviews/plotting/mpl/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from ...core import util
from ...core import (OrderedDict, Collator, NdOverlay, HoloMap, DynamicMap,
CompositeOverlay, Element3D, Columns, NdElement)
CompositeOverlay, Element3D, Columns, NdElement, Element)
from ...element import Table, ItemTable
from ..plot import GenericElementPlot, GenericOverlayPlot
from ..util import dynamic_update
Expand Down Expand Up @@ -232,25 +232,36 @@ def _finalize_limits(self, axis, view, subplots, ranges):
extents = self.get_extents(view, ranges)
if extents and not self.overlaid:
coords = [coord if np.isreal(coord) else np.NaN for coord in extents]
valid_lim = lambda c: util.isnumeric(c) and not np.isnan(c)
if isinstance(view, Element3D) or self.projection == '3d':
l, b, zmin, r, t, zmax = coords
zmin, zmax = (c if np.isfinite(c) else None for c in (zmin, zmax))
if not zmin == zmax:
axis.set_zlim((zmin, zmax))
zmin, zmax = (c if util.isnumeric(c) and not np.isnan(c) else None
for c in (zmin, zmax))
if zmin != zmax:
if valid_lim(zmin):
axis.set_zlim(bottom=zmin)
if valid_lim(zmax):
axis.set_zlim(top=zmax)
else:
l, b, r, t = [coord if np.isreal(coord) else np.NaN for coord in extents]
if self.invert_axes:
l, b, r, t = b, l, t, r
l, r = (c if np.isfinite(c) else None for c in (l, r))

if self.invert_xaxis or any(p.invert_xaxis for p in subplots):
r, l = l, r
if not l == r:
axis.set_xlim((l, r))
b, t = (c if np.isfinite(c) else None for c in (b, t))
if l != r:
if valid_lim(l):
axis.set_xlim(left=l)
if valid_lim(r):
axis.set_xlim(right=r)

if self.invert_yaxis or any(p.invert_yaxis for p in subplots):
t, b = b, t
if not b == t:
axis.set_ylim((b, t))
if b != t:
if valid_lim(b):
axis.set_ylim(bottom=b)
if valid_lim(t):
axis.set_ylim(top=t)


def _finalize_axes(self, axis):
Expand Down Expand Up @@ -638,10 +649,11 @@ class OverlayPlot(LegendPlot, GenericOverlayPlot):
_passed_handles = ['fig', 'axis']

def __init__(self, overlay, ranges=None, **params):
if overlay.traverse(lambda x: x, (Element3D,)):
params['projection'] = '3d'
if 'projection' not in params:
params['projection'] = self._get_projection(overlay)
super(OverlayPlot, self).__init__(overlay, ranges=ranges, **params)


def _finalize_artist(self, key):
for subplot in self.subplots.values():
subplot._finalize_artist(key)
Expand Down
29 changes: 9 additions & 20 deletions holoviews/plotting/mpl/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,11 @@ class MPLPlot(DimensionedPlot):
sublabel_size = param.Number(default=18, doc="""
Size of optional subfigure label.""")

projection = param.ObjectSelector(default=None,
objects=['3d', 'polar', None], doc="""
projection = param.Parameter(default=None, doc="""
The projection of the plot axis, default of None is equivalent to
2D plot, '3d' and 'polar' are also supported.""")
2D plot, '3d' and 'polar' are also supported by matplotlib by default.
May also supply a custom projection that is either a matplotlib
projection type or implements the `_as_mpl_axes` method.""")

show_frame = param.Boolean(default=True, doc="""
Whether or not to show a complete frame around the plot.""")
Expand Down Expand Up @@ -330,9 +331,8 @@ def _get_size(self):
def _create_subplots(self, layout, axis, ranges, create_axes):
layout = layout.map(Compositor.collapse_element, [CompositeOverlay],
clone=False)
norm_opts = self._deep_options(layout, 'norm', ['axiswise'], [Element])
axiswise = all(v.get('axiswise', False) for v in norm_opts.values())

norm_opts = self._traverse_options(layout, 'norm', ['axiswise'], [Element])
axiswise = all(norm_opts['axiswise'])
if not ranges:
self.handles['fig'].set_size_inches(self.fig_inches)
subplots, subaxes = OrderedDict(), OrderedDict()
Expand All @@ -354,9 +354,8 @@ def _create_subplots(self, layout, axis, ranges, create_axes):
# Create axes
kwargs = {}
if create_axes:
threed = issubclass(vtype, Element3D) if vtype else None
subax = plt.subplot(self._layoutspec[r, c],
projection='3d' if threed else None)
projection = self._get_projection(view) if vtype else None
subax = plt.subplot(self._layoutspec[r, c], projection=projection)
if not axiswise and self.shared_xaxis and self.xaxis is not None:
self.xaxis = 'top'
if not axiswise and self.shared_yaxis and self.yaxis is not None:
Expand Down Expand Up @@ -962,17 +961,7 @@ def _create_subplots(self, layout, positions, layout_dimensions, ranges, axes={}
continue

# Determine projection type for plot
components = view.traverse(lambda x: x)
projs = ['3d' if isinstance(c, Element3D) else
self.lookup_options(c, 'plot').options.get('projection', None)
for c in components]
projs = [p for p in projs if p is not None]
if len(set(projs)) > 1:
raise Exception("A single axis may only be assigned one projection type")
elif projs:
projections.append(projs[0])
else:
projections.append(None)
projections.append(self._get_projection(view))

if not create:
continue
Expand Down
83 changes: 71 additions & 12 deletions holoviews/plotting/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"""

from itertools import groupby, product
from collections import Counter
from collections import Counter, defaultdict

import numpy as np
import param
Expand Down Expand Up @@ -166,7 +166,11 @@ class DimensionedPlot(Plot):
of plotting. Allows selecting normalization at different levels
for nested data containers.""")

projection = param.ObjectSelector(default=None)
projection = param.Parameter(default=None, doc="""
Allows supplying a custom projection to transform the axis
coordinates during display. Example projections include '3d'
and 'polar' projections supported by some backends. Depending
on the backend custom projection objects may be supplied.""")

def __init__(self, keys=None, dimensions=None, layout_dimensions=None,
uniform=True, subplot=False, adjoined=None, layout_num=0,
Expand Down Expand Up @@ -393,15 +397,67 @@ def _compute_group_range(group, elements, ranges):


@classmethod
def _deep_options(cls, obj, opt_type, opts, specs=None):
"""
Traverses the supplied object getting all options
in opts for the specified opt_type and specs
"""
lookup = lambda x: ((type(x).__name__, x.group, x.label),
{o: cls.lookup_options(x, opt_type).options.get(o, None)
for o in opts})
return dict(obj.traverse(lookup, specs))
def _traverse_options(cls, obj, opt_type, opts, specs=None, keyfn=None):
"""
Traverses the supplied object getting all options in opts for
the specified opt_type and specs. Also takes into account the
plotting class defaults for plot options. If a keyfn is
supplied the returned options will be grouped by the returned
keys.
"""
def lookup(x):
"""
Looks up options for object, including plot defaults,
keyfn determines returned key otherwise None key is used.
"""
options = cls.lookup_options(x, opt_type)
selected = {o: options.options[o]
for o in opts if o in options.options}
if opt_type == 'plot':
plot = Store.registry[cls.renderer.backend].get(type(x))
selected['defaults'] = {o: getattr(plot, o) for o in opts
if o not in selected and hasattr(plot, o)}
key = keyfn(x) if keyfn else None
return (key, selected)

# Traverse object and accumulate options by key
traversed = obj.traverse(lookup, specs)
options = defaultdict(lambda: defaultdict(list))
default_opts = defaultdict(lambda: defaultdict(list))
for key, opts in traversed:
defaults = opts.pop('defaults', {})
for opt, v in opts.items():
options[key][opt].append(v)
for opt, v in defaults.items():
default_opts[key][opt].append(v)

# Merge defaults into dictionary if not explicitly specified
for key, opts in default_opts.items():
for opt, v in opts.items():
if opt not in options[key]:
options[key][opt] = v
return options if keyfn else options[None]


def _get_projection(cls, obj):
"""
Uses traversal to find the appropriate projection
for a nested object. Respects projections set on
Overlays before considering Element based settings,
before finally looking up the default projection on
the plot type. If more than one non-None projection
type is found an exception is raised.
"""
isoverlay = lambda x: isinstance(x, CompositeOverlay)
opts = cls._traverse_options(obj, 'plot', ['projection'],
[CompositeOverlay, Element],
keyfn=isoverlay)
from_overlay = not all(p is None for p in opts[True]['projection'])
projections = opts[from_overlay]['projection']
custom_projs = [p for p in projections if p is not None]
if len(set(custom_projs)) > 1:
raise Exception("An axis may only be assigned one projection type")
return custom_projs[0] if custom_projs else None


def update(self, key):
Expand Down Expand Up @@ -525,7 +581,10 @@ def get_extents(self, view, ranges):
else:
y0, y1 = (np.NaN, np.NaN)
if self.projection == '3d':
z0, z1 = ranges[dims[2].name]
if len(dims) > 2:
z0, z1 = ranges[dims[2].name]
else:
z0, z1 = np.NaN, np.NaN
else:
x0, x1 = view.range(0)
y0, y1 = view.range(1) if ndims > 1 else (np.NaN, np.NaN)
Expand Down
11 changes: 1 addition & 10 deletions holoviews/plotting/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,9 @@
from ...core import OrderedDict, NdMapping
from ...core.options import Store
from ...core.util import (dimension_sanitizer, safe_unicode, basestring,
unique_iterator, unicode)
unique_iterator, unicode, isnumeric)
from ...core.traversal import hierarchical

def isnumeric(val):
if isinstance(val, (basestring, bool, np.bool_)):
return False
try:
float(val)
return True
except:
return False

def escape_vals(vals, escape_numerics=True):
"""
Escapes a list of values to a string, converting to
Expand Down

0 comments on commit 915bb56

Please sign in to comment.