1
"""
2
Defines Layout classes which may be used to arrange panes and widgets
3
in flexible ways to build complex dashboards.
4
"""
5 7
from __future__ import absolute_import, division, unicode_literals
6

7 7
from collections import defaultdict, namedtuple
8

9 7
import param
10

11 7
from bokeh.models import Column as BkColumn, Row as BkRow
12

13 7
from ..io.model import hold
14 7
from ..io.state import state
15 7
from ..reactive import Reactive
16 7
from ..util import param_name, param_reprs
17

18 7
_row = namedtuple("row", ["children"])
19 7
_col = namedtuple("col", ["children"])
20

21

22 7
class Panel(Reactive):
23
    """
24
    Abstract baseclass for a layout of Viewables.
25
    """
26

27 7
    _bokeh_model = None
28

29 7
    __abstract = True
30

31 7
    _rename = {'objects': 'children'}
32

33 7
    _linked_props = []
34

35 7
    def __repr__(self, depth=0, max_depth=10):
36 7
        if depth > max_depth:
37 0
            return '...'
38 7
        spacer = '\n' + ('    ' * (depth+1))
39 7
        cls = type(self).__name__
40 7
        params = param_reprs(self, ['objects'])
41 7
        objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self)]
42 7
        if not params and not objs:
43 0
            return super(Panel, self).__repr__(depth+1)
44 7
        elif not params:
45 7
            template = '{cls}{spacer}{objs}'
46 0
        elif not objs:
47 0
            template = '{cls}({params})'
48
        else:
49 0
            template = '{cls}({params}){spacer}{objs}'
50 7
        return template.format(
51
            cls=cls, params=', '.join(params),
52
            objs=('%s' % spacer).join(objs), spacer=spacer)
53

54
    #----------------------------------------------------------------
55
    # Callback API
56
    #----------------------------------------------------------------
57

58 7
    def _update_model(self, events, msg, root, model, doc, comm=None):
59 7
        msg = dict(msg)
60 7
        if self._rename['objects'] in msg:
61 7
            old = events['objects'].old
62 7
            msg[self._rename['objects']] = self._get_objects(model, old, doc, root, comm)
63

64 7
        with hold(doc):
65 7
            super(Panel, self)._update_model(events, msg, root, model, doc, comm)
66 7
            from ..io import state
67 7
            ref = root.ref['id']
68 7
            if ref in state._views:
69 7
                state._views[ref][0]._preprocess(root)
70

71
    #----------------------------------------------------------------
72
    # Model API
73
    #----------------------------------------------------------------
74

75 7
    def _init_properties(self):
76 7
        properties = {k: v for k, v in self.param.get_param_values()
77
                      if v is not None}
78 7
        del properties['objects']
79 7
        return self._process_param_change(properties)
80

81 7
    def _get_objects(self, model, old_objects, doc, root, comm=None):
82
        """
83
        Returns new child models for the layout while reusing unchanged
84
        models and cleaning up any dropped objects.
85
        """
86 7
        from ..pane.base import panel, RerenderError
87 7
        new_models = []
88 7
        for i, pane in enumerate(self.objects):
89 7
            pane = panel(pane)
90 7
            self.objects[i] = pane
91

92 7
        for obj in old_objects:
93 7
            if obj not in self.objects:
94 7
                obj._cleanup(root)
95

96 7
        current_objects = list(self.objects)
97 7
        for i, pane in enumerate(self.objects):
98 7
            if pane in old_objects:
99 7
                child, _ = pane._models[root.ref['id']]
100
            else:
101 7
                try:
102 7
                    child = pane._get_model(doc, root, model, comm)
103 0
                except RerenderError:
104 0
                    return self._get_objects(model, current_objects[:i], doc, root, comm)
105 7
            new_models.append(child)
106 7
        return new_models
107

108 7
    def _get_model(self, doc, root=None, parent=None, comm=None):
109 7
        model = self._bokeh_model()
110 7
        if root is None:
111 7
            root = model
