dataframe-textual 1.9.0__py3-none-any.whl → 1.12.0__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/common.py +1 -1
- dataframe_textual/data_frame_table.py +197 -174
- dataframe_textual/data_frame_viewer.py +1 -5
- dataframe_textual/sql_screen.py +6 -9
- dataframe_textual/table_screen.py +56 -58
- dataframe_textual/yes_no_screen.py +2 -2
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-1.12.0.dist-info}/METADATA +128 -133
- dataframe_textual-1.12.0.dist-info/RECORD +14 -0
- dataframe_textual-1.9.0.dist-info/RECORD +0 -14
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-1.12.0.dist-info}/WHEEL +0 -0
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-1.12.0.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-1.12.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -36,7 +36,7 @@ class DataFrameViewer(App):
|
|
|
36
36
|
- **Ctrl+A** - 💾 Save all tabs to file
|
|
37
37
|
- **Ctrl+D** - 📋 Duplicate current tab
|
|
38
38
|
- **Ctrl+O** - 📁 Open a file
|
|
39
|
-
- **Double-click tab** - ✏️ Rename
|
|
39
|
+
- **Double-click tab** - ✏️ Rename tab
|
|
40
40
|
|
|
41
41
|
## 🎨 View & Settings
|
|
42
42
|
- **F1** - ❓ Toggle this help panel
|
|
@@ -81,10 +81,6 @@ class DataFrameViewer(App):
|
|
|
81
81
|
ContentTab.dirty {
|
|
82
82
|
background: $warning-darken-3;
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
.underline--bar {
|
|
86
|
-
color: red;
|
|
87
|
-
}
|
|
88
84
|
"""
|
|
89
85
|
|
|
90
86
|
def __init__(self, *sources: Source) -> None:
|
dataframe_textual/sql_screen.py
CHANGED
|
@@ -157,11 +157,8 @@ class SimpleSqlScreen(SqlScreen):
|
|
|
157
157
|
"""
|
|
158
158
|
super().__init__(
|
|
159
159
|
dftable,
|
|
160
|
-
on_yes_callback=self.
|
|
161
|
-
on_maybe_callback=partial(
|
|
162
|
-
self._handle_simple,
|
|
163
|
-
view=False,
|
|
164
|
-
),
|
|
160
|
+
on_yes_callback=self.handle_simple,
|
|
161
|
+
on_maybe_callback=partial(self.handle_simple, view=False),
|
|
165
162
|
)
|
|
166
163
|
|
|
167
164
|
def compose(self) -> ComposeResult:
|
|
@@ -177,7 +174,7 @@ class SimpleSqlScreen(SqlScreen):
|
|
|
177
174
|
yield Input(placeholder="e.g., age > 30 and height < 180", id="where-input")
|
|
178
175
|
yield from super().compose()
|
|
179
176
|
|
|
180
|
-
def
|
|
177
|
+
def handle_simple(self, view: bool = True) -> None:
|
|
181
178
|
"""Handle Yes button/Enter key press."""
|
|
182
179
|
selections = self.query_one(SelectionList).selected
|
|
183
180
|
if not selections:
|
|
@@ -221,8 +218,8 @@ class AdvancedSqlScreen(SqlScreen):
|
|
|
221
218
|
"""
|
|
222
219
|
super().__init__(
|
|
223
220
|
dftable,
|
|
224
|
-
on_yes_callback=self.
|
|
225
|
-
on_maybe_callback=partial(self.
|
|
221
|
+
on_yes_callback=self.handle_advanced,
|
|
222
|
+
on_maybe_callback=partial(self.handle_advanced, view=False),
|
|
226
223
|
)
|
|
227
224
|
|
|
228
225
|
def compose(self) -> ComposeResult:
|
|
@@ -236,6 +233,6 @@ class AdvancedSqlScreen(SqlScreen):
|
|
|
236
233
|
)
|
|
237
234
|
yield from super().compose()
|
|
238
235
|
|
|
239
|
-
def
|
|
236
|
+
def handle_advanced(self, view: bool = True) -> None:
|
|
240
237
|
"""Handle Yes button/Enter key press."""
|
|
241
238
|
return self.query_one(TextArea).text.strip(), view
|
|
@@ -97,60 +97,61 @@ class TableScreen(ModalScreen):
|
|
|
97
97
|
self.build_table()
|
|
98
98
|
event.stop()
|
|
99
99
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
) -> None:
|
|
103
|
-
"""Apply filter or highlight action by the selected value.
|
|
100
|
+
def filter_or_view_selected_value(self, cidx_name_value: tuple[int, str, Any] | None, action: str = "view") -> None:
|
|
101
|
+
"""Apply filter or view action by the selected value.
|
|
104
102
|
|
|
105
|
-
Filters or
|
|
103
|
+
Filters or views rows in the main table based on a selected value from
|
|
106
104
|
this table (typically frequency or row detail). Updates the main table's display
|
|
107
105
|
and notifies the user of the action.
|
|
108
106
|
|
|
109
107
|
Args:
|
|
110
|
-
col_name_value: Tuple of (column_name, column_value) to filter/
|
|
111
|
-
action: Either "filter" to hide non-matching rows, or "
|
|
112
|
-
|
|
113
|
-
Returns:
|
|
114
|
-
None
|
|
108
|
+
col_name_value: Tuple of (column_name, column_value) to filter/view by, or None.
|
|
109
|
+
action: Either "filter" to hide non-matching rows, or "view" to show matching rows. Defaults to "view".
|
|
115
110
|
"""
|
|
116
|
-
if
|
|
111
|
+
if cidx_name_value is None:
|
|
117
112
|
return
|
|
118
|
-
col_name, col_value =
|
|
113
|
+
cidx, col_name, col_value = cidx_name_value
|
|
114
|
+
self.log(f"Filtering or viewing by {col_name} == {col_value}")
|
|
119
115
|
|
|
120
116
|
# Handle NULL values
|
|
121
117
|
if col_value == NULL:
|
|
122
118
|
# Create expression for NULL values
|
|
123
119
|
expr = pl.col(col_name).is_null()
|
|
124
|
-
value_display = "[$success]
|
|
120
|
+
value_display = f"[$success]{NULL_DISPLAY}[/]"
|
|
125
121
|
else:
|
|
126
122
|
# Create expression for the selected value
|
|
127
123
|
expr = pl.col(col_name) == col_value
|
|
128
124
|
value_display = f"[$success]{col_value}[/]"
|
|
129
125
|
|
|
130
|
-
|
|
126
|
+
df_filtered = self.dftable.df.with_row_index(RIDX).filter(expr)
|
|
127
|
+
self.log(f"Filtered dataframe has {len(df_filtered)} rows")
|
|
128
|
+
|
|
129
|
+
matched_indices = set(df_filtered[RIDX].to_list())
|
|
130
|
+
if not matched_indices:
|
|
131
|
+
self.notify(
|
|
132
|
+
f"No matches found for [$warning]{col_name}[/] == {value_display}",
|
|
133
|
+
title="No Matches",
|
|
134
|
+
severity="warning",
|
|
135
|
+
)
|
|
136
|
+
return
|
|
131
137
|
|
|
132
138
|
# Apply the action
|
|
133
139
|
if action == "filter":
|
|
134
|
-
# Update
|
|
135
|
-
for i in range(len(self.dftable.visible_rows)):
|
|
136
|
-
self.dftable.visible_rows[i] = i in matched_indices
|
|
137
|
-
title = "Filter"
|
|
138
|
-
message = f"Filtered by [$accent]{col_name}[/] == [$success]{value_display}[/]"
|
|
139
|
-
else: # action == "highlight"
|
|
140
|
-
# Update selected_rows to reflect the highlights
|
|
140
|
+
# Update selections
|
|
141
141
|
for i in range(len(self.dftable.selected_rows)):
|
|
142
142
|
self.dftable.selected_rows[i] = i in matched_indices
|
|
143
|
-
title = "Highlight"
|
|
144
|
-
message = f"Highlighted [$accent]{col_name}[/] == [$success]{value_display}[/]"
|
|
145
143
|
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
# Update main table display
|
|
145
|
+
self.dftable.do_filter_rows()
|
|
146
|
+
|
|
147
|
+
else: # action == "view"
|
|
148
|
+
# Update visible rows
|
|
149
|
+
expr = [i in matched_indices for i in range(len(self.dftable.df))]
|
|
150
|
+
self.dftable.view_rows((expr, cidx, False, True))
|
|
148
151
|
|
|
149
152
|
# Dismiss the frequency screen
|
|
150
153
|
self.app.pop_screen()
|
|
151
154
|
|
|
152
|
-
self.notify(message, title=title)
|
|
153
|
-
|
|
154
155
|
|
|
155
156
|
class RowDetailScreen(TableScreen):
|
|
156
157
|
"""Modal screen to display a single row's details."""
|
|
@@ -199,30 +200,27 @@ class RowDetailScreen(TableScreen):
|
|
|
199
200
|
|
|
200
201
|
Args:
|
|
201
202
|
event: The key event object.
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
None
|
|
205
203
|
"""
|
|
206
204
|
if event.key == "v":
|
|
207
|
-
#
|
|
208
|
-
self.
|
|
205
|
+
# View the main table by the selected value
|
|
206
|
+
self.filter_or_view_selected_value(self.get_cidx_name_value(), action="view")
|
|
209
207
|
event.stop()
|
|
210
208
|
elif event.key == "quotation_mark": # '"'
|
|
211
|
-
#
|
|
212
|
-
self.
|
|
209
|
+
# Filter the main table by the selected value
|
|
210
|
+
self.filter_or_view_selected_value(self.get_cidx_name_value(), action="filter")
|
|
213
211
|
event.stop()
|
|
214
212
|
elif event.key == "comma":
|
|
215
213
|
event.stop()
|
|
216
214
|
|
|
217
|
-
def
|
|
218
|
-
|
|
219
|
-
if
|
|
215
|
+
def get_cidx_name_value(self) -> tuple[int, str, Any] | None:
|
|
216
|
+
cidx = self.table.cursor_row
|
|
217
|
+
if cidx >= len(self.df.columns):
|
|
220
218
|
return None # Invalid row
|
|
221
219
|
|
|
222
|
-
col_name = self.df.columns[
|
|
223
|
-
col_value = self.df.item(self.ridx,
|
|
220
|
+
col_name = self.df.columns[cidx]
|
|
221
|
+
col_value = self.df.item(self.ridx, cidx)
|
|
224
222
|
|
|
225
|
-
return col_name, col_value
|
|
223
|
+
return cidx, col_name, col_value
|
|
226
224
|
|
|
227
225
|
|
|
228
226
|
class StatisticsScreen(TableScreen):
|
|
@@ -244,14 +242,14 @@ class StatisticsScreen(TableScreen):
|
|
|
244
242
|
|
|
245
243
|
if self.col_idx is None:
|
|
246
244
|
# Dataframe statistics
|
|
247
|
-
self.
|
|
245
|
+
self.build_dataframe_stats()
|
|
248
246
|
self.table.cursor_type = "column"
|
|
249
247
|
else:
|
|
250
248
|
# Column statistics
|
|
251
|
-
self.
|
|
249
|
+
self.build_column_stats()
|
|
252
250
|
self.table.cursor_type = "row"
|
|
253
251
|
|
|
254
|
-
def
|
|
252
|
+
def build_column_stats(self) -> None:
|
|
255
253
|
"""Build statistics for a single column."""
|
|
256
254
|
col_name = self.df.columns[self.col_idx]
|
|
257
255
|
lf = self.df.lazy()
|
|
@@ -292,7 +290,7 @@ class StatisticsScreen(TableScreen):
|
|
|
292
290
|
Text(value, style=dc.style, justify=dc.justify),
|
|
293
291
|
)
|
|
294
292
|
|
|
295
|
-
def
|
|
293
|
+
def build_dataframe_stats(self) -> None:
|
|
296
294
|
"""Build statistics for the entire dataframe."""
|
|
297
295
|
lf = self.df.lazy()
|
|
298
296
|
|
|
@@ -351,16 +349,16 @@ class FrequencyScreen(TableScreen):
|
|
|
351
349
|
|
|
352
350
|
CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "FrequencyScreen")
|
|
353
351
|
|
|
354
|
-
def __init__(self,
|
|
352
|
+
def __init__(self, cidx: int, dftable: "DataFrameTable") -> None:
|
|
355
353
|
super().__init__(dftable)
|
|
356
|
-
self.
|
|
354
|
+
self.cidx = cidx
|
|
357
355
|
self.sorted_columns = {
|
|
358
356
|
1: True, # Count
|
|
359
357
|
}
|
|
360
358
|
|
|
361
359
|
df = dftable.df.filter(dftable.visible_rows) if False in dftable.visible_rows else dftable.df
|
|
362
360
|
self.total_count = len(df)
|
|
363
|
-
self.df: pl.DataFrame = df[df.columns[self.
|
|
361
|
+
self.df: pl.DataFrame = df[df.columns[self.cidx]].value_counts(sort=True).sort("count", descending=True)
|
|
364
362
|
|
|
365
363
|
def on_mount(self) -> None:
|
|
366
364
|
"""Create the frequency table."""
|
|
@@ -369,19 +367,19 @@ class FrequencyScreen(TableScreen):
|
|
|
369
367
|
def on_key(self, event):
|
|
370
368
|
if event.key == "left_square_bracket": # '['
|
|
371
369
|
# Sort by current column in ascending order
|
|
372
|
-
self.
|
|
370
|
+
self.sort_by_column(descending=False)
|
|
373
371
|
event.stop()
|
|
374
372
|
elif event.key == "right_square_bracket": # ']'
|
|
375
373
|
# Sort by current column in descending order
|
|
376
|
-
self.
|
|
374
|
+
self.sort_by_column(descending=True)
|
|
377
375
|
event.stop()
|
|
378
376
|
elif event.key == "v":
|
|
379
377
|
# Filter the main table by the selected value
|
|
380
|
-
self.
|
|
378
|
+
self.filter_or_view_selected_value(self.get_cidx_name_value(), action="view")
|
|
381
379
|
event.stop()
|
|
382
380
|
elif event.key == "quotation_mark": # '"'
|
|
383
381
|
# Highlight the main table by the selected value
|
|
384
|
-
self.
|
|
382
|
+
self.filter_or_view_selected_value(self.get_cidx_name_value(), action="filter")
|
|
385
383
|
event.stop()
|
|
386
384
|
|
|
387
385
|
def build_table(self) -> None:
|
|
@@ -389,8 +387,8 @@ class FrequencyScreen(TableScreen):
|
|
|
389
387
|
self.table.clear(columns=True)
|
|
390
388
|
|
|
391
389
|
# Create frequency table
|
|
392
|
-
column = self.dftable.df.columns[self.
|
|
393
|
-
dtype = self.dftable.df.dtypes[self.
|
|
390
|
+
column = self.dftable.df.columns[self.cidx]
|
|
391
|
+
dtype = self.dftable.df.dtypes[self.cidx]
|
|
394
392
|
dc = DtypeConfig(dtype)
|
|
395
393
|
|
|
396
394
|
# Add column headers with sort indicators
|
|
@@ -468,7 +466,7 @@ class FrequencyScreen(TableScreen):
|
|
|
468
466
|
key="total",
|
|
469
467
|
)
|
|
470
468
|
|
|
471
|
-
def
|
|
469
|
+
def sort_by_column(self, descending: bool) -> None:
|
|
472
470
|
"""Sort the dataframe by the selected column and refresh the main table."""
|
|
473
471
|
row_idx, col_idx = self.table.cursor_coordinate
|
|
474
472
|
col_sort = col_idx if col_idx == 0 else 1
|
|
@@ -493,15 +491,15 @@ class FrequencyScreen(TableScreen):
|
|
|
493
491
|
# order = "desc" if descending else "asc"
|
|
494
492
|
# self.notify(f"Sorted by [on $primary]{col_name}[/] ({order})", title="Sort")
|
|
495
493
|
|
|
496
|
-
def
|
|
494
|
+
def get_cidx_name_value(self) -> tuple[str, str, str] | None:
|
|
497
495
|
row_idx = self.table.cursor_row
|
|
498
496
|
if row_idx >= len(self.df[:, 0]): # first column
|
|
499
497
|
return None # Skip the last `Total` row
|
|
500
498
|
|
|
501
|
-
col_name = self.dftable.df.columns[self.
|
|
502
|
-
col_dtype = self.dftable.df.dtypes[self.
|
|
499
|
+
col_name = self.dftable.df.columns[self.cidx]
|
|
500
|
+
col_dtype = self.dftable.df.dtypes[self.cidx]
|
|
503
501
|
|
|
504
502
|
cell_value = self.table.get_cell_at(Coordinate(row_idx, 0))
|
|
505
503
|
col_value = NULL if cell_value.plain == NULL_DISPLAY else DtypeConfig(col_dtype).convert(cell_value.plain)
|
|
506
504
|
|
|
507
|
-
return col_name, col_value
|
|
505
|
+
return self.cidx, col_name, col_value
|
|
@@ -494,13 +494,13 @@ class FilterScreen(YesNoScreen):
|
|
|
494
494
|
|
|
495
495
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "FilterScreen")
|
|
496
496
|
|
|
497
|
-
def __init__(self, df: pl.DataFrame, cidx: int,
|
|
497
|
+
def __init__(self, df: pl.DataFrame, cidx: int, term: str | None = None):
|
|
498
498
|
self.df = df
|
|
499
499
|
self.cidx = cidx
|
|
500
500
|
super().__init__(
|
|
501
501
|
title="Filter by Expression",
|
|
502
502
|
label="e.g., NULL, $1 > 50, $name == 'text', $_ > 100, $a < $b, $_.str.contains('sub')",
|
|
503
|
-
input=
|
|
503
|
+
input=term,
|
|
504
504
|
checkbox="Match Nocase",
|
|
505
505
|
checkbox2="Match Whole",
|
|
506
506
|
on_yes_callback=self._get_input,
|