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.
Files changed (189) hide show
  1. supervisely/__init__.py +25 -1
  2. supervisely/annotation/annotation.py +8 -2
  3. supervisely/annotation/json_geometries_map.py +13 -12
  4. supervisely/api/annotation_api.py +6 -3
  5. supervisely/api/api.py +2 -0
  6. supervisely/api/app_api.py +10 -1
  7. supervisely/api/dataset_api.py +74 -12
  8. supervisely/api/entities_collection_api.py +10 -0
  9. supervisely/api/entity_annotation/figure_api.py +28 -0
  10. supervisely/api/entity_annotation/object_api.py +3 -3
  11. supervisely/api/entity_annotation/tag_api.py +63 -12
  12. supervisely/api/guides_api.py +210 -0
  13. supervisely/api/image_api.py +4 -0
  14. supervisely/api/labeling_job_api.py +83 -1
  15. supervisely/api/labeling_queue_api.py +33 -7
  16. supervisely/api/module_api.py +5 -0
  17. supervisely/api/project_api.py +71 -26
  18. supervisely/api/storage_api.py +3 -1
  19. supervisely/api/task_api.py +13 -2
  20. supervisely/api/team_api.py +4 -3
  21. supervisely/api/video/video_annotation_api.py +119 -3
  22. supervisely/api/video/video_api.py +65 -14
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +23 -7
  25. supervisely/app/development/development.py +18 -2
  26. supervisely/app/fastapi/__init__.py +1 -0
  27. supervisely/app/fastapi/custom_static_files.py +1 -1
  28. supervisely/app/fastapi/multi_user.py +105 -0
  29. supervisely/app/fastapi/subapp.py +88 -42
  30. supervisely/app/fastapi/websocket.py +77 -9
  31. supervisely/app/singleton.py +21 -0
  32. supervisely/app/v1/app_service.py +18 -2
  33. supervisely/app/v1/constants.py +7 -1
  34. supervisely/app/widgets/__init__.py +6 -0
  35. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  36. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  37. supervisely/app/widgets/activity_feed/style.css +78 -0
  38. supervisely/app/widgets/activity_feed/template.html +22 -0
  39. supervisely/app/widgets/card/card.py +20 -0
  40. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  41. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  42. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  43. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  44. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  45. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  46. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  47. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  48. supervisely/app/widgets/fast_table/template.html +1 -1
  49. supervisely/app/widgets/heatmap/__init__.py +0 -0
  50. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  51. supervisely/app/widgets/heatmap/script.js +533 -0
  52. supervisely/app/widgets/heatmap/style.css +233 -0
  53. supervisely/app/widgets/heatmap/template.html +21 -0
  54. supervisely/app/widgets/modal/__init__.py +0 -0
  55. supervisely/app/widgets/modal/modal.py +198 -0
  56. supervisely/app/widgets/modal/template.html +10 -0
  57. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  58. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  59. supervisely/app/widgets/radio_tabs/template.html +1 -0
  60. supervisely/app/widgets/select/select.py +6 -3
  61. supervisely/app/widgets/select_class/__init__.py +0 -0
  62. supervisely/app/widgets/select_class/select_class.py +363 -0
  63. supervisely/app/widgets/select_class/template.html +50 -0
  64. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  65. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  66. supervisely/app/widgets/select_tag/__init__.py +0 -0
  67. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  68. supervisely/app/widgets/select_tag/template.html +64 -0
  69. supervisely/app/widgets/select_team/select_team.py +37 -4
  70. supervisely/app/widgets/select_team/template.html +4 -5
  71. supervisely/app/widgets/select_user/__init__.py +0 -0
  72. supervisely/app/widgets/select_user/select_user.py +270 -0
  73. supervisely/app/widgets/select_user/template.html +13 -0
  74. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  75. supervisely/app/widgets/select_workspace/template.html +9 -12
  76. supervisely/app/widgets/table/table.py +68 -13
  77. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  78. supervisely/aug/aug.py +6 -2
  79. supervisely/convert/base_converter.py +1 -0
  80. supervisely/convert/converter.py +2 -2
  81. supervisely/convert/image/image_converter.py +3 -1
  82. supervisely/convert/image/image_helper.py +48 -4
  83. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  84. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  85. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  86. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  87. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  88. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  89. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  90. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  91. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  92. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  93. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  94. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  95. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  96. supervisely/convert/video/__init__.py +1 -0
  97. supervisely/convert/video/multi_view/__init__.py +0 -0
  98. supervisely/convert/video/multi_view/multi_view.py +543 -0
  99. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  100. supervisely/convert/video/video_converter.py +22 -2
  101. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  102. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  103. supervisely/geometry/constants.py +1 -0
  104. supervisely/geometry/geometry.py +4 -0
  105. supervisely/geometry/helpers.py +5 -1
  106. supervisely/geometry/oriented_bbox.py +676 -0
  107. supervisely/geometry/rectangle.py +2 -1
  108. supervisely/io/env.py +76 -1
  109. supervisely/io/fs.py +21 -0
  110. supervisely/nn/benchmark/base_evaluator.py +104 -11
  111. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  112. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  113. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  114. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  115. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  116. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  117. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  118. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  119. supervisely/nn/inference/cache.py +43 -18
  120. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  121. supervisely/nn/inference/inference.py +795 -199
  122. supervisely/nn/inference/inference_request.py +42 -9
  123. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  124. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  125. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  126. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  127. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  128. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  129. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  130. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  131. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  132. supervisely/nn/inference/session.py +43 -35
  133. supervisely/nn/inference/tracking/bbox_tracking.py +113 -34
  134. supervisely/nn/inference/tracking/tracker_interface.py +7 -2
  135. supervisely/nn/inference/uploader.py +139 -12
  136. supervisely/nn/live_training/__init__.py +7 -0
  137. supervisely/nn/live_training/api_server.py +111 -0
  138. supervisely/nn/live_training/artifacts_utils.py +243 -0
  139. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  140. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  141. supervisely/nn/live_training/helpers.py +14 -0
  142. supervisely/nn/live_training/incremental_dataset.py +146 -0
  143. supervisely/nn/live_training/live_training.py +497 -0
  144. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  145. supervisely/nn/live_training/request_queue.py +52 -0
  146. supervisely/nn/model/model_api.py +9 -0
  147. supervisely/nn/prediction_dto.py +12 -1
  148. supervisely/nn/tracker/base_tracker.py +11 -1
  149. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  150. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  151. supervisely/nn/tracker/botsort_tracker.py +94 -65
  152. supervisely/nn/tracker/visualize.py +87 -90
  153. supervisely/nn/training/gui/classes_selector.py +16 -1
  154. supervisely/nn/training/train_app.py +28 -29
  155. supervisely/project/data_version.py +115 -51
  156. supervisely/project/download.py +1 -1
  157. supervisely/project/pointcloud_episode_project.py +37 -8
  158. supervisely/project/pointcloud_project.py +30 -2
  159. supervisely/project/project.py +14 -2
  160. supervisely/project/project_meta.py +27 -1
  161. supervisely/project/project_settings.py +32 -18
  162. supervisely/project/versioning/__init__.py +1 -0
  163. supervisely/project/versioning/common.py +20 -0
  164. supervisely/project/versioning/schema_fields.py +35 -0
  165. supervisely/project/versioning/video_schema.py +221 -0
  166. supervisely/project/versioning/volume_schema.py +87 -0
  167. supervisely/project/video_project.py +717 -15
  168. supervisely/project/volume_project.py +623 -5
  169. supervisely/template/experiment/experiment.html.jinja +4 -4
  170. supervisely/template/experiment/experiment_generator.py +14 -21
  171. supervisely/template/live_training/__init__.py +0 -0
  172. supervisely/template/live_training/header.html.jinja +96 -0
  173. supervisely/template/live_training/live_training.html.jinja +51 -0
  174. supervisely/template/live_training/live_training_generator.py +464 -0
  175. supervisely/template/live_training/sly-style.css +402 -0
  176. supervisely/template/live_training/template.html.jinja +18 -0
  177. supervisely/versions.json +28 -26
  178. supervisely/video/sampling.py +39 -20
  179. supervisely/video/video.py +40 -11
  180. supervisely/video_annotation/video_object.py +29 -4
  181. supervisely/volume/stl_converter.py +2 -0
  182. supervisely/worker_api/agent_rpc.py +24 -1
  183. supervisely/worker_api/rpc_servicer.py +31 -7
  184. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/METADATA +56 -39
  185. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/RECORD +189 -142
  186. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  187. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  188. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  189. {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
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
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
- temp_parsed_data = [d["items"] for d in self._parsed_active_data["data"]]
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
- temp_parsed_data = [d["items"] for d in self._parsed_source_data["data"]]
584
- packed_data = pd.DataFrame(data=temp_parsed_data, columns=self._columns_first_idx)
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
- row_data = row.get("row", row.get("items", None))
625
- if row_index is None or row_data is None:
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
- row = clicked_row["row"]
637
- if row_index is None or row is None:
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]["clickedCell"]
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
- column_value = row[column_index]
655
- if column_index is None or row is None:
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
- data = data[data.applymap(lambda x: search_value in str(x)).any(axis=1)]
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 = self._filtered_data.copy()
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
- data = data.sort_values(by=data.columns[column_idx], ascending=ascending)
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, column_idx: Optional[int] = None, order: Optional[Literal["asc", "desc"]] = None
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
- self._sort_column_idx = column_idx
916
- self._sort_order = order
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
- if self._sort_column_idx is not None:
919
- StateJson()[self.widget_id]["sort"]["column"] = self._sort_column_idx
920
- if self._sort_order is not None:
921
- StateJson()[self.widget_id]["sort"]["order"] = self._sort_order
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
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
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
- if key == "data":
940
- source_data = self._sort_table_data(
941
- pd.DataFrame(data=source_data, columns=self._multi_idx_columns)
942
- )
943
- if key == "options":
944
- options = data.get(key, default_value)
945
- if options is not None:
946
- sort = options.get("sort", None)
947
- if sort is not None:
948
- column_idx = sort.get("columnIndex", None)
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
- data = data.replace({np.nan: None})
1034
- # data = data.astype(object).replace(np.nan, "-") # TODO: replace None later
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": data.columns.to_list(),
1038
- "data": data.values.tolist(),
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
- idx = self._source_data[self._source_data[column] == value].index.tolist()
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
- idxs = self._source_data[self._source_data[column].isin(values)].index.tolist()
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