112 7
        objects = self._get_objects(model, [], doc, root, comm)
113 7
        props = dict(self._init_properties(), objects=objects)
114 7
        model.update(**self._process_param_change(props))
115 7
        self._models[root.ref['id']] = (model, parent)
116 7
        self._link_props(model, self._linked_props, doc, root, comm)
117 7
        return model
118

119
    #----------------------------------------------------------------
120
    # Public API
121
    #----------------------------------------------------------------
122

123 7
    def select(self, selector=None):
124
        """
125
        Iterates over the Viewable and any potential children in the
126
        applying the Selector.
127

128
        Arguments
129
        ---------
130
        selector: type or callable or None
131
          The selector allows selecting a subset of Viewables by
132
          declaring a type or callable function to filter by.
133

134
        Returns
135
        -------
136
        viewables: list(Viewable)
137
        """
138 7
        objects = super(Panel, self).select(selector)
139 7
        for obj in self:
140 7
            objects += obj.select(selector)
141 7
        return objects
142

143

144

145 7
class ListLike(param.Parameterized):
146

147 7
    objects = param.List(default=[], doc="""
148
        The list of child objects that make up the layout.""")
149
    
150 7
    def __getitem__(self, index):
151 7
        return self.objects[index]
152

153 7
    def __len__(self):
154 7
        return len(self.objects)
155

156 7
    def __iter__(self):
157 7
        for obj in self.objects:
158 7
            yield obj
159

160 7
    def __iadd__(self, other):
161 7
        self.extend(other)
162 7
        return self
163

164 7
    def __add__(self, other):
165 7
        if isinstance(other, ListLike):
166 7
            other = other.objects
167 7
        if not isinstance(other, list):
168 7
            stype = type(self).__name__
169 7
            otype = type(other).__name__
170 7
            raise ValueError("Cannot add items of type %s and %s, can only "
171
                             "combine %s.objects with list or ListLike object."
172
                             % (stype, otype, stype))
173 7
        return self.clone(*(self.objects+other))
174

175 7
    def __radd__(self, other):
176 7
        if isinstance(other, ListLike):
177 0
            other = other.objects
178 7
        if not isinstance(other, list):
179 0
            stype = type(self).__name__
180 0
            otype = type(other).__name__
181 0
            raise ValueError("Cannot add items of type %s and %s, can only "
182
                             "combine %s.objects with list or ListLike object."
183
                             % (otype, stype, stype))
184 7
        return self.clone(*(other+self.objects))
185

186 7
    def __contains__(self, obj):
187 7
        return obj in self.objects
188

189 7
    def __setitem__(self, index, panes):
190 7
        from ..pane import panel
191 7
        new_objects = list(self)
192 7
        if not isinstance(index, slice):
193 7
            start, end = index, index+1
194 7
            if start > len(self.objects):
195 0
                raise IndexError('Index %d out of bounds on %s '
196
                                 'containing %d objects.' %
197
                                 (end, type(self).__name__, len(self.objects)))
198 7
            panes = [panes]
199
        else:
200 7
            start = index.start or 0
201 7
            end = len(self) if index.stop is None else index.stop
202 7
            if index.start is None and index.stop is None:
203 7
                if not isinstance(panes, list):
204 7
                    raise IndexError('Expected a list of objects to '
205
                                     'replace the objects in the %s, '
206
                                     'got a %s type.' %
207
                                     (type(self).__name__, type(panes).__name__))
208 7
                expected = len(panes)
209 7
                new_objects = [None]*expected
210 7
                end = expected
211 7
            elif end > len(self.objects):
212 7
                raise IndexError('Index %d out of bounds on %s '
213
                                 'containing %d objects.' %
214
                                 (end, type(self).__name__, len(self.objects)))
215
            else:
216 7
                expected = end-start
217 7
            if not isinstance(panes, list) or len(panes) != expected:
218 7
                raise IndexError('Expected a list of %d objects to set '
219
                                 'on the %s to match the supplied slice.' %
220
                                 (expected, type(self).__name__))
