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

7 2
from collections import defaultdict, namedtuple
8

9 2
import param
10

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

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

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

21

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

27 2
    _bokeh_model = None
28

29 2
    __abstract = True
30

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

33 2
    _linked_props = []
34

35 2
    def __repr__(self, depth=0, max_depth=10):
36 2
        if depth > max_depth:
37 0
            return '...'
38 2
        spacer = '\n' + ('    ' * (depth+1))
39 2
        cls = type(self).__name__
40 2
        params = param_reprs(self, ['objects'])
41 2
        objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self)]
42 2
        if not params and not objs:
43 0
            return super(Panel, self).__repr__(depth+1)
44 2
        elif not params:
45 2
            template = '{cls}{spacer}{objs}'
46 0
        elif not objs:
47 0
            template = '{cls}({params})'
48
        else:
49 0
            template = '{cls}({params}){spacer}{objs}'
50 2
        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 2
    def _update_model(self, events, msg, root, model, doc, comm=None):
59 2
        msg = dict(msg)
60 2
        if self._rename['objects'] in msg:
61 2
            old = events['objects'].old
62 2
            msg[self._rename['objects']] = self._get_objects(model, old, doc, root, comm)
63

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

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

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

81 2
    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 2
        from ..pane.base import panel, RerenderError
87 2
        new_models = []
88 2
        for i, pane in enumerate(self.objects):
89 2
            pane = panel(pane)
90 2
            self.objects[i] = pane
91

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

96 2
        current_objects = list(self.objects)
97 2
        for i, pane in enumerate(self.objects):
98 2
            if pane in old_objects:
99 2
                child, _ = pane._models[root.ref['id']]
100
            else:
101 2
                try:
102 2
                    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 2
            new_models.append(child)
106 2
        return new_models
107

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

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

123 2
    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 2
        objects = super(Panel, self).select(selector)
139 2
        for obj in self:
140 2
            objects += obj.select(selector)
141 2
        return objects
142

143

144

145 2
class ListLike(param.Parameterized):
146

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

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

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

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

164 2
    def __add__(self, other):
165 2
        if isinstance(other, ListLike):
166 2
            other = other.objects
167 2
        if not isinstance(other, list):
168 2
            stype = type(self).__name__
169 2
            otype = type(other).__name__
170 2
            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 2
        return self.clone(*(self.objects+other))
174

175 2
    def __radd__(self, other):
176 2
        if isinstance(other, ListLike):
177 0
            other = other.objects
178 2
        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 2
        return self.clone(*(other+self.objects))
185

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

189 2
    def __setitem__(self, index, panes):
190 2
        from ..pane import panel
191 2
        new_objects = list(self)
192 2
        if not isinstance(index, slice):
193 2
            start, end = index, index+1
194 2
            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 2
            panes = [panes]
199
        else:
200 2
            start = index.start or 0
201 2
            end = len(self) if index.stop is None else index.stop
202 2
            if index.start is None and index.stop is None:
203 2
                if not isinstance(panes, list):
204 2
                    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 2
                expected = len(panes)
209 2
                new_objects = [None]*expected
210 2
                end = expected
211 2
            elif end > len(self.objects):
212 2
                raise IndexError('Index %d out of bounds on %s '
213
                                 'containing %d objects.' %
214
                                 (end, type(self).__name__, len(self.objects)))
215
            else:
216 2
                expected = end-start
217 2
            if not isinstance(panes, list) or len(panes) != expected:
218 2
                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 2
        for i, pane in zip(range(start, end), panes):
222 2
            new_objects[i] = panel(pane)
223

224 2
        self.objects = new_objects
225

226 2
    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 2
        if not objects:
240 2
            if 'objects' in params:
241 2
                objects = params.pop('objects')
242
            else:
243 2
                objects = self.objects
244 2
        elif 'objects' in params:
245 2
            raise ValueError("A %s's objects should be supplied either "
246
                             "as arguments or as a keyword, not both."
247
                             % type(self).__name__)
248 2
        p = dict(self.param.get_param_values(), **params)
249 2
        del p['objects']
250 2
        return type(self)(*objects, **p)
251

252 2
    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 2
        from ..pane import panel
261 2
        new_objects = list(self)
262 2
        new_objects.append(panel(obj))
263 2
        self.objects = new_objects
264

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

271 2
    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 2
        from ..pane import panel
280 2
        new_objects = list(self)
281 2
        new_objects.extend(list(map(panel, objects)))
282 2
        self.objects = new_objects
283

284 2
    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 2
        from ..pane import panel
294 2
        new_objects = list(self)
295 2
        new_objects.insert(index, panel(obj))
296 2
        self.objects = new_objects
297

298 2
    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 2
        new_objects = list(self)
307 2
        if index in new_objects:
308 2
            index = new_objects.index(index)
309 2
        obj = new_objects.pop(index)
310 2
        self.objects = new_objects
311 2
        return obj
312

313 2
    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 2
        new_objects = list(self)
