Skip to content

Commit

Permalink
Merge dc9d9bb into 3afcf01
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Feb 25, 2017
2 parents 3afcf01 + dc9d9bb commit 41fb219
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 34 deletions.
4 changes: 4 additions & 0 deletions holoviews/plotting/bokeh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,7 @@
# Annotations
options.HLine = Options('style', line_color=Cycle(), line_width=3, line_alpha=1)
options.VLine = Options('style', line_color=Cycle(), line_width=3, line_alpha=1)

# Define composite defaults
options.GridMatrix = Options('plot', shared_xaxis=True, shared_yaxis=True,
xaxis=None, yaxis=None)
120 changes: 97 additions & 23 deletions holoviews/plotting/bokeh/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
AdjointLayout, NdLayout, Empty, GridSpace, HoloMap)
from ...core import traversal
from ...core.options import Compositor, SkipRendering
from ...core.util import basestring, wrap_tuple
from ...core.util import basestring, wrap_tuple, unique_iterator
from ...element import Histogram
from ..plot import DimensionedPlot, GenericCompositePlot, GenericLayoutPlot
from ..util import get_dynamic_mode, initialize_sampled
from .renderer import BokehRenderer
from .util import bokeh_version, layout_padding, pad_plots, filter_toolboxes
from .util import (bokeh_version, layout_padding, pad_plots,
filter_toolboxes, make_axis)