221 7
        for i, pane in zip(range(start, end), panes):
222 7
            new_objects[i] = panel(pane)
223

224 7
        self.objects = new_objects
225

226 7
    def clone(self, *objects, **params):
227
        """
228
        Makes a copy of the layout sharing the same parameters.
229

230
        Arguments
231
        ---------
232
        objects: Objects to add to the cloned layout.
233
        params: Keyword arguments override the parameters on the clone.
234

235
        Returns
236
        -------
237
        Cloned layout object
238
        """
239 7
        if not objects:
240 7
            if 'objects' in params:
241 7
                objects = params.pop('objects')
242
            else:
243 7
                objects = self.objects
244 7
        elif 'objects' in params:
245 7
            raise ValueError("A %s's objects should be supplied either "
246
                             "as arguments or as a keyword, not both."
247
                             % type(self).__name__)
248 7
        p = dict(self.param.get_param_values(), **params)
249 7
        del p['objects']
250 7
        return type(self)(*objects, **p)
251

252 7
    def append(self, obj):
253
        """
254
        Appends an object to the layout.
255

256
        Arguments
257
        ---------
258
        obj (object): Panel component to add to the layout.
259
        """
260 7
        from ..pane import panel
261 7
        new_objects = list(self)
262 7
        new_objects.append(panel(obj))
263 7
        self.objects = new_objects
264

265 7
    def clear(self):
266
        """
267
        Clears the objects on this layout.
268
        """
269 7
        self.objects = []
270

271 7
    def extend(self, objects):
272
        """
273
        Extends the objects on this layout with a list.
274

275
        Arguments
276
        ---------
277
        objects (list): List of panel components to add to the layout.
278
        """
279 7
        from ..pane import panel
280 7
        new_objects = list(self)
281 7
        new_objects.extend(list(map(panel, objects)))
282 7
        self.objects = new_objects
283

284 7
    def insert(self, index, obj):
285
        """
286
        Inserts an object in the layout at the specified index.
287

288
        Arguments
289
        ---------
290
        index (int): Index at which to insert the object.
291
        object (object): Panel components to insert in the layout.
292
        """
293 7
        from ..pane import panel
294 7
        new_objects = list(self)
295 7
        new_objects.insert(index, panel(obj))
296 7
        self.objects = new_objects
297

298 7
    def pop(self, index):
299
        """
300
        Pops an item from the layout by index.
301

302
        Arguments
303
        ---------
304
        index (int): The index of the item to pop from the layout.
305
        """
306 7
        new_objects = list(self)
307 7
        if index in new_objects:
308 7
            index = new_objects.index(index)
309 7
        obj = new_objects.pop(index)
310 7
        self.objects = new_objects
311 7
        return obj
312

313 7
    def remove(self, obj):
314
        """
315
        Removes an object from the layout.
316

317
        Arguments
318
        ---------
319
        obj (object): The object to remove from the layout.
320
        """
321 7
        new_objects = list(self)
322 7
        new_objects.remove(obj)
323 7
        self.objects = new_objects
324

325 7
    def reverse(self):
326
        """
327
        Reverses the objects in the layout.
328
        """
329 7
        new_objects = list(self)
330 7
        new_objects.reverse()
331 7
        self.objects = new_objects
332

333
    
334

335 7
class ListPanel(ListLike, Panel):
336
    """
337
    An abstract baseclass for Panel objects with list-like children.
338
    """
339

340 7
    margin = param.Parameter(default=0, doc="""
341
        Allows to create additional space around the component. May
342
        be specified as a two-tuple of the form (vertical, horizontal)
343
        or a four-tuple (top, right, bottom, left).""")
344

345 7
    scroll = param.Boolean(default=False, doc="""
346
        Whether to add scrollbars if the content overflows the size
347
        of the container.""")
348

349 7
    _source_transforms = {'scroll': None}
350

351 7
    __abstract = True
352

353 7
    def __init__(self, *objects, **params):
354 7
        from ..pane import panel
