#-----------------------------------------------------------------------------# Copyright (c) 2012 - 2022, Anaconda, Inc., and Bokeh Contributors.# All rights reserved.## The full license is in the file LICENSE.txt, distributed with this software.#-----------------------------------------------------------------------------''' Functions for arranging bokeh layout objects.'''#-----------------------------------------------------------------------------# Boilerplate#-----------------------------------------------------------------------------from__future__importannotationsimportlogging# isort:skiplog=logging.getLogger(__name__)#-----------------------------------------------------------------------------# Imports#-----------------------------------------------------------------------------# Standard library importsimportmathfromtypingimport(TYPE_CHECKING,Any,Dict,Iterable,Iterator,List,Sequence,Tuple,TypeVar,cast,overload,)# Bokeh importsfrom.core.enumsimportLocation,LocationType,SizingModeTypefrom.modelsimport(Box,Column,GridBox,LayoutDOM,Plot,ProxyToolbar,Row,Spacer,ToolbarBox,WidgetBox,)from.util.dataclassesimportdataclassfrom.util.deprecationimportdeprecatedifTYPE_CHECKING:from.modelsimportToolbar,Widget#-----------------------------------------------------------------------------# Globals and constants#-----------------------------------------------------------------------------__all__=('column','grid','gridplot','GridSpec','layout','row','Spacer','widgetbox',)#-----------------------------------------------------------------------------# General API#-----------------------------------------------------------------------------@overloaddefrow(children:List[LayoutDOM],*,sizing_mode:SizingModeType|None=None,**kwargs:Any)->Row:...@overloaddefrow(*children:LayoutDOM,sizing_mode:SizingModeType|None=None,**kwargs:Any)->Row:...
[docs]defrow(*children:LayoutDOM|List[LayoutDOM],sizing_mode:SizingModeType|None=None,**kwargs:Any)->Row:""" 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.LayoutDOM` ): A list of instances for the row. Can be any of the following - |Plot|, :class:`~bokeh.models.Widget`, :class:`~bokeh.models.Row`, :class:`~bokeh.models.Column`, :class:`~bokeh.models.ToolbarBox`, :class:`~bokeh.models.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.LayoutDOM.sizing_mode` description on :class:`~bokeh.models.LayoutDOM`. Returns: Row: A row of LayoutDOM objects all with the same sizing_mode. Examples: >>> row(plot1, plot2) >>> row(children=[widgets, plot], sizing_mode='stretch_both') """_children=_parse_children_arg(*children,children=kwargs.pop("children",None))_handle_child_sizing(_children,sizing_mode,widget="row")returnRow(children=_children,sizing_mode=sizing_mode,**kwargs)
[docs]defcolumn(*children:LayoutDOM|List[LayoutDOM],sizing_mode:SizingModeType|None=None,**kwargs:Any)->Column:""" 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.LayoutDOM` ): A list of instances for the column. Can be any of the following - |Plot|, :class:`~bokeh.models.Widget`, :class:`~bokeh.models.Row`, :class:`~bokeh.models.Column`, :class:`~bokeh.models.ToolbarBox`, :class:`~bokeh.models.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.LayoutDOM.sizing_mode` description on :class:`~bokeh.models.LayoutDOM`. Returns: Column: A column of LayoutDOM objects all with the same sizing_mode. Examples: >>> column(plot1, plot2) >>> column(children=[widgets, plot], sizing_mode='stretch_both') """_children=_parse_children_arg(*children,children=kwargs.pop("children",None))_handle_child_sizing(_children,sizing_mode,widget="column")returnColumn(children=_children,sizing_mode=sizing_mode,**kwargs)
[docs]defwidgetbox(*args:Widget,children:List[Widget]|None=None,sizing_mode:SizingModeType|None=None,**kwargs:Any)->WidgetBox:""" Create a column of bokeh widgets with predefined styling. Args: children (list of :class:`~bokeh.models.Widget`): A list of widgets. 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.LayoutDOM.sizing_mode` description on :class:`~bokeh.models.LayoutDOM`. Returns: WidgetBox: A column layout of widget instances all with the same ``sizing_mode``. Examples: >>> widgetbox([button, select]) >>> widgetbox(children=[slider], sizing_mode='scale_width') """_children=_parse_children_arg(*args,children=children)_handle_child_sizing(_children,sizing_mode,widget="widget box")returnWidgetBox(children=_children,sizing_mode=sizing_mode,**kwargs)
[docs]deflayout(*args:LayoutDOM,children:List[LayoutDOM]|None=None,sizing_mode:SizingModeType|None=None,**kwargs:Any)->Column:""" Create a grid-based arrangement of Bokeh Layout objects. Args: children (list of lists of :class:`~bokeh.models.LayoutDOM` ): A list of lists of instances for a grid layout. Can be any of the following - |Plot|, :class:`~bokeh.models.Widget`, :class:`~bokeh.models.Row`, :class:`~bokeh.models.Column`, :class:`~bokeh.models.ToolbarBox`, :class:`~bokeh.models.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.LayoutDOM.sizing_mode` description on :class:`~bokeh.models.LayoutDOM`. 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_1, plot_1], [slider], [widget_2, plot_2, plot_3] ], sizing_mode='fixed', ) """_children=_parse_children_arg(*args,children=children)return_create_grid(_children,sizing_mode,**kwargs)
[docs]defgridplot(children:List[List[LayoutDOM|None]]|GridSpec,*,sizing_mode:SizingModeType|None=None,toolbar_location:LocationType|None="above",ncols:int|None=None,width:int|None=None,height:int|None=None,plot_width:int|None=None,plot_height:int|None=None,toolbar_options:Any=None,# TODOmerge_tools:bool=True)->LayoutDOM:''' Create a grid of plots rendered on separate canvases. The ``gridplot`` function 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 |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 |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.LayoutDOM.sizing_mode` description on :class:`~bokeh.models.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. width (int, optional): The width you would like all your plots to be 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.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, width=200, height=100) >>> gridplot( children=[[plot_1, plot_2], [None, plot_3]], toolbar_location='right' sizing_mode='fixed', toolbar_options=dict(logo='gray') ) '''ifplot_widthisnotNoneorplot_heightisnotNone:deprecated((2,4,0),"plot_width and plot_height","width or height")iftoolbar_optionsisNone:toolbar_options={}iftoolbar_location:ifnothasattr(Location,toolbar_location):raiseValueError(f"Invalid value of toolbar_location: {toolbar_location}")children=_parse_children_arg(children=children)ifncols:ifany(isinstance(child,list)forchildinchildren):raiseValueError("Cannot provide a nested list when using ncols")children=list(_chunks(children,ncols))# Additional children set-up for grid plotifnotchildren:children=[]# Make the gridtoolbars:List[Toolbar]=[]items:List[Tuple[LayoutDOM,int,int]]=[]fory,rowinenumerate(children):forx,iteminenumerate(row):ifitemisNone:continueelifisinstance(item,LayoutDOM):ifmerge_tools:forplotinitem.select(dict(type=Plot)):toolbars.append(plot.toolbar)plot.toolbar_location=Noneifisinstance(item,Plot):ifplot_widthisnotNone:item.width=plot_widthifplot_heightisnotNone:item.height=plot_heightifwidthisnotNone:item.width=widthifheightisnotNone:item.height=heightifsizing_modeisnotNoneand_has_auto_sizing(item):item.sizing_mode=sizing_modeitems.append((item,y,x))else:raiseValueError("Only LayoutDOM items can be inserted into a grid")ifnotmerge_toolsornottoolbar_location:returnGridBox(children=items,sizing_mode=sizing_mode)grid=GridBox(children=items)tools=sum([toolbar.toolsfortoolbarintoolbars],[])proxy=ProxyToolbar(toolbars=toolbars,tools=tools,**toolbar_options)toolbar=ToolbarBox(toolbar=proxy,toolbar_location=toolbar_location)iftoolbar_location=='above':returnColumn(children=[toolbar,grid],sizing_mode=sizing_mode)eliftoolbar_location=='below':returnColumn(children=[grid,toolbar],sizing_mode=sizing_mode)eliftoolbar_location=='left':returnRow(children=[toolbar,grid],sizing_mode=sizing_mode)eliftoolbar_location=='right':returnRow(children=[grid,toolbar],sizing_mode=sizing_mode)
[docs]defgrid(children:Any=[],sizing_mode:SizingModeType|None=None,nrows:int|None=None,ncols:int|None=None)->GridBox:""" Conveniently create a grid of layoutable objects. Grids are created by using ``GridBox`` model. This gives the most control over the layout of a grid, but is also tedious and may result in unreadable code in practical applications. ``grid()`` function remedies this by reducing the level of control, but in turn providing a more convenient API. Supported patterns: 1. Nested lists of layoutable objects. Assumes the top-level list represents a column and alternates between rows and columns in subsequent nesting levels. One can use ``None`` for padding purpose. >>> grid([p1, [[p2, p3], p4]]) GridBox(children=[ (p1, 0, 0, 1, 2), (p2, 1, 0, 1, 1), (p3, 2, 0, 1, 1), (p4, 1, 1, 2, 1), ]) 2. Nested ``Row`` and ``Column`` instances. Similar to the first pattern, just instead of using nested lists, it uses nested ``Row`` and ``Column`` models. This can be much more readable that the former. Note, however, that only models that don't have ``sizing_mode`` set are used. >>> grid(column(p1, row(column(p2, p3), p4))) GridBox(children=[ (p1, 0, 0, 1, 2), (p2, 1, 0, 1, 1), (p3, 2, 0, 1, 1), (p4, 1, 1, 2, 1), ]) 3. Flat list of layoutable objects. This requires ``nrows`` and/or ``ncols`` to be set. The input list will be rearranged into a 2D array accordingly. One can use ``None`` for padding purpose. >>> grid([p1, p2, p3, p4], ncols=2) GridBox(children=[ (p1, 0, 0, 1, 1), (p2, 0, 1, 1, 1), (p3, 1, 0, 1, 1), (p4, 1, 1, 1, 1), ]) """@dataclassclassrow:children:List[row|col]@dataclassclasscol:children:List[row|col]@dataclassclassItem:layout:LayoutDOMr0:intc0:intr1:intc1:int@dataclassclassGrid:nrows:intncols:intitems:List[Item]defflatten(layout)->GridBox:defgcd(a:int,b:int)->int:a,b=abs(a),abs(b)whileb!=0:a,b=b,a%breturnadeflcm(a:int,*rest:int)->int:forbinrest:a=(a*b)//gcd(a,b)returnadefnonempty(child:Grid)->bool:returnchild.nrows!=0andchild.ncols!=0def_flatten(layout:row|col|LayoutDOM)->Grid:ifisinstance(layout,row):children=list(filter(nonempty,map(_flatten,layout.children)))ifnotchildren:returnGrid(0,0,[])nrows=lcm(*[child.nrowsforchildinchildren])ncols=sum(child.ncolsforchildinchildren)items:List[Item]=[]offset=0forchildinchildren:factor=nrows//child.nrowsforiinchild.items:items.append(Item(i.layout,factor*i.r0,i.c0+offset,factor*i.r1,i.c1+offset))offset+=child.ncolsreturnGrid(nrows,ncols,items)elifisinstance(layout,col):children=list(filter(nonempty,map(_flatten,layout.children)))ifnotchildren:returnGrid(0,0,[])nrows=sum(child.nrowsforchildinchildren)ncols=lcm(*[child.ncolsforchildinchildren])items=[]offset=0forchildinchildren:factor=ncols//child.ncolsforiinchild.items:items.append(Item(i.layout,i.r0+offset,factor*i.c0,i.r1+offset,factor*i.c1))offset+=child.nrowsreturnGrid(nrows,ncols,items)else:returnGrid(1,1,[Item(layout,0,0,1,1)])grid=_flatten(layout)children=[]foriingrid.items:ifi.layoutisnotNone:children.append((i.layout,i.r0,i.c0,i.r1-i.r0,i.c1-i.c0))returnGridBox(children=children)layout:row|colifisinstance(children,list):ifnrowsisnotNoneorncolsisnotNone:N=len(children)ifncolsisNone:ncols=math.ceil(N/nrows)layout=col([row(children[i:i+ncols])foriinrange(0,N,ncols)])else:deftraverse(children:List[LayoutDOM],level:int=0):ifisinstance(children,list):container=coliflevel%2==0elserowreturncontainer([traverse(child,level+1)forchildinchildren])else:returnchildrenlayout=traverse(children)elifisinstance(children,LayoutDOM):defis_usable(child:LayoutDOM)->bool:return_has_auto_sizing(child)andchild.spacing==0deftraverse(item:LayoutDOM,top_level:bool=False):ifisinstance(item,Box)and(top_leveloris_usable(item)):container=colifisinstance(item,Column)elserowreturncontainer(list(map(traverse,item.children)))else:returnitemlayout=traverse(children,top_level=True)elifisinstance(children,str):raiseNotImplementedErrorelse:raiseValueError("expected a list, string or model")grid=flatten(layout)ifsizing_modeisnotNone:grid.sizing_mode=sizing_modeforchildingrid.children:layout=child[0]if_has_auto_sizing(layout):layout.sizing_mode=sizing_modereturngrid
#-----------------------------------------------------------------------------# Dev API#-----------------------------------------------------------------------------
[docs]classGridSpec:""" Simplifies grid layout specification. """nrows:intncols:int_arrangement:Dict[Tuple[int,int],LayoutDOM|None]def__init__(self,nrows:int,ncols:int)->None:self.nrows=nrowsself.ncols=ncolsself._arrangement={}from.util.deprecationimportdeprecateddeprecated("'GridSpec' is deprecated and will be removed in Bokeh 3.0")def__setitem__(self,key:Tuple[int|slice,int|slice],obj:LayoutDOM|List[LayoutDOM]|List[List[LayoutDOM]])->None:k1,k2=keyrow1:introw2:int|Noneifisinstance(k1,slice):row1,row2,_=k1.indices(self.nrows)else:ifk1<0:k1+=self.nrowsifk1>=self.nrowsork1<0:raiseIndexError("index out of range")row1,row2=k1,Nonecol1:intcol2:int|Noneifisinstance(k2,slice):col1,col2,_=k2.indices(self.ncols)else:ifk2<0:k2+=self.ncolsifk2>=self.ncolsork2<0:raiseIndexError("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] = [[...], ...]T=TypeVar("T")defget(obj:List[T]|None,i:int)->T|None:returnobj[i]ifobjisnotNoneand0<=i<len(obj)elseNoneifrow2isNoneandcol2isNone:assertisinstance(obj,LayoutDOM)self._arrangement[row1,col1]=objelifrow2isNone:assertcol2isnotNone_obj=cast(List[LayoutDOM],obj)forcolinrange(col1,col2):self._arrangement[row1,col]=get(_obj,col-col1)elifcol2isNone:assertrow2isnotNone_obj=cast(List[LayoutDOM],obj)forrowinrange(row1,row2):self._arrangement[row,col1]=get(_obj,row-row1)else:_obj=cast(List[List[LayoutDOM]],obj)forrow,colinzip(range(row1,row2),range(col1,col2)):self._arrangement[row,col]=get(get(_obj,row-row1),col-col1)def__iter__(self)->Iterator[List[LayoutDOM|None]]:array:List[List[LayoutDOM|None]]=[[None]*self.ncolsfor_inrange(0,self.nrows)]for(row,col),objinself._arrangement.items():array[row][col]=objreturniter(array)
#-----------------------------------------------------------------------------# Private API#-----------------------------------------------------------------------------def_has_auto_sizing(item:LayoutDOM)->bool:returnitem.sizing_modeisNoneanditem.width_policy=="auto"anditem.height_policy=="auto"L=TypeVar("L",bound=LayoutDOM)def_parse_children_arg(*args:L|List[L]|GridSpec,children:List[L]|None=None)->List[L]|GridSpec:# Set-up Children from args or kwargsiflen(args)>0andchildrenisnotNone:raiseValueError("'children' keyword cannot be used with positional arguments")ifnotchildren:iflen(args)==1:[arg]=argsifisinstance(arg,(GridSpec,list)):returnargreturnlist(args)returnchildrendef_handle_child_sizing(children:List[LayoutDOM],sizing_mode:SizingModeType|None,*,widget:str)->None:foriteminchildren:ifnotisinstance(item,LayoutDOM):raiseValueError(f"Only LayoutDOM items can be inserted into a {widget}. Tried to insert: {item} of type {type(item)}")ifsizing_modeisnotNoneand_has_auto_sizing(item):item.sizing_mode=sizing_modedef_create_grid(iterable:Iterable[LayoutDOM|List[LayoutDOM]],sizing_mode:SizingModeType|None,layer:int=0,**kwargs)->Row|Column:"""Recursively create grid from input lists."""return_list:List[LayoutDOM]=[]foriteminiterable:ifisinstance(item,list):return_list.append(_create_grid(item,sizing_mode,layer+1))elifisinstance(item,LayoutDOM):ifsizing_modeisnotNoneand_has_auto_sizing(item):item.sizing_mode=sizing_modereturn_list.append(item)else:raiseValueError("""Only LayoutDOM items can be inserted into a layout. Tried to insert: %s of type %s"""%(item,type(item)))iflayer%2==0:returncolumn(children=return_list,sizing_mode=sizing_mode,**kwargs)else:returnrow(children=return_list,sizing_mode=sizing_mode,**kwargs)T=TypeVar("T")def_chunks(l:Sequence[T],ncols:int)->Iterator[Sequence[T]]:"""Yield successive n-sized chunks from list, l."""assertisinstance(ncols,int),"ncols must be an integer"foriinrange(0,len(l),ncols):yieldl[i:i+ncols]#-----------------------------------------------------------------------------# Code#-----------------------------------------------------------------------------