solara-ui 1.41.0__py2.py3-none-any.whl → 1.43.0__py2.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.
- solara/__init__.py +1 -1
- solara/__main__.py +17 -6
- solara/_stores.py +189 -0
- solara/components/__init__.py +18 -1
- solara/components/component_vue.py +23 -0
- solara/components/datatable.py +4 -4
- solara/components/echarts.py +5 -2
- solara/components/echarts.vue +22 -5
- solara/components/file_drop.py +20 -0
- solara/components/input.py +21 -1
- solara/components/markdown.py +62 -17
- solara/components/misc.py +2 -2
- solara/components/spinner-solara.vue +2 -2
- solara/components/spinner.py +17 -2
- solara/hooks/use_reactive.py +8 -1
- solara/lab/components/__init__.py +1 -0
- solara/lab/components/chat.py +3 -3
- solara/lab/components/input_time.py +133 -0
- solara/lab/hooks/dataframe.py +1 -0
- solara/lab/utils/dataframe.py +11 -1
- solara/reactive.py +9 -3
- solara/server/app.py +63 -30
- solara/server/flask.py +12 -2
- solara/server/jupyter/server_extension.py +1 -0
- solara/server/kernel.py +52 -4
- solara/server/kernel_context.py +66 -7
- solara/server/patch.py +25 -29
- solara/server/qt.py +1 -1
- solara/server/server.py +15 -5
- solara/server/settings.py +11 -0
- solara/server/shell.py +19 -1
- solara/server/starlette.py +39 -11
- solara/server/static/solara_bootstrap.py +1 -1
- solara/settings.py +17 -0
- solara/tasks.py +18 -8
- solara/template/portal/pyproject.toml +1 -1
- solara/test/pytest_plugin.py +4 -0
- solara/toestand.py +170 -16
- solara/util.py +40 -0
- solara/website/components/docs.py +4 -0
- solara/website/components/markdown.py +60 -2
- solara/website/pages/changelog/changelog.md +17 -0
- solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +10 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +4 -5
- solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +3 -5
- solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +3 -5
- solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +3 -5
- solara/website/pages/documentation/api/hooks/use_cross_filter.py +3 -5
- solara/website/pages/documentation/api/hooks/use_exception.py +9 -11
- solara/website/pages/documentation/api/hooks/use_previous.py +6 -9
- solara/website/pages/documentation/api/hooks/use_state_or_update.py +23 -26
- solara/website/pages/documentation/api/hooks/use_thread.py +11 -13
- solara/website/pages/documentation/api/routing/route.py +10 -12
- solara/website/pages/documentation/api/routing/use_route.py +26 -30
- solara/website/pages/documentation/api/utilities/on_kernel_start.py +17 -0
- solara/website/pages/documentation/components/advanced/link.py +6 -8
- solara/website/pages/documentation/components/advanced/meta.py +6 -9
- solara/website/pages/documentation/components/advanced/style.py +7 -9
- solara/website/pages/documentation/components/input/file_browser.py +12 -14
- solara/website/pages/documentation/components/input/input.py +22 -0
- solara/website/pages/documentation/components/lab/input_time.py +15 -0
- solara/website/pages/documentation/components/layout/columns_responsive.py +37 -39
- solara/website/pages/documentation/components/layout/gridfixed.py +4 -6
- solara/website/pages/documentation/components/output/html.py +1 -3
- solara/website/pages/documentation/components/page/head.py +4 -7
- solara/website/pages/documentation/components/viz/echarts.py +3 -1
- solara/website/pages/documentation/examples/__init__.py +9 -0
- solara/website/pages/documentation/examples/ai/chatbot.py +60 -44
- solara/website/pages/documentation/examples/general/live_update.py +1 -0
- solara/website/pages/documentation/examples/general/pokemon_search.py +3 -3
- solara/website/pages/documentation/examples/visualization/linked_views.py +0 -3
- solara/website/pages/documentation/faq/content/99-faq.md +9 -0
- solara/website/pages/documentation/getting_started/content/00-quickstart.md +2 -2
- solara/website/pages/documentation/getting_started/content/01-introduction.md +1 -1
- solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +23 -19
- solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +2 -2
- solara/website/pages/roadmap/roadmap.md +6 -0
- {solara_ui-1.41.0.dist-info → solara_ui-1.43.0.dist-info}/METADATA +9 -6
- {solara_ui-1.41.0.dist-info → solara_ui-1.43.0.dist-info}/RECORD +83 -80
- {solara_ui-1.41.0.dist-info → solara_ui-1.43.0.dist-info}/WHEEL +1 -1
- {solara_ui-1.41.0.data → solara_ui-1.43.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
- {solara_ui-1.41.0.data → solara_ui-1.43.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
- {solara_ui-1.41.0.dist-info → solara_ui-1.43.0.dist-info}/licenses/LICENSE +0 -0
solara/__init__.py
CHANGED
solara/__main__.py
CHANGED
|
@@ -21,6 +21,8 @@ import solara.server.threaded
|
|
|
21
21
|
|
|
22
22
|
from .server import telemetry
|
|
23
23
|
|
|
24
|
+
print_mutex = threading.Lock()
|
|
25
|
+
|
|
24
26
|
try:
|
|
25
27
|
from solara_enterprise.ssg import ssg_crawl
|
|
26
28
|
except ImportError:
|
|
@@ -75,13 +77,15 @@ def _check_version():
|
|
|
75
77
|
import requests
|
|
76
78
|
|
|
77
79
|
try:
|
|
78
|
-
response = requests.get("https://pypi.org/pypi/solara/json")
|
|
80
|
+
response = requests.get("https://pypi.org/pypi/solara/json", timeout=0.5)
|
|
79
81
|
latest_version = response.json()["info"]["version"]
|
|
80
82
|
except: # noqa: E722
|
|
83
|
+
# in case of a firewall, or timeout, we just abort
|
|
81
84
|
return
|
|
82
85
|
if latest_version != solara.__version__:
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
with print_mutex:
|
|
87
|
+
print(f"New version of Solara available: {latest_version}. You have {solara.__version__}. Please upgrade using:") # noqa: T201
|
|
88
|
+
print(f'\t$ pip install "solara=={latest_version}"') # noqa: T201
|
|
85
89
|
|
|
86
90
|
|
|
87
91
|
def find_all_packages_paths():
|
|
@@ -306,7 +310,7 @@ def run(
|
|
|
306
310
|
print("solara: --reload is deprecated, use --auto-restart/-a instead", file=sys.stderr) # noqa: T201
|
|
307
311
|
auto_restart = reload
|
|
308
312
|
if check_version:
|
|
309
|
-
_check_version()
|
|
313
|
+
threading.Thread(target=_check_version, daemon=True).run()
|
|
310
314
|
|
|
311
315
|
# uvicorn calls it reload, we call it auto restart
|
|
312
316
|
reload = auto_restart
|
|
@@ -341,7 +345,13 @@ def run(
|
|
|
341
345
|
reload_excludes = restart_excludes if restart_excludes else []
|
|
342
346
|
del restart_excludes
|
|
343
347
|
reload_excludes = [str(solara_root / "website"), str(solara_root / "template")]
|
|
344
|
-
|
|
348
|
+
app_path = Path(app)
|
|
349
|
+
if app_path.exists():
|
|
350
|
+
# if app is not a child of the current working directory
|
|
351
|
+
# uvicorn crashes
|
|
352
|
+
if not str(app_path.resolve()).startswith(str(Path.cwd().resolve())):
|
|
353
|
+
reload_excludes.append(str(app_path.resolve()))
|
|
354
|
+
del app_path
|
|
345
355
|
del solara_root
|
|
346
356
|
reload = True
|
|
347
357
|
# avoid sending many restarts
|
|
@@ -384,7 +394,8 @@ def run(
|
|
|
384
394
|
if open and not qt:
|
|
385
395
|
threading.Thread(target=open_browser, daemon=True).start()
|
|
386
396
|
|
|
387
|
-
|
|
397
|
+
with print_mutex:
|
|
398
|
+
rich.print(f"Solara server is starting at {url}")
|
|
388
399
|
|
|
389
400
|
if log_level is not None:
|
|
390
401
|
LOGGING_CONFIG["loggers"]["solara"]["level"] = log_level.upper()
|
solara/_stores.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import dataclasses
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Callable, ContextManager, Generic, Optional, Union, cast
|
|
5
|
+
import warnings
|
|
6
|
+
from .toestand import ValueBase, KernelStore, S, _find_outside_solara_frame
|
|
7
|
+
import solara.util
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _PublicValueNotSet:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _SetValueNotSet:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclasses.dataclass
|
|
19
|
+
class StoreValue(Generic[S]):
|
|
20
|
+
private: S # the internal private value, should never be mutated
|
|
21
|
+
public: Union[S, _PublicValueNotSet] # this is the value that is exposed in .get(), it is a deep copy of private
|
|
22
|
+
get_traceback: Optional[inspect.Traceback]
|
|
23
|
+
set_value: Union[S, _SetValueNotSet] # the value that was set using .set(..), we deepcopy this to set private
|
|
24
|
+
set_traceback: Optional[inspect.Traceback]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MutateDetectorStore(ValueBase[S]):
|
|
28
|
+
def __init__(self, store: KernelStore[StoreValue[S]], equals=solara.util.equals_extra):
|
|
29
|
+
self._storage = store
|
|
30
|
+
self._enabled = True
|
|
31
|
+
super().__init__(equals=equals)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def lock(self):
|
|
35
|
+
return self._storage.lock
|
|
36
|
+
|
|
37
|
+
def get(self) -> S:
|
|
38
|
+
self.check_mutations()
|
|
39
|
+
self._ensure_public_exists()
|
|
40
|
+
value = self._storage.get()
|
|
41
|
+
# value.public is of type Optional[S], so it's tempting to check for None here,
|
|
42
|
+
# but S could include None as a valid value, so best we can do is cast
|
|
43
|
+
public_value = cast(S, value.public)
|
|
44
|
+
return public_value
|
|
45
|
+
|
|
46
|
+
def peek(self) -> S:
|
|
47
|
+
"""Return the value without automatically subscribing to listeners."""
|
|
48
|
+
self.check_mutations()
|
|
49
|
+
store_value = self._storage.peek()
|
|
50
|
+
self._ensure_public_exists()
|
|
51
|
+
public_value = cast(S, store_value.public)
|
|
52
|
+
return public_value
|
|
53
|
+
|
|
54
|
+
def set(self, value: S):
|
|
55
|
+
self.check_mutations()
|
|
56
|
+
self._ensure_public_exists()
|
|
57
|
+
private = copy.deepcopy(value)
|
|
58
|
+
self._check_equals(private, value)
|
|
59
|
+
frame = _find_outside_solara_frame()
|
|
60
|
+
if frame is not None:
|
|
61
|
+
frame_info = inspect.getframeinfo(frame)
|
|
62
|
+
else:
|
|
63
|
+
frame_info = None
|
|
64
|
+
store_value = StoreValue(private=private, public=_PublicValueNotSet(), get_traceback=None, set_value=value, set_traceback=frame_info)
|
|
65
|
+
self._storage.set(store_value)
|
|
66
|
+
|
|
67
|
+
def check_mutations(self):
|
|
68
|
+
self._storage._check_mutation()
|
|
69
|
+
if not self._enabled:
|
|
70
|
+
return
|
|
71
|
+
store_value = self._storage.peek()
|
|
72
|
+
if not isinstance(store_value.public, _PublicValueNotSet) and not self.equals(store_value.public, store_value.private):
|
|
73
|
+
tb = store_value.get_traceback
|
|
74
|
+
# TODO: make the error message as elaborate as below
|
|
75
|
+
msg = (
|
|
76
|
+
f"Reactive variable was read when it had the value of {store_value.private!r}, but was later mutated to {store_value.public!r}.\n"
|
|
77
|
+
"Mutation should not be done on the value of a reactive variable, as in production mode we would be unable to track changes.\n"
|
|
78
|
+
)
|
|
79
|
+
if tb:
|
|
80
|
+
if tb.code_context:
|
|
81
|
+
code = tb.code_context[0]
|
|
82
|
+
else:
|
|
83
|
+
code = "<No code context available>"
|
|
84
|
+
msg += f"The last value was read in the following code:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
|
|
85
|
+
raise ValueError(msg)
|
|
86
|
+
elif not isinstance(store_value.set_value, _SetValueNotSet) and not self.equals(store_value.set_value, store_value.private):
|
|
87
|
+
tb = store_value.set_traceback
|
|
88
|
+
msg = f"""Reactive variable was set with a value of {store_value.private!r}, but was later mutated mutated to {store_value.set_value!r}.
|
|
89
|
+
|
|
90
|
+
Mutation should not be done on the value of a reactive variable, as in production mode we would be unable to track changes.
|
|
91
|
+
|
|
92
|
+
Bad:
|
|
93
|
+
mylist = reactive([]]
|
|
94
|
+
some_values = [1, 2, 3]
|
|
95
|
+
mylist.value = some_values # you give solara a reference to your list
|
|
96
|
+
some_values.append(4) # but later mutate it (solara cannot detect this change, so a render will not be triggered)
|
|
97
|
+
# if later on a re-render happens for a different reason, you will read of the mutated list.
|
|
98
|
+
|
|
99
|
+
Good (if you want the reactive variable to be updated):
|
|
100
|
+
mylist = reactive([]]
|
|
101
|
+
some_values = [1, 2, 3]
|
|
102
|
+
mylist.value = some_values
|
|
103
|
+
mylist.value = some_values + [4]
|
|
104
|
+
|
|
105
|
+
Good (if you want to keep mutating your own list):
|
|
106
|
+
mylist = reactive([]]
|
|
107
|
+
some_values = [1, 2, 3]
|
|
108
|
+
mylist.value = some_values.copy() # this gives solara a copy of the list
|
|
109
|
+
some_values.append(4) # you are free to mutate your own list, solara will not see this
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
if tb:
|
|
113
|
+
if tb.code_context:
|
|
114
|
+
code = tb.code_context[0]
|
|
115
|
+
else:
|
|
116
|
+
code = "<No code context available>"
|
|
117
|
+
msg += "The last time the value was set was at:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
|
|
118
|
+
raise ValueError(msg)
|
|
119
|
+
|
|
120
|
+
def _ensure_public_exists(self):
|
|
121
|
+
store_value = self._storage.peek()
|
|
122
|
+
if isinstance(store_value.public, _PublicValueNotSet):
|
|
123
|
+
with self.lock:
|
|
124
|
+
if isinstance(store_value.public, _PublicValueNotSet):
|
|
125
|
+
frame = _find_outside_solara_frame()
|
|
126
|
+
if frame is not None:
|
|
127
|
+
frame_info = inspect.getframeinfo(frame)
|
|
128
|
+
else:
|
|
129
|
+
frame_info = None
|
|
130
|
+
store_value.public = copy.deepcopy(store_value.private)
|
|
131
|
+
self._check_equals(store_value.public, store_value.private)
|
|
132
|
+
store_value.get_traceback = frame_info
|
|
133
|
+
|
|
134
|
+
def _check_equals(self, a: S, b: S):
|
|
135
|
+
if not self._enabled:
|
|
136
|
+
return
|
|
137
|
+
if not self.equals(a, b):
|
|
138
|
+
frame = _find_outside_solara_frame()
|
|
139
|
+
if frame is not None:
|
|
140
|
+
frame_info = inspect.getframeinfo(frame)
|
|
141
|
+
else:
|
|
142
|
+
frame_info = None
|
|
143
|
+
|
|
144
|
+
warn = """The equals function for this reactive value returned False when comparing a deepcopy to itself.
|
|
145
|
+
|
|
146
|
+
This reactive variable will not be able to detect mutations correctly, and is therefore disabled.
|
|
147
|
+
|
|
148
|
+
To avoid this warning, and to ensure that mutation detection works correctly, please provide a better equals function to the reactive variable.
|
|
149
|
+
A good choice for dataframes and numpy arrays might be solara.util.equals_pickle, which will also attempt to compare the pickled values of the objects.
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
|
|
153
|
+
reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
|
|
154
|
+
"""
|
|
155
|
+
tb = frame_info
|
|
156
|
+
if tb:
|
|
157
|
+
if tb.code_context:
|
|
158
|
+
code = tb.code_context[0]
|
|
159
|
+
else:
|
|
160
|
+
code = "<No code context available>"
|
|
161
|
+
warn += "This warning was triggered from:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
|
|
162
|
+
warnings.warn(warn)
|
|
163
|
+
self._enabled = False
|
|
164
|
+
|
|
165
|
+
def subscribe(self, listener: Callable[[S], None], scope: Optional[ContextManager] = None):
|
|
166
|
+
def listener_wrapper(new: StoreValue[S], previous: StoreValue[S]):
|
|
167
|
+
self._ensure_public_exists()
|
|
168
|
+
assert not isinstance(new.public, _PublicValueNotSet)
|
|
169
|
+
assert not isinstance(previous.public, _PublicValueNotSet)
|
|
170
|
+
previous_value = previous.set_value if not isinstance(previous.set_value, _SetValueNotSet) else previous.private
|
|
171
|
+
new_value = new.set_value
|
|
172
|
+
assert not isinstance(new_value, _SetValueNotSet)
|
|
173
|
+
if not self.equals(new_value, previous_value):
|
|
174
|
+
listener(new_value)
|
|
175
|
+
|
|
176
|
+
return self._storage.subscribe_change(listener_wrapper, scope=scope)
|
|
177
|
+
|
|
178
|
+
def subscribe_change(self, listener: Callable[[S, S], None], scope: Optional[ContextManager] = None):
|
|
179
|
+
def listener_wrapper(new: StoreValue[S], previous: StoreValue[S]):
|
|
180
|
+
self._ensure_public_exists()
|
|
181
|
+
assert not isinstance(new.public, _PublicValueNotSet)
|
|
182
|
+
assert not isinstance(previous.public, _PublicValueNotSet)
|
|
183
|
+
previous_value = previous.set_value if not isinstance(previous.set_value, _SetValueNotSet) else previous.private
|
|
184
|
+
new_value = new.set_value
|
|
185
|
+
assert not isinstance(new_value, _SetValueNotSet)
|
|
186
|
+
if not self.equals(new_value, previous_value):
|
|
187
|
+
listener(new_value, previous_value)
|
|
188
|
+
|
|
189
|
+
return self._storage.subscribe_change(listener_wrapper, scope=scope)
|
solara/components/__init__.py
CHANGED
|
@@ -57,4 +57,21 @@ from .progress import ProgressLinear # noqa: F401 F403
|
|
|
57
57
|
from .component_vue import _component_vue, component_vue # noqa: F401 F403
|
|
58
58
|
import reacton.core
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
try:
|
|
61
|
+
from reacton import Fragment as Fragment # type: ignore
|
|
62
|
+
except ImportError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
import logging
|
|
66
|
+
from ..settings import main
|
|
67
|
+
|
|
68
|
+
_container = None
|
|
69
|
+
|
|
70
|
+
if main.default_container in globals():
|
|
71
|
+
_container = globals()[main.default_container]
|
|
72
|
+
else:
|
|
73
|
+
logger = logging.getLogger("solara.components")
|
|
74
|
+
logger.warning(f"Default container {main.default_container} not found in solara.components. Defaulting to Column.")
|
|
75
|
+
|
|
76
|
+
# TODO: When Solara 2.0 releases Column should be replaced with Fragment
|
|
77
|
+
reacton.core._default_container = _container or Column # noqa: F405
|
|
@@ -82,6 +82,29 @@ def component_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]],
|
|
|
82
82
|
|
|
83
83
|
See the [Vue component example](/documentation/examples/general/vue_component) for an example of how to use this decorator.
|
|
84
84
|
|
|
85
|
+
|
|
86
|
+
## Examples
|
|
87
|
+
|
|
88
|
+
A component that takes a `foo` argument and an `on_foo` callback that gets called when `foo` changes (from the frontend).
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import solara
|
|
92
|
+
|
|
93
|
+
@solara.component_vue("my_foo_component.vue")
|
|
94
|
+
def MyFooComponent(foo: int, on_foo: Callable[[int], None]):
|
|
95
|
+
pass
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The following component only takes in a month argument and an event_date_clicked callback that gets called from
|
|
99
|
+
the vue template using `this.date_clicked({'extra-data': 42, 'day': this.day})`.
|
|
100
|
+
```python
|
|
101
|
+
import solara
|
|
102
|
+
|
|
103
|
+
@solara.component_vue("my_date_component.vue")
|
|
104
|
+
def MyDateComponent(month: int, event_date_clicked: Callable):
|
|
105
|
+
pass
|
|
106
|
+
```
|
|
107
|
+
|
|
85
108
|
## Arguments
|
|
86
109
|
|
|
87
110
|
* `vue_path`: The path to the Vue template file.
|
solara/components/datatable.py
CHANGED
|
@@ -10,14 +10,14 @@ import solara
|
|
|
10
10
|
import solara.hooks.dataframe
|
|
11
11
|
import solara.lab
|
|
12
12
|
import traitlets
|
|
13
|
-
from solara.lab.hooks.dataframe import use_df_column_names
|
|
13
|
+
from solara.lab.hooks.dataframe import use_df_column_names, df_row_names
|
|
14
14
|
from solara.lab.utils.dataframe import df_len, df_records, df_slice
|
|
15
15
|
|
|
16
16
|
from .. import CellAction, ColumnAction
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def _ensure_dict(d):
|
|
20
|
-
if dataclasses.is_dataclass(d):
|
|
20
|
+
if dataclasses.is_dataclass(d) and not isinstance(d, type): # is_dataclass also returns True for dataclass type, rather than instance
|
|
21
21
|
return dataclasses.asdict(d)
|
|
22
22
|
return d
|
|
23
23
|
|
|
@@ -100,12 +100,12 @@ def DataTable(
|
|
|
100
100
|
i2 = min(total_length, (page + 1) * items_per_page)
|
|
101
101
|
|
|
102
102
|
columns = use_df_column_names(df)
|
|
103
|
-
|
|
103
|
+
rows = df_row_names(df)
|
|
104
104
|
items = []
|
|
105
105
|
dfs = df_slice(df, i1, i2)
|
|
106
106
|
records = df_records(dfs)
|
|
107
107
|
for i in range(i2 - i1):
|
|
108
|
-
item = {"__row__": i + i1} # special key for the row number
|
|
108
|
+
item = {"__row__": format(dfs, columns, i + 1, rows[i + i1])} # special key for the row number
|
|
109
109
|
for column in columns:
|
|
110
110
|
item[column] = format(dfs, column, i + i1, records[i][column])
|
|
111
111
|
items.append(item)
|
solara/components/echarts.py
CHANGED
|
@@ -10,7 +10,7 @@ class EchartsWidget(ipyvuetify.VuetifyTemplate):
|
|
|
10
10
|
template_file = (__file__, "echarts.vue")
|
|
11
11
|
|
|
12
12
|
attributes = traitlets.Dict(default_value=None, allow_none=True).tag(sync=True)
|
|
13
|
-
|
|
13
|
+
responsive = traitlets.Bool(False).tag(sync=True)
|
|
14
14
|
maps = traitlets.Any({}).tag(sync=True)
|
|
15
15
|
option = traitlets.Any({}).tag(sync=True)
|
|
16
16
|
on_click = traitlets.Callable(None, allow_none=True)
|
|
@@ -49,7 +49,8 @@ def FigureEcharts(
|
|
|
49
49
|
on_mouseover: Callable[[Any], Any] = None,
|
|
50
50
|
on_mouseout: Callable[[Any], Any] = None,
|
|
51
51
|
maps: dict = {},
|
|
52
|
-
attributes={"style": "height: 400px"},
|
|
52
|
+
attributes={"style": "height: 400px;"},
|
|
53
|
+
responsive: bool = False,
|
|
53
54
|
):
|
|
54
55
|
"""Create a Echarts figure.
|
|
55
56
|
|
|
@@ -68,6 +69,7 @@ def FigureEcharts(
|
|
|
68
69
|
* on_mouseout: Callable, a function that will be called when the user moves the mouse out of a certain component.
|
|
69
70
|
* maps: dict, a dictionary of maps to be used in the figure.
|
|
70
71
|
* attributes: dict, a dictionary of attributes to be passed to the container (like style, class).
|
|
72
|
+
* responsive: bool, whether the chart should resize when the container changes size.
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
"""
|
|
@@ -80,4 +82,5 @@ def FigureEcharts(
|
|
|
80
82
|
on_mouseout=on_mouseout,
|
|
81
83
|
on_mouseout_enabled=on_mouseout is not None,
|
|
82
84
|
attributes=attributes,
|
|
85
|
+
responsive=responsive,
|
|
83
86
|
)
|
solara/components/echarts.vue
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<div ref="echarts" class="solara-echarts" v-bind="attributes"></div>
|
|
4
|
-
</div>
|
|
2
|
+
<div ref="echarts" class="solara-echarts" v-bind="attributes"></div>
|
|
5
3
|
</template>
|
|
6
4
|
<script>
|
|
7
5
|
module.exports = {
|
|
@@ -14,6 +12,22 @@ module.exports = {
|
|
|
14
12
|
this.echarts = echarts;
|
|
15
13
|
this.create();
|
|
16
14
|
})();
|
|
15
|
+
if(this.responsive){
|
|
16
|
+
this.resizeObserver = new ResizeObserver(entries => {
|
|
17
|
+
for (let entry of entries) {
|
|
18
|
+
if (entry.target === this.$refs.echarts) {
|
|
19
|
+
this.handleContainerResize();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
this.resizeObserver.observe(this.$refs.echarts);
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
beforeDestroy() {
|
|
27
|
+
if (this.resizeObserver) {
|
|
28
|
+
this.resizeObserver.unobserve(this.$refs.echarts);
|
|
29
|
+
this.resizeObserver.disconnect();
|
|
30
|
+
}
|
|
17
31
|
},
|
|
18
32
|
watch: {
|
|
19
33
|
option() {
|
|
@@ -25,9 +39,7 @@ module.exports = {
|
|
|
25
39
|
methods: {
|
|
26
40
|
create() {
|
|
27
41
|
this.chart = this.echarts.init(this.$refs.echarts);
|
|
28
|
-
console.log(this.maps);
|
|
29
42
|
Object.keys(this.maps).forEach((mapName) => {
|
|
30
|
-
console.log(mapName);
|
|
31
43
|
this.echarts.registerMap(mapName, this.maps[mapName]);
|
|
32
44
|
});
|
|
33
45
|
|
|
@@ -66,6 +78,11 @@ module.exports = {
|
|
|
66
78
|
if (this.on_mouseout_enabled) this.on_mouseout(eventData);
|
|
67
79
|
});
|
|
68
80
|
},
|
|
81
|
+
handleContainerResize() {
|
|
82
|
+
if (this.chart) {
|
|
83
|
+
this.chart.resize();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
69
86
|
import(deps) {
|
|
70
87
|
return this.loadRequire().then(() => {
|
|
71
88
|
if (window.jupyterVue) {
|
solara/components/file_drop.py
CHANGED
|
@@ -111,6 +111,26 @@ def FileDrop(
|
|
|
111
111
|
* `lazy`: Whether to load the file contents into memory or not. If `False`,
|
|
112
112
|
the file contents will be loaded into memory via the `.data` attribute of file object(s).
|
|
113
113
|
|
|
114
|
+
## Load into Pandas
|
|
115
|
+
To load the data into a Pandas DF, set `lazy=False` and use `file['file_obj']` (be careful of memory)<br>
|
|
116
|
+
You can run this directly in your Jupyter notebook
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import io
|
|
120
|
+
import pandas as pd
|
|
121
|
+
import solara
|
|
122
|
+
|
|
123
|
+
@solara.component
|
|
124
|
+
def Page():
|
|
125
|
+
def load_file_df(file):
|
|
126
|
+
df = pd.read_csv(file["file_obj"])
|
|
127
|
+
print("Loaded dataframe:")
|
|
128
|
+
print(df)
|
|
129
|
+
|
|
130
|
+
solara.FileDrop(label="Drop file to see dataframe!", on_file=load_file_df)
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
|
|
114
134
|
"""
|
|
115
135
|
|
|
116
136
|
return _FileDrop(label=label, on_total_progress=on_total_progress, on_file=on_file, lazy=lazy, multiple=False)
|
solara/components/input.py
CHANGED
|
@@ -49,6 +49,7 @@ def InputText(
|
|
|
49
49
|
message: Optional[str] = None,
|
|
50
50
|
classes: List[str] = [],
|
|
51
51
|
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
52
|
+
autofocus: bool = False,
|
|
52
53
|
):
|
|
53
54
|
"""Free form text input.
|
|
54
55
|
|
|
@@ -105,6 +106,7 @@ def InputText(
|
|
|
105
106
|
* `message`: Message to show below the input. If `error` is a string, this will be ignored.
|
|
106
107
|
* `classes`: List of CSS classes to apply to the input.
|
|
107
108
|
* `style`: CSS style to apply to the input.
|
|
109
|
+
* `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur during page load and only one component per page can have autofocus active.
|
|
108
110
|
"""
|
|
109
111
|
reactive_value = solara.use_reactive(value, on_value)
|
|
110
112
|
del value, on_value
|
|
@@ -133,6 +135,7 @@ def InputText(
|
|
|
133
135
|
messages=messages,
|
|
134
136
|
class_=classes_flat,
|
|
135
137
|
style_=style_flat,
|
|
138
|
+
autofocus=autofocus,
|
|
136
139
|
)
|
|
137
140
|
use_change(text_field, set_value_cast, enabled=not continuous_update, update_events=update_events)
|
|
138
141
|
return text_field
|
|
@@ -150,6 +153,7 @@ def InputFloat(
|
|
|
150
153
|
clearable: bool = ...,
|
|
151
154
|
classes: List[str] = ...,
|
|
152
155
|
style: Optional[Union[str, Dict[str, str]]] = ...,
|
|
156
|
+
autofocus: bool = False,
|
|
153
157
|
) -> reacton.core.ValueElement[vw.TextField, Any]: ...
|
|
154
158
|
|
|
155
159
|
|
|
@@ -165,6 +169,7 @@ def InputFloat(
|
|
|
165
169
|
clearable: bool = ...,
|
|
166
170
|
classes: List[str] = ...,
|
|
167
171
|
style: Optional[Union[str, Dict[str, str]]] = ...,
|
|
172
|
+
autofocus: bool = False,
|
|
168
173
|
) -> reacton.core.ValueElement[vw.TextField, Any]: ...
|
|
169
174
|
|
|
170
175
|
|
|
@@ -179,6 +184,7 @@ def InputFloat(
|
|
|
179
184
|
clearable: bool = False,
|
|
180
185
|
classes: List[str] = [],
|
|
181
186
|
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
187
|
+
autofocus: bool = False,
|
|
182
188
|
):
|
|
183
189
|
"""Numeric input (floats).
|
|
184
190
|
|
|
@@ -211,6 +217,7 @@ def InputFloat(
|
|
|
211
217
|
* `clearable`: Whether the input can be cleared.
|
|
212
218
|
* `classes`: List of CSS classes to apply to the input.
|
|
213
219
|
* `style`: CSS style to apply to the input.
|
|
220
|
+
* `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur either during page load, or when the component becomes visible (for example, dialog being opened). Only one component per page should have autofocus on each such event.
|
|
214
221
|
|
|
215
222
|
"""
|
|
216
223
|
|
|
@@ -237,6 +244,7 @@ def InputFloat(
|
|
|
237
244
|
clearable=clearable,
|
|
238
245
|
classes=classes,
|
|
239
246
|
style=style,
|
|
247
|
+
autofocus=autofocus,
|
|
240
248
|
)
|
|
241
249
|
|
|
242
250
|
|
|
@@ -252,6 +260,7 @@ def InputInt(
|
|
|
252
260
|
clearable: bool = ...,
|
|
253
261
|
classes: List[str] = ...,
|
|
254
262
|
style: Optional[Union[str, Dict[str, str]]] = ...,
|
|
263
|
+
autofocus: bool = False,
|
|
255
264
|
) -> reacton.core.ValueElement[vw.TextField, Any]: ...
|
|
256
265
|
|
|
257
266
|
|
|
@@ -267,6 +276,7 @@ def InputInt(
|
|
|
267
276
|
clearable: bool = ...,
|
|
268
277
|
classes: List[str] = ...,
|
|
269
278
|
style: Optional[Union[str, Dict[str, str]]] = ...,
|
|
279
|
+
autofocus: bool = False,
|
|
270
280
|
) -> reacton.core.ValueElement[vw.TextField, Any]: ...
|
|
271
281
|
|
|
272
282
|
|
|
@@ -281,6 +291,7 @@ def InputInt(
|
|
|
281
291
|
clearable: bool = False,
|
|
282
292
|
classes: List[str] = [],
|
|
283
293
|
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
294
|
+
autofocus: bool = False,
|
|
284
295
|
):
|
|
285
296
|
"""Numeric input (integers).
|
|
286
297
|
|
|
@@ -312,6 +323,7 @@ def InputInt(
|
|
|
312
323
|
* `clearable`: Whether the input can be cleared.
|
|
313
324
|
* `classes`: List of CSS classes to apply to the input.
|
|
314
325
|
* `style`: CSS style to apply to the input.
|
|
326
|
+
* `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur either during page load, or when the component becomes visible (for example, dialog being opened). Only one component per page should have autofocus on each such event.
|
|
315
327
|
"""
|
|
316
328
|
|
|
317
329
|
def str_to_int(value: Optional[str]):
|
|
@@ -336,6 +348,7 @@ def InputInt(
|
|
|
336
348
|
clearable=clearable,
|
|
337
349
|
classes=classes,
|
|
338
350
|
style=style,
|
|
351
|
+
autofocus=autofocus,
|
|
339
352
|
)
|
|
340
353
|
|
|
341
354
|
|
|
@@ -360,6 +373,10 @@ def _use_input_type(
|
|
|
360
373
|
error_message = str(e.args[0])
|
|
361
374
|
|
|
362
375
|
def sync_back_input_value():
|
|
376
|
+
# Make sure we update string_value when the effect is rerun,
|
|
377
|
+
# Since the parsing & stringigying functions might have changed
|
|
378
|
+
set_string_value(stringify(reactive_value.value) if reactive_value.value is not None else None)
|
|
379
|
+
|
|
363
380
|
def on_external_value_change(new_value: Optional[T]):
|
|
364
381
|
new_string_value = stringify(new_value)
|
|
365
382
|
try:
|
|
@@ -373,7 +390,7 @@ def _use_input_type(
|
|
|
373
390
|
|
|
374
391
|
return reactive_value.subscribe(on_external_value_change)
|
|
375
392
|
|
|
376
|
-
solara.use_effect(sync_back_input_value, [reactive_value])
|
|
393
|
+
solara.use_effect(sync_back_input_value, [reactive_value, parse, stringify])
|
|
377
394
|
|
|
378
395
|
return string_value, error_message, set_string_value
|
|
379
396
|
|
|
@@ -389,6 +406,7 @@ def _InputNumeric(
|
|
|
389
406
|
clearable: bool = False,
|
|
390
407
|
classes: List[str] = [],
|
|
391
408
|
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
409
|
+
autofocus: bool = False,
|
|
392
410
|
):
|
|
393
411
|
"""Numeric input.
|
|
394
412
|
|
|
@@ -401,6 +419,7 @@ def _InputNumeric(
|
|
|
401
419
|
* `continuous_update`: Whether to call the `on_value` callback on every change or only when the input loses focus or the enter key is pressed.
|
|
402
420
|
* `classes`: List of CSS classes to apply to the input.
|
|
403
421
|
* `style`: CSS style to apply to the input.
|
|
422
|
+
* `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur either during page load, or when the component becomes visible (for example, dialog being opened). Only one component per page should have autofocus on each such event.
|
|
404
423
|
"""
|
|
405
424
|
style_flat = solara.util._flatten_style(style)
|
|
406
425
|
classes_flat = solara.util._combine_classes(classes)
|
|
@@ -431,6 +450,7 @@ def _InputNumeric(
|
|
|
431
450
|
error=bool(error),
|
|
432
451
|
class_=classes_flat,
|
|
433
452
|
style_=style_flat,
|
|
453
|
+
autofocus=autofocus,
|
|
434
454
|
)
|
|
435
455
|
use_change(text_field, set_value_cast, enabled=not continuous_update)
|
|
436
456
|
return text_field
|