355 7
        if objects:
356 7
            if 'objects' in params:
357 0
                raise ValueError("A %s's objects should be supplied either "
358
                                 "as positional arguments or as a keyword, "
359
                                 "not both." % type(self).__name__)
360 7
            params['objects'] = [panel(pane) for pane in objects]
361 7
        elif 'objects' in params:
362 7
            params['objects'] = [panel(pane) for pane in params['objects']]
363 7
        super(Panel, self).__init__(**params)
364

365 7
    def _process_param_change(self, params):
366 7
        scroll = params.pop('scroll', None)
367 7
        css_classes = self.css_classes or []
368 7
        if scroll:
369 0
            params['css_classes'] = css_classes + ['scrollable']
370 7
        elif scroll == False:
371 7
            params['css_classes'] = css_classes
372 7
        return super(ListPanel, self)._process_param_change(params)
373

374 7
    def _cleanup(self, root):
375 7
        if root.ref['id'] in state._fake_roots:
376 7
            state._fake_roots.remove(root.ref['id'])
377 7
        super(ListPanel, self)._cleanup(root)
378 7
        for p in self.objects:
379 7
            p._cleanup(root)
380

381

382 7
class NamedListPanel(ListPanel):
383

384 7
    active = param.Integer(default=0, bounds=(0, None), doc="""
385
        Index of the currently displayed objects.""")
386

387 7
    objects = param.List(default=[], doc="""
388
        The list of child objects that make up the tabs.""")
389

390 7
    def __init__(self, *items, **params):
391 7
        if 'objects' in params:
392 0
            if items:
393 0
                raise ValueError('%s objects should be supplied either '
394
                                 'as positional arguments or as a keyword, '
395
                                 'not both.' % type(self).__name__)
396 0
            items = params['objects']
397 7
        objects, self._names = self._to_objects_and_names(items)
398 7
        super(NamedListPanel, self).__init__(*objects, **params)
399 7
        self._panels = defaultdict(dict)
400 7
        self.param.watch(self._update_names, 'objects')
401
        # ALERT: Ensure that name update happens first, should be
402
        #        replaced by watch precedence support in param
403 7
        self._param_watchers['objects']['value'].reverse()
404

405 7
    def _to_object_and_name(self, item):
406 7
        from ..pane import panel
407 7
        if isinstance(item, tuple):
408 7
            name, item = item
409
        else:
410 7
            name = getattr(item, 'name', None)
411 7
        pane = panel(item, name=name)
412 7
        name = param_name(pane.name) if name is None else name
413 7
        return pane, name
414

415 7
    def _to_objects_and_names(self, items):
416 7
        objects, names = [], []
417 7
        for item in items:
418 7
            pane, name = self._to_object_and_name(item)
419 7
            objects.append(pane)
420 7
            names.append(name)
421 7
        return objects, names
422

423 7
    def _update_names(self, event):
424 7
        if len(event.new) == len(self._names):
425 7
            return
426 7
        names = []
427 7
        for obj in event.new:
428 7
            if obj in event.old:
429 7
                index = event.old.index(obj)
430 7
                name = self._names[index]
431
            else:
432 7
                name = obj.name
433 7
            names.append(name)
434 7
        self._names = names
435

436 7
    def _update_active(self, *events):
437 0
        pass
438

439
    #----------------------------------------------------------------
440
    # Public API
441
    #----------------------------------------------------------------
442

443 7
    def __add__(self, other):
444 7
        if isinstance(other, NamedListPanel):
445 7
            other = list(zip(other._names, other.objects))
446 7
        elif isinstance(other, ListLike):
447 0
            other = other.objects
448 7
        if not isinstance(other, list):
449 0
            stype = type(self).__name__
450 0
            otype = type(other).__name__
451 0
            raise ValueError("Cannot add items of type %s and %s, can only "
452
                             "combine %s.objects with list or ListLike object."
453
                             % (stype, otype, stype))
454 7
        objects = list(zip(self._names, self.objects))
