''' Functions for arranging bokeh Layout objects.
'''
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
from __future__ import absolute_import
from .core.enums import Location, SizingMode
from .models.tools import ToolbarBox
from .models.plots import Plot
from .models.layouts import LayoutDOM, Row, Column, Spacer, WidgetBox
from .models.widgets import Widget
from .util._plot_arg_helpers import _convert_responsive
#-----------------------------------------------------------------------------
# Common helper functions
#-----------------------------------------------------------------------------
def _handle_children(*args, **kwargs):
children = kwargs.get('children')
# Set-up Children from args or kwargs
if len(args) > 0 and children is not None:
raise ValueError("'children' keyword cannot be used with positional arguments")
if not children:
if len(args) == 1 and isinstance(args[0], list):
children = args[0]
elif len(args) == 1 and isinstance(args[0], GridSpec):
children = args[0]
else:
children = list(args)
return children
def _verify_sizing_mode(sizing_mode):
if sizing_mode not in SizingMode:
raise ValueError("Invalid value of sizing_mode: %s" % sizing_mode)
[docs]def row(*args, **kwargs):
""" Create a row of Bokeh Layout objects. Forces all objects to
have the same sizing_mode, which is required for complex layouts to work.
Args:
children (list of :class:`~bokeh.models.layouts.LayoutDOM` ): A list of instances for
the row. Can be any of the following - :class:`~bokeh.models.plots.Plot`,
:class:`~bokeh.models.widgets.widget.Widget`, :class:`~bokeh.models.layouts.WidgetBox`,
:class:`~bokeh.models.layouts.Row`,
:class:`~bokeh.models.layouts.Column`,
:class:`~bokeh.models.tools.ToolbarBox`,
:class:`~bokeh.models.layouts.Spacer`.
sizing_mode (``"fixed"``, ``"stretch_both"``, ``"scale_width"``, ``"scale_height"``, ``"scale_both"`` ): How
will the items in the layout resize to fill the available space.
Default is ``"fixed"``. For more information on the different
modes see :attr:`~bokeh.models.layouts.LayoutDOM.sizing_mode`
description on :class:`~bokeh.models.layouts.LayoutDOM`.
responsive (``True``, ``False``): True sets ``sizing_mode`` to
``"width_ar"``. ``False`` sets sizing_mode to ``"fixed"``. Using
responsive will override sizing_mode.
Returns:
Row: A row of LayoutDOM objects all with the same sizing_mode.
Examples:
>>> row([plot_1, plot_2])
>>> row(children=[widget_box_1, plot_1], sizing_mode='stretch_both')
"""
responsive = kwargs.pop('responsive', None)
sizing_mode = kwargs.pop('sizing_mode', 'fixed')
children = kwargs.pop('children', None)
if responsive:
sizing_mode = _convert_responsive(responsive)
_verify_sizing_mode(sizing_mode)
children = _handle_children(*args, children=children)
row_children = []
for item in children:
if isinstance(item, LayoutDOM):
item.sizing_mode = sizing_mode
row_children.append(item)
else:
raise ValueError(
"""Only LayoutDOM items can be inserted into a row.
Tried to insert: %s of type %s""" % (item, type(item))
)
return Row(children=row_children, sizing_mode=sizing_mode, **kwargs)
[docs]def column(*args, **kwargs):
""" Create a column of Bokeh Layout objects. Forces all objects to
have the same sizing_mode, which is required for complex layouts to work.
Args:
children (list of :class:`~bokeh.models.layouts.LayoutDOM` ): A list of instances for
the column. Can be any of the following - :class:`~bokeh.models.plots.Plot`,
:class:`~bokeh.models.widgets.widget.Widget`, :class:`~bokeh.models.layouts.WidgetBox`,
:class:`~bokeh.models.layouts.Row`,
:class:`~bokeh.models.layouts.Column`,
:class:`~bokeh.models.tools.ToolbarBox`,
:class:`~bokeh.models.layouts.Spacer`.
sizing_mode (``"fixed"``, ``"stretch_both"``, ``"scale_width"``, ``"scale_height"``, ``"scale_both"`` ): How
will the items in the layout resize to fill the available space.
Default is ``"fixed"``. For more information on the different
modes see :attr:`~bokeh.models.layouts.LayoutDOM.sizing_mode`
description on :class:`~bokeh.models.layouts.LayoutDOM`.
responsive (``True``, ``False``): True sets ``sizing_mode`` to
``"width_ar"``. ``False`` sets sizing_mode to ``"fixed"``. Using
responsive will override sizing_mode.
Returns:
Column: A column of LayoutDOM objects all with the same sizing_mode.
Examples:
>>> column([plot_1, plot_2])
>>> column(children=[widget_box_1, plot_1], sizing_mode='stretch_both')
"""
responsive = kwargs.pop('responsive', None)
sizing_mode = kwargs.pop('sizing_mode', 'fixed')
children = kwargs.pop('children', None)
if responsive:
sizing_mode = _convert_responsive(responsive)
_verify_sizing_mode(sizing_mode)
children = _handle_children(*args, children=children)
col_children = []
for item in children:
if isinstance(item, LayoutDOM):
item.sizing_mode = sizing_mode
col_children.append(item)
else:
raise ValueError(
"""Only LayoutDOM items can be inserted into a column.
Tried to insert: %s of type %s""" % (item, type(item))
)
return Column(children=col_children, sizing_mode=sizing_mode, **kwargs)
[docs]def layout(*args, **kwargs):
""" Create a grid-based arrangement of Bokeh Layout objects. Forces all objects to
have the same sizing mode, which is required for complex layouts to work. Returns a nested set
of Rows and Columns.
Args:
children (list of lists of :class:`~bokeh.models.layouts.LayoutDOM` ): A list of lists of instances
for a grid layout. Can be any of the following - :class:`~bokeh.models.plots.Plot`,
:class:`~bokeh.models.widgets.widget.Widget`, :class:`~bokeh.models.layouts.WidgetBox`,
:class:`~bokeh.models.layouts.Row`,
:class:`~bokeh.models.layouts.Column`,
:class:`~bokeh.models.tools.ToolbarBox`,
:class:`~bokeh.models.layouts.Spacer`.
sizing_mode (``"fixed"``, ``"stretch_both"``, ``"scale_width"``, ``"scale_height"``, ``"scale_both"`` ): How
will the items in the layout resize to fill the available space.
Default is ``"fixed"``. For more information on the different
modes see :attr:`~bokeh.models.layouts.LayoutDOM.sizing_mode`
description on :class:`~bokeh.models.layouts.LayoutDOM`.
responsive (``True``, ``False``): True sets ``sizing_mode`` to
``"width_ar"``. ``False`` sets sizing_mode to ``"fixed"``. Using
responsive will override sizing_mode.
Returns:
Column: A column of ``Row`` layouts of the children, all with the same sizing_mode.
Examples:
>>> layout([[plot_1, plot_2], [plot_3, plot_4]])
>>> layout(
children=[
[widget_box_1, plot_1],
[slider],
[widget_box_2, plot_2, plot_3]
],
sizing_mode='fixed',
)
"""
responsive = kwargs.pop('responsive', None)
sizing_mode = kwargs.pop('sizing_mode', 'fixed')
children = kwargs.pop('children', None)
if responsive:
sizing_mode = _convert_responsive(responsive)
_verify_sizing_mode(sizing_mode)
children = _handle_children(*args, children=children)
# Make the grid
return _create_grid(children, sizing_mode)
def _create_grid(iterable, sizing_mode, layer=0):
"""Recursively create grid from input lists."""
return_list = []
for item in iterable:
if isinstance(item, list):
return_list.append(_create_grid(item, sizing_mode, layer+1))
elif isinstance(item, LayoutDOM):
item.sizing_mode = sizing_mode
return_list.append(item)
else:
raise ValueError(
"""Only LayoutDOM items can be inserted into a layout.
Tried to insert: %s of type %s""" % (item, type(item))
)
if layer % 2 == 0:
return column(children=return_list, sizing_mode=sizing_mode)
return row(children=return_list, sizing_mode=sizing_mode)
return return_list
def _chunks(l, ncols):
"""Yield successive n-sized chunks from list, l."""
assert isinstance(ncols, int), "ncols must be an integer"
for i in range(0, len(l), ncols):
yield l[i: i+ncols]
[docs]def gridplot(*args, **kwargs):
""" Create a grid of plots rendered on separate canvases. ``gridplot`` builds a single toolbar
for all the plots in the grid. ``gridplot`` is designed to layout a set of plots. For general
grid layout, use the :func:`~bokeh.layouts.layout` function.
Args:
children (list of lists of :class:`~bokeh.models.plots.Plot` ): An
array of plots to display in a grid, given as a list of lists of Plot
objects. To leave a position in the grid empty, pass None for that
position in the children list. OR list of :class:`~bokeh.models.plots.Plot` if called with
ncols. OR an instance of GridSpec.
sizing_mode (``"fixed"``, ``"stretch_both"``, ``"scale_width"``, ``"scale_height"``, ``"scale_both"`` ): How
will the items in the layout resize to fill the available space.
Default is ``"fixed"``. For more information on the different
modes see :attr:`~bokeh.models.layouts.LayoutDOM.sizing_mode`
description on :class:`~bokeh.models.layouts.LayoutDOM`.
toolbar_location (``above``, ``below``, ``left``, ``right`` ): Where the
toolbar will be located, with respect to the grid. Default is
``above``. If set to None, no toolbar will be attached to the grid.
ncols ``Int`` (optional): Specify the number of columns you would like in your grid.
You must only pass an un-nested list of plots (as opposed to a list of lists of plots)
when using ncols.
responsive (``True``, ``False``): True sets ``sizing_mode`` to
``"width_ar"``. ``False`` sets sizing_mode to ``"fixed"``. Using
responsive will override sizing_mode.
plot_width (int, optional): The width you would like all your plots to be
plot_height (int, optional): The height you would like all your plots to be.
toolbar_options (dict, optional) : A dictionary of options that will be
used to construct the grid's toolbar (an instance of
:class:`~bokeh.models.tools.ToolbarBox`). If none is supplied,
ToolbarBox's defaults will be used.
merge_tools (``True``, ``False``): Combine tools from all child plots into
a single toolbar.
Returns:
Row or Column: A row or column containing the grid toolbar and the grid
of plots (depending on whether the toolbar is left/right or
above/below. The grid is always a Column of Rows of plots.
Examples:
>>> gridplot([[plot_1, plot_2], [plot_3, plot_4]])
>>> gridplot([plot_1, plot_2, plot_3, plot_4], ncols=2, plot_width=200, plot_height=100)
>>> gridplot(
children=[[plot_1, plot_2], [None, plot_3]],
toolbar_location='right'
sizing_mode='fixed',
toolbar_options=dict(logo='gray')
)
"""
toolbar_location = kwargs.get('toolbar_location', 'above')
sizing_mode = kwargs.get('sizing_mode', 'fixed')
children = kwargs.get('children')
responsive = kwargs.get('responsive')
toolbar_options = kwargs.get('toolbar_options')
plot_width = kwargs.get('plot_width')
plot_height = kwargs.get('plot_height')
ncols = kwargs.get('ncols')
merge_tools = kwargs.get('merge_tools', True)
# Integrity checks & set-up
if responsive:
sizing_mode = _convert_responsive(responsive)
_verify_sizing_mode(sizing_mode)
if toolbar_location:
if not hasattr(Location, toolbar_location):
raise ValueError("Invalid value of toolbar_location: %s" % toolbar_location)
children = _handle_children(*args, children=children)
if ncols:
if any(isinstance(child, list) for child in children):
raise ValueError("Cannot provide a nested list when using ncols")
children = list(_chunks(children, ncols))
# Additional children set-up for grid plot
if not children:
children = []
# Make the grid
tools = []
rows = []
for row in children:
row_tools = []
row_children = []
for item in row:
if merge_tools:
if item is not None:
for plot in item.select(dict(type=Plot)):
row_tools = row_tools + plot.toolbar.tools
plot.toolbar_location = None
if item is None:
for neighbor in row:
if isinstance(neighbor, Plot):
break
item = Spacer(width=neighbor.plot_width, height=neighbor.plot_height)
if isinstance(item, LayoutDOM):
item.sizing_mode = sizing_mode
if isinstance(item, Plot):
if plot_width:
item.plot_width = plot_width
if plot_height:
item.plot_height = plot_height
row_children.append(item)
else:
raise ValueError("Only LayoutDOM items can be inserted into Grid")
tools = tools + row_tools
rows.append(Row(children=row_children, sizing_mode=sizing_mode))
grid = Column(children=rows, sizing_mode=sizing_mode)
if not merge_tools:
return grid
# Make the toolbar
if toolbar_location:
if not toolbar_options:
toolbar_options = {}
if 'toolbar_location' not in toolbar_options:
toolbar_options['toolbar_location'] = toolbar_location
# Fixed sizing mode needs scale_width for the toolbar
# for layout to work correctly.
if sizing_mode == 'fixed':
toolbar_sizing_mode = 'scale_width'
else:
toolbar_sizing_mode = sizing_mode
toolbar = ToolbarBox(
tools=tools,
sizing_mode=toolbar_sizing_mode,
**toolbar_options
)
# Set up children
if toolbar_location == 'above':
return Column(children=[toolbar, grid], sizing_mode=sizing_mode)
elif toolbar_location == 'below':
return Column(children=[grid, toolbar], sizing_mode=sizing_mode)
elif toolbar_location == 'left':
return Row(children=[toolbar, grid], sizing_mode=sizing_mode)
elif toolbar_location == 'right':
return Row(children=[grid, toolbar], sizing_mode=sizing_mode)
else:
return grid
[docs]class GridSpec(object):
""" Simplifies grid layout specification. """
def __init__(self, nrows, ncols):
self.nrows = nrows
self.ncols = ncols
self._arrangement = {}
def __setitem__(self, key, obj):
k1, k2 = key
if isinstance(k1, slice):
row1, row2, _ = k1.indices(self.nrows)
else:
if k1 < 0:
k1 += self.nrows
if k1 >= self.nrows or k1 < 0:
raise IndexError("index out of range")
row1, row2 = k1, None
if isinstance(k2, slice):
col1, col2, _ = k2.indices(self.ncols)
else:
if k2 < 0:
k2 += self.ncols
if k2 >= self.ncols or k2 < 0:
raise IndexError("index out of range")
col1, col2 = k2, None
# gs[row, col] = obj
# gs[row1:row2, col] = [...]
# gs[row, col1:col2] = [...]
# gs[row1:row2, col1:col2] = [[...], ...]
def get_or_else(fn, default):
try:
return fn()
except IndexError:
return default
if row2 is None and col2 is None:
self._arrangement[row1, col1] = obj
elif row2 is None:
for col in range(col1, col2):
self._arrangement[row1, col] = get_or_else(lambda: obj[col-col1], None)
elif col2 is None:
for row in range(row1, row2):
self._arrangement[row, col1] = get_or_else(lambda: obj[row-row1], None)
else:
for row, col in zip(range(row1, row2), range(col1, col2)):
self._arrangement[row, col] = get_or_else(lambda: obj[row-row1][col-col1], None)
def __iter__(self):
array = [ [ None ]*self.ncols for _ in range(0, self.nrows) ]
for (row, col), obj in self._arrangement.items():
array[row][col] = obj
return iter(array)