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.

@@ -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
- from supervisely.app.widgets import Widget
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[Any]
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
- from bokeh.plotting import figure # pylint: disable=import-error
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._plot.xaxis.visible = x_axis_visible
167
- self._plot.yaxis.visible = y_axis_visible
168
- self._plot.grid.visible = grid_visible
169
- super().__init__(widget_id=widget_id, file_path=__file__)
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
- self._process_plots(plots)
172
- self._update_html()
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(selected_ids: SelectedIds) -> None:
269
- func(selected_ids.selected_ids)
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.265
3
+ Version: 6.73.267
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -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=9s9Vx8f-NI-2hr9xjuYapFMsZkFF_n65WAWeMXgqTWY,9563
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.265.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1066
- supervisely-6.73.265.dist-info/METADATA,sha256=CUKYTMDjylR1OQXXME1xmz_FSf3CtFgdGxZCNUXBrUg,33573
1067
- supervisely-6.73.265.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1068
- supervisely-6.73.265.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1069
- supervisely-6.73.265.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1070
- supervisely-6.73.265.dist-info/RECORD,,
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,,