supervisely 6.73.265__py3-none-any.whl → 6.73.267__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.
Potentially problematic release.
This version of supervisely might be problematic. Click here for more details.
- supervisely/app/widgets/bokeh/bokeh.py +174 -16
- {supervisely-6.73.265.dist-info → supervisely-6.73.267.dist-info}/METADATA +1 -1
- {supervisely-6.73.265.dist-info → supervisely-6.73.267.dist-info}/RECORD +7 -7
- {supervisely-6.73.265.dist-info → supervisely-6.73.267.dist-info}/LICENSE +0 -0
- {supervisely-6.73.265.dist-info → supervisely-6.73.267.dist-info}/WHEEL +0 -0
- {supervisely-6.73.265.dist-info → supervisely-6.73.267.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.265.dist-info → supervisely-6.73.267.dist-info}/top_level.txt +0 -0
|
@@ -1,23 +1,96 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import re
|
|
4
5
|
from abc import ABC, abstractmethod
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
from typing import Any, Callable, Dict, List, Literal, Optional, Union
|
|
8
|
+
from uuid import uuid4
|
|
7
9
|
|
|
8
10
|
from fastapi.responses import HTMLResponse
|
|
9
11
|
from pydantic import BaseModel
|
|
10
|
-
|
|
12
|
+
|
|
11
13
|
from supervisely._utils import is_production
|
|
12
|
-
from supervisely.io.env import task_id
|
|
13
14
|
from supervisely.api.api import Api
|
|
15
|
+
from supervisely.app.widgets import Widget
|
|
16
|
+
from supervisely.io.env import task_id
|
|
17
|
+
from supervisely.sly_logger import logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DebouncedEventHandler:
|
|
21
|
+
def __init__(self, debounce_time: float = 0.1):
|
|
22
|
+
self._event_queue = []
|
|
23
|
+
self._debounce_time = debounce_time
|
|
24
|
+
self._task = None
|
|
25
|
+
|
|
26
|
+
async def _process_events(self, func: Callable):
|
|
27
|
+
await asyncio.sleep(self._debounce_time)
|
|
28
|
+
aggregated_events = self._event_queue.copy()
|
|
29
|
+
self._event_queue.clear()
|
|
30
|
+
func(aggregated_events)
|
|
31
|
+
|
|
32
|
+
def handle_event(self, event_data, func: Callable):
|
|
33
|
+
self._event_queue.append(event_data)
|
|
34
|
+
if self._task is None or self._task.done():
|
|
35
|
+
self._task = asyncio.create_task(self._process_events(func))
|
|
14
36
|
|
|
15
37
|
|
|
16
38
|
class SelectedIds(BaseModel):
|
|
17
|
-
selected_ids: List[
|
|
39
|
+
selected_ids: List[int]
|
|
40
|
+
plot_id: Optional[Union[str, int]] = None
|
|
18
41
|
|
|
19
42
|
|
|
20
43
|
class Bokeh(Widget):
|
|
44
|
+
"""
|
|
45
|
+
Bokeh widget for creating interactive plots.
|
|
46
|
+
|
|
47
|
+
Note:
|
|
48
|
+
Only Bokeh version 3.1.1 is supported.
|
|
49
|
+
|
|
50
|
+
:param plots: List of plots to be displayed.
|
|
51
|
+
:type plots: List[Plot]
|
|
52
|
+
:param width: Width of the chart in pixels.
|
|
53
|
+
:type width: int
|
|
54
|
+
:param height: Height of the chart in pixels.
|
|
55
|
+
:type height: int
|
|
56
|
+
:param tools: List of tools to be displayed on the chart.
|
|
57
|
+
:type tools: List[str]
|
|
58
|
+
:param toolbar_location: Location of the toolbar.
|
|
59
|
+
:type toolbar_location: Literal["above", "below", "left", "right"]
|
|
60
|
+
:param x_axis_visible: If True, x-axis will be visible.
|
|
61
|
+
:type x_axis_visible: bool
|
|
62
|
+
:param y_axis_visible: If True, y-axis will be visible.
|
|
63
|
+
:type y_axis_visible: bool
|
|
64
|
+
:param grid_visible: If True, grid will be visible.
|
|
65
|
+
:type grid_visible: bool
|
|
66
|
+
:param widget_id: Unique widget identifier.
|
|
67
|
+
:type widget_id: str
|
|
68
|
+
:param show_legend: If True, ckickable legend widget will be displayed.
|
|
69
|
+
:type show_legend: bool
|
|
70
|
+
:param legend_location: Location of the clickable legend widget.
|
|
71
|
+
:type legend_location: Literal["left", "top", "right", "bottom"]
|
|
72
|
+
:param legend_click_policy: Click policy of the clickable legend widget.
|
|
73
|
+
:type legend_click_policy: Literal["hide", "mute"]
|
|
74
|
+
|
|
75
|
+
:Usage example:
|
|
76
|
+
.. code-block:: python
|
|
77
|
+
|
|
78
|
+
from supervisely.app.widgets import Bokeh, IFrame
|
|
79
|
+
|
|
80
|
+
plot = Bokeh.Circle(
|
|
81
|
+
x_coordinates=[1, 2, 3, 4, 5],
|
|
82
|
+
y_coordinates=[1, 2, 3, 4, 5],
|
|
83
|
+
radii=10,
|
|
84
|
+
colors="red",
|
|
85
|
+
legend_label="Circle plot",
|
|
86
|
+
)
|
|
87
|
+
bokeh = Bokeh(plots=[plot], width=1000, height=600)
|
|
88
|
+
|
|
89
|
+
# To allow the widget to be interacted with, you need to add it to the IFrame widget.
|
|
90
|
+
iframe = IFrame()
|
|
91
|
+
iframe.set(bokeh.html_route_with_timestamp, height="650px", width="100%")
|
|
92
|
+
"""
|
|
93
|
+
|
|
21
94
|
class Routes:
|
|
22
95
|
VALUE_CHANGED = "value_changed"
|
|
23
96
|
HTML_ROUTE = "bokeh.html"
|
|
@@ -42,6 +115,8 @@ class Bokeh(Widget):
|
|
|
42
115
|
dynamic_selection: bool = False,
|
|
43
116
|
fill_alpha: float = 0.5,
|
|
44
117
|
line_color: Optional[str] = None,
|
|
118
|
+
legend_label: Optional[str] = None,
|
|
119
|
+
plot_id: Optional[Union[str, int]] = None,
|
|
45
120
|
):
|
|
46
121
|
if not colors:
|
|
47
122
|
colors = Bokeh._generate_colors(x_coordinates, y_coordinates)
|
|
@@ -73,6 +148,8 @@ class Bokeh(Widget):
|
|
|
73
148
|
self._dynamic_selection = dynamic_selection
|
|
74
149
|
self._fill_alpha = fill_alpha
|
|
75
150
|
self._line_color = line_color
|
|
151
|
+
self._plot_id = plot_id or uuid4().hex
|
|
152
|
+
self._legend_label = legend_label or str(self._plot_id)
|
|
76
153
|
|
|
77
154
|
def add(self, plot) -> None:
|
|
78
155
|
from bokeh.models import ( # pylint: disable=import-error
|
|
@@ -97,6 +174,7 @@ class Bokeh(Widget):
|
|
|
97
174
|
fill_alpha=self._fill_alpha,
|
|
98
175
|
line_color=self._line_color,
|
|
99
176
|
source=self._source,
|
|
177
|
+
legend_label=self._legend_label,
|
|
100
178
|
)
|
|
101
179
|
if not self._dynamic_selection:
|
|
102
180
|
for tool in plot.tools:
|
|
@@ -127,9 +205,10 @@ class Bokeh(Widget):
|
|
|
127
205
|
var xhr = new XMLHttpRequest();
|
|
128
206
|
xhr.open("POST", "{route_path}", true);
|
|
129
207
|
xhr.setRequestHeader("Content-Type", "application/json");
|
|
130
|
-
xhr.send(JSON.stringify({{selected_ids: selected_ids}}));
|
|
208
|
+
xhr.send(JSON.stringify({{selected_ids: selected_ids, plot_id: '{plot_id}'}}));
|
|
131
209
|
""".format(
|
|
132
|
-
route_path=route_path
|
|
210
|
+
route_path=route_path,
|
|
211
|
+
plot_id=self._plot_id,
|
|
133
212
|
),
|
|
134
213
|
)
|
|
135
214
|
self._source.selected.js_on_change("indices", callback)
|
|
@@ -154,22 +233,33 @@ class Bokeh(Widget):
|
|
|
154
233
|
y_axis_visible: bool = False,
|
|
155
234
|
grid_visible: bool = False,
|
|
156
235
|
widget_id: Optional[str] = None,
|
|
236
|
+
show_legend: bool = False,
|
|
237
|
+
legend_location: Literal["left", "top", "right", "bottom"] = "right",
|
|
238
|
+
legend_click_policy: Literal["hide", "mute"] = "hide",
|
|
157
239
|
**kwargs,
|
|
158
240
|
):
|
|
159
|
-
|
|
241
|
+
import bokeh # pylint: disable=import-error
|
|
242
|
+
|
|
243
|
+
# check Bokeh version compatibility (only 3.1.1 is supported)
|
|
244
|
+
if bokeh.__version__ != "3.1.1":
|
|
245
|
+
raise RuntimeError(f"Bokeh version {bokeh.__version__} is not supported. Use 3.1.1")
|
|
160
246
|
|
|
161
247
|
self.widget_id = widget_id
|
|
162
248
|
self._plots = plots
|
|
163
|
-
self._plot = figure(width=width, height=height, tools=tools, toolbar_location="above")
|
|
164
|
-
self._renderers = []
|
|
165
249
|
|
|
166
|
-
self.
|
|
167
|
-
self.
|
|
168
|
-
self.
|
|
169
|
-
|
|
250
|
+
self._width = width
|
|
251
|
+
self._height = height
|
|
252
|
+
self._tools = tools
|
|
253
|
+
self._toolbar_location = toolbar_location
|
|
254
|
+
self._x_axis_visible = x_axis_visible
|
|
255
|
+
self._y_axis_visible = y_axis_visible
|
|
256
|
+
self._grid_visible = grid_visible
|
|
257
|
+
self._show_legend = show_legend
|
|
258
|
+
self._legend_location = legend_location
|
|
259
|
+
self._legend_click_policy = legend_click_policy
|
|
170
260
|
|
|
171
|
-
|
|
172
|
-
self.
|
|
261
|
+
super().__init__(widget_id=widget_id, file_path=__file__)
|
|
262
|
+
self._load_chart()
|
|
173
263
|
|
|
174
264
|
server = self._sly_app.get_server()
|
|
175
265
|
|
|
@@ -177,8 +267,45 @@ class Bokeh(Widget):
|
|
|
177
267
|
def _html_response() -> None:
|
|
178
268
|
return HTMLResponse(content=self.get_html())
|
|
179
269
|
|
|
270
|
+
# TODO: support for offline mode
|
|
180
271
|
# JinjaWidgets().context.pop(self.widget_id, None) # remove the widget from index.html
|
|
181
272
|
|
|
273
|
+
def _load_chart(self, **kwargs):
|
|
274
|
+
from bokeh.models import Legend # pylint: disable=import-error
|
|
275
|
+
from bokeh.plotting import figure # pylint: disable=import-error
|
|
276
|
+
|
|
277
|
+
self._width = kwargs.get("width", self._width)
|
|
278
|
+
self._height = kwargs.get("height", self._height)
|
|
279
|
+
self._tools = kwargs.get("tools", self._tools)
|
|
280
|
+
self._toolbar_location = kwargs.get("toolbar_location", self._toolbar_location)
|
|
281
|
+
self._show_legend = kwargs.get("show_legend", self._show_legend)
|
|
282
|
+
self._legend_location = kwargs.get("legend_location", self._legend_location)
|
|
283
|
+
self._legend_click_policy = kwargs.get("legend_click_policy", self._legend_click_policy)
|
|
284
|
+
self._x_axis_visible = kwargs.get("x_axis_visible", self._x_axis_visible)
|
|
285
|
+
self._y_axis_visible = kwargs.get("y_axis_visible", self._y_axis_visible)
|
|
286
|
+
self._grid_visible = kwargs.get("grid_visible", self._grid_visible)
|
|
287
|
+
|
|
288
|
+
self._plot = figure(
|
|
289
|
+
width=self._width,
|
|
290
|
+
height=self._height,
|
|
291
|
+
tools=self._tools,
|
|
292
|
+
toolbar_location=self._toolbar_location,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if self._show_legend:
|
|
296
|
+
self._plot.add_layout(
|
|
297
|
+
Legend(click_policy=self._legend_click_policy),
|
|
298
|
+
self._legend_location,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
self._plot.xaxis.visible = self._x_axis_visible
|
|
302
|
+
self._plot.yaxis.visible = self._y_axis_visible
|
|
303
|
+
self._plot.grid.visible = self._grid_visible
|
|
304
|
+
|
|
305
|
+
self._renderers = []
|
|
306
|
+
self._process_plots(self._plots)
|
|
307
|
+
self._update_html()
|
|
308
|
+
|
|
182
309
|
@property
|
|
183
310
|
def route_path(self) -> str:
|
|
184
311
|
return self.get_route_path(Bokeh.Routes.VALUE_CHANGED)
|
|
@@ -263,10 +390,11 @@ class Bokeh(Widget):
|
|
|
263
390
|
def value_changed(self, func: Callable) -> Callable:
|
|
264
391
|
server = self._sly_app.get_server()
|
|
265
392
|
self._changes_handled = True
|
|
393
|
+
debounced_handler = DebouncedEventHandler(debounce_time=0.2) # TODO: check if it's enough
|
|
266
394
|
|
|
267
395
|
@server.post(self.route_path)
|
|
268
|
-
def _click(
|
|
269
|
-
func
|
|
396
|
+
async def _click(data: SelectedIds) -> None:
|
|
397
|
+
debounced_handler.handle_event(data, func)
|
|
270
398
|
|
|
271
399
|
return _click
|
|
272
400
|
|
|
@@ -276,3 +404,33 @@ class Bokeh(Widget):
|
|
|
276
404
|
<script type="text/javascript"> {self._script} </script>
|
|
277
405
|
{self._div}
|
|
278
406
|
</div>"""
|
|
407
|
+
|
|
408
|
+
def update_radii(self, new_radii: Union[List[Union[list, int, float]], int, float]) -> None:
|
|
409
|
+
if isinstance(new_radii, (int, float)):
|
|
410
|
+
new_radii = [new_radii] * len(self._plots)
|
|
411
|
+
elif len(new_radii) != len(self._plots):
|
|
412
|
+
logger.warning(
|
|
413
|
+
f"{len(new_radii)} != {len(self._plots)}: new_radii will be broadcasted to all plots"
|
|
414
|
+
)
|
|
415
|
+
new_radii = [new_radii[0]] * len(self._plots)
|
|
416
|
+
for idx, radii in enumerate(new_radii):
|
|
417
|
+
self.update_radii_by_plot_idx(idx, radii)
|
|
418
|
+
|
|
419
|
+
def update_radii_by_plot_idx(self, plot_idx: int, new_radii: List[Union[int, float]]) -> None:
|
|
420
|
+
coords_length = len(self._plots[plot_idx]._x_coordinates)
|
|
421
|
+
if isinstance(new_radii, (int, float)):
|
|
422
|
+
new_radii = [new_radii] * coords_length
|
|
423
|
+
elif len(new_radii) != coords_length:
|
|
424
|
+
logger.warning(
|
|
425
|
+
f"{len(new_radii)} != {coords_length}: new_radii will be broadcasted to all plots"
|
|
426
|
+
)
|
|
427
|
+
new_radii = [new_radii[0]] * coords_length
|
|
428
|
+
|
|
429
|
+
self._plots[plot_idx]._radii = new_radii
|
|
430
|
+
self._plots[plot_idx]._source.data["radius"] = new_radii
|
|
431
|
+
self._load_chart()
|
|
432
|
+
|
|
433
|
+
def update_chart_size(self, width: Optional[int] = None, height: Optional[int] = None) -> None:
|
|
434
|
+
self._width = width or self._width
|
|
435
|
+
self._height = height or self._height
|
|
436
|
+
self._load_chart()
|
|
@@ -129,7 +129,7 @@ supervisely/app/widgets/binded_input_number/__init__.py,sha256=47DEQpj8HBSa-_TIm
|
|
|
129
129
|
supervisely/app/widgets/binded_input_number/binded_input_number.py,sha256=RXTAGaMXtGOl4pprVLyQosr61t2rI_U_U8VNHhSyBhA,6251
|
|
130
130
|
supervisely/app/widgets/binded_input_number/template.html,sha256=uCEZ54BNC2itr39wxxThXw62WlJ9659cuz8osCm0WZE,162
|
|
131
131
|
supervisely/app/widgets/bokeh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
132
|
-
supervisely/app/widgets/bokeh/bokeh.py,sha256=
|
|
132
|
+
supervisely/app/widgets/bokeh/bokeh.py,sha256=zob2F-cJ6JX_Dk2FANfN94uHDyxvgoxuBpeL0OebVyg,16128
|
|
133
133
|
supervisely/app/widgets/bokeh/template.html,sha256=ntsh7xx4q9OHG62sa_r3INDxsXgvdPFIWTtYaWn_0t8,12
|
|
134
134
|
supervisely/app/widgets/button/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
135
135
|
supervisely/app/widgets/button/button.py,sha256=zDQinhOOjNNxdP2GIFrwTmVfGAeJJoKV6CT6C8KzQNI,10405
|
|
@@ -1062,9 +1062,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
1062
1062
|
supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
|
|
1063
1063
|
supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
|
|
1064
1064
|
supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
|
|
1065
|
-
supervisely-6.73.
|
|
1066
|
-
supervisely-6.73.
|
|
1067
|
-
supervisely-6.73.
|
|
1068
|
-
supervisely-6.73.
|
|
1069
|
-
supervisely-6.73.
|
|
1070
|
-
supervisely-6.73.
|
|
1065
|
+
supervisely-6.73.267.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
1066
|
+
supervisely-6.73.267.dist-info/METADATA,sha256=TvqvMWrInseSCyvWD2sOf5_eLRMycpEQqX775mWpMos,33573
|
|
1067
|
+
supervisely-6.73.267.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
1068
|
+
supervisely-6.73.267.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
|
|
1069
|
+
supervisely-6.73.267.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
|
|
1070
|
+
supervisely-6.73.267.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|