Skip to content

Commit

Permalink
Merge 95a4577 into 2b51c63
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Jan 11, 2020
2 parents 2b51c63 + 95a4577 commit 83ca2fd
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 31 deletions.
24 changes: 12 additions & 12 deletions holoviews/plotting/bokeh/element.py
Expand Up @@ -40,7 +40,7 @@
TOOL_TYPES, bokeh_version, date_to_integer, decode_bytes, get_tab_title,
glyph_order, py2js_tickformatter, recursive_model_update,
theme_attr_json, cds_column_replace, hold_policy, match_dim_specs,
compute_layout_properties, wrap_formatter)
compute_layout_properties, wrap_formatter, match_ax_type)



Expand Down Expand Up @@ -282,7 +282,7 @@ def _get_hover_data(self, data, element, dimensions=None):
data[dim] = [v for _ in range(len(list(data.values())[0]))]


def _merge_ranges(self, plots, xspecs, yspecs):
def _merge_ranges(self, plots, xspecs, yspecs, xtype, ytype):
"""
Given a list of other plots return axes that are shared
with another plot by matching the dimensions specs stored
Expand All @@ -293,14 +293,14 @@ def _merge_ranges(self, plots, xspecs, yspecs):
if plot is None:
continue
if hasattr(plot, 'x_range') and plot.x_range.tags and xspecs is not None:
if match_dim_specs(plot.x_range.tags[0], xspecs):
if match_dim_specs(plot.x_range.tags[0], xspecs) and match_ax_type(plot.xaxis, xtype):
plot_ranges['x_range'] = plot.x_range
if match_dim_specs(plot.x_range.tags[0], yspecs):
if match_dim_specs(plot.x_range.tags[0], yspecs) and match_ax_type(plot.xaxis, ytype):
plot_ranges['y_range'] = plot.x_range
if hasattr(plot, 'y_range') and plot.y_range.tags and yspecs is not None:
if match_dim_specs(plot.y_range.tags[0], yspecs):
if match_dim_specs(plot.y_range.tags[0], yspecs) and match_ax_type(plot.yaxis, ytype):
plot_ranges['y_range'] = plot.y_range
if match_dim_specs(plot.y_range.tags[0], xspecs):
if match_dim_specs(plot.y_range.tags[0], xspecs) and match_ax_type(plot.yaxis, xtype):
plot_ranges['x_range'] = plot.y_range
return plot_ranges

Expand Down Expand Up @@ -343,12 +343,6 @@ def _axes_props(self, plots, subplots, element, ranges):
else:
yspecs = None

plot_ranges = {}
# Try finding shared ranges in other plots in the same Layout
norm_opts = self.lookup_options(el, 'norm').options
if plots and self.shared_axes and not norm_opts.get('axiswise', False):
plot_ranges = self._merge_ranges(plots, xspecs, yspecs)

# Get the Element that determines the range and get_extents
range_el = el if self.batched and not isinstance(self, OverlayPlot) else element
l, b, r, t = self.get_extents(range_el, ranges)
Expand Down Expand Up @@ -394,6 +388,12 @@ def _axes_props(self, plots, subplots, element, ranges):
or ytype in util.datetime_types):
y_axis_type = 'datetime'

plot_ranges = {}
# Try finding shared ranges in other plots in the same Layout
norm_opts = self.lookup_options(el, 'norm').options
if plots and self.shared_axes and not norm_opts.get('axiswise', False):
plot_ranges = self._merge_ranges(plots, xspecs, yspecs, x_axis_type, y_axis_type)

# Declare shared axes
if 'x_range' in plot_ranges:
self._shared['x'] = True
Expand Down
24 changes: 21 additions & 3 deletions holoviews/plotting/bokeh/util.py
Expand Up @@ -19,8 +19,13 @@
from bokeh.core.validation import silence
from bokeh.layouts import WidgetBox, Row, Column
from bokeh.models import tools
from bokeh.models import Model, ToolbarBox, FactorRange, Range1d, Plot, Spacer, CustomJS, GridBox
from bokeh.models.formatters import FuncTickFormatter, TickFormatter, PrintfTickFormatter
from bokeh.models import (
Model, ToolbarBox, FactorRange, Range1d, Plot, Spacer, CustomJS,
GridBox, DatetimeAxis, CategoricalAxis
)
from bokeh.models.formatters import (
FuncTickFormatter, TickFormatter, PrintfTickFormatter
)
from bokeh.models.widgets import DataTable, Tabs, Div
from bokeh.plotting import Figure
from bokeh.themes.theme import Theme
Expand All @@ -39,7 +44,8 @@
from ...core.overlay import Overlay
from ...core.util import (
LooseVersion, _getargspec, basestring, callable_name, cftime_types,
cftime_to_timestamp, pd, unique_array, isnumeric, arraylike_types)
cftime_to_timestamp, pd, unique_array, isnumeric, arraylike_types
)
from ...core.spaces import get_nested_dmaps, DynamicMap
from ..util import dim_axis_label

Expand Down Expand Up @@ -914,6 +920,18 @@ def match_dim_specs(specs1, specs2):
return True


def match_ax_type(ax, range_type):
"""
Ensure the range_type matches the axis model being matched.
"""
if isinstance(ax[0], CategoricalAxis):
return range_type == 'categorical'
elif isinstance(ax[0], DatetimeAxis):
return range_type == 'datetime'
else:
return range_type in ('auto', 'log')


def wrap_formatter(formatter, axis):
"""
Wraps formatting function or string in
Expand Down
51 changes: 35 additions & 16 deletions holoviews/plotting/plot.py
Expand Up @@ -575,7 +575,7 @@ def compute_ranges(self, obj, key, ranges):
if obj is None or not self.normalize or all_table:
return OrderedDict()
# Get inherited ranges
ranges = self.ranges if ranges is None else dict(ranges)
ranges = self.ranges if ranges is None else {k: dict(v) for k, v in ranges.items()}

