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.
Files changed (24) hide show
  1. {dashlab-0.2.2 → dashlab-0.3.0}/PKG-INFO +1 -1
  2. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/_internal.py +2 -1
  3. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/_version.py +1 -1
  4. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/base.py +106 -42
  5. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/core.py +1 -1
  6. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/listw.css +22 -6
  7. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/listw.js +3 -3
  8. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/widgets.py +6 -8
  9. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab.egg-info/PKG-INFO +1 -1
  10. {dashlab-0.2.2 → dashlab-0.3.0}/LICENSE +0 -0
  11. {dashlab-0.2.2 → dashlab-0.3.0}/README.md +0 -0
  12. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/__init__.py +0 -0
  13. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/patches.py +0 -0
  14. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/animator.css +0 -0
  15. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/animator.js +0 -0
  16. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/fscreen.css +0 -0
  17. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/static/fscreen.js +0 -0
  18. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab/utils.py +0 -0
  19. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab.egg-info/SOURCES.txt +0 -0
  20. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab.egg-info/dependency_links.txt +0 -0
  21. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab.egg-info/requires.txt +0 -0
  22. {dashlab-0.2.2 → dashlab-0.3.0}/dashlab.egg-info/top_level.txt +0 -0
  23. {dashlab-0.2.2 → dashlab-0.3.0}/pyproject.toml +0 -0
  24. {dashlab-0.2.2 → dashlab-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dashlab
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: A Python package for dashboard and data visualization tools.
5
5
  Author-email: Abdul Saboor <asaboor.my@outlook.com>
6
6
  License-Expression: MIT
@@ -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': '1px inset var(--jp-border-color2, #8988)',
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':{
@@ -1,4 +1,4 @@
1
1
  # This is automatically updated at build time, do not edit manually.
2
2
  # Double qoites are checked here
3
3
 
4
- __version__ = "0.2.2"
4
+ __version__ = "0.3.0"
@@ -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 = ['_groups','groups','outputs','params', 'changed','isfullscreen', 'children', 'layout','comm']
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=dash.groups.controls, # controls on left
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.out
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._groups = self.__create_groups(self.__all_widgets) # create groups of widgets
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
- def gather(self, *widgets: 'str | DOMWidget') -> 'tuple[DOMWidget]':
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
- - widgets (str | DOMWidget): Widget names as strings or widget instances.
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
- collected = []
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 in self.__all_widgets:
426
- collected.append(self.__all_widgets[name])
427
- self.__app._used_names.add(name) # mark as used
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
- raise ValueError(f"Invalid widget name {name!r}. Valid names are: {list(self.__all_widgets.keys())}")
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=dash.groups.controls,
480
- center=['fig', *dash.groups.outputs],
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._used_names.clear() # reset used names, will be filled again via gather internally
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=app.groups.controls,center=app.groups.outputs)
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: 2px solid var(--accent-color, #8884);
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 tabs = model.get('tabs');
12
- el.classList.toggle('tabs', Boolean(tabs));
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:tabs', updateTabs);
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
- - `tabs`: bool, if True, display as tabs instead of list. This alongwith `ipywidgets.Stack` can be used to create tabbed views like `dashlab.widgets.TabsWidget`.
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
- tabs = traitlets.Bool(False,help="If True, display as tabs instead of list").tag(sync=True)
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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dashlab
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: A Python package for dashboard and data visualization tools.
5
5
  Author-email: Abdul Saboor <asaboor.my@outlook.com>
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes