supervisely 6.73.452__py3-none-any.whl → 6.73.513__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.
- supervisely/__init__.py +25 -1
- supervisely/annotation/annotation.py +8 -2
- supervisely/annotation/json_geometries_map.py +13 -12
- supervisely/api/annotation_api.py +6 -3
- supervisely/api/api.py +2 -0
- supervisely/api/app_api.py +10 -1
- supervisely/api/dataset_api.py +74 -12
- supervisely/api/entities_collection_api.py +10 -0
- supervisely/api/entity_annotation/figure_api.py +28 -0
- supervisely/api/entity_annotation/object_api.py +3 -3
- supervisely/api/entity_annotation/tag_api.py +63 -12
- supervisely/api/guides_api.py +210 -0
- supervisely/api/image_api.py +4 -0
- supervisely/api/labeling_job_api.py +83 -1
- supervisely/api/labeling_queue_api.py +33 -7
- supervisely/api/module_api.py +5 -0
- supervisely/api/project_api.py +71 -26
- supervisely/api/storage_api.py +3 -1
- supervisely/api/task_api.py +13 -2
- supervisely/api/team_api.py +4 -3
- supervisely/api/video/video_annotation_api.py +119 -3
- supervisely/api/video/video_api.py +65 -14
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +23 -7
- supervisely/app/development/development.py +18 -2
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +105 -0
- supervisely/app/fastapi/subapp.py +88 -42
- 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 +6 -0
- supervisely/app/widgets/activity_feed/__init__.py +0 -0
- supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
- supervisely/app/widgets/activity_feed/style.css +78 -0
- supervisely/app/widgets/activity_feed/template.html +22 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
- supervisely/app/widgets/classes_list_selector/template.html +60 -93
- supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
- supervisely/app/widgets/classes_table/classes_table.py +1 -0
- supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
- supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
- supervisely/app/widgets/fast_table/fast_table.py +184 -60
- supervisely/app/widgets/fast_table/template.html +1 -1
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +564 -0
- supervisely/app/widgets/heatmap/script.js +533 -0
- supervisely/app/widgets/heatmap/style.css +233 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/modal/__init__.py +0 -0
- supervisely/app/widgets/modal/modal.py +198 -0
- supervisely/app/widgets/modal/template.html +10 -0
- supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
- 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 -3
- supervisely/app/widgets/select_class/__init__.py +0 -0
- supervisely/app/widgets/select_class/select_class.py +363 -0
- supervisely/app/widgets/select_class/template.html +50 -0
- supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
- supervisely/app/widgets/select_tag/__init__.py +0 -0
- supervisely/app/widgets/select_tag/select_tag.py +352 -0
- supervisely/app/widgets/select_tag/template.html +64 -0
- supervisely/app/widgets/select_team/select_team.py +37 -4
- supervisely/app/widgets/select_team/template.html +4 -5
- supervisely/app/widgets/select_user/__init__.py +0 -0
- supervisely/app/widgets/select_user/select_user.py +270 -0
- supervisely/app/widgets/select_user/template.html +13 -0
- supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
- supervisely/app/widgets/select_workspace/template.html +9 -12
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/aug/aug.py +6 -2
- supervisely/convert/base_converter.py +1 -0
- supervisely/convert/converter.py +2 -2
- supervisely/convert/image/image_converter.py +3 -1
- supervisely/convert/image/image_helper.py +48 -4
- supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
- supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
- supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
- supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
- supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
- supervisely/convert/pointcloud/las/las_converter.py +13 -1
- supervisely/convert/pointcloud/las/las_helper.py +110 -11
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
- supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
- supervisely/convert/video/__init__.py +1 -0
- supervisely/convert/video/multi_view/__init__.py +0 -0
- supervisely/convert/video/multi_view/multi_view.py +543 -0
- supervisely/convert/video/sly/sly_video_converter.py +359 -3
- supervisely/convert/video/video_converter.py +22 -2
- supervisely/convert/volume/dicom/dicom_converter.py +13 -5
- supervisely/convert/volume/dicom/dicom_helper.py +30 -18
- supervisely/geometry/constants.py +1 -0
- supervisely/geometry/geometry.py +4 -0
- supervisely/geometry/helpers.py +5 -1
- supervisely/geometry/oriented_bbox.py +676 -0
- supervisely/geometry/rectangle.py +2 -1
- supervisely/io/env.py +76 -1
- supervisely/io/fs.py +21 -0
- supervisely/nn/benchmark/base_evaluator.py +104 -11
- supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
- supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
- supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
- supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
- supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
- supervisely/nn/inference/cache.py +43 -18
- supervisely/nn/inference/gui/serving_gui_template.py +5 -2
- supervisely/nn/inference/inference.py +795 -199
- supervisely/nn/inference/inference_request.py +42 -9
- supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
- supervisely/nn/inference/predict_app/gui/gui.py +676 -488
- supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
- supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
- supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
- supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
- supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
- supervisely/nn/inference/predict_app/gui/utils.py +236 -119
- supervisely/nn/inference/predict_app/predict_app.py +2 -2
- supervisely/nn/inference/session.py +43 -35
- supervisely/nn/inference/tracking/bbox_tracking.py +113 -34
- supervisely/nn/inference/tracking/tracker_interface.py +7 -2
- supervisely/nn/inference/uploader.py +139 -12
- supervisely/nn/live_training/__init__.py +7 -0
- supervisely/nn/live_training/api_server.py +111 -0
- supervisely/nn/live_training/artifacts_utils.py +243 -0
- supervisely/nn/live_training/checkpoint_utils.py +229 -0
- supervisely/nn/live_training/dynamic_sampler.py +44 -0
- supervisely/nn/live_training/helpers.py +14 -0
- supervisely/nn/live_training/incremental_dataset.py +146 -0
- supervisely/nn/live_training/live_training.py +497 -0
- supervisely/nn/live_training/loss_plateau_detector.py +111 -0
- supervisely/nn/live_training/request_queue.py +52 -0
- supervisely/nn/model/model_api.py +9 -0
- supervisely/nn/prediction_dto.py +12 -1
- supervisely/nn/tracker/base_tracker.py +11 -1
- supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
- supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
- supervisely/nn/tracker/botsort_tracker.py +94 -65
- supervisely/nn/tracker/visualize.py +87 -90
- supervisely/nn/training/gui/classes_selector.py +16 -1
- supervisely/nn/training/train_app.py +28 -29
- supervisely/project/data_version.py +115 -51
- supervisely/project/download.py +1 -1
- supervisely/project/pointcloud_episode_project.py +37 -8
- supervisely/project/pointcloud_project.py +30 -2
- supervisely/project/project.py +14 -2
- supervisely/project/project_meta.py +27 -1
- supervisely/project/project_settings.py +32 -18
- supervisely/project/versioning/__init__.py +1 -0
- supervisely/project/versioning/common.py +20 -0
- supervisely/project/versioning/schema_fields.py +35 -0
- supervisely/project/versioning/video_schema.py +221 -0
- supervisely/project/versioning/volume_schema.py +87 -0
- supervisely/project/video_project.py +717 -15
- supervisely/project/volume_project.py +623 -5
- supervisely/template/experiment/experiment.html.jinja +4 -4
- supervisely/template/experiment/experiment_generator.py +14 -21
- supervisely/template/live_training/__init__.py +0 -0
- supervisely/template/live_training/header.html.jinja +96 -0
- supervisely/template/live_training/live_training.html.jinja +51 -0
- supervisely/template/live_training/live_training_generator.py +464 -0
- supervisely/template/live_training/sly-style.css +402 -0
- supervisely/template/live_training/template.html.jinja +18 -0
- supervisely/versions.json +28 -26
- supervisely/video/sampling.py +39 -20
- supervisely/video/video.py +40 -11
- supervisely/video_annotation/video_object.py +29 -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.452.dist-info → supervisely-6.73.513.dist-info}/METADATA +56 -39
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/RECORD +189 -142
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
|
@@ -221,6 +221,11 @@ class FastTable(Widget):
|
|
|
221
221
|
self._validate_input_data(data)
|
|
222
222
|
self._source_data = self._prepare_input_data(data)
|
|
223
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
|
+
|
|
224
229
|
# prepare parsed_source_data, sliced_data, parsed_active_data
|
|
225
230
|
(
|
|
226
231
|
self._parsed_source_data,
|
|
@@ -265,7 +270,7 @@ class FastTable(Widget):
|
|
|
265
270
|
self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
|
|
266
271
|
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
267
272
|
StateJson().send_changes()
|
|
268
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
273
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
269
274
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
270
275
|
DataJson().send_changes()
|
|
271
276
|
StateJson()["reactToChanges"] = True
|
|
@@ -295,7 +300,7 @@ class FastTable(Widget):
|
|
|
295
300
|
:rtype: Dict[str, Any]
|
|
296
301
|
"""
|
|
297
302
|
return {
|
|
298
|
-
"data": self._parsed_active_data["data"],
|
|
303
|
+
"data": list(self._parsed_active_data["data"]),
|
|
299
304
|
"columns": self._parsed_source_data["columns"],
|
|
300
305
|
"projectMeta": self._project_meta,
|
|
301
306
|
"columnsOptions": self._columns_options,
|
|
@@ -307,7 +312,7 @@ class FastTable(Widget):
|
|
|
307
312
|
"isRadio": self._is_radio,
|
|
308
313
|
"isRowSelectable": self._is_selectable,
|
|
309
314
|
"maxSelectedRows": self._max_selected_rows,
|
|
310
|
-
"searchPosition": self._search_position
|
|
315
|
+
"searchPosition": self._search_position,
|
|
311
316
|
},
|
|
312
317
|
"pageSize": self._page_size,
|
|
313
318
|
"showHeader": self._show_header,
|
|
@@ -420,6 +425,8 @@ class FastTable(Widget):
|
|
|
420
425
|
def read_json(self, data: Dict, meta: Dict = None, custom_columns: Optional[List[Union[str, tuple]]] = None) -> None:
|
|
421
426
|
"""Replace table data with options and project meta in the widget
|
|
422
427
|
|
|
428
|
+
More about options in `Developer Portal <https://developer.supervisely.com/app-development/widgets/tables/fasttable#read_json>`_
|
|
429
|
+
|
|
423
430
|
:param data: Table data with options:
|
|
424
431
|
- data: table data
|
|
425
432
|
- columns: list of column names
|
|
@@ -475,12 +482,7 @@ class FastTable(Widget):
|
|
|
475
482
|
table_data = data.get("data", None)
|
|
476
483
|
self._validate_input_data(table_data)
|
|
477
484
|
self._source_data = self._prepare_input_data(table_data)
|
|
478
|
-
|
|
479
|
-
self._parsed_source_data,
|
|
480
|
-
self._sliced_data,
|
|
481
|
-
self._parsed_active_data,
|
|
482
|
-
) = self._prepare_working_data()
|
|
483
|
-
self._rows_total = len(self._parsed_source_data["data"])
|
|
485
|
+
|
|
484
486
|
init_options = DataJson()[self.widget_id]["options"]
|
|
485
487
|
init_options.update(self._table_options)
|
|
486
488
|
sort = init_options.pop("sort", {"column": None, "order": None})
|
|
@@ -489,8 +491,15 @@ class FastTable(Widget):
|
|
|
489
491
|
if self._sort_column_idx is not None and self._sort_column_idx > len(self._columns_first_idx) - 1:
|
|
490
492
|
self._sort_column_idx = None
|
|
491
493
|
self._sort_order = sort.get("order", None)
|
|
492
|
-
self._page_size = init_options.pop("pageSize", 10)
|
|
493
|
-
|
|
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"])
|
|
494
503
|
DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
|
|
495
504
|
DataJson()[self.widget_id]["columnsOptions"] = self._columns_options
|
|
496
505
|
DataJson()[self.widget_id]["options"] = init_options
|
|
@@ -519,7 +528,7 @@ class FastTable(Widget):
|
|
|
519
528
|
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
520
529
|
self._parsed_source_data = self._unpack_pandas_table_data(self._source_data)
|
|
521
530
|
self._rows_total = len(self._parsed_source_data["data"])
|
|
522
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
531
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
523
532
|
DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
|
|
524
533
|
DataJson()[self.widget_id]["total"] = len(self._source_data)
|
|
525
534
|
DataJson().send_changes()
|
|
@@ -578,10 +587,17 @@ class FastTable(Widget):
|
|
|
578
587
|
:rtype: pd.DataFrame
|
|
579
588
|
"""
|
|
580
589
|
if active_page is True:
|
|
581
|
-
|
|
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")
|
|
582
595
|
else:
|
|
583
|
-
|
|
584
|
-
|
|
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")
|
|
585
601
|
return packed_data
|
|
586
602
|
|
|
587
603
|
def clear_selection(self) -> None:
|
|
@@ -621,8 +637,12 @@ class FastTable(Widget):
|
|
|
621
637
|
rows = []
|
|
622
638
|
for row in selected_rows:
|
|
623
639
|
row_index = row["idx"]
|
|
624
|
-
|
|
625
|
-
|
|
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):
|
|
626
646
|
continue
|
|
627
647
|
rows.append(self.ClickedRow(row_data, row_index))
|
|
628
648
|
return rows
|
|
@@ -633,8 +653,12 @@ class FastTable(Widget):
|
|
|
633
653
|
if clicked_row is None:
|
|
634
654
|
return None
|
|
635
655
|
row_index = clicked_row["idx"]
|
|
636
|
-
|
|
637
|
-
|
|
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):
|
|
638
662
|
return None
|
|
639
663
|
return self.ClickedRow(row, row_index)
|
|
640
664
|
|
|
@@ -644,15 +668,19 @@ class FastTable(Widget):
|
|
|
644
668
|
:return: Selected cell
|
|
645
669
|
:rtype: ClickedCell
|
|
646
670
|
"""
|
|
647
|
-
cell_data = StateJson()[self.widget_id]["
|
|
671
|
+
cell_data = StateJson()[self.widget_id]["selectedCell"]
|
|
648
672
|
if cell_data is None:
|
|
649
673
|
return None
|
|
650
674
|
row_index = cell_data["idx"]
|
|
651
|
-
row = cell_data["row"]
|
|
652
675
|
column_index = cell_data["column"]
|
|
676
|
+
if column_index is None or row_index is None:
|
|
677
|
+
return None
|
|
653
678
|
column_name = self._columns_first_idx[column_index]
|
|
654
|
-
|
|
655
|
-
|
|
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):
|
|
656
684
|
return None
|
|
657
685
|
return self.ClickedCell(row, column_index, row_index, column_name, column_value)
|
|
658
686
|
|
|
@@ -715,7 +743,25 @@ class FastTable(Widget):
|
|
|
715
743
|
self._parsed_active_data,
|
|
716
744
|
) = self._prepare_working_data()
|
|
717
745
|
self._rows_total = len(self._parsed_source_data["data"])
|
|
718
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
746
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
747
|
+
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
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"])
|
|
719
765
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
720
766
|
DataJson().send_changes()
|
|
721
767
|
self._maybe_update_selected_row()
|
|
@@ -743,7 +789,7 @@ class FastTable(Widget):
|
|
|
743
789
|
self._parsed_active_data,
|
|
744
790
|
) = self._prepare_working_data()
|
|
745
791
|
self._rows_total = len(self._parsed_source_data["data"])
|
|
746
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
792
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
747
793
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
748
794
|
self._maybe_update_selected_row()
|
|
749
795
|
return popped_row
|
|
@@ -755,7 +801,7 @@ class FastTable(Widget):
|
|
|
755
801
|
self._sliced_data = pd.DataFrame(columns=self._columns_first_idx)
|
|
756
802
|
self._parsed_active_data = {"data": [], "columns": []}
|
|
757
803
|
self._rows_total = 0
|
|
758
|
-
DataJson()[self.widget_id]["data"] =
|
|
804
|
+
DataJson()[self.widget_id]["data"] = {}
|
|
759
805
|
DataJson()[self.widget_id]["total"] = 0
|
|
760
806
|
DataJson().send_changes()
|
|
761
807
|
self._maybe_update_selected_row()
|
|
@@ -856,7 +902,11 @@ class FastTable(Widget):
|
|
|
856
902
|
self._refresh()
|
|
857
903
|
|
|
858
904
|
def _default_search_function(self, data: pd.DataFrame, search_value: str) -> pd.DataFrame:
|
|
859
|
-
|
|
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.lower() in str(x).lower()).any(axis=1)]
|
|
908
|
+
else:
|
|
909
|
+
data = data[data.applymap(lambda x: search_value.lower() in str(x).lower()).any(axis=1)]
|
|
860
910
|
return data
|
|
861
911
|
|
|
862
912
|
def _search(self, search_value: str) -> pd.DataFrame:
|
|
@@ -867,8 +917,14 @@ class FastTable(Widget):
|
|
|
867
917
|
:return: Filtered data
|
|
868
918
|
:rtype: pd.DataFrame
|
|
869
919
|
"""
|
|
870
|
-
filtered_data
|
|
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()
|
|
923
|
+
else:
|
|
924
|
+
filtered_data = self._source_data.copy()
|
|
925
|
+
|
|
871
926
|
if search_value == "":
|
|
927
|
+
self._search_str = search_value
|
|
872
928
|
return filtered_data
|
|
873
929
|
if self._search_str != search_value:
|
|
874
930
|
self._active_page = 1
|
|
@@ -894,7 +950,24 @@ class FastTable(Widget):
|
|
|
894
950
|
else:
|
|
895
951
|
ascending = False
|
|
896
952
|
try:
|
|
897
|
-
|
|
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")
|
|
898
971
|
except IndexError as e:
|
|
899
972
|
e.args = (
|
|
900
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}",
|
|
@@ -903,29 +976,59 @@ class FastTable(Widget):
|
|
|
903
976
|
return data
|
|
904
977
|
|
|
905
978
|
def sort(
|
|
906
|
-
self,
|
|
979
|
+
self,
|
|
980
|
+
column_idx: Optional[int] = None,
|
|
981
|
+
order: Optional[Literal["asc", "desc"]] = None,
|
|
982
|
+
reset: bool = False,
|
|
907
983
|
) -> None:
|
|
908
984
|
"""Sorts table data by column index and order.
|
|
909
985
|
|
|
910
|
-
: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).
|
|
911
987
|
:type column_idx: Optional[int]
|
|
912
|
-
:param order: Sorting order
|
|
988
|
+
:param order: Sorting order. If None, keeps current order (unless reset=True).
|
|
913
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
|
|
914
1001
|
"""
|
|
915
|
-
|
|
916
|
-
|
|
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
|
+
|
|
917
1016
|
self._validate_sort_attrs()
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
|
922
1023
|
self._filtered_data = self._filter(self._filter_value)
|
|
923
1024
|
self._searched_data = self._search(self._search_str)
|
|
924
1025
|
self._rows_total = len(self._searched_data)
|
|
925
1026
|
self._sorted_data = self._sort_table_data(self._searched_data)
|
|
926
1027
|
self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
|
|
927
1028
|
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
928
|
-
|
|
1029
|
+
|
|
1030
|
+
# Update DataJson with sorted and paginated data
|
|
1031
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
929
1032
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
930
1033
|
self._maybe_update_selected_row()
|
|
931
1034
|
StateJson().send_changes()
|
|
@@ -933,22 +1036,22 @@ class FastTable(Widget):
|
|
|
933
1036
|
def _prepare_json_data(self, data: dict, key: str):
|
|
934
1037
|
if key in ("data", "columns"):
|
|
935
1038
|
default_value = []
|
|
1039
|
+
elif key == "options":
|
|
1040
|
+
default_value = {}
|
|
936
1041
|
else:
|
|
937
1042
|
default_value = None
|
|
1043
|
+
|
|
938
1044
|
source_data = data.get(key, default_value)
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
)
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
if column_idx is not None:
|
|
950
|
-
sort["column"] = sort.get("columnIndex")
|
|
951
|
-
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
|
+
|
|
952
1055
|
return source_data
|
|
953
1056
|
|
|
954
1057
|
def _validate_sort(
|
|
@@ -1030,12 +1133,21 @@ class FastTable(Widget):
|
|
|
1030
1133
|
def _get_pandas_unpacked_data(self, data: pd.DataFrame) -> dict:
|
|
1031
1134
|
if not isinstance(data, pd.DataFrame):
|
|
1032
1135
|
raise TypeError("Cannot parse input data, please use Pandas Dataframe as input data")
|
|
1033
|
-
|
|
1034
|
-
#
|
|
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()
|
|
1035
1147
|
|
|
1036
1148
|
unpacked_data = {
|
|
1037
|
-
"columns":
|
|
1038
|
-
"data":
|
|
1149
|
+
"columns": columns,
|
|
1150
|
+
"data": display_data.values.tolist(),
|
|
1039
1151
|
}
|
|
1040
1152
|
return unpacked_data
|
|
1041
1153
|
|
|
@@ -1206,7 +1318,7 @@ class FastTable(Widget):
|
|
|
1206
1318
|
|
|
1207
1319
|
self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
|
|
1208
1320
|
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
1209
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
1321
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
1210
1322
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
1211
1323
|
DataJson().send_changes()
|
|
1212
1324
|
StateJson().send_changes()
|
|
@@ -1275,6 +1387,7 @@ class FastTable(Widget):
|
|
|
1275
1387
|
|
|
1276
1388
|
def select_row_by_value(self, column, value: Any):
|
|
1277
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.
|
|
1278
1391
|
|
|
1279
1392
|
:param column: Column name to filter by
|
|
1280
1393
|
:type column: str
|
|
@@ -1288,7 +1401,12 @@ class FastTable(Widget):
|
|
|
1288
1401
|
if column not in self._columns_first_idx:
|
|
1289
1402
|
raise ValueError(f"Column '{column}' does not exist in the table.")
|
|
1290
1403
|
|
|
1291
|
-
|
|
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()
|
|
1292
1410
|
if not idx:
|
|
1293
1411
|
raise ValueError(f"No rows found with {column} = {value}.")
|
|
1294
1412
|
if len(idx) > 1:
|
|
@@ -1299,6 +1417,7 @@ class FastTable(Widget):
|
|
|
1299
1417
|
|
|
1300
1418
|
def select_rows_by_value(self, column, values: List):
|
|
1301
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.
|
|
1302
1421
|
|
|
1303
1422
|
:param column: Column name to filter by
|
|
1304
1423
|
:type column: str
|
|
@@ -1312,7 +1431,12 @@ class FastTable(Widget):
|
|
|
1312
1431
|
if column not in self._columns_first_idx:
|
|
1313
1432
|
raise ValueError(f"Column '{column}' does not exist in the table.")
|
|
1314
1433
|
|
|
1315
|
-
|
|
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()
|
|
1316
1440
|
self.select_rows(idxs)
|
|
1317
1441
|
|
|
1318
1442
|
def _read_custom_columns(self, columns: List[Union[str, tuple]]) -> None:
|
|
@@ -1335,4 +1459,4 @@ class FastTable(Widget):
|
|
|
1335
1459
|
else:
|
|
1336
1460
|
raise TypeError(f"Column name must be a string or a tuple, got {type(col)}")
|
|
1337
1461
|
|
|
1338
|
-
self._validate_sort_attrs()
|
|
1462
|
+
self._validate_sort_attrs()
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
:project-meta="data.{{{widget.widget_id}}}.projectMeta"
|
|
12
12
|
:sort.sync="state.{{{widget.widget_id}}}.sort"
|
|
13
13
|
:search.sync="state.{{{widget.widget_id}}}.search"
|
|
14
|
-
:data="data.{{{widget.widget_id}}}.data"
|
|
14
|
+
:data="Object.values(data.{{{widget.widget_id}}}.data || [])"
|
|
15
15
|
:show-header="data.{{{widget.widget_id}}}.showHeader"
|
|
16
16
|
:selected-rows="state.{{{widget.widget_id}}}.selectedRows"
|
|
17
17
|
:selected-radio-idx="state.{{{widget.widget_id}}}.selectedRows && state.{{{widget.widget_id}}}.selectedRows.length > 0 ? state.{{{widget.widget_id}}}.selectedRows[0].idx : null"
|
|
File without changes
|