455 7
        return self.clone(*(objects+other))
456

457 7
    def __radd__(self, other):
458 7
        if isinstance(other, NamedListPanel):
459 0
            other = list(zip(other._names, other.objects))
460 7
        elif isinstance(other, ListLike):
461 0
            other = other.objects
462 7
        if not isinstance(other, list):
463 0
            stype = type(self).__name__
464 0
            otype = type(other).__name__
465 0
            raise ValueError("Cannot add items of type %s and %s, can only "
466
                             "combine %s.objects with list or ListLike object."
467
                             % (otype, stype, stype))
468 7
        objects = list(zip(self._names, self.objects))
469 7
        return self.clone(*(other+objects))
470

471 7
    def __setitem__(self, index, panes):
472 7
        new_objects = list(self)
473 7
        if not isinstance(index, slice):
474 7
            if index > len(self.objects):
475 0
                raise IndexError('Index %d out of bounds on %s '
476
                                 'containing %d objects.' %
477
                                 (index, type(self).__name__, len(self.objects)))
478 7
            start, end = index, index+1
479 7
            panes = [panes]
480
        else:
481 7
            start = index.start or 0
482 7
            end = len(self.objects) if index.stop is None else index.stop
483 7
            if index.start is None and index.stop is None:
484 7
                if not isinstance(panes, list):
485 7
                    raise IndexError('Expected a list of objects to '
486
                                     'replace the objects in the %s, '
487
                                     'got a %s type.' %
488
                                     (type(self).__name__, type(panes).__name__))
489 7
                expected = len(panes)
490 7
                new_objects = [None]*expected
491 7
                self._names = [None]*len(panes)
492 7
                end = expected
493
            else:
494 7
                expected = end-start
495 7
                if end > len(self.objects):
496 7
                    raise IndexError('Index %d out of bounds on %s '
497
                                     'containing %d objects.' %
498
                                     (end, type(self).__name__, len(self.objects)))
499 7
            if not isinstance(panes, list) or len(panes) != expected:
500 7
                raise IndexError('Expected a list of %d objects to set '
501
                                 'on the %s to match the supplied slice.' %
502
                                 (expected, type(self).__name__))
503 7
        for i, pane in zip(range(start, end), panes):
504 7
            new_objects[i], self._names[i] = self._to_object_and_name(pane)
505 7
        self.objects = new_objects
506

507 7
    def clone(self, *objects, **params):
508
        """
509
        Makes a copy of the Tabs sharing the same parameters.
510

511
        Arguments
512
        ---------
513
        objects: Objects to add to the cloned Tabs object.
514
        params: Keyword arguments override the parameters on the clone.
515

516
        Returns
517
        -------
518
        Cloned Tabs object
519
        """
520 7
        if not objects:
521 7
            if 'objects' in params:
522 0
                objects = params.pop('objects')
523
            else:
524 7
                objects = zip(self._names, self.objects)
525 7
        elif 'objects' in params:
526 0
            raise ValueError('Tabs objects should be supplied either '
527
                             'as positional arguments or as a keyword, '
528
                             'not both.')
529 7
        p = dict(self.param.get_param_values(), **params)
530 7
        del p['objects']
531 7
        return type(self)(*objects, **params)
532

533 7
    def append(self, pane):
534
        """
535
        Appends an object to the tabs.
536

537
        Arguments
538
        ---------
539
        obj (object): Panel component to add as a tab.
540
        """
541 7
        new_object, new_name = self._to_object_and_name(pane)
542 7
        new_objects = list(self)
543 7
        new_objects.append(new_object)
544 7
        self._names.append(new_name)
545 7
        self.objects = new_objects
546

547 7
    def clear(self):
548
        """
549
        Clears the tabs.
550
        """
551 7
        self._names = []
552 7
        self.objects = []
553

554 7
    def extend(self, panes):
555
        """
556
        Extends the the tabs with a list.
557

558
        Arguments
559
        ---------
560
        objects (list): List of panel components to add as tabs.
561
        """
