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.
- modelBLfunk.py +180 -0
- model_Excel.py +332 -0
- model_cvx.py +139 -0
- model_dynare.py +173 -0
- model_financial_stability.py +88 -0
- model_latex.py +497 -0
- model_latex_class.py +808 -0
- model_parquet_mixin.py +424 -0
- modelclass.py +9828 -0
- modelconstruct.py +1496 -0
- modelconstruct_estimation.py +2872 -0
- modeldash.py +265 -0
- modeldashboot.py +202 -0
- modeldashsidebar.py +456 -0
- modeldekom.py +651 -0
- modeldiff.py +561 -0
- modeldisplay.py +550 -0
- modelestimation.py +1776 -0
- modelestimator_new.py +2613 -0
- modelflowib-2.73.dist-info/METADATA +156 -0
- modelflowib-2.73.dist-info/RECORD +44 -0
- modelflowib-2.73.dist-info/WHEEL +5 -0
- modelflowib-2.73.dist-info/licenses/license.md +10 -0
- modelflowib-2.73.dist-info/top_level.txt +39 -0
- modelgrab.py +318 -0
- modelgrabgdx.py +584 -0
- modelgrabwf2.py +1107 -0
- modelhelp.py +543 -0
- modelhtml.py +606 -0
- modelinvert.py +250 -0
- modeljupyter.py +824 -0
- modeljupytermagic.py +813 -0
- modelmacrograb.py +98 -0
- modelmanipulation.py +1461 -0
- modelmf.py +349 -0
- modelnet.py +114 -0
- modelnewton.py +2178 -0
- modelnormalize.py +430 -0
- modelpattern.py +428 -0
- modelreport.py +2187 -0
- modeluserfunk.py +97 -0
- modelvis.py +1038 -0
- modelwidget.py +718 -0
- modelwidget_input.py +1933 -0
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
|
+
|