# Get element identifiers from current object and resolve
# with selected normalization options
Expand All @@ -591,9 +591,7 @@ def compute_ranges(self, obj, key, ranges):
# Skip if ranges are cached or already computed by a
# higher-level container object.
framewise = framewise or self.dynamic or len(elements) == 1
if group in ranges and (not framewise or ranges is not self.ranges):
continue
elif not framewise: # Traverse to get all elements
if not framewise: # Traverse to get all elements
elements = obj.traverse(return_fn, [group])
elif key is not None: # Traverse to get elements for each frame
frame = self._get_frame(key)
Expand All @@ -602,7 +600,7 @@ def compute_ranges(self, obj, key, ranges):
# or not framewise on a Overlay or ElementPlot
if (not (axiswise and not isinstance(obj, HoloMap)) or
(not framewise and isinstance(obj, HoloMap))):
self._compute_group_range(group, elements, ranges)
self._compute_group_range(group, elements, ranges, framewise)
self.ranges.update(ranges)
return ranges

Expand Down Expand Up @@ -654,7 +652,7 @@ def _get_norm_opts(self, obj):


@classmethod
def _compute_group_range(cls, group, elements, ranges):
def _compute_group_range(cls, group, elements, ranges, framewise):
# Iterate over all elements in a normalization group
# and accumulate their ranges into the supplied dictionary.
elements = [el for el in elements if el is not None]
Expand All @@ -670,6 +668,8 @@ def _compute_group_range(cls, group, elements, ranges):
continue
if isinstance(v, dim) and v.applies(el):
dim_name = repr(v)
if dim_name in ranges.get(group, {}) and not framewise:
continue
values = v.apply(el, expanded=False, all_values=True)
factors = None
if values.dtype.kind == 'M':
Expand All @@ -696,6 +696,9 @@ def _compute_group_range(cls, group, elements, ranges):

# Compute dimension normalization
for el_dim in el.dimensions('ranges'):
dim_name = el_dim.name
if dim_name in ranges.get(group, {}) and not framewise:
continue
if hasattr(el, 'interface'):
if isinstance(el, Graph) and el_dim in el.nodes.dimensions():
dtype = el.nodes.interface.dtype(el.nodes, el_dim)
Expand All @@ -715,15 +718,15 @@ def _compute_group_range(cls, group, elements, ranges):
else:
data_range = el.range(el_dim, dimension_range=False)