562 7
        new_objects, new_names = self._to_objects_and_names(panes)
563 7
        objects = list(self)
564 7
        objects.extend(new_objects)
565 7
        self._names.extend(new_names)
566 7
        self.objects = objects
567

568 7
    def insert(self, index, pane):
569
        """
570
        Inserts an object in the tabs at the specified index.
571

572
        Arguments
573
        ---------
574
        index (int): Index at which to insert the object.
575
        object (object): Panel components to insert as tabs.
576
        """
577 7
        new_object, new_name = self._to_object_and_name(pane)
578 7
        new_objects = list(self.objects)
579 7
        new_objects.insert(index, new_object)
580 7
        self._names.insert(index, new_name)
581 7
        self.objects = new_objects
582

583 7
    def pop(self, index):
584
        """
585
        Pops an item from the tabs by index.
586

587
        Arguments
588
        ---------
589
        index (int): The index of the item to pop from the tabs.
590
        """
591 7
        new_objects = list(self)
592 7
        if index in new_objects:
593 0
            index = new_objects.index(index)
594 7
        new_objects.pop(index)
595 7
        self._names.pop(index)
596 7
        self.objects = new_objects
597

598 7
    def remove(self, pane):
599
        """
600
        Removes an object from the tabs.
601

602
        Arguments
603
        ---------
604
        obj (object): The object to remove from the tabs.
605
        """
606 7
        new_objects = list(self)
607 7
        if pane in new_objects:
608 7
            index = new_objects.index(pane)
609 7
        new_objects.remove(pane)
610 7
        self._names.pop(index)
611 7
        self.objects = new_objects
612

613 7
    def reverse(self):
614
        """
615
        Reverses the tabs.
616
        """
617 7
        new_objects = list(self)
618 7
        new_objects.reverse()
619 7
        self._names.reverse()
620 7
        self.objects = new_objects
621

622

623

624 7
class Row(ListPanel):
625
    """
626
    Horizontal layout of Viewables.
627
    """
628

629 7
    col_sizing = param.Parameter()
630

631 7
    _bokeh_model = BkRow
632

633 7
    _rename = dict(ListPanel._rename, col_sizing='cols')
634

635

636 7
class Column(ListPanel):
637
    """
638
    Vertical layout of Viewables.
639
    """
640

641 7
    row_sizing = param.Parameter()
642

643 7
    _bokeh_model = BkColumn
644

645 7
    _rename = dict(ListPanel._rename, row_sizing='rows')
646

647

648 7
class WidgetBox(ListPanel):
649
    """
650
    Vertical layout of widgets.
651
    """
652

653 7
    css_classes = param.List(default=['panel-widget-box'], doc="""
654
        CSS classes to apply to the layout.""")
655

656 7
    disabled = param.Boolean(default=False, doc="""
657
        Whether the widget is disabled.""")
658

659 7
    horizontal = param.Boolean(default=False, doc="""
660
        Whether to lay out the widgets in a Row layout as opposed 
661
        to a Column layout.""")
662

663 7
    margin = param.Parameter(default=5, doc="""
664
        Allows to create additional space around the component. May
665
        be specified as a two-tuple of the form (vertical, horizontal)
666
        or a four-tuple (top, right, bottom, left).""")
667

668 7
    _source_transforms = {'disabled': None, 'horizontal': None}
669

670 7
    _rename = {'objects': 'children', 'horizontal': None}
671

672 7
    @property
673 2
    def _bokeh_model(self):
674 7
        return BkRow if self.horizontal else BkColumn
675

676 7
    @param.depends('disabled', 'objects', watch=True)
677 2
    def _disable_widgets(self):
678 0
        for obj in self:
679 0
            if hasattr(obj, 'disabled'):
680 0
                obj.disabled = self.disabled
681

682 7
    def __init__(self, *objects, **params):
683 7
        super(WidgetBox, self).__init__(*objects, **params)
684 7
        if self.disabled:
685 0
            self._disable_widgets()

Read our documentation on viewing source code .

Loading