322 2
        new_objects.remove(obj)
323 2
        self.objects = new_objects
324

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

333
    
334

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

340 2
    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 2
    scroll = param.Boolean(default=False, doc="""
346
        Whether to add scrollbars if the content overflows the size
347
        of the container.""")
348

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

351 2
    __abstract = True
352

353 2
    def __init__(self, *objects, **params):
354 2
        from ..pane import panel
355 2
        if objects:
356 2
            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 2
            params['objects'] = [panel(pane) for pane in objects]
361 2
        elif 'objects' in params:
362 2
            params['objects'] = [panel(pane) for pane in params['objects']]
363 2
        super(Panel, self).__init__(**params)
364

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

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

381

382 2
class NamedListPanel(ListPanel):
383

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

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

390 2
    def __init__(self, *items, **params):
391 2
        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 2
        objects, self._names = self._to_objects_and_names(items)
398 2
        super(NamedListPanel, self).__init__(*objects, **params)
399 2
        self._panels = defaultdict(dict)
400 2
        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 2
        self._param_watchers['objects']['value'].reverse()
404

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

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

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

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

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

443 2
    def __add__(self, other):
444 2
        if isinstance(other, NamedListPanel):
445 2
            other = list(zip(other._names, other.objects))
446 2
        elif isinstance(other, ListLike):
447 0
            other = other.objects
448 2
        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 2
        objects = list(zip(self._names, self.objects))
455 2
        return self.clone(*(objects+other))
456

457 2
    def __radd__(self, other):
458 2
        if isinstance(other, NamedListPanel):
459 0
            other = list(zip(other._names, other.objects))
460 2
        elif isinstance(other, ListLike):
461 0
            other = other.objects
462 2
        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 2
        objects = list(zip(self._names, self.objects))
469 2
        return self.clone(*(other+objects))
470

471 2
    def __setitem__(self, index, panes):
472 2
        new_objects = list(self)
473 2
        if not isinstance(index, slice):
474 2
            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 2
            start, end = index, index+1
479 2
            panes = [panes]
480
        else:
481 2
            start = index.start or 0
482 2
            end = len(self.objects) if index.stop is None else index.stop
483 2
            if index.start is None and index.stop is None:
484 2
                if not isinstance(panes, list):
485 2
                    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 2
                expected = len(panes)
490 2
                new_objects = [None]*expected
491 2
                self._names = [None]*len(panes)
492 2
                end = expected
493
            else:
494 2
                expected = end-start
495 2
                if end > len(self.objects):
496 2
                    raise IndexError('Index %d out of bounds on %s '
497
                                     'containing %d objects.' %
498
                                     (end, type(self).__name__, len(self.objects)))
499 2
            if not isinstance(panes, list) or len(panes) != expected:
500 2
                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 2
        for i, pane in zip(range(start, end), panes):
504 2
            new_objects[i], self._names[i] = self._to_object_and_name(pane)
505 2
        self.objects = new_objects
506

507 2
    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 2
        if not objects:
521 2
            if 'objects' in params:
522 0
                objects = params.pop('objects')
523
            else:
524 2
                objects = zip(self._names, self.objects)
525 2
        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 2
        p = dict(self.param.get_param_values(), **params)
530 2
        del p['objects']
531 2
        return type(self)(*objects, **params)
532

533 2
    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 2
        new_object, new_name = self._to_object_and_name(pane)
542 2
        new_objects = list(self)
543 2
        new_objects.append(new_object)
544 2
        self._names.append(new_name)
545 2
        self.objects = new_objects
546

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

554 2
    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 2
        new_objects, new_names = self._to_objects_and_names(panes)
563 2
        objects = list(self)
564 2
        objects.extend(new_objects)
565 2
        self._names.extend(new_names)
566 2
        self.objects = objects
567

568 2
    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 2
        new_object, new_name = self._to_object_and_name(pane)
578 2
        new_objects = list(self.objects)
579 2
        new_objects.insert(index, new_object)
580 2
        self._names.insert(index, new_name)
581 2
        self.objects = new_objects
582

583 2
    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 2
        new_objects = list(self)
592 2
        if index in new_objects:
593 0
            index = new_objects.index(index)
594 2
        new_objects.pop(index)
595 2
        self._names.pop(index)
596 2
        self.objects = new_objects
597

598 2
    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 2
        new_objects = list(self)
607 2
        if pane in new_objects:
608 2
            index = new_objects.index(pane)
609 2
        new_objects.remove(pane)
610 2
        self._names.pop(index)
611 2
        self.objects = new_objects
612

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

622

623

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

629 2
    col_sizing = param.Parameter()
630

631 2
    _bokeh_model = BkRow
632

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

635

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

641 2
    row_sizing = param.Parameter()
642

643 2
    _bokeh_model = BkColumn
644

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

647

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

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

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

659 2
    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 2
    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 2
    _source_transforms = {'disabled': None, 'horizontal': None}
669

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

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

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

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

Read our documentation on viewing source code .

Loading