nova-trame 0.8.0__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.
nova/__init__.py ADDED
File without changes
nova/trame/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .view.theme import ThemedApp
2
+
3
+ __all__ = ["ThemedApp"]
@@ -0,0 +1,98 @@
1
+ """Model state for RemoteFileInput."""
2
+
3
+ import os
4
+ from typing import Any, Union
5
+
6
+
7
+ class RemoteFileInputModel:
8
+ """Manages interactions between RemoteFileInput and the file system."""
9
+
10
+ def __init__(self, allow_files: bool, allow_folders: bool, base_paths: list[str], extensions: list[str]) -> None:
11
+ """Creates a new RemoteFileInputModel."""
12
+ self.allow_files = allow_files
13
+ self.allow_folders = allow_folders
14
+ self.base_paths = base_paths
15
+ self.extensions = extensions
16
+
17
+ def get_base_paths(self) -> list[dict[str, Any]]:
18
+ return [{"path": base_path, "directory": True} for base_path in self.base_paths]
19
+
20
+ def scan_current_path(self, current_path: str, showing_all_files: bool) -> tuple[list[dict[str, Any]], bool]:
21
+ failed = False
22
+
23
+ try:
24
+ if current_path and (not self.valid_subpath(current_path) or not os.path.exists(current_path)):
25
+ raise FileNotFoundError
26
+
27
+ files = [{"path": "..", "directory": True}]
28
+
29
+ if os.path.isdir(current_path):
30
+ scan_path = current_path
31
+ else:
32
+ scan_path = os.path.dirname(current_path)
33
+
34
+ for entry in os.scandir(scan_path):
35
+ if self.valid_entry(entry, showing_all_files):
36
+ files.append({"path": entry.name, "directory": entry.is_dir()})
37
+ except OSError:
38
+ files = self.get_base_paths()
39
+ failed = True
40
+
41
+ sorted_files = sorted(files, key=lambda entry: str(entry["path"]).lower())
42
+
43
+ return (sorted_files, failed)
44
+
45
+ def select_file(self, file: Union[dict[str, str], str], old_path: str, showing_base_paths: bool) -> str:
46
+ if file == "":
47
+ return ""
48
+
49
+ if isinstance(file, dict):
50
+ file = file["path"]
51
+
52
+ if not os.path.isdir(old_path):
53
+ # If the previous selection is a file, then we need to append to its parent directory
54
+ old_path = os.path.dirname(old_path)
55
+
56
+ if not showing_base_paths and file != "..":
57
+ return os.path.join(old_path, file)
58
+ elif not showing_base_paths:
59
+ if old_path in self.base_paths:
60
+ return ""
61
+ else:
62
+ return os.path.dirname(old_path)
63
+ else:
64
+ return file
65
+
66
+ def valid_entry(self, entry: os.DirEntry, showing_all_files: bool) -> bool:
67
+ if entry.is_dir():
68
+ return True
69
+
70
+ if not self.allow_files:
71
+ return False
72
+
73
+ return showing_all_files or not self.extensions or any(entry.name.endswith(ext) for ext in self.extensions)
74
+
75
+ def valid_selection(self, selection: str) -> bool:
76
+ if self.valid_subpath(selection):
77
+ if os.path.isdir(selection) and self.allow_folders:
78
+ return True
79
+
80
+ if os.path.isfile(selection) and self.allow_files:
81
+ return True
82
+
83
+ return False
84
+
85
+ def valid_subpath(self, subpath: str) -> bool:
86
+ if subpath == "":
87
+ return False
88
+
89
+ try:
90
+ real_path = os.path.realpath(subpath)
91
+ except TypeError:
92
+ return False
93
+
94
+ for base_path in self.base_paths:
95
+ if real_path.startswith(base_path):
96
+ return True
97
+
98
+ return False
@@ -0,0 +1,4 @@
1
+ from .input_field import InputField
2
+ from .remote_file_input import RemoteFileInput
3
+
4
+ __all__ = ["InputField", "RemoteFileInput"]
@@ -0,0 +1,273 @@
1
+ """View Implementation for InputField."""
2
+
3
+ import logging
4
+ import re
5
+ from typing import Any, Dict
6
+
7
+ from mvvm_lib.pydantic_utils import get_field_info
8
+ from trame.app import get_server
9
+ from trame.widgets import client
10
+ from trame.widgets import vuetify3 as vuetify
11
+ from trame_client.widgets.core import AbstractElement
12
+ from trame_server.controller import Controller
13
+ from trame_server.state import State
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class InputField:
19
+ """Factory class for generating Vuetify input components."""
20
+
21
+ @staticmethod
22
+ def create_boilerplate_properties(v_model: str | None) -> dict:
23
+ if not v_model:
24
+ return {}
25
+ object_name_in_state = v_model.split(".")[0]
26
+ field_info = None
27
+ try:
28
+ field_name = ".".join(v_model.split(".")[1:])
29
+ if "[" in field_name:
30
+ index_field_name = re.sub(r"\[.*?\]", "[0]", field_name)
31
+ field_info = get_field_info(index_field_name)
32
+ if "[" in field_name and "[index]" not in field_name:
33
+ field_info = None
34
+ logger.warning(
35
+ f"{v_model}: validation ignored. We currently only "
36
+ f"support single loop with index variable that should be called 'index'"
37
+ )
38
+ else:
39
+ field_info = get_field_info(field_name)
40
+ except Exception as _:
41
+ pass
42
+ label = ""
43
+ help_dict: dict = {}
44
+ placeholder = None
45
+ if field_info:
46
+ label = field_info.title
47
+ if field_info.examples and len(field_info.examples) > 0:
48
+ placeholder = field_info.examples[0]
49
+ help_dict = {"hint": field_info.description, "placeholder": placeholder}
50
+
51
+ args: Dict[str, Any] = {}
52
+ if v_model:
53
+ args |= {
54
+ "v_model": v_model,
55
+ "label": label,
56
+ "help": help_dict,
57
+ "update_modelValue": f"flushState('{object_name_in_state}')",
58
+ }
59
+ if field_info:
60
+ args |= {
61
+ "rules": (f"[(v) => trigger('validate_pydantic_field', ['{field_name}', v, index])]",),
62
+ }
63
+ return args
64
+
65
+ def __new__(
66
+ cls, v_model: str | None = None, required: bool = False, type: str = "text", **kwargs: Any
67
+ ) -> AbstractElement:
68
+ """Constructor for InputField.
69
+
70
+ Parameters
71
+ ----------
72
+ required : bool
73
+ If true, the input will be visually marked as required and a required rule will be added to the end of the
74
+ rules list.
75
+ type : str
76
+ The type of input to create. This can be any of the following:
77
+
78
+ - autocomplete
79
+ - autoscroll (produces a textarea that automatically scrolls to the bottom as new content is added)
80
+ - checkbox
81
+ - combobox
82
+ - file
83
+ - input
84
+ - otp
85
+ - radio
86
+ - range-slider
87
+ - select
88
+ - slider
89
+ - switch
90
+ - textarea
91
+
92
+ Any other value will produce a text field with your type used as an HTML input type attribute.
93
+
94
+ Returns
95
+ -------
96
+ `trame_client.widgets.core.AbstractElement <https://trame.readthedocs.io/en/latest/core.widget.html#trame_client.widgets.core.AbstractElement>`_
97
+ The Vuetify input component.
98
+ """
99
+ server = get_server(None, client_type="vue3")
100
+
101
+ kwargs = {**cls.create_boilerplate_properties(v_model), **kwargs}
102
+
103
+ if "__events" not in kwargs or kwargs["__events"] is None:
104
+ kwargs["__events"] = []
105
+
106
+ # This must be present before each input is created or change events won't be triggered.
107
+ if isinstance(kwargs["__events"], list):
108
+ if "change" not in kwargs["__events"]:
109
+ kwargs["__events"].append("change")
110
+ if "scroll" not in kwargs["__events"]:
111
+ kwargs["__events"].append("scroll")
112
+
113
+ match type:
114
+ case "autocomplete":
115
+ input = vuetify.VAutocomplete(**kwargs)
116
+ case "autoscroll":
117
+ input = vuetify.VTextarea(**kwargs)
118
+ cls._setup_autoscroll(server.state, input)
119
+ case "checkbox":
120
+ input = vuetify.VCheckbox(**kwargs)
121
+ case "combobox":
122
+ input = vuetify.VCombobox(**kwargs)
123
+ case "file":
124
+ input = vuetify.VFileInput(**kwargs)
125
+ case "input":
126
+ input = vuetify.VInput(**kwargs)
127
+ case "otp":
128
+ input = vuetify.VOtpInput(**kwargs)
129
+ case "radio":
130
+ input = vuetify.VRadioGroup(**kwargs)
131
+ case "range-slider":
132
+ input = vuetify.VRangeSlider(**kwargs)
133
+ case "select":
134
+ items = kwargs.pop("items", None)
135
+ if isinstance(items, str):
136
+ items = (items,)
137
+
138
+ input = vuetify.VSelect(items=items, **kwargs)
139
+ case "slider":
140
+ input = vuetify.VSlider(**kwargs)
141
+ case "switch":
142
+ input = vuetify.VSwitch(**kwargs)
143
+ case "textarea":
144
+ input = vuetify.VTextarea(**kwargs)
145
+ case _:
146
+ input = vuetify.VTextField(type=type, **kwargs)
147
+
148
+ cls._setup_help(input, **kwargs)
149
+
150
+ cls._check_rules(input)
151
+ if required:
152
+ cls._setup_required_label(input)
153
+ cls._setup_required_rule(input)
154
+
155
+ cls._setup_ref(input)
156
+ cls._setup_change_listener(server.controller, input)
157
+
158
+ return input
159
+
160
+ @staticmethod
161
+ def _check_rules(input: AbstractElement) -> None:
162
+ if "rules" in input._py_attr and input.rules and not isinstance(input.rules, tuple):
163
+ raise ValueError(f"Rules for '{input.label}' must be a tuple")
164
+
165
+ @staticmethod
166
+ def _setup_autoscroll(state: State, input: AbstractElement) -> None:
167
+ if input.v_model:
168
+ if "id" not in input._py_attr or input.id is None:
169
+ input.id = f"nova__{input._id}"
170
+ input.scroll = f"window.nova__autoscroll('{input.id}');"
171
+
172
+ with state:
173
+ if state["nova_scroll_position"] is None:
174
+ state["nova_scroll_position"] = {}
175
+ state.nova_scroll_position[input.id] = 0
176
+
177
+ autoscroll = client.JSEval(
178
+ exec=(
179
+ "if (window.nova__autoscroll !== undefined) {"
180
+ # If the autoscroll function already exists, call it.
181
+ " window.nova__autoscroll($event);"
182
+ "} else {"
183
+ # Save the JS so it can be called from outside of this script (ie during a scroll event).
184
+ " window.nova__autoscroll = function(id) {"
185
+ # Get the element in the browser by ID
186
+ " const element = window.document.querySelector(`#${id}`);"
187
+ # If the user is at the bottom of the textarea, then we should autoscroll.
188
+ " if (element.scrollTop === window.trame.state.state.nova_scroll_position[id]) {"
189
+ # Scroll to the bottom
190
+ " element.scrollTop = element.scrollHeight;"
191
+ # Save the new scroll position
192
+ " window.trame.state.state.nova_scroll_position[id] = element.scrollTop;"
193
+ " flushState('nova_scroll_position');"
194
+ # If the user has scrolled back to the bottom, then we should reenable scrolling.
195
+ " } else if (element.scrollTop === element.scrollHeight - element.clientHeight) {"
196
+ # Save the new scroll position
197
+ " window.trame.state.state.nova_scroll_position[id] = element.scrollTop;"
198
+ " flushState('nova_scroll_position');"
199
+ " }"
200
+ " };"
201
+ " window.nova__autoscroll($event);"
202
+ "}"
203
+ )
204
+ ).exec
205
+
206
+ @state.change(input.v_model)
207
+ def _autoscroll(**kwargs: Any) -> None:
208
+ autoscroll(input.id)
209
+
210
+ @staticmethod
211
+ def _setup_help(_input: AbstractElement, **kwargs: Any) -> None:
212
+ help = kwargs.get("help", None)
213
+ if help and isinstance(help, dict):
214
+ _input.hint = help.get("hint", None)
215
+ _input.placeholder = help.get("placeholder", None)
216
+
217
+ @staticmethod
218
+ def _setup_required_label(input: AbstractElement) -> None:
219
+ if input.label:
220
+ input.label = f"{input.label}*"
221
+ else:
222
+ input.label = "*"
223
+
224
+ @staticmethod
225
+ def _setup_ref(input: AbstractElement) -> None:
226
+ if "ref" not in input._py_attr or input.ref is None:
227
+ input.ref = f"nova__{input._id}"
228
+
229
+ @staticmethod
230
+ def _setup_required_rule(input: AbstractElement) -> None:
231
+ required_rule = "(value) => value?.length > 0 || 'Field is required'"
232
+ if "rules" in input._py_attr and input.rules:
233
+ # Existing rules will be in format ("[rule1, rule2]",) and we need to append to this list
234
+ rule_end_index = input.rules[0].rindex("]")
235
+ input.rules = (f"{input.rules[0][:rule_end_index]}, {required_rule}{input.rules[0][rule_end_index:]}",)
236
+ else:
237
+ input.rules = (f"[{required_rule}]",)
238
+
239
+ @staticmethod
240
+ def _setup_change_listener(ctrl: Controller, input: AbstractElement) -> None:
241
+ base_handler = None
242
+ if "change" in input._py_attr and input.change is not None:
243
+ base_handler = input.change
244
+
245
+ # Iterate over all saved refs and perform validation if there is a value that can be validated.
246
+ change_handler = (
247
+ "Object.values(window.trame.refs).map("
248
+ " (ref) => typeof ref.validate === 'function' && ref.value ? ref.validate() : null"
249
+ ");"
250
+ )
251
+
252
+ # We need to coerce the developer's change handler, which could be a string, callable, or tuple containing a
253
+ # callable, to a single string to be compatible with our change handler.
254
+ if callable(base_handler):
255
+ base_handler = (base_handler,)
256
+ if isinstance(base_handler, tuple):
257
+
258
+ @ctrl.trigger(f"{input.ref}__trigger")
259
+ def _(*args: str, **kwargs: Any) -> None:
260
+ base_handler[0](*args, **kwargs)
261
+
262
+ change_handler = (
263
+ "trigger("
264
+ f"'{input.ref}__trigger', "
265
+ f"{base_handler[1] if len(base_handler) > 1 else []}, "
266
+ f"{base_handler[2] if len(base_handler) > 2 else {} }"
267
+ f"); {change_handler}"
268
+ ) # Call the developer's provided change method via a trigger, then call ours.
269
+ elif isinstance(base_handler, str):
270
+ # Call the developer's provided change JS expression, then call ours.
271
+ change_handler = f"{base_handler}; {change_handler}"
272
+
273
+ input.change = change_handler
@@ -0,0 +1,193 @@
1
+ """View implementation for RemoteFileInput."""
2
+
3
+ from functools import partial
4
+ from typing import Any, Optional, cast
5
+
6
+ from mvvm_lib.trame_binding import TrameBinding
7
+ from trame.app import get_server
8
+ from trame.widgets import client, html
9
+ from trame.widgets import vuetify3 as vuetify
10
+ from trame_client.widgets.core import AbstractElement
11
+
12
+ from nova.trame.model.remote_file_input import RemoteFileInputModel
13
+ from nova.trame.view.components import InputField
14
+ from nova.trame.view_model.remote_file_input import RemoteFileInputViewModel
15
+
16
+
17
+ class RemoteFileInput:
18
+ """Generates a file selection dialog for picking files off of the server.
19
+
20
+ You cannot use typical Trame :code:`with` syntax to add children to this.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ v_model: Optional[str] = None,
26
+ allow_files: bool = True,
27
+ allow_folders: bool = False,
28
+ allow_nonexistent_path: bool = False,
29
+ base_paths: Optional[list[str]] = None,
30
+ dialog_props: Optional[dict[str, Any]] = None,
31
+ extensions: Optional[list[str]] = None,
32
+ input_props: Optional[dict[str, Any]] = None,
33
+ label: str = "",
34
+ ) -> None:
35
+ """Constructor for RemoteFileInput.
36
+
37
+ Parameters
38
+ ----------
39
+ v_model : str
40
+ The v-model for the input field.
41
+ allow_files : bool
42
+ If true, the user can save a file selection.
43
+ allow_folders : bool
44
+ If true, the user can save a folder selection.
45
+ allow_nonexistent_path : bool
46
+ If false, the user will be warned when they've selected a non-existent path on the filesystem.
47
+ base_paths : list[str], optional
48
+ Only files under these paths will be shown.
49
+ dialog_props : dict[str, typing.Any], optional
50
+ Props to be passed to VDialog.
51
+ extensions : list[str], optional
52
+ Only files with these extensions will be shown by default. The user can still choose to view all files.
53
+ input_props : dict[str, typing.Any], optional
54
+ Props to be passed to InputField. Must not include label prop, use the top-level label parameter instead.
55
+ label : str
56
+ Label shown in the input field and the dialog title.
57
+
58
+ Raises
59
+ ------
60
+ ValueError
61
+ If v_model is None.
62
+
63
+ Returns
64
+ -------
65
+ None
66
+ """
67
+ if v_model is None:
68
+ raise ValueError("RemoteFileInput must have a v_model attribute.")
69
+
70
+ self.v_model = v_model
71
+ self.allow_files = allow_files
72
+ self.allow_folders = allow_folders
73
+ self.allow_nonexistent_path = allow_nonexistent_path
74
+ self.base_paths = base_paths if base_paths else ["/"]
75
+ self.dialog_props = dict(dialog_props) if dialog_props else {}
76
+ self.extensions = extensions if extensions else []
77
+ self.input_props = dict(input_props) if input_props else {}
78
+ self.label = label
79
+
80
+ if "__events" not in self.input_props:
81
+ self.input_props["__events"] = []
82
+ self.input_props["__events"].append("change")
83
+
84
+ if "width" not in self.dialog_props:
85
+ self.dialog_props["width"] = 600
86
+
87
+ self.create_model()
88
+ self.create_viewmodel()
89
+ self.create_ui()
90
+
91
+ def create_ui(self) -> None:
92
+ with cast(
93
+ AbstractElement,
94
+ InputField(
95
+ v_model=self.v_model,
96
+ label=self.label,
97
+ change=(self.vm.select_file, "[$event.target.value]"),
98
+ **self.input_props,
99
+ ),
100
+ ):
101
+ self.vm.init_view()
102
+
103
+ with vuetify.Template(v_slot_append=True):
104
+ with vuetify.VBtn(icon=True, size="small", click=self.vm.open_dialog):
105
+ vuetify.VIcon("mdi-folder-open")
106
+
107
+ with vuetify.VDialog(
108
+ v_model=self.vm.get_dialog_state_name(),
109
+ activator="parent",
110
+ persistent=True,
111
+ **self.dialog_props,
112
+ ):
113
+ with vuetify.VCard(classes="pa-4"):
114
+ vuetify.VCardTitle(self.label)
115
+ vuetify.VTextField(
116
+ v_model=self.v_model,
117
+ classes="mb-4 px-4",
118
+ label="Current Selection",
119
+ __events=["change"],
120
+ change=(self.vm.select_file, "[$event.target.value]"),
121
+ )
122
+
123
+ if self.allow_files and self.extensions:
124
+ with html.Div(v_if=(f"{self.vm.get_showing_all_state_name()}",)):
125
+ vuetify.VListSubheader("All Available Files")
126
+ vuetify.VBtn(
127
+ "Don't show all",
128
+ classes="mb-4",
129
+ size="small",
130
+ click=self.vm.toggle_showing_all_files,
131
+ )
132
+ with html.Div(v_else=True):
133
+ vuetify.VListSubheader(
134
+ f"Available Files with Extensions: {', '.join(self.extensions)}"
135
+ )
136
+ vuetify.VBtn(
137
+ "Show all",
138
+ classes="mb-4",
139
+ size="small",
140
+ click=self.vm.toggle_showing_all_files,
141
+ )
142
+ elif self.allow_files:
143
+ vuetify.VListSubheader("Available Files")
144
+ else:
145
+ vuetify.VListSubheader("Available Folders")
146
+
147
+ with vuetify.VList(classes="mb-4"):
148
+ self.vm.populate_file_list()
149
+
150
+ vuetify.VListItem(
151
+ "{{ file.path }}",
152
+ v_for=f"file, index in {self.vm.get_file_list_state_name()}",
153
+ classes=(
154
+ f"index < {self.vm.get_file_list_state_name()}.length - 1 "
155
+ "? 'border-b-thin' "
156
+ ": ''",
157
+ ),
158
+ prepend_icon=("file.directory ? 'mdi-folder' : 'mdi-file'",),
159
+ click=(self.vm.select_file, "[file]"),
160
+ )
161
+
162
+ with html.Div(classes="text-center"):
163
+ vuetify.VBtn(
164
+ "OK",
165
+ classes="mr-4",
166
+ disabled=(f"!{self.vm.get_valid_selection_state_name()}",),
167
+ click=self.vm.close_dialog,
168
+ )
169
+ vuetify.VBtn(
170
+ "Cancel",
171
+ color="lightgrey",
172
+ click=partial(self.vm.close_dialog, cancel=True),
173
+ )
174
+
175
+ def create_model(self) -> None:
176
+ self.model = RemoteFileInputModel(self.allow_files, self.allow_folders, self.base_paths, self.extensions)
177
+
178
+ def create_viewmodel(self) -> None:
179
+ server = get_server(None, client_type="vue3")
180
+ binding = TrameBinding(server.state)
181
+
182
+ self.vm = RemoteFileInputViewModel(self.model, binding)
183
+
184
+ self.vm.dialog_bind.connect(self.vm.get_dialog_state_name())
185
+ self.vm.file_list_bind.connect(self.vm.get_file_list_state_name())
186
+ self.vm.on_close_bind.connect(client.JSEval(exec=f"{self.vm.get_dialog_state_name()} = false;").exec)
187
+ self.vm.on_update_bind.connect(
188
+ client.JSEval(
189
+ exec=f"{self.v_model} = $event; flushState('{self.v_model.split('.')[0].split('[')[0]}');"
190
+ ).exec
191
+ )
192
+ self.vm.showing_all_bind.connect(self.vm.get_showing_all_state_name())
193
+ self.vm.valid_selection_bind.connect(self.vm.get_valid_selection_state_name())
@@ -0,0 +1,3 @@
1
+ from .interactive_2d_plot import Interactive2DPlot
2
+
3
+ __all__ = ["Interactive2DPlot"]
@@ -0,0 +1,85 @@
1
+ """View implementation for Interactive2DPlot."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ from altair import Chart
6
+ from trame.widgets import client, vega
7
+
8
+
9
+ class Interactive2DPlot(vega.Figure):
10
+ """Creates an interactive 2D plot in Trame using Vega.
11
+
12
+ Trame provides two primary mechanisms for composing 2D plots: `Plotly <https://github.com/Kitware/trame-plotly>`_
13
+ and `Vega-Lite/Altair <https://github.com/Kitware/trame-vega>`_. If you only need static plots or basic browser
14
+ event handling, then please use these libraries directly.
15
+
16
+ If you need to capture complex front-end interactions, then you can
17
+ use our provided Interactive2DPlot widget that is based on Vega-Lite. This uses the same API as Trame's vega.Figure,
18
+ except that it will automatically sync Vega's signal states as the user interacts with the plot.
19
+
20
+ The following allows the user to select a region of the plot and tracks the selected region in Python:
21
+
22
+ .. literalinclude:: ../tests/test_interactive_2d_plot.py
23
+ :start-after: setup 2d plot
24
+ :end-before: setup 2d plot complete
25
+ :dedent:
26
+ """
27
+
28
+ def __init__(self, figure: Optional[Chart] = None, **kwargs: Any) -> None:
29
+ """Constructor for Interactive2DPlot.
30
+
31
+ Parameters
32
+ ----------
33
+ figure : `altair.Chart <https://altair-viz.github.io/user_guide/generated/toplevel/altair.Chart.html#altair.Chart>`_
34
+ Altair chart object
35
+ kwargs
36
+ Arguments to be passed to `AbstractElement <https://trame.readthedocs.io/en/latest/core.widget.html#trame_client.widgets.core.AbstractElement>`_
37
+
38
+ Returns
39
+ -------
40
+ None
41
+ """
42
+ self._initialized = False
43
+
44
+ super().__init__(figure=figure, **kwargs)
45
+ self.ref = f"nova__vega_{self._id}"
46
+ self.server.state[self.ref] = {}
47
+ self._start_update_handlers = client.JSEval(
48
+ exec=(
49
+ "async () => {"
50
+ f" let ref = window.trame.refs['{self.ref}'];"
51
+ " await ref.mountVis();" # wait for the new visualization to be rendered in the front-end
52
+ " if (ref.viz === undefined) { return; }" # If the component is not mounted, do nothing
53
+ " for (const [key, value] of Object.entries(ref.viz.view._signals)) {"
54
+ " if (key === 'unit') { continue; }" # this causes a JSError for some reason if not skipped
55
+ " ref.viz.view.addSignalListener(key, (name, value) => {"
56
+ f" window.trame.state.state['{self.ref}'][name] = value;" # sync front-end state
57
+ f" flushState('{self.ref}');" # sync back-end state
58
+ " })"
59
+ " }"
60
+ "}"
61
+ )
62
+ ).exec
63
+
64
+ client.ClientTriggers(mounted=self.update)
65
+
66
+ def get_signal_state(self, name: str) -> Any:
67
+ """Retrieves a Vega signal's state by name.
68
+
69
+ Parameters
70
+ ----------
71
+ name : str
72
+ The name of the signal to retrieve.
73
+
74
+ Returns
75
+ -------
76
+ typing.Any
77
+ The current value of the Vega signal.
78
+ """
79
+ return self.server.state[self.ref].get(name, None)
80
+
81
+ def update(self, figure: Optional[Chart] = None, **kwargs: Any) -> None:
82
+ super().update(figure=figure, **kwargs)
83
+
84
+ if hasattr(self, "_start_update_handlers"):
85
+ self._start_update_handlers()
@@ -0,0 +1,5 @@
1
+ from .grid import GridLayout
2
+ from .hbox import HBoxLayout
3
+ from .vbox import VBoxLayout
4
+
5
+ __all__ = ["GridLayout", "HBoxLayout", "VBoxLayout"]