dataframe-textual 1.16.2__py3-none-any.whl → 2.0.1__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.
- dataframe_textual/__init__.py +27 -1
- dataframe_textual/__main__.py +7 -0
- dataframe_textual/common.py +15 -9
- dataframe_textual/data_frame_help_panel.py +0 -3
- dataframe_textual/data_frame_table.py +622 -559
- dataframe_textual/data_frame_viewer.py +16 -8
- dataframe_textual/sql_screen.py +8 -2
- dataframe_textual/table_screen.py +25 -51
- dataframe_textual/yes_no_screen.py +9 -18
- {dataframe_textual-1.16.2.dist-info → dataframe_textual-2.0.1.dist-info}/METADATA +10 -10
- dataframe_textual-2.0.1.dist-info/RECORD +14 -0
- dataframe_textual-1.16.2.dist-info/RECORD +0 -14
- {dataframe_textual-1.16.2.dist-info → dataframe_textual-2.0.1.dist-info}/WHEEL +0 -0
- {dataframe_textual-1.16.2.dist-info → dataframe_textual-2.0.1.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-1.16.2.dist-info → dataframe_textual-2.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,7 +10,6 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
import polars as pl
|
|
12
12
|
from rich.text import Text, TextType
|
|
13
|
-
from textual import work
|
|
14
13
|
from textual._two_way_dict import TwoWayDict
|
|
15
14
|
from textual.coordinate import Coordinate
|
|
16
15
|
from textual.events import Click
|
|
@@ -32,7 +31,7 @@ from .common import (
|
|
|
32
31
|
CURSOR_TYPES,
|
|
33
32
|
NULL,
|
|
34
33
|
NULL_DISPLAY,
|
|
35
|
-
|
|
34
|
+
RID,
|
|
36
35
|
SUBSCRIPT_DIGITS,
|
|
37
36
|
SUPPORTED_FORMATS,
|
|
38
37
|
DtypeConfig,
|
|
@@ -40,7 +39,6 @@ from .common import (
|
|
|
40
39
|
get_next_item,
|
|
41
40
|
parse_placeholders,
|
|
42
41
|
round_to_nearest_hundreds,
|
|
43
|
-
sleep_async,
|
|
44
42
|
tentative_expr,
|
|
45
43
|
validate_expr,
|
|
46
44
|
)
|
|
@@ -79,16 +77,16 @@ class History:
|
|
|
79
77
|
|
|
80
78
|
description: str
|
|
81
79
|
df: pl.DataFrame
|
|
80
|
+
df_view: pl.DataFrame | None
|
|
82
81
|
filename: str
|
|
83
82
|
loaded_rows: int
|
|
84
|
-
sorted_columns: dict[str, bool]
|
|
85
83
|
hidden_columns: set[str]
|
|
86
|
-
selected_rows:
|
|
87
|
-
|
|
84
|
+
selected_rows: set[int]
|
|
85
|
+
sorted_columns: dict[str, bool] # col_name -> descending
|
|
88
86
|
fixed_rows: int
|
|
89
87
|
fixed_columns: int
|
|
90
88
|
cursor_coordinate: Coordinate
|
|
91
|
-
matches: dict[int, set[
|
|
89
|
+
matches: dict[int, set[str]] # RID -> set of col names
|
|
92
90
|
dirty: bool = False # Whether this history state has unsaved changes
|
|
93
91
|
|
|
94
92
|
|
|
@@ -155,7 +153,7 @@ class DataFrameTable(DataTable):
|
|
|
155
153
|
- *(Multi-column sort supported)*
|
|
156
154
|
|
|
157
155
|
## ✅ Row Selection
|
|
158
|
-
- **\\\\** - ✅ Select rows
|
|
156
|
+
- **\\\\** - ✅ Select rows with cell matches or those matching cursor value in current column
|
|
159
157
|
- **|** - ✅ Select rows with expression
|
|
160
158
|
- **'** - ✅ Select/deselect current row
|
|
161
159
|
- **t** - 💡 Toggle row selection (invert all)
|
|
@@ -177,8 +175,8 @@ class DataFrameTable(DataTable):
|
|
|
177
175
|
|
|
178
176
|
## 👁️ View & Filter
|
|
179
177
|
- **"** - 📍 Filter selected rows (removes others)
|
|
180
|
-
- **v** - 👁️ View
|
|
181
|
-
- **V** - 🔧 View rows
|
|
178
|
+
- **v** - 👁️ View selected rows (hides others)
|
|
179
|
+
- **V** - 🔧 View selected rows matching expression (hides others)
|
|
182
180
|
|
|
183
181
|
## 🔍 SQL Interface
|
|
184
182
|
- **l** - 💬 Open simple SQL interface (select columns & where clause)
|
|
@@ -221,10 +219,8 @@ class DataFrameTable(DataTable):
|
|
|
221
219
|
# Navigation
|
|
222
220
|
("g", "jump_top", "Jump to top"),
|
|
223
221
|
("G", "jump_bottom", "Jump to bottom"),
|
|
224
|
-
("ctrl+
|
|
225
|
-
("ctrl+
|
|
226
|
-
("pageup", "page_up", "Page up"),
|
|
227
|
-
("pagedown", "page_down", "Page down"),
|
|
222
|
+
("pageup,ctrl+b", "page_up", "Page up"),
|
|
223
|
+
("pagedown,ctrl+f", "page_down", "Page down"),
|
|
228
224
|
# Undo/Redo/Reset
|
|
229
225
|
("u", "undo", "Undo"),
|
|
230
226
|
("U", "redo", "Redo"),
|
|
@@ -237,6 +233,7 @@ class DataFrameTable(DataTable):
|
|
|
237
233
|
("z", "freeze_row_column", "Freeze rows/columns"),
|
|
238
234
|
("comma", "show_thousand_separator", "Toggle thousand separator"), # `,`
|
|
239
235
|
("underscore", "expand_column", "Expand column to full width"), # `_`
|
|
236
|
+
("circumflex_accent", "toggle_rid", "Toggle internal row index"), # `^`
|
|
240
237
|
# Copy
|
|
241
238
|
("c", "copy_cell", "Copy cell to clipboard"),
|
|
242
239
|
("ctrl+c", "copy_column", "Copy column to clipboard"),
|
|
@@ -254,11 +251,11 @@ class DataFrameTable(DataTable):
|
|
|
254
251
|
("left_square_bracket", "sort_ascending", "Sort ascending"), # `[`
|
|
255
252
|
("right_square_bracket", "sort_descending", "Sort descending"), # `]`
|
|
256
253
|
# View & Filter
|
|
257
|
-
("v", "view_rows", "View rows"),
|
|
258
|
-
("V", "view_rows_expr", "View rows
|
|
259
|
-
("quotation_mark", "filter_rows", "Filter selected"), # `"`
|
|
254
|
+
("v", "view_rows", "View selected rows"),
|
|
255
|
+
("V", "view_rows_expr", "View selected rows matching expression"),
|
|
256
|
+
("quotation_mark", "filter_rows", "Filter selected rows"), # `"`
|
|
260
257
|
# Row Selection
|
|
261
|
-
("backslash", "
|
|
258
|
+
("backslash", "select_row", "Select rows with cell matches or those matching cursor value in current column"), # `\`
|
|
262
259
|
("vertical_line", "select_row_expr", "Select rows with expression"), # `|`
|
|
263
260
|
("right_curly_bracket", "next_selected_row", "Go to next selected row"), # `}`
|
|
264
261
|
("left_curly_bracket", "previous_selected_row", "Go to previous selected row"), # `{`
|
|
@@ -324,34 +321,40 @@ class DataFrameTable(DataTable):
|
|
|
324
321
|
super().__init__(**kwargs)
|
|
325
322
|
|
|
326
323
|
# DataFrame state
|
|
327
|
-
self.dataframe = df # Original dataframe
|
|
328
|
-
self.df =
|
|
324
|
+
self.dataframe = df.lazy().with_row_index(RID).select(pl.exclude(RID), RID).collect() # Original dataframe
|
|
325
|
+
self.df = self.dataframe # Internal/working dataframe
|
|
329
326
|
self.filename = filename or "untitled.csv" # Current filename
|
|
330
327
|
self.tabname = tabname or Path(filename).stem # Tab name
|
|
328
|
+
|
|
329
|
+
# In view mode, this is the copy of self.df
|
|
330
|
+
self.df_view = None
|
|
331
|
+
|
|
331
332
|
# Pagination & Loading
|
|
332
333
|
self.BATCH_SIZE = max((self.app.size.height // 100 + 1) * 100, 100)
|
|
333
334
|
self.loaded_rows = 0 # Track how many rows are currently loaded
|
|
334
335
|
self.loaded_ranges: list[tuple[int, int]] = [] # List of (start, end) row indices that are loaded
|
|
335
336
|
|
|
336
337
|
# State tracking (all 0-based indexing)
|
|
337
|
-
self.sorted_columns: dict[str, bool] = {} # col_name -> descending
|
|
338
338
|
self.hidden_columns: set[str] = set() # Set of hidden column names
|
|
339
|
-
self.selected_rows:
|
|
340
|
-
self.
|
|
341
|
-
self.matches: dict[int, set[
|
|
339
|
+
self.selected_rows: set[int] = set() # Track selected rows by RID
|
|
340
|
+
self.sorted_columns: dict[str, bool] = {} # col_name -> descending
|
|
341
|
+
self.matches: dict[int, set[str]] = defaultdict(set) # Track search matches: RID -> set of col_names
|
|
342
342
|
|
|
343
343
|
# Freezing
|
|
344
344
|
self.fixed_rows = 0 # Number of fixed rows
|
|
345
345
|
self.fixed_columns = 0 # Number of fixed columns
|
|
346
346
|
|
|
347
347
|
# History stack for undo
|
|
348
|
-
self.
|
|
349
|
-
#
|
|
350
|
-
self.
|
|
348
|
+
self.histories_undo: deque[History] = deque()
|
|
349
|
+
# History stack for redo
|
|
350
|
+
self.histories_redo: deque[History] = deque()
|
|
351
351
|
|
|
352
352
|
# Whether to use thousand separator for numeric display
|
|
353
353
|
self.thousand_separator = False
|
|
354
354
|
|
|
355
|
+
# Whether to show internal row index column
|
|
356
|
+
self.show_rid = False
|
|
357
|
+
|
|
355
358
|
@property
|
|
356
359
|
def cursor_key(self) -> CellKey:
|
|
357
360
|
"""Get the current cursor position as a CellKey.
|
|
@@ -418,22 +421,13 @@ class DataFrameTable(DataTable):
|
|
|
418
421
|
|
|
419
422
|
@property
|
|
420
423
|
def cursor_value(self) -> Any:
|
|
421
|
-
"""Get the current cursor cell value.
|
|
424
|
+
"""Get the current cursor cell value in the dataframe.
|
|
422
425
|
|
|
423
426
|
Returns:
|
|
424
427
|
Any: The value of the cell at the cursor position.
|
|
425
428
|
"""
|
|
426
429
|
return self.df.item(self.cursor_row_idx, self.cursor_col_idx)
|
|
427
430
|
|
|
428
|
-
@property
|
|
429
|
-
def has_hidden_rows(self) -> bool:
|
|
430
|
-
"""Check if there are any hidden rows.
|
|
431
|
-
|
|
432
|
-
Returns:
|
|
433
|
-
bool: True if there are hidden rows, False otherwise.
|
|
434
|
-
"""
|
|
435
|
-
return any(1 for v in self.visible_rows if v is False)
|
|
436
|
-
|
|
437
431
|
@property
|
|
438
432
|
def ordered_selected_rows(self) -> list[int]:
|
|
439
433
|
"""Get the list of selected row indices in order.
|
|
@@ -441,7 +435,7 @@ class DataFrameTable(DataTable):
|
|
|
441
435
|
Returns:
|
|
442
436
|
list[int]: A list of 0-based row indices that are currently selected.
|
|
443
437
|
"""
|
|
444
|
-
return [ridx for ridx,
|
|
438
|
+
return [ridx for ridx, rid in enumerate(self.df[RID]) if rid in self.selected_rows]
|
|
445
439
|
|
|
446
440
|
@property
|
|
447
441
|
def ordered_matches(self) -> list[tuple[int, int]]:
|
|
@@ -451,19 +445,22 @@ class DataFrameTable(DataTable):
|
|
|
451
445
|
list[tuple[int, int]]: A list of (row_idx, col_idx) tuples for matched cells.
|
|
452
446
|
"""
|
|
453
447
|
matches = []
|
|
454
|
-
for ridx in sorted(self.matches.keys()):
|
|
455
|
-
for cidx in sorted(self.matches[ridx]):
|
|
456
|
-
matches.append((ridx, cidx))
|
|
457
|
-
return matches
|
|
458
448
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
449
|
+
# Uniq columns
|
|
450
|
+
cols_to_check = set()
|
|
451
|
+
for cols in self.matches.values():
|
|
452
|
+
cols_to_check.update(cols)
|
|
462
453
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
454
|
+
# Ordered columns
|
|
455
|
+
cidx2col = {cidx: col for cidx, col in enumerate(self.df.columns) if col in cols_to_check}
|
|
456
|
+
|
|
457
|
+
for ridx, rid in enumerate(self.df[RID]):
|
|
458
|
+
if cols := self.matches.get(rid):
|
|
459
|
+
for cidx, col in cidx2col.items():
|
|
460
|
+
if col in cols:
|
|
461
|
+
matches.append((ridx, cidx))
|
|
462
|
+
|
|
463
|
+
return matches
|
|
467
464
|
|
|
468
465
|
def _round_to_nearest_hundreds(self, num: int):
|
|
469
466
|
"""Round a number to the nearest hundreds.
|
|
@@ -677,42 +674,19 @@ class DataFrameTable(DataTable):
|
|
|
677
674
|
# Action handlers for BINDINGS
|
|
678
675
|
def action_jump_top(self) -> None:
|
|
679
676
|
"""Jump to the top of the table."""
|
|
680
|
-
self.
|
|
677
|
+
self.do_jump_top()
|
|
681
678
|
|
|
682
679
|
def action_jump_bottom(self) -> None:
|
|
683
680
|
"""Jump to the bottom of the table."""
|
|
684
|
-
|
|
685
|
-
start = max(0, ((stop - self.BATCH_SIZE) // self.BATCH_SIZE + 1) * self.BATCH_SIZE)
|
|
686
|
-
self.load_rows_range(start, stop)
|
|
687
|
-
self.move_cursor(row=self.row_count - 1)
|
|
681
|
+
self.do_jump_bottom()
|
|
688
682
|
|
|
689
683
|
def action_page_up(self) -> None:
|
|
690
684
|
"""Move the cursor one page up."""
|
|
691
|
-
self.
|
|
692
|
-
if self.show_cursor and self.cursor_type in ("cell", "row"):
|
|
693
|
-
height = self.scrollable_content_region.height - (self.header_height if self.show_header else 0)
|
|
694
|
-
|
|
695
|
-
col_idx = self.cursor_column
|
|
696
|
-
ridx = self.cursor_row_idx
|
|
697
|
-
next_ridx = max(0, ridx - height - BUFFER_SIZE)
|
|
698
|
-
start, stop = self._round_to_nearest_hundreds(next_ridx)
|
|
699
|
-
self.load_rows_range(start, stop)
|
|
700
|
-
|
|
701
|
-
self.move_cursor(row=self.get_row_idx(str(next_ridx)), column=col_idx)
|
|
702
|
-
else:
|
|
703
|
-
super().action_page_up()
|
|
685
|
+
self.do_page_up()
|
|
704
686
|
|
|
705
687
|
def action_page_down(self) -> None:
|
|
706
|
-
|
|
707
|
-
self.
|
|
708
|
-
|
|
709
|
-
def action_backward_page(self) -> None:
|
|
710
|
-
"""Scroll up one page."""
|
|
711
|
-
self.action_page_up()
|
|
712
|
-
|
|
713
|
-
def action_forward_page(self) -> None:
|
|
714
|
-
"""Scroll down one page."""
|
|
715
|
-
self.action_page_down()
|
|
688
|
+
"""Move the cursor one page down."""
|
|
689
|
+
self.do_page_down()
|
|
716
690
|
|
|
717
691
|
def action_view_row_detail(self) -> None:
|
|
718
692
|
"""View details of the current row."""
|
|
@@ -730,6 +704,10 @@ class DataFrameTable(DataTable):
|
|
|
730
704
|
"""Expand the current column to its full width."""
|
|
731
705
|
self.do_expand_column()
|
|
732
706
|
|
|
707
|
+
def action_toggle_rid(self) -> None:
|
|
708
|
+
"""Toggle the internal row index column visibility."""
|
|
709
|
+
self.do_toggle_rid()
|
|
710
|
+
|
|
733
711
|
def action_show_hidden_rows_columns(self) -> None:
|
|
734
712
|
"""Show all hidden rows/columns."""
|
|
735
713
|
self.do_show_hidden_rows_columns()
|
|
@@ -802,9 +780,9 @@ class DataFrameTable(DataTable):
|
|
|
802
780
|
"""Clear the current cell (set to None)."""
|
|
803
781
|
self.do_clear_cell()
|
|
804
782
|
|
|
805
|
-
def
|
|
783
|
+
def action_select_row(self) -> None:
|
|
806
784
|
"""Select rows with cursor value in the current column."""
|
|
807
|
-
self.
|
|
785
|
+
self.do_select_row()
|
|
808
786
|
|
|
809
787
|
def action_select_row_expr(self) -> None:
|
|
810
788
|
"""Select rows by expression."""
|
|
@@ -1014,51 +992,31 @@ class DataFrameTable(DataTable):
|
|
|
1014
992
|
# Set new dataframe and reset table
|
|
1015
993
|
self.df = new_df
|
|
1016
994
|
self.loaded_rows = 0
|
|
1017
|
-
self.sorted_columns = {}
|
|
1018
995
|
self.hidden_columns = set()
|
|
1019
|
-
self.selected_rows =
|
|
1020
|
-
self.
|
|
996
|
+
self.selected_rows = set()
|
|
997
|
+
self.sorted_columns = {}
|
|
1021
998
|
self.fixed_rows = 0
|
|
1022
999
|
self.fixed_columns = 0
|
|
1023
1000
|
self.matches = defaultdict(set)
|
|
1024
1001
|
# self.histories.clear()
|
|
1025
|
-
# self.
|
|
1002
|
+
# self.histories2.clear()
|
|
1026
1003
|
self.dirty = dirty # Mark as dirty since data changed
|
|
1027
1004
|
|
|
1028
|
-
def setup_table(self
|
|
1005
|
+
def setup_table(self) -> None:
|
|
1029
1006
|
"""Setup the table for display.
|
|
1030
1007
|
|
|
1031
1008
|
Row keys are 0-based indices, which map directly to dataframe row indices.
|
|
1032
1009
|
Column keys are header names from the dataframe.
|
|
1033
1010
|
"""
|
|
1034
1011
|
self.loaded_rows = 0
|
|
1012
|
+
self.loaded_ranges.clear()
|
|
1035
1013
|
self.show_row_labels = True
|
|
1036
1014
|
|
|
1037
|
-
# Reset to original dataframe
|
|
1038
|
-
if reset:
|
|
1039
|
-
self.reset_df(self.dataframe, dirty=False)
|
|
1040
|
-
|
|
1041
|
-
# Lazy load up to BATCH_SIZE visible rows
|
|
1042
|
-
stop, visible_count, row_idx = self.BATCH_SIZE, 0, 0
|
|
1043
|
-
for row_idx, visible in enumerate(self.visible_rows):
|
|
1044
|
-
if not visible:
|
|
1045
|
-
continue
|
|
1046
|
-
visible_count += 1
|
|
1047
|
-
if visible_count > self.BATCH_SIZE:
|
|
1048
|
-
stop = row_idx
|
|
1049
|
-
break
|
|
1050
|
-
else:
|
|
1051
|
-
stop = row_idx
|
|
1052
|
-
|
|
1053
|
-
# Round up to next hundreds
|
|
1054
|
-
if stop % self.BATCH_SIZE != 0:
|
|
1055
|
-
stop = (stop // self.BATCH_SIZE + 1) * self.BATCH_SIZE
|
|
1056
|
-
|
|
1057
1015
|
# Save current cursor position before clearing
|
|
1058
1016
|
row_idx, col_idx = self.cursor_coordinate
|
|
1059
1017
|
|
|
1060
1018
|
self.setup_columns()
|
|
1061
|
-
self.load_rows_range(0,
|
|
1019
|
+
self.load_rows_range(0, self.BATCH_SIZE) # Load initial rows
|
|
1062
1020
|
|
|
1063
1021
|
# Restore cursor position
|
|
1064
1022
|
if row_idx < len(self.rows) and col_idx < len(self.columns):
|
|
@@ -1102,28 +1060,30 @@ class DataFrameTable(DataTable):
|
|
|
1102
1060
|
# Get column label width
|
|
1103
1061
|
# Add padding for sort indicators if any
|
|
1104
1062
|
label_width = measure(self.app.console, col, 1) + 2
|
|
1063
|
+
if dtype != pl.String:
|
|
1064
|
+
available_width -= label_width
|
|
1065
|
+
continue
|
|
1105
1066
|
|
|
1106
1067
|
try:
|
|
1107
1068
|
# Get sample values from the column
|
|
1108
|
-
sample_values = sample_lf.select(col).collect().get_column(col).to_list()
|
|
1069
|
+
sample_values = sample_lf.select(col).collect().get_column(col).drop_nulls().to_list()
|
|
1109
1070
|
if any(val.startswith(("https://", "http://")) for val in sample_values):
|
|
1110
1071
|
continue # Skip link columns so they can auto-size and be clickable
|
|
1111
1072
|
|
|
1112
1073
|
# Find maximum width in sample
|
|
1113
1074
|
max_cell_width = max(
|
|
1114
|
-
(measure(self.app.console,
|
|
1075
|
+
(measure(self.app.console, val, 1) for val in sample_values),
|
|
1115
1076
|
default=label_width,
|
|
1116
1077
|
)
|
|
1117
1078
|
|
|
1118
1079
|
# Set column width to max of label and sampled data (capped at reasonable max)
|
|
1119
1080
|
max_width = max(label_width, max_cell_width)
|
|
1120
|
-
except Exception:
|
|
1081
|
+
except Exception as e:
|
|
1121
1082
|
# If any error, let Textual auto-size
|
|
1122
1083
|
max_width = label_width
|
|
1084
|
+
self.log(f"Error determining width for column '{col}': {e}")
|
|
1123
1085
|
|
|
1124
|
-
|
|
1125
|
-
column_widths[col] = max_width
|
|
1126
|
-
|
|
1086
|
+
column_widths[col] = max_width
|
|
1127
1087
|
available_width -= max_width
|
|
1128
1088
|
|
|
1129
1089
|
# If there's no more available width, auto-size remaining columns
|
|
@@ -1147,8 +1107,8 @@ class DataFrameTable(DataTable):
|
|
|
1147
1107
|
|
|
1148
1108
|
# Add columns with justified headers
|
|
1149
1109
|
for col, dtype in zip(self.df.columns, self.df.dtypes):
|
|
1150
|
-
if col in self.hidden_columns:
|
|
1151
|
-
continue # Skip hidden columns
|
|
1110
|
+
if col in self.hidden_columns or (col == RID and not self.show_rid):
|
|
1111
|
+
continue # Skip hidden columns and internal RID
|
|
1152
1112
|
for idx, c in enumerate(self.sorted_columns, 1):
|
|
1153
1113
|
if c == col:
|
|
1154
1114
|
# Add sort indicator to column header
|
|
@@ -1166,71 +1126,6 @@ class DataFrameTable(DataTable):
|
|
|
1166
1126
|
|
|
1167
1127
|
self.add_column(Text(cell_value, justify=DtypeConfig(dtype).justify), key=col, width=width)
|
|
1168
1128
|
|
|
1169
|
-
def load_rows(self, stop: int | None = None, move_to_end: bool = False) -> None:
|
|
1170
|
-
"""Load a batch of rows into the table (synchronous wrapper).
|
|
1171
|
-
|
|
1172
|
-
Args:
|
|
1173
|
-
stop: Stop loading rows when this index is reached.
|
|
1174
|
-
If None, load until the end of the dataframe.
|
|
1175
|
-
"""
|
|
1176
|
-
if stop is None or stop > len(self.df):
|
|
1177
|
-
stop = len(self.df)
|
|
1178
|
-
|
|
1179
|
-
# If already loaded enough rows, just move cursor if needed
|
|
1180
|
-
if stop <= self.loaded_rows:
|
|
1181
|
-
if move_to_end:
|
|
1182
|
-
self.move_cursor(row=self.row_count - 1)
|
|
1183
|
-
|
|
1184
|
-
return
|
|
1185
|
-
|
|
1186
|
-
# Warn user if loading a large number of rows
|
|
1187
|
-
elif (nrows := stop - self.loaded_rows) >= WARN_ROWS_THRESHOLD:
|
|
1188
|
-
|
|
1189
|
-
def _continue(result: bool) -> None:
|
|
1190
|
-
if result:
|
|
1191
|
-
self.load_rows_async(stop, move_to_end=move_to_end)
|
|
1192
|
-
|
|
1193
|
-
self.app.push_screen(
|
|
1194
|
-
ConfirmScreen(
|
|
1195
|
-
f"Load {nrows} Rows",
|
|
1196
|
-
label="Loading a large number of rows may cause the application to become unresponsive. Do you want to continue?",
|
|
1197
|
-
),
|
|
1198
|
-
callback=_continue,
|
|
1199
|
-
)
|
|
1200
|
-
|
|
1201
|
-
return
|
|
1202
|
-
|
|
1203
|
-
# Load rows asynchronously
|
|
1204
|
-
self.load_rows_async(stop, move_to_end=move_to_end)
|
|
1205
|
-
|
|
1206
|
-
@work(exclusive=True, description="Loading rows...")
|
|
1207
|
-
async def load_rows_async(self, stop: int, move_to_end: bool = False) -> None:
|
|
1208
|
-
"""Perform loading with async to avoid blocking.
|
|
1209
|
-
|
|
1210
|
-
Args:
|
|
1211
|
-
stop: Stop loading rows when this index is reached.
|
|
1212
|
-
move_to_end: If True, move cursor to the last loaded row after loading completes.
|
|
1213
|
-
"""
|
|
1214
|
-
# Load rows in smaller chunks to avoid blocking
|
|
1215
|
-
if stop > self.loaded_rows:
|
|
1216
|
-
self.log(f"Async loading up to row {self.loaded_rows = }, {stop = }")
|
|
1217
|
-
# Load incrementally to avoid one big block
|
|
1218
|
-
# Load max BATCH_SIZE rows at a time
|
|
1219
|
-
chunk_size = min(self.BATCH_SIZE, stop - self.loaded_rows)
|
|
1220
|
-
next_stop = min(self.loaded_rows + chunk_size, stop)
|
|
1221
|
-
self.load_rows_range(self.loaded_rows, next_stop)
|
|
1222
|
-
self.loaded_rows = next_stop
|
|
1223
|
-
|
|
1224
|
-
# If there's more to load, yield to event loop with delay
|
|
1225
|
-
if next_stop < stop:
|
|
1226
|
-
await sleep_async(0.05) # 50ms delay to allow UI updates
|
|
1227
|
-
self.load_rows_async(stop, move_to_end=move_to_end)
|
|
1228
|
-
return
|
|
1229
|
-
|
|
1230
|
-
# After loading completes, move cursor to end if requested
|
|
1231
|
-
if move_to_end:
|
|
1232
|
-
self.call_after_refresh(lambda: self.move_cursor(row=self.row_count - 1))
|
|
1233
|
-
|
|
1234
1129
|
def _calculate_load_range(self, start: int, stop: int) -> list[tuple[int, int]]:
|
|
1235
1130
|
"""Calculate the actual ranges to load, accounting for already-loaded ranges.
|
|
1236
1131
|
|
|
@@ -1262,8 +1157,11 @@ class DataFrameTable(DataTable):
|
|
|
1262
1157
|
# Merge overlapping/adjacent ranges
|
|
1263
1158
|
merged = []
|
|
1264
1159
|
for range_start, range_stop in sorted_ranges:
|
|
1265
|
-
|
|
1266
|
-
|
|
1160
|
+
# Fully covered, no need to load anything
|
|
1161
|
+
if range_start <= start and range_stop >= stop:
|
|
1162
|
+
return []
|
|
1163
|
+
# Overlapping or adjacent: merge
|
|
1164
|
+
elif merged and range_start <= merged[-1][1]:
|
|
1267
1165
|
merged[-1] = (merged[-1][0], max(merged[-1][1], range_stop))
|
|
1268
1166
|
else:
|
|
1269
1167
|
merged.append((range_start, range_stop))
|
|
@@ -1356,23 +1254,20 @@ class DataFrameTable(DataTable):
|
|
|
1356
1254
|
df_slice = self.df.slice(segment_start, segment_stop - segment_start)
|
|
1357
1255
|
|
|
1358
1256
|
# Load each row at the correct position
|
|
1359
|
-
for ridx, row in enumerate(df_slice.rows(), segment_start):
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
is_selected = self.selected_rows[ridx]
|
|
1364
|
-
match_cols = self.matches.get(ridx, set())
|
|
1257
|
+
for (ridx, row), rid in zip(enumerate(df_slice.rows(), segment_start), df_slice[RID]):
|
|
1258
|
+
is_selected = rid in self.selected_rows
|
|
1259
|
+
match_cols = self.matches.get(rid, set())
|
|
1365
1260
|
|
|
1366
1261
|
vals, dtypes, styles = [], [], []
|
|
1367
|
-
for
|
|
1368
|
-
if col in self.hidden_columns:
|
|
1369
|
-
continue # Skip hidden columns
|
|
1262
|
+
for val, col, dtype in zip(row, self.df.columns, self.df.dtypes, strict=True):
|
|
1263
|
+
if col in self.hidden_columns or (col == RID and not self.show_rid):
|
|
1264
|
+
continue # Skip hidden columns and internal RID
|
|
1370
1265
|
|
|
1371
1266
|
vals.append(val)
|
|
1372
1267
|
dtypes.append(dtype)
|
|
1373
1268
|
|
|
1374
1269
|
# Highlight entire row with selection or cells with matches
|
|
1375
|
-
styles.append(HIGHLIGHT_COLOR if is_selected or
|
|
1270
|
+
styles.append(HIGHLIGHT_COLOR if is_selected or col in match_cols else None)
|
|
1376
1271
|
|
|
1377
1272
|
formatted_row = format_row(vals, dtypes, styles=styles, thousand_separator=self.thousand_separator)
|
|
1378
1273
|
|
|
@@ -1413,8 +1308,7 @@ class DataFrameTable(DataTable):
|
|
|
1413
1308
|
|
|
1414
1309
|
# If nothing needs loading, return early
|
|
1415
1310
|
if not ranges_to_load:
|
|
1416
|
-
|
|
1417
|
-
return 0
|
|
1311
|
+
return 0 # Already loaded
|
|
1418
1312
|
|
|
1419
1313
|
# Track the number of loaded rows in this range
|
|
1420
1314
|
range_count = 0
|
|
@@ -1446,26 +1340,12 @@ class DataFrameTable(DataTable):
|
|
|
1446
1340
|
if top_row_key:
|
|
1447
1341
|
top_ridx = int(top_row_key.value)
|
|
1448
1342
|
else:
|
|
1449
|
-
top_ridx = 0
|
|
1450
|
-
self.log(f"No top row key at index {top_row_index}, defaulting to 0")
|
|
1343
|
+
top_ridx = 0 # No top row key at index, default to 0
|
|
1451
1344
|
|
|
1452
1345
|
# Load upward
|
|
1453
1346
|
start, stop = self._round_to_nearest_hundreds(top_ridx - BUFFER_SIZE * 2)
|
|
1454
1347
|
range_count = self.load_rows_range(start, stop)
|
|
1455
1348
|
|
|
1456
|
-
# self.log(
|
|
1457
|
-
# "========",
|
|
1458
|
-
# f"{self.scrollable_content_region.height = },",
|
|
1459
|
-
# f"{self.header_height = },",
|
|
1460
|
-
# f"{self.scroll_y = },",
|
|
1461
|
-
# f"{top_row_index = },",
|
|
1462
|
-
# f"{top_ridx = },",
|
|
1463
|
-
# f"{start = },",
|
|
1464
|
-
# f"{stop = },",
|
|
1465
|
-
# f"{range_count = },",
|
|
1466
|
-
# f"{self.loaded_ranges = }",
|
|
1467
|
-
# )
|
|
1468
|
-
|
|
1469
1349
|
# Adjust scroll to maintain position if rows were loaded above
|
|
1470
1350
|
if range_count > 0:
|
|
1471
1351
|
self.move_cursor(row=top_row_index + range_count)
|
|
@@ -1477,33 +1357,19 @@ class DataFrameTable(DataTable):
|
|
|
1477
1357
|
if self.loaded_rows >= len(self.df):
|
|
1478
1358
|
return
|
|
1479
1359
|
|
|
1480
|
-
visible_row_count = self.scrollable_content_region.height - self.header_height
|
|
1360
|
+
visible_row_count = self.scrollable_content_region.height - (self.header_height if self.show_header else 0)
|
|
1481
1361
|
bottom_row_index = self.scroll_y + visible_row_count - BUFFER_SIZE
|
|
1482
1362
|
|
|
1483
1363
|
bottom_row_key = self.get_row_key(bottom_row_index)
|
|
1484
1364
|
if bottom_row_key:
|
|
1485
1365
|
bottom_ridx = int(bottom_row_key.value)
|
|
1486
1366
|
else:
|
|
1487
|
-
bottom_ridx = 0
|
|
1488
|
-
self.log(f"No bottom row key at index {bottom_row_index}, defaulting to 0")
|
|
1367
|
+
bottom_ridx = 0 # No bottom row key at index, default to 0
|
|
1489
1368
|
|
|
1490
1369
|
# Load downward
|
|
1491
1370
|
start, stop = self._round_to_nearest_hundreds(bottom_ridx + BUFFER_SIZE * 2)
|
|
1492
1371
|
range_count = self.load_rows_range(start, stop)
|
|
1493
1372
|
|
|
1494
|
-
# self.log(
|
|
1495
|
-
# "========",
|
|
1496
|
-
# f"{self.scrollable_content_region.height = },",
|
|
1497
|
-
# f"{self.header_height = },",
|
|
1498
|
-
# f"{self.scroll_y = },",
|
|
1499
|
-
# f"{bottom_row_index = },",
|
|
1500
|
-
# f"{bottom_ridx = },",
|
|
1501
|
-
# f"{start = },",
|
|
1502
|
-
# f"{stop = },",
|
|
1503
|
-
# f"{range_count = },",
|
|
1504
|
-
# f"{self.loaded_ranges = }",
|
|
1505
|
-
# )
|
|
1506
|
-
|
|
1507
1373
|
if range_count > 0:
|
|
1508
1374
|
self.log(f"Loaded down: {range_count} rows in range {start}-{stop}/{len(self.df)}")
|
|
1509
1375
|
|
|
@@ -1608,18 +1474,58 @@ class DataFrameTable(DataTable):
|
|
|
1608
1474
|
self.check_idle()
|
|
1609
1475
|
return row_key
|
|
1610
1476
|
|
|
1477
|
+
# Navigation
|
|
1478
|
+
def do_jump_top(self) -> None:
|
|
1479
|
+
"""Jump to the top of the table."""
|
|
1480
|
+
self.move_cursor(row=0)
|
|
1481
|
+
|
|
1482
|
+
def do_jump_bottom(self) -> None:
|
|
1483
|
+
"""Jump to the bottom of the table."""
|
|
1484
|
+
stop = len(self.df)
|
|
1485
|
+
start = max(0, stop - self.BATCH_SIZE)
|
|
1486
|
+
|
|
1487
|
+
if start % self.BATCH_SIZE != 0:
|
|
1488
|
+
start = (start // self.BATCH_SIZE + 1) * self.BATCH_SIZE
|
|
1489
|
+
|
|
1490
|
+
if stop - start < self.BATCH_SIZE:
|
|
1491
|
+
start -= self.BATCH_SIZE
|
|
1492
|
+
|
|
1493
|
+
self.load_rows_range(start, stop)
|
|
1494
|
+
self.move_cursor(row=self.row_count - 1)
|
|
1495
|
+
|
|
1496
|
+
def do_page_up(self) -> None:
|
|
1497
|
+
"""Move the cursor one page up."""
|
|
1498
|
+
self._set_hover_cursor(False)
|
|
1499
|
+
if self.show_cursor and self.cursor_type in ("cell", "row"):
|
|
1500
|
+
height = self.scrollable_content_region.height - (self.header_height if self.show_header else 0)
|
|
1501
|
+
|
|
1502
|
+
col_idx = self.cursor_column
|
|
1503
|
+
ridx = self.cursor_row_idx
|
|
1504
|
+
next_ridx = max(0, ridx - height - BUFFER_SIZE)
|
|
1505
|
+
start, stop = self._round_to_nearest_hundreds(next_ridx)
|
|
1506
|
+
self.load_rows_range(start, stop)
|
|
1507
|
+
|
|
1508
|
+
self.move_cursor(row=self.get_row_idx(str(next_ridx)), column=col_idx)
|
|
1509
|
+
else:
|
|
1510
|
+
super().action_page_up()
|
|
1511
|
+
|
|
1512
|
+
def do_page_down(self) -> None:
|
|
1513
|
+
"""Move the cursor one page down."""
|
|
1514
|
+
super().action_page_down()
|
|
1515
|
+
self.load_rows_down()
|
|
1516
|
+
|
|
1611
1517
|
# History & Undo
|
|
1612
1518
|
def create_history(self, description: str) -> None:
|
|
1613
1519
|
"""Create the initial history state."""
|
|
1614
1520
|
return History(
|
|
1615
1521
|
description=description,
|
|
1616
1522
|
df=self.df,
|
|
1523
|
+
df_view=self.df_view,
|
|
1617
1524
|
filename=self.filename,
|
|
1618
1525
|
loaded_rows=self.loaded_rows,
|
|
1619
|
-
sorted_columns=self.sorted_columns.copy(),
|
|
1620
1526
|
hidden_columns=self.hidden_columns.copy(),
|
|
1621
1527
|
selected_rows=self.selected_rows.copy(),
|
|
1622
|
-
|
|
1528
|
+
sorted_columns=self.sorted_columns.copy(),
|
|
1623
1529
|
fixed_rows=self.fixed_rows,
|
|
1624
1530
|
fixed_columns=self.fixed_columns,
|
|
1625
1531
|
cursor_coordinate=self.cursor_coordinate,
|
|
@@ -1634,12 +1540,12 @@ class DataFrameTable(DataTable):
|
|
|
1634
1540
|
|
|
1635
1541
|
# Restore state
|
|
1636
1542
|
self.df = history.df
|
|
1543
|
+
self.df_view = history.df_view
|
|
1637
1544
|
self.filename = history.filename
|
|
1638
1545
|
self.loaded_rows = history.loaded_rows
|
|
1639
|
-
self.sorted_columns = history.sorted_columns.copy()
|
|
1640
1546
|
self.hidden_columns = history.hidden_columns.copy()
|
|
1641
1547
|
self.selected_rows = history.selected_rows.copy()
|
|
1642
|
-
self.
|
|
1548
|
+
self.sorted_columns = history.sorted_columns.copy()
|
|
1643
1549
|
self.fixed_rows = history.fixed_rows
|
|
1644
1550
|
self.fixed_columns = history.fixed_columns
|
|
1645
1551
|
self.cursor_coordinate = history.cursor_coordinate
|
|
@@ -1649,15 +1555,18 @@ class DataFrameTable(DataTable):
|
|
|
1649
1555
|
# Recreate table for display
|
|
1650
1556
|
self.setup_table()
|
|
1651
1557
|
|
|
1652
|
-
def add_history(self, description: str, dirty: bool = False) -> None:
|
|
1558
|
+
def add_history(self, description: str, dirty: bool = False, clear_redo: bool = True) -> None:
|
|
1653
1559
|
"""Add the current state to the history stack.
|
|
1654
1560
|
|
|
1655
1561
|
Args:
|
|
1656
1562
|
description: Description of the action for this history entry.
|
|
1657
1563
|
dirty: Whether this operation modifies the data (True) or just display state (False).
|
|
1658
1564
|
"""
|
|
1659
|
-
|
|
1660
|
-
|
|
1565
|
+
self.histories_undo.append(self.create_history(description))
|
|
1566
|
+
|
|
1567
|
+
# Clear redo stack when a new action is performed
|
|
1568
|
+
if clear_redo:
|
|
1569
|
+
self.histories_redo.clear()
|
|
1661
1570
|
|
|
1662
1571
|
# Mark table as dirty if this operation modifies data
|
|
1663
1572
|
if dirty:
|
|
@@ -1665,52 +1574,43 @@ class DataFrameTable(DataTable):
|
|
|
1665
1574
|
|
|
1666
1575
|
def do_undo(self) -> None:
|
|
1667
1576
|
"""Undo the last action."""
|
|
1668
|
-
if not self.
|
|
1577
|
+
if not self.histories_undo:
|
|
1669
1578
|
self.notify("No actions to undo", title="Undo", severity="warning")
|
|
1670
1579
|
return
|
|
1671
1580
|
|
|
1672
|
-
# Pop the last history state for undo
|
|
1673
|
-
history = self.
|
|
1674
|
-
|
|
1675
|
-
# Save current state for redo
|
|
1676
|
-
self.history = self.create_history(history.description)
|
|
1581
|
+
# Pop the last history state for undo and save to redo stack
|
|
1582
|
+
history = self.histories_undo.pop()
|
|
1583
|
+
self.histories_redo.append(self.create_history(history.description))
|
|
1677
1584
|
|
|
1678
1585
|
# Restore state
|
|
1679
1586
|
self.apply_history(history)
|
|
1680
1587
|
|
|
1681
|
-
self.notify(f"Reverted:
|
|
1588
|
+
self.notify(f"Reverted: {history.description}", title="Undo")
|
|
1682
1589
|
|
|
1683
1590
|
def do_redo(self) -> None:
|
|
1684
1591
|
"""Redo the last undone action."""
|
|
1685
|
-
if self.
|
|
1592
|
+
if not self.histories_redo:
|
|
1686
1593
|
self.notify("No actions to redo", title="Redo", severity="warning")
|
|
1687
1594
|
return
|
|
1688
1595
|
|
|
1689
|
-
|
|
1596
|
+
# Pop the last undone state from redo stack
|
|
1597
|
+
history = self.histories_redo.pop()
|
|
1598
|
+
description = history.description
|
|
1690
1599
|
|
|
1691
1600
|
# Save current state for undo
|
|
1692
|
-
self.add_history(description)
|
|
1601
|
+
self.add_history(description, clear_redo=False)
|
|
1693
1602
|
|
|
1694
1603
|
# Restore state
|
|
1695
|
-
self.apply_history(
|
|
1696
|
-
|
|
1697
|
-
# Clear redo state
|
|
1698
|
-
self.history = None
|
|
1604
|
+
self.apply_history(history)
|
|
1699
1605
|
|
|
1700
|
-
self.notify(f"Reapplied:
|
|
1606
|
+
self.notify(f"Reapplied: {description}", title="Redo")
|
|
1701
1607
|
|
|
1702
1608
|
def do_reset(self) -> None:
|
|
1703
1609
|
"""Reset the table to the initial state."""
|
|
1704
|
-
self.
|
|
1610
|
+
self.reset_df(self.dataframe, dirty=False)
|
|
1611
|
+
self.setup_table()
|
|
1705
1612
|
self.notify("Restored initial state", title="Reset")
|
|
1706
1613
|
|
|
1707
|
-
def restore_dirty(self, default: bool | None = None) -> None:
|
|
1708
|
-
"""Restore the dirty state from the last history entry."""
|
|
1709
|
-
if self.last_history:
|
|
1710
|
-
self.dirty = self.last_history.dirty
|
|
1711
|
-
elif default is not None:
|
|
1712
|
-
self.dirty = default
|
|
1713
|
-
|
|
1714
1614
|
# Display
|
|
1715
1615
|
def do_cycle_cursor_type(self) -> None:
|
|
1716
1616
|
"""Cycle through cursor types: cell -> row -> column -> cell."""
|
|
@@ -1817,14 +1717,20 @@ class DataFrameTable(DataTable):
|
|
|
1817
1717
|
max_width = len(col_name) + 2 # Start with column name width + padding
|
|
1818
1718
|
|
|
1819
1719
|
try:
|
|
1720
|
+
need_expand = False
|
|
1721
|
+
|
|
1820
1722
|
# Scan through all loaded rows that are visible to find max width
|
|
1821
1723
|
for row_idx in range(self.loaded_rows):
|
|
1822
|
-
if not self.visible_rows[row_idx]:
|
|
1823
|
-
continue # Skip hidden rows
|
|
1824
1724
|
cell_value = str(self.df.item(row_idx, col_idx))
|
|
1825
1725
|
cell_width = measure(self.app.console, cell_value, 1)
|
|
1726
|
+
|
|
1727
|
+
if cell_width > max_width:
|
|
1728
|
+
need_expand = True
|
|
1826
1729
|
max_width = max(max_width, cell_width)
|
|
1827
1730
|
|
|
1731
|
+
if not need_expand:
|
|
1732
|
+
return
|
|
1733
|
+
|
|
1828
1734
|
# Update the column width
|
|
1829
1735
|
col = self.columns[col_key]
|
|
1830
1736
|
col.width = max_width
|
|
@@ -1841,32 +1747,34 @@ class DataFrameTable(DataTable):
|
|
|
1841
1747
|
)
|
|
1842
1748
|
self.log(f"Error expanding column `{col_name}`: {str(e)}")
|
|
1843
1749
|
|
|
1844
|
-
def
|
|
1845
|
-
"""
|
|
1846
|
-
|
|
1847
|
-
visible_cols = set(col.key for col in self.ordered_columns)
|
|
1750
|
+
def do_toggle_rid(self) -> None:
|
|
1751
|
+
"""Toggle display of the internal RID column."""
|
|
1752
|
+
self.show_rid = not self.show_rid
|
|
1848
1753
|
|
|
1849
|
-
|
|
1850
|
-
|
|
1754
|
+
# Recreate table for display
|
|
1755
|
+
self.setup_table()
|
|
1851
1756
|
|
|
1852
|
-
|
|
1853
|
-
|
|
1757
|
+
def do_show_hidden_rows_columns(self) -> None:
|
|
1758
|
+
"""Show all hidden rows/columns by recreating the table."""
|
|
1759
|
+
if not self.hidden_columns and self.df_view is None:
|
|
1760
|
+
self.notify("No hidden rows or columns to show", title="Show", severity="warning")
|
|
1854
1761
|
return
|
|
1855
1762
|
|
|
1856
1763
|
# Add to history
|
|
1857
1764
|
self.add_history("Showed hidden rows/columns")
|
|
1858
1765
|
|
|
1766
|
+
# If in a filtered view, restore the full dataframe
|
|
1767
|
+
if self.df_view is not None:
|
|
1768
|
+
self.df = self.df_view
|
|
1769
|
+
self.df_view = None
|
|
1770
|
+
|
|
1859
1771
|
# Clear hidden rows/columns tracking
|
|
1860
|
-
self.visible_rows = [True] * len(self.df)
|
|
1861
1772
|
self.hidden_columns.clear()
|
|
1862
1773
|
|
|
1863
1774
|
# Recreate table for display
|
|
1864
1775
|
self.setup_table()
|
|
1865
1776
|
|
|
1866
|
-
self.notify(
|
|
1867
|
-
f"Showed [$success]{hidden_row_count}[/] hidden row(s) and/or [$accent]{hidden_col_count}[/] column(s)",
|
|
1868
|
-
title="Show",
|
|
1869
|
-
)
|
|
1777
|
+
self.notify("Showed hidden row(s) and/or hidden column(s)", title="Show")
|
|
1870
1778
|
|
|
1871
1779
|
# Sort
|
|
1872
1780
|
def do_sort_by_column(self, descending: bool = False) -> None:
|
|
@@ -1888,41 +1796,39 @@ class DataFrameTable(DataTable):
|
|
|
1888
1796
|
# Add to history
|
|
1889
1797
|
self.add_history(f"Sorted on column [$success]{col_name}[/]", dirty=True)
|
|
1890
1798
|
|
|
1799
|
+
# New column - add to sort
|
|
1891
1800
|
if old_desc is None:
|
|
1892
|
-
# Add new column to sort
|
|
1893
1801
|
self.sorted_columns[col_name] = descending
|
|
1802
|
+
|
|
1803
|
+
# Old column, same direction - remove from sort
|
|
1894
1804
|
elif old_desc == descending:
|
|
1895
|
-
# Same direction - remove from sort
|
|
1896
1805
|
del self.sorted_columns[col_name]
|
|
1806
|
+
|
|
1807
|
+
# Old column, different direction - add to sort at end
|
|
1897
1808
|
else:
|
|
1898
|
-
# Move to end of sort order
|
|
1899
1809
|
del self.sorted_columns[col_name]
|
|
1900
1810
|
self.sorted_columns[col_name] = descending
|
|
1901
1811
|
|
|
1902
|
-
lf = self.df.lazy()
|
|
1812
|
+
lf = self.df.lazy()
|
|
1813
|
+
sort_by = {}
|
|
1903
1814
|
|
|
1904
1815
|
# Apply multi-column sort
|
|
1905
1816
|
if sort_cols := list(self.sorted_columns.keys()):
|
|
1906
1817
|
descending_flags = list(self.sorted_columns.values())
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
if
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
self.matches = {
|
|
1919
|
-
new_ridx: self.matches[old_ridx]
|
|
1920
|
-
for new_ridx, old_ridx in enumerate(old_row_indices)
|
|
1921
|
-
if old_ridx in self.matches
|
|
1922
|
-
}
|
|
1818
|
+
sort_by = {"by": sort_cols, "descending": descending_flags, "nulls_last": True}
|
|
1819
|
+
else:
|
|
1820
|
+
# No sort - restore original order by adding a temporary index column
|
|
1821
|
+
sort_by = {"by": RID}
|
|
1822
|
+
|
|
1823
|
+
# Perform the sort
|
|
1824
|
+
df_sorted = lf.sort(**sort_by).collect()
|
|
1825
|
+
|
|
1826
|
+
# Also update df_view if applicable
|
|
1827
|
+
if self.df_view is not None:
|
|
1828
|
+
self.df_view = self.df_view.lazy().sort(**sort_by).collect()
|
|
1923
1829
|
|
|
1924
1830
|
# Update the dataframe
|
|
1925
|
-
self.df = df_sorted
|
|
1831
|
+
self.df = df_sorted
|
|
1926
1832
|
|
|
1927
1833
|
# Recreate table for display
|
|
1928
1834
|
self.setup_table()
|
|
@@ -1969,6 +1875,17 @@ class DataFrameTable(DataTable):
|
|
|
1969
1875
|
.alias(col_name)
|
|
1970
1876
|
)
|
|
1971
1877
|
|
|
1878
|
+
# Also update the view if applicable
|
|
1879
|
+
if self.df_view is not None:
|
|
1880
|
+
# Get the RID value for this row in df_view
|
|
1881
|
+
ridx_view = self.df.item(ridx, self.df.columns.index(RID))
|
|
1882
|
+
self.df_view = self.df_view.with_columns(
|
|
1883
|
+
pl.when(pl.col(RID) == ridx_view)
|
|
1884
|
+
.then(pl.lit(new_value))
|
|
1885
|
+
.otherwise(pl.col(col_name))
|
|
1886
|
+
.alias(col_name)
|
|
1887
|
+
)
|
|
1888
|
+
|
|
1972
1889
|
# Update the display
|
|
1973
1890
|
cell_value = self.df.item(ridx, cidx)
|
|
1974
1891
|
if cell_value is None:
|
|
@@ -2044,11 +1961,26 @@ class DataFrameTable(DataTable):
|
|
|
2044
1961
|
|
|
2045
1962
|
try:
|
|
2046
1963
|
# Apply the expression to the column
|
|
2047
|
-
self.df = self.df.with_columns(expr.alias(col_name))
|
|
1964
|
+
self.df = self.df.lazy().with_columns(expr.alias(col_name)).collect()
|
|
1965
|
+
|
|
1966
|
+
# Also update the view if applicable
|
|
1967
|
+
# Update the value of col_name in df_view using the value of col_name from df based on RID mapping between them
|
|
1968
|
+
if self.df_view is not None:
|
|
1969
|
+
# Get updated column from df for rows that exist in df_view
|
|
1970
|
+
col_updated = f"^_{col_name}_^"
|
|
1971
|
+
lf_updated = self.df.lazy().select(RID, pl.col(col_name).alias(col_updated))
|
|
1972
|
+
# Join and use coalesce to prefer updated value or keep original
|
|
1973
|
+
self.df_view = (
|
|
1974
|
+
self.df_view.lazy()
|
|
1975
|
+
.join(lf_updated, on=RID, how="left")
|
|
1976
|
+
.with_columns(pl.coalesce(pl.col(col_updated), pl.col(col_name)).alias(col_name))
|
|
1977
|
+
.drop(col_updated)
|
|
1978
|
+
.collect()
|
|
1979
|
+
)
|
|
2048
1980
|
except Exception as e:
|
|
2049
1981
|
self.notify(
|
|
2050
1982
|
f"Error applying expression: [$error]{term}[/] to column [$accent]{col_name}[/]",
|
|
2051
|
-
title="Edit",
|
|
1983
|
+
title="Edit Column",
|
|
2052
1984
|
severity="error",
|
|
2053
1985
|
timeout=10,
|
|
2054
1986
|
)
|
|
@@ -2090,14 +2022,25 @@ class DataFrameTable(DataTable):
|
|
|
2090
2022
|
# Rename the column in the dataframe
|
|
2091
2023
|
self.df = self.df.rename({col_name: new_name})
|
|
2092
2024
|
|
|
2093
|
-
#
|
|
2025
|
+
# Also update the view if applicable
|
|
2026
|
+
if self.df_view is not None:
|
|
2027
|
+
self.df_view = self.df_view.rename({col_name: new_name})
|
|
2028
|
+
|
|
2029
|
+
# Update sorted_columns if this column was sorted and maintain order
|
|
2094
2030
|
if col_name in self.sorted_columns:
|
|
2095
|
-
|
|
2031
|
+
sorted_columns = {}
|
|
2032
|
+
for col, order in self.sorted_columns.items():
|
|
2033
|
+
if col == col_name:
|
|
2034
|
+
sorted_columns[new_name] = order
|
|
2035
|
+
else:
|
|
2036
|
+
sorted_columns[col] = order
|
|
2037
|
+
self.sorted_columns = sorted_columns
|
|
2096
2038
|
|
|
2097
|
-
# Update
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2039
|
+
# Update matches if this column had cell matches
|
|
2040
|
+
for cols in self.matches.values():
|
|
2041
|
+
if col_name in cols:
|
|
2042
|
+
cols.remove(col_name)
|
|
2043
|
+
cols.add(new_name)
|
|
2101
2044
|
|
|
2102
2045
|
# Recreate table for display
|
|
2103
2046
|
self.setup_table()
|
|
@@ -2126,6 +2069,13 @@ class DataFrameTable(DataTable):
|
|
|
2126
2069
|
.alias(col_name)
|
|
2127
2070
|
)
|
|
2128
2071
|
|
|
2072
|
+
# Also update the view if applicable
|
|
2073
|
+
if self.df_view is not None:
|
|
2074
|
+
ridx_view = self.df.item(ridx, self.df.columns.index(RID))
|
|
2075
|
+
self.df_view = self.df_view.with_columns(
|
|
2076
|
+
pl.when(pl.col(RID) == ridx_view).then(pl.lit(None)).otherwise(pl.col(col_name)).alias(col_name)
|
|
2077
|
+
)
|
|
2078
|
+
|
|
2129
2079
|
# Update the display
|
|
2130
2080
|
dtype = self.df.dtypes[cidx]
|
|
2131
2081
|
dc = DtypeConfig(dtype)
|
|
@@ -2144,30 +2094,27 @@ class DataFrameTable(DataTable):
|
|
|
2144
2094
|
self.log(f"Error clearing cell ({ridx}, {col_name}): {str(e)}")
|
|
2145
2095
|
raise e
|
|
2146
2096
|
|
|
2147
|
-
def do_add_column(self, col_name: str = None
|
|
2097
|
+
def do_add_column(self, col_name: str = None) -> None:
|
|
2148
2098
|
"""Add acolumn after the current column."""
|
|
2149
2099
|
cidx = self.cursor_col_idx
|
|
2150
2100
|
|
|
2151
2101
|
if not col_name:
|
|
2152
2102
|
# Generate a unique column name
|
|
2153
2103
|
base_name = "new_col"
|
|
2154
|
-
|
|
2104
|
+
new_col_name = base_name
|
|
2155
2105
|
counter = 1
|
|
2156
|
-
while
|
|
2157
|
-
|
|
2106
|
+
while new_col_name in self.df.columns:
|
|
2107
|
+
new_col_name = f"{base_name}_{counter}"
|
|
2158
2108
|
counter += 1
|
|
2159
2109
|
else:
|
|
2160
|
-
|
|
2110
|
+
new_col_name = col_name
|
|
2161
2111
|
|
|
2162
2112
|
# Add to history
|
|
2163
|
-
self.add_history(f"Added column [$success]{
|
|
2113
|
+
self.add_history(f"Added column [$success]{new_col_name}[/] after column [$accent]{cidx + 1}[/]", dirty=True)
|
|
2164
2114
|
|
|
2165
2115
|
try:
|
|
2166
2116
|
# Create an empty column (all None values)
|
|
2167
|
-
|
|
2168
|
-
new_col = col_value.alias(new_name)
|
|
2169
|
-
else:
|
|
2170
|
-
new_col = pl.lit(col_value).alias(new_name)
|
|
2117
|
+
new_col_name = pl.lit(None).alias(new_col_name)
|
|
2171
2118
|
|
|
2172
2119
|
# Get columns up to current, the new column, then remaining columns
|
|
2173
2120
|
cols = self.df.columns
|
|
@@ -2175,8 +2122,12 @@ class DataFrameTable(DataTable):
|
|
|
2175
2122
|
cols_after = cols[cidx + 1 :]
|
|
2176
2123
|
|
|
2177
2124
|
# Build the new dataframe with columns reordered
|
|
2178
|
-
select_cols = cols_before + [
|
|
2179
|
-
self.df = self.df.with_columns(
|
|
2125
|
+
select_cols = cols_before + [new_col_name] + cols_after
|
|
2126
|
+
self.df = self.df.lazy().with_columns(new_col_name).select(select_cols).collect()
|
|
2127
|
+
|
|
2128
|
+
# Also update the view if applicable
|
|
2129
|
+
if self.df_view is not None:
|
|
2130
|
+
self.df_view = self.df_view.lazy().with_columns(new_col_name).select(select_cols).collect()
|
|
2180
2131
|
|
|
2181
2132
|
# Recreate table for display
|
|
2182
2133
|
self.setup_table()
|
|
@@ -2186,8 +2137,10 @@ class DataFrameTable(DataTable):
|
|
|
2186
2137
|
|
|
2187
2138
|
# self.notify(f"Added column [$success]{new_name}[/]", title="Add Column")
|
|
2188
2139
|
except Exception as e:
|
|
2189
|
-
self.notify(
|
|
2190
|
-
|
|
2140
|
+
self.notify(
|
|
2141
|
+
f"Error adding column [$error]{new_col_name}[/]", title="Add Column", severity="error", timeout=10
|
|
2142
|
+
)
|
|
2143
|
+
self.log(f"Error adding column `{new_col_name}`: {str(e)}")
|
|
2191
2144
|
raise e
|
|
2192
2145
|
|
|
2193
2146
|
def do_add_column_expr(self) -> None:
|
|
@@ -2219,7 +2172,14 @@ class DataFrameTable(DataTable):
|
|
|
2219
2172
|
|
|
2220
2173
|
# Build the new dataframe with columns reordered
|
|
2221
2174
|
select_cols = cols_before + [new_col_name] + cols_after
|
|
2222
|
-
self.df = self.df.
|
|
2175
|
+
self.df = self.df.lazy().with_columns(new_col).select(select_cols).collect()
|
|
2176
|
+
|
|
2177
|
+
# Also update the view if applicable
|
|
2178
|
+
if self.df_view is not None:
|
|
2179
|
+
# Get updated column from df for rows that exist in df_view
|
|
2180
|
+
lf_updated = self.df.lazy().select(RID, pl.col(new_col_name))
|
|
2181
|
+
# Join and use coalesce to prefer updated value or keep original
|
|
2182
|
+
self.df_view = self.df_view.lazy().join(lf_updated, on=RID, how="left").select(select_cols).collect()
|
|
2223
2183
|
|
|
2224
2184
|
# Recreate table for display
|
|
2225
2185
|
self.setup_table()
|
|
@@ -2285,7 +2245,14 @@ class DataFrameTable(DataTable):
|
|
|
2285
2245
|
|
|
2286
2246
|
# Build the new dataframe with columns reordered
|
|
2287
2247
|
select_cols = cols_before + [new_col_name] + cols_after
|
|
2288
|
-
self.df = self.df.with_columns(new_col).select(select_cols)
|
|
2248
|
+
self.df = self.df.lazy().with_columns(new_col).select(select_cols).collect()
|
|
2249
|
+
|
|
2250
|
+
# Also update the view if applicable
|
|
2251
|
+
if self.df_view is not None:
|
|
2252
|
+
# Get updated column from df for rows that exist in df_view
|
|
2253
|
+
lf_updated = self.df.lazy().select(RID, pl.col(new_col_name))
|
|
2254
|
+
# Join and use coalesce to prefer updated value or keep original
|
|
2255
|
+
self.df_view = self.df_view.lazy().join(lf_updated, on=RID, how="left").select(select_cols).collect()
|
|
2289
2256
|
|
|
2290
2257
|
# Recreate table for display
|
|
2291
2258
|
self.setup_table()
|
|
@@ -2352,17 +2319,24 @@ class DataFrameTable(DataTable):
|
|
|
2352
2319
|
if col_name in self.sorted_columns:
|
|
2353
2320
|
del self.sorted_columns[col_name]
|
|
2354
2321
|
|
|
2322
|
+
# Remove from hidden columns if present
|
|
2323
|
+
for col_name in col_names_to_remove:
|
|
2324
|
+
self.hidden_columns.discard(col_name)
|
|
2325
|
+
|
|
2355
2326
|
# Remove from matches
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
self.matches[row_idx].difference_update(col_indices_to_remove)
|
|
2327
|
+
for rid in list(self.matches.keys()):
|
|
2328
|
+
self.matches[rid].difference_update(col_names_to_remove)
|
|
2359
2329
|
# Remove empty entries
|
|
2360
|
-
if not self.matches[
|
|
2361
|
-
del self.matches[
|
|
2330
|
+
if not self.matches[rid]:
|
|
2331
|
+
del self.matches[rid]
|
|
2362
2332
|
|
|
2363
2333
|
# Remove from dataframe
|
|
2364
2334
|
self.df = self.df.drop(col_names_to_remove)
|
|
2365
2335
|
|
|
2336
|
+
# Also update the view if applicable
|
|
2337
|
+
if self.df_view is not None:
|
|
2338
|
+
self.df_view = self.df_view.drop(col_names_to_remove)
|
|
2339
|
+
|
|
2366
2340
|
self.notify(message, title="Delete")
|
|
2367
2341
|
|
|
2368
2342
|
def do_duplicate_column(self) -> None:
|
|
@@ -2373,29 +2347,28 @@ class DataFrameTable(DataTable):
|
|
|
2373
2347
|
col_idx = self.cursor_column
|
|
2374
2348
|
new_col_name = f"{col_name}_copy"
|
|
2375
2349
|
|
|
2350
|
+
# Ensure new column name is unique
|
|
2351
|
+
counter = 1
|
|
2352
|
+
while new_col_name in self.df.columns:
|
|
2353
|
+
new_col_name = f"{new_col_name}{counter}"
|
|
2354
|
+
counter += 1
|
|
2355
|
+
|
|
2376
2356
|
# Add to history
|
|
2377
2357
|
self.add_history(f"Duplicated column [$success]{col_name}[/]", dirty=True)
|
|
2378
2358
|
|
|
2379
2359
|
# Create new column and reorder columns to insert after current column
|
|
2380
2360
|
cols_before = self.df.columns[: cidx + 1]
|
|
2381
2361
|
cols_after = self.df.columns[cidx + 1 :]
|
|
2362
|
+
cols_new = cols_before + [new_col_name] + cols_after
|
|
2382
2363
|
|
|
2383
2364
|
# Add the new column and reorder columns for insertion after current column
|
|
2384
|
-
self.df = self.df.with_columns(pl.col(col_name).alias(new_col_name)).select(
|
|
2385
|
-
list(cols_before) + [new_col_name] + list(cols_after)
|
|
2386
|
-
)
|
|
2365
|
+
self.df = self.df.lazy().with_columns(pl.col(col_name).alias(new_col_name)).select(cols_new).collect()
|
|
2387
2366
|
|
|
2388
|
-
#
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
if col_idx_in_set <= cidx:
|
|
2394
|
-
new_cols.add(col_idx_in_set)
|
|
2395
|
-
else:
|
|
2396
|
-
new_cols.add(col_idx_in_set + 1)
|
|
2397
|
-
new_matches[row_idx] = new_cols
|
|
2398
|
-
self.matches = new_matches
|
|
2367
|
+
# Also update the view if applicable
|
|
2368
|
+
if self.df_view is not None:
|
|
2369
|
+
self.df_view = (
|
|
2370
|
+
self.df_view.lazy().with_columns(pl.col(col_name).alias(new_col_name)).select(cols_new).collect()
|
|
2371
|
+
)
|
|
2399
2372
|
|
|
2400
2373
|
# Recreate table for display
|
|
2401
2374
|
self.setup_table()
|
|
@@ -2411,58 +2384,61 @@ class DataFrameTable(DataTable):
|
|
|
2411
2384
|
Supports deleting multiple selected rows. If no rows are selected, deletes the row at the cursor.
|
|
2412
2385
|
"""
|
|
2413
2386
|
old_count = len(self.df)
|
|
2414
|
-
|
|
2387
|
+
rids_to_delete = set()
|
|
2415
2388
|
|
|
2416
2389
|
# Delete all selected rows
|
|
2417
|
-
if selected_count := self.selected_rows
|
|
2390
|
+
if selected_count := len(self.selected_rows):
|
|
2418
2391
|
history_desc = f"Deleted {selected_count} selected row(s)"
|
|
2419
|
-
|
|
2420
|
-
for ridx, selected in enumerate(self.selected_rows):
|
|
2421
|
-
if selected:
|
|
2422
|
-
predicates[ridx] = False
|
|
2392
|
+
rids_to_delete = self.selected_rows
|
|
2423
2393
|
|
|
2424
2394
|
# Delete current row and those above
|
|
2425
2395
|
elif more == "above":
|
|
2426
2396
|
ridx = self.cursor_row_idx
|
|
2427
2397
|
history_desc = f"Deleted current row [$success]{ridx + 1}[/] and those above"
|
|
2428
|
-
for
|
|
2429
|
-
|
|
2398
|
+
for rid in self.df[RID][: ridx + 1]:
|
|
2399
|
+
rids_to_delete.add(rid)
|
|
2430
2400
|
|
|
2431
2401
|
# Delete current row and those below
|
|
2432
2402
|
elif more == "below":
|
|
2433
2403
|
ridx = self.cursor_row_idx
|
|
2434
2404
|
history_desc = f"Deleted current row [$success]{ridx + 1}[/] and those below"
|
|
2435
|
-
for
|
|
2436
|
-
|
|
2437
|
-
predicates[i] = False
|
|
2405
|
+
for rid in self.df[RID][ridx:]:
|
|
2406
|
+
rids_to_delete.add(rid)
|
|
2438
2407
|
|
|
2439
2408
|
# Delete the row at the cursor
|
|
2440
2409
|
else:
|
|
2441
2410
|
ridx = self.cursor_row_idx
|
|
2442
2411
|
history_desc = f"Deleted row [$success]{ridx + 1}[/]"
|
|
2443
|
-
|
|
2444
|
-
predicates[ridx] = False
|
|
2412
|
+
rids_to_delete.add(self.df[RID][ridx])
|
|
2445
2413
|
|
|
2446
2414
|
# Add to history
|
|
2447
2415
|
self.add_history(history_desc, dirty=True)
|
|
2448
2416
|
|
|
2449
2417
|
# Apply the filter to remove rows
|
|
2450
2418
|
try:
|
|
2451
|
-
|
|
2419
|
+
df_filtered = self.df.lazy().filter(~pl.col(RID).is_in(rids_to_delete)).collect()
|
|
2452
2420
|
except Exception as e:
|
|
2453
2421
|
self.notify(f"Error deleting row(s): {e}", title="Delete", severity="error", timeout=10)
|
|
2454
|
-
self.
|
|
2422
|
+
self.histories_undo.pop() # Remove last history entry
|
|
2455
2423
|
return
|
|
2456
2424
|
|
|
2457
|
-
|
|
2425
|
+
# RIDs of remaining rows
|
|
2426
|
+
ok_rids = set(df_filtered[RID])
|
|
2458
2427
|
|
|
2459
|
-
# Update selected
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
self.visible_rows = [visible for i, visible in enumerate(self.visible_rows) if i in old_row_indices]
|
|
2428
|
+
# Update selected rows tracking
|
|
2429
|
+
if self.selected_rows:
|
|
2430
|
+
self.selected_rows.intersection_update(ok_rids)
|
|
2463
2431
|
|
|
2464
|
-
#
|
|
2465
|
-
self.
|
|
2432
|
+
# Update the dataframe
|
|
2433
|
+
self.df = df_filtered
|
|
2434
|
+
|
|
2435
|
+
# Update matches since row indices have changed
|
|
2436
|
+
if self.matches:
|
|
2437
|
+
self.matches = {rid: cols for rid, cols in self.matches.items() if rid in ok_rids}
|
|
2438
|
+
|
|
2439
|
+
# Also update the view if applicable
|
|
2440
|
+
if self.df_view is not None:
|
|
2441
|
+
self.df_view = self.df_view.lazy().filter(~pl.col(RID).is_in(rids_to_delete)).collect()
|
|
2466
2442
|
|
|
2467
2443
|
# Recreate table for display
|
|
2468
2444
|
self.setup_table()
|
|
@@ -2474,34 +2450,29 @@ class DataFrameTable(DataTable):
|
|
|
2474
2450
|
def do_duplicate_row(self) -> None:
|
|
2475
2451
|
"""Duplicate the currently selected row, inserting it right after the current row."""
|
|
2476
2452
|
ridx = self.cursor_row_idx
|
|
2453
|
+
rid = self.df[RID][ridx]
|
|
2454
|
+
|
|
2455
|
+
lf = self.df.lazy()
|
|
2477
2456
|
|
|
2478
2457
|
# Get the row to duplicate
|
|
2479
|
-
row_to_duplicate =
|
|
2458
|
+
row_to_duplicate = lf.slice(ridx, 1).with_columns(pl.col(RID) + 1)
|
|
2480
2459
|
|
|
2481
2460
|
# Add to history
|
|
2482
2461
|
self.add_history(f"Duplicated row [$success]{ridx + 1}[/]", dirty=True)
|
|
2483
2462
|
|
|
2484
2463
|
# Concatenate: rows before + duplicated row + rows after
|
|
2485
|
-
|
|
2486
|
-
|
|
2464
|
+
lf_before = lf.slice(0, ridx + 1)
|
|
2465
|
+
lf_after = lf.slice(ridx + 1).with_columns(pl.col(RID) + 1)
|
|
2487
2466
|
|
|
2488
2467
|
# Combine the parts
|
|
2489
|
-
self.df = pl.concat([
|
|
2490
|
-
|
|
2491
|
-
#
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
# Update matches to account for new row
|
|
2498
|
-
new_matches = defaultdict(set)
|
|
2499
|
-
for row_idx, cols in self.matches.items():
|
|
2500
|
-
if row_idx <= ridx:
|
|
2501
|
-
new_matches[row_idx] = cols
|
|
2502
|
-
else:
|
|
2503
|
-
new_matches[row_idx + 1] = cols
|
|
2504
|
-
self.matches = new_matches
|
|
2468
|
+
self.df = pl.concat([lf_before, row_to_duplicate, lf_after]).collect()
|
|
2469
|
+
|
|
2470
|
+
# Also update the view if applicable
|
|
2471
|
+
if self.df_view is not None:
|
|
2472
|
+
lf_view = self.df_view.lazy()
|
|
2473
|
+
lf_view_before = lf_view.slice(0, rid + 1)
|
|
2474
|
+
lf_view_after = lf_view.slice(rid + 1).with_columns(pl.col(RID) + 1)
|
|
2475
|
+
self.df_view = pl.concat([lf_view_before, row_to_duplicate, lf_view_after]).collect()
|
|
2505
2476
|
|
|
2506
2477
|
# Recreate table for display
|
|
2507
2478
|
self.setup_table()
|
|
@@ -2567,6 +2538,10 @@ class DataFrameTable(DataTable):
|
|
|
2567
2538
|
cols[cidx], cols[swap_cidx] = cols[swap_cidx], cols[cidx]
|
|
2568
2539
|
self.df = self.df.select(cols)
|
|
2569
2540
|
|
|
2541
|
+
# Also update the view if applicable
|
|
2542
|
+
if self.df_view is not None:
|
|
2543
|
+
self.df_view = self.df_view.select(cols)
|
|
2544
|
+
|
|
2570
2545
|
# self.notify(f"Moved column [$success]{col_name}[/] {direction}", title="Move")
|
|
2571
2546
|
|
|
2572
2547
|
def do_move_row(self, direction: str) -> None:
|
|
@@ -2575,65 +2550,88 @@ class DataFrameTable(DataTable):
|
|
|
2575
2550
|
Args:
|
|
2576
2551
|
direction: "up" to move up, "down" to move down.
|
|
2577
2552
|
"""
|
|
2578
|
-
|
|
2553
|
+
curr_row_idx, col_idx = self.cursor_coordinate
|
|
2579
2554
|
|
|
2580
2555
|
# Validate move is possible
|
|
2581
2556
|
if direction == "up":
|
|
2582
|
-
if
|
|
2557
|
+
if curr_row_idx <= 0:
|
|
2583
2558
|
self.notify("Cannot move row up", title="Move", severity="warning")
|
|
2584
2559
|
return
|
|
2585
|
-
|
|
2560
|
+
swap_row_idx = curr_row_idx - 1
|
|
2586
2561
|
elif direction == "down":
|
|
2587
|
-
if
|
|
2562
|
+
if curr_row_idx >= len(self.rows) - 1:
|
|
2588
2563
|
self.notify("Cannot move row down", title="Move", severity="warning")
|
|
2589
2564
|
return
|
|
2590
|
-
|
|
2565
|
+
swap_row_idx = curr_row_idx + 1
|
|
2591
2566
|
else:
|
|
2592
2567
|
# Invalid direction
|
|
2593
2568
|
return
|
|
2594
2569
|
|
|
2595
|
-
row_key = self.coordinate_to_cell_key((row_idx, 0)).row_key
|
|
2596
|
-
swap_key = self.coordinate_to_cell_key((swap_idx, 0)).row_key
|
|
2597
|
-
|
|
2598
2570
|
# Add to history
|
|
2599
2571
|
self.add_history(
|
|
2600
|
-
f"Moved row [$success]{
|
|
2572
|
+
f"Moved row [$success]{curr_row_idx}[/] [$accent]{direction}[/] (swapped with row [$success]{swap_row_idx}[/])",
|
|
2601
2573
|
dirty=True,
|
|
2602
2574
|
)
|
|
2603
2575
|
|
|
2604
2576
|
# Swap rows in the table's internal row locations
|
|
2577
|
+
curr_key = self.coordinate_to_cell_key((curr_row_idx, 0)).row_key
|
|
2578
|
+
swap_key = self.coordinate_to_cell_key((swap_row_idx, 0)).row_key
|
|
2579
|
+
|
|
2605
2580
|
self.check_idle()
|
|
2606
2581
|
|
|
2607
2582
|
(
|
|
2608
|
-
self._row_locations[
|
|
2583
|
+
self._row_locations[curr_key],
|
|
2609
2584
|
self._row_locations[swap_key],
|
|
2610
2585
|
) = (
|
|
2611
2586
|
self.get_row_idx(swap_key),
|
|
2612
|
-
self.get_row_idx(
|
|
2587
|
+
self.get_row_idx(curr_key),
|
|
2613
2588
|
)
|
|
2614
2589
|
|
|
2615
2590
|
self._update_count += 1
|
|
2616
2591
|
self.refresh()
|
|
2617
2592
|
|
|
2618
2593
|
# Restore cursor position on the moved row
|
|
2619
|
-
self.move_cursor(row=
|
|
2594
|
+
self.move_cursor(row=swap_row_idx, column=col_idx)
|
|
2620
2595
|
|
|
2621
|
-
#
|
|
2622
|
-
|
|
2623
|
-
swap_ridx =
|
|
2624
|
-
first, second = sorted([
|
|
2596
|
+
# Locate the rows to swap
|
|
2597
|
+
curr_ridx = curr_row_idx
|
|
2598
|
+
swap_ridx = swap_row_idx
|
|
2599
|
+
first, second = sorted([curr_ridx, swap_ridx])
|
|
2625
2600
|
|
|
2601
|
+
# Swap the rows in the dataframe
|
|
2626
2602
|
self.df = pl.concat(
|
|
2627
2603
|
[
|
|
2628
|
-
self.df.slice(0, first),
|
|
2629
|
-
self.df.slice(second, 1),
|
|
2630
|
-
self.df.slice(first + 1, second - first - 1),
|
|
2631
|
-
self.df.slice(first, 1),
|
|
2632
|
-
self.df.slice(second + 1),
|
|
2604
|
+
self.df.slice(0, first).lazy(),
|
|
2605
|
+
self.df.slice(second, 1).lazy(),
|
|
2606
|
+
self.df.slice(first + 1, second - first - 1).lazy(),
|
|
2607
|
+
self.df.slice(first, 1).lazy(),
|
|
2608
|
+
self.df.slice(second + 1).lazy(),
|
|
2633
2609
|
]
|
|
2634
|
-
)
|
|
2610
|
+
).collect()
|
|
2611
|
+
|
|
2612
|
+
# Also update the view if applicable
|
|
2613
|
+
if self.df_view is not None:
|
|
2614
|
+
# Find RID values
|
|
2615
|
+
curr_rid = self.df[RID][curr_row_idx]
|
|
2616
|
+
swap_rid = self.df[RID][swap_row_idx]
|
|
2617
|
+
|
|
2618
|
+
# Locate the rows by RID in the view
|
|
2619
|
+
curr_ridx = self.df_view[RID].index_of(curr_rid)
|
|
2620
|
+
swap_ridx = self.df_view[RID].index_of(swap_rid)
|
|
2621
|
+
first, second = sorted([curr_ridx, swap_ridx])
|
|
2622
|
+
|
|
2623
|
+
# Swap the rows in the view
|
|
2624
|
+
self.df_view = pl.concat(
|
|
2625
|
+
[
|
|
2626
|
+
self.df_view.slice(0, first).lazy(),
|
|
2627
|
+
self.df_view.slice(second, 1).lazy(),
|
|
2628
|
+
self.df_view.slice(first + 1, second - first - 1).lazy(),
|
|
2629
|
+
self.df_view.slice(first, 1).lazy(),
|
|
2630
|
+
self.df_view.slice(second + 1).lazy(),
|
|
2631
|
+
]
|
|
2632
|
+
).collect()
|
|
2635
2633
|
|
|
2636
|
-
# self.notify(f"Moved row [$success]{row_key.value}[/] {direction}", title="Move")
|
|
2634
|
+
# self.notify(f"Moved row [$success]{row_key.value}[/] {direction}", title="Move Row")
|
|
2637
2635
|
|
|
2638
2636
|
# Type casting
|
|
2639
2637
|
def do_cast_column_dtype(self, dtype: str) -> None:
|
|
@@ -2670,6 +2668,10 @@ class DataFrameTable(DataTable):
|
|
|
2670
2668
|
# Cast the column using Polars
|
|
2671
2669
|
self.df = self.df.with_columns(pl.col(col_name).cast(target_dtype))
|
|
2672
2670
|
|
|
2671
|
+
# Also update the view if applicable
|
|
2672
|
+
if self.df_view is not None:
|
|
2673
|
+
self.df_view = self.df_view.with_columns(pl.col(col_name).cast(target_dtype))
|
|
2674
|
+
|
|
2673
2675
|
# Recreate table for display
|
|
2674
2676
|
self.setup_table()
|
|
2675
2677
|
|
|
@@ -2684,17 +2686,26 @@ class DataFrameTable(DataTable):
|
|
|
2684
2686
|
self.log(f"Error casting column `{col_name}`: {str(e)}")
|
|
2685
2687
|
|
|
2686
2688
|
# Row selection
|
|
2687
|
-
def
|
|
2688
|
-
"""
|
|
2689
|
+
def do_select_row(self) -> None:
|
|
2690
|
+
"""Select rows.
|
|
2691
|
+
|
|
2692
|
+
If there are existing cell matches, use those to select rows.
|
|
2693
|
+
Otherwise, use the current cell value as the search term and select rows matching that value.
|
|
2694
|
+
"""
|
|
2689
2695
|
cidx = self.cursor_col_idx
|
|
2690
|
-
col_name = self.cursor_col_name
|
|
2691
2696
|
|
|
2692
|
-
#
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
term = pl.col(col_name).is_null()
|
|
2697
|
+
# Use existing cell matches if present
|
|
2698
|
+
if self.matches:
|
|
2699
|
+
term = pl.col(RID).is_in(self.matches)
|
|
2696
2700
|
else:
|
|
2697
|
-
|
|
2701
|
+
col_name = self.cursor_col_name
|
|
2702
|
+
|
|
2703
|
+
# Get the value of the currently selected cell
|
|
2704
|
+
term = NULL if self.cursor_value is None else str(self.cursor_value)
|
|
2705
|
+
if self.cursor_value is None:
|
|
2706
|
+
term = pl.col(col_name).is_null()
|
|
2707
|
+
else:
|
|
2708
|
+
term = pl.col(col_name) == self.cursor_value
|
|
2698
2709
|
|
|
2699
2710
|
self.select_row((term, cidx, False, True))
|
|
2700
2711
|
|
|
@@ -2707,7 +2718,7 @@ class DataFrameTable(DataTable):
|
|
|
2707
2718
|
|
|
2708
2719
|
# Push the search modal screen
|
|
2709
2720
|
self.app.push_screen(
|
|
2710
|
-
SearchScreen("
|
|
2721
|
+
SearchScreen("Select", term, self.df, cidx),
|
|
2711
2722
|
callback=self.select_row,
|
|
2712
2723
|
)
|
|
2713
2724
|
|
|
@@ -2717,12 +2728,16 @@ class DataFrameTable(DataTable):
|
|
|
2717
2728
|
return
|
|
2718
2729
|
|
|
2719
2730
|
term, cidx, match_nocase, match_whole = result
|
|
2720
|
-
col_name = self.df.columns[cidx]
|
|
2731
|
+
col_name = "all columns" if cidx is None else self.df.columns[cidx]
|
|
2721
2732
|
|
|
2722
2733
|
# Already a Polars expression
|
|
2723
2734
|
if isinstance(term, pl.Expr):
|
|
2724
2735
|
expr = term
|
|
2725
2736
|
|
|
2737
|
+
# bool list or Series
|
|
2738
|
+
elif isinstance(term, (list, pl.Series)):
|
|
2739
|
+
expr = term
|
|
2740
|
+
|
|
2726
2741
|
# Null case
|
|
2727
2742
|
elif term == NULL:
|
|
2728
2743
|
expr = pl.col(col_name).is_null()
|
|
@@ -2764,13 +2779,11 @@ class DataFrameTable(DataTable):
|
|
|
2764
2779
|
)
|
|
2765
2780
|
|
|
2766
2781
|
# Lazyframe for filtering
|
|
2767
|
-
lf = self.df.lazy()
|
|
2768
|
-
if self.has_hidden_rows:
|
|
2769
|
-
lf = lf.filter(self.visible_rows)
|
|
2782
|
+
lf = self.df.lazy()
|
|
2770
2783
|
|
|
2771
2784
|
# Apply filter to get matched row indices
|
|
2772
2785
|
try:
|
|
2773
|
-
|
|
2786
|
+
ok_rids = set(lf.filter(expr).collect()[RID])
|
|
2774
2787
|
except Exception as e:
|
|
2775
2788
|
self.notify(
|
|
2776
2789
|
f"Error applying search filter `[$error]{term}[/]`", title="Search", severity="error", timeout=10
|
|
@@ -2778,7 +2791,7 @@ class DataFrameTable(DataTable):
|
|
|
2778
2791
|
self.log(f"Error applying search filter `{term}`: {str(e)}")
|
|
2779
2792
|
return
|
|
2780
2793
|
|
|
2781
|
-
match_count = len(
|
|
2794
|
+
match_count = len(ok_rids)
|
|
2782
2795
|
if match_count == 0:
|
|
2783
2796
|
self.notify(
|
|
2784
2797
|
f"No matches found for `[$warning]{term}[/]`. Try [$accent](?i)abc[/] for case-insensitive search.",
|
|
@@ -2787,14 +2800,13 @@ class DataFrameTable(DataTable):
|
|
|
2787
2800
|
)
|
|
2788
2801
|
return
|
|
2789
2802
|
|
|
2790
|
-
message = f"Found [$success]{match_count}[/] matching row(s)
|
|
2803
|
+
message = f"Found [$success]{match_count}[/] matching row(s)"
|
|
2791
2804
|
|
|
2792
2805
|
# Add to history
|
|
2793
2806
|
self.add_history(message)
|
|
2794
2807
|
|
|
2795
|
-
# Update selected rows to include new
|
|
2796
|
-
|
|
2797
|
-
self.selected_rows[m] = True
|
|
2808
|
+
# Update selected rows to include new selections
|
|
2809
|
+
self.selected_rows.update(ok_rids)
|
|
2798
2810
|
|
|
2799
2811
|
# Show notification immediately, then start highlighting
|
|
2800
2812
|
self.notify(message, title="Select Row")
|
|
@@ -2807,20 +2819,12 @@ class DataFrameTable(DataTable):
|
|
|
2807
2819
|
# Add to history
|
|
2808
2820
|
self.add_history("Toggled row selection")
|
|
2809
2821
|
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
for i in range(len(self.selected_rows)):
|
|
2813
|
-
if self.visible_rows[i]:
|
|
2814
|
-
self.selected_rows[i] = not self.selected_rows[i]
|
|
2815
|
-
else:
|
|
2816
|
-
self.selected_rows[i] = False
|
|
2817
|
-
else:
|
|
2818
|
-
# Invert all selected rows
|
|
2819
|
-
self.selected_rows = [not selected for selected in self.selected_rows]
|
|
2822
|
+
# Invert all selected rows
|
|
2823
|
+
self.selected_rows = {rid for rid in self.df[RID] if rid not in self.selected_rows}
|
|
2820
2824
|
|
|
2821
2825
|
# Check if we're highlighting or un-highlighting
|
|
2822
|
-
if
|
|
2823
|
-
self.notify(f"Toggled selection for [$success]{
|
|
2826
|
+
if selected_count := len(self.selected_rows):
|
|
2827
|
+
self.notify(f"Toggled selection for [$success]{selected_count}[/] rows", title="Toggle")
|
|
2824
2828
|
|
|
2825
2829
|
# Recreate table for display
|
|
2826
2830
|
self.setup_table()
|
|
@@ -2830,16 +2834,25 @@ class DataFrameTable(DataTable):
|
|
|
2830
2834
|
# Add to history
|
|
2831
2835
|
self.add_history("Toggled row selection")
|
|
2832
2836
|
|
|
2837
|
+
# Get current row RID
|
|
2833
2838
|
ridx = self.cursor_row_idx
|
|
2834
|
-
|
|
2839
|
+
rid = self.df[RID][ridx]
|
|
2840
|
+
|
|
2841
|
+
if rid in self.selected_rows:
|
|
2842
|
+
self.selected_rows.discard(rid)
|
|
2843
|
+
else:
|
|
2844
|
+
self.selected_rows.add(rid)
|
|
2845
|
+
|
|
2846
|
+
row_key = self.cursor_row_key
|
|
2847
|
+
is_selected = rid in self.selected_rows
|
|
2848
|
+
match_cols = self.matches.get(rid, set())
|
|
2835
2849
|
|
|
2836
|
-
row_key = str(ridx)
|
|
2837
|
-
match_cols = self.matches.get(ridx, set())
|
|
2838
2850
|
for col_idx, col in enumerate(self.ordered_columns):
|
|
2839
2851
|
col_key = col.key
|
|
2852
|
+
col_name = col_key.value
|
|
2840
2853
|
cell_text: Text = self.get_cell(row_key, col_key)
|
|
2841
2854
|
|
|
2842
|
-
if
|
|
2855
|
+
if is_selected or (col_name in match_cols):
|
|
2843
2856
|
cell_text.style = HIGHLIGHT_COLOR
|
|
2844
2857
|
else:
|
|
2845
2858
|
# Reset to default style based on dtype
|
|
@@ -2852,19 +2865,17 @@ class DataFrameTable(DataTable):
|
|
|
2852
2865
|
def do_clear_selections_and_matches(self) -> None:
|
|
2853
2866
|
"""Clear all selected rows and matches without removing them from the dataframe."""
|
|
2854
2867
|
# Check if any selected rows or matches
|
|
2855
|
-
if not
|
|
2868
|
+
if not self.selected_rows and not self.matches:
|
|
2856
2869
|
self.notify("No selections to clear", title="Clear", severity="warning")
|
|
2857
2870
|
return
|
|
2858
2871
|
|
|
2859
|
-
row_count =
|
|
2860
|
-
1 if (selected or idx in self.matches) else 0 for idx, selected in enumerate(self.selected_rows)
|
|
2861
|
-
)
|
|
2872
|
+
row_count = len(self.selected_rows | set(self.matches.keys()))
|
|
2862
2873
|
|
|
2863
2874
|
# Add to history
|
|
2864
2875
|
self.add_history("Cleared all selected rows")
|
|
2865
2876
|
|
|
2866
2877
|
# Clear all selections
|
|
2867
|
-
self.selected_rows =
|
|
2878
|
+
self.selected_rows = set()
|
|
2868
2879
|
self.matches = defaultdict(set)
|
|
2869
2880
|
|
|
2870
2881
|
# Recreate table for display
|
|
@@ -2875,7 +2886,7 @@ class DataFrameTable(DataTable):
|
|
|
2875
2886
|
# Find & Replace
|
|
2876
2887
|
def find_matches(
|
|
2877
2888
|
self, term: str, cidx: int | None = None, match_nocase: bool = False, match_whole: bool = False
|
|
2878
|
-
) -> dict[int, set[
|
|
2889
|
+
) -> dict[int, set[str]]:
|
|
2879
2890
|
"""Find matches for a term in the dataframe.
|
|
2880
2891
|
|
|
2881
2892
|
Args:
|
|
@@ -2892,12 +2903,10 @@ class DataFrameTable(DataTable):
|
|
|
2892
2903
|
Raises:
|
|
2893
2904
|
Exception: If expression validation or filtering fails.
|
|
2894
2905
|
"""
|
|
2895
|
-
matches: dict[int, set[
|
|
2906
|
+
matches: dict[int, set[str]] = defaultdict(set)
|
|
2896
2907
|
|
|
2897
2908
|
# Lazyframe for filtering
|
|
2898
|
-
lf = self.df.lazy()
|
|
2899
|
-
if self.has_hidden_rows:
|
|
2900
|
-
lf = lf.filter(self.visible_rows)
|
|
2909
|
+
lf = self.df.lazy()
|
|
2901
2910
|
|
|
2902
2911
|
# Determine which columns to search: single column or all columns
|
|
2903
2912
|
if cidx is not None:
|
|
@@ -2928,14 +2937,14 @@ class DataFrameTable(DataTable):
|
|
|
2928
2937
|
|
|
2929
2938
|
# Get matched row indices
|
|
2930
2939
|
try:
|
|
2931
|
-
matched_ridxs = lf.filter(expr).
|
|
2940
|
+
matched_ridxs = lf.filter(expr).collect()[RID]
|
|
2932
2941
|
except Exception as e:
|
|
2933
2942
|
self.notify(f"Error applying filter: [$error]{expr}[/]", title="Find", severity="error", timeout=10)
|
|
2934
2943
|
self.log(f"Error applying filter: {str(e)}")
|
|
2935
2944
|
return matches
|
|
2936
2945
|
|
|
2937
2946
|
for ridx in matched_ridxs:
|
|
2938
|
-
matches[ridx].add(
|
|
2947
|
+
matches[ridx].add(col_name)
|
|
2939
2948
|
|
|
2940
2949
|
return matches
|
|
2941
2950
|
|
|
@@ -2997,9 +3006,9 @@ class DataFrameTable(DataTable):
|
|
|
2997
3006
|
self.add_history(f"Found `[$success]{term}[/]` in column [$accent]{col_name}[/]")
|
|
2998
3007
|
|
|
2999
3008
|
# Add to matches and count total
|
|
3000
|
-
match_count = sum(len(
|
|
3001
|
-
for
|
|
3002
|
-
self.matches[
|
|
3009
|
+
match_count = sum(len(cols) for cols in matches.values())
|
|
3010
|
+
for rid, cols in matches.items():
|
|
3011
|
+
self.matches[rid].update(cols)
|
|
3003
3012
|
|
|
3004
3013
|
self.notify(f"Found [$success]{match_count}[/] matches for `[$accent]{term}[/]`", title="Find")
|
|
3005
3014
|
|
|
@@ -3031,9 +3040,9 @@ class DataFrameTable(DataTable):
|
|
|
3031
3040
|
self.add_history(f"Found `[$success]{term}[/]` across all columns")
|
|
3032
3041
|
|
|
3033
3042
|
# Add to matches and count total
|
|
3034
|
-
match_count = sum(len(
|
|
3035
|
-
for
|
|
3036
|
-
self.matches[
|
|
3043
|
+
match_count = sum(len(cols) for cols in matches.values())
|
|
3044
|
+
for rid, cols in matches.items():
|
|
3045
|
+
self.matches[rid].update(cols)
|
|
3037
3046
|
|
|
3038
3047
|
self.notify(
|
|
3039
3048
|
f"Found [$success]{match_count}[/] matches for `[$accent]{term}[/]` across all columns",
|
|
@@ -3095,7 +3104,7 @@ class DataFrameTable(DataTable):
|
|
|
3095
3104
|
|
|
3096
3105
|
def do_next_selected_row(self) -> None:
|
|
3097
3106
|
"""Move cursor to the next selected row."""
|
|
3098
|
-
if not
|
|
3107
|
+
if not self.selected_rows:
|
|
3099
3108
|
self.notify("No selected rows to navigate", title="Next Selected Row", severity="warning")
|
|
3100
3109
|
return
|
|
3101
3110
|
|
|
@@ -3117,7 +3126,7 @@ class DataFrameTable(DataTable):
|
|
|
3117
3126
|
|
|
3118
3127
|
def do_previous_selected_row(self) -> None:
|
|
3119
3128
|
"""Move cursor to the previous selected row."""
|
|
3120
|
-
if not
|
|
3129
|
+
if not self.selected_rows:
|
|
3121
3130
|
self.notify("No selected rows to navigate", title="Previous Selected Row", severity="warning")
|
|
3122
3131
|
return
|
|
3123
3132
|
|
|
@@ -3190,25 +3199,34 @@ class DataFrameTable(DataTable):
|
|
|
3190
3199
|
)
|
|
3191
3200
|
|
|
3192
3201
|
# Update matches
|
|
3193
|
-
self.matches =
|
|
3202
|
+
self.matches = matches
|
|
3194
3203
|
|
|
3195
3204
|
# Recreate table for display
|
|
3196
3205
|
self.setup_table()
|
|
3197
3206
|
|
|
3198
3207
|
# Store state for interactive replacement using dataclass
|
|
3199
|
-
|
|
3208
|
+
rid2ridx = {rid: ridx for ridx, rid in enumerate(self.df[RID]) if rid in self.matches}
|
|
3209
|
+
|
|
3210
|
+
# Unique columns to replace
|
|
3211
|
+
cols_to_replace = set()
|
|
3212
|
+
for cols in self.matches.values():
|
|
3213
|
+
cols_to_replace.update(cols)
|
|
3214
|
+
|
|
3215
|
+
# Sorted column indices to replace
|
|
3216
|
+
cidx2col = {cidx: col for cidx, col in enumerate(self.df.columns) if col in cols_to_replace}
|
|
3217
|
+
|
|
3200
3218
|
self.replace_state = ReplaceState(
|
|
3201
3219
|
term_find=term_find,
|
|
3202
3220
|
term_replace=term_replace,
|
|
3203
3221
|
match_nocase=match_nocase,
|
|
3204
3222
|
match_whole=match_whole,
|
|
3205
3223
|
cidx=cidx,
|
|
3206
|
-
rows=
|
|
3207
|
-
cols_per_row=[
|
|
3224
|
+
rows=list(rid2ridx.values()),
|
|
3225
|
+
cols_per_row=[[cidx for cidx, col in cidx2col.items() if col in self.matches[rid]] for rid in rid2ridx],
|
|
3208
3226
|
current_rpos=0,
|
|
3209
3227
|
current_cpos=0,
|
|
3210
3228
|
current_occurrence=0,
|
|
3211
|
-
total_occurrence=sum(len(
|
|
3229
|
+
total_occurrence=sum(len(cols) for cols in self.matches.values()),
|
|
3212
3230
|
replaced_occurrence=0,
|
|
3213
3231
|
skipped_occurrence=0,
|
|
3214
3232
|
done=False,
|
|
@@ -3292,6 +3310,18 @@ class DataFrameTable(DataTable):
|
|
|
3292
3310
|
pl.when(mask).then(pl.lit(value)).otherwise(pl.col(col_name)).alias(col_name)
|
|
3293
3311
|
)
|
|
3294
3312
|
|
|
3313
|
+
# Also update the view if applicable
|
|
3314
|
+
if self.df_view is not None:
|
|
3315
|
+
col_updated = f"^_{col_name}_^"
|
|
3316
|
+
lf_updated = self.df.lazy().filter(mask).select(pl.col(col_name).alias(col_updated), pl.col(RID))
|
|
3317
|
+
self.df_view = (
|
|
3318
|
+
self.df_view.lazy()
|
|
3319
|
+
.join(lf_updated, on=RID, how="left")
|
|
3320
|
+
.with_columns(pl.coalesce(pl.col(col_updated), pl.col(col_name)).alias(col_name))
|
|
3321
|
+
.drop(col_updated)
|
|
3322
|
+
.collect()
|
|
3323
|
+
)
|
|
3324
|
+
|
|
3295
3325
|
state.replaced_occurrence += len(ridxs)
|
|
3296
3326
|
|
|
3297
3327
|
# Recreate table for display
|
|
@@ -3303,7 +3333,7 @@ class DataFrameTable(DataTable):
|
|
|
3303
3333
|
|
|
3304
3334
|
col_name = "all columns" if state.cidx is None else self.df.columns[state.cidx]
|
|
3305
3335
|
self.notify(
|
|
3306
|
-
f"Replaced [$success]{state.replaced_occurrence}[/] of [$
|
|
3336
|
+
f"Replaced [$success]{state.replaced_occurrence}[/] of [$success]{state.total_occurrence}[/] in [$accent]{col_name}[/]",
|
|
3307
3337
|
title="Replace",
|
|
3308
3338
|
)
|
|
3309
3339
|
|
|
@@ -3327,7 +3357,7 @@ class DataFrameTable(DataTable):
|
|
|
3327
3357
|
if state.done:
|
|
3328
3358
|
# All done - show final notification
|
|
3329
3359
|
col_name = "all columns" if state.cidx is None else self.df.columns[state.cidx]
|
|
3330
|
-
msg = f"Replaced [$success]{state.replaced_occurrence}[/] of [$
|
|
3360
|
+
msg = f"Replaced [$success]{state.replaced_occurrence}[/] of [$success]{state.total_occurrence}[/] in [$accent]{col_name}[/]"
|
|
3331
3361
|
if state.skipped_occurrence > 0:
|
|
3332
3362
|
msg += f", [$warning]{state.skipped_occurrence}[/] skipped"
|
|
3333
3363
|
self.notify(msg, title="Replace")
|
|
@@ -3362,6 +3392,7 @@ class DataFrameTable(DataTable):
|
|
|
3362
3392
|
cidx = state.cols_per_row[state.current_rpos][state.current_cpos]
|
|
3363
3393
|
col_name = self.df.columns[cidx]
|
|
3364
3394
|
dtype = self.df.dtypes[cidx]
|
|
3395
|
+
rid = self.df[RID][ridx]
|
|
3365
3396
|
|
|
3366
3397
|
# Replace
|
|
3367
3398
|
if result is True:
|
|
@@ -3374,6 +3405,15 @@ class DataFrameTable(DataTable):
|
|
|
3374
3405
|
.otherwise(pl.col(col_name))
|
|
3375
3406
|
.alias(col_name)
|
|
3376
3407
|
)
|
|
3408
|
+
|
|
3409
|
+
# Also update the view if applicable
|
|
3410
|
+
if self.df_view is not None:
|
|
3411
|
+
self.df_view = self.df_view.with_columns(
|
|
3412
|
+
pl.when(pl.col(RID) == rid)
|
|
3413
|
+
.then(pl.col(col_name).str.replace_all(term_find, state.term_replace))
|
|
3414
|
+
.otherwise(pl.col(col_name))
|
|
3415
|
+
.alias(col_name)
|
|
3416
|
+
)
|
|
3377
3417
|
else:
|
|
3378
3418
|
# try to convert replacement value to column dtype
|
|
3379
3419
|
try:
|
|
@@ -3388,6 +3428,12 @@ class DataFrameTable(DataTable):
|
|
|
3388
3428
|
.alias(col_name)
|
|
3389
3429
|
)
|
|
3390
3430
|
|
|
3431
|
+
# Also update the view if applicable
|
|
3432
|
+
if self.df_view is not None:
|
|
3433
|
+
self.df_view = self.df_view.with_columns(
|
|
3434
|
+
pl.when(pl.col(RID) == rid).then(pl.lit(value)).otherwise(pl.col(col_name)).alias(col_name)
|
|
3435
|
+
)
|
|
3436
|
+
|
|
3391
3437
|
state.replaced_occurrence += 1
|
|
3392
3438
|
|
|
3393
3439
|
# Skip
|
|
@@ -3424,18 +3470,16 @@ class DataFrameTable(DataTable):
|
|
|
3424
3470
|
def do_view_rows(self) -> None:
|
|
3425
3471
|
"""View rows.
|
|
3426
3472
|
|
|
3427
|
-
If there are selected rows
|
|
3428
|
-
Otherwise, view based on the value
|
|
3473
|
+
If there are selected rows, view those.
|
|
3474
|
+
Otherwise, view based on the cursor value.
|
|
3429
3475
|
"""
|
|
3430
3476
|
|
|
3431
3477
|
cidx = self.cursor_col_idx
|
|
3432
|
-
col_name = self.
|
|
3478
|
+
col_name = self.cursor_col_name
|
|
3433
3479
|
|
|
3434
|
-
# If there are rows
|
|
3435
|
-
if
|
|
3436
|
-
term =
|
|
3437
|
-
True if (selected or idx in self.matches) else False for idx, selected in enumerate(self.selected_rows)
|
|
3438
|
-
]
|
|
3480
|
+
# If there are selected rows, use those
|
|
3481
|
+
if self.selected_rows:
|
|
3482
|
+
term = pl.col(RID).is_in(self.selected_rows)
|
|
3439
3483
|
# Otherwise, use the current cell value
|
|
3440
3484
|
else:
|
|
3441
3485
|
ridx = self.cursor_row_idx
|
|
@@ -3457,7 +3501,7 @@ class DataFrameTable(DataTable):
|
|
|
3457
3501
|
)
|
|
3458
3502
|
|
|
3459
3503
|
def view_rows(self, result) -> None:
|
|
3460
|
-
"""
|
|
3504
|
+
"""View selected rows and hide others. Do not modify the dataframe."""
|
|
3461
3505
|
if result is None:
|
|
3462
3506
|
return
|
|
3463
3507
|
term, cidx, match_nocase, match_whole = result
|
|
@@ -3467,13 +3511,17 @@ class DataFrameTable(DataTable):
|
|
|
3467
3511
|
# Support for polars expression
|
|
3468
3512
|
if isinstance(term, pl.Expr):
|
|
3469
3513
|
expr = term
|
|
3514
|
+
|
|
3470
3515
|
# Support for list of booleans (selected rows)
|
|
3471
3516
|
elif isinstance(term, (list, pl.Series)):
|
|
3472
3517
|
expr = term
|
|
3518
|
+
|
|
3519
|
+
# Null case
|
|
3473
3520
|
elif term == NULL:
|
|
3474
3521
|
expr = pl.col(col_name).is_null()
|
|
3522
|
+
|
|
3523
|
+
# Support for polars expression in string form
|
|
3475
3524
|
elif tentative_expr(term):
|
|
3476
|
-
# Support for polars expression in string form
|
|
3477
3525
|
try:
|
|
3478
3526
|
expr = validate_expr(term, self.df.columns, cidx)
|
|
3479
3527
|
except Exception as e:
|
|
@@ -3482,6 +3530,8 @@ class DataFrameTable(DataTable):
|
|
|
3482
3530
|
)
|
|
3483
3531
|
self.log(f"Error validating expression `{term}`: {str(e)}")
|
|
3484
3532
|
return
|
|
3533
|
+
|
|
3534
|
+
# Type-aware search based on column dtype
|
|
3485
3535
|
else:
|
|
3486
3536
|
dtype = self.df.dtypes[cidx]
|
|
3487
3537
|
if dtype == pl.String:
|
|
@@ -3505,11 +3555,7 @@ class DataFrameTable(DataTable):
|
|
|
3505
3555
|
)
|
|
3506
3556
|
|
|
3507
3557
|
# Lazyframe with row indices
|
|
3508
|
-
lf = self.df.lazy()
|
|
3509
|
-
|
|
3510
|
-
# Apply existing visibility filter first
|
|
3511
|
-
if self.has_hidden_rows:
|
|
3512
|
-
lf = lf.filter(self.visible_rows)
|
|
3558
|
+
lf = self.df.lazy()
|
|
3513
3559
|
|
|
3514
3560
|
expr_str = "boolean list or series" if isinstance(expr, (list, pl.Series)) else str(expr)
|
|
3515
3561
|
|
|
@@ -3517,7 +3563,7 @@ class DataFrameTable(DataTable):
|
|
|
3517
3563
|
try:
|
|
3518
3564
|
df_filtered = lf.filter(expr).collect()
|
|
3519
3565
|
except Exception as e:
|
|
3520
|
-
self.
|
|
3566
|
+
self.histories_undo.pop() # Remove last history entry
|
|
3521
3567
|
self.notify(f"Error applying filter [$error]{expr_str}[/]", title="Filter", severity="error", timeout=10)
|
|
3522
3568
|
self.log(f"Error applying filter `{expr_str}`: {str(e)}")
|
|
3523
3569
|
return
|
|
@@ -3530,26 +3576,37 @@ class DataFrameTable(DataTable):
|
|
|
3530
3576
|
# Add to history
|
|
3531
3577
|
self.add_history(f"Filtered by expression [$success]{expr_str}[/]")
|
|
3532
3578
|
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3579
|
+
ok_rids = set(df_filtered[RID])
|
|
3580
|
+
|
|
3581
|
+
# Create a view of self.df as a copy
|
|
3582
|
+
if self.df_view is None:
|
|
3583
|
+
self.df_view = self.df
|
|
3584
|
+
|
|
3585
|
+
# Update dataframe
|
|
3586
|
+
self.df = df_filtered
|
|
3587
|
+
|
|
3588
|
+
# Update selected rows
|
|
3589
|
+
if self.selected_rows:
|
|
3590
|
+
self.selected_rows.intersection_update(ok_rids)
|
|
3591
|
+
|
|
3592
|
+
# Update matches
|
|
3593
|
+
if self.matches:
|
|
3594
|
+
self.matches = {rid: cols for rid, cols in self.matches.items() if rid in ok_rids}
|
|
3539
3595
|
|
|
3540
3596
|
# Recreate table for display
|
|
3541
3597
|
self.setup_table()
|
|
3542
3598
|
|
|
3543
|
-
self.notify(f"Filtered to [$success]{matched_count}[/] matching
|
|
3599
|
+
self.notify(f"Filtered to [$success]{matched_count}[/] matching row(s)", title="Filter")
|
|
3544
3600
|
|
|
3545
3601
|
def do_filter_rows(self) -> None:
|
|
3546
|
-
"""
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3602
|
+
"""Filter rows.
|
|
3603
|
+
|
|
3604
|
+
If there are selected rows, use those.
|
|
3605
|
+
Otherwise, filter based on the cursor value.
|
|
3606
|
+
"""
|
|
3607
|
+
if self.selected_rows:
|
|
3608
|
+
message = "Filtered to selected rows (other rows removed)"
|
|
3609
|
+
filter_expr = pl.col(RID).is_in(self.selected_rows)
|
|
3553
3610
|
else: # Search cursor value in current column
|
|
3554
3611
|
message = "Filtered to rows matching cursor value (other rows removed)"
|
|
3555
3612
|
cidx = self.cursor_col_idx
|
|
@@ -3565,16 +3622,26 @@ class DataFrameTable(DataTable):
|
|
|
3565
3622
|
self.add_history(message, dirty=True)
|
|
3566
3623
|
|
|
3567
3624
|
# Apply filter to dataframe with row indices
|
|
3568
|
-
df_filtered = self.df.
|
|
3625
|
+
df_filtered = self.df.lazy().filter(filter_expr).collect()
|
|
3626
|
+
ok_rids = set(df_filtered[RID])
|
|
3569
3627
|
|
|
3570
3628
|
# Update selected rows
|
|
3571
|
-
|
|
3629
|
+
if self.selected_rows:
|
|
3630
|
+
selected_rows = {rid for rid in self.selected_rows if rid in ok_rids}
|
|
3631
|
+
else:
|
|
3632
|
+
selected_rows = set()
|
|
3572
3633
|
|
|
3573
3634
|
# Update matches
|
|
3574
|
-
|
|
3635
|
+
if self.matches:
|
|
3636
|
+
matches = {rid: cols for rid, cols in self.matches.items() if rid in ok_rids}
|
|
3637
|
+
else:
|
|
3638
|
+
matches = defaultdict(set)
|
|
3575
3639
|
|
|
3576
3640
|
# Update dataframe
|
|
3577
|
-
self.reset_df(df_filtered
|
|
3641
|
+
self.reset_df(df_filtered)
|
|
3642
|
+
|
|
3643
|
+
# Clear view for filter mode
|
|
3644
|
+
self.df_view = None
|
|
3578
3645
|
|
|
3579
3646
|
# Restore selected rows and matches
|
|
3580
3647
|
self.selected_rows = selected_rows
|
|
@@ -3583,7 +3650,7 @@ class DataFrameTable(DataTable):
|
|
|
3583
3650
|
# Recreate table for display
|
|
3584
3651
|
self.setup_table()
|
|
3585
3652
|
|
|
3586
|
-
self.notify(f"{message}. Now showing [$success]{len(self.df)}[/] rows", title="Filter")
|
|
3653
|
+
self.notify(f"{message}. Now showing [$success]{len(self.df)}[/] rows.", title="Filter")
|
|
3587
3654
|
|
|
3588
3655
|
# Copy & Save
|
|
3589
3656
|
def do_copy_to_clipboard(self, content: str, message: str) -> None:
|
|
@@ -3609,20 +3676,24 @@ class DataFrameTable(DataTable):
|
|
|
3609
3676
|
except FileNotFoundError:
|
|
3610
3677
|
self.notify("Error copying to clipboard", title="Clipboard", severity="error", timeout=10)
|
|
3611
3678
|
|
|
3612
|
-
def do_save_to_file(
|
|
3613
|
-
self, title: str = "Save to File", all_tabs: bool | None = None, task_after_save: str | None = None
|
|
3614
|
-
) -> None:
|
|
3679
|
+
def do_save_to_file(self, all_tabs: bool | None = None, task_after_save: str | None = None) -> None:
|
|
3615
3680
|
"""Open screen to save file."""
|
|
3616
3681
|
self._task_after_save = task_after_save
|
|
3682
|
+
tab_count = len(self.app.tabs)
|
|
3683
|
+
save_all = tab_count > 1 and all_tabs is not False
|
|
3684
|
+
|
|
3685
|
+
filepath = Path(self.filename)
|
|
3686
|
+
if save_all:
|
|
3687
|
+
ext = filepath.suffix.lower()
|
|
3688
|
+
if ext in (".xlsx", ".xls"):
|
|
3689
|
+
filename = self.filename
|
|
3690
|
+
else:
|
|
3691
|
+
filename = "all-tabs.xlsx"
|
|
3692
|
+
else:
|
|
3693
|
+
filename = str(filepath.with_stem(self.tabname))
|
|
3617
3694
|
|
|
3618
|
-
multi_tab = len(self.app.tabs) > 1
|
|
3619
|
-
filename = (
|
|
3620
|
-
"all-tabs.xlsx"
|
|
3621
|
-
if all_tabs or (all_tabs is None and multi_tab)
|
|
3622
|
-
else str(Path(self.filename).with_stem(self.tabname))
|
|
3623
|
-
)
|
|
3624
3695
|
self.app.push_screen(
|
|
3625
|
-
SaveFileScreen(filename,
|
|
3696
|
+
SaveFileScreen(filename, save_all=save_all, tab_count=tab_count),
|
|
3626
3697
|
callback=self.save_to_file,
|
|
3627
3698
|
)
|
|
3628
3699
|
|
|
@@ -3630,10 +3701,8 @@ class DataFrameTable(DataTable):
|
|
|
3630
3701
|
"""Handle result from SaveFileScreen."""
|
|
3631
3702
|
if result is None:
|
|
3632
3703
|
return
|
|
3633
|
-
filename,
|
|
3634
|
-
|
|
3635
|
-
# Whether to save all tabs (for Excel files)
|
|
3636
|
-
self._all_tabs = all_tabs
|
|
3704
|
+
filename, save_all, overwrite_prompt = result
|
|
3705
|
+
self._save_all = save_all
|
|
3637
3706
|
|
|
3638
3707
|
# Check if file exists
|
|
3639
3708
|
if overwrite_prompt and Path(filename).exists():
|
|
@@ -3652,7 +3721,7 @@ class DataFrameTable(DataTable):
|
|
|
3652
3721
|
else:
|
|
3653
3722
|
# Go back to SaveFileScreen to allow user to enter a different name
|
|
3654
3723
|
self.app.push_screen(
|
|
3655
|
-
SaveFileScreen(self._pending_filename),
|
|
3724
|
+
SaveFileScreen(self._pending_filename, save_all=self._save_all),
|
|
3656
3725
|
callback=self.save_to_file,
|
|
3657
3726
|
)
|
|
3658
3727
|
|
|
@@ -3660,7 +3729,7 @@ class DataFrameTable(DataTable):
|
|
|
3660
3729
|
"""Actually save the dataframe to a file."""
|
|
3661
3730
|
filepath = Path(filename)
|
|
3662
3731
|
ext = filepath.suffix.lower()
|
|
3663
|
-
if ext
|
|
3732
|
+
if ext == ".gz":
|
|
3664
3733
|
ext = Path(filename).with_suffix("").suffix.lower()
|
|
3665
3734
|
|
|
3666
3735
|
fmt = ext.removeprefix(".")
|
|
@@ -3672,30 +3741,28 @@ class DataFrameTable(DataTable):
|
|
|
3672
3741
|
)
|
|
3673
3742
|
fmt = "csv"
|
|
3674
3743
|
|
|
3675
|
-
|
|
3676
|
-
self.add_history(f"Saved dataframe to [$success]{filename}[/]")
|
|
3677
|
-
|
|
3744
|
+
df = (self.df if self.df_view is None else self.df_view).select(pl.exclude(RID))
|
|
3678
3745
|
try:
|
|
3679
3746
|
if fmt == "csv":
|
|
3680
|
-
|
|
3747
|
+
df.write_csv(filename)
|
|
3681
3748
|
elif fmt in ("tsv", "tab"):
|
|
3682
|
-
|
|
3749
|
+
df.write_csv(filename, separator="\t")
|
|
3683
3750
|
elif fmt in ("xlsx", "xls"):
|
|
3684
3751
|
self.save_excel(filename)
|
|
3685
3752
|
elif fmt == "json":
|
|
3686
|
-
|
|
3753
|
+
df.write_json(filename)
|
|
3687
3754
|
elif fmt == "ndjson":
|
|
3688
|
-
|
|
3755
|
+
df.write_ndjson(filename)
|
|
3689
3756
|
elif fmt == "parquet":
|
|
3690
|
-
|
|
3757
|
+
df.write_parquet(filename)
|
|
3691
3758
|
else: # Fallback to CSV
|
|
3692
|
-
|
|
3759
|
+
df.write_csv(filename)
|
|
3693
3760
|
|
|
3694
3761
|
# Update current filename
|
|
3695
3762
|
self.filename = filename
|
|
3696
3763
|
|
|
3697
3764
|
# Reset dirty flag after save
|
|
3698
|
-
if self.
|
|
3765
|
+
if self._save_all:
|
|
3699
3766
|
tabs: dict[TabPane, DataFrameTable] = self.app.tabs
|
|
3700
3767
|
for table in tabs.values():
|
|
3701
3768
|
table.dirty = False
|
|
@@ -3709,7 +3776,7 @@ class DataFrameTable(DataTable):
|
|
|
3709
3776
|
self.app.exit()
|
|
3710
3777
|
|
|
3711
3778
|
# From ConfirmScreen callback, so notify accordingly
|
|
3712
|
-
if self.
|
|
3779
|
+
if self._save_all:
|
|
3713
3780
|
self.notify(f"Saved all tabs to [$success]{filename}[/]", title="Save to File")
|
|
3714
3781
|
else:
|
|
3715
3782
|
self.notify(f"Saved current tab to [$success]{filename}[/]", title="Save to File")
|
|
@@ -3722,16 +3789,18 @@ class DataFrameTable(DataTable):
|
|
|
3722
3789
|
"""Save to an Excel file."""
|
|
3723
3790
|
import xlsxwriter
|
|
3724
3791
|
|
|
3725
|
-
if not self.
|
|
3792
|
+
if not self._save_all or len(self.app.tabs) == 1:
|
|
3726
3793
|
# Single tab - save directly
|
|
3727
|
-
self.df.
|
|
3794
|
+
df = (self.df if self.df_view is None else self.df_view).select(pl.exclude(RID))
|
|
3795
|
+
df.write_excel(filename, worksheet=self.tabname)
|
|
3728
3796
|
else:
|
|
3729
3797
|
# Multiple tabs - use xlsxwriter to create multiple sheets
|
|
3730
3798
|
with xlsxwriter.Workbook(filename) as wb:
|
|
3731
3799
|
tabs: dict[TabPane, DataFrameTable] = self.app.tabs
|
|
3732
3800
|
for table in tabs.values():
|
|
3733
3801
|
worksheet = wb.add_worksheet(table.tabname)
|
|
3734
|
-
table.df.
|
|
3802
|
+
df = (table.df if table.df_view is None else table.df_view).select(pl.exclude(RID))
|
|
3803
|
+
df.write_excel(workbook=wb, worksheet=worksheet)
|
|
3735
3804
|
|
|
3736
3805
|
# SQL Interface
|
|
3737
3806
|
def do_simple_sql(self) -> None:
|
|
@@ -3775,19 +3844,17 @@ class DataFrameTable(DataTable):
|
|
|
3775
3844
|
sql: The SQL query string to execute.
|
|
3776
3845
|
"""
|
|
3777
3846
|
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3847
|
+
sql = sql.replace("$#", f"(`{RID}` + 1)")
|
|
3848
|
+
if RID not in sql and "*" not in sql:
|
|
3849
|
+
# Ensure RID is selected
|
|
3850
|
+
import re
|
|
3781
3851
|
|
|
3782
|
-
|
|
3852
|
+
RE_FROM_SELF = re.compile(r"\bFROM\s+self\b", re.IGNORECASE)
|
|
3853
|
+
sql = RE_FROM_SELF.sub(f", `{RID}` FROM self", sql)
|
|
3783
3854
|
|
|
3784
3855
|
# Execute the SQL query
|
|
3785
3856
|
try:
|
|
3786
|
-
|
|
3787
|
-
if self.has_hidden_rows:
|
|
3788
|
-
lf = lf.filter(self.visible_rows)
|
|
3789
|
-
|
|
3790
|
-
df_filtered = lf.sql(sql).collect()
|
|
3857
|
+
df_filtered = self.df.lazy().sql(sql).collect()
|
|
3791
3858
|
|
|
3792
3859
|
if not len(df_filtered):
|
|
3793
3860
|
self.notify(
|
|
@@ -3795,38 +3862,34 @@ class DataFrameTable(DataTable):
|
|
|
3795
3862
|
)
|
|
3796
3863
|
return
|
|
3797
3864
|
|
|
3798
|
-
# Add to history
|
|
3799
|
-
self.add_history(f"SQL Query:\n[$success]{sql}[/]", dirty=not view)
|
|
3800
|
-
|
|
3801
|
-
if view:
|
|
3802
|
-
# Just view - do not modify the dataframe
|
|
3803
|
-
filtered_row_indices = set(df_filtered[RIDX].to_list())
|
|
3804
|
-
if filtered_row_indices:
|
|
3805
|
-
self.visible_rows = [ridx in filtered_row_indices for ridx in range(len(self.visible_rows))]
|
|
3806
|
-
|
|
3807
|
-
filtered_col_names = set(df_filtered.columns)
|
|
3808
|
-
if filtered_col_names:
|
|
3809
|
-
self.hidden_columns = {
|
|
3810
|
-
col_name for col_name in self.df.columns if col_name not in filtered_col_names
|
|
3811
|
-
}
|
|
3812
|
-
else: # filter - modify the dataframe
|
|
3813
|
-
# Update selected rows
|
|
3814
|
-
selected_rows = [self.selected_rows[df_filtered[RIDX][ridx]] for ridx in range(len(df_filtered))]
|
|
3815
|
-
|
|
3816
|
-
# Update matches
|
|
3817
|
-
matches = {ridx: self.matches[df_filtered[RIDX][ridx]] for ridx in range(len(df_filtered))}
|
|
3818
|
-
|
|
3819
|
-
# Update dataframe
|
|
3820
|
-
self.reset_df(df_filtered.drop(RIDX))
|
|
3821
|
-
|
|
3822
|
-
# Restore selected rows and matches
|
|
3823
|
-
self.selected_rows = selected_rows
|
|
3824
|
-
self.matches = matches
|
|
3825
3865
|
except Exception as e:
|
|
3826
3866
|
self.notify(f"Error executing SQL query [$error]{sql}[/]", title="SQL Query", severity="error", timeout=10)
|
|
3827
3867
|
self.log(f"Error executing SQL query `{sql}`: {str(e)}")
|
|
3828
3868
|
return
|
|
3829
3869
|
|
|
3870
|
+
# Add to history
|
|
3871
|
+
self.add_history(f"SQL Query:\n[$success]{sql}[/]", dirty=not view)
|
|
3872
|
+
|
|
3873
|
+
# Create a view of self.df as a copy
|
|
3874
|
+
if view and self.df_view is None:
|
|
3875
|
+
self.df_view = self.df
|
|
3876
|
+
|
|
3877
|
+
# Clear view for filter mode
|
|
3878
|
+
if not view:
|
|
3879
|
+
self.df_view = None
|
|
3880
|
+
|
|
3881
|
+
# Update dataframe
|
|
3882
|
+
self.df = df_filtered
|
|
3883
|
+
ok_rids = set(df_filtered[RID])
|
|
3884
|
+
|
|
3885
|
+
# Update selected rows
|
|
3886
|
+
if self.selected_rows:
|
|
3887
|
+
self.selected_rows.intersection_update(ok_rids)
|
|
3888
|
+
|
|
3889
|
+
# Update matches
|
|
3890
|
+
if self.matches:
|
|
3891
|
+
self.matches = {rid: cols for rid, cols in self.matches.items() if rid in ok_rids}
|
|
3892
|
+
|
|
3830
3893
|
# Recreate table for display
|
|
3831
3894
|
self.setup_table()
|
|
3832
3895
|
|