diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 0f0a00e999..42eb99be71 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -3,7 +3,8 @@ import param -from bokeh.models import ColumnDataSource, VBox, HBox, GridPlot as BokehGridPlot +from bokeh.models import (ColumnDataSource, VBox, HBox, Column, + GridPlot as BokehGridPlot, Div) from bokeh.models.widgets import Panel, Tabs from ...core import (OrderedDict, CompositeOverlay, Store, Layout, GridMatrix, @@ -153,7 +154,48 @@ def sync_sources(self): -class GridPlot(BokehPlot, GenericCompositePlot): +class CompositePlot(BokehPlot): + """ + CompositePlot is an abstract baseclass for plot types that draw + render multiple axes. It implements methods to add an overall title + to such a plot. + """ + + fontsize = param.Parameter(default={'title': '16pt'}, allow_None=True, doc=""" + Specifies various fontsizes of the displayed text. + + Finer control is available by supplying a dictionary where any + unmentioned keys reverts to the default sizes, e.g: + + {'title': '15pt'}""") + + _title_template = "{title}" + + def _get_title(self, key): + title_div = None + title = self._format_title(key) if self.show_title else '' + if title: + fontsize = self._fontsize('title') + title_tags = self._title_template.format(title=title, + **fontsize) + if 'title' in self.handles: + title_div = self.handles['title'] + else: + title_div = Div() + title_div.text = title_tags + return title_div + + @property + def current_handles(self): + """ + Should return a list of plot objects that have changed and + should be updated. + """ + return [self.handles['title']] + + + +class GridPlot(CompositePlot, GenericCompositePlot): """ Plot a group of elements in a grid layout based on a GridSpace element object. @@ -256,9 +298,14 @@ def initialize_plot(self, ranges=None, plots=[]): plots[r].append(None) passed_plots.append(None) if bokeh_version < '0.12': - self.handles['plot'] = BokehGridPlot(children=plots[::-1]) + plot = BokehGridPlot(children=plots[::-1]) else: - self.handles['plot'] = gridplot(plots[::-1]) + plot = gridplot(plots[::-1]) + title = self._get_title(self.keys[-1]) + if title: + self.handles['title'] = title + plot = Column(title, plot) + self.handles['plot'] = plot self.handles['plots'] = plots if self.shared_datasource: self.sync_sources() @@ -278,10 +325,13 @@ def update_frame(self, key, ranges=None): subplot = self.subplots.get(coord, None) if subplot is not None: subplot.update_frame(key, ranges) + title = self._get_title(key) + if title: + self.handles['title'] -class LayoutPlot(BokehPlot, GenericLayoutPlot): +class LayoutPlot(CompositePlot, GenericLayoutPlot): shared_axes = param.Boolean(default=True, doc=""" Whether axes should be shared across plots""") @@ -505,6 +555,10 @@ def initialize_plot(self, ranges=None): else: layout_plot = BokehGridPlot(children=plots) + title = self._get_title(self.keys[-1]) + if title: + self.handles['title'] = title + layout_plot = Column(title, layout_plot) self.handles['plot'] = layout_plot self.handles['plots'] = plots if self.shared_datasource: @@ -526,6 +580,10 @@ def update_frame(self, key, ranges=None): subplot = self.subplots.get((r, c), None) if subplot is not None: subplot.update_frame(key, ranges) + title = self._get_title(key) + if title: + self.handles['title'] = title + class AdjointLayoutPlot(BokehPlot): diff --git a/tests/testplotinstantiation.py b/tests/testplotinstantiation.py index a9b1d9f891..0775fd30b4 100644 --- a/tests/testplotinstantiation.py +++ b/tests/testplotinstantiation.py @@ -11,7 +11,7 @@ import param import numpy as np from holoviews import (Dimension, Overlay, DynamicMap, Store, - NdOverlay, GridSpace) + NdOverlay, GridSpace, HoloMap, Layout) from holoviews.element import (Curve, Scatter, Image, VLine, Points, HeatMap, QuadMesh, Spikes, ErrorBars, Scatter3D, Path, Polygons, Bars) @@ -32,6 +32,7 @@ import holoviews.plotting.bokeh bokeh_renderer = Store.renderers['bokeh'] from holoviews.plotting.bokeh.callbacks import Callback + from bokeh.models import Div from bokeh.models.mappers import LinearColorMapper, LogColorMapper from bokeh.models.tools import HoverTool except: @@ -282,6 +283,63 @@ def test_image_boolean_array(self): self.assertEqual(source.data['image'][0], np.array([[0, 1], [1, 0]])) + def test_layout_title(self): + hmap1 = HoloMap({a: Image(np.random.rand(10,10)) for a in range(3)}) + hmap2 = HoloMap({a: Image(np.random.rand(10,10)) for a in range(3)}) + plot = bokeh_renderer.get_plot(hmap1+hmap2) + title = plot.handles['title'] + self.assertIsInstance(title, Div) + text = "Default: 0" + self.assertEqual(title.text, text) + + def test_layout_title_fontsize(self): + hmap1 = HoloMap({a: Image(np.random.rand(10,10)) for a in range(3)}) + hmap2 = HoloMap({a: Image(np.random.rand(10,10)) for a in range(3)}) + layout = Layout([hmap1, hmap2])(plot=dict(fontsize={'title': '12pt'})) + plot = bokeh_renderer.get_plot(layout) + title = plot.handles['title'] + self.assertIsInstance(title, Div) + text = "Default: 0" + self.assertEqual(title.text, text) + + def test_layout_title_show_title_false(self): + hmap1 = HoloMap({a: Image(np.random.rand(10,10)) for a in range(3)}) + hmap2 = HoloMap({a: Image(np.random.rand(10,10)) for a in range(3)}) + layout = Layout([hmap1, hmap2])(plot=dict(show_title=False)) + plot = bokeh_renderer.get_plot(layout) + self.assertTrue('title' not in plot.handles) + + def test_layout_title_update(self): + hmap1 = HoloMap({a: Image(np.random.rand(10,10)) for a in range(3)}) + hmap2 = HoloMap({a: Image(np.random.rand(10,10)) for a in range(3)}) + plot = bokeh_renderer.get_plot(hmap1+hmap2) + plot.update(1) + title = plot.handles['title'] + self.assertIsInstance(title, Div) + text = "Default: 1" + self.assertEqual(title.text, text) + + def test_grid_title(self): + grid = GridSpace({(i, j): HoloMap({a: Image(np.random.rand(10,10)) + for a in range(3)}, kdims=['X']) + for i in range(2) for j in range(3)}) + plot = bokeh_renderer.get_plot(grid) + title = plot.handles['title'] + self.assertIsInstance(title, Div) + text = "X: 0" + self.assertEqual(title.text, text) + + def test_grid_title_update(self): + grid = GridSpace({(i, j): HoloMap({a: Image(np.random.rand(10,10)) + for a in range(3)}, kdims=['X']) + for i in range(2) for j in range(3)}) + plot = bokeh_renderer.get_plot(grid) + plot.update(1) + title = plot.handles['title'] + self.assertIsInstance(title, Div) + text = "X: 1" + self.assertEqual(title.text, text) + def test_points_non_numeric_size_warning(self): data = (np.arange(10), np.arange(10), list(map(chr, range(94,104)))) points = Points(data, vdims=['z'])(plot=dict(size_index=2))