#-----------------------------------------------------------------------------# Copyright (c) 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 importsimportmathfromcollectionsimportdefaultdictfromtypingimport(TYPE_CHECKING,Any,Callable,Iterable,Iterator,Literal,Sequence,TypeVar,Union,overload,)# Bokeh importsfrom.core.enumsimportLocation,LocationType,SizingModeTypefrom.core.property.singletonsimportUndefined,UndefinedTypefrom.modelsimport(Column,CopyTool,ExamineTool,FlexBox,FullscreenTool,GridBox,GridPlot,LayoutDOM,Plot,Row,SaveTool,Spacer,Tool,Toolbar,ToolProxy,UIElement,)from.util.dataclassesimportdataclassfrom.util.warningsimportwarnifTYPE_CHECKING:fromtyping_extensionsimportTypeAlias#-----------------------------------------------------------------------------# Globals and constants#-----------------------------------------------------------------------------__all__=('column','grid','gridplot','layout','row','Spacer',)ifTYPE_CHECKING:ToolbarOptions=Literal["logo","autohide","active_drag","active_inspect","active_scroll","active_tap","active_multi"]#-----------------------------------------------------------------------------# General API#-----------------------------------------------------------------------------@overloaddefrow(children:list[UIElement],*,sizing_mode:SizingModeType|None=None,**kwargs:Any)->Row:...@overloaddefrow(*children:UIElement,sizing_mode:SizingModeType|None=None,**kwargs:Any)->Row:...
[docs]defrow(*children:UIElement|list[UIElement],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.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:UIElement|list[UIElement],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.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]deflayout(*args:UIElement,children:list[UIElement]|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.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[UIElement|None]],*,sizing_mode:SizingModeType|None=None,toolbar_location:LocationType|None="above",ncols:int|None=None,width:int|None=None,height:int|None=None,toolbar_options:dict[ToolbarOptions,Any]|None=None,merge_tools:bool=True)->GridPlot:''' 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. 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.Toolbar`). If none is supplied, Toolbar's defaults will be used. merge_tools (``True``, ``False``): Combine tools from all child plots into a single toolbar. Returns: GridPlot: 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') ) '''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[UIElement,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=NoneifwidthisnotNone:item.width=widthifheightisnotNone:item.height=heightifsizing_modeisnotNoneand_has_auto_sizing(item):item.sizing_mode=sizing_modeitems.append((item,y,x))elifisinstance(item,UIElement):continueelse:raiseValueError("Only UIElement and LayoutDOM items can be inserted into a grid")defmerge(cls:type[Tool],group:list[Tool])->Tool|ToolProxy|None:ifissubclass(cls,(SaveTool,CopyTool,ExamineTool,FullscreenTool)):returncls()else:returnNonetools:list[Tool|ToolProxy]=[]fortoolbarintoolbars:tools.extend(toolbar.tools)ifmerge_tools:tools=group_tools(tools,merge=merge)logos=[toolbar.logofortoolbarintoolbars]autohides=[toolbar.autohidefortoolbarintoolbars]active_drags=[toolbar.active_dragfortoolbarintoolbars]active_inspects=[toolbar.active_inspectfortoolbarintoolbars]active_scrolls=[toolbar.active_scrollfortoolbarintoolbars]active_taps=[toolbar.active_tapfortoolbarintoolbars]active_multis=[toolbar.active_multifortoolbarintoolbars]V=TypeVar("V")defassert_unique(values:list[V],name:ToolbarOptions)->V|UndefinedType:ifnameintoolbar_options:returntoolbar_options[name]n=len(set(values))ifn==0:returnUndefinedelifn>1:warn(f"found multiple competing values for 'toolbar.{name}' property; using the latest value")returnvalues[-1]logo=assert_unique(logos,"logo")autohide=assert_unique(autohides,"autohide")active_drag=assert_unique(active_drags,"active_drag")active_inspect=assert_unique(active_inspects,"active_inspect")active_scroll=assert_unique(active_scrolls,"active_scroll")active_tap=assert_unique(active_taps,"active_tap")active_multi=assert_unique(active_multis,"active_multi")toolbar=Toolbar(tools=tools,logo=logo,autohide=autohide,active_drag=active_drag,active_inspect=active_inspect,active_scroll=active_scroll,active_tap=active_tap,active_multi=active_multi,)gp=GridPlot(children=items,toolbar=toolbar,toolbar_location=toolbar_location,sizing_mode=sizing_mode,)returngp
[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,FlexBox)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#-----------------------------------------------------------------------------T=TypeVar("T",bound=Tool)MergeFn:TypeAlias=Callable[[type[T],list[T]],Union[Tool,ToolProxy,None]]defgroup_tools(tools:list[Tool|ToolProxy],*,merge:MergeFn[Tool]|None=None,ignore:set[str]|None=None)->list[Tool|ToolProxy]:""" Group common tools into tool proxies. """@dataclassclassToolEntry:tool:Toolprops:Anyby_type:defaultdict[type[Tool],list[ToolEntry]]=defaultdict(list)computed:list[Tool|ToolProxy]=[]ifignoreisNone:ignore={"overlay","renderers"}fortoolintools:ifisinstance(tool,ToolProxy):computed.append(tool)else:props=tool.properties_with_values()forattrinignore:ifattrinprops:delprops[attr]by_type[tool.__class__].append(ToolEntry(tool,props))forcls,entriesinby_type.items():ifmergeisnotNone:merged=merge(cls,[entry.toolforentryinentries])ifmergedisnotNone:computed.append(merged)continuewhileentries:head,*tail=entriesgroup:list[Tool]=[head.tool]foriteminlist(tail):ifitem.props==head.props:group.append(item.tool)entries.remove(item)entries.remove(head)iflen(group)==1:computed.append(group[0])elifmergeisnotNoneand(tool:=merge(cls,group))isnotNone:computed.append(tool)else:computed.append(ToolProxy(tools=group))returncomputed#-----------------------------------------------------------------------------# 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],children:list[L]|None=None)->list[L]:# 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,list):returnargreturnlist(args)returnchildrendef_handle_child_sizing(children:list[UIElement],sizing_mode:SizingModeType|None,*,widget:str)->None:foriteminchildren:ifisinstance(item,UIElement):continueifnotisinstance(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[UIElement|list[UIElement]],sizing_mode:SizingModeType|None,layer:int=0,**kwargs)->Row|Column:"""Recursively create grid from input lists."""return_list:list[UIElement]=[]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)elifisinstance(item,UIElement):return_list.append(item)else:raiseValueError(f"""Only LayoutDOM items can be inserted into a layout. Tried to insert: {item} of type {type(item)}""",)iflayer%2==0:returncolumn(children=return_list,sizing_mode=sizing_mode,**kwargs)else:returnrow(children=return_list,sizing_mode=sizing_mode,**kwargs)I=TypeVar("I")def_chunks(l:Sequence[I],ncols:int)->Iterator[Sequence[I]]:"""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#-----------------------------------------------------------------------------