if bokeh_version >= '0.12':
from bokeh.layouts import gridplot
Expand Down Expand Up @@ -201,6 +202,33 @@ class GridPlot(CompositePlot, GenericCompositePlot):
object.
"""

shared_xaxis = param.Boolean(default=False, doc="""
If enabled the x-axes of the GridSpace will be drawn from the
objects inside the Grid rather than the GridSpace dimensions.""")

shared_yaxis = param.Boolean(default=False, doc="""
If enabled the x-axes of the GridSpace will be drawn from the
objects inside the Grid rather than the GridSpace dimensions.""")

xaxis = param.ObjectSelector(default=True,
objects=['bottom', 'top', None, True, False], doc="""
Whether and where to display the xaxis, supported options are
'bottom', 'top' and None.""")

yaxis = param.ObjectSelector(default=True,
objects=['left', 'right', None, True, False], doc="""
Whether and where to display the yaxis, supported options are
'left', 'right' and None.""")

xrotation = param.Integer(default=0, bounds=(0, 360), doc="""
Rotation angle of the xticks.""")

yrotation = param.Integer(default=0, bounds=(0, 360), doc="""
Rotation angle of the yticks.""")

plot_size = param.Integer(default=120, doc="""
Defines the width and height of each plot in the grid""")

def __init__(self, layout, ranges=None, layout_num=1, **params):
if not isinstance(layout, GridSpace):
raise Exception("GridPlot only accepts GridSpace.")
Expand All @@ -220,8 +248,8 @@ def _create_subplots(self, layout, ranges):
for key in self.keys])
collapsed_layout = layout.clone(shared_data=False, id=layout.id)
for i, coord in enumerate(layout.keys(full_grid=True)):
r = i % self.cols
c = i // self.cols
r = i % self.rows
c = i // self.rows

if not isinstance(coord, tuple): coord = (coord,)
view = layout.data.get(coord, None)
Expand All @@ -235,25 +263,31 @@ def _create_subplots(self, layout, ranges):
# Create axes
kwargs = {}
if c == 0 and r != 0:
kwargs['xaxis'] = 'bottom-bare'
kwargs['width'] = 150
kwargs['xaxis'] = None
kwargs['width'] = self.plot_size+50
if c != 0 and r == 0:
kwargs['yaxis'] = 'left-bare'
kwargs['height'] = 150
kwargs['yaxis'] = None
kwargs['height'] = self.plot_size+50
if c == 0 and r == 0:
kwargs['width'] = 150
kwargs['height'] = 150
kwargs['width'] = self.plot_size+50
kwargs['height'] = self.plot_size+50
if r != 0 and c != 0:
kwargs['xaxis'] = 'bottom-bare'
kwargs['yaxis'] = 'left-bare'
kwargs['xaxis'] = None
kwargs['yaxis'] = None

if 'width' not in kwargs:
kwargs['width'] = 105
if 'height' not in kwargs:
kwargs['height'] = 105
if 'width' not in kwargs or not self.shared_yaxis:
kwargs['width'] = self.plot_size
if 'height' not in kwargs or not self.shared_xaxis:
kwargs['height'] = self.plot_size
if 'border' not in kwargs:
kwargs['border'] = 0

if not self.shared_xaxis:
kwargs['xaxis'] = None

if not self.shared_yaxis:
kwargs['yaxis'] = None

if isinstance(layout, GridMatrix):
if view.traverse(lambda x: x, [Histogram]):
kwargs['shared_axes'] = False
Expand Down Expand Up @@ -286,25 +320,30 @@ def sync_tools(self, subplots):
def initialize_plot(self, ranges=None, plots=[]):
ranges = self.compute_ranges(self.layout, self.keys[-1], None)
passed_plots = list(plots)
plots = [[] for r in range(self.cols)]
plots = [[None for c in range(self.cols)] for r in range(self.rows)]
for i, coord in enumerate(self.layout.keys(full_grid=True)):
r = i % self.cols
r = i % self.rows
c = i // self.rows
subplot = self.subplots.get(wrap_tuple(coord), None)
if subplot is not None:
plot = subplot.initialize_plot(ranges=ranges, plots=passed_plots)
plots[r].append(plot)
passed_plots.append(plots[r][-1])
plots[r][c] = plot
passed_plots.append(plot)
else:
plots[r].append(None)
passed_plots.append(None)

if bokeh_version < '0.12':
plot = BokehGridPlot(children=plots[::-1])
else:
plot = gridplot(plots[::-1])

plot = self._make_axes(plot)

title = self._get_title(self.keys[-1])
if title:
self.handles['title'] = title
plot = Column(title, plot)
self.handles['title'] = title

self.handles['plot'] = plot
self.handles['plots'] = plots
if self.shared_datasource:
Expand All @@ -314,6 +353,41 @@ def initialize_plot(self, ranges=None, plots=[]):
return self.handles['plot']


def _make_axes(self, plot):
width, height = self.renderer.get_size(plot)
x_axis, y_axis = None, None
if self.xaxis:
flip = self.shared_xaxis
rotation = self.xrotation
xfactors = list(unique_iterator(self.layout.dimension_values(0)))
x_axis = make_axis('x', width, xfactors, self.layout.kdims[0],
flip=flip, rotation=rotation)
if self.yaxis and self.layout.ndims > 1:
flip = self.shared_yaxis
rotation = self.yrotation
yfactors = list(unique_iterator(self.layout.dimension_values(1)))
y_axis = make_axis('y', height, yfactors, self.layout.kdims[1],
flip=flip, rotation=rotation)
if x_axis and y_axis:
plot = filter_toolboxes(plot)
r1, r2 = ([y_axis, plot], [None, x_axis])
if self.shared_xaxis:
r1, r2 = r2, r1
if self.shared_yaxis:
r1, r2 = r1[::-1], r2[::-1]
models = layout_padding([r1, r2], self.renderer)
plot = gridplot(models)
elif y_axis:
models = [y_axis, plot]
if self.shared_yaxis: models = models[::-1]
plot = Row(*models)
elif x_axis:
models = [plot, x_axis]
if self.shared_xaxis: models = models[::-1]
plot = Column(*models)
return plot


def update_frame(self, key, ranges=None):
"""
Update the internal state of the Plot to represent the given
Expand Down Expand Up @@ -528,7 +602,7 @@ def initialize_plot(self, plots=None, ranges=None):
# Replace None types with empty plots
# to avoid bokeh bug
if adjoined:
plots = layout_padding(plots)
plots = layout_padding(plots, self.renderer)

# Wrap in appropriate layout model
if self.tabs:
Expand Down
67 changes: 57 additions & 10 deletions holoviews/plotting/bokeh/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from bokeh.core.json_encoder import serialize_json # noqa (API import)
from bokeh.document import Document
from bokeh.models.plots import Plot
from bokeh.models import (GlyphRenderer, Model, HasProps, Column, Row, ToolbarBox)
from bokeh.models import (GlyphRenderer, Model, HasProps, Column, Row,
ToolbarBox, FactorRange, Range1d)
from bokeh.models.widgets import DataTable, Tabs
from bokeh.plotting import Figure
if bokeh_version >= '0.12':
Expand All @@ -26,6 +27,8 @@
from ...core.options import abbreviated_exception
from ...core.overlay import Overlay

from ..util import dim_axis_label

# Conversion between matplotlib and bokeh markers
markers = {'s': {'marker': 'square'},
'd': {'marker': 'diamond'},
Expand Down Expand Up @@ -125,7 +128,7 @@ def mpl_to_bokeh(properties):
return new_properties


def layout_padding(plots):
def layout_padding(plots, renderer):
"""
Temporary workaround to allow empty plots in a
row of a bokeh GridPlot type. Should be removed
Expand All @@ -136,8 +139,7 @@ def layout_padding(plots):
for r, row in enumerate(plots):
for c, p in enumerate(row):
if p is not None:
width = p.plot_width if isinstance(p, Plot) else p.width
height = p.plot_height if isinstance(p, Plot) else p.height
width, height = renderer.get_size(p)
widths[c] = max(widths[c], width)
heights[r] = max(heights[r], height)

Expand All @@ -146,18 +148,63 @@ def layout_padding(plots):
expanded_plots.append([])
for c, p in enumerate(row):
if p is None:
p = Figure(plot_width=widths[c],
plot_height=heights[r])
p.text(x=0, y=0, text=[' '])
x_range = Range1d(start=0, end=1)
y_range = Range1d(start=0, end=1)
p = Figure(plot_width=widths[c], plot_height=heights[r],
x_range=x_range, y_range=y_range)
p.xaxis.visible = False
p.yaxis.visible = False
p.outline_line_color = None
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None
p.outline_line_alpha = 0
p.grid.grid_line_alpha = 0
expanded_plots[r].append(p)
return expanded_plots


def make_axis(axis, size, factors, dim, flip=False, rotation=0):
factors = list(map(dim.pprint_value, factors))
ranges = FactorRange(factors=factors)
ranges2 = Range1d(start=0, end=1)
axis_label = dim_axis_label(dim)

rotation = np.radians(rotation)
if axis == 'x':
align = 'center'
# Adjust height to compensate for label rotation
height = int(50 + np.abs(np.sin(rotation)) * 30)
opts = dict(x_axis_type='auto', x_axis_label=axis_label,
x_range=ranges, y_range=ranges2, plot_height=height,
plot_width=size)
else:
# Adjust width to compensate for label rotation
align = 'left' if flip else 'right'
width = int(80 - np.abs(np.sin(rotation)) * 30)
opts = dict(y_axis_label=axis_label, x_range=ranges2,
y_range=ranges, plot_width=width, plot_height=size)

p = Figure(toolbar_location=None, **opts)
p.outline_line_alpha = 0
p.grid.grid_line_alpha = 0

if axis == 'x':
p.yaxis.visible = False
axis = p.xaxis[0]
if flip:
p.above = p.below
p.below = []
p.xaxis[:] = p.above
else:
p.xaxis.visible = False
axis = p.yaxis[0]
if flip:
p.right = p.left
p.left = []
p.yaxis[:] = p.right
axis.major_label_orientation = rotation
axis.major_label_text_align = align
axis.major_label_text_baseline = 'middle'
return p


def convert_datetime(time):
return time.astype('datetime64[s]').astype(float)*1000

Expand Down
2 changes: 2 additions & 0 deletions tests/testplotinstantiation.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,8 @@ def test_layout_gridspaces(self):
self.assertIsInstance(grow2, Row)
self.assertEqual(len(grow1.children), 2)
self.assertEqual(len(grow2.children), 2)
ax_row, grid_row = grow1.children
grow1, grow2 = grid_row.children[0].children
gfig1, gfig2 = grow1.children
gfig3, gfig4 = grow2.children
self.assertIsInstance(gfig1, Figure)
Expand Down
2 changes: 1 addition & 1 deletion tests/testrenderclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def test_get_size_grid_plot(self):
grid = GridSpace({(i, j): self.image1 for i in range(3) for j in range(3)})
plot = self.renderer.get_plot(grid)
w, h = self.renderer.get_size(plot)
self.assertEqual((w, h), (360, 360))
self.assertEqual((w, h), (440, 410))

def test_get_size_table(self):
table = Table(range(10), kdims=['x'])
Expand Down

0 comments on commit 41fb219

Please sign in to comment.