dashlab 0.2.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dashlab-0.2.2 → dashlab-0.3.0}/PKG-INFO +1 -1
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/_internal.py +2 -1
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/_version.py +1 -1
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/base.py +106 -42
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/core.py +1 -1
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/listw.css +22 -6
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/listw.js +3 -3
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/widgets.py +6 -8
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab.egg-info/PKG-INFO +1 -1
- {dashlab-0.2.2 → dashlab-0.3.0}/LICENSE +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/README.md +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/__init__.py +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/patches.py +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/animator.css +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/animator.js +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/fscreen.css +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/fscreen.js +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/utils.py +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab.egg-info/SOURCES.txt +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab.egg-info/dependency_links.txt +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab.egg-info/requires.txt +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/dashlab.egg-info/top_level.txt +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/pyproject.toml +0 -0
- {dashlab-0.2.2 → dashlab-0.3.0}/setup.cfg +0 -0
|
@@ -279,13 +279,14 @@ _general_css = {
|
|
|
279
279
|
"75%": { "content": "'●●●●●'" }, "100%": { "content": "''" },
|
|
280
280
|
},
|
|
281
281
|
'< .dl-dashboard > .other-area:not(:empty)': { # to distinguish other area when not empty
|
|
282
|
-
'border-top': '
|
|
282
|
+
'border-top': '0.2px inset var(--jp-border-color2, #8988)',
|
|
283
283
|
},
|
|
284
284
|
'.widget-vslider, .jupyter-widget-vslider': {'width': 'auto'}, # otherwise it spans too much area
|
|
285
285
|
'.content-width-button.jupyter-button, .content-width-button .jupyter-button': {
|
|
286
286
|
'width':'max-content',
|
|
287
287
|
'padding-left': '8px', 'padding-right': '8px',
|
|
288
288
|
},
|
|
289
|
+
'.widget-gridbox, .jupyter-widget-gridbox': {'overflow': 'unset'}, # unexpected scrollbars in gridbox
|
|
289
290
|
'> * .widget-box': {'flex-shrink': 0}, # avoid collapse of boxes,
|
|
290
291
|
'.js-plotly-plot': {'flex-grow': 1}, # stretch parent, rest is autosize stuff
|
|
291
292
|
'.columns':{
|
|
@@ -109,7 +109,7 @@ _useful_traits = [
|
|
|
109
109
|
]
|
|
110
110
|
# for validation
|
|
111
111
|
_pmethods = ['set_css','set_layout','update','gather','_handle_callbacks']
|
|
112
|
-
_pattrs = ['
|
|
112
|
+
_pattrs = ['params','changed','isfullscreen', 'children', 'layout','comm']
|
|
113
113
|
_omethods = ["_interactive_params"]
|
|
114
114
|
|
|
115
115
|
class _DashMeta(type):
|
|
@@ -184,7 +184,15 @@ _docs = {
|
|
|
184
184
|
- Dynamic widget property updates
|
|
185
185
|
- Built-in fullscreen support
|
|
186
186
|
""",
|
|
187
|
-
"css_info": re.sub(r'\bcode(\[.*?\])?\`', '`', _build_css.__doc__, flags=re.DOTALL) # inline code` or code['css']` not supported is dashlab itself
|
|
187
|
+
"css_info": re.sub(r'\bcode(\[.*?\])?\`', '`', _build_css.__doc__, flags=re.DOTALL), # inline code` or code['css']` not supported is dashlab itself
|
|
188
|
+
"gather": """
|
|
189
|
+
- Name of widgets from params or output widgets from callbacks by their CSS class names (e.g. 'out-stats').
|
|
190
|
+
- Special group names: `*all`, `*out`, `*ctrl`, `*repr` for all widgets, outputs, controls, representation widgets respectively.
|
|
191
|
+
- Regex patterns to match full widget names (e.g. 'fig.*' to match 'fig1', 'fig2' etc.). Only raises error if regex is invalid, not if no matches found.
|
|
192
|
+
- Exclusion patterns with '!' prefix (e.g. '!widget-name', '!out-.*' to exclude specific widgets or regex patterns).
|
|
193
|
+
This takes precedence over inclusion and can not be used with special group names, e.g. '!*out' is invalid.
|
|
194
|
+
- Direct DOMWidget instances can also be passed in any order to include external widgets not in params/outputs.
|
|
195
|
+
"""
|
|
188
196
|
}
|
|
189
197
|
|
|
190
198
|
def _expose_widget(v):
|
|
@@ -194,6 +202,15 @@ def _expose_widget(v):
|
|
|
194
202
|
return v.value
|
|
195
203
|
return v
|
|
196
204
|
|
|
205
|
+
def _used_widgets(box): # recursively find used widget names in layout
|
|
206
|
+
used_names = {box._kwarg} if hasattr(box, '_kwarg') else set() # box itself can be a widget with _kwarg
|
|
207
|
+
for w in box.children:
|
|
208
|
+
if isinstance(w, ipw.Box):
|
|
209
|
+
used_names.update(_used_widgets(w))
|
|
210
|
+
elif hasattr(w, '_kwarg') and w._kwarg not in used_names:
|
|
211
|
+
used_names.add(w._kwarg)
|
|
212
|
+
return used_names
|
|
213
|
+
|
|
197
214
|
@_add_traits
|
|
198
215
|
@_fix_init_sig
|
|
199
216
|
@_format_docs(**_docs)
|
|
@@ -234,7 +251,7 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
234
251
|
# Create and layout
|
|
235
252
|
dash = MyDashboard()
|
|
236
253
|
dash.set_layout(
|
|
237
|
-
left_sidebar=
|
|
254
|
+
left_sidebar=['*ctrl'], # control widgets on left
|
|
238
255
|
center= ipw.VBox(dash.gather('fig', ipw.HTML('Showing Stats'), # plot and stats in a VBox explicitly
|
|
239
256
|
)
|
|
240
257
|
|
|
@@ -281,7 +298,6 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
281
298
|
self.__app.layout.position = 'relative' # contain absolute items inside
|
|
282
299
|
self.__app._size_to_css = _size_to_css # enables em, rem
|
|
283
300
|
self.__app._user_layout = {} # store user layout for dynamic updates
|
|
284
|
-
self.__app._used_names = set() # store used names to avoid duplicates, handled in set_layout and gather functions
|
|
285
301
|
self.__other = ipw.VBox().add_class('other-area') # this should be empty to enable CSS perfectly, unless filled below
|
|
286
302
|
self.update = self.__update # needs avoid checking in metaclass, but restric in subclasses, need before setup
|
|
287
303
|
self.__setup()
|
|
@@ -308,7 +324,11 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
308
324
|
for child in self.children:
|
|
309
325
|
if hasattr(child, '_kwarg') and child._kwarg in wparams: # keep only in params, not all, like button, and outputs
|
|
310
326
|
wparams[child._kwarg] = child # Exposes widgets in params for setting options inside callbacks to trigger updates in chain
|
|
311
|
-
|
|
327
|
+
|
|
328
|
+
# Make sure widgets are not used from any other instance, which can cause side effects
|
|
329
|
+
for k, w in wparams.items():
|
|
330
|
+
self.__mark_instance(k, w, check=True)
|
|
331
|
+
|
|
312
332
|
self.set_trait('params', namedtuple('InteractiveParams', wparams.keys())(**wparams))
|
|
313
333
|
|
|
314
334
|
# Need to fix kwargs_widgets too
|
|
@@ -324,6 +344,8 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
324
344
|
self.children += (*extras, self.__style_html) # add extra widgets to box children
|
|
325
345
|
self.out.add_class("out-main")
|
|
326
346
|
self.out._kwarg = "out-main" # needs to be in all widgets
|
|
347
|
+
self.__mark_instance("out-main", self.out)
|
|
348
|
+
self.__out_main = self.out # keep a reference, so if user changes self.out, we still have it
|
|
327
349
|
|
|
328
350
|
for w in self.kwargs_widgets:
|
|
329
351
|
c = getattr(w, '_kwarg','')
|
|
@@ -331,6 +353,13 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
331
353
|
getattr(w, 'add_class', lambda v: None)(c) # for grid area
|
|
332
354
|
|
|
333
355
|
self._handle_callbacks() # collects callbacks and run updates
|
|
356
|
+
self.set_css() # apply default CSS
|
|
357
|
+
|
|
358
|
+
def __mark_instance(self, name, widget, check=False):
|
|
359
|
+
if isinstance(widget,DOMWidget): # check used only for params widgets
|
|
360
|
+
if check and hasattr(widget, '_dl_instance_id') and widget._dl_instance_id != self.__css_class:
|
|
361
|
+
raise ValueError(f"Widget {name!r} is already used in another Dashboard instance, use a unique widget for each instance to avoid side effects!")
|
|
362
|
+
widget._dl_instance_id = self.__css_class # mark as used in this instance
|
|
334
363
|
|
|
335
364
|
def __order_widgets(self, outputs):
|
|
336
365
|
kw_map = {w._kwarg: w for w in self.params if hasattr(w, '_kwarg') and isinstance(w, DOMWidget)}
|
|
@@ -343,7 +372,7 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
343
372
|
# 2) outputs in registration order and shoould have _kwarg internally
|
|
344
373
|
ordered.update({out._kwarg: out for out in outputs})
|
|
345
374
|
# 3) main output
|
|
346
|
-
ordered["out-main"] = self.
|
|
375
|
+
ordered["out-main"] = self.__out_main
|
|
347
376
|
# 4) anything else with _kwarg not yet included
|
|
348
377
|
ordered.update({name: w for name, w in kw_map.items() if name not in ordered})
|
|
349
378
|
return ordered
|
|
@@ -355,7 +384,7 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
355
384
|
|
|
356
385
|
outputs = self.__func2widgets() # build stuff, before actual interact
|
|
357
386
|
self.__all_widgets = self.__order_widgets(outputs) # save it once for sending to app layout set afterwards
|
|
358
|
-
self.
|
|
387
|
+
self.__groups = self.__create_groups(self.__all_widgets) # create groups of widgets
|
|
359
388
|
if self.__app._user_layout:
|
|
360
389
|
self.set_layout(**self.__app._user_layout) # this will reset new and old outputs in layout
|
|
361
390
|
else:
|
|
@@ -391,48 +420,87 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
391
420
|
raise type(e)(f"In {key!r} of layout: {e}") from e # better error message
|
|
392
421
|
return layout_widgets
|
|
393
422
|
|
|
394
|
-
|
|
423
|
+
@_format_docs(**_docs)
|
|
424
|
+
def gather(self, *widgets: 'str | DOMWidget') -> tuple[DOMWidget]:
|
|
395
425
|
"""Get list of widgets by names or general widgets for layout configuration.
|
|
396
426
|
This can be used to collect widgets to embed at any nesting level in layout.
|
|
397
427
|
|
|
398
|
-
**Parameters
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
**Returns**:
|
|
403
|
-
|
|
404
|
-
- List of DOMWidget instances corresponding to the provided names or instances.
|
|
428
|
+
**Parameters** (str | DOMWidget):
|
|
429
|
+
{gather}
|
|
430
|
+
**Returns**: List of DOMWidget instances corresponding to the provided names or instances.
|
|
405
431
|
|
|
406
432
|
**Example**:
|
|
407
433
|
|
|
408
434
|
```python
|
|
409
435
|
# fig is a FigureWidget param, out-stats is a callback output
|
|
410
436
|
app.set_layout(
|
|
411
|
-
left_sidebar = ['x','y'], # or dash.gather('x','y') is same
|
|
437
|
+
left_sidebar = ['x','y'], # or dash.gather('x','y') is same, or if only two controls x,y then *ctrl works too
|
|
412
438
|
center = app.TabsWidget(
|
|
413
439
|
app.gether('fig', ipw.HTML('Showing Stats'), 'out-stats')
|
|
414
440
|
) # Not possible to pass names list directly inside a nested widget container, so use gather in nesting levels
|
|
415
441
|
) # app is instance of DashboardBase
|
|
416
442
|
# Placed (FigureWidget, HTML, Output) at, can be used in set_layout
|
|
417
443
|
```
|
|
418
|
-
|
|
419
|
-
**⚠️ Important**: Widgets gathered but not actually used in `set_layout()` will be marked as "used"
|
|
420
|
-
and will NOT appear in the layout. Only call `gather()` when you intend to pass the result to `set_layout()`.
|
|
421
444
|
"""
|
|
422
|
-
|
|
445
|
+
specials = ['*all','*out','*ctrl','*repr']
|
|
446
|
+
Gs, Ws = self.__groups, self.__all_widgets
|
|
447
|
+
|
|
448
|
+
# First collect all excluded names
|
|
449
|
+
exclude = set()
|
|
450
|
+
for name in widgets:
|
|
451
|
+
if isinstance(name, str) and name.startswith('!'):
|
|
452
|
+
exp = name[1:] # Remove '!' prefix
|
|
453
|
+
if not exp.strip(): # Handle empty exclusion patterns
|
|
454
|
+
raise ValueError(f"Invalid exclusion pattern {name!r} - empty patterns not allowed")
|
|
455
|
+
if exp in specials:
|
|
456
|
+
raise ValueError(f"Exclusion pattern {name!r} cannot be a special group name, use '!widget-name' or regex patterns prefixed with '!'.")
|
|
457
|
+
if exp in Ws:
|
|
458
|
+
exclude.add(exp) # Exact name exclusion
|
|
459
|
+
else: # Regex exclusion pattern - check against all widget names
|
|
460
|
+
try:
|
|
461
|
+
exclude.update([wname for wname in Ws.keys() if re.fullmatch(exp, wname)])
|
|
462
|
+
except re.error as e:
|
|
463
|
+
raise ValueError(f"Invalid regex pattern in exclusion {name!r}: {e}")
|
|
464
|
+
|
|
465
|
+
collected = [] # And collect included names keeping exluded out
|
|
423
466
|
for name in widgets:
|
|
424
467
|
if isinstance(name, str):
|
|
425
|
-
if name
|
|
426
|
-
|
|
427
|
-
|
|
468
|
+
if not name.strip(): # Handle empty strings and whitespace
|
|
469
|
+
raise ValueError(f"Invalid widget name {name!r} - empty strings not allowed")
|
|
470
|
+
|
|
471
|
+
if name.startswith('!'):
|
|
472
|
+
continue # Skip exclusion patterns, already processed above
|
|
473
|
+
elif name in Ws: # catch all names without regex first and exlcuded above
|
|
474
|
+
if name not in exclude: # exact name inclusion
|
|
475
|
+
collected.append(Ws[name])
|
|
476
|
+
elif name.startswith('*'): # special groups
|
|
477
|
+
if name == '*all':
|
|
478
|
+
collected.extend([Ws[k] for k in Ws.keys() if k not in exclude])
|
|
479
|
+
elif name == '*out':
|
|
480
|
+
collected.extend([Ws[n] for n in Gs.outputs if n not in exclude])
|
|
481
|
+
elif name == '*ctrl':
|
|
482
|
+
collected.extend([Ws[n] for n in Gs.controls if n not in exclude])
|
|
483
|
+
elif name == '*repr':
|
|
484
|
+
collected.extend([Ws[n] for n in Gs.others if n not in exclude])
|
|
485
|
+
else:
|
|
486
|
+
raise ValueError(f"Invalid special group name {name!r}, valid names are: {specials}")
|
|
428
487
|
else:
|
|
429
|
-
|
|
488
|
+
try:
|
|
489
|
+
matches = [wname for wname in Ws.keys() if re.fullmatch(name, wname)]
|
|
490
|
+
collected.extend([Ws[wname] for wname in matches if wname not in exclude])
|
|
491
|
+
except re.error as e: # only raise error if regex is invalid, not if no matches found
|
|
492
|
+
raise ValueError(
|
|
493
|
+
f"Invalid widget name {name!r}.\n"
|
|
494
|
+
f"Valid names: {list(Ws.keys())}\n"
|
|
495
|
+
f"Special groups: {specials}\n"
|
|
496
|
+
f"regex patterns to match full name in params/outputs are also supported.")
|
|
430
497
|
elif isinstance(name, ipw.DOMWidget):
|
|
431
498
|
collected.append(name)
|
|
432
499
|
else:
|
|
433
500
|
raise TypeError(f"Each item must be a string or DOMWidget, got {type(name).__name__}")
|
|
434
501
|
return tuple(collected)
|
|
435
502
|
|
|
503
|
+
@_format_docs(gather = textwrap.indent(_docs['gather'], ' '))
|
|
436
504
|
def set_layout(self,
|
|
437
505
|
header: 'list[str, DOMWidget] | DOMWidget' = None,
|
|
438
506
|
center: 'list[str, DOMWidget] | DOMWidget' = None,
|
|
@@ -459,8 +527,10 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
459
527
|
- right_sidebar: Right side widgets
|
|
460
528
|
- footer: Bottom widgets
|
|
461
529
|
- Each of above must be of type `list[str, DOMWidget] | DOMWidget` of widgets/ params names if given.
|
|
462
|
-
- To get params/outputs by names at a nesting level, use `gather()` method, e.g. `center = TabsWidget(dash.gather('fig', 'out-stats'))`
|
|
463
530
|
- If a single widget is passed, it will be used directly, if a list/tuple is passed, it will be wrapped in a VBox (except center which uses GridBox).
|
|
531
|
+
{gather}
|
|
532
|
+
- If None, the area will be hidden.
|
|
533
|
+
- To get params/outputs by names at a nesting level, use `gather()` method, e.g. `center = TabsWidget(dash.gather('fig', 'out-stats'))`
|
|
464
534
|
- Grid Properties:
|
|
465
535
|
- pane_widths: list[str] - Widths for [left, center, right]
|
|
466
536
|
- pane_heights: list[str] - Heights for [header, center, footer]
|
|
@@ -476,8 +546,8 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
476
546
|
|
|
477
547
|
```python
|
|
478
548
|
dash.set_layout( # dash is an instance of DashboardBase
|
|
479
|
-
left_sidebar=
|
|
480
|
-
center=['fig', *
|
|
549
|
+
left_sidebar=['*ctrl'], # control widgets on left
|
|
550
|
+
center=['fig', '*out'], # fig and other outputs in center
|
|
481
551
|
pane_widths=['200px', '1fr', 'auto'],
|
|
482
552
|
pane_heights=['auto', '1fr', 'auto'],
|
|
483
553
|
grid_gap='1rem'
|
|
@@ -505,11 +575,11 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
505
575
|
self.__app.set_trait(key, value.add_class(key.replace('_','-'))) # for user CSS
|
|
506
576
|
elif value: # class own traits and Layout properties are linked here
|
|
507
577
|
self.__app.set_trait(key, value)
|
|
508
|
-
|
|
509
|
-
self.__other.children += tuple([v for k,v in self.__all_widgets.items() if k not in self.__app._used_names])
|
|
578
|
+
|
|
510
579
|
del layout_widgets # release references
|
|
511
|
-
self.__app
|
|
512
|
-
|
|
580
|
+
if names := _used_widgets(self.__app):
|
|
581
|
+
self.__other.children += tuple([v for k,v in self.__all_widgets.items() if k not in names])
|
|
582
|
+
|
|
513
583
|
# We are adding a reaonly isfullscreen trait set through button on parent class
|
|
514
584
|
fs_btn = FullscreenButton()
|
|
515
585
|
fs_btn.observe(lambda c: self.set_trait('isfullscreen',c.new), names='isfullscreen') # setting readonly property
|
|
@@ -573,11 +643,9 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
573
643
|
cent_sl = f"{main_sl} > .center"
|
|
574
644
|
_css = _build_css(('.dl-dashboard > .dl-DashApp',),_general_css)
|
|
575
645
|
|
|
576
|
-
fs_css = main.pop(':fullscreen',{}) or main.pop('^:fullscreen',{}) # both valid
|
|
577
|
-
if fs_css: # fullscreen css given by user, full screen is top interact, not inside one as button is there
|
|
578
|
-
_css += ('\n' + _build_css((f".{self.__css_class}.widget-interact.dl-dashboard:fullscreen > .dl-DashApp",), fs_css))
|
|
579
|
-
|
|
580
646
|
if main:
|
|
647
|
+
if fs_css := main.pop(':fullscreen',{}) or main.pop('^:fullscreen',{}): # both valid
|
|
648
|
+
_css += ('\n' + _build_css((f".{self.__css_class}.widget-interact.dl-dashboard:fullscreen > .dl-DashApp",), fs_css))
|
|
581
649
|
_css += ("\n" + _build_css((main_sl,), main))
|
|
582
650
|
if center:
|
|
583
651
|
_css += ("\n" + _build_css((cent_sl,), center))
|
|
@@ -731,6 +799,7 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
731
799
|
callbacks.append(new_func)
|
|
732
800
|
|
|
733
801
|
if out is not None:
|
|
802
|
+
self.__mark_instance(out._kwarg, out)
|
|
734
803
|
outputs.append(out)
|
|
735
804
|
|
|
736
805
|
self.__icallbacks = tuple(callbacks) # set back
|
|
@@ -793,15 +862,10 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
793
862
|
w._hinting_btn_update = update_hint # keep reference to clean up before adding next time
|
|
794
863
|
|
|
795
864
|
@property
|
|
796
|
-
def outputs(self) -> tuple:
|
|
797
|
-
return tuple([w for _, w in self.__all_widgets.items() if isinstance(w, ipw.Output)])
|
|
865
|
+
def outputs(self) -> tuple: raise DeprecationWarning("outputs property is deprecated, use gather('*out') instead.")
|
|
798
866
|
|
|
799
867
|
@property
|
|
800
|
-
def groups(self) -> namedtuple:
|
|
801
|
-
"""NamedTuple of widget groups: controls, outputs, others."""
|
|
802
|
-
if not hasattr(self, '_groups'):
|
|
803
|
-
self._groups = self.__create_groups(self.__all_widgets)
|
|
804
|
-
return self._groups
|
|
868
|
+
def groups(self) -> namedtuple: raise DeprecationWarning("groups property is deprecated, use gather('*ctrl'), gather('*out'), gather('*repr') instead to pick a group of widgets.")
|
|
805
869
|
|
|
806
870
|
def __run_updates(self, **kwargs):
|
|
807
871
|
with self.__user_ctx(), print_error():
|
|
@@ -138,7 +138,7 @@ class Dashboard(DashboardBase):
|
|
|
138
138
|
print(x+5,y)
|
|
139
139
|
|
|
140
140
|
# after adding callbacks, we can set CSS and layout to include all output widgets created
|
|
141
|
-
app.set_layout(left_sidebar=
|
|
141
|
+
app.set_layout(left_sidebar=['*ctrl'],center=['*out'])
|
|
142
142
|
|
|
143
143
|
app # at end of cell to display the app
|
|
144
144
|
```
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-direction: column;
|
|
4
4
|
overflow-y: auto; /* user can set max height, so allow this */
|
|
5
|
+
border-radius: 4px;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
.list-widget:not(.tabs) {
|
|
8
|
-
border-radius: 4px;
|
|
9
9
|
margin-block: 0.5em 0.5em;
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
.list-widget.tabs {
|
|
14
14
|
flex-direction: row;
|
|
15
15
|
margin-bottom: 0;
|
|
16
|
-
border-radius: 4px 4px 0 0;
|
|
17
|
-
border-bottom: 1px inset #8884;
|
|
18
16
|
box-sizing: border-box;
|
|
19
17
|
}
|
|
20
18
|
|
|
@@ -28,7 +26,6 @@
|
|
|
28
26
|
.list-widget.tabs .list-item {
|
|
29
27
|
flex: 0 1 auto; /* don't grow, but shrink, and base of auto like browser tabs */
|
|
30
28
|
min-width: 60px; /* Minimum tab width for usability */
|
|
31
|
-
border-radius: 4px 4px 0 0;
|
|
32
29
|
margin: 0 1px 0 0; /* Reduced margin for tighter spacing */
|
|
33
30
|
white-space: nowrap;
|
|
34
31
|
text-align: center; /* Center text in each tab */
|
|
@@ -44,7 +41,6 @@
|
|
|
44
41
|
/* Selected tab styling */
|
|
45
42
|
.list-widget.tabs .list-item.selected {
|
|
46
43
|
border-bottom: 1px inset var(--accent-color, #8884);
|
|
47
|
-
border-radius: 4px 4px 0 0;
|
|
48
44
|
border-left: none;
|
|
49
45
|
font-weight: bold;
|
|
50
46
|
}
|
|
@@ -98,9 +94,29 @@
|
|
|
98
94
|
box-shadow: 0 0 1px inset #8884;
|
|
99
95
|
font-weight: bold;
|
|
100
96
|
border-radius: 4px;
|
|
101
|
-
border-left:
|
|
97
|
+
border-left: 1px solid var(--accent-color, #8884);
|
|
102
98
|
}
|
|
103
99
|
|
|
104
100
|
.list-item:active {
|
|
105
101
|
transform: scale(0.99);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Since TabsWidget does not inherit from AnyWidget, but has ListWidget there, we can use it for CSS */
|
|
105
|
+
|
|
106
|
+
.tabs-widget .list-widget.tabs {
|
|
107
|
+
border-radius: 4px 4px 0 0;
|
|
108
|
+
border-bottom: 1px inset #8884;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.tabs-widget .list-widget.tabs .list-item:hover {
|
|
112
|
+
border-radius: 4px 4px 0 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.tabs-widget .list-item.selected {
|
|
116
|
+
border-radius: 4px 4px 0 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.tabs-widget .tabs-stack {
|
|
120
|
+
border-radius: 4px;
|
|
121
|
+
padding: 4px;
|
|
106
122
|
}
|
|
@@ -8,8 +8,8 @@ function render({model, el}) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function updateTabs() {
|
|
11
|
-
const
|
|
12
|
-
el.classList.toggle('tabs', Boolean(
|
|
11
|
+
const vert = model.get('vertical');
|
|
12
|
+
el.classList.toggle('tabs', Boolean(!vert));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function createItem(opt) {
|
|
@@ -62,7 +62,7 @@ function render({model, el}) {
|
|
|
62
62
|
|
|
63
63
|
model.on('change:description', updateDescription);
|
|
64
64
|
model.on('change:_options', updateList);
|
|
65
|
-
model.on('change:
|
|
65
|
+
model.on('change:vertical', updateTabs);
|
|
66
66
|
|
|
67
67
|
// Intercept custom messages from the backend
|
|
68
68
|
model.on('msg:custom', (msg) => {
|
|
@@ -34,7 +34,7 @@ class ListWidget(AnyWidget,ValueWidget):
|
|
|
34
34
|
- `value`: Any, currently selected value.
|
|
35
35
|
- `transform`: Callable, function such that transform(item) -> str, for each item in options. Default is `repr`.
|
|
36
36
|
- `html`: str, HTML representation of the currently selected item through transform.
|
|
37
|
-
- `
|
|
37
|
+
- `vertical`: bool, if False, display as tabs instead of list. If you want to use as tabs, consider using `TabsWidget` instead.
|
|
38
38
|
|
|
39
39
|
You can set `ListWidget.layout.max_height` to limit the maximum height (default 400px) of the list. The list will scroll if it exceeds this height.
|
|
40
40
|
"""
|
|
@@ -45,7 +45,7 @@ class ListWidget(AnyWidget,ValueWidget):
|
|
|
45
45
|
options = traitlets.List() # only on backend
|
|
46
46
|
value = traitlets.Any(None, allow_none=True,read_only=True) # only backend
|
|
47
47
|
html = traitlets.Unicode('',read_only=True, help="html = transform(value)") # This is only python side
|
|
48
|
-
|
|
48
|
+
vertical = traitlets.Bool(True,help="If False, display as tabs instead of list").tag(sync=True)
|
|
49
49
|
|
|
50
50
|
_esm = Path(__file__).with_name('static') / 'listw.js'
|
|
51
51
|
_css = Path(__file__).with_name('static') / 'listw.css'
|
|
@@ -136,13 +136,13 @@ class TabsWidget(GridBox):
|
|
|
136
136
|
"""
|
|
137
137
|
titles = traitlets.List([], help="List of tab titles")
|
|
138
138
|
selected_index = traitlets.Int(0, allow_none=True, help="Index of currently selected tab")
|
|
139
|
-
vertical = traitlets.Bool(False, help="If True, display tabs vertically on left")
|
|
140
139
|
tabs_width = traitlets.Unicode('auto', help="width of tabs when vertical") # can be any valid css width
|
|
141
140
|
tabs_height = traitlets.Unicode('2em', help="height of tabs when horizontal") # can be any valid css height
|
|
141
|
+
vertical = traitlets.Bool(False, help="If True, display tabs vertically on the left side")
|
|
142
142
|
|
|
143
143
|
def __init__(self, children=None, titles=None, vertical=False, tabs_width='auto', tabs_height='2em', **kwargs):
|
|
144
|
-
self._lw = ListWidget(description=None)
|
|
145
|
-
self._stack = Stack()
|
|
144
|
+
self._lw = ListWidget(description=None, vertical=vertical)
|
|
145
|
+
self._stack = Stack().add_class('tabs-stack')
|
|
146
146
|
self._init_titles = titles or [] # store initial titles as list even if no children yet
|
|
147
147
|
super().__init__(**kwargs)
|
|
148
148
|
self.add_class('tabs-widget') # for custom styling
|
|
@@ -152,11 +152,9 @@ class TabsWidget(GridBox):
|
|
|
152
152
|
self.children = children or [] # set children to stack
|
|
153
153
|
self.tabs_width = tabs_width
|
|
154
154
|
self.tabs_height = tabs_height
|
|
155
|
-
self.vertical = vertical
|
|
156
155
|
self._update_layout(None) # initial layout update
|
|
157
|
-
|
|
158
|
-
traitlets.link((self,'vertical'),(self._lw,'tabs'), transform=(lambda v: not v, lambda v: not v))
|
|
159
156
|
traitlets.link((self._lw,'index'),(self,'selected_index')) # on click, it should update selected_index
|
|
157
|
+
traitlets.link((self._lw,'vertical'),(self,'vertical')) # link vertical to listwidget
|
|
160
158
|
|
|
161
159
|
@traitlets.observe('selected_index')
|
|
162
160
|
def _validate_selected(self, change):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|