dataframe-textual 0.3.1__tar.gz → 0.3.2__tar.gz
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-0.3.1 → dataframe_textual-0.3.2}/PKG-INFO +1 -1
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/pyproject.toml +1 -1
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/src/dataframe_textual/data_frame_table.py +7 -7
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/src/dataframe_textual/data_frame_viewer.py +18 -3
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/src/dataframe_textual/table_screen.py +56 -65
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/src/dataframe_textual/yes_no_screen.py +12 -14
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/uv.lock +1 -1
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/.gitignore +0 -0
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/.python-version +0 -0
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/LICENSE +0 -0
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/README.md +0 -0
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/main.py +0 -0
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/screenshot.png +0 -0
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/src/dataframe_textual/__init__.py +0 -0
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/src/dataframe_textual/__main__.py +0 -0
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/src/dataframe_textual/common.py +0 -0
- {dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/src/dataframe_textual/data_frame_help_panel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataframe-textual
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Interactive CSV/Excel viewer for the terminal (Textual TUI)
|
|
5
5
|
Project-URL: Homepage, https://github.com/need47/dataframe-textual
|
|
6
6
|
Project-URL: Repository, https://github.com/need47/dataframe-textual.git
|
{dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/src/dataframe_textual/data_frame_table.py
RENAMED
|
@@ -577,7 +577,7 @@ class DataFrameTable(DataTable):
|
|
|
577
577
|
|
|
578
578
|
# Add to history
|
|
579
579
|
self._add_history(
|
|
580
|
-
f"Pinned [
|
|
580
|
+
f"Pinned [$accent]{fixed_rows}[/] rows and [$accent]{fixed_columns}[/] columns"
|
|
581
581
|
)
|
|
582
582
|
|
|
583
583
|
# Apply the pin settings to the table
|
|
@@ -587,7 +587,7 @@ class DataFrameTable(DataTable):
|
|
|
587
587
|
self.fixed_columns = fixed_columns
|
|
588
588
|
|
|
589
589
|
self.app.notify(
|
|
590
|
-
f"Pinned [
|
|
590
|
+
f"Pinned [$accent]{fixed_rows}[/] rows and [$accent]{fixed_columns}[/] columns",
|
|
591
591
|
title="Pin",
|
|
592
592
|
)
|
|
593
593
|
|
|
@@ -1107,7 +1107,7 @@ class DataFrameTable(DataTable):
|
|
|
1107
1107
|
self.update_cell(row_key, col_key, cell_text)
|
|
1108
1108
|
|
|
1109
1109
|
self.app.notify(
|
|
1110
|
-
f"Found [
|
|
1110
|
+
f"Found [$accent]{match_count}[/] matches for [on $primary]{term}[/] across all columns",
|
|
1111
1111
|
title="Global Search",
|
|
1112
1112
|
)
|
|
1113
1113
|
|
|
@@ -1141,7 +1141,7 @@ class DataFrameTable(DataTable):
|
|
|
1141
1141
|
# Check if we're highlighting or un-highlighting
|
|
1142
1142
|
if new_selected_count := self.selected_rows.count(True):
|
|
1143
1143
|
self.app.notify(
|
|
1144
|
-
f"Toggled selection - now showing [
|
|
1144
|
+
f"Toggled selection - now showing [$accent]{new_selected_count}[/] rows",
|
|
1145
1145
|
title="Toggle",
|
|
1146
1146
|
)
|
|
1147
1147
|
|
|
@@ -1165,7 +1165,7 @@ class DataFrameTable(DataTable):
|
|
|
1165
1165
|
self._highlight_rows(clear=True)
|
|
1166
1166
|
|
|
1167
1167
|
self.app.notify(
|
|
1168
|
-
f"Cleared [
|
|
1168
|
+
f"Cleared [$accent]{selected_count}[/] selected rows", title="Clear"
|
|
1169
1169
|
)
|
|
1170
1170
|
|
|
1171
1171
|
def _filter_selected_rows(self) -> None:
|
|
@@ -1188,7 +1188,7 @@ class DataFrameTable(DataTable):
|
|
|
1188
1188
|
self._setup_table()
|
|
1189
1189
|
|
|
1190
1190
|
self.app.notify(
|
|
1191
|
-
f"Removed unselected rows. Now showing [
|
|
1191
|
+
f"Removed unselected rows. Now showing [$accent]{selected_count}[/] rows",
|
|
1192
1192
|
title="Filter",
|
|
1193
1193
|
)
|
|
1194
1194
|
|
|
@@ -1253,7 +1253,7 @@ class DataFrameTable(DataTable):
|
|
|
1253
1253
|
self._setup_table()
|
|
1254
1254
|
|
|
1255
1255
|
self.app.notify(
|
|
1256
|
-
f"Filtered to [
|
|
1256
|
+
f"Filtered to [$accent]{matched_count}[/] matching rows",
|
|
1257
1257
|
title="Filter",
|
|
1258
1258
|
)
|
|
1259
1259
|
|
{dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/src/dataframe_textual/data_frame_viewer.py
RENAMED
|
@@ -117,7 +117,9 @@ class DataFrameViewer(App):
|
|
|
117
117
|
def on_key(self, event):
|
|
118
118
|
if event.key == "k":
|
|
119
119
|
self.theme = _next(list(BUILTIN_THEMES.keys()), self.theme)
|
|
120
|
-
self.notify(
|
|
120
|
+
self.notify(
|
|
121
|
+
f"Switched to theme: [on $primary]{self.theme}[/]", title="Theme"
|
|
122
|
+
)
|
|
121
123
|
|
|
122
124
|
def on_tabbed_content_tab_activated(
|
|
123
125
|
self, event: TabbedContent.TabActivated
|
|
@@ -202,12 +204,24 @@ class DataFrameViewer(App):
|
|
|
202
204
|
|
|
203
205
|
def _add_tab(self, df: pl.DataFrame, filename: str) -> None:
|
|
204
206
|
"""Add new table tab. If single file, replace table; if multiple, add tab."""
|
|
205
|
-
table = DataFrameTable(df, filename, zebra_stripes=True)
|
|
206
207
|
tabname = Path(filename).stem
|
|
207
208
|
if any(tab.name == tabname for tab in self.tabs):
|
|
208
209
|
tabname = f"{tabname}_{len(self.tabs) + 1}"
|
|
209
210
|
|
|
210
|
-
|
|
211
|
+
# Find an available tab index
|
|
212
|
+
tab_idx = f"tab_{len(self.tabs) + 1}"
|
|
213
|
+
for idx in range(len(self.tabs)):
|
|
214
|
+
pending_tab_idx = f"tab_{idx + 1}"
|
|
215
|
+
if any(tab.id == pending_tab_idx for tab in self.tabs):
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
tab_idx = pending_tab_idx
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
table = DataFrameTable(
|
|
222
|
+
df, filename, zebra_stripes=True, id=tab_idx, name=tabname
|
|
223
|
+
)
|
|
224
|
+
tab = TabPane(tabname, table, name=tabname, id=tab_idx)
|
|
211
225
|
self.tabbed.add_pane(tab)
|
|
212
226
|
self.tabs[tab] = table
|
|
213
227
|
|
|
@@ -226,6 +240,7 @@ class DataFrameViewer(App):
|
|
|
226
240
|
else:
|
|
227
241
|
if active_pane := self.tabbed.active_pane:
|
|
228
242
|
self.tabbed.remove_pane(active_pane.id)
|
|
243
|
+
self.tabs.pop(active_pane)
|
|
229
244
|
self.notify(
|
|
230
245
|
f"Closed tab [on $primary]{active_pane.name}[/]", title="Close"
|
|
231
246
|
)
|
|
@@ -13,7 +13,7 @@ from textual.renderables.bar import Bar
|
|
|
13
13
|
from textual.screen import ModalScreen
|
|
14
14
|
from textual.widgets import DataTable
|
|
15
15
|
|
|
16
|
-
from .common import
|
|
16
|
+
from .common import DtypeConfig, _format_row
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class TableScreen(ModalScreen):
|
|
@@ -171,25 +171,54 @@ class FrequencyScreen(TableScreen):
|
|
|
171
171
|
|
|
172
172
|
CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "FrequencyScreen")
|
|
173
173
|
|
|
174
|
-
def __init__(self, col_idx: int, dftable):
|
|
174
|
+
def __init__(self, col_idx: int, dftable: DataFrameTable):
|
|
175
175
|
super().__init__(dftable)
|
|
176
176
|
self.col_idx = col_idx
|
|
177
177
|
self.sorted_columns = {
|
|
178
178
|
1: True, # Count
|
|
179
179
|
2: True, # %
|
|
180
180
|
}
|
|
181
|
+
self.df: pl.DataFrame = (
|
|
182
|
+
dftable.df[dftable.df.columns[self.col_idx]]
|
|
183
|
+
.value_counts(sort=True)
|
|
184
|
+
.sort("count", descending=True)
|
|
185
|
+
)
|
|
181
186
|
|
|
182
187
|
def on_mount(self) -> None:
|
|
183
188
|
"""Create the frequency table."""
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
self.build_table()
|
|
190
|
+
|
|
191
|
+
def on_key(self, event):
|
|
192
|
+
if event.key == "left_square_bracket": # '['
|
|
193
|
+
# Sort by current column in ascending order
|
|
194
|
+
self._sort_by_column(descending=False)
|
|
195
|
+
event.stop()
|
|
196
|
+
elif event.key == "right_square_bracket": # ']'
|
|
197
|
+
# Sort by current column in descending order
|
|
198
|
+
self._sort_by_column(descending=True)
|
|
199
|
+
event.stop()
|
|
200
|
+
elif event.key == "v":
|
|
201
|
+
# Filter the main table by the selected value
|
|
202
|
+
self._filter_or_highlight_selected_value(
|
|
203
|
+
self._get_col_name_value(), action="filter"
|
|
204
|
+
)
|
|
205
|
+
event.stop()
|
|
206
|
+
elif event.key == "quotation_mark": # '"'
|
|
207
|
+
# Highlight the main table by the selected value
|
|
208
|
+
self._filter_or_highlight_selected_value(
|
|
209
|
+
self._get_col_name_value(), action="highlight"
|
|
210
|
+
)
|
|
211
|
+
event.stop()
|
|
212
|
+
|
|
213
|
+
def build_table(self) -> None:
|
|
214
|
+
# Create frequency table
|
|
215
|
+
column = self.dftable.df.columns[self.col_idx]
|
|
216
|
+
dtype = str(self.dftable.df.dtypes[self.col_idx])
|
|
186
217
|
dc = DtypeConfig(dtype)
|
|
187
218
|
|
|
188
219
|
# Calculate frequencies using Polars
|
|
189
|
-
|
|
190
|
-
total_count = len(self.df)
|
|
220
|
+
total_count = len(self.dftable.df)
|
|
191
221
|
|
|
192
|
-
# Create frequency table
|
|
193
222
|
self.table.add_column(Text(column, justify=dc.justify), key=column)
|
|
194
223
|
self.table.add_column(Text("Count", justify="right"), key="Count")
|
|
195
224
|
self.table.add_column(Text("%", justify="right"), key="%")
|
|
@@ -200,7 +229,7 @@ class FrequencyScreen(TableScreen):
|
|
|
200
229
|
ds_float = DtypeConfig("Float64")
|
|
201
230
|
|
|
202
231
|
# Add rows to the frequency table
|
|
203
|
-
for row_idx, row in enumerate(
|
|
232
|
+
for row_idx, row in enumerate(self.df.rows()):
|
|
204
233
|
value, count = row
|
|
205
234
|
percentage = (count / total_count) * 100
|
|
206
235
|
|
|
@@ -228,39 +257,22 @@ class FrequencyScreen(TableScreen):
|
|
|
228
257
|
Text("Total", style="bold", justify=dc.justify),
|
|
229
258
|
Text(f"{total_count:,}", style="bold", justify="right"),
|
|
230
259
|
Text("100.00", style="bold", justify="right"),
|
|
260
|
+
Bar(
|
|
261
|
+
highlight_range=(0.0, 10),
|
|
262
|
+
width=10,
|
|
263
|
+
),
|
|
231
264
|
key="total",
|
|
232
265
|
)
|
|
233
266
|
|
|
234
|
-
def on_key(self, event):
|
|
235
|
-
if event.key == "left_square_bracket": # '['
|
|
236
|
-
# Sort by current column in ascending order
|
|
237
|
-
self._sort_by_column(descending=False)
|
|
238
|
-
event.stop()
|
|
239
|
-
elif event.key == "right_square_bracket": # ']'
|
|
240
|
-
# Sort by current column in descending order
|
|
241
|
-
self._sort_by_column(descending=True)
|
|
242
|
-
event.stop()
|
|
243
|
-
elif event.key == "v":
|
|
244
|
-
# Filter the main table by the selected value
|
|
245
|
-
self._filter_or_highlight_selected_value(
|
|
246
|
-
self._get_col_name_value(), action="filter"
|
|
247
|
-
)
|
|
248
|
-
event.stop()
|
|
249
|
-
elif event.key == "quotation_mark": # '"'
|
|
250
|
-
# Highlight the main table by the selected value
|
|
251
|
-
self._filter_or_highlight_selected_value(
|
|
252
|
-
self._get_col_name_value(), action="highlight"
|
|
253
|
-
)
|
|
254
|
-
event.stop()
|
|
255
|
-
|
|
256
267
|
def _sort_by_column(self, descending: bool) -> None:
|
|
257
268
|
"""Sort the dataframe by the selected column and refresh the main table."""
|
|
258
|
-
freq_table = self.query_one(DataTable)
|
|
259
269
|
|
|
260
|
-
|
|
261
|
-
|
|
270
|
+
self.log(self.df)
|
|
271
|
+
|
|
272
|
+
row_idx, col_idx = self.table.cursor_coordinate
|
|
273
|
+
col_sort = col_idx if col_idx == 0 else 1
|
|
262
274
|
|
|
263
|
-
sort_dir = self.sorted_columns.get(
|
|
275
|
+
sort_dir = self.sorted_columns.get(col_sort)
|
|
264
276
|
if sort_dir is not None:
|
|
265
277
|
# If already sorted in the same direction, do nothing
|
|
266
278
|
if sort_dir == descending:
|
|
@@ -270,37 +282,16 @@ class FrequencyScreen(TableScreen):
|
|
|
270
282
|
return
|
|
271
283
|
|
|
272
284
|
self.sorted_columns.clear()
|
|
273
|
-
self.sorted_columns[
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
col_dtype = "Float64"
|
|
284
|
-
|
|
285
|
-
def key_fun(freq_col):
|
|
286
|
-
col_value = freq_col.plain
|
|
287
|
-
|
|
288
|
-
try:
|
|
289
|
-
if col_dtype == "Int64":
|
|
290
|
-
return int(col_value)
|
|
291
|
-
elif col_dtype == "Float64":
|
|
292
|
-
return float(col_value)
|
|
293
|
-
elif col_dtype == "Boolean":
|
|
294
|
-
return BOOLS[col_value]
|
|
295
|
-
else:
|
|
296
|
-
return col_value
|
|
297
|
-
except ValueError:
|
|
298
|
-
return 0
|
|
299
|
-
|
|
300
|
-
# Sort the table
|
|
301
|
-
freq_table.sort(
|
|
302
|
-
col_name, key=lambda freq_col: key_fun(freq_col), reverse=descending
|
|
303
|
-
)
|
|
285
|
+
self.sorted_columns[col_sort] = descending
|
|
286
|
+
|
|
287
|
+
col_name = self.df.columns[col_sort]
|
|
288
|
+
self.df = self.df.sort(col_name, descending=descending)
|
|
289
|
+
|
|
290
|
+
# Rebuild the frequency table
|
|
291
|
+
self.table.clear(columns=True)
|
|
292
|
+
self.build_table()
|
|
293
|
+
|
|
294
|
+
self.table.move_cursor(row=row_idx, column=col_idx)
|
|
304
295
|
|
|
305
296
|
# Notify the user
|
|
306
297
|
order = "desc" if descending else "asc"
|
|
@@ -65,7 +65,7 @@ class YesNoScreen(ModalScreen):
|
|
|
65
65
|
input: Optional input value to pre-fill an Input widget. If None, no Input is shown. If it is a 2-value tuple, the first value is the pre-filled input, and the second value is the type of input (e.g., "integer", "number", "text")
|
|
66
66
|
yes: Text for the Yes button. If None, hides the Yes button
|
|
67
67
|
no: Text for the No button. If None, hides the No button
|
|
68
|
-
on_yes_callback: Optional callable that takes no args and returns the value to dismiss with
|
|
68
|
+
on_yes_callback: Optional callable that takes no args and returns the value to dismiss with when Yes is pressed
|
|
69
69
|
"""
|
|
70
70
|
super().__init__()
|
|
71
71
|
self.title = title
|
|
@@ -179,7 +179,7 @@ class ConfirmScreen(YesNoScreen):
|
|
|
179
179
|
)
|
|
180
180
|
|
|
181
181
|
def handle_confirm(self) -> None:
|
|
182
|
-
|
|
182
|
+
return True
|
|
183
183
|
|
|
184
184
|
|
|
185
185
|
class EditCellScreen(YesNoScreen):
|
|
@@ -218,20 +218,18 @@ class EditCellScreen(YesNoScreen):
|
|
|
218
218
|
|
|
219
219
|
# Check if value changed
|
|
220
220
|
if new_value_str == self.input_value:
|
|
221
|
-
self.dismiss(None)
|
|
222
221
|
self.notify("No changes made", title="Edit", severity="warning")
|
|
223
|
-
return
|
|
222
|
+
return None
|
|
224
223
|
|
|
225
224
|
# Parse and validate based on column dtype
|
|
226
225
|
try:
|
|
227
226
|
new_value = DtypeConfig(self.col_dtype).convert(new_value_str)
|
|
228
227
|
except Exception as e:
|
|
229
|
-
self.dismiss(None) # Dismiss without changes
|
|
230
228
|
self.notify(f"Invalid value: {str(e)}", title="Edit", severity="error")
|
|
231
|
-
return
|
|
229
|
+
return None
|
|
232
230
|
|
|
233
|
-
#
|
|
234
|
-
self.
|
|
231
|
+
# New value
|
|
232
|
+
return self.row_key, self.col_idx, new_value
|
|
235
233
|
|
|
236
234
|
|
|
237
235
|
class SearchScreen(YesNoScreen):
|
|
@@ -265,8 +263,8 @@ class SearchScreen(YesNoScreen):
|
|
|
265
263
|
self.notify("Search term cannot be empty", title="Search", severity="error")
|
|
266
264
|
return
|
|
267
265
|
|
|
268
|
-
#
|
|
269
|
-
|
|
266
|
+
# Search term
|
|
267
|
+
return term, self.col_dtype, self.col_name
|
|
270
268
|
|
|
271
269
|
|
|
272
270
|
class FilterScreen(YesNoScreen):
|
|
@@ -303,20 +301,20 @@ class FilterScreen(YesNoScreen):
|
|
|
303
301
|
# Test the expression by evaluating it
|
|
304
302
|
expr = eval(expr_str, {"pl": pl})
|
|
305
303
|
|
|
306
|
-
#
|
|
307
|
-
|
|
304
|
+
# Expression is valid
|
|
305
|
+
return expr, expr_str
|
|
308
306
|
except Exception as e:
|
|
309
307
|
self.notify(
|
|
310
308
|
f"Error evaluating expression: {str(e)}",
|
|
311
309
|
title="Filter",
|
|
312
310
|
severity="error",
|
|
313
311
|
)
|
|
314
|
-
self.dismiss(None)
|
|
315
312
|
except ValueError as ve:
|
|
316
313
|
self.notify(
|
|
317
314
|
f"Invalid expression: {str(ve)}", title="Filter", severity="error"
|
|
318
315
|
)
|
|
319
|
-
|
|
316
|
+
|
|
317
|
+
return None
|
|
320
318
|
|
|
321
319
|
|
|
322
320
|
class FreezeScreen(YesNoScreen):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dataframe_textual-0.3.1 → dataframe_textual-0.3.2}/src/dataframe_textual/data_frame_help_panel.py
RENAMED
|
File without changes
|