-
Notifications
You must be signed in to change notification settings - Fork 68
Expand file tree
/
Copy pathmpcomponent.py
More file actions
820 lines (698 loc) · 31.3 KB
/
mpcomponent.py
File metadata and controls
820 lines (698 loc) · 31.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
from __future__ import annotations
import logging
import re
from abc import ABC
from ast import literal_eval
from base64 import b64encode
from collections import defaultdict
from itertools import chain, zip_longest
from json import JSONDecodeError, dumps, loads
from typing import TYPE_CHECKING, Any, ClassVar, Literal
from warnings import warn
import dash
import dash_mp_components as mpc
import numpy as np
import plotly.io as pio
from dash import dcc, html
from dash.dependencies import ALL
from monty.json import MontyDecoder, MSONable
from crystal_toolkit.core.plugin import CrystalToolkitPlugin
from crystal_toolkit.helpers.layouts import H6, Button, Icon, Loading, add_label_help
from crystal_toolkit.settings import SETTINGS
if TYPE_CHECKING:
import plotly.graph_objects as go
from flask_caching import Cache
# Crystal Toolkit namespace, added to the start of all ids
# so we can see which layouts have been added by Crystal Toolkit
CT_NAMESPACE = "CT"
class MPComponent(ABC): # noqa: B024
"""The abstract base class for an MPComponent.
MPComponent is designed to help render an MSONable object.
"""
# reference to global Dash app
app = None
# reference to Flask cache
cache = None
# used to track all dcc.Stores required for all MPComponents to work
# keyed by the MPComponent id
_app_stores_dict: ClassVar[dict[str, list[dcc.Store]]] = defaultdict(list)
# used to track what individual Dash components are defined
# by this MPComponent
_all_id_basenames: ClassVar[set[str]] = set()
# used to defer generation of callbacks until app.layout defined
# can be helpful to callback exceptions retained
_callbacks_to_generate: ClassVar[set[MPComponent]] = set()
@staticmethod
def register_app(app: dash.Dash):
"""This method has been deprecated. Please use crystal_toolkit.CrystalToolkitPlugin."""
warn(
"The register_app method is no longer required, please instead use the "
"crystal_toolkit.CrystalToolkitPlugin when instantiating your Dash app.",
category=PendingDeprecationWarning,
)
return
@staticmethod
def register_cache(cache: Cache) -> None:
"""This method has been deprecated. Please use crystal_toolkit.CrystalToolkitPlugin."""
warn(
"The register_cache method is no longer required, please instead use the "
"crystal_toolkit.CrystalToolkitPlugin when instantiating your Dash app.",
category=PendingDeprecationWarning,
)
return
@staticmethod
def crystal_toolkit_layout(layout: html.Div) -> html.Div:
"""This method has been deprecated. Please use crystal_toolkit.CrystalToolkitPlugin."""
warn(
"The crystal_toolkit_layout method is no longer required, please instead use the "
"crystal_toolkit.CrystalToolkitPlugin when instantiating your Dash app.",
category=PendingDeprecationWarning,
)
return
@staticmethod
def register_crystal_toolkit(app, layout, cache=None):
"""This method has been deprecated. Please use crystal_toolkit.CrystalToolkitPlugin."""
warn(
"The register_crystal_toolkit method is no longer required, please instead use the "
"crystal_toolkit.CrystalToolkitPlugin when instantiating your Dash app.",
category=PendingDeprecationWarning,
)
# call the plugin manually for backwards compatibility, but the
# user should instead use Dash(..., plugins=[CrystalToolkitPlugin(cache=cache, layout=layout)])
plugin = CrystalToolkitPlugin(layout=layout, cache=cache)
plugin.plug(app)
@staticmethod
def all_app_stores() -> html.Div:
"""This must be included somewhere in your Crystal Toolkit Dash app's layout for
interactivity to work. This is a hidden element that contains the MSON for each MPComponent.
Returns: a html.Div Dash Layout
"""
return html.Div(
list(chain.from_iterable(MPComponent._app_stores_dict.values()))
)
def __init__(
self,
default_data: MSONable | dict | str | None = None,
id: str | None = None,
links: dict[str, str] | None = None,
storage_type: Literal["memory", "local", "session"] = "memory",
disable_callbacks: bool = False,
) -> None:
"""The abstract base class for an MPComponent.
The MPComponent is designed to help render any MSONable object,
for example many of the objects in pymatgen (Structure, PhaseDiagram, etc.)
To instantiate an MPComponent, you will need to create it outside
of your Dash app layout:
my_component = MPComponent(my_msonable_object)
Then, inside the app.layout, you can include the component's layout
anywhere you choose: my_component.layout
If you want the layouts to be interactive, i.e. to respond to callbacks,
you have to also use the CrystalToolkitPlugin when instantiating your
Dash app.
If you do not want the layouts to be interactive, set disable_callbacks
to True to prevent errors.
If including multiple MPComponents of the same type, make sure
to set the id field to a unique value, as you would in any other
Dash component.
When sub-classing MPComponent, the most important methods to implement
are _sub_layouts and generate_callbacks().
Args:
default_data: initial contents for the component, can be None
id: a unique id, required if multiple of the same type of
MPComponent are included in an app
links: if set, will set store contents from the stores of another
component to reduce unnecessary callbacks and duplication of data,
note that links are one directional only and specific the origin
stores, e.g. set {"default": my_other_component.id()} to fill this
component's default store contents from the other component's default store,
or {"graph": my_other_component.id("graph")} to fill this component's
"graph" store from another component's "graph" store
storage_type: whether to persist contents of component through
browser refresh or browser sessions, use with caution, defaults
to "memory" so component store will be emptied on refresh, see
dcc.Store documentation for more information
disable_callbacks: if True, will not generate callbacks, useful
for static layouts or returning new MPComponents dynamically where
generating callbacks are not possible due to limitations of Dash
"""
# ensure ids are unique
# Note: shadowing Python built-in here, but only because Dash does it...
# TODO: do something else here
if id is None:
# TODO: this could lead to duplicate ids and an error, but if
# setting random ids, this could also lead to undefined behavior
id = f"{CT_NAMESPACE}{type(self).__name__}"
elif not id.startswith(CT_NAMESPACE):
id = f"{CT_NAMESPACE}{id}"
while id in MPComponent._all_id_basenames:
# check if id ends with counter
if not re.search(r"-\d+$", id):
id += "-1"
else:
# increment counter at end of id until unique
next_ids = int(re.search(r"-(\d+)$", id).group(1)) + 1
id = re.sub(r"-\d+$", f"-{next_ids}", id)
MPComponent._all_id_basenames.add(id)
self._id = id
self._all_ids: set[str] = set()
self._stores = {}
self._initial_data = {}
self.links = links or {}
self.create_store(
name="default", initial_data=default_data, storage_type=storage_type
)
self.links["default"] = self.id()
if not disable_callbacks:
# callbacks generated as final step by crystal_toolkit_layout()
self._callbacks_to_generate.add(self)
self.logger = logging.getLogger(type(self).__name__)
def id(
self,
name: str = "default",
is_kwarg: bool = False,
idx: bool | int = False,
hint: str | None = None,
) -> str | dict[str, str]:
"""Generate an id from a name combined with the base id of the MPComponent itself, useful
for generating ids of individual components in the layout.
In the special case of the id of an element that is used to re-construct
a keyword argument for a specific class, it will store information necessary
to reconstruct that keyword argument (e.g. its type hint and, in the case of
a vector or matrix, the corresponding index).
A hint could be a tuple for a numpy array of that shape, e.g. (3, 3) for a 3x3 matrix,
(1, 3) for a vector, or "literal" to parse kwarg value using ast.literal_eval, or "bool"
to parse a boolean value. In future iterations, we may be able to replace this with native
Python type hints. The problem here is being able to specify array shape where appropriate.
Args:
name (str): The name of the element, e.g. "graph", "structure". Defaults to "default".
is_kwarg (bool): If True, return a dict with information necessary to reconstruct
the keyword argument for a specific class.
idx (bool | int): The index to return if is_kwarg is True. Defaults to False.
hint (str): The type hint to return if is_kwarg is True. Defaults to None.
Returns: e.g. "MPComponent_default"
"""
if is_kwarg:
return dict(
component_id=self._id, kwarg_label=name, idx=str(idx), hint=str(hint)
)
# if we're linking to another component, return that id
if name in self.links:
return self.links[name]
# otherwise create a new id
self._all_ids.add(name)
return f"{self._id}_{name}" if name != "default" else f"{self._id}"
def create_store(
self,
name: str,
initial_data: MSONable | dict | str | None = None,
storage_type: Literal["memory", "local", "session"] = "memory",
debug_clear: bool = False,
) -> None:
"""Generate a dcc.Store to hold something (MSONable object, Dict or string), and register it
so that it will be included in the Dash app automatically.
The initial data will be stored in a class attribute as
self._initial_data[name].
Args:
name: name for the store
initial_data: initial data to include
storage_type: as in dcc.Store
debug_clear: set to True to empty the store if using
persistent storage
"""
# if we're linking to another component, do not create a new store
if name in self.links:
return
store = dcc.Store(
id=self.id(name),
data=initial_data,
storage_type=storage_type,
clear_data=debug_clear,
)
self._stores[name] = store
self._initial_data[name] = initial_data
MPComponent._app_stores_dict[self.id()].append(store)
@property
def initial_data(self) -> dict[str, Any]:
"""Initial data for all the stores defined by component, keyed by store name."""
return self._initial_data
@staticmethod
def from_data(data: dict[str, Any]) -> MPComponent:
"""Converts the contents of a dcc.Store back into a Python object.
:param data: contents of a dcc.Store created by to_data
:return: a Python object
"""
return loads(dumps(data), cls=MontyDecoder)
@property
def all_stores(self) -> list[str]:
"""List of all store ids generated by this component."""
return list(self._stores)
@property
def all_ids(self) -> list[str]:
"""List of all ids generated by this component."""
return [
component_id
for component_id in self._all_ids
if component_id not in self.all_stores
]
def __repr__(self) -> str:
return f"{self.id()}<{type(self).__name__}>"
def __str__(self) -> str:
ids = "\n".join(
[f"* {component_id} " for component_id in sorted(self.all_ids)]
)
stores = "\n".join(f"* {store} " for store in sorted(self.all_stores))
layouts = "\n".join(f"* {layout} " for layout in sorted(self._sub_layouts))
return f"""{self.id()}<{type(self).__name__}> \n
IDs: \n{ids} \n
Stores: \n{stores} \n
Sub-layouts: \n{layouts}"""
@property
def _sub_layouts(self) -> dict[str, dash.development.base_component.Component]:
"""Layouts associated with this component, available for book-keeping if your component is
complex, so that the layout() method is just assembles individual sub-layouts.
:return: A dictionary with names of layouts as keys (str) and Dash
layouts (e.g. html.Div) as values.
"""
return {}
def layout(self) -> html.Div:
"""A Dash layout for the full component. Basic implementation
provided, but should in general be overridden.
"""
return html.Div(list(self._sub_layouts.values()))
def generate_callbacks(self, app, cache) -> None:
"""Generate all callbacks associated with the layouts in this app.
Assume that "suppress_callback_exceptions" is True, since it is not always guaranteed that
all layouts will be displayed to the end user at all times, but it's important the callbacks
are defined on the server.
"""
return
def get_numerical_input(
self,
kwarg_label: str,
default: float | list | None = None,
state: dict | None = None,
label: str | None = None,
help_str: str | None = None,
is_int: bool = False,
shape: tuple[int, ...] = (),
**kwargs,
) -> html.Div:
"""For Python classes which take matrices as inputs, this will generate a corresponding Dash
input layout.
:param kwarg_label: The name of the corresponding Python input, this is used
to name the component.
:param label: A description for this input.
:param default: A default value for this input.
:param state: Used to set default state for this input, use a dict with the kwarg_label as a key
and the default value as a value. Ignored if `default` is set. It can be useful to use
`state` if you want to set defaults for multiple inputs from a single dictionary.
:param help_str: Text for a tooltip when hovering over label.
:param is_int: if True, will use a numeric input
:param shape: (3, 3) for matrix, (1, 3) for vector, (1, 1) for scalar
:return: a Dash layout
"""
state = state or {}
default = np.full(shape, state.get(kwarg_label) if default is None else default)
default = np.reshape(default, shape)
style = {
"textAlign": "center",
# shorter default width if matrix or vector
"width": "5rem",
"marginRight": "0.2rem",
"marginBottom": "0.2rem",
"height": "36px",
}
style.update(kwargs.pop("style", {}))
def matrix_element(idx, value=0):
# TODO: maybe move element out of the name
mid = self.id(kwarg_label, is_kwarg=True, idx=idx, hint=shape)
if isinstance(value, np.ndarray):
value = value.item()
if not is_int:
return dcc.Input(
id=mid,
inputMode="numeric",
debounce=True,
className="input",
style=style,
value=float(value) if value is not None else None,
persistence="True",
type="number",
**kwargs,
)
return dcc.Input(
id=mid,
inputMode="numeric",
debounce=True,
className="input",
style=style,
value=int(value) if value is not None else None,
persistence="True",
type="number",
**kwargs,
)
# dict of row indices, column indices to element
matrix_contents: dict[int, dict[int, Any]] = defaultdict(dict)
# determine what individual input boxes we need
# note that shape = () for floats, shape = (3,) for vectors
# but we may also need to accept input for e.g. (3, 1)
it = np.nditer(default, flags=["multi_index", "refs_ok"])
while not it.finished:
idx = it.multi_index
row = (idx[1] if len(idx) > 1 else 0,)
column = idx[0] if len(idx) > 0 else 0
matrix_contents[row][column] = matrix_element(idx, value=it[0])
it.iternext()
# arrange the input boxes in two dimensions (rows, columns)
matrix_div_contents = []
for column_idx in sorted(matrix_contents):
row = [
matrix_contents[column_idx][row_idx]
for row_idx in sorted(matrix_contents[column_idx])
]
matrix_div_contents.append(html.Div(row))
matrix = html.Div(matrix_div_contents)
return add_label_help(matrix, label, help_str)
def get_slider_input(
self,
kwarg_label: str,
default: Any | None = None,
state: dict | None = None,
label: str | None = None,
help_str: str | None = None,
multiple: bool = False,
**kwargs,
):
state = state or {}
# TODO: bug if default == 0
default = default or state.get(kwarg_label)
# mpc.RangeSlider requires a domain to be specified
slider_kwargs = {"domain": [0, default * 2]}
slider_kwargs.update(**kwargs)
if multiple:
slider_input = mpc.DualRangeSlider(
id=self.id(kwarg_label, is_kwarg=True, hint="slider"),
value=default,
**slider_kwargs,
)
else:
slider_input = mpc.RangeSlider(
id=self.id(kwarg_label, is_kwarg=True, hint="slider"),
value=default,
**slider_kwargs,
)
return add_label_help(slider_input, label, help_str)
def get_bool_input(
self,
kwarg_label: str,
default: bool | None = None,
state: dict | None = None,
label: str | None = None,
help_str: str | None = None,
**kwargs,
):
"""For Python classes which take boolean values as inputs, this will generate a
corresponding Dash input layout.
:param kwarg_label: The name of the corresponding Python input, this is used
to name the component.
:param label: A description for this input.
:param default: A default value for this input.
:param state: Used to set default state for this input, use a dict with the
kwarg_label as a key
and the default value as a value. Ignored if `default` is set. It can be useful
to use `state` if you want to set defaults for multiple inputs from a single dictionary.
:param help_str: Text for a tooltip when hovering over label.
:return: a Dash layout
"""
state = state or {}
default = default or state.get(kwarg_label) or False
bool_input = mpc.Switch(
id=self.id(kwarg_label, is_kwarg=True, hint="bool"),
value=bool(default),
hasLabel=True,
**kwargs,
)
return add_label_help(bool_input, label, help_str)
def get_choice_input(
self,
kwarg_label: str,
default: str | None = None,
state: dict | None = None,
label: str | None = None,
help_str: str | None = None,
options: list[dict] | None = None,
clearable: bool = False,
**kwargs,
):
"""For Python classes which take pre-defined values as inputs, this will generate a
corresponding input layout using mpc.Select.
:param kwarg_label: The name of the corresponding Python input, this is used
to name the component.
:param label: A description for this input.
:param default: A default value for this input.
:param state: Used to set default state for this input, use a dict with the kwarg_label as a key
and the default value as a value. Ignored if `default` is set. It can be useful to use
`state` if you want to set defaults for multiple inputs from a single dictionary.
:param help_str: Text for a tooltip when hovering over label.
:param options: Options to choose from, as per dcc.Dropdown
:param clearable: If True, will allow Dropdown to be cleared after a selection is made.
:return: a Dash layout
"""
state = state or {}
default = default or state.get(kwarg_label)
option_input = mpc.Select(
id=self.id(kwarg_label, is_kwarg=True, hint="literal"),
options=options if options else [],
value=default,
isClearable=clearable,
arbitraryProps={**kwargs},
)
return add_label_help(option_input, label, help_str)
def get_dict_input(
self,
kwarg_label: str,
default: dict | None = None,
state: dict | None = None,
label: str | None = None,
help_str: str | None = None,
dict_size: int | None = None,
key_name: str = "key",
value_name: str = "value",
**kwargs,
) -> mpc.FilterField:
"""For Python classes which take dictionaries as inputs. The keys are fixed and only the
values can be modified. This will generate a corresponding Dash input layout.
:param kwarg_label: The name of the corresponding Python input, this is used
to name the component.
:param label: A description for this input.
:param default: A default value for this input.
:param state: Used to set default state for this input, use a dict with the kwarg_label as a key
and the default value as a value. Ignored if `default` is set. It can be useful to use
`state` if you want to set defaults for multiple inputs from a single dictionary.
:param help_str: Text for a tooltip when hovering over label.
:param dict_size: size of the dict. Can be specified in case there is no initial default or state.
:param key_name: name describing the keys of the dictionary.
:param value_name: name describing the values of the dictionary.
:return: a Dash layout
"""
state = state or {}
default = default or state.get(kwarg_label, {})
dict_size = len(default) or dict_size or 0
style = {
"textAlign": "center",
"width": "20rem",
"marginRight": "0.2rem",
"marginBottom": "0.2rem",
"height": "36px",
}
if "style" in kwargs:
style.update(kwargs.pop("style"))
def pair_element(idx, key=None, value=None):
# TODO: maybe move element out of the name
kid = self.id(kwarg_label, is_kwarg=True, idx=("k", idx), hint="dict")
key_in = dcc.Input(
id=kid,
debounce=True,
className="input",
style=style,
value=value,
persistence="True",
**kwargs,
)
vid = self.id(kwarg_label, is_kwarg=True, idx=("v", idx), hint="dict")
value_in = dcc.Input(
id=vid,
debounce=True,
className="input",
style=style,
value=value,
persistence="True",
**kwargs,
)
return [key_in, value_in]
# arrange the input boxes in two columns
dict_div_contents = [html.Div(H6(f"{key_name}: {value_name}"))]
# dict_div_contents = []
for n_idx, k in zip_longest(range(dict_size), default):
dict_div_contents.append(html.Div(pair_element(n_idx, k, default.get(k))))
dict_input = html.Div(dict_div_contents)
return add_label_help(dict_input, label, help_str)
def get_alarm_window(
self,
id: str,
message: str,
**kwargs,
):
"""Get the pop-out alarm window component, default set to not display
Eaxmple:
self.get_alarm_window(
self.id("invalid-comp-alarm"),
message="Illegal composition entry!"
),
Return True in the callback function to show alarm
Output(self.id("invalid-conc-alarm"), "displayed") -> True
Args:
:param id: The name of the corresponding Python input, this is used
to name the component.
:param message (str): A default value for this input.
"""
if not message:
raise ValueError("The error message cannot be empty!")
return dcc.ConfirmDialog(id=id, message=message, displayed=False, **kwargs)
def get_kwarg_id(self, kwarg_name) -> dict[str, str]:
return {
"component_id": self._id,
"kwarg_label": kwarg_name,
"idx": ALL,
"hint": ALL,
}
def get_all_kwargs_id(self) -> dict[str, str]:
return {"component_id": self._id, "kwarg_label": ALL, "idx": ALL, "hint": ALL}
def reconstruct_kwarg_from_state(self, state, kwarg_name):
return self.reconstruct_kwargs_from_state(
state=state, kwarg_labels=[kwarg_name]
)[kwarg_name]
def reconstruct_kwargs_from_state(self, state=None, kwarg_labels=None) -> dict:
"""Generate.
:param state: optional, a Dash callback context input or state
:param kwarg_labels: optional, parse only a specific kwarg or list of kwargs
:return: A dictionary of keyword arguments with their values
"""
if not state:
state = {}
state.update(dash.callback_context.inputs)
state.update(dash.callback_context.states)
kwargs = {}
for key, val in state.items():
# TODO: hopefully this will be less hacky in future Dash versions
# remove trailing ".value" and convert back into dictionary
# need to sort k somehow ...
try:
d = loads(key[: -len(".value")])
except JSONDecodeError:
continue
kwarg_label = d["kwarg_label"]
if kwarg_labels and kwarg_label not in kwarg_labels:
continue
try:
k_type = literal_eval(d["hint"])
except ValueError:
k_type = d["hint"]
idx = literal_eval(d["idx"])
try:
if isinstance(k_type, tuple):
# matrix or vector
if kwarg_label not in kwargs:
kwargs[kwarg_label] = np.empty(k_type)
val = literal_eval(str(val)) # noqa: PLW2901
if (val is not None) and (kwargs[kwarg_label] is not None):
# print("debugging", kwargs, kwarg_label, idx, v)
if isinstance(val, list):
print(
"This shouldn't happen! Debug required.",
kwarg_label,
idx,
val,
)
kwargs[kwarg_label][idx] = None
else:
kwargs[kwarg_label][idx] = val
else:
# require all elements to have value, otherwise set
# entire kwarg to None
kwargs[kwarg_label] = None
elif k_type == "literal":
try:
kwargs[kwarg_label] = literal_eval(str(val))
except (ValueError, SyntaxError):
kwargs[kwarg_label] = str(val)
elif k_type in ("bool", "slider"):
kwargs[kwarg_label] = val
elif k_type == "dict":
# Build a temporary dictionary here to accumulate all the data.
# The real dictionary will be reconstructed at the end
if kwarg_label not in kwargs:
kwargs[kwarg_label] = {"_tmp_dict": {"k": {}, "v": {}}}
try:
val = literal_eval(str(val)) # noqa: PLW2901
except ValueError:
pass
d = kwargs[kwarg_label]["_tmp_dict"]
d[idx[0]][idx[1]] = val
except Exception as exc:
# Not raised intentionally but if you notice this in logs please investigate.
print("This is a problem, debug required.", exc, d, val, type(val))
for key, val in kwargs.items():
if isinstance(val, np.ndarray):
kwargs[key] = val.tolist()
elif isinstance(val, dict) and "_tmp_dict" in val:
# reconstruct the real dict in the case hint=dict
full_dict = {}
for kv_index, k_dict in val["_tmp_dict"]["k"].items():
# Ignore if the key is None
if k_dict is not None:
full_dict[k_dict] = val["_tmp_dict"]["v"][kv_index]
kwargs[key] = full_dict
if SETTINGS.DEBUG_MODE:
print(type(self).__name__, "kwargs", kwargs)
return kwargs
@staticmethod
def data_uri_from_fig(
fig: go.Figure,
fmt: str = "png",
width: int = 600,
height: int = 400,
scale: int = 4,
) -> str:
"""Generate a data URI from a Plotly Figure.
Args:
fig (Figure): Plotly Figure object or corresponding dictionary
fmt (str, optional): "png", "jpg", etc. (see PlotlyScope for supported formats). Defaults to "png".
width (int, optional): width in pixels. Defaults to 600.
height (int, optional): height in pixels. Defaults to 400.
scale (int, optional): scale factor. Defaults to 4.
Returns:
str: Data URI containing base64-encoded image.
"""
output = pio.to_image(fig, format=fmt, height=height, width=width, scale=scale)
image = b64encode(output).decode("ascii")
return f"data:image/{fmt};base64,{image}"
def get_figure_placeholder(self, figure_id: str) -> html.Div:
"""Get a layout to act as a placeholder for an interactive figure.
When used with `generate_static_figure_callbacks`, and assuming
kaleido is installed on the server, a static image placeholder will
be generated.
"""
return html.Div(
[
html.Div(
[Loading(id=self.id(f"{figure_id}-wrapped-figure-inner"))],
id=self.id("wrapped-figure-outer"),
),
Button(
[Icon(kind="chart-pie"), html.Span(), "Make Plot Interactive"],
kind="primary",
id=self.id(f"{figure_id}-wrapped-figure-button"),
),
]
)