if el_dim.name not in group_ranges:
group_ranges[el_dim.name] = {'data': [], 'hard': [], 'soft': []}
group_ranges[el_dim.name]['data'].append(data_range)
group_ranges[el_dim.name]['hard'].append(el_dim.range)
group_ranges[el_dim.name]['soft'].append(el_dim.soft_range)
if dim_name not in group_ranges:
group_ranges[dim_name] = {'data': [], 'hard': [], 'soft': []}
group_ranges[dim_name]['data'].append(data_range)
group_ranges[dim_name]['hard'].append(el_dim.range)
group_ranges[dim_name]['soft'].append(el_dim.soft_range)
if (any(isinstance(r, util.basestring) for r in data_range) or
el_dim.type is not None and issubclass(el_dim.type, util.basestring)):
if 'factors' not in group_ranges[el_dim.name]:
group_ranges[el_dim.name]['factors'] = []
if 'factors' not in group_ranges[dim_name]:
group_ranges[dim_name]['factors'] = []
if el_dim.values not in ([], None):
values = el_dim.values
elif el_dim in el:
Expand All @@ -738,10 +741,23 @@ def _compute_group_range(cls, group, elements, ranges):
all(isinstance(v, (np.ndarray)) for v in values)):
values = np.concatenate(values)
factors = util.unique_array(values)
group_ranges[el_dim.name]['factors'].append(factors)
group_ranges[dim_name]['factors'].append(factors)

dim_ranges = []
group_dim_ranges = defaultdict(dict)
for gdim, values in group_ranges.items():
matching = True
for t, rs in values.items():
if t == 'factors':
continue
matching &= (
len({'date' if isinstance(v, util.datetime_types) else 'number'
for rng in rs for v in rng if util.isfinite(v)}) < 2
)
if matching:
group_dim_ranges[gdim] = values

dim_ranges = []
for gdim, values in group_dim_ranges.items():
hard_range = util.max_range(values['hard'], combined=False)
soft_range = util.max_range(values['soft'])
data_range = util.max_range(values['data'])
Expand All @@ -753,7 +769,10 @@ def _compute_group_range(cls, group, elements, ranges):
dranges['factors'] = util.unique_array([
v for fctrs in values['factors'] for v in fctrs])
dim_ranges.append((gdim, dranges))
ranges[group] = OrderedDict(dim_ranges)
if group not in ranges:
ranges[group] = OrderedDict(dim_ranges)
else:
ranges[group].update(OrderedDict(dim_ranges))


@classmethod
Expand Down
19 changes: 19 additions & 0 deletions holoviews/tests/plotting/bokeh/testlayoutplot.py
@@ -1,3 +1,4 @@
import datetime as dt
import re

import numpy as np
Expand Down Expand Up @@ -337,3 +338,21 @@ def test_layout_shared_axes_disabled(self):
self.assertEqual(cp1.handles['y_range'].end, 3)
self.assertEqual(cp2.handles['y_range'].start, 10)
self.assertEqual(cp2.handles['y_range'].end, 30)

def test_layout_categorical_numeric_type_axes_not_linked(self):
curve1 = Curve([1, 2, 3])
curve2 = Curve([('A', 0), ('B', 1), ('C', 2)])
layout = curve1 + curve2
plot = bokeh_renderer.get_plot(layout)
cp1, cp2 = plot.subplots[(0, 0)].subplots['main'], plot.subplots[(0, 1)].subplots['main']
self.assertIsNot(cp1.handles['x_range'], cp2.handles['x_range'])
self.assertIs(cp1.handles['y_range'], cp2.handles['y_range'])

def test_layout_datetime_numeric_type_axes_not_linked(self):
curve1 = Curve([1, 2, 3])
curve2 = Curve([(dt.datetime(2020, 1, 1), 0), (dt.datetime(2020, 1, 2), 1), (dt.datetime(2020, 1, 3), 2)])
layout = curve1 + curve2
plot = bokeh_renderer.get_plot(layout)
cp1, cp2 = plot.subplots[(0, 0)].subplots['main'], plot.subplots[(0, 1)].subplots['main']
self.assertIsNot(cp1.handles['x_range'], cp2.handles['x_range'])
self.assertIs(cp1.handles['y_range'], cp2.handles['y_range'])

0 comments on commit 83ca2fd

Please sign in to comment.