dashlab 0.2.2__tar.gz → 0.3.1__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.1}/PKG-INFO +1 -1
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/_internal.py +2 -1
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/_version.py +1 -1
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/base.py +143 -51
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/core.py +1 -1
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/static/listw.css +22 -6
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/static/listw.js +3 -3
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/widgets.py +6 -8
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab.egg-info/PKG-INFO +1 -1
- {dashlab-0.2.2 → dashlab-0.3.1}/LICENSE +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/README.md +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/__init__.py +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/patches.py +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/static/animator.css +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/static/animator.js +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/static/fscreen.css +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/static/fscreen.js +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab/utils.py +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab.egg-info/SOURCES.txt +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab.egg-info/dependency_links.txt +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab.egg-info/requires.txt +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/dashlab.egg-info/top_level.txt +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/pyproject.toml +0 -0
- {dashlab-0.2.2 → dashlab-0.3.1}/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
|
+
- Special groups support exclusion patterns with '!' suffix (e.g. '*all!debug.*', '*ctrl!btn.*' to exclude specific widgets or regex patterns from the group).
|
|
192
|
+
- Exclusion patterns can be exact names or regex patterns and are applied only to the widgets in the specified group.
|
|
193
|
+
- 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.
|
|
194
|
+
- Direct DOMWidget instances can also be passed inside list 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,113 @@ 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
|
-
def
|
|
423
|
+
def __unpack_group(self, pattern, groups):
|
|
424
|
+
group, excp = pattern.split('!',1) if '!' in pattern else (pattern, '')
|
|
425
|
+
exclude = []
|
|
426
|
+
if excp.strip(): # only try to exclude if excp is not empty or just whitespace
|
|
427
|
+
try:
|
|
428
|
+
exclude = [name for name in self.__all_widgets if re.fullmatch(excp, name)]
|
|
429
|
+
except re.error as e:
|
|
430
|
+
raise ValueError(f"Invalid exclusion regex pattern {excp!r} in {pattern!r}.\n{e}") from e
|
|
431
|
+
|
|
432
|
+
if group == '*all':
|
|
433
|
+
return [k for k in self.__all_widgets if k not in exclude]
|
|
434
|
+
elif group == '*out':
|
|
435
|
+
return [n for n in self.__groups.outputs if n not in exclude]
|
|
436
|
+
elif group == '*ctrl':
|
|
437
|
+
return [n for n in self.__groups.controls if n not in exclude]
|
|
438
|
+
elif group == '*repr':
|
|
439
|
+
return [n for n in self.__groups.others if n not in exclude]
|
|
440
|
+
else:
|
|
441
|
+
raise ValueError(f"Invalid special group name {group!r}, valid names are: {groups} followed by optional '!name|regex...' exclusion")
|
|
442
|
+
|
|
443
|
+
@_format_docs(**_docs)
|
|
444
|
+
def gather(self, *widgets: 'str | DOMWidget', verbose: bool=False) -> tuple[DOMWidget]:
|
|
395
445
|
"""Get list of widgets by names or general widgets for layout configuration.
|
|
396
446
|
This can be used to collect widgets to embed at any nesting level in layout.
|
|
397
447
|
|
|
398
|
-
**Parameters
|
|
448
|
+
**Parameters** (str | DOMWidget):
|
|
449
|
+
{gather}
|
|
450
|
+
Use `verbose=True` to print matched widgets for each pattern to ensure correct matching.
|
|
451
|
+
|
|
452
|
+
**Returns**: List of DOMWidget instances corresponding to the provided names or instances.
|
|
399
453
|
|
|
400
|
-
|
|
454
|
+
**Example**:
|
|
401
455
|
|
|
402
|
-
|
|
456
|
+
```python
|
|
457
|
+
# Basic usage
|
|
458
|
+
widgets = dash.gather('fig1', 'fig2', 'out-stats')
|
|
403
459
|
|
|
404
|
-
|
|
460
|
+
# Special groups
|
|
461
|
+
all_controls = dash.gather('*ctrl')
|
|
462
|
+
all_outputs = dash.gather('*out')
|
|
405
463
|
|
|
406
|
-
|
|
464
|
+
# Groups with exclusions
|
|
465
|
+
controls_no_buttons = dash.gather('*ctrl!btn.*')
|
|
466
|
+
all_except_debug = dash.gather('*all!.*debug.*')
|
|
407
467
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
468
|
+
# Regex patterns
|
|
469
|
+
fig_widgets = dash.gather('fig.*') # fig1, fig2, fig_debug
|
|
470
|
+
numbered = dash.gather('.*[0-9].*') # any widget with numbers at end
|
|
471
|
+
|
|
472
|
+
# Mixed patterns
|
|
473
|
+
result = dash.gather('fig1', '*ctrl!btn.*', external_widget)
|
|
474
|
+
|
|
475
|
+
# Verbose output with colors
|
|
476
|
+
widgets = dash.gather('*all!debug.*', verbose=True)
|
|
477
|
+
# Shows: [gather (group)]: *all!debug.* → fig1,fig2,x,y,out-stats
|
|
417
478
|
```
|
|
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
479
|
"""
|
|
422
|
-
|
|
480
|
+
specials = ['*all','*out','*ctrl','*repr']
|
|
481
|
+
Ws, LC = self.__all_widgets, [name.lower() for name in self.__all_widgets] # all widgets by name and lower case for case insensitive search
|
|
482
|
+
|
|
483
|
+
collected = [] # And collect included names keeping exluded out
|
|
423
484
|
for name in widgets:
|
|
424
485
|
if isinstance(name, str):
|
|
425
|
-
if name
|
|
426
|
-
|
|
427
|
-
|
|
486
|
+
if not name.strip(): # Handle empty strings and whitespace
|
|
487
|
+
raise ValueError(f"Invalid widget name {name!r} - empty strings not allowed")
|
|
488
|
+
|
|
489
|
+
if name in Ws: # catch all names without regex first and exlcuded above
|
|
490
|
+
collected.append(Ws[name])
|
|
491
|
+
if verbose:
|
|
492
|
+
print(f"\033[92m[gather (exact)]\033[0m: {name} → {name}")
|
|
493
|
+
elif name.lower() in LC: # case insensitive match
|
|
494
|
+
raise ValueError(f"Widget name {name!r} not found, did you mean {list(Ws)[LC.index(name.lower())]!r}? Widget names are case-sensitive.")
|
|
495
|
+
elif name.startswith('*'): # special groups
|
|
496
|
+
names = []
|
|
497
|
+
try:
|
|
498
|
+
names = self.__unpack_group(name, specials)
|
|
499
|
+
collected.extend([Ws[n] for n in names]) # already filtered above
|
|
500
|
+
finally:
|
|
501
|
+
if verbose:
|
|
502
|
+
print(f"\033[94m[gather (group)]\033[0m: {name} → {','.join(names) if names else 'No matches'}")
|
|
428
503
|
else:
|
|
429
|
-
|
|
504
|
+
matches = []
|
|
505
|
+
try:
|
|
506
|
+
matches = [wname for wname in Ws.keys() if re.fullmatch(name, wname)]
|
|
507
|
+
if matches:
|
|
508
|
+
collected.extend([Ws[wname] for wname in matches])
|
|
509
|
+
elif name.isidentifier(): # if simple name, raise error
|
|
510
|
+
raise ValueError(f"Invalid widget name {name!r}. Valid names: {list(Ws.keys())}, specials: {specials}")
|
|
511
|
+
except re.error as e: # only raise error if regex is invalid, not if no matches found
|
|
512
|
+
raise ValueError(
|
|
513
|
+
f"Invalid widget name {name!r}.\n"
|
|
514
|
+
f"Valid names: {list(Ws.keys())}\n"
|
|
515
|
+
f"Special groups: {specials}, optionally followed by exclusion '!name|regex...'\n"
|
|
516
|
+
f"regex patterns to match full name in params/outputs are also supported.\n{e}")
|
|
517
|
+
finally:
|
|
518
|
+
if verbose:
|
|
519
|
+
print(f"\033[93m[gather (regex)]\033[0m: {name} → {','.join(matches) if matches else 'No matches'}")
|
|
520
|
+
|
|
430
521
|
elif isinstance(name, ipw.DOMWidget):
|
|
431
522
|
collected.append(name)
|
|
523
|
+
if verbose:
|
|
524
|
+
print(f"\033[95m[gather (widget)]\033[0m: {type(name).__name__} → Direct widget")
|
|
432
525
|
else:
|
|
433
526
|
raise TypeError(f"Each item must be a string or DOMWidget, got {type(name).__name__}")
|
|
434
527
|
return tuple(collected)
|
|
435
528
|
|
|
529
|
+
@_format_docs(gather = textwrap.indent(_docs['gather'], ' '))
|
|
436
530
|
def set_layout(self,
|
|
437
531
|
header: 'list[str, DOMWidget] | DOMWidget' = None,
|
|
438
532
|
center: 'list[str, DOMWidget] | DOMWidget' = None,
|
|
@@ -452,15 +546,19 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
452
546
|
|
|
453
547
|
**Parameters**:
|
|
454
548
|
|
|
455
|
-
- Content Areas (list
|
|
549
|
+
- Content Areas (list[str, DOMWidget] | DOMWidget):
|
|
456
550
|
- header: Widgets at top
|
|
457
551
|
- center: Main content area (uses CSS Grid)
|
|
458
552
|
- left_sidebar: Left side widgets
|
|
459
553
|
- right_sidebar: Right side widgets
|
|
460
554
|
- footer: Bottom widgets
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
555
|
+
|
|
556
|
+
Each of content areas expect `list[str, DOMWidget] | DOMWidget` of widgets/ params names if given. See details below:
|
|
557
|
+
|
|
558
|
+
- If a single widget is passed, it will be used directly. If None, the area will be hidden.
|
|
559
|
+
- To get params/outputs by names at a nesting level, use `gather()` method, e.g. `center = TabsWidget(dash.gather('fig', 'out-stats'))`
|
|
560
|
+
- If a list/tuple is passed, it will be wrapped in a VBox (except center which uses GridBox).{gather}
|
|
561
|
+
|
|
464
562
|
- Grid Properties:
|
|
465
563
|
- pane_widths: list[str] - Widths for [left, center, right]
|
|
466
564
|
- pane_heights: list[str] - Heights for [header, center, footer]
|
|
@@ -476,8 +574,8 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
476
574
|
|
|
477
575
|
```python
|
|
478
576
|
dash.set_layout( # dash is an instance of DashboardBase
|
|
479
|
-
left_sidebar=
|
|
480
|
-
center=['fig', *
|
|
577
|
+
left_sidebar=['*ctrl'], # control widgets on left
|
|
578
|
+
center=['fig', '*out'], # fig and other outputs in center
|
|
481
579
|
pane_widths=['200px', '1fr', 'auto'],
|
|
482
580
|
pane_heights=['auto', '1fr', 'auto'],
|
|
483
581
|
grid_gap='1rem'
|
|
@@ -505,11 +603,11 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
505
603
|
self.__app.set_trait(key, value.add_class(key.replace('_','-'))) # for user CSS
|
|
506
604
|
elif value: # class own traits and Layout properties are linked here
|
|
507
605
|
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])
|
|
606
|
+
|
|
510
607
|
del layout_widgets # release references
|
|
511
|
-
self.__app
|
|
512
|
-
|
|
608
|
+
if names := _used_widgets(self.__app):
|
|
609
|
+
self.__other.children += tuple([v for k,v in self.__all_widgets.items() if k not in names])
|
|
610
|
+
|
|
513
611
|
# We are adding a reaonly isfullscreen trait set through button on parent class
|
|
514
612
|
fs_btn = FullscreenButton()
|
|
515
613
|
fs_btn.observe(lambda c: self.set_trait('isfullscreen',c.new), names='isfullscreen') # setting readonly property
|
|
@@ -573,11 +671,9 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
573
671
|
cent_sl = f"{main_sl} > .center"
|
|
574
672
|
_css = _build_css(('.dl-dashboard > .dl-DashApp',),_general_css)
|
|
575
673
|
|
|
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
674
|
if main:
|
|
675
|
+
if fs_css := main.pop(':fullscreen',{}) or main.pop('^:fullscreen',{}): # both valid
|
|
676
|
+
_css += ('\n' + _build_css((f".{self.__css_class}.widget-interact.dl-dashboard:fullscreen > .dl-DashApp",), fs_css))
|
|
581
677
|
_css += ("\n" + _build_css((main_sl,), main))
|
|
582
678
|
if center:
|
|
583
679
|
_css += ("\n" + _build_css((cent_sl,), center))
|
|
@@ -731,6 +827,7 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
731
827
|
callbacks.append(new_func)
|
|
732
828
|
|
|
733
829
|
if out is not None:
|
|
830
|
+
self.__mark_instance(out._kwarg, out)
|
|
734
831
|
outputs.append(out)
|
|
735
832
|
|
|
736
833
|
self.__icallbacks = tuple(callbacks) # set back
|
|
@@ -793,15 +890,10 @@ class DashboardBase(ipw.interactive, metaclass = _metaclass):
|
|
|
793
890
|
w._hinting_btn_update = update_hint # keep reference to clean up before adding next time
|
|
794
891
|
|
|
795
892
|
@property
|
|
796
|
-
def outputs(self) -> tuple:
|
|
797
|
-
return tuple([w for _, w in self.__all_widgets.items() if isinstance(w, ipw.Output)])
|
|
893
|
+
def outputs(self) -> tuple: raise DeprecationWarning("outputs property is deprecated, use gather('*out') instead.")
|
|
798
894
|
|
|
799
895
|
@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
|
|
896
|
+
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
897
|
|
|
806
898
|
def __run_updates(self, **kwargs):
|
|
807
899
|
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
|