supervisely 6.73.410__py3-none-any.whl → 6.73.470__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/__init__.py +136 -1
- supervisely/_utils.py +81 -0
- supervisely/annotation/json_geometries_map.py +2 -0
- supervisely/annotation/label.py +80 -3
- supervisely/api/annotation_api.py +9 -9
- supervisely/api/api.py +67 -43
- supervisely/api/app_api.py +72 -5
- supervisely/api/dataset_api.py +108 -33
- supervisely/api/entity_annotation/figure_api.py +113 -49
- supervisely/api/image_api.py +82 -0
- supervisely/api/module_api.py +10 -0
- supervisely/api/nn/deploy_api.py +15 -9
- supervisely/api/nn/ecosystem_models_api.py +201 -0
- supervisely/api/nn/neural_network_api.py +12 -3
- supervisely/api/pointcloud/pointcloud_api.py +38 -0
- supervisely/api/pointcloud/pointcloud_episode_annotation_api.py +3 -0
- supervisely/api/project_api.py +213 -6
- supervisely/api/task_api.py +11 -1
- supervisely/api/video/video_annotation_api.py +4 -2
- supervisely/api/video/video_api.py +79 -1
- supervisely/api/video/video_figure_api.py +24 -11
- supervisely/api/volume/volume_api.py +38 -0
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +14 -6
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +88 -0
- supervisely/app/fastapi/subapp.py +175 -42
- supervisely/app/fastapi/templating.py +1 -1
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/app/v1/app_service.py +18 -2
- supervisely/app/v1/constants.py +7 -1
- supervisely/app/widgets/__init__.py +11 -1
- supervisely/app/widgets/agent_selector/template.html +1 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py +11 -2
- supervisely/app/widgets/dataset_thumbnail/template.html +3 -1
- supervisely/app/widgets/deploy_model/deploy_model.py +750 -0
- supervisely/app/widgets/dialog/dialog.py +12 -0
- supervisely/app/widgets/dialog/template.html +2 -1
- supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
- supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
- supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
- supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +195 -0
- supervisely/app/widgets/experiment_selector/experiment_selector.py +454 -263
- supervisely/app/widgets/fast_table/fast_table.py +713 -126
- supervisely/app/widgets/fast_table/script.js +492 -95
- supervisely/app/widgets/fast_table/style.css +54 -0
- supervisely/app/widgets/fast_table/template.html +45 -5
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +523 -0
- supervisely/app/widgets/heatmap/script.js +378 -0
- supervisely/app/widgets/heatmap/style.css +227 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/input_tag/input_tag.py +102 -15
- supervisely/app/widgets/input_tag_list/__init__.py +0 -0
- supervisely/app/widgets/input_tag_list/input_tag_list.py +274 -0
- supervisely/app/widgets/input_tag_list/template.html +70 -0
- supervisely/app/widgets/radio_table/radio_table.py +10 -2
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select/select.py +6 -4
- supervisely/app/widgets/select_dataset/select_dataset.py +6 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +83 -7
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tabs/tabs.py +22 -6
- supervisely/app/widgets/tabs/template.html +5 -1
- supervisely/app/widgets/transfer/style.css +3 -0
- supervisely/app/widgets/transfer/template.html +3 -1
- supervisely/app/widgets/transfer/transfer.py +48 -45
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/convert/image/csv/csv_converter.py +24 -15
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +43 -41
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +75 -51
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +137 -124
- supervisely/convert/video/video_converter.py +2 -2
- supervisely/geometry/polyline_3d.py +110 -0
- supervisely/io/env.py +161 -1
- supervisely/nn/artifacts/__init__.py +1 -1
- supervisely/nn/artifacts/artifacts.py +10 -2
- supervisely/nn/artifacts/detectron2.py +1 -0
- supervisely/nn/artifacts/hrda.py +1 -0
- supervisely/nn/artifacts/mmclassification.py +20 -0
- supervisely/nn/artifacts/mmdetection.py +5 -3
- supervisely/nn/artifacts/mmsegmentation.py +1 -0
- supervisely/nn/artifacts/ritm.py +1 -0
- supervisely/nn/artifacts/rtdetr.py +1 -0
- supervisely/nn/artifacts/unet.py +1 -0
- supervisely/nn/artifacts/utils.py +3 -0
- supervisely/nn/artifacts/yolov5.py +2 -0
- supervisely/nn/artifacts/yolov8.py +1 -0
- supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
- supervisely/nn/experiments.py +9 -0
- supervisely/nn/inference/cache.py +37 -17
- supervisely/nn/inference/gui/serving_gui_template.py +39 -13
- supervisely/nn/inference/inference.py +953 -211
- supervisely/nn/inference/inference_request.py +15 -8
- supervisely/nn/inference/instance_segmentation/instance_segmentation.py +1 -0
- supervisely/nn/inference/object_detection/object_detection.py +1 -0
- supervisely/nn/inference/predict_app/__init__.py +0 -0
- supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
- supervisely/nn/inference/predict_app/gui/classes_selector.py +160 -0
- supervisely/nn/inference/predict_app/gui/gui.py +915 -0
- supervisely/nn/inference/predict_app/gui/input_selector.py +344 -0
- supervisely/nn/inference/predict_app/gui/model_selector.py +77 -0
- supervisely/nn/inference/predict_app/gui/output_selector.py +179 -0
- supervisely/nn/inference/predict_app/gui/preview.py +93 -0
- supervisely/nn/inference/predict_app/gui/settings_selector.py +881 -0
- supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
- supervisely/nn/inference/predict_app/gui/utils.py +399 -0
- supervisely/nn/inference/predict_app/predict_app.py +176 -0
- supervisely/nn/inference/session.py +47 -39
- supervisely/nn/inference/tracking/bbox_tracking.py +5 -1
- supervisely/nn/inference/tracking/point_tracking.py +5 -1
- supervisely/nn/inference/tracking/tracker_interface.py +4 -0
- supervisely/nn/inference/uploader.py +9 -5
- supervisely/nn/model/model_api.py +44 -22
- supervisely/nn/model/prediction.py +15 -1
- supervisely/nn/model/prediction_session.py +70 -14
- supervisely/nn/prediction_dto.py +7 -0
- supervisely/nn/tracker/__init__.py +6 -8
- supervisely/nn/tracker/base_tracker.py +54 -0
- supervisely/nn/tracker/botsort/__init__.py +1 -0
- supervisely/nn/tracker/botsort/botsort_config.yaml +30 -0
- supervisely/nn/tracker/botsort/osnet_reid/__init__.py +0 -0
- supervisely/nn/tracker/botsort/osnet_reid/osnet.py +566 -0
- supervisely/nn/tracker/botsort/osnet_reid/osnet_reid_interface.py +88 -0
- supervisely/nn/tracker/botsort/tracker/__init__.py +0 -0
- supervisely/nn/tracker/{bot_sort → botsort/tracker}/basetrack.py +1 -2
- supervisely/nn/tracker/{utils → botsort/tracker}/gmc.py +51 -59
- supervisely/nn/tracker/{deep_sort/deep_sort → botsort/tracker}/kalman_filter.py +71 -33
- supervisely/nn/tracker/botsort/tracker/matching.py +202 -0
- supervisely/nn/tracker/{bot_sort/bot_sort.py → botsort/tracker/mc_bot_sort.py} +68 -81
- supervisely/nn/tracker/botsort_tracker.py +273 -0
- supervisely/nn/tracker/calculate_metrics.py +264 -0
- supervisely/nn/tracker/utils.py +273 -0
- supervisely/nn/tracker/visualize.py +520 -0
- supervisely/nn/training/gui/gui.py +152 -49
- supervisely/nn/training/gui/hyperparameters_selector.py +1 -1
- supervisely/nn/training/gui/model_selector.py +8 -6
- supervisely/nn/training/gui/train_val_splits_selector.py +144 -71
- supervisely/nn/training/gui/training_artifacts.py +3 -1
- supervisely/nn/training/train_app.py +225 -46
- supervisely/project/pointcloud_episode_project.py +12 -8
- supervisely/project/pointcloud_project.py +12 -8
- supervisely/project/project.py +221 -75
- supervisely/template/experiment/experiment.html.jinja +105 -55
- supervisely/template/experiment/experiment_generator.py +258 -112
- supervisely/template/experiment/header.html.jinja +31 -13
- supervisely/template/experiment/sly-style.css +7 -2
- supervisely/versions.json +3 -1
- supervisely/video/sampling.py +42 -20
- supervisely/video/video.py +41 -12
- supervisely/video_annotation/video_figure.py +38 -4
- supervisely/volume/stl_converter.py +2 -0
- supervisely/worker_api/agent_rpc.py +24 -1
- supervisely/worker_api/rpc_servicer.py +31 -7
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/METADATA +22 -14
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/RECORD +167 -148
- supervisely_lib/__init__.py +6 -1
- supervisely/app/widgets/experiment_selector/style.css +0 -27
- supervisely/app/widgets/experiment_selector/template.html +0 -61
- supervisely/nn/tracker/bot_sort/__init__.py +0 -21
- supervisely/nn/tracker/bot_sort/fast_reid_interface.py +0 -152
- supervisely/nn/tracker/bot_sort/matching.py +0 -127
- supervisely/nn/tracker/bot_sort/sly_tracker.py +0 -401
- supervisely/nn/tracker/deep_sort/__init__.py +0 -6
- supervisely/nn/tracker/deep_sort/deep_sort/__init__.py +0 -1
- supervisely/nn/tracker/deep_sort/deep_sort/detection.py +0 -49
- supervisely/nn/tracker/deep_sort/deep_sort/iou_matching.py +0 -81
- supervisely/nn/tracker/deep_sort/deep_sort/linear_assignment.py +0 -202
- supervisely/nn/tracker/deep_sort/deep_sort/nn_matching.py +0 -176
- supervisely/nn/tracker/deep_sort/deep_sort/track.py +0 -166
- supervisely/nn/tracker/deep_sort/deep_sort/tracker.py +0 -145
- supervisely/nn/tracker/deep_sort/deep_sort.py +0 -301
- supervisely/nn/tracker/deep_sort/generate_clip_detections.py +0 -90
- supervisely/nn/tracker/deep_sort/preprocessing.py +0 -70
- supervisely/nn/tracker/deep_sort/sly_tracker.py +0 -273
- supervisely/nn/tracker/tracker.py +0 -285
- supervisely/nn/tracker/utils/kalman_filter.py +0 -492
- supervisely/nn/tracking/__init__.py +0 -1
- supervisely/nn/tracking/boxmot.py +0 -114
- supervisely/nn/tracking/tracking.py +0 -24
- /supervisely/{nn/tracker/utils → app/widgets/deploy_model}/__init__.py +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/LICENSE +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/WHEEL +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/top_level.txt +0 -0
|
@@ -46,6 +46,21 @@ class FastTable(Widget):
|
|
|
46
46
|
:type width: str, optional
|
|
47
47
|
:param widget_id: Unique widget identifier.
|
|
48
48
|
:type widget_id: str, optional
|
|
49
|
+
:param show_header: Whether to show table header
|
|
50
|
+
:type show_header: bool, optional
|
|
51
|
+
:param is_radio: Enable radio button selection mode (single row selection)
|
|
52
|
+
:type is_radio: bool, optional
|
|
53
|
+
:param is_selectable: Enable multiple row selection
|
|
54
|
+
:type is_selectable: bool, optional
|
|
55
|
+
:param header_left_content: Widget to display in the left side of the header
|
|
56
|
+
:type header_left_content: Widget, optional
|
|
57
|
+
:param header_right_content: Widget to display in the right side of the header
|
|
58
|
+
:type header_right_content: Widget, optional
|
|
59
|
+
:param max_selected_rows: Maximum number of rows that can be selected
|
|
60
|
+
:type max_selected_rows: int, optional
|
|
61
|
+
:param search_position: Position of the search input ("left" or "right")
|
|
62
|
+
:type search_position: Literal["left", "right"], optional
|
|
63
|
+
|
|
49
64
|
|
|
50
65
|
:Usage example:
|
|
51
66
|
.. code-block:: python
|
|
@@ -72,6 +87,7 @@ class FastTable(Widget):
|
|
|
72
87
|
"""
|
|
73
88
|
|
|
74
89
|
class Routes:
|
|
90
|
+
SELECTION_CHANGED = "selection_changed_cb"
|
|
75
91
|
ROW_CLICKED = "row_clicked_cb"
|
|
76
92
|
CELL_CLICKED = "cell_clicked_cb"
|
|
77
93
|
UPDATE_DATA = "update_data_cb"
|
|
@@ -100,6 +116,29 @@ class FastTable(Widget):
|
|
|
100
116
|
self.column_name = column_name
|
|
101
117
|
self.column_value = column_value
|
|
102
118
|
|
|
119
|
+
class ColumnData:
|
|
120
|
+
def __init__(self, name, is_widget=False, widget: Widget = None):
|
|
121
|
+
self.name = name
|
|
122
|
+
self.is_widget = is_widget
|
|
123
|
+
self.widget = widget
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def widget_html(self):
|
|
127
|
+
html = self.widget.to_html()
|
|
128
|
+
html = html.replace(f".{self.widget.widget_id}", "[JSON.parse(cellValue).widget_id]")
|
|
129
|
+
html = html.replace(
|
|
130
|
+
f"/{self.widget.widget_id}", "/' + JSON.parse(cellValue).widget_id + '"
|
|
131
|
+
)
|
|
132
|
+
if hasattr(self.widget, "_widgets"):
|
|
133
|
+
for i, widget in enumerate(self.widget._widgets):
|
|
134
|
+
html = html.replace(
|
|
135
|
+
f".{widget.widget_id}", f"[JSON.parse(cellValue).widgets[{i}]]"
|
|
136
|
+
)
|
|
137
|
+
html = html.replace(
|
|
138
|
+
f"/{widget.widget_id}", f"/' + JSON.parse(cellValue).widgets[{i}] + '"
|
|
139
|
+
)
|
|
140
|
+
return html
|
|
141
|
+
|
|
103
142
|
def __init__(
|
|
104
143
|
self,
|
|
105
144
|
data: Optional[Union[pd.DataFrame, List]] = None,
|
|
@@ -113,23 +152,54 @@ class FastTable(Widget):
|
|
|
113
152
|
width: Optional[str] = "auto",
|
|
114
153
|
widget_id: Optional[str] = None,
|
|
115
154
|
show_header: bool = True,
|
|
155
|
+
is_radio: bool = False,
|
|
156
|
+
is_selectable: bool = False,
|
|
157
|
+
header_left_content: Optional[Widget] = None,
|
|
158
|
+
header_right_content: Optional[Widget] = None,
|
|
159
|
+
max_selected_rows: Optional[int] = None,
|
|
160
|
+
search_position: Optional[Literal["left", "right"]] = None,
|
|
116
161
|
):
|
|
117
162
|
self._supported_types = tuple([pd.DataFrame, list, type(None)])
|
|
118
163
|
self._row_click_handled = False
|
|
119
164
|
self._cell_click_handled = False
|
|
120
|
-
self.
|
|
165
|
+
self._selection_changed_handled = False
|
|
166
|
+
self._columns = columns
|
|
167
|
+
self._columns_data = []
|
|
168
|
+
if columns is None:
|
|
169
|
+
self._columns_first_idx = None
|
|
170
|
+
else:
|
|
171
|
+
self._columns_first_idx = []
|
|
172
|
+
for col in columns:
|
|
173
|
+
if isinstance(col, str):
|
|
174
|
+
self._columns_first_idx.append(col)
|
|
175
|
+
self._columns_data.append(self.ColumnData(name=col))
|
|
176
|
+
elif isinstance(col, tuple):
|
|
177
|
+
self._columns_first_idx.append(col[0])
|
|
178
|
+
self._columns_data.append(
|
|
179
|
+
self.ColumnData(name=col[0], is_widget=True, widget=col[1])
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
raise TypeError(f"Column name must be a string or a tuple, got {type(col)}")
|
|
183
|
+
|
|
121
184
|
self._columns_options = columns_options
|
|
122
185
|
self._sorted_data = None
|
|
123
186
|
self._filtered_data = None
|
|
187
|
+
self._searched_data = None
|
|
124
188
|
self._active_page = 1
|
|
125
189
|
self._width = width
|
|
126
|
-
self.
|
|
190
|
+
self._selected_rows = []
|
|
127
191
|
self._selected_cell = None
|
|
128
|
-
self.
|
|
129
|
-
self.
|
|
192
|
+
self._clicked_row = None
|
|
193
|
+
self._is_row_clickable = False
|
|
194
|
+
self._is_cell_clickable = False
|
|
130
195
|
self._search_str = ""
|
|
131
196
|
self._show_header = show_header
|
|
132
197
|
self._project_meta = self._unpack_project_meta(project_meta)
|
|
198
|
+
self._header_left_content = header_left_content
|
|
199
|
+
self._header_right_content = header_right_content
|
|
200
|
+
self._max_selected_rows = max_selected_rows
|
|
201
|
+
acceptable_search_positions = ["left", "right"]
|
|
202
|
+
self._search_position = search_position if search_position in acceptable_search_positions else "left"
|
|
133
203
|
|
|
134
204
|
# table_options
|
|
135
205
|
self._page_size = page_size
|
|
@@ -137,6 +207,12 @@ class FastTable(Widget):
|
|
|
137
207
|
self._sort_column_idx = sort_column_idx
|
|
138
208
|
self._sort_order = sort_order
|
|
139
209
|
self._validate_sort_attrs()
|
|
210
|
+
self._is_radio = is_radio
|
|
211
|
+
self._is_selectable = is_selectable
|
|
212
|
+
self._search_function = self._default_search_function
|
|
213
|
+
self._sort_function = self._default_sort_function
|
|
214
|
+
self._filter_function = self._default_filter_function
|
|
215
|
+
self._filter_value = None
|
|
140
216
|
|
|
141
217
|
# to avoid errors with the duplicated names in columns
|
|
142
218
|
self._multi_idx_columns = None
|
|
@@ -145,6 +221,11 @@ class FastTable(Widget):
|
|
|
145
221
|
self._validate_input_data(data)
|
|
146
222
|
self._source_data = self._prepare_input_data(data)
|
|
147
223
|
|
|
224
|
+
# Initialize filtered and searched data for proper initialization
|
|
225
|
+
self._filtered_data = self._filter(self._filter_value)
|
|
226
|
+
self._searched_data = self._search(self._search_str)
|
|
227
|
+
self._sorted_data = self._sort_table_data(self._searched_data)
|
|
228
|
+
|
|
148
229
|
# prepare parsed_source_data, sliced_data, parsed_active_data
|
|
149
230
|
(
|
|
150
231
|
self._parsed_source_data,
|
|
@@ -154,6 +235,9 @@ class FastTable(Widget):
|
|
|
154
235
|
|
|
155
236
|
self._rows_total = len(self._parsed_source_data["data"])
|
|
156
237
|
|
|
238
|
+
if self._is_radio and self._rows_total > 0:
|
|
239
|
+
self._selected_rows = [self._parsed_source_data["data"][0]]
|
|
240
|
+
|
|
157
241
|
super().__init__(widget_id=widget_id, file_path=__file__)
|
|
158
242
|
|
|
159
243
|
script_path = "./sly/css/app/widgets/fast_table/script.js"
|
|
@@ -163,27 +247,34 @@ class FastTable(Widget):
|
|
|
163
247
|
server = self._sly_app.get_server()
|
|
164
248
|
|
|
165
249
|
@server.post(filter_changed_route_path)
|
|
166
|
-
def
|
|
167
|
-
self.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
self.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
250
|
+
def _filter_changed_handler():
|
|
251
|
+
self._refresh()
|
|
252
|
+
|
|
253
|
+
def _refresh(self):
|
|
254
|
+
# TODO sort widgets
|
|
255
|
+
self._active_page = StateJson()[self.widget_id]["page"]
|
|
256
|
+
self._sort_order = StateJson()[self.widget_id]["sort"]["order"]
|
|
257
|
+
self._sort_column_idx = StateJson()[self.widget_id]["sort"]["column"]
|
|
258
|
+
search_value = StateJson()[self.widget_id]["search"]
|
|
259
|
+
self._filtered_data = self._filter(self._filter_value)
|
|
260
|
+
self._searched_data = self._search(search_value)
|
|
261
|
+
self._rows_total = len(self._searched_data)
|
|
262
|
+
|
|
263
|
+
# if active page is greater than the number of pages (e.g. after filtering)
|
|
264
|
+
max_page = (self._rows_total - 1) // self._page_size + 1
|
|
265
|
+
if (self._rows_total > 0 and self._active_page == 0) or self._active_page > max_page:
|
|
266
|
+
self._active_page = 1
|
|
267
|
+
StateJson()[self.widget_id]["page"] = self._active_page
|
|
268
|
+
|
|
269
|
+
self._sorted_data = self._sort_table_data(self._searched_data)
|
|
270
|
+
self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
|
|
271
|
+
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
272
|
+
StateJson().send_changes()
|
|
273
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
274
|
+
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
275
|
+
DataJson().send_changes()
|
|
276
|
+
StateJson()["reactToChanges"] = True
|
|
277
|
+
StateJson().send_changes()
|
|
187
278
|
|
|
188
279
|
def get_json_data(self) -> Dict[str, Any]:
|
|
189
280
|
"""Returns dictionary with widget data, which defines the appearance and behavior of the widget.
|
|
@@ -197,32 +288,44 @@ class FastTable(Widget):
|
|
|
197
288
|
- isRowClickable: whether rows are clickable
|
|
198
289
|
- isCellClickable: whether cells are clickable
|
|
199
290
|
- fixColumns: number of fixed columns
|
|
291
|
+
- isRadio: whether radio button selection mode is enabled
|
|
292
|
+
- isRowSelectable: whether multiple row selection is enabled
|
|
293
|
+
- maxSelectedRows: maximum number of rows that can be selected
|
|
294
|
+
- searchPosition: position of the search input ("left" or "right")
|
|
200
295
|
- pageSize: number of rows per page
|
|
296
|
+
- showHeader: whether to show table header
|
|
297
|
+
- selectionChangedHandled: whether selection changed event listener is set
|
|
201
298
|
|
|
202
299
|
:return: Dictionary with widget data
|
|
203
300
|
:rtype: Dict[str, Any]
|
|
204
301
|
"""
|
|
205
302
|
return {
|
|
206
|
-
"data": self._parsed_active_data["data"],
|
|
303
|
+
"data": list(self._parsed_active_data["data"]),
|
|
207
304
|
"columns": self._parsed_source_data["columns"],
|
|
208
305
|
"projectMeta": self._project_meta,
|
|
209
306
|
"columnsOptions": self._columns_options,
|
|
210
307
|
"total": self._rows_total,
|
|
211
308
|
"options": {
|
|
212
|
-
"isRowClickable": self.
|
|
213
|
-
"isCellClickable": self.
|
|
309
|
+
"isRowClickable": self._is_row_clickable,
|
|
310
|
+
"isCellClickable": self._is_cell_clickable,
|
|
214
311
|
"fixColumns": self._fix_columns,
|
|
312
|
+
"isRadio": self._is_radio,
|
|
313
|
+
"isRowSelectable": self._is_selectable,
|
|
314
|
+
"maxSelectedRows": self._max_selected_rows,
|
|
315
|
+
"searchPosition": self._search_position,
|
|
215
316
|
},
|
|
216
317
|
"pageSize": self._page_size,
|
|
217
318
|
"showHeader": self._show_header,
|
|
319
|
+
"selectionChangedHandled": self._selection_changed_handled,
|
|
218
320
|
}
|
|
219
321
|
|
|
220
322
|
def get_json_state(self) -> Dict[str, Any]:
|
|
221
323
|
"""Returns dictionary with widget state.
|
|
222
324
|
Dictionary contains the following fields:
|
|
223
325
|
- search: search string
|
|
224
|
-
-
|
|
326
|
+
- selectedRows: selected rows
|
|
225
327
|
- selectedCell: selected cell
|
|
328
|
+
- clickedRow: clicked row
|
|
226
329
|
- page: active page
|
|
227
330
|
- sort: sorting options with the following fields:
|
|
228
331
|
- column: index of the column to sort by
|
|
@@ -233,8 +336,9 @@ class FastTable(Widget):
|
|
|
233
336
|
"""
|
|
234
337
|
return {
|
|
235
338
|
"search": self._search_str,
|
|
236
|
-
"
|
|
339
|
+
"selectedRows": self._selected_rows,
|
|
237
340
|
"selectedCell": self._selected_cell,
|
|
341
|
+
"clickedRow": self._clicked_row,
|
|
238
342
|
"page": self._active_page,
|
|
239
343
|
"sort": {
|
|
240
344
|
"column": self._sort_column_idx,
|
|
@@ -289,37 +393,128 @@ class FastTable(Widget):
|
|
|
289
393
|
self._page_size = size
|
|
290
394
|
DataJson()[self.widget_id]["pageSize"] = self._page_size
|
|
291
395
|
|
|
292
|
-
def
|
|
396
|
+
def set_sort(
|
|
397
|
+
self, func: Callable[[pd.DataFrame, int, Optional[Literal["asc", "desc"]]], pd.DataFrame]
|
|
398
|
+
) -> None:
|
|
399
|
+
"""Sets custom sort function for the table.
|
|
400
|
+
|
|
401
|
+
:param func: Custom sort function
|
|
402
|
+
:type func: Callable[[pd.DataFrame, int, Optional[Literal["asc", "desc"]]], pd.DataFrame]
|
|
403
|
+
"""
|
|
404
|
+
self._sort_function = func
|
|
405
|
+
|
|
406
|
+
def set_search(self, func: Callable[[pd.DataFrame, str], pd.DataFrame]) -> None:
|
|
407
|
+
"""Sets custom search function for the table.
|
|
408
|
+
|
|
409
|
+
:param func: Custom search function
|
|
410
|
+
:type func: Callable[[pd.DataFrame, str], pd.DataFrame]
|
|
411
|
+
"""
|
|
412
|
+
self._search_function = func
|
|
413
|
+
|
|
414
|
+
def set_filter(self, filter_function: Callable[[pd.DataFrame, Any], pd.DataFrame]) -> None:
|
|
415
|
+
"""Sets a custom filter function for the table.
|
|
416
|
+
first argument is a DataFrame, second argument is a filter value.
|
|
417
|
+
|
|
418
|
+
:param filter_function: Custom filter function
|
|
419
|
+
:type filter_function: Callable[[pd.DataFrame, Any], pd.DataFrame]
|
|
420
|
+
"""
|
|
421
|
+
if filter_function is None:
|
|
422
|
+
filter_function = self._default_filter_function
|
|
423
|
+
self._filter_function = filter_function
|
|
424
|
+
|
|
425
|
+
def read_json(self, data: Dict, meta: Dict = None, custom_columns: Optional[List[Union[str, tuple]]] = None) -> None:
|
|
293
426
|
"""Replace table data with options and project meta in the widget
|
|
294
427
|
|
|
295
|
-
|
|
428
|
+
More about options in `Developer Portal <https://developer.supervisely.com/app-development/widgets/tables/fasttable#read_json>`_
|
|
429
|
+
|
|
430
|
+
:param data: Table data with options:
|
|
431
|
+
- data: table data
|
|
432
|
+
- columns: list of column names
|
|
433
|
+
- projectMeta: project meta information - if provided
|
|
434
|
+
- columnsOptions: list of dicts with options for each column
|
|
435
|
+
- total: total number of rows
|
|
436
|
+
- options: table options with the following fields:
|
|
437
|
+
- isRowClickable: whether rows are clickable
|
|
438
|
+
- isCellClickable: whether cells are clickable
|
|
439
|
+
- fixColumns: number of fixed columns
|
|
440
|
+
- isRadio: whether radio button selection mode is enabled
|
|
441
|
+
- isRowSelectable: whether multiple row selection is enabled
|
|
442
|
+
- maxSelectedRows: maximum number of rows that can be selected
|
|
443
|
+
- searchPosition: position of the search input ("left" or "right")
|
|
444
|
+
- pageSize: number of rows per page
|
|
445
|
+
- showHeader: whether to show table header
|
|
446
|
+
- selectionChangedHandled: whether selection changed event listener is set
|
|
447
|
+
|
|
296
448
|
:type data: dict
|
|
297
449
|
:param meta: Project meta information
|
|
298
450
|
:type meta: dict
|
|
451
|
+
:param custom_columns: List of column names. Can include widgets as tuples (column_name, widget)
|
|
452
|
+
:type custom_columns: List[Union[str, tuple]], optional
|
|
453
|
+
|
|
454
|
+
Example of data dict:
|
|
455
|
+
.. code-block:: python
|
|
456
|
+
|
|
457
|
+
data = {
|
|
458
|
+
"data": [["apple", "21"], ["banana", "15"]],
|
|
459
|
+
"columns": ["Class", "Items"],
|
|
460
|
+
"columnsOptions": [
|
|
461
|
+
{ "type": "class"},
|
|
462
|
+
{ "maxValue": 21, "postfix": "pcs", "tooltip": "description text", "subtitle": "boxes" }
|
|
463
|
+
],
|
|
464
|
+
"options": {
|
|
465
|
+
"isRowClickable": True,
|
|
466
|
+
"isCellClickable": True,
|
|
467
|
+
"fixColumns": 1,
|
|
468
|
+
"isRadio": False,
|
|
469
|
+
"isRowSelectable": True,
|
|
470
|
+
"maxSelectedRows": 5,
|
|
471
|
+
"searchPosition": "right",
|
|
472
|
+
"sort": {"column": 0, "order": "asc"},
|
|
473
|
+
},
|
|
474
|
+
}
|
|
299
475
|
"""
|
|
300
|
-
self._columns_first_idx = self._prepare_json_data(data, "columns")
|
|
301
476
|
self._columns_options = self._prepare_json_data(data, "columnsOptions")
|
|
477
|
+
self._read_custom_columns(custom_columns)
|
|
478
|
+
if not self._columns_first_idx:
|
|
479
|
+
self._columns_first_idx = self._prepare_json_data(data, "columns")
|
|
302
480
|
self._table_options = self._prepare_json_data(data, "options")
|
|
303
481
|
self._project_meta = self._unpack_project_meta(meta)
|
|
304
|
-
|
|
305
|
-
self.
|
|
306
|
-
self.
|
|
307
|
-
|
|
482
|
+
table_data = data.get("data", None)
|
|
483
|
+
self._validate_input_data(table_data)
|
|
484
|
+
self._source_data = self._prepare_input_data(table_data)
|
|
485
|
+
|
|
308
486
|
init_options = DataJson()[self.widget_id]["options"]
|
|
309
487
|
init_options.update(self._table_options)
|
|
310
488
|
sort = init_options.pop("sort", {"column": None, "order": None})
|
|
311
|
-
|
|
312
|
-
|
|
489
|
+
self._active_page = 1
|
|
490
|
+
self._sort_column_idx = sort.get("column", None)
|
|
491
|
+
if self._sort_column_idx is not None and self._sort_column_idx > len(self._columns_first_idx) - 1:
|
|
492
|
+
self._sort_column_idx = None
|
|
493
|
+
self._sort_order = sort.get("order", None)
|
|
494
|
+
self._page_size = init_options.pop("pageSize", 10)
|
|
495
|
+
|
|
496
|
+
# Apply sorting before preparing working data
|
|
497
|
+
self._sorted_data = self._sort_table_data(self._source_data)
|
|
498
|
+
self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
|
|
499
|
+
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
500
|
+
self._parsed_source_data = self._unpack_pandas_table_data(self._source_data)
|
|
501
|
+
self._rows_total = len(self._parsed_source_data["data"])
|
|
502
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
313
503
|
DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
|
|
314
504
|
DataJson()[self.widget_id]["columnsOptions"] = self._columns_options
|
|
315
505
|
DataJson()[self.widget_id]["options"] = init_options
|
|
316
506
|
DataJson()[self.widget_id]["total"] = len(self._source_data)
|
|
317
|
-
DataJson()[self.widget_id]["pageSize"] =
|
|
507
|
+
DataJson()[self.widget_id]["pageSize"] = self._page_size
|
|
318
508
|
DataJson()[self.widget_id]["projectMeta"] = self._project_meta
|
|
319
|
-
StateJson()[self.widget_id]["sort"] =
|
|
509
|
+
StateJson()[self.widget_id]["sort"]["column"] = self._sort_column_idx
|
|
510
|
+
StateJson()[self.widget_id]["sort"]["order"] = self._sort_order
|
|
511
|
+
StateJson()[self.widget_id]["page"] = self._active_page
|
|
512
|
+
StateJson()[self.widget_id]["selectedRows"] = []
|
|
513
|
+
StateJson()[self.widget_id]["selectedCell"] = None
|
|
514
|
+
self._maybe_update_selected_row()
|
|
515
|
+
self._validate_sort_attrs()
|
|
320
516
|
DataJson().send_changes()
|
|
321
517
|
StateJson().send_changes()
|
|
322
|
-
self.clear_selection()
|
|
323
518
|
|
|
324
519
|
def read_pandas(self, data: pd.DataFrame) -> None:
|
|
325
520
|
"""Replace table data (rows and columns) in the widget.
|
|
@@ -332,7 +527,8 @@ class FastTable(Widget):
|
|
|
332
527
|
self._sliced_data = self._slice_table_data(self._sorted_data)
|
|
333
528
|
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
334
529
|
self._parsed_source_data = self._unpack_pandas_table_data(self._source_data)
|
|
335
|
-
|
|
530
|
+
self._rows_total = len(self._parsed_source_data["data"])
|
|
531
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
336
532
|
DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
|
|
337
533
|
DataJson()[self.widget_id]["total"] = len(self._source_data)
|
|
338
534
|
DataJson().send_changes()
|
|
@@ -344,6 +540,24 @@ class FastTable(Widget):
|
|
|
344
540
|
def to_json(self, active_page: Optional[bool] = False) -> Dict[str, Any]:
|
|
345
541
|
"""Export table data with current options as dict.
|
|
346
542
|
|
|
543
|
+
Dictionary contains the following fields:
|
|
544
|
+
- data: table data
|
|
545
|
+
- columns: list of column names
|
|
546
|
+
- projectMeta: project meta information - if provided
|
|
547
|
+
- columnsOptions: list of dicts with options for each column
|
|
548
|
+
- total: total number of rows
|
|
549
|
+
- options: table options with the following fields:
|
|
550
|
+
- isRowClickable: whether rows are clickable
|
|
551
|
+
- isCellClickable: whether cells are clickable
|
|
552
|
+
- fixColumns: number of fixed columns
|
|
553
|
+
- isRadio: whether radio button selection mode is enabled
|
|
554
|
+
- isRowSelectable: whether multiple row selection is enabled
|
|
555
|
+
- maxSelectedRows: maximum number of rows that can be selected
|
|
556
|
+
- searchPosition: position of the search input ("left" or "right")
|
|
557
|
+
- pageSize: number of rows per page
|
|
558
|
+
- showHeader: whether to show table header
|
|
559
|
+
- selectionChangedHandled: whether selection changed event listener is set
|
|
560
|
+
|
|
347
561
|
:param active_page: Specifies the size of the data to be exported. If True - returns only the active page of the table
|
|
348
562
|
:type active_page: Optional[bool]
|
|
349
563
|
:return: Table data with current options
|
|
@@ -373,17 +587,25 @@ class FastTable(Widget):
|
|
|
373
587
|
:rtype: pd.DataFrame
|
|
374
588
|
"""
|
|
375
589
|
if active_page is True:
|
|
376
|
-
|
|
590
|
+
# Return sliced data directly from source to preserve None/NaN values
|
|
591
|
+
packed_data = self._sliced_data.copy()
|
|
592
|
+
# Reset column names to first level only
|
|
593
|
+
if isinstance(packed_data.columns, pd.MultiIndex):
|
|
594
|
+
packed_data.columns = packed_data.columns.get_level_values("first")
|
|
377
595
|
else:
|
|
378
|
-
|
|
379
|
-
|
|
596
|
+
# Return source data directly to preserve None/NaN values
|
|
597
|
+
packed_data = self._source_data.copy()
|
|
598
|
+
# Reset column names to first level only
|
|
599
|
+
if isinstance(packed_data.columns, pd.MultiIndex):
|
|
600
|
+
packed_data.columns = packed_data.columns.get_level_values("first")
|
|
380
601
|
return packed_data
|
|
381
602
|
|
|
382
603
|
def clear_selection(self) -> None:
|
|
383
604
|
"""Clears the selection of the table."""
|
|
384
|
-
StateJson()[self.widget_id]["
|
|
605
|
+
StateJson()[self.widget_id]["selectedRows"] = []
|
|
385
606
|
StateJson()[self.widget_id]["selectedCell"] = None
|
|
386
607
|
StateJson().send_changes()
|
|
608
|
+
self._maybe_update_selected_row()
|
|
387
609
|
|
|
388
610
|
def get_selected_row(self) -> ClickedRow:
|
|
389
611
|
"""Returns the selected row.
|
|
@@ -391,29 +613,111 @@ class FastTable(Widget):
|
|
|
391
613
|
:return: Selected row
|
|
392
614
|
:rtype: ClickedRow
|
|
393
615
|
"""
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
616
|
+
if self._is_radio or self._is_selectable:
|
|
617
|
+
selected_rows = StateJson()[self.widget_id]["selectedRows"]
|
|
618
|
+
if selected_rows is None:
|
|
619
|
+
return None
|
|
620
|
+
if len(selected_rows) == 0:
|
|
621
|
+
return None
|
|
622
|
+
if len(selected_rows) > 1:
|
|
623
|
+
raise ValueError(
|
|
624
|
+
"Multiple rows selected. Use get_selected_rows() method to get all selected rows."
|
|
625
|
+
)
|
|
626
|
+
row = selected_rows[0]
|
|
627
|
+
row_index = row["idx"]
|
|
628
|
+
row = row.get("row", row.get("items", None))
|
|
629
|
+
if row_index is None or row is None:
|
|
630
|
+
return None
|
|
631
|
+
return self.ClickedRow(row, row_index)
|
|
632
|
+
return self.get_clicked_row()
|
|
633
|
+
|
|
634
|
+
def get_selected_rows(self) -> List[ClickedRow]:
|
|
635
|
+
if self._is_radio or self._is_selectable:
|
|
636
|
+
selected_rows = StateJson()[self.widget_id]["selectedRows"]
|
|
637
|
+
rows = []
|
|
638
|
+
for row in selected_rows:
|
|
639
|
+
row_index = row["idx"]
|
|
640
|
+
if row_index is None:
|
|
641
|
+
continue
|
|
642
|
+
# Get original data from source_data to preserve None/NaN values
|
|
643
|
+
try:
|
|
644
|
+
row_data = self._source_data.loc[row_index].values.tolist()
|
|
645
|
+
except (KeyError, IndexError):
|
|
646
|
+
continue
|
|
647
|
+
rows.append(self.ClickedRow(row_data, row_index))
|
|
648
|
+
return rows
|
|
649
|
+
return [self.get_clicked_row()]
|
|
650
|
+
|
|
651
|
+
def get_clicked_row(self) -> ClickedRow:
|
|
652
|
+
clicked_row = StateJson()[self.widget_id]["clickedRow"]
|
|
653
|
+
if clicked_row is None:
|
|
654
|
+
return None
|
|
655
|
+
row_index = clicked_row["idx"]
|
|
656
|
+
if row_index is None:
|
|
657
|
+
return None
|
|
658
|
+
# Get original data from source_data to preserve None/NaN values
|
|
659
|
+
try:
|
|
660
|
+
row = self._source_data.loc[row_index].values.tolist()
|
|
661
|
+
except (KeyError, IndexError):
|
|
398
662
|
return None
|
|
399
663
|
return self.ClickedRow(row, row_index)
|
|
400
664
|
|
|
401
|
-
def
|
|
665
|
+
def get_clicked_cell(self) -> ClickedCell:
|
|
402
666
|
"""Returns the selected cell.
|
|
403
667
|
|
|
404
668
|
:return: Selected cell
|
|
405
669
|
:rtype: ClickedCell
|
|
406
670
|
"""
|
|
407
671
|
cell_data = StateJson()[self.widget_id]["selectedCell"]
|
|
672
|
+
if cell_data is None:
|
|
673
|
+
return None
|
|
408
674
|
row_index = cell_data["idx"]
|
|
409
|
-
row = cell_data["row"]
|
|
410
675
|
column_index = cell_data["column"]
|
|
676
|
+
if column_index is None or row_index is None:
|
|
677
|
+
return None
|
|
411
678
|
column_name = self._columns_first_idx[column_index]
|
|
412
|
-
|
|
413
|
-
|
|
679
|
+
# Get original data from source_data to preserve None/NaN values
|
|
680
|
+
try:
|
|
681
|
+
row = self._source_data.loc[row_index].values.tolist()
|
|
682
|
+
column_value = row[column_index]
|
|
683
|
+
except (KeyError, IndexError):
|
|
414
684
|
return None
|
|
415
685
|
return self.ClickedCell(row, column_index, row_index, column_name, column_value)
|
|
416
686
|
|
|
687
|
+
def get_selected_cell(self) -> ClickedCell:
|
|
688
|
+
"""Alias for get_clicked_cell method.
|
|
689
|
+
Will be removed in future versions.
|
|
690
|
+
"""
|
|
691
|
+
return self.get_clicked_cell()
|
|
692
|
+
|
|
693
|
+
def _maybe_update_selected_row(self) -> None:
|
|
694
|
+
if self._is_radio:
|
|
695
|
+
if self._rows_total != 0:
|
|
696
|
+
self.select_row(0)
|
|
697
|
+
else:
|
|
698
|
+
self._selected_rows = []
|
|
699
|
+
StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
|
|
700
|
+
StateJson().send_changes()
|
|
701
|
+
return
|
|
702
|
+
if not self._selected_rows:
|
|
703
|
+
return
|
|
704
|
+
if self._rows_total == 0:
|
|
705
|
+
self._selected_rows = []
|
|
706
|
+
StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
|
|
707
|
+
StateJson().send_changes()
|
|
708
|
+
return
|
|
709
|
+
if self._is_selectable:
|
|
710
|
+
updated_selected_rows = []
|
|
711
|
+
for row in self._parsed_source_data["data"]:
|
|
712
|
+
items = row.get("items", row.get("row", None))
|
|
713
|
+
if items is not None:
|
|
714
|
+
for selected_row in self._selected_rows:
|
|
715
|
+
if selected_row.get("row", selected_row.get("items", None)) == items:
|
|
716
|
+
updated_selected_rows.append(row)
|
|
717
|
+
self._selected_rows = updated_selected_rows
|
|
718
|
+
StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
|
|
719
|
+
StateJson().send_changes()
|
|
720
|
+
|
|
417
721
|
def insert_row(self, row: List, index: Optional[int] = -1) -> None:
|
|
418
722
|
"""Inserts a row into the table to the specified position.
|
|
419
723
|
|
|
@@ -424,8 +728,7 @@ class FastTable(Widget):
|
|
|
424
728
|
"""
|
|
425
729
|
self._validate_table_sizes(row)
|
|
426
730
|
self._validate_row_values_types(row)
|
|
427
|
-
|
|
428
|
-
index = len(table_data) if index > len(table_data) or index < 0 else index
|
|
731
|
+
index = len(self._source_data) if index > len(self._source_data) or index < 0 else index
|
|
429
732
|
|
|
430
733
|
self._source_data = pd.concat(
|
|
431
734
|
[
|
|
@@ -440,9 +743,28 @@ class FastTable(Widget):
|
|
|
440
743
|
self._parsed_active_data,
|
|
441
744
|
) = self._prepare_working_data()
|
|
442
745
|
self._rows_total = len(self._parsed_source_data["data"])
|
|
443
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
746
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
444
747
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
445
748
|
DataJson().send_changes()
|
|
749
|
+
self._maybe_update_selected_row()
|
|
750
|
+
|
|
751
|
+
def add_rows(self, rows: List):
|
|
752
|
+
for row in rows:
|
|
753
|
+
self._validate_table_sizes(row)
|
|
754
|
+
self._validate_row_values_types(row)
|
|
755
|
+
self._source_data = pd.concat(
|
|
756
|
+
[self._source_data, pd.DataFrame(rows, columns=self._source_data.columns)]
|
|
757
|
+
).reset_index(drop=True)
|
|
758
|
+
(
|
|
759
|
+
self._parsed_source_data,
|
|
760
|
+
self._sliced_data,
|
|
761
|
+
self._parsed_active_data,
|
|
762
|
+
) = self._prepare_working_data()
|
|
763
|
+
self._rows_total = len(self._parsed_source_data["data"])
|
|
764
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
765
|
+
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
766
|
+
DataJson().send_changes()
|
|
767
|
+
self._maybe_update_selected_row()
|
|
446
768
|
|
|
447
769
|
def pop_row(self, index: Optional[int] = -1) -> List:
|
|
448
770
|
"""Removes a row from the table at the specified position and returns it.
|
|
@@ -467,9 +789,9 @@ class FastTable(Widget):
|
|
|
467
789
|
self._parsed_active_data,
|
|
468
790
|
) = self._prepare_working_data()
|
|
469
791
|
self._rows_total = len(self._parsed_source_data["data"])
|
|
470
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
792
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
471
793
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
472
|
-
|
|
794
|
+
self._maybe_update_selected_row()
|
|
473
795
|
return popped_row
|
|
474
796
|
|
|
475
797
|
def clear(self) -> None:
|
|
@@ -479,9 +801,10 @@ class FastTable(Widget):
|
|
|
479
801
|
self._sliced_data = pd.DataFrame(columns=self._columns_first_idx)
|
|
480
802
|
self._parsed_active_data = {"data": [], "columns": []}
|
|
481
803
|
self._rows_total = 0
|
|
482
|
-
DataJson()[self.widget_id]["data"] =
|
|
804
|
+
DataJson()[self.widget_id]["data"] = {}
|
|
483
805
|
DataJson()[self.widget_id]["total"] = 0
|
|
484
806
|
DataJson().send_changes()
|
|
807
|
+
self._maybe_update_selected_row()
|
|
485
808
|
|
|
486
809
|
def row_click(self, func: Callable[[ClickedRow], Any]) -> Callable[[], None]:
|
|
487
810
|
"""Decorator for function that handles row click event.
|
|
@@ -495,8 +818,8 @@ class FastTable(Widget):
|
|
|
495
818
|
server = self._sly_app.get_server()
|
|
496
819
|
|
|
497
820
|
self._row_click_handled = True
|
|
498
|
-
self.
|
|
499
|
-
DataJson()[self.widget_id]["options"]["isRowClickable"] = self.
|
|
821
|
+
self._is_row_clickable = True
|
|
822
|
+
DataJson()[self.widget_id]["options"]["isRowClickable"] = self._is_row_clickable
|
|
500
823
|
DataJson().send_changes()
|
|
501
824
|
|
|
502
825
|
if self._cell_click_handled is True:
|
|
@@ -528,8 +851,8 @@ class FastTable(Widget):
|
|
|
528
851
|
server = self._sly_app.get_server()
|
|
529
852
|
|
|
530
853
|
self._cell_click_handled = True
|
|
531
|
-
self.
|
|
532
|
-
DataJson()[self.widget_id]["options"]["isCellClickable"] = self.
|
|
854
|
+
self._is_cell_clickable = True
|
|
855
|
+
DataJson()[self.widget_id]["options"]["isCellClickable"] = self._is_cell_clickable
|
|
533
856
|
DataJson().send_changes()
|
|
534
857
|
|
|
535
858
|
if self._row_click_handled is True:
|
|
@@ -549,7 +872,44 @@ class FastTable(Widget):
|
|
|
549
872
|
|
|
550
873
|
return _click
|
|
551
874
|
|
|
552
|
-
def
|
|
875
|
+
def _default_filter_function(self, data: pd.DataFrame, filter_value: Any) -> pd.DataFrame:
|
|
876
|
+
return data
|
|
877
|
+
|
|
878
|
+
def _filter_table_data(self, data: pd.DataFrame) -> pd.DataFrame:
|
|
879
|
+
"""Filter source data using a self._filter_function as filter function.
|
|
880
|
+
To apply a custom filter function, use the set_filter method.
|
|
881
|
+
|
|
882
|
+
:return: Filtered data
|
|
883
|
+
:rtype: pd.DataFrame
|
|
884
|
+
"""
|
|
885
|
+
filtered_data = self._filter_function(data, self._filter_value)
|
|
886
|
+
return filtered_data
|
|
887
|
+
|
|
888
|
+
def _filter(self, filter_value: Any) -> pd.DataFrame:
|
|
889
|
+
filtered_data = self._source_data.copy()
|
|
890
|
+
if filter_value is None:
|
|
891
|
+
return filtered_data
|
|
892
|
+
if self._filter_value != filter_value:
|
|
893
|
+
self._active_page = 1
|
|
894
|
+
StateJson()[self.widget_id]["page"] = self._active_page
|
|
895
|
+
StateJson().send_changes()
|
|
896
|
+
self._filter_value = filter_value
|
|
897
|
+
filtered_data = self._filter_table_data(filtered_data)
|
|
898
|
+
return filtered_data
|
|
899
|
+
|
|
900
|
+
def filter(self, filter_value) -> None:
|
|
901
|
+
self._filter_value = filter_value
|
|
902
|
+
self._refresh()
|
|
903
|
+
|
|
904
|
+
def _default_search_function(self, data: pd.DataFrame, search_value: str) -> pd.DataFrame:
|
|
905
|
+
# Use map() for pandas >= 2.1.0, fallback to applymap() for older versions
|
|
906
|
+
if hasattr(pd.DataFrame, "map"):
|
|
907
|
+
data = data[data.map(lambda x: search_value in str(x)).any(axis=1)]
|
|
908
|
+
else:
|
|
909
|
+
data = data[data.applymap(lambda x: search_value in str(x)).any(axis=1)]
|
|
910
|
+
return data
|
|
911
|
+
|
|
912
|
+
def _search(self, search_value: str) -> pd.DataFrame:
|
|
553
913
|
"""Search source data for search value.
|
|
554
914
|
|
|
555
915
|
:param search_value: Search value
|
|
@@ -557,65 +917,141 @@ class FastTable(Widget):
|
|
|
557
917
|
:return: Filtered data
|
|
558
918
|
:rtype: pd.DataFrame
|
|
559
919
|
"""
|
|
560
|
-
filtered_data
|
|
561
|
-
if
|
|
562
|
-
|
|
920
|
+
# Use filtered_data if available, otherwise use source_data directly
|
|
921
|
+
if self._filtered_data is not None:
|
|
922
|
+
filtered_data = self._filtered_data.copy()
|
|
563
923
|
else:
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
StateJson().send_changes()
|
|
568
|
-
filtered_data = filtered_data[
|
|
569
|
-
filtered_data.applymap(lambda x: search_value in str(x)).any(axis=1)
|
|
570
|
-
]
|
|
924
|
+
filtered_data = self._source_data.copy()
|
|
925
|
+
|
|
926
|
+
if search_value == "":
|
|
571
927
|
self._search_str = search_value
|
|
928
|
+
return filtered_data
|
|
929
|
+
if self._search_str != search_value:
|
|
930
|
+
self._active_page = 1
|
|
931
|
+
StateJson()[self.widget_id]["page"] = self._active_page
|
|
932
|
+
StateJson().send_changes()
|
|
933
|
+
filtered_data = self._search_function(filtered_data, search_value)
|
|
934
|
+
self._search_str = search_value
|
|
572
935
|
return filtered_data
|
|
573
936
|
|
|
937
|
+
def search(self, search_value: str) -> None:
|
|
938
|
+
StateJson()[self.widget_id]["search"] = search_value
|
|
939
|
+
StateJson().send_changes()
|
|
940
|
+
self._refresh()
|
|
941
|
+
|
|
942
|
+
def _default_sort_function(
|
|
943
|
+
self,
|
|
944
|
+
data: pd.DataFrame,
|
|
945
|
+
column_idx: Optional[int],
|
|
946
|
+
order: Optional[Literal["asc", "desc"]],
|
|
947
|
+
) -> pd.DataFrame:
|
|
948
|
+
if order == "asc":
|
|
949
|
+
ascending = True
|
|
950
|
+
else:
|
|
951
|
+
ascending = False
|
|
952
|
+
try:
|
|
953
|
+
column = data.columns[column_idx]
|
|
954
|
+
# Try to convert to numeric for proper sorting
|
|
955
|
+
numeric_column = pd.to_numeric(data[column], errors="coerce")
|
|
956
|
+
|
|
957
|
+
# Check if column contains numeric data (has at least one non-NaN numeric value)
|
|
958
|
+
if numeric_column.notna().sum() > 0:
|
|
959
|
+
# Create temporary column for sorting
|
|
960
|
+
data_copy = data.copy()
|
|
961
|
+
data_copy["_sort_key"] = numeric_column
|
|
962
|
+
# Sort by numeric values with NaN at the end
|
|
963
|
+
data_copy = data_copy.sort_values(
|
|
964
|
+
by="_sort_key", ascending=ascending, na_position="last"
|
|
965
|
+
)
|
|
966
|
+
# Remove temporary column and return original data in sorted order
|
|
967
|
+
data = data.loc[data_copy.index]
|
|
968
|
+
else:
|
|
969
|
+
# Sort as strings with NaN values at the end
|
|
970
|
+
data = data.sort_values(by=column, ascending=ascending, na_position="last")
|
|
971
|
+
except IndexError as e:
|
|
972
|
+
e.args = (
|
|
973
|
+
f"Sorting by column idx = {column_idx} is not possible, your table has only {len(data.columns)} columns with idx from 0 to {len(data.columns) - 1}",
|
|
974
|
+
)
|
|
975
|
+
raise e
|
|
976
|
+
return data
|
|
977
|
+
|
|
574
978
|
def sort(
|
|
575
|
-
self,
|
|
979
|
+
self,
|
|
980
|
+
column_idx: Optional[int] = None,
|
|
981
|
+
order: Optional[Literal["asc", "desc"]] = None,
|
|
982
|
+
reset: bool = False,
|
|
576
983
|
) -> None:
|
|
577
984
|
"""Sorts table data by column index and order.
|
|
578
985
|
|
|
579
|
-
:param column_idx: Index of the column to sort by
|
|
986
|
+
:param column_idx: Index of the column to sort by. If None, keeps current column (unless reset=True).
|
|
580
987
|
:type column_idx: Optional[int]
|
|
581
|
-
:param order: Sorting order
|
|
988
|
+
:param order: Sorting order. If None, keeps current order (unless reset=True).
|
|
582
989
|
:type order: Optional[Literal["asc", "desc"]]
|
|
990
|
+
:param reset: If True, clears sorting completely. Default is False.
|
|
991
|
+
:type reset: bool
|
|
992
|
+
|
|
993
|
+
:Usage example:
|
|
994
|
+
|
|
995
|
+
.. code-block:: python
|
|
996
|
+
# Sorting examples
|
|
997
|
+
sort(column_idx=0, order="asc") # sort by column 0 ascending
|
|
998
|
+
sort(column_idx=1) # sort by column 1, keep current order
|
|
999
|
+
sort(order="desc") # keep current column, change order to descending
|
|
1000
|
+
sort(reset=True) # clear sorting completely
|
|
583
1001
|
"""
|
|
584
|
-
|
|
585
|
-
|
|
1002
|
+
# If reset=True, clear sorting completely
|
|
1003
|
+
if reset:
|
|
1004
|
+
self._sort_column_idx = None
|
|
1005
|
+
self._sort_order = None
|
|
1006
|
+
else:
|
|
1007
|
+
# Preserve current values if new ones are not provided
|
|
1008
|
+
if column_idx is not None:
|
|
1009
|
+
self._sort_column_idx = column_idx
|
|
1010
|
+
# else: keep current self._sort_column_idx
|
|
1011
|
+
|
|
1012
|
+
if order is not None:
|
|
1013
|
+
self._sort_order = order
|
|
1014
|
+
# else: keep current self._sort_order
|
|
1015
|
+
|
|
586
1016
|
self._validate_sort_attrs()
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
self.
|
|
1017
|
+
|
|
1018
|
+
# Always update StateJson with current values (including None)
|
|
1019
|
+
StateJson()[self.widget_id]["sort"]["column"] = self._sort_column_idx
|
|
1020
|
+
StateJson()[self.widget_id]["sort"]["order"] = self._sort_order
|
|
1021
|
+
|
|
1022
|
+
# Apply filter, search, sort pipeline
|
|
1023
|
+
self._filtered_data = self._filter(self._filter_value)
|
|
1024
|
+
self._searched_data = self._search(self._search_str)
|
|
1025
|
+
self._rows_total = len(self._searched_data)
|
|
1026
|
+
self._sorted_data = self._sort_table_data(self._searched_data)
|
|
594
1027
|
self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
|
|
595
1028
|
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
596
|
-
|
|
1029
|
+
|
|
1030
|
+
# Update DataJson with sorted and paginated data
|
|
1031
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
597
1032
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
1033
|
+
self._maybe_update_selected_row()
|
|
598
1034
|
StateJson().send_changes()
|
|
599
1035
|
|
|
600
1036
|
def _prepare_json_data(self, data: dict, key: str):
|
|
601
1037
|
if key in ("data", "columns"):
|
|
602
1038
|
default_value = []
|
|
1039
|
+
elif key == "options":
|
|
1040
|
+
default_value = {}
|
|
603
1041
|
else:
|
|
604
1042
|
default_value = None
|
|
1043
|
+
|
|
605
1044
|
source_data = data.get(key, default_value)
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
)
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if column_idx is not None:
|
|
617
|
-
sort["column"] = sort.get("columnIndex")
|
|
618
|
-
sort.pop("columnIndex")
|
|
1045
|
+
|
|
1046
|
+
# Normalize options format: convert "columnIndex" to "column"
|
|
1047
|
+
if key == "options" and source_data is not None:
|
|
1048
|
+
sort = source_data.get("sort", None)
|
|
1049
|
+
if sort is not None:
|
|
1050
|
+
column_idx = sort.get("columnIndex", None)
|
|
1051
|
+
if column_idx is not None:
|
|
1052
|
+
sort["column"] = column_idx
|
|
1053
|
+
sort.pop("columnIndex")
|
|
1054
|
+
|
|
619
1055
|
return source_data
|
|
620
1056
|
|
|
621
1057
|
def _validate_sort(
|
|
@@ -697,12 +1133,21 @@ class FastTable(Widget):
|
|
|
697
1133
|
def _get_pandas_unpacked_data(self, data: pd.DataFrame) -> dict:
|
|
698
1134
|
if not isinstance(data, pd.DataFrame):
|
|
699
1135
|
raise TypeError("Cannot parse input data, please use Pandas Dataframe as input data")
|
|
700
|
-
|
|
701
|
-
#
|
|
1136
|
+
|
|
1137
|
+
# Create a copy for frontend display to avoid modifying source data
|
|
1138
|
+
display_data = data.copy()
|
|
1139
|
+
# Replace NaN and None with empty string only for display
|
|
1140
|
+
display_data = display_data.replace({np.nan: "", None: ""})
|
|
1141
|
+
|
|
1142
|
+
# Handle MultiIndex columns - extract only the first level
|
|
1143
|
+
if isinstance(display_data.columns, pd.MultiIndex):
|
|
1144
|
+
columns = display_data.columns.get_level_values("first").tolist()
|
|
1145
|
+
else:
|
|
1146
|
+
columns = display_data.columns.to_list()
|
|
702
1147
|
|
|
703
1148
|
unpacked_data = {
|
|
704
|
-
"columns":
|
|
705
|
-
"data":
|
|
1149
|
+
"columns": columns,
|
|
1150
|
+
"data": display_data.values.tolist(),
|
|
706
1151
|
}
|
|
707
1152
|
return unpacked_data
|
|
708
1153
|
|
|
@@ -746,28 +1191,29 @@ class FastTable(Widget):
|
|
|
746
1191
|
data = data.iloc[start_idx:end_idx]
|
|
747
1192
|
return data
|
|
748
1193
|
|
|
749
|
-
def _sort_table_data(
|
|
1194
|
+
def _sort_table_data(
|
|
1195
|
+
self,
|
|
1196
|
+
input_data: pd.DataFrame,
|
|
1197
|
+
column_index: Optional[int] = None,
|
|
1198
|
+
sort_order: Optional[Literal["asc", "desc"]] = None,
|
|
1199
|
+
) -> pd.DataFrame:
|
|
750
1200
|
"""
|
|
751
1201
|
Apply sorting to received data
|
|
752
1202
|
|
|
753
1203
|
"""
|
|
754
|
-
if
|
|
1204
|
+
if column_index is None:
|
|
1205
|
+
column_index = self._sort_column_idx
|
|
1206
|
+
if sort_order is None:
|
|
1207
|
+
sort_order = self._sort_order
|
|
1208
|
+
|
|
1209
|
+
if sort_order is None or column_index is None:
|
|
755
1210
|
return input_data # unsorted
|
|
756
1211
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
else:
|
|
761
|
-
ascending = False
|
|
762
|
-
data: pd.DataFrame = copy.deepcopy(input_data)
|
|
763
|
-
try:
|
|
764
|
-
data = data.sort_values(by=data.columns[self._sort_column_idx], ascending=ascending)
|
|
765
|
-
except IndexError as e:
|
|
766
|
-
e.args = (
|
|
767
|
-
f"Sorting by column idx = {self._sort_column_idx} is not possible, your table has only {len(data.columns)} columns with idx from 0 to {len(data.columns) - 1}",
|
|
768
|
-
)
|
|
769
|
-
raise e
|
|
1212
|
+
data = copy.deepcopy(input_data)
|
|
1213
|
+
if input_data is None:
|
|
1214
|
+
return data
|
|
770
1215
|
|
|
1216
|
+
data = self._sort_function(data=input_data, column_idx=column_index, order=sort_order)
|
|
771
1217
|
return data
|
|
772
1218
|
|
|
773
1219
|
def _unpack_project_meta(self, project_meta: Union[ProjectMeta, dict]) -> dict:
|
|
@@ -857,19 +1303,160 @@ class FastTable(Widget):
|
|
|
857
1303
|
self._sort_column_idx = StateJson()[self.widget_id]["sort"]["column"]
|
|
858
1304
|
self._sort_order = StateJson()[self.widget_id]["sort"]["order"]
|
|
859
1305
|
self._validate_sort_attrs()
|
|
860
|
-
self._filtered_data = self.
|
|
861
|
-
self.
|
|
862
|
-
self.
|
|
1306
|
+
self._filtered_data = self._filter(self._filter_value)
|
|
1307
|
+
self._searched_data = self._search(self._search_str)
|
|
1308
|
+
self._rows_total = len(self._searched_data)
|
|
1309
|
+
self._sorted_data = self._sort_table_data(self._searched_data)
|
|
863
1310
|
|
|
864
1311
|
increment = 0 if self._rows_total % self._page_size == 0 else 1
|
|
865
1312
|
max_page = self._rows_total // self._page_size + increment
|
|
866
|
-
if
|
|
1313
|
+
if (
|
|
1314
|
+
self._active_page > max_page
|
|
1315
|
+
): # active page is out of range (in case of the filtered data)
|
|
867
1316
|
self._active_page = max_page
|
|
868
1317
|
StateJson()[self.widget_id]["page"] = self._active_page
|
|
869
1318
|
|
|
870
1319
|
self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
|
|
871
1320
|
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
872
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
1321
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
873
1322
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
874
1323
|
DataJson().send_changes()
|
|
875
1324
|
StateJson().send_changes()
|
|
1325
|
+
|
|
1326
|
+
def selection_changed(self, func):
|
|
1327
|
+
"""Decorator for function that handles selection change event.
|
|
1328
|
+
|
|
1329
|
+
:param func: Function that handles selection change event
|
|
1330
|
+
:type func: Callable[[], Any]
|
|
1331
|
+
:return: Decorated function
|
|
1332
|
+
:rtype: Callable[[], None]
|
|
1333
|
+
"""
|
|
1334
|
+
selection_changed_route_path = self.get_route_path(FastTable.Routes.SELECTION_CHANGED)
|
|
1335
|
+
server = self._sly_app.get_server()
|
|
1336
|
+
|
|
1337
|
+
@server.post(selection_changed_route_path)
|
|
1338
|
+
def _selection_changed():
|
|
1339
|
+
if self._is_radio:
|
|
1340
|
+
selected_row = self.get_selected_row()
|
|
1341
|
+
func(selected_row)
|
|
1342
|
+
elif self._is_selectable:
|
|
1343
|
+
selected_rows = self.get_selected_rows()
|
|
1344
|
+
func(selected_rows)
|
|
1345
|
+
|
|
1346
|
+
self._selection_changed_handled = True
|
|
1347
|
+
DataJson()[self.widget_id]["selectionChangedHandled"] = True
|
|
1348
|
+
DataJson().send_changes()
|
|
1349
|
+
return _selection_changed
|
|
1350
|
+
|
|
1351
|
+
def select_row(self, idx: int):
|
|
1352
|
+
if not self._is_selectable and not self._is_radio:
|
|
1353
|
+
raise ValueError(
|
|
1354
|
+
"Table is not selectable. Set 'is_selectable' or 'is_radio' to True to use this method."
|
|
1355
|
+
)
|
|
1356
|
+
if idx < 0 or idx >= len(self._parsed_source_data["data"]):
|
|
1357
|
+
raise IndexError(
|
|
1358
|
+
f"Row index {idx} is out of range. Valid range is 0 to {len(self._parsed_source_data['data']) - 1}."
|
|
1359
|
+
)
|
|
1360
|
+
selected_row = self._parsed_source_data["data"][idx]
|
|
1361
|
+
self._selected_rows = [
|
|
1362
|
+
{"idx": idx, "row": selected_row.get("items", selected_row.get("row", None))}
|
|
1363
|
+
]
|
|
1364
|
+
StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
|
|
1365
|
+
page = idx // self._page_size + 1
|
|
1366
|
+
if self._active_page != page:
|
|
1367
|
+
self._active_page = page
|
|
1368
|
+
StateJson()[self.widget_id]["page"] = self._active_page
|
|
1369
|
+
self._refresh()
|
|
1370
|
+
|
|
1371
|
+
def select_rows(self, idxs: List[int]):
|
|
1372
|
+
if not self._is_selectable:
|
|
1373
|
+
raise ValueError(
|
|
1374
|
+
"Table is not selectable. Set 'is_selectable' to True to use this method."
|
|
1375
|
+
)
|
|
1376
|
+
selected_rows = [
|
|
1377
|
+
self._parsed_source_data["data"][idx]
|
|
1378
|
+
for idx in idxs
|
|
1379
|
+
if 0 <= idx < len(self._parsed_source_data["data"])
|
|
1380
|
+
]
|
|
1381
|
+
self._selected_rows = [
|
|
1382
|
+
{"idx": row["idx"], "row": row.get("items", row.get("row", None))}
|
|
1383
|
+
for row in selected_rows
|
|
1384
|
+
]
|
|
1385
|
+
StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
|
|
1386
|
+
StateJson().send_changes()
|
|
1387
|
+
|
|
1388
|
+
def select_row_by_value(self, column, value: Any):
|
|
1389
|
+
"""Selects a row by value in a specific column.
|
|
1390
|
+
The first column with the given name is used in case of duplicate column names.
|
|
1391
|
+
|
|
1392
|
+
:param column: Column name to filter by
|
|
1393
|
+
:type column: str
|
|
1394
|
+
:param value: Value to select row by
|
|
1395
|
+
:type value: Any
|
|
1396
|
+
"""
|
|
1397
|
+
if not self._is_selectable and not self._is_radio:
|
|
1398
|
+
raise ValueError(
|
|
1399
|
+
"Table is not selectable. Set 'is_selectable' to True to use this method."
|
|
1400
|
+
)
|
|
1401
|
+
if column not in self._columns_first_idx:
|
|
1402
|
+
raise ValueError(f"Column '{column}' does not exist in the table.")
|
|
1403
|
+
|
|
1404
|
+
# Find the first column index with this name (in case of duplicates)
|
|
1405
|
+
column_idx = self._columns_first_idx.index(column)
|
|
1406
|
+
column_tuple = self._source_data.columns[column_idx]
|
|
1407
|
+
|
|
1408
|
+
# Use column tuple to access the specific column
|
|
1409
|
+
idx = self._source_data[self._source_data[column_tuple] == value].index.tolist()
|
|
1410
|
+
if not idx:
|
|
1411
|
+
raise ValueError(f"No rows found with {column} = {value}.")
|
|
1412
|
+
if len(idx) > 1:
|
|
1413
|
+
raise ValueError(
|
|
1414
|
+
f"Multiple rows found with {column} = {value}. Please use select_rows_by_value method."
|
|
1415
|
+
)
|
|
1416
|
+
self.select_row(idx[0])
|
|
1417
|
+
|
|
1418
|
+
def select_rows_by_value(self, column, values: List):
|
|
1419
|
+
"""Selects rows by value in a specific column.
|
|
1420
|
+
The first column with the given name is used in case of duplicate column names.
|
|
1421
|
+
|
|
1422
|
+
:param column: Column name to filter by
|
|
1423
|
+
:type column: str
|
|
1424
|
+
:param values: List of values to select rows by
|
|
1425
|
+
:type values: List
|
|
1426
|
+
"""
|
|
1427
|
+
if not self._is_selectable:
|
|
1428
|
+
raise ValueError(
|
|
1429
|
+
"Table is not selectable. Set 'is_selectable' to True to use this method."
|
|
1430
|
+
)
|
|
1431
|
+
if column not in self._columns_first_idx:
|
|
1432
|
+
raise ValueError(f"Column '{column}' does not exist in the table.")
|
|
1433
|
+
|
|
1434
|
+
# Find the first column index with this name (in case of duplicates)
|
|
1435
|
+
column_idx = self._columns_first_idx.index(column)
|
|
1436
|
+
column_tuple = self._source_data.columns[column_idx]
|
|
1437
|
+
|
|
1438
|
+
# Use column tuple to access the specific column
|
|
1439
|
+
idxs = self._source_data[self._source_data[column_tuple].isin(values)].index.tolist()
|
|
1440
|
+
self.select_rows(idxs)
|
|
1441
|
+
|
|
1442
|
+
def _read_custom_columns(self, columns: List[Union[str, tuple]]) -> None:
|
|
1443
|
+
if not columns:
|
|
1444
|
+
return
|
|
1445
|
+
self._columns = columns
|
|
1446
|
+
self._columns_options = self._columns_options or [{} for _ in columns]
|
|
1447
|
+
self._columns_data = []
|
|
1448
|
+
self._columns_first_idx = []
|
|
1449
|
+
for i, col in enumerate(columns):
|
|
1450
|
+
if isinstance(col, str):
|
|
1451
|
+
self._columns_first_idx.append(col)
|
|
1452
|
+
self._columns_data.append(self.ColumnData(name=col))
|
|
1453
|
+
elif isinstance(col, tuple):
|
|
1454
|
+
self._columns_first_idx.append(col[0])
|
|
1455
|
+
self._columns_data.append(
|
|
1456
|
+
self.ColumnData(name=col[0], is_widget=True, widget=col[1])
|
|
1457
|
+
)
|
|
1458
|
+
self._columns_options[i]["customCell"] = True
|
|
1459
|
+
else:
|
|
1460
|
+
raise TypeError(f"Column name must be a string or a tuple, got {type(col)}")
|
|
1461
|
+
|
|
1462
|
+
self._validate_sort_attrs()
|