modelflowib 2.73__py3-none-any.whl

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.
modelwidget_input.py ADDED
@@ -0,0 +1,1933 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ modelwidget_input
4
+ =================
5
+
6
+ Jupyter/Shiny-friendly input widgets used to *edit* a pandas DataFrame, run
7
+ ModelFlow-style scenarios, and interactively inspect results.
8
+
9
+ This module provides two layers:
10
+
11
+ 1) A **widget framework** (based on a runtime-enforced interface using
12
+ ``abc.ABC``), where each widget knows how to:
13
+ - expose a displayable ipywidget (``datawidget``),
14
+ - apply its current user inputs to a DataFrame (``update_df``),
15
+ - restore its initial state (``reset``).
16
+
17
+ 2) A **scenario runner + plotting UI**:
18
+ - :class:`updatewidget` builds a small “scenario lab” around a widget tree:
19
+ update data → run model → store results.
20
+ - :class:`keep_plot_widget` visualizes stored solutions from the model
21
+ (typically ``mmodel.keep_solutions``) and can optionally save figures.
22
+
23
+ Motivation
24
+ ----------
25
+ The original ``modelwidget_input.py`` grew organically and relied on implicit
26
+ “duck-typing” (certain attributes/methods were assumed to exist). This revision
27
+ makes that contract explicit by introducing an ABC-based interface that is
28
+ validated at runtime.
29
+
30
+ Core interfaces
31
+ --------------
32
+ **Leaf widgets** implement :class:`WidgetABC`:
33
+
34
+ - ``datawidget``:
35
+ The displayable ipywidgets object (or a container of ipywidgets).
36
+ - ``update_df(df, current_per)``:
37
+ Apply the widget’s user input to ``df``.
38
+ - ``reset(g)``:
39
+ Restore widget state to the initial values (``g`` is the callback payload
40
+ from ipywidgets and can be ignored by most widgets).
41
+
42
+ **Container widgets** implement :class:`ContainerWidgetABC`:
43
+
44
+ - ``datachildren``:
45
+ A list of child widgets.
46
+ - ``update_df`` and ``reset``:
47
+ Default implementations forward to children.
48
+
49
+ Widget definitions and the factory
50
+ ---------------------------------
51
+ Widgets are defined using a **recursive list-based definition format**:
52
+
53
+ ['widgettype', {widget_dictionary}]
54
+
55
+ A widget dictionary always contains configuration parameters and usually a
56
+ ``'content'`` field. Widgets can be nested to form complex layouts.
57
+
58
+ Widgets are instantiated with the factory:
59
+
60
+ w = make_widget(widgetdef)
61
+
62
+ The factory validates that the created object satisfies :class:`WidgetABC`
63
+ (so errors appear early and clearly).
64
+
65
+ Assumptions about data / periods
66
+ --------------------------------
67
+ - DataFrames are indexed by time/period and variables are columns.
68
+ - ``current_per`` is typically a ModelFlow “current period” selection; most
69
+ widgets use ``df.loc[current_per, var]``.
70
+ - Some slider operators treat ``current_per[0]`` as the first period (impulse).
71
+
72
+ Provided widget types
73
+ ---------------------
74
+ Containers
75
+ - ``'base'`` → :class:`basewidget`
76
+ Vertical stack of widgets (VBox).
77
+ - ``'tab'`` → :class:`tabwidget`
78
+ Tabbed or accordion layout.
79
+
80
+ Leaf widgets
81
+ - ``'sheet'`` → :class:`sheetwidget`
82
+ Spreadsheet-like editor (requires ``ipydatagrid``).
83
+ - ``'slide'`` → :class:`slidewidget`
84
+ One or more sliders that update variables using an operator (add/set/percent,
85
+ impulse variants, etc.).
86
+ - ``'sumslide'`` → :class:`sumslidewidget`
87
+ Like ``slide`` but enforces that all slider values sum to ``maxsum``.
88
+ - ``'radio'`` → :class:`radiowidget`
89
+ One-of-N selection that writes 0/1 indicator variables.
90
+ - ``'check'`` → :class:`checkwidget`
91
+ On/off selection that writes 0/1 values.
92
+
93
+ Scenario runner and plotting
94
+ ----------------------------
95
+ :class:`updatewidget`
96
+ A small control panel that ties a widget tree to a ModelFlow-like model:
97
+
98
+ - Builds an “experiment” DataFrame from a baseline.
99
+ - Calls ``datawidget.update_df(experiment_df, mmodel.current_per)``.
100
+ - Runs the model and stores results (typically in ``mmodel.keep_solutions``).
101
+ - Presents a :class:`keep_plot_widget` to inspect the stored solutions.
102
+
103
+ :class:`keep_plot_widget`
104
+ An interactive viewer for solutions in ``mmodel.keep_solutions``. Lets the
105
+ user select scenarios and variables and produces matplotlib figures. It can
106
+ optionally expose a save dialog (:class:`savefigs_widget`).
107
+
108
+ Defining and using widgets
109
+ --------------------------
110
+
111
+ 1) Slider widget (type ``'slide'``)
112
+ Example::
113
+
114
+ slidedef = ['slide', {
115
+ 'heading': 'Macroeconomic Shocks',
116
+ 'content': {
117
+ 'Productivity': {
118
+ 'var': 'ALFA',
119
+ 'value': 0.5,
120
+ 'min': 0.0,
121
+ 'max': 1.0,
122
+ 'step': 0.01,
123
+ 'op': '+'
124
+ },
125
+ 'Labor growth': {
126
+ 'var': 'LABOR_GROWTH',
127
+ 'value': 0.01,
128
+ 'min': 0.0,
129
+ 'max': 1.0,
130
+ 'op': '='
131
+ }
132
+ }
133
+ }]
134
+
135
+ 2) Sum-constrained sliders (type ``'sumslide'``)
136
+ Example::
137
+
138
+ sharedef = {
139
+ '1Y bond': {'var': 'BOND_1Y', 'value': 20, 'min': 0, 'max': 100, 'step': 5},
140
+ '5Y bond': {'var': 'BOND_5Y', 'value': 30, 'min': 0, 'max': 100, 'step': 5},
141
+ '10Y bond': {'var': 'BOND_10Y', 'value': 50, 'min': 0, 'max': 100, 'step': 5},
142
+ }
143
+
144
+ wsharedef = ['sumslide', {
145
+ 'heading': 'Issuance Policy',
146
+ 'content': sharedef,
147
+ 'maxsum': 100.0
148
+ }]
149
+
150
+ 3) Base container (type ``'base'``)
151
+ Example::
152
+
153
+ basedef = ['base', {'content': [slidedef, wsharedef]}]
154
+
155
+ 4) Tab / accordion container (type ``'tab'``)
156
+ Example::
157
+
158
+ portdef = ['tab', {
159
+ 'content': [
160
+ ['Markets', basedef],
161
+ ['Issuance', wsharedef],
162
+ ],
163
+ 'tab': True # True = tabs, False = accordion
164
+ }]
165
+
166
+ Creating and displaying
167
+ Example::
168
+
169
+ wport = make_widget(portdef)
170
+ display(wport.datawidget)
171
+
172
+ Typical workflow with a model
173
+ -----------------------------
174
+ Example::
175
+
176
+ wport = make_widget(portdef)
177
+ ui = updatewidget(mmodel, wport, varpat="*")
178
+ ui # (or display(ui))
179
+
180
+ Structural rules (quick reference)
181
+ ----------------------------------
182
+ - Every widget definition must be::
183
+
184
+ ['widgettype', {widgetdict}]
185
+
186
+ - ``'tab'`` widgets use::
187
+
188
+ ['tab', {'content': [[title, widgetdef], ...], 'tab': True/False}]
189
+
190
+ - ``'base'`` widgets use::
191
+
192
+ ['base', {'content': [widgetdef, widgetdef, ...]}]
193
+
194
+ - ``'sumslide'`` widgets require::
195
+
196
+ ['sumslide', {'content': dict, 'maxsum': float, ...}]
197
+
198
+ If these structures are violated, widget creation will fail.
199
+ """
200
+
201
+
202
+ from __future__ import annotations
203
+
204
+ from abc import ABC, abstractmethod
205
+ from copy import copy
206
+ from dataclasses import dataclass, field
207
+ from typing import Any, Callable, Dict, List, Tuple
208
+
209
+ import pandas as pd
210
+
211
+ from IPython.display import display
212
+
213
+ from ipywidgets import (
214
+ Accordion,
215
+ Button,
216
+ Checkbox,
217
+ FloatSlider,
218
+ HBox,
219
+ HTML,
220
+ Label,
221
+ Layout,
222
+ RadioButtons,
223
+ Select,
224
+ SelectMultiple,
225
+ SelectionRangeSlider,
226
+ Tab,
227
+ Text,
228
+ VBox,
229
+ Box,Output
230
+ )
231
+
232
+ from ipydatagrid import DataGrid
233
+ from ipydatagrid import TextRenderer
234
+
235
+ import matplotlib.pyplot as plt
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # Optional dependency: ipydatagrid
239
+ # ---------------------------------------------------------------------------
240
+
241
+
242
+ from modelhelp import debug_var
243
+
244
+ WIDGET_REGISTRY = {}
245
+
246
+ def register_widget(cls):
247
+ WIDGET_REGISTRY[cls.__name__] = cls
248
+
249
+
250
+ return cls
251
+
252
+
253
+ # safe helper near the top of the file
254
+ def _detect_render_mode() -> str:
255
+ try:
256
+ from IPython import get_ipython
257
+ ip = get_ipython()
258
+ if ip is not None and 'google.colab' in str(ip):
259
+ return 'colab'
260
+ except Exception:
261
+ pass
262
+ return 'classic'
263
+ # ---------------------------------------------------------------------------
264
+ # ABC interfaces
265
+ # ---------------------------------------------------------------------------
266
+
267
+ class WidgetABC(ABC):
268
+ """Runtime-enforced interface for all widgets in this module."""
269
+
270
+ @property
271
+ @abstractmethod
272
+ def datawidget(self) -> Any:
273
+ """Return the displayable ipywidget (or widget container)."""
274
+ raise NotImplementedError
275
+
276
+ @abstractmethod
277
+ def update_df(self, df: pd.DataFrame, current_per: Any) -> None:
278
+ """Apply the widget's current values to ``df``."""
279
+ raise NotImplementedError
280
+
281
+ @abstractmethod
282
+ def reset(self, g: Any) -> None:
283
+ """Reset widget state back to its initial value(s)."""
284
+ raise NotImplementedError
285
+
286
+
287
+ class ContainerWidgetABC(WidgetABC):
288
+ """A widget that contains basic widgets as children """
289
+
290
+ @property
291
+ @abstractmethod
292
+ def datachildren(self) -> List[WidgetABC]:
293
+ """Return child widgets in display order."""
294
+ raise NotImplementedError
295
+
296
+ def update_df(self, df: pd.DataFrame, current_per: Any=None) -> None:
297
+ """Forward updates to all children."""
298
+ for child in self.datachildren:
299
+ child.update_df(df, current_per)
300
+
301
+ def reset(self, g: Any) -> None:
302
+ """Forward reset to all children."""
303
+ for child in self.datachildren:
304
+ child.reset(g)
305
+
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # Helper bases
309
+ # ---------------------------------------------------------------------------
310
+
311
+
312
+
313
+ @dataclass
314
+ class SingleWidgetBase(WidgetABC):
315
+ """
316
+ Common parsing / fields for widget classes.
317
+
318
+ Parameters
319
+ ----------
320
+ widgetdef:
321
+ Dictionary describing a widget. At minimum it must contain ``'content'``.
322
+ Many widgets also accept ``'heading'``.
323
+ """
324
+
325
+ widgetdef: Dict[str, Any]
326
+ content: Any = field(init=False)
327
+ heading: str = field(init=False)
328
+
329
+ def __post_init__(self) -> None:
330
+ self.content = self.widgetdef["content"]
331
+ self.heading = self.widgetdef.get("heading", "Heading")
332
+
333
+ @property
334
+ def show(self):
335
+ display(self.datawidget)
336
+
337
+ def _ipython_display_(self):
338
+ """Auto-display in Jupyter when this object is the last expression in a cell."""
339
+ display(self.datawidget)
340
+
341
+
342
+
343
+ @dataclass
344
+ class ContainerWidgetBase(ContainerWidgetABC):
345
+ """
346
+ Base for container which contains widgets
347
+
348
+ Subclasses should build:
349
+ - ``self._children``: list of :class:`WidgetABC`
350
+ - ``self._datawidget``: displayable widget container (VBox/Tab/Accordion/...)
351
+ """
352
+
353
+ widgetdef: Dict[str, Any]
354
+ content: Any = field(init=False)
355
+ heading: str = field(init=False)
356
+ _children: List[WidgetABC] = field(init=False, default_factory=list)
357
+ _datawidget: Any = field(init=False, default=None)
358
+
359
+ def __post_init__(self) -> None:
360
+ self.content = self.widgetdef["content"]
361
+ self.heading = self.widgetdef.get("heading", "Heading")
362
+
363
+ @property
364
+ def datachildren(self) -> List[WidgetABC]:
365
+ return self._children
366
+
367
+ @property
368
+ def datawidget(self) -> Any:
369
+ return self._datawidget
370
+
371
+ @property
372
+ def show(self):
373
+ display(self.datawidget)
374
+
375
+ def _ipython_display_(self):
376
+ display(self.datawidget)
377
+
378
+
379
+
380
+ # ---------------------------------------------------------------------------
381
+ # Factory
382
+ # ---------------------------------------------------------------------------
383
+
384
+
385
+ def make_widget(widgetdef_or_type, widgetdict=None, render_mode=None):
386
+ """
387
+ Create and return a widget instance from a widget type and definition.
388
+
389
+ The function resolves the widget class name from the requested widget type
390
+ and rendering mode, looks it up in ``WIDGET_REGISTRY``, instantiates it with
391
+ ``widgetdict``, and verifies that the created object inherits from
392
+ ``WidgetABC``.
393
+
394
+ Parameters
395
+ ----------
396
+ widgetdef_or_type : str or tuple[str, dict]
397
+ Either the widget type as a string, or a ``(widgettype, widgetdict)``
398
+ pair. If ``widgetdict`` is not provided, this argument must be the pair.
399
+ widgetdict : dict, optional
400
+ Dictionary with the widget configuration passed to the widget class
401
+ constructor. If omitted, ``widgetdef_or_type`` is expected to contain
402
+ both the widget type and the widget definition.
403
+ render_mode : {"classic", "colab"}, optional
404
+ Rendering mode used to select the widget class name. If omitted, the
405
+ mode is detected automatically: ``"colab"`` when running in Google
406
+ Colab, otherwise ``"classic"``.
407
+
408
+ Returns
409
+ -------
410
+ WidgetABC
411
+ An instantiated widget object.
412
+
413
+ Raises
414
+ ------
415
+ KeyError
416
+ If the resolved widget class name is not found in ``WIDGET_REGISTRY``.
417
+ TypeError
418
+ If the instantiated object does not inherit from ``WidgetABC``.
419
+
420
+ Notes
421
+ -----
422
+ For ``render_mode="classic"``, the class name is resolved as
423
+ ``f"{widgettype}widget"``.
424
+
425
+ For other render modes, the class name is usually also
426
+ ``f"{widgettype}widget"``, except for ``widgettype == "tab"``, where the
427
+ Colab-specific class name ``f"colab{widgettype}widget"`` is used.
428
+
429
+ Examples
430
+ --------
431
+ Create a widget from a type and config::
432
+
433
+ w = make_widget("sheet", {"title": "Overview"})
434
+
435
+ Create a widget from a ``(type, config)`` tuple::
436
+
437
+ w = make_widget(("sheet", {"title": "Overview"}))
438
+
439
+ Force Colab rendering::
440
+
441
+ w = make_widget("tab", {"tabs": [...]}, render_mode="colab")
442
+ """
443
+ if render_mode is None:
444
+ render_mode = _detect_render_mode()
445
+
446
+ if widgetdict is None:
447
+ widgettype, widgetdict = widgetdef_or_type
448
+ else:
449
+ widgettype = widgetdef_or_type
450
+
451
+ if render_mode == 'classic':
452
+ clsname = f"{widgettype}widget"
453
+ else:
454
+ clsname = f"colab{widgettype}widget" if widgettype == "tab" else f"{widgettype}widget"
455
+
456
+ try:
457
+ cls = WIDGET_REGISTRY[clsname]
458
+ except KeyError as e:
459
+ print(f"Unknown widget class: {clsname[:-6]}. These are alloved:"+
460
+ '\n' + '\n'.join(v[:-6] for v in WIDGET_REGISTRY.keys() ))
461
+
462
+ raise KeyError(f"Unknown widget class: {clsname[:-6]}") from e
463
+
464
+ obj = cls(widgetdict)
465
+
466
+ if not isinstance(obj, WidgetABC):
467
+ raise TypeError(
468
+ f"{clsname} must inherit WidgetABC/ContainerWidgetABC (got {type(obj)!r})."
469
+ )
470
+
471
+ return obj
472
+ # ---------------------------------------------------------------------------
473
+ # Container widgets
474
+ # ---------------------------------------------------------------------------
475
+
476
+ @register_widget
477
+ @dataclass
478
+ class basewidget(ContainerWidgetBase):
479
+ """
480
+ Vertical container (VBox) for a list of
481
+
482
+ ``content`` must be an iterable of pairs:
483
+ ``[(widgettype, widgetdef), ...]``
484
+ """
485
+
486
+ def __post_init__(self) -> None:
487
+ super().__post_init__()
488
+ self._children = [make_widget(widgettype, widgetdict) for widgettype, widgetdict in self.content]
489
+ self._datawidget = VBox([child.datawidget for child in self._children])
490
+
491
+ @register_widget
492
+ @dataclass
493
+ class tabwidget(ContainerWidgetBase):
494
+ """
495
+ Tab/Accordion container.
496
+
497
+ ``content`` must be an iterable like:
498
+ ``[(tab_title, (widgettype, widgetdef)), ...]``
499
+
500
+ Widget options are read from ``widgetdef``:
501
+ - ``selected_index`` (default '0')
502
+ - ``tab`` (True => Tab, False => Accordion)
503
+ """
504
+
505
+ selected_index: Any = "0"
506
+ tab: bool = True
507
+
508
+ def __post_init__(self) -> None:
509
+ super().__post_init__()
510
+
511
+ self.selected_index = int(self.widgetdef.get("selected_index", "0"))
512
+ self.tab = self.widgetdef.get("tab", True)
513
+
514
+ tab_titles = [tab_title for tab_title, _ in self.content]
515
+
516
+ ctor = Tab if self.tab else Accordion
517
+ self._children = [make_widget(subtype, subdef) for _, (subtype, subdef) in self.content]
518
+
519
+ self._datawidget = ctor(
520
+ [child.datawidget for child in self._children],
521
+ selected_index=self.selected_index,
522
+ layout={"height": "max-content"},
523
+ )
524
+ for i, title in enumerate(tab_titles):
525
+ self._datawidget.set_title(i, title)
526
+
527
+ @register_widget
528
+ @dataclass
529
+ class colabtabwidget(ContainerWidgetBase):
530
+ """
531
+ Colab-safe replacement for Tab/Accordion.
532
+ Uses a selector to switch the visible child widget.
533
+ """
534
+
535
+ selected_index: int = 0
536
+ tab: bool = True # kept only for compatibility
537
+
538
+ def __post_init__(self):
539
+ super().__post_init__()
540
+
541
+ self.selected_index = int(self.widgetdef.get("selected_index", 0))
542
+ entries = list(self.content)
543
+
544
+ titles = [str(title) for title, _ in entries]
545
+ self._children = [
546
+ make_widget(subtype, subdef)
547
+ for _, (subtype, subdef) in entries
548
+ ]
549
+
550
+ self._selector = Select(
551
+ options=titles,
552
+ value=titles[self.selected_index] if titles else None,
553
+ description="Section:",
554
+ layout=Layout(width="250px"),
555
+ style={"description_width": "initial"},
556
+ )
557
+
558
+ self._panel = VBox(
559
+ [self._children[self.selected_index].datawidget] if self._children else [],
560
+ layout=Layout(width="100%")
561
+ )
562
+
563
+ def _change(change):
564
+ if change["name"] == "value" and change["new"] in titles:
565
+ idx = titles.index(change["new"])
566
+ self._panel.children = (self._children[idx].datawidget,)
567
+
568
+ self._selector.observe(_change, names="value")
569
+
570
+ self._datawidget = VBox(
571
+ [self._selector, self._panel],
572
+ layout=Layout(width="100%", border="1px solid #999", padding="8px")
573
+ )
574
+
575
+
576
+ # ---------------------------------------------------------------------------
577
+ # Leaf widgets
578
+ # ---------------------------------------------------------------------------
579
+ def auto_row_header_width(df: pd.DataFrame, *, min_px=120, max_px=800, px_per_char=9, padding_px=24):
580
+ # longest index label (as shown in the row header)
581
+ labels = [str(x) for x in df.index]
582
+ max_chars = max((len(s) for s in labels), default=0)
583
+ est = max_chars * px_per_char + padding_px
584
+ return max(min_px, min(max_px, est))
585
+
586
+
587
+ def df_to_grid(df,dec):
588
+ max_value = df.abs().max().max()
589
+ fmt = f",.{dec}f"
590
+ max_len = len(f"{max_value:{fmt}}")
591
+ row_header_width = auto_row_header_width(df)
592
+ column_widths = {col: max(len(str(col)) + 4, max_len) * 9 for col in df.columns}| { "Year": row_header_width}
593
+
594
+ renderers = {col: TextRenderer(format=fmt, horizontal_alignment="right") for col in df.columns}
595
+ renderers["Year"] = TextRenderer(horizontal_alignment="left")
596
+
597
+ # debug_var(self.org_df_var,self.org_df_var.index,row_header_width)
598
+ wsheet = DataGrid(
599
+ df,
600
+ column_widths=column_widths,
601
+ row_header_width=row_header_width,
602
+ enable_filters=False,
603
+ enable_sort=False,
604
+ editable=True,
605
+ index_name="Year",
606
+ renderers=renderers,
607
+ )
608
+
609
+ # constrain the grid itself
610
+ wsheet.layout = Layout(
611
+ height="360px", # <- key: forces internal vertical scroll
612
+ width="100%",
613
+ min_width="900px" # <- key: enables horizontal scroll when output area is narrower
614
+ )
615
+ return wsheet
616
+
617
+
618
+ @register_widget
619
+ @dataclass
620
+ class sheetwidget(SingleWidgetBase):
621
+ """
622
+ Editable DataFrame grid widget (requires ``ipydatagrid``).
623
+
624
+ Widget content:
625
+ - ``df``: DataFrame to display/edit (required)
626
+ - ``dec``: decimals to display (default=2)
627
+
628
+ Widget options (on widgetdef):
629
+ - ``transpose``: show variables as rows (default False)
630
+
631
+ Behavior
632
+ --------
633
+ ``update_df`` adds the edited grid values to the corresponding cells in ``df``.
634
+ """
635
+
636
+ df_var: pd.DataFrame = field(init=False) # input update dataframe before transpose and rename
637
+ org_df_var: pd.DataFrame = field(init=False) # input datadateframe as displayed by sheet, so renamed and transposed
638
+ org_values: pd.DataFrame = field(init=False) # copy of abowe for reset of update
639
+ # df_to_update: pd.DataFrame = field(init=False) # A dataframe to update
640
+
641
+ trans: Callable[[str], str] = field(default=lambda x: x)
642
+ transpose: bool = field(default=False)
643
+ wexp: Label = field(init=False)
644
+ wsheet: Any = field(init=False)
645
+ _datawidget: Any = field(init=False)
646
+ dec: int = field(init=False, default=2)
647
+
648
+ def __post_init__(self) -> None:
649
+ super().__post_init__()
650
+ self.op = self.content.get('operator','+')
651
+
652
+ update_col = self.content.get("update_col", None)
653
+ update_index = self.content.get("update_index", None)
654
+ update_df = self.content.get("update_df", None)
655
+
656
+ if update_col is not None and update_index is not None:
657
+ # normalize to Index objects (handles list/tuple/Index)
658
+ cols = pd.Index(update_col) if not isinstance(update_col, pd.Index) else update_col
659
+ idx = pd.Index(update_index) if not isinstance(update_index, pd.Index) else update_index
660
+
661
+ self.df_var = pd.DataFrame(0, index=idx, columns=cols)
662
+
663
+ elif isinstance(update_df, pd.DataFrame):
664
+ self.df_var = update_df
665
+
666
+ elif update_df is not None and not isinstance(update_df, pd.DataFrame):
667
+ raise TypeError("'update_df' must be a pandas DataFrame")
668
+ else:
669
+ raise ValueError(
670
+ "Provide either both 'update_col' and 'update_index', or 'update_df'."
671
+ )
672
+
673
+ self.dec = int(self.content.get("dec", 2))
674
+ self.transpose = bool(self.widgetdef.get("transpose", True))
675
+ self.trans = self.widgetdef.get("trans", {})
676
+ if not self.transpose:
677
+ raise NotImplementedError("Non-transposed sheetwidget is not implemented yet.")
678
+ self.wexp = Label(value=self.heading, layout={"width": "54%"})
679
+
680
+ newnamedf = self.df_var.copy().rename(columns=self.trans)
681
+ self.org_df_var = newnamedf.T if self.transpose else newnamedf
682
+
683
+
684
+
685
+ self.wsheet = df_to_grid(self.org_df_var, self.dec)
686
+ # self.wsheet2 = df_to_grid(self.org_df_var-1.0, self.dec)
687
+
688
+ # wtab = Tab([self.wsheet,self.wsheet2])
689
+
690
+ grid_container = Box(
691
+ [self.wsheet],
692
+ layout=Layout(
693
+ width="100%",
694
+ height="360px",
695
+ overflow="scroll",
696
+ border="1px solid #ddd"
697
+ )
698
+ )
699
+
700
+ self._datawidget = VBox([self.wexp, self.wsheet])
701
+
702
+
703
+ self.org_values = self.org_df_var.copy()
704
+
705
+ @property
706
+ def datawidget(self) -> Any:
707
+ return self._datawidget
708
+
709
+ def update_df(self, df: pd.DataFrame, current_per: Any = None) -> None:
710
+ updated_df = pd.DataFrame(self.wsheet.data)
711
+ if self.transpose:
712
+ updated_df = updated_df.T
713
+
714
+ updated_df.columns = self.df_var.columns
715
+ updated_df.index = self.df_var.index
716
+ df_copy = df.loc[updated_df.index, updated_df.columns].copy()
717
+ # debug_var(updated_df,df_copy)
718
+ match self.op:
719
+ case "+":
720
+ df.loc[updated_df.index, updated_df.columns] =df_copy + updated_df
721
+
722
+ case "=":
723
+ df.loc[updated_df.index, updated_df.columns] = updated_df
724
+
725
+ case "*":
726
+ df.loc[updated_df.index, updated_df.columns] = df_copy * updated_df
727
+
728
+ case "%":
729
+ df.loc[updated_df.index, updated_df.columns] = df_copy * (1+updated_df/100)
730
+
731
+
732
+ case _:
733
+ raise ValueError(
734
+ f"Unsupported operator {self.op!r} in sheetwidget mapping for {self.heading!r}."
735
+ )
736
+
737
+
738
+
739
+ def reset(self, g: Any) -> None:
740
+ self.wsheet.data = self.org_values
741
+
742
+
743
+
744
+ @register_widget
745
+ @dataclass
746
+ class slidewidget(SingleWidgetBase):
747
+ """
748
+ A set of FloatSliders that map to one or more variables in a DataFrame.
749
+
750
+ Each line in ``content`` defines a slider and its mapping:
751
+
752
+ Example content::
753
+
754
+ {
755
+ "Shock to X": {"min": -1, "max": 1, "value": 0.0, "var": "X", "op": "+", "dec": 2},
756
+ "Set Y": {"min": 0, "max": 10, "value": 1.0, "var": "Y Z", "op": "=", "step": 0.5},
757
+ }
758
+
759
+ Supported operators (``op``):
760
+ - ``"+"``: add value to df.loc[current_per, var]
761
+ - ``"+impulse"``: add only to current_per[0]
762
+ - ``"="``: set df.loc[current_per, var] = value
763
+ - ``"=impulse"``: set only current_per[0]
764
+ - ``"=start-"``: set all periods *before* current_per[0] to value
765
+ - ``"%"``: multiply by (1 - value/100)
766
+
767
+ Notes
768
+ -----
769
+ - ``var`` can be a string with space-separated variable names.
770
+ """
771
+
772
+ altname: str = field(default="Alternative")
773
+ basename: str = field(default="Baseline")
774
+
775
+ wset: List[FloatSlider] = field(init=False)
776
+ wslide: List[HBox] = field(init=False)
777
+ current_values: Dict[str, Dict[str, Any]] = field(init=False)
778
+ _datawidget: VBox = field(init=False)
779
+
780
+ def __post_init__(self) -> None:
781
+ super().__post_init__()
782
+
783
+ self.altname = self.widgetdef.get("altname", "Alternative")
784
+ self.basename = self.widgetdef.get("basename", "Baseline")
785
+
786
+ wexp = Label(value=self.heading, layout={"width": "54%"})
787
+ walt = Label(value=self.altname, layout={"width": "8%", "border": "hide"})
788
+ wbas = Label(value=self.basename, layout={"width": "10%", "border": "hide"})
789
+ whead = HBox([wexp, walt, wbas])
790
+
791
+ self.wset = [
792
+ FloatSlider(
793
+ description=des,
794
+ min=cont["min"],
795
+ max=cont["max"],
796
+ value=cont["value"],
797
+ step=cont.get("step", 0.01),
798
+ layout={"width": "60%"},
799
+ style={"description_width": "40%"},
800
+ readout_format=f":>,.{cont.get('dec', 2)}f",
801
+ continuous_update=False,
802
+ )
803
+ for des, cont in self.content.items()
804
+ ]
805
+
806
+ for w in self.wset:
807
+ w.observe(self._on_slider_change, names="value", type="change")
808
+
809
+ waltval = [
810
+ Label(
811
+ value=f"{cont['value']:>,.{cont.get('dec', 2)}f}",
812
+ layout=Layout(display="flex", justify_content="center", width="10%", border="hide"),
813
+ )
814
+ for _, cont in self.content.items()
815
+ ]
816
+
817
+ self.wslide = [HBox([s, v]) for s, v in zip(self.wset, waltval)]
818
+ self._datawidget = VBox([whead] + self.wslide)
819
+
820
+ # Normalize current_values (notably split var strings)
821
+ self.current_values = {
822
+ # des: {k: (v.split() if k == "var" else v) for k, v in cont.items() if k in {"value", "var", "op"}}
823
+ des: {k: (v.split() if k == "var" else v) for k, v in cont.items()}
824
+ for des, cont in self.widgetdef["content"].items()
825
+ }
826
+
827
+ @property
828
+ def datawidget(self) -> Any:
829
+ return self._datawidget
830
+
831
+ def reset(self, g: Any) -> None:
832
+ for i, (_, cont) in enumerate(self.content.items()):
833
+ self.wset[i].value = cont["value"]
834
+
835
+ def update_df(self, df: pd.DataFrame, current_per: Any = None) -> None:
836
+ # If not provided, operate on the full index
837
+ if current_per is None:
838
+ current_per = df.index
839
+
840
+ for _, cont in self.current_values.items():
841
+ op = cont.get("op", "=")
842
+ value = cont["value"]
843
+ # debug_var(value,current_per)
844
+ for var in cont["var"]:
845
+ if op == "+":
846
+ df.loc[current_per, var] = df.loc[current_per, var] + value
847
+ elif op == "+impulse":
848
+ df.loc[current_per[0], var] = df.loc[current_per[0], var] + value
849
+ elif op == "=start-":
850
+ startindex = df.index.get_loc(current_per[0])
851
+ varloc = df.columns.get_loc(var)
852
+ df.iloc[:startindex, varloc] = value
853
+ elif op == "=":
854
+ df.loc[current_per, var] = value
855
+ elif op == "=impulse":
856
+ df.loc[current_per[0], var] = value
857
+ elif op == "%":
858
+ df.loc[current_per, var] = df.loc[current_per, var] * (1 + value / 100)
859
+ elif op == "%growth":
860
+ startindex = df.index.get_loc(current_per[0])
861
+ varloc = df.columns.get_loc(var)
862
+ start_value = cont.get('start_value','')
863
+
864
+ if startindex < 1:
865
+ if start_value :
866
+ df.iloc[startindex,varloc]= float(start_value)
867
+ else:
868
+ raise ValueError('For %growth we need a start value=')
869
+ else:
870
+
871
+ df.iloc[startindex,varloc]= df.iloc[startindex-1,varloc] * (1 + value / 100)
872
+
873
+ for i,per in enumerate(current_per) :
874
+ if i == 0:
875
+ continue
876
+ df.iloc[i+startindex,varloc]= df.iloc[i+startindex-1,varloc] * (1 + value / 100)
877
+ else:
878
+ raise ValueError(f"Unsupported operator {op!r} in slider mapping for {var!r}.")
879
+
880
+ def _on_slider_change(self, g: dict) -> None:
881
+ """Update internal mapping when a slider changes."""
882
+ line_des = g["owner"].description
883
+ self.current_values[line_des]["value"] = g["new"]
884
+
885
+ @register_widget
886
+ @dataclass
887
+ class sumslidewidget(SingleWidgetBase):
888
+ """
889
+ Like :class:`slidewidget`, but enforces that the *sum* of all slider values
890
+ does not exceed ``maxsum``.
891
+
892
+ The last slider is adjusted automatically when the sum would exceed ``maxsum``.
893
+ """
894
+
895
+ altname: str = field(default="Alternative")
896
+ basename: str = field(default="Baseline")
897
+ maxsum: float = field(default=1.0)
898
+
899
+ lastdes: str = field(init=False)
900
+ wset: List[FloatSlider] = field(init=False)
901
+ wslide: List[HBox] = field(init=False)
902
+ current_values: Dict[str, Dict[str, Any]] = field(init=False)
903
+ _datawidget: VBox = field(init=False)
904
+
905
+ _in_programmatic_update: bool = field(default=False, init=False)
906
+
907
+
908
+ def __post_init__(self) -> None:
909
+ super().__post_init__()
910
+
911
+ self.altname = self.widgetdef.get("altname", "Alternative")
912
+ self.basename = self.widgetdef.get("basename", "Baseline")
913
+ self.maxsum = float(self.widgetdef.get("maxsum", 1.0))
914
+
915
+ self.lastdes = list(self.content.keys())[-1]
916
+
917
+ # fixed columns (won't disappear)
918
+ val_layout = Layout(
919
+ width="90px", min_width="90px",
920
+ flex="0 0 auto",
921
+ display="flex", justify_content="center", align_items="center",
922
+ )
923
+ slack_layout = Layout(
924
+ width="70px", min_width="70px",
925
+ flex="0 0 auto",
926
+ display="flex", justify_content="center", align_items="center",
927
+ )
928
+
929
+ # slider column (takes the remaining space)
930
+ slider_layout = Layout(
931
+ width="auto",
932
+ flex="1 1 auto", # <-- this one flexes
933
+ min_width="260px", # prevent it collapsing too far
934
+ )
935
+
936
+ # header uses same locked layouts
937
+ wexp = Label(self.heading, layout=Layout(flex="1 1 auto")) # take remaining space
938
+ wbas_h = Label(self.basename, layout=val_layout)
939
+ wsl_h = Label("Slack var", layout=slack_layout)
940
+ whead = HBox([wexp, wbas_h, wsl_h], layout=Layout(width="100%", align_items="center"))
941
+
942
+ # sliders keep description INSIDE
943
+ self.wset = [
944
+ FloatSlider(
945
+ description=des,
946
+ min=cont["min"], max=cont["max"], value=cont["value"],
947
+ step=cont.get("step", 0.01),
948
+ layout=slider_layout,
949
+ style={"description_width": "40%"}, # you can shrink this to 30% if tight
950
+ readout_format=f":>,.{cont.get('dec', 2)}f",
951
+ continuous_update=False,
952
+ )
953
+ for des, cont in self.content.items()
954
+ ]
955
+
956
+ for w in self.wset:
957
+ w.observe(self._on_slider_change, names="value", type="change")
958
+
959
+ wbasval = [
960
+ Label(f"{cont['value']:>,.{cont.get('dec', 2)}f}", layout=val_layout)
961
+ for _, cont in self.content.items()
962
+ ]
963
+
964
+ slackvar = [ '' != cont.get('slack','') for _, cont in self.content.items()]
965
+ if not any(slackvar):
966
+ slackvar[-1] = True
967
+
968
+ self.wslackval = [
969
+ Checkbox(value=slack, indent=False, layout=slack_layout)
970
+ for slack in slackvar
971
+ ]
972
+
973
+ row_layout = Layout(width="100%", align_items="center")
974
+ self.wslide = [
975
+ HBox([s, v, sla], layout=row_layout)
976
+ for s, v, sla in zip(self.wset, wbasval, self.wslackval)
977
+ ]
978
+
979
+ self._datawidget = VBox([whead] + self.wslide, layout=Layout(width="100%"))
980
+ self.current_values = {
981
+ des: {k: (v.split() if k == "var" else v) for k, v in cont.items()
982
+ if k in {"value", "var", "op", "min", "max"}}
983
+ for des, cont in self.content.items()
984
+ }
985
+
986
+ @property
987
+ def datawidget(self) -> Any:
988
+ return self._datawidget
989
+
990
+ def reset(self, g: Any) -> None:
991
+ self._in_programmatic_update = True
992
+ try:
993
+ for i, (_, cont) in enumerate(self.content.items()):
994
+ self.wset[i].value = cont["value"]
995
+ finally:
996
+ self._in_programmatic_update = False
997
+
998
+
999
+ def update_df(self, df: pd.DataFrame, current_per: Any = None) -> None:
1000
+ # If not provided, operate on the full index
1001
+ if current_per is None:
1002
+ current_per = df.index
1003
+
1004
+
1005
+ for i,cont in enumerate(self.current_values.values()):
1006
+ op = cont.get("op", "=")
1007
+ value = self.wset[i].value
1008
+
1009
+
1010
+ for var in cont["var"]:
1011
+ match op:
1012
+ case "+":
1013
+ df.loc[current_per, var] = df.loc[current_per, var] + value
1014
+
1015
+ case "+impulse":
1016
+ df.loc[current_per[0], var] = df.loc[current_per[0], var] + value
1017
+
1018
+ case "=":
1019
+ df.loc[current_per, var] = value
1020
+
1021
+ case "=impulse":
1022
+ df.loc[current_per[0], var] = value
1023
+
1024
+ case _:
1025
+ raise ValueError(
1026
+ f"Unsupported operator {op!r} in sumslide mapping for {var!r}."
1027
+ )
1028
+ def _on_slider_change_old (self, g: dict) -> None:
1029
+ """Maintain the sum constraint when a slider changes."""
1030
+ if self._in_programmatic_update:
1031
+ return
1032
+
1033
+ line_des = g["owner"].description
1034
+ line_index = list(self.current_values.keys()).index(line_des)
1035
+ self.current_values[line_des]["value"] = g["new"]
1036
+ # debug_var(g,line_des,line_index)
1037
+
1038
+ allvalues = [v["value"] for v in self.current_values.values()]
1039
+
1040
+ self.slacklines = [cb.value for cb in self.wslackval]
1041
+ if not any(self.slacklines):
1042
+ self.wslackval[0].value = True
1043
+ self.slacklines = [cb.value for cb in self.wslackval]
1044
+
1045
+
1046
+
1047
+ sumall = sum(allvalues)
1048
+
1049
+ if round(sum(allvalues),6) == round(self.maxsum,6) :
1050
+ return
1051
+
1052
+ sumslack = sum(v for v, slack in zip(allvalues, self.slacklines) if slack)
1053
+ sumnoslack = sumall-sumslack
1054
+ numberslack = float(self.slacklines.count(True))
1055
+ # debug_var(allvalues,self.slacklines,sumall,sumslack,sumnoslack,numberslack)
1056
+
1057
+ # Adjust last slider first
1058
+ adjustment = self.maxsum - sumall
1059
+ adjustment_pr_slack = [adjustment/numberslack if a_slack else 0.0 for a_slack in self.slacklines]
1060
+ newvalues = [v+a for v,a in zip(allvalues,adjustment_pr_slack)]
1061
+ newvalues = [max(cont['min'],min(value,cont['max'])) for
1062
+ cont,value in zip(self.current_values.values(),newvalues)]
1063
+
1064
+ # debug_var('before adjustment',allvalues,sumall,adjustment_pr_slack)
1065
+
1066
+ newsum = sum(newvalues)
1067
+
1068
+ if not round(sum(allvalues),6) == round(self.maxsum,6) :
1069
+ newvalues = [max(cont['min'],min(value,cont['max'])) for
1070
+ cont,value in zip(self.current_values.values(),newvalues)]
1071
+ # If still too high, reduce the changed slider to fit
1072
+ newvalues[line_index] = newvalues[line_index] - newsum + self.maxsum
1073
+ self._in_programmatic_update = True
1074
+ # debug_var(sum(newvalues), newvalues)
1075
+
1076
+ for i,v in enumerate(newvalues):
1077
+ # print(i,v)
1078
+ self.wset[i].value = v
1079
+
1080
+ self._in_programmatic_update = False
1081
+
1082
+ def _on_slider_change(self, g: dict) -> None:
1083
+ """Maintain the sum constraint when a slider changes."""
1084
+ if self._in_programmatic_update:
1085
+ return
1086
+
1087
+ line_des = g["owner"].description
1088
+ line_index = list(self.current_values.keys()).index(line_des)
1089
+ self.current_values[line_des]["value"] = g["new"]
1090
+
1091
+ values = [v["value"] for v in self.current_values.values()]
1092
+ slacklines = [cb.value for cb in self.wslackval]
1093
+
1094
+ # Ensure at least one slack variable
1095
+ if not any(slacklines):
1096
+ self.wslackval[-1].value = True
1097
+ slacklines = [cb.value for cb in self.wslackval]
1098
+
1099
+ total = sum(values)
1100
+ if round(total, 6) == round(self.maxsum, 6):
1101
+ return
1102
+
1103
+ slack_count = slacklines.count(True)
1104
+ adjustment = (self.maxsum - total) / slack_count
1105
+
1106
+ newvalues = []
1107
+ for value, is_slack, cont in zip(values, slacklines, self.current_values.values()):
1108
+ candidate = value + adjustment if is_slack else value
1109
+ candidate = max(cont["min"], min(candidate, cont["max"]))
1110
+ newvalues.append(candidate)
1111
+
1112
+ # Final correction on the changed slider to hit maxsum exactly
1113
+ gap = self.maxsum - sum(newvalues)
1114
+ cont = list(self.current_values.values())[line_index]
1115
+ newvalues[line_index] = max(
1116
+ cont["min"],
1117
+ min(newvalues[line_index] + gap, cont["max"])
1118
+ )
1119
+
1120
+ self._in_programmatic_update = True
1121
+ try:
1122
+ for widget, value in zip(self.wset, newvalues):
1123
+ widget.value = value
1124
+ finally:
1125
+ self._in_programmatic_update = False
1126
+
1127
+ @register_widget
1128
+ @dataclass
1129
+ class radiowidget(SingleWidgetBase):
1130
+ """
1131
+ Multiple RadioButtons groups.
1132
+
1133
+ ``content`` format::
1134
+
1135
+ {
1136
+ "Group 1": [["Label A", "VAR_A"], ["Label B", "VAR_B"]],
1137
+ "Group 2": [["On", "FLAG_ON"], ["Off", "FLAG_OFF"]],
1138
+ }
1139
+
1140
+ Behavior
1141
+ --------
1142
+ On update:
1143
+ - sets all variables in the group to 0
1144
+ - sets the selected variable to 1
1145
+ """
1146
+
1147
+ wradiolist: List[RadioButtons] = field(init=False)
1148
+ _datawidget: VBox = field(init=False)
1149
+
1150
+ def __post_init__(self) -> None:
1151
+ super().__post_init__()
1152
+
1153
+ wexp = Label(value=self.heading, layout={"width": "54%"})
1154
+ whead = HBox([wexp])
1155
+
1156
+ self.wradiolist = [
1157
+ RadioButtons(
1158
+ options=[label for label, _ in cont],
1159
+ description=des,
1160
+ layout={"width": "70%"},
1161
+ style={"description_width": "37%"},
1162
+ )
1163
+ for des, cont in self.content.items()
1164
+ ]
1165
+
1166
+ groups = HBox(self.wradiolist) if len(self.wradiolist) <= 2 else VBox(self.wradiolist)
1167
+ self._datawidget = VBox([whead, groups])
1168
+
1169
+ @property
1170
+ def datawidget(self) -> Any:
1171
+ return self._datawidget
1172
+
1173
+ def reset(self, g: Any) -> None:
1174
+ for wradio in self.wradiolist:
1175
+ wradio.index = 0
1176
+
1177
+ def update_df(self, df: pd.DataFrame, current_per: Any = None) -> None:
1178
+ # If not provided, operate on the full index
1179
+ if current_per is None:
1180
+ current_per = df.index
1181
+
1182
+ for wradio, (_, cont) in zip(self.wradiolist, self.content.items()):
1183
+ for _, variable in cont:
1184
+ df.loc[current_per, variable] = 0
1185
+ selected_variable = cont[wradio.index][1]
1186
+ df.loc[current_per, selected_variable] = 1
1187
+
1188
+ @register_widget
1189
+ @dataclass
1190
+ class checkwidget(SingleWidgetBase):
1191
+ """
1192
+ Checkbox list.
1193
+
1194
+ ``content`` format::
1195
+
1196
+ {
1197
+ "Enable A": ["VAR_A", True],
1198
+ "Enable B": ["VAR_B", False],
1199
+ }
1200
+
1201
+ On update: variable is set to 1.0 if checked else 0.0.
1202
+ """
1203
+
1204
+ wchecklist: List[Checkbox] = field(init=False)
1205
+ _datawidget: VBox = field(init=False)
1206
+
1207
+ def __post_init__(self) -> None:
1208
+ super().__post_init__()
1209
+
1210
+ wexp = Label(value=self.heading, layout={"width": "54%"})
1211
+ whead = HBox([wexp])
1212
+
1213
+ self.wchecklist = [
1214
+ Checkbox(description=des, value=val)
1215
+ for des, (variable, val) in self.content.items()
1216
+ ]
1217
+
1218
+ self._datawidget = VBox([whead, VBox(self.wchecklist)])
1219
+
1220
+ @property
1221
+ def datawidget(self) -> Any:
1222
+ return self._datawidget
1223
+
1224
+ def reset(self, g: Any) -> None:
1225
+ for wcheck, (_, (variable, val)) in zip(self.wchecklist, self.content.items()):
1226
+ wcheck.value = val
1227
+
1228
+ def update_df(self, df: pd.DataFrame, current_per: Any = None) -> None:
1229
+ # If not provided, operate on the full index
1230
+ if current_per is None:
1231
+ current_per = df.index
1232
+
1233
+ for wcheck, (_, (variable, _)) in zip(self.wchecklist, self.content.items()):
1234
+ df.loc[current_per, variable] = 1.0 if wcheck.value else 0.0
1235
+
1236
+
1237
+
1238
+
1239
+ @dataclass
1240
+ class updatewidget:
1241
+ ''' class to input and run a model
1242
+
1243
+ - display(wtotal) to activate the widget
1244
+
1245
+ '''
1246
+
1247
+ mmodel : Any # a model
1248
+ datawidget : Any # a widget to update from
1249
+ basename : str ='Business as usual'
1250
+ keeppat : str = '*'
1251
+ varpat : str ='*'
1252
+ showvarpat : bool = True # Show varpaths to
1253
+ lwrun : bool = True
1254
+ lwupdate : bool = False
1255
+ lwreset : bool = True
1256
+ lwsetbas : bool = True
1257
+ outputwidget : str = 'jupviz'
1258
+ display_first :Any = None
1259
+ # to the plot widget
1260
+
1261
+ vline : list = field(default_factory=list)
1262
+ relativ_start : int = 0
1263
+ short :bool = False
1264
+ render_mode : Any = None
1265
+
1266
+ exodif : Any = field(default_factory=pd.DataFrame) # definition
1267
+
1268
+
1269
+
1270
+ def __post_init__(self):
1271
+ if self.render_mode is None:
1272
+ self.render_mode = _detect_render_mode()
1273
+
1274
+
1275
+
1276
+ self.baseline = self.mmodel.basedf.copy()
1277
+ self.wrun = Button(description="Run scenario")
1278
+ self.wrun .on_click(self.run)
1279
+ self.wrun .tooltip = 'Click to run'
1280
+ self.wrun.style.button_color = 'Lime'
1281
+
1282
+
1283
+ wupdate = Button(description="Update the dataset ")
1284
+ wupdate.on_click(self.update)
1285
+
1286
+ wreset = Button(description="Reset to start")
1287
+ wreset.on_click(self.reset)
1288
+
1289
+
1290
+ wsetbas = Button(description="Use as baseline")
1291
+ wsetbas.on_click(self.setbasis)
1292
+ self.experiment = 0
1293
+
1294
+ lbut = []
1295
+
1296
+ if self.lwrun: lbut.append(self.wrun )
1297
+ if self.lwupdate: lbut.append(wupdate)
1298
+ if self.lwreset: lbut.append(wreset)
1299
+ if self.lwsetbas : lbut.append(wsetbas)
1300
+
1301
+ wbut = HBox(lbut)
1302
+
1303
+
1304
+ self.wname = Text(value=self.basename,placeholder='Type something',description='Scenario name:',
1305
+ layout={'width':'30%'},style={'description_width':'50%'})
1306
+ self.wselectfrom = Text(value= self.varpat,placeholder='Type something',description='Display variables:',
1307
+ layout={'width':'65%'},style={'description_width':'30%'})
1308
+
1309
+ self.wselectfrom.layout.visibility = 'visible' if self.showvarpat else 'hidden'
1310
+
1311
+ winputstring = HBox([self.wname,self.wselectfrom])
1312
+
1313
+
1314
+ self.mmodel.keep_solutions = {}
1315
+ self.mmodel.keep_solutions = {self.wname.value : self.baseline}
1316
+ self.mmodel.keep_exodif = {}
1317
+
1318
+ self.experiment += 1
1319
+ self.wname.value = f'Experiment {self.experiment}'
1320
+
1321
+ self.wtotal = VBox([HTML(value="Hello <b>World</b>")])
1322
+
1323
+ def init_run(g):
1324
+ # print(f'{g=}')
1325
+ self.varpat = g['new']
1326
+ self.keep_ui = keep_plot_widget(mmodel = self.mmodel,
1327
+ selectfrom = self.varpat,
1328
+ vline=self.vline,relativ_start=self.relativ_start,
1329
+ short = self.short,
1330
+ render_mode = self.render_mode)
1331
+
1332
+ # self.wtotal = VBox([self.datawidget.datawidget,winputstring,wbut,
1333
+ # self.keep_ui.datawidget])
1334
+ self.wtotal.children = [self.datawidget.datawidget,winputstring,wbut,
1335
+ self.keep_ui.datawidget]
1336
+
1337
+ self.start = copy(self.mmodel.current_per[0])
1338
+ self.end = copy(self.mmodel.current_per[-1])
1339
+
1340
+ self.wselectfrom.observe(init_run,names='value',type='change')
1341
+
1342
+ init_run({'new':self.varpat})
1343
+
1344
+ def update(self,g):
1345
+ self.thisexperiment = self.baseline.copy()
1346
+ # print(f'update smpl {self.mmodel.current_per=}')
1347
+
1348
+ self.datawidget.update_df(self.thisexperiment,self.mmodel.current_per)
1349
+ self.exodif = self.mmodel.exodif(self.baseline,self.thisexperiment)
1350
+ # print(f'update 2 smpl {self.mmodel.current_per[0]=}')
1351
+
1352
+
1353
+
1354
+
1355
+ def run(self,g):
1356
+ self.update(g)
1357
+ # print(f'run smpl {self.mmodel.current_per[0]=}')
1358
+ # self.start = self.mmodel.current_per[0]
1359
+ # self.end = self.mmodel.current_per[-1]
1360
+ # print(f'{self.start=} {self.end=}')
1361
+ self.wrun .tooltip = 'Running'
1362
+ self.wrun.style.button_color = 'Red'
1363
+
1364
+ self.mmodel(self.thisexperiment,start=self.start,end=self.end,progressbar=0,keep = self.wname.value,
1365
+ keep_variables = self.keeppat)
1366
+ self.wrun .tooltip = 'Click to run'
1367
+ self.wrun.style.button_color = 'Lime'
1368
+ # print(f'run efter smpl {self.mmodel.current_per[0]=}')
1369
+ self.mmodel.keep_exodif[self.wname.value] = self.exodif
1370
+ self.mmodel.inputwidget_alternativerun = True
1371
+ self.current_experiment = self.wname.value
1372
+ self.experiment += 1
1373
+ self.wname.value = f'Experiment {self.experiment}'
1374
+ self.keep_ui.trigger(None)
1375
+ # --- FULL REBUILD of keep_plot_widget to force scenario refresh ---
1376
+ # debug_var(self.plot_render_mode)
1377
+ self.keep_ui = keep_plot_widget(
1378
+ mmodel=self.mmodel,
1379
+ selectfrom=self.varpat,
1380
+ vline=self.vline,
1381
+ relativ_start=self.relativ_start,
1382
+ short=self.short,
1383
+ render_mode = self.render_mode
1384
+ )
1385
+
1386
+ self.wtotal.children = [
1387
+ self.datawidget.datawidget,
1388
+ self.wtotal.children[1], # scenario name + variable selector row
1389
+ self.wtotal.children[2], # buttons
1390
+ self.keep_ui.datawidget # NEW plot widget
1391
+ ]
1392
+
1393
+
1394
+ def setbasis(self,g):
1395
+ if not hasattr(self, "current_experiment"):
1396
+ return
1397
+
1398
+ self.mmodel.keep_solutions={self.current_experiment:self.mmodel.keep_solutions[self.current_experiment]}
1399
+
1400
+ self.mmodel.keep_exodif[self.current_experiment] = self.exodif
1401
+ self.mmodel.inputwidget_alternativerun = True
1402
+
1403
+
1404
+
1405
+
1406
+ def reset(self,g):
1407
+ self.datawidget.reset(g)
1408
+
1409
+
1410
+ def _ipython_display_(self):
1411
+ """Displays the widget in a Jupyter Notebook."""
1412
+ display(self.wtotal)
1413
+
1414
+
1415
+ def fig_to_image(fig,format='svg'):
1416
+ from io import StringIO
1417
+ f = StringIO()
1418
+ fig.savefig(f,format=format,bbox_inches="tight")
1419
+ f.seek(0)
1420
+ image= f.read()
1421
+ return image
1422
+
1423
+
1424
+
1425
+
1426
+
1427
+ @dataclass
1428
+ class savefigs_widget:
1429
+ """
1430
+ Provides a widget for saving matplotlib figures from a dictionary to files.
1431
+
1432
+ The widget allows the user to specify the save location, file format(s), and
1433
+ additional naming details for the saved figures. It also includes an option to
1434
+ open the save location in the file explorer after saving.
1435
+
1436
+ Attributes:
1437
+ figs (dict, optional): A dictionary containing matplotlib figures.
1438
+ Defaults to an empty dict.
1439
+ location (str, optional): The default directory where figures will be saved.
1440
+ Defaults to './graph'.
1441
+ addname (str, optional): An additional suffix to append to the figure filenames.
1442
+ Defaults to an empty string.
1443
+
1444
+ The widget layout includes:
1445
+ - A button to initiate the save process.
1446
+ - Input fields to specify the experiment name, save location, and filename suffix.
1447
+ - A multiple-selection dropdown to choose the file formats (e.g., svg, pdf, png, eps).
1448
+ - A checkbox to optionally open the save location after saving.
1449
+ - An output field displaying the final save location.
1450
+ """
1451
+
1452
+ figs: dict = field(default_factory=dict)
1453
+ location: str = './graph'
1454
+ addname: str = ''
1455
+
1456
+ def __post_init__(self):
1457
+ wgo = Button(description='Save charts to file', style={'button_color': 'lightgreen'})
1458
+ wlocation = Text(value=self.location, description='Save location:',
1459
+ layout={'width': '350px'}, style={'description_width': '200px'})
1460
+ wexperimentname = Text(value='Experiment_1', description='Name of these experiments:',
1461
+ layout={'width': '350px'}, style={'description_width': '200px'})
1462
+ self.waddname = Text(value=self.addname, placeholder='If needed, type a suffix',
1463
+ description='Suffix for these charts:',
1464
+ layout={'width': '350px'}, style={'description_width': '200px'})
1465
+ wextensions = SelectMultiple(value=('svg',), options=['svg', 'pdf', 'png', 'eps'],
1466
+ description='Output type:',
1467
+ layout={'width': '250px'}, style={'description_width': '200px'}, rows=4)
1468
+ wxopen = Checkbox(value=True, description='Open location', disabled=False,
1469
+ layout={'width': '300px'}, style={'description_width': '5%'})
1470
+ wsavelocation = Text(value='', description='Saved at:',
1471
+ layout={'width': '90%'}, style={'description_width': '100px'},
1472
+ disabled=True)
1473
+ wsavelocation.layout.visibility = 'hidden'
1474
+
1475
+ def go(g):
1476
+ from modelclass import model
1477
+ result = model.savefigs(self.figs, location=wlocation.value, experimentname=wexperimentname.value,
1478
+ addname=self.waddname.value, extensions=wextensions.value, xopen=wxopen.value)
1479
+ wsavelocation.value = result
1480
+ wsavelocation.layout.visibility = 'visible'
1481
+
1482
+ wgo.on_click(go)
1483
+ self.datawidget = VBox([HBox([wgo, wxopen]), wexperimentname, wlocation, self.waddname,
1484
+ wextensions, wsavelocation])
1485
+
1486
+
1487
+
1488
+
1489
+
1490
+ @dataclass
1491
+ class keep_plot_widget:
1492
+ """
1493
+ Interactive plotting widget for ModelFlow solutions.
1494
+
1495
+ render_mode:
1496
+ - 'classic': old behavior using HTML/SVG and Tab/Accordion
1497
+ - 'colab': Colab-safe behavior using Select + Output
1498
+ """
1499
+
1500
+ mmodel: Any
1501
+ smpl: Tuple[str, str] = ('', '')
1502
+ relativ_start: int = 0
1503
+ selected: str = ''
1504
+ selectfrom: str = '*'
1505
+ showselectfrom: bool = True
1506
+ legend: bool = False
1507
+ dec: str = ''
1508
+ use_descriptions: bool = True
1509
+ select_width: str = ''
1510
+ select_height: str = '200px'
1511
+ vline: Any = None
1512
+ var_groups: dict = field(default_factory=dict)
1513
+ use_var_groups: bool = True
1514
+ add_var_name: bool = False
1515
+ short: Any = 0
1516
+ select_scenario: bool = True
1517
+ displaytype: str = 'tab'
1518
+ save_location: str = './graph'
1519
+ switch: bool = False
1520
+ use_smpl: bool = False
1521
+ init_dif: bool = False
1522
+
1523
+ # new
1524
+ render_mode: str = 'classic' # 'classic' or 'colab'
1525
+ colab_selector_width: str = '350px'
1526
+
1527
+ prefix_dict: dict = field(default_factory=dict, init=False)
1528
+
1529
+ def __post_init__(self):
1530
+ minper = self.mmodel.lastdf.index[0]
1531
+ maxper = self.mmodel.lastdf.index[-1]
1532
+ options = [(ind, nr) for nr, ind in enumerate(self.mmodel.lastdf.index)]
1533
+ self.first_prefix = True
1534
+
1535
+ self.old_current_per = copy(self.mmodel.current_per)
1536
+ self.select_scenario = False if self.switch else self.select_scenario
1537
+
1538
+ with self.mmodel.set_smpl(*self.smpl):
1539
+ with self.mmodel.set_smpl_relative(self.relativ_start, 0):
1540
+ show_per = copy(list(self.mmodel.current_per)[:])
1541
+
1542
+ self.mmodel.current_per = copy(self.old_current_per)
1543
+
1544
+ self.save_dialog = savefigs_widget(location=self.save_location)
1545
+
1546
+ allkeepvar = [set(df.columns) for df in self.mmodel.keep_solutions.values()]
1547
+ keepvar = sorted(allkeepvar[0].intersection(*allkeepvar[1:])) if allkeepvar else []
1548
+ keepvar_set = set(keepvar)
1549
+ wselectfrom = Text(
1550
+ value=self.selectfrom,
1551
+ placeholder='Type something',
1552
+ description='Display variables:',
1553
+ layout={'width': '65%'},
1554
+ style={'description_width': '30%'}
1555
+ )
1556
+ wselectfrom.layout.visibility = 'visible' if self.showselectfrom else 'hidden'
1557
+
1558
+ self.wxopen = Checkbox(
1559
+ value=False,
1560
+ description='Allow save figures',
1561
+ disabled=False,
1562
+ layout={'width': '25%'},
1563
+ style={'description_width': '10%'}
1564
+ )
1565
+
1566
+ gross_selectfrom = []
1567
+
1568
+ def changeselectfrom(g):
1569
+ nonlocal gross_selectfrom
1570
+ _selectfrom = [s.upper() for s in self.mmodel.vlist(wselectfrom.value) if s in keepvar_set] if self.selectfrom else keepvar
1571
+ gross_selectfrom = [
1572
+ (
1573
+ f'{(v+" ") if self.add_var_name else ""}{self.mmodel.var_description[v] if self.use_descriptions else v}',
1574
+ v
1575
+ )
1576
+ for v in _selectfrom
1577
+ ]
1578
+ try:
1579
+ selected_vars.options = gross_selectfrom
1580
+ if len(gross_selectfrom):
1581
+ selected_vars.value = [gross_selectfrom[0][1]]
1582
+ else:
1583
+ selected_vars.value = []
1584
+ except Exception:
1585
+ ...
1586
+
1587
+ wselectfrom.observe(changeselectfrom, names='value', type='change')
1588
+ self.wxopen.observe(self.trigger, names='value', type='change')
1589
+ changeselectfrom(None)
1590
+
1591
+ with self.mmodel.keepswitch(switch=self.switch, scenarios='*'):
1592
+ gross_keys = list(self.mmodel.keep_solutions.keys())
1593
+
1594
+ scenariobase = Select(
1595
+ options=gross_keys,
1596
+ value=gross_keys[0] if gross_keys else None,
1597
+ description='First scenario',
1598
+ layout=Layout(width='50%', font="monospace")
1599
+ )
1600
+
1601
+ scenarioselect = SelectMultiple(
1602
+ options=[s for s in gross_keys if s != scenariobase.value],
1603
+ value=[s for s in gross_keys if s != scenariobase.value],
1604
+ description='Next',
1605
+ layout=Layout(width='50%', font="monospace")
1606
+ )
1607
+
1608
+ keep_keys = list(self.mmodel.keep_solutions.keys())
1609
+ self.scenarioselected = '|'.join(keep_keys)
1610
+ keep_first = keep_keys[0] if keep_keys else ""
1611
+
1612
+ def changescenariobase(g):
1613
+ scenarioselect.options = [s for s in gross_keys if s != scenariobase.value]
1614
+ scenarioselect.value = [s for s in gross_keys if s != scenariobase.value]
1615
+ self.scenarioselected = '|'.join([scenariobase.value] + list(scenarioselect.value))
1616
+ diff.description = fr'Difference to: "{scenariobase.value}"'
1617
+ self.trigger(None)
1618
+
1619
+ def changescenarioselect(g):
1620
+ self.scenarioselected = '|'.join([scenariobase.value] + list(scenarioselect.value))
1621
+ diff.description = fr'Difference to: "{scenariobase.value}"'
1622
+ self.trigger(None)
1623
+
1624
+ scenariobase.observe(changescenariobase, names='value', type='change')
1625
+ scenarioselect.observe(changescenarioselect, names='value', type='change')
1626
+
1627
+ wscenario = HBox([scenariobase, scenarioselect])
1628
+ wscenario.layout.visibility = 'visible' if self.select_scenario else 'hidden'
1629
+
1630
+ init_start = self.mmodel.lastdf.index.get_loc(show_per[0])
1631
+ init_end = self.mmodel.lastdf.index.get_loc(show_per[-1])
1632
+ width = self.select_width if self.select_width else '50%'
1633
+
1634
+ description_width = 'initial'
1635
+
1636
+ if self.use_var_groups:
1637
+ if len(self.var_groups):
1638
+ self.prefix_dict = self.var_groups
1639
+ elif hasattr(self.mmodel, 'var_groups') and len(self.mmodel.var_groups):
1640
+ self.prefix_dict = self.mmodel.var_groups
1641
+ else:
1642
+ self.prefix_dict = {}
1643
+ else:
1644
+ self.prefix_dict = {}
1645
+
1646
+ select_prefix = [(iso, c) for iso, c in self.prefix_dict.items()]
1647
+
1648
+ i_smpl = SelectionRangeSlider(
1649
+ value=[init_start, init_end],
1650
+ continuous_update=False,
1651
+ options=options,
1652
+ min=minper,
1653
+ max=maxper,
1654
+ layout=Layout(width='75%'),
1655
+ description='Show interval'
1656
+ )
1657
+
1658
+ selected_vars = SelectMultiple(
1659
+ options=gross_selectfrom,
1660
+ layout=Layout(width=width, height=self.select_height, font="monospace"),
1661
+ description='Select one or more',
1662
+ style={'description_width': description_width}
1663
+ )
1664
+
1665
+ diff = RadioButtons(
1666
+ options=[('No', False), ('Yes', True), ('In percent', 'pct')],
1667
+ description=fr'Difference to: "{keep_first}"',
1668
+ value=self.init_dif,
1669
+ style={'description_width': 'auto'},
1670
+ layout=Layout(width='auto')
1671
+ )
1672
+
1673
+ showtype = RadioButtons(
1674
+ options=[('Level', 'level'), ('Growth', 'growth')],
1675
+ description='Data type',
1676
+ value='level',
1677
+ style={'description_width': description_width}
1678
+ )
1679
+
1680
+ scale = RadioButtons(
1681
+ options=[('Linear', 'linear'), ('Log', 'log')],
1682
+ description='Y-scale',
1683
+ value='linear',
1684
+ style={'description_width': description_width}
1685
+ )
1686
+
1687
+ legend = RadioButtons(
1688
+ options=[('Yes', 1), ('No', 0)],
1689
+ description='Legends',
1690
+ value=self.legend,
1691
+ style={'description_width': description_width},
1692
+ layout=Layout(width='auto', margin="0% 0% 0% 5%")
1693
+ )
1694
+
1695
+ self.widget_dict = {
1696
+ 'i_smpl': i_smpl,
1697
+ 'selected_vars': selected_vars,
1698
+ 'diff': diff,
1699
+ 'showtype': showtype,
1700
+ 'scale': scale,
1701
+ 'legend': legend
1702
+ }
1703
+
1704
+ for wid in self.widget_dict.values():
1705
+ wid.observe(self.trigger, names='value', type='change')
1706
+
1707
+ # output areas
1708
+ self.out_widget = VBox([HTML(value="")]) # classic mode
1709
+ self.plot_output = Output() # colab mode
1710
+ self.figure_selector = None
1711
+ self.figure_box = VBox()
1712
+ self._current_figs = {}
1713
+ self._current_titles = {}
1714
+
1715
+ def get_prefix(g):
1716
+ try:
1717
+ current_suffix = {v[len(g['old'][0]):] for v in selected_vars.value}
1718
+ except Exception:
1719
+ current_suffix = ''
1720
+
1721
+ new_prefix = g['new']
1722
+
1723
+ gross_selectfrom_vars = [self.mmodel.string_substitution(variable) for des, variable in gross_selectfrom]
1724
+ gross_pat = ' '.join([self.mmodel.string_substitution(ppat) for ppat in new_prefix])
1725
+ selected_match_var = set(self.mmodel.list_names(gross_selectfrom_vars, gross_pat))
1726
+
1727
+ selected_prefix_var = tuple(
1728
+ (des, variable) for des, variable in gross_selectfrom
1729
+ if variable in selected_match_var
1730
+ )
1731
+
1732
+ try:
1733
+ selected_vars.options = selected_prefix_var
1734
+ except Exception:
1735
+ ...
1736
+
1737
+ if not self.first_prefix:
1738
+ new_selection = [
1739
+ f'{n}{c}' for c in current_suffix for n in new_prefix
1740
+ if f'{n}{c}' in {s for p, s in selected_prefix_var}
1741
+ ]
1742
+ selected_vars.value = new_selection
1743
+ else:
1744
+ self.first_prefix = False
1745
+ if len(selected_prefix_var):
1746
+ selected_vars.value = [varname for des, varname in selected_prefix_var]
1747
+
1748
+ if len(self.prefix_dict):
1749
+ selected_prefix = SelectMultiple(
1750
+ value=[select_prefix[0][1]],
1751
+ options=select_prefix,
1752
+ layout=Layout(width='25%', height=self.select_height, font="monospace"),
1753
+ description=''
1754
+ )
1755
+ selected_prefix.observe(get_prefix, names='value', type='change')
1756
+ select = HBox([selected_vars, selected_prefix])
1757
+ get_prefix({'new': select_prefix[0]})
1758
+ else:
1759
+ select = VBox([selected_vars])
1760
+ if len(gross_selectfrom):
1761
+ selected_vars.value = [gross_selectfrom[0][1]]
1762
+
1763
+ options1 = HBox([diff]) if self.short >= 2 else HBox([diff, legend])
1764
+ options2 = HBox([scale, showtype, self.wxopen])
1765
+
1766
+ if self.short:
1767
+ vui = [select, options1]
1768
+ else:
1769
+ if self.select_scenario:
1770
+ vui = [wscenario, select, options1, options2]
1771
+ else:
1772
+ vui = [select, options1, options2]
1773
+
1774
+ vui = vui + [i_smpl] if self.use_smpl else vui
1775
+
1776
+ if self.render_mode == 'colab':
1777
+ self.datawidget = VBox(vui + [self.figure_box])
1778
+ else:
1779
+ self.datawidget = VBox(vui + [self.out_widget])
1780
+
1781
+ @property
1782
+ def show(self):
1783
+ display(self.datawidget)
1784
+
1785
+ def _repr_html_(self):
1786
+ display(self.datawidget)
1787
+
1788
+ def __repr__(self):
1789
+ return ' '
1790
+
1791
+ def _make_figure_title(self, varname: str) -> str:
1792
+ return f'{(varname+" ") if self.add_var_name else ""}{self.mmodel.var_description[varname] if self.use_descriptions else varname}'
1793
+
1794
+ def explain(self, i_smpl=None, selected_vars=None, diff=None, showtype=None, scale=None, legend=None):
1795
+ variabler = ' '.join(v for v in selected_vars)
1796
+ smpl = (self.mmodel.lastdf.index[i_smpl[0]], self.mmodel.lastdf.index[i_smpl[1]])
1797
+
1798
+ if type(diff) == str:
1799
+ diffpct = True
1800
+ ldiff = False
1801
+ else:
1802
+ ldiff = diff
1803
+ diffpct = False
1804
+
1805
+ self.save_dialog.waddname.value = (
1806
+ ('_level' if showtype == 'level' else '_growth') +
1807
+ ('_diff' if ldiff else '') +
1808
+ ('_diffpct' if diffpct else '') +
1809
+ ('_log' if scale == 'log' else '')
1810
+ )
1811
+
1812
+ with self.mmodel.keepswitch(switch=self.switch, scenarios=self.scenarioselected):
1813
+ with self.mmodel.set_smpl(*smpl):
1814
+ self.keep_wiz_figs = self.mmodel.keep_plot(
1815
+ variabler,
1816
+ diff=ldiff,
1817
+ diffpct=diffpct,
1818
+ scale=scale,
1819
+ showtype=showtype,
1820
+ showfig=False,
1821
+ legend=legend,
1822
+ dec=self.dec,
1823
+ vline=self.vline
1824
+ )
1825
+ plt.close('all')
1826
+ return self.keep_wiz_figs
1827
+
1828
+ def _show_selected_figure(self, key: str) -> None:
1829
+ with self.plot_output:
1830
+ self.plot_output.clear_output(wait=True)
1831
+ fig = self._current_figs[key]
1832
+ display(fig)
1833
+ plt.close(fig)
1834
+
1835
+ def _render_figures_colab(self, figs: dict) -> None:
1836
+ titles = {self._make_figure_title(k): k for k in figs.keys()}
1837
+ title_list = list(titles.keys())
1838
+
1839
+ if not title_list:
1840
+ self.figure_box.children = ()
1841
+ return
1842
+
1843
+ self._current_figs = figs
1844
+ self._current_titles = titles
1845
+
1846
+ if self.figure_selector is None:
1847
+ self.figure_selector = Select(
1848
+ options=title_list,
1849
+ value=title_list[0],
1850
+ description='Figure:',
1851
+ layout=Layout(width=self.colab_selector_width),
1852
+ style={'description_width': 'initial'}
1853
+ )
1854
+
1855
+ def _on_select(change):
1856
+ if change['name'] == 'value' and change['new'] in self._current_titles:
1857
+ self._show_selected_figure(self._current_titles[change['new']])
1858
+
1859
+ self.figure_selector.observe(_on_select, names='value')
1860
+ else:
1861
+ old = self.figure_selector.value
1862
+ self.figure_selector.options = title_list
1863
+ self.figure_selector.value = old if old in title_list else title_list[0]
1864
+
1865
+ self._show_selected_figure(self._current_titles[self.figure_selector.value])
1866
+
1867
+ children = [self.figure_selector, self.plot_output]
1868
+ self.save_dialog.figs = figs
1869
+ if self.wxopen.value:
1870
+ children.append(self.save_dialog.datawidget)
1871
+
1872
+ self.figure_box.children = tuple(children)
1873
+
1874
+ def _render_figures_classic(self, figs: dict) -> None:
1875
+ figlist = [
1876
+ # HTML(fig_to_image(a_fig), width='100%', format='svg', layout=Layout(width='90%'))
1877
+ HTML(value = fig_to_image(a_fig), layout=Layout(width='90%'))
1878
+ for key, a_fig in figs.items()
1879
+ ]
1880
+
1881
+ if self.displaytype in {'tab', 'accordion'}:
1882
+ wtab = Tab(figlist) if self.displaytype == 'tab' else Accordion(figlist)
1883
+ for i, v in enumerate(figs.keys()):
1884
+ wtab.set_title(i, self._make_figure_title(v))
1885
+ wtab.selected_index = 0
1886
+ res = [wtab]
1887
+ else:
1888
+ res = figlist
1889
+
1890
+ self.save_dialog.figs = figs
1891
+ self.out_widget.children = tuple(res + [self.save_dialog.datawidget]) if self.wxopen.value else tuple(res)
1892
+ self.out_widget.layout.visibility = 'visible'
1893
+
1894
+ def trigger(self, g):
1895
+ self.mmodel.current_per = copy(self.old_current_per)
1896
+ values = {widname: wid.value for widname, wid in self.widget_dict.items()}
1897
+
1898
+ if len(values['selected_vars']):
1899
+ figs = self.explain(**values)
1900
+ self.keep_wiz_figs = figs
1901
+
1902
+ if self.render_mode == 'colab':
1903
+ self._render_figures_colab(figs)
1904
+ else:
1905
+ self._render_figures_classic(figs)
1906
+ else:
1907
+ if self.render_mode == 'colab':
1908
+ self.figure_box.children = ()
1909
+ with self.plot_output:
1910
+ self.plot_output.clear_output(wait=True)
1911
+ else:
1912
+ self.out_widget.layout.visibility = 'hidden'
1913
+
1914
+ class shinywidget:
1915
+ """Placeholder for a future Shiny wrapper."""
1916
+ a_widget : Any # The datawidget to wrap
1917
+ widget_id : str
1918
+
1919
+ def __post_init__(self):
1920
+ from shinywidgets import register_widget
1921
+ ...
1922
+ register_widget(self.widget_id, self.a_widget.datawidget)
1923
+
1924
+
1925
+ def update_df(self,df,current_per):
1926
+ ''' will update container widgets'''
1927
+ self.a_widget.update_df(df,current_per)
1928
+
1929
+
1930
+ def reset(self,g):
1931
+ ''' will reset container widgets'''
1932
+ self.a_widget.reset(g)
1933
+