dataframe-textual 1.2.0__py3-none-any.whl → 1.4.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/__main__.py +42 -20
- dataframe_textual/common.py +280 -72
- dataframe_textual/data_frame_help_panel.py +6 -4
- dataframe_textual/data_frame_table.py +633 -370
- dataframe_textual/data_frame_viewer.py +24 -28
- dataframe_textual/sql_screen.py +202 -0
- dataframe_textual/table_screen.py +31 -20
- dataframe_textual/yes_no_screen.py +12 -8
- {dataframe_textual-1.2.0.dist-info → dataframe_textual-1.4.0.dist-info}/METADATA +149 -13
- dataframe_textual-1.4.0.dist-info/RECORD +14 -0
- {dataframe_textual-1.2.0.dist-info → dataframe_textual-1.4.0.dist-info}/entry_points.txt +1 -0
- dataframe_textual-1.2.0.dist-info/RECORD +0 -13
- {dataframe_textual-1.2.0.dist-info → dataframe_textual-1.4.0.dist-info}/WHEEL +0 -0
- {dataframe_textual-1.2.0.dist-info → dataframe_textual-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,7 +10,7 @@ from textual.app import App, ComposeResult
|
|
|
10
10
|
from textual.css.query import NoMatches
|
|
11
11
|
from textual.theme import BUILTIN_THEMES
|
|
12
12
|
from textual.widgets import TabbedContent, TabPane
|
|
13
|
-
from textual.widgets.tabbed_content import
|
|
13
|
+
from textual.widgets.tabbed_content import ContentTabs
|
|
14
14
|
|
|
15
15
|
from .common import get_next_item, load_file
|
|
16
16
|
from .data_frame_help_panel import DataFrameHelpPanel
|
|
@@ -26,7 +26,7 @@ class DataFrameViewer(App):
|
|
|
26
26
|
|
|
27
27
|
## 🎯 File & Tab Management
|
|
28
28
|
- **Ctrl+O** - 📁 Add a new tab
|
|
29
|
-
- **Ctrl+
|
|
29
|
+
- **Ctrl+A** - 💾 Save all tabs
|
|
30
30
|
- **Ctrl+W** - ❌ Close current tab
|
|
31
31
|
- **>** or **b** - ▶️ Next tab
|
|
32
32
|
- **<** - ◀️ Previous tab
|
|
@@ -34,7 +34,7 @@ class DataFrameViewer(App):
|
|
|
34
34
|
- **q** - 🚪 Quit application
|
|
35
35
|
|
|
36
36
|
## 🎨 View & Settings
|
|
37
|
-
- **
|
|
37
|
+
- **F1** - ❓ Toggle this help panel
|
|
38
38
|
- **k** - 🌙 Cycle through themes
|
|
39
39
|
|
|
40
40
|
## ⭐ Features
|
|
@@ -51,30 +51,25 @@ class DataFrameViewer(App):
|
|
|
51
51
|
|
|
52
52
|
BINDINGS = [
|
|
53
53
|
("q", "quit", "Quit"),
|
|
54
|
-
("
|
|
54
|
+
("f1", "toggle_help_panel", "Help"),
|
|
55
55
|
("B", "toggle_tab_bar", "Toggle Tab Bar"),
|
|
56
56
|
("ctrl+o", "add_tab", "Add Tab"),
|
|
57
|
-
("ctrl+
|
|
57
|
+
("ctrl+a", "save_all_tabs", "Save All Tabs"),
|
|
58
58
|
("ctrl+w", "close_tab", "Close Tab"),
|
|
59
59
|
("greater_than_sign,b", "next_tab(1)", "Next Tab"),
|
|
60
60
|
("less_than_sign", "next_tab(-1)", "Prev Tab"),
|
|
61
61
|
]
|
|
62
62
|
|
|
63
63
|
CSS = """
|
|
64
|
-
TabbedContent {
|
|
65
|
-
height: 100%; /* Or a specific value, e.g., 20; */
|
|
66
|
-
}
|
|
67
64
|
TabbedContent > ContentTabs {
|
|
68
65
|
dock: bottom;
|
|
69
66
|
}
|
|
70
67
|
TabbedContent > ContentSwitcher {
|
|
71
68
|
overflow: auto;
|
|
72
|
-
height: 1fr;
|
|
69
|
+
height: 1fr;
|
|
73
70
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
background: $primary;
|
|
77
|
-
color: $text;
|
|
71
|
+
ContentTab.-active {
|
|
72
|
+
background: $block-cursor-background; /* Same as underline */
|
|
78
73
|
}
|
|
79
74
|
"""
|
|
80
75
|
|
|
@@ -85,7 +80,7 @@ class DataFrameViewer(App):
|
|
|
85
80
|
|
|
86
81
|
Args:
|
|
87
82
|
sources: sources to load dataframes from, each as a tuple of
|
|
88
|
-
(DataFrame
|
|
83
|
+
(DataFrame, filename, tabname).
|
|
89
84
|
|
|
90
85
|
Returns:
|
|
91
86
|
None
|
|
@@ -127,7 +122,11 @@ class DataFrameViewer(App):
|
|
|
127
122
|
self.tabs[tab] = table
|
|
128
123
|
yield tab
|
|
129
124
|
except Exception as e:
|
|
130
|
-
self.notify(
|
|
125
|
+
self.notify(
|
|
126
|
+
f"Error loading [$error]{filename}[/]: Try [$accent]-I[/] to disable schema inference",
|
|
127
|
+
severity="error",
|
|
128
|
+
)
|
|
129
|
+
self.log(f"Error loading `{filename}`: {str(e)}")
|
|
131
130
|
|
|
132
131
|
def on_mount(self) -> None:
|
|
133
132
|
"""Set up the application when it starts.
|
|
@@ -178,12 +177,6 @@ class DataFrameViewer(App):
|
|
|
178
177
|
if table.loaded_rows == 0:
|
|
179
178
|
table._setup_table()
|
|
180
179
|
|
|
181
|
-
# Apply background color to active tab
|
|
182
|
-
event.tab.add_class("active")
|
|
183
|
-
for tab in self.tabbed.query(ContentTab):
|
|
184
|
-
if tab != event.tab:
|
|
185
|
-
tab.remove_class("active")
|
|
186
|
-
|
|
187
180
|
def action_toggle_help_panel(self) -> None:
|
|
188
181
|
"""Toggle the help panel on or off.
|
|
189
182
|
|
|
@@ -305,11 +298,11 @@ class DataFrameViewer(App):
|
|
|
305
298
|
try:
|
|
306
299
|
n_tab = 0
|
|
307
300
|
for lf, filename, tabname in load_file(filename, prefix_sheet=True):
|
|
308
|
-
self._add_tab(lf
|
|
301
|
+
self._add_tab(lf, filename, tabname)
|
|
309
302
|
n_tab += 1
|
|
310
|
-
self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open")
|
|
303
|
+
# self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open")
|
|
311
304
|
except Exception as e:
|
|
312
|
-
self.notify(f"Error: {e}", title="Open", severity="error")
|
|
305
|
+
self.notify(f"Error loading [$error]{filename}[/]: {str(e)}", title="Open", severity="error")
|
|
313
306
|
else:
|
|
314
307
|
self.notify(f"File does not exist: [$warning]{filename}[/]", title="Open", severity="warning")
|
|
315
308
|
|
|
@@ -321,15 +314,18 @@ class DataFrameViewer(App):
|
|
|
321
314
|
if this is no longer the only tab.
|
|
322
315
|
|
|
323
316
|
Args:
|
|
324
|
-
|
|
317
|
+
lf: The Polars DataFrame to display in the new tab.
|
|
325
318
|
filename: The source filename for this data (used in table metadata).
|
|
326
319
|
tabname: The display name for the tab.
|
|
327
320
|
|
|
328
321
|
Returns:
|
|
329
322
|
None
|
|
330
323
|
"""
|
|
331
|
-
|
|
332
|
-
|
|
324
|
+
# Ensure unique tab names
|
|
325
|
+
counter = 1
|
|
326
|
+
while any(tab.name == tabname for tab in self.tabs):
|
|
327
|
+
tabname = f"{tabname}_{counter}"
|
|
328
|
+
counter += 1
|
|
333
329
|
|
|
334
330
|
# Find an available tab index
|
|
335
331
|
tab_idx = f"tab_{len(self.tabs) + 1}"
|
|
@@ -369,6 +365,6 @@ class DataFrameViewer(App):
|
|
|
369
365
|
if active_pane := self.tabbed.active_pane:
|
|
370
366
|
self.tabbed.remove_pane(active_pane.id)
|
|
371
367
|
self.tabs.pop(active_pane)
|
|
372
|
-
self.notify(f"Closed tab [$success]{active_pane.name}[/]", title="Close")
|
|
368
|
+
# self.notify(f"Closed tab [$success]{active_pane.name}[/]", title="Close")
|
|
373
369
|
except NoMatches:
|
|
374
370
|
pass
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Modal screens for Polars sql manipulation"""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .data_frame_table import DataFrameTable
|
|
7
|
+
|
|
8
|
+
import polars as pl
|
|
9
|
+
from textual.app import ComposeResult
|
|
10
|
+
from textual.containers import Container, Horizontal
|
|
11
|
+
from textual.screen import ModalScreen
|
|
12
|
+
from textual.widgets import Button, Input, Label, SelectionList, TextArea
|
|
13
|
+
from textual.widgets.selection_list import Selection
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SqlScreen(ModalScreen):
|
|
17
|
+
"""Base class for modal screens handling SQL query."""
|
|
18
|
+
|
|
19
|
+
DEFAULT_CSS = """
|
|
20
|
+
SqlScreen {
|
|
21
|
+
align: center middle;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
SqlScreen > Container {
|
|
25
|
+
width: auto;
|
|
26
|
+
height: auto;
|
|
27
|
+
border: heavy $accent;
|
|
28
|
+
border-title-color: $accent;
|
|
29
|
+
border-title-background: $panel;
|
|
30
|
+
border-title-style: bold;
|
|
31
|
+
background: $background;
|
|
32
|
+
padding: 1 2;
|
|
33
|
+
overflow: auto;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#button-container {
|
|
37
|
+
width: auto;
|
|
38
|
+
margin: 1 0 0 0;
|
|
39
|
+
height: 3;
|
|
40
|
+
align: center middle;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Button {
|
|
44
|
+
margin: 0 2;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, dftable: "DataFrameTable", on_yes_callback=None) -> None:
|
|
50
|
+
"""Initialize the SQL screen."""
|
|
51
|
+
super().__init__()
|
|
52
|
+
self.dftable = dftable # DataFrameTable
|
|
53
|
+
self.df: pl.DataFrame = dftable.df # Polars DataFrame
|
|
54
|
+
self.on_yes_callback = on_yes_callback
|
|
55
|
+
|
|
56
|
+
def compose(self) -> ComposeResult:
|
|
57
|
+
"""Compose the SQL screen widget structure."""
|
|
58
|
+
# Shared by subclasses
|
|
59
|
+
with Horizontal(id="button-container"):
|
|
60
|
+
yield Button("Apply", id="yes", variant="success")
|
|
61
|
+
yield Button("Cancel", id="no", variant="error")
|
|
62
|
+
|
|
63
|
+
def on_key(self, event) -> None:
|
|
64
|
+
"""Handle key press events in the SQL screen"""
|
|
65
|
+
if event.key in ("q", "escape"):
|
|
66
|
+
self.app.pop_screen()
|
|
67
|
+
event.stop()
|
|
68
|
+
elif event.key == "enter":
|
|
69
|
+
self._handle_yes()
|
|
70
|
+
event.stop()
|
|
71
|
+
elif event.key == "escape":
|
|
72
|
+
self.dismiss(None)
|
|
73
|
+
event.stop()
|
|
74
|
+
|
|
75
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
76
|
+
"""Handle button press events in the SQL screen."""
|
|
77
|
+
if event.button.id == "yes":
|
|
78
|
+
self._handle_yes()
|
|
79
|
+
elif event.button.id == "no":
|
|
80
|
+
self.dismiss(None)
|
|
81
|
+
|
|
82
|
+
def _handle_yes(self) -> None:
|
|
83
|
+
"""Handle Yes button/Enter key press."""
|
|
84
|
+
if self.on_yes_callback:
|
|
85
|
+
result = self.on_yes_callback()
|
|
86
|
+
self.dismiss(result)
|
|
87
|
+
else:
|
|
88
|
+
self.dismiss(True)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class SimpleSqlScreen(SqlScreen):
|
|
92
|
+
"""Simple SQL query screen."""
|
|
93
|
+
|
|
94
|
+
DEFAULT_CSS = SqlScreen.DEFAULT_CSS.replace("SqlScreen", "SimpleSqlScreen")
|
|
95
|
+
|
|
96
|
+
CSS = """
|
|
97
|
+
SimpleSqlScreen SelectionList {
|
|
98
|
+
width: auto;
|
|
99
|
+
min-width: 40;
|
|
100
|
+
margin: 1 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
SimpleSqlScreen SelectionList:blur {
|
|
104
|
+
border: solid $secondary;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
SimpleSqlScreen Label {
|
|
108
|
+
width: auto;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
SimpleSqlScreen Input {
|
|
112
|
+
width: auto;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
SimpleSqlScreen Input:blur {
|
|
116
|
+
border: solid $secondary;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#button-container {
|
|
120
|
+
min-width: 40;
|
|
121
|
+
}
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, dftable: "DataFrameTable") -> None:
|
|
125
|
+
"""Initialize the simple SQL screen.
|
|
126
|
+
|
|
127
|
+
Sets up the modal screen with reference to the main DataFrameTable widget
|
|
128
|
+
and stores the DataFrame for display.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
dftable: Reference to the parent DataFrameTable widget.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
None
|
|
135
|
+
"""
|
|
136
|
+
super().__init__(dftable, on_yes_callback=self._handle_simple)
|
|
137
|
+
|
|
138
|
+
def compose(self) -> ComposeResult:
|
|
139
|
+
"""Compose the simple SQL screen widget structure."""
|
|
140
|
+
with Container(id="sql-container") as container:
|
|
141
|
+
container.border_title = "SQL Query"
|
|
142
|
+
yield Label("Select columns (default to all):", id="select-label")
|
|
143
|
+
yield SelectionList(*[Selection(col, col) for col in self.df.columns], id="column-selection")
|
|
144
|
+
yield Label("Where condition (optional)", id="where-label")
|
|
145
|
+
yield Input(placeholder="e.g., age > 30 and height < 180", id="where-input")
|
|
146
|
+
yield from super().compose()
|
|
147
|
+
|
|
148
|
+
def _handle_simple(self) -> None:
|
|
149
|
+
"""Handle Yes button/Enter key press."""
|
|
150
|
+
selections = self.query_one(SelectionList).selected
|
|
151
|
+
columns = ", ".join(f"`{s}`" for s in selections) if selections else "*"
|
|
152
|
+
where = self.query_one(Input).value.strip()
|
|
153
|
+
|
|
154
|
+
return columns, where
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class AdvancedSqlScreen(SqlScreen):
|
|
158
|
+
"""Advanced SQL query screen."""
|
|
159
|
+
|
|
160
|
+
DEFAULT_CSS = SqlScreen.DEFAULT_CSS.replace("SqlScreen", "AdvancedSqlScreen")
|
|
161
|
+
|
|
162
|
+
CSS = """
|
|
163
|
+
AdvancedSqlScreen TextArea {
|
|
164
|
+
width: auto;
|
|
165
|
+
min-width: 60;
|
|
166
|
+
height: auto;
|
|
167
|
+
min-height: 10;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#button-container {
|
|
171
|
+
min-width: 60;
|
|
172
|
+
}
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(self, dftable: "DataFrameTable") -> None:
|
|
176
|
+
"""Initialize the simple SQL screen.
|
|
177
|
+
|
|
178
|
+
Sets up the modal screen with reference to the main DataFrameTable widget
|
|
179
|
+
and stores the DataFrame for display.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
dftable: Reference to the parent DataFrameTable widget.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
None
|
|
186
|
+
"""
|
|
187
|
+
super().__init__(dftable, on_yes_callback=self._handle_advanced)
|
|
188
|
+
|
|
189
|
+
def compose(self) -> ComposeResult:
|
|
190
|
+
"""Compose the advanced SQL screen widget structure."""
|
|
191
|
+
with Container(id="sql-container") as container:
|
|
192
|
+
container.border_title = "Advanced SQL Query"
|
|
193
|
+
yield TextArea.code_editor(
|
|
194
|
+
placeholder="Enter SQL query (use `self` as the table name), e.g., \n\nSELECT * \nFROM self \nWHERE age > 30",
|
|
195
|
+
id="sql-textarea",
|
|
196
|
+
language="sql",
|
|
197
|
+
)
|
|
198
|
+
yield from super().compose()
|
|
199
|
+
|
|
200
|
+
def _handle_advanced(self) -> None:
|
|
201
|
+
"""Handle Yes button/Enter key press."""
|
|
202
|
+
return self.query_one(TextArea).text.strip()
|
|
@@ -37,7 +37,7 @@ class TableScreen(ModalScreen):
|
|
|
37
37
|
}
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
def __init__(self, dftable: DataFrameTable) -> None:
|
|
40
|
+
def __init__(self, dftable: "DataFrameTable") -> None:
|
|
41
41
|
"""Initialize the table screen.
|
|
42
42
|
|
|
43
43
|
Sets up the base modal screen with reference to the main DataFrameTable widget
|
|
@@ -50,8 +50,8 @@ class TableScreen(ModalScreen):
|
|
|
50
50
|
None
|
|
51
51
|
"""
|
|
52
52
|
super().__init__()
|
|
53
|
-
self.df: pl.DataFrame = dftable.df # Polars DataFrame
|
|
54
53
|
self.dftable = dftable # DataFrameTable
|
|
54
|
+
self.df: pl.DataFrame = dftable.df # Polars DataFrame
|
|
55
55
|
self.thousand_separator = False # Whether to use thousand separators in numbers
|
|
56
56
|
|
|
57
57
|
def compose(self) -> ComposeResult:
|
|
@@ -181,7 +181,12 @@ class RowDetailScreen(TableScreen):
|
|
|
181
181
|
# Get all columns and values from the dataframe row
|
|
182
182
|
for col, val, dtype in zip(self.df.columns, self.df.row(self.ridx), self.df.dtypes):
|
|
183
183
|
self.table.add_row(
|
|
184
|
-
*format_row(
|
|
184
|
+
*format_row(
|
|
185
|
+
[col, val],
|
|
186
|
+
[None, dtype],
|
|
187
|
+
apply_justify=False,
|
|
188
|
+
thousand_separator=self.thousand_separator,
|
|
189
|
+
)
|
|
185
190
|
)
|
|
186
191
|
|
|
187
192
|
self.table.cursor_type = "row"
|
|
@@ -225,7 +230,7 @@ class StatisticsScreen(TableScreen):
|
|
|
225
230
|
|
|
226
231
|
CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "StatisticsScreen")
|
|
227
232
|
|
|
228
|
-
def __init__(self, dftable: DataFrameTable, col_idx: int | None = None):
|
|
233
|
+
def __init__(self, dftable: "DataFrameTable", col_idx: int | None = None):
|
|
229
234
|
super().__init__(dftable)
|
|
230
235
|
self.col_idx = col_idx # None for dataframe statistics, otherwise column index
|
|
231
236
|
|
|
@@ -240,9 +245,11 @@ class StatisticsScreen(TableScreen):
|
|
|
240
245
|
if self.col_idx is None:
|
|
241
246
|
# Dataframe statistics
|
|
242
247
|
self._build_dataframe_stats()
|
|
248
|
+
self.table.cursor_type = "column"
|
|
243
249
|
else:
|
|
244
250
|
# Column statistics
|
|
245
251
|
self._build_column_stats()
|
|
252
|
+
self.table.cursor_type = "row"
|
|
246
253
|
|
|
247
254
|
def _build_column_stats(self) -> None:
|
|
248
255
|
"""Build statistics for a single column."""
|
|
@@ -344,15 +351,16 @@ class FrequencyScreen(TableScreen):
|
|
|
344
351
|
|
|
345
352
|
CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "FrequencyScreen")
|
|
346
353
|
|
|
347
|
-
def __init__(self, col_idx: int, dftable: DataFrameTable):
|
|
354
|
+
def __init__(self, col_idx: int, dftable: "DataFrameTable") -> None:
|
|
348
355
|
super().__init__(dftable)
|
|
349
356
|
self.col_idx = col_idx
|
|
350
357
|
self.sorted_columns = {
|
|
351
358
|
1: True, # Count
|
|
352
359
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
)
|
|
360
|
+
|
|
361
|
+
df = dftable.df.filter(dftable.visible_rows) if False in dftable.visible_rows else dftable.df
|
|
362
|
+
self.total_count = len(df)
|
|
363
|
+
self.df: pl.DataFrame = df[df.columns[self.col_idx]].value_counts(sort=True).sort("count", descending=True)
|
|
356
364
|
|
|
357
365
|
def on_mount(self) -> None:
|
|
358
366
|
"""Create the frequency table."""
|
|
@@ -385,9 +393,6 @@ class FrequencyScreen(TableScreen):
|
|
|
385
393
|
dtype = self.dftable.df.dtypes[self.col_idx]
|
|
386
394
|
dc = DtypeConfig(dtype)
|
|
387
395
|
|
|
388
|
-
# Calculate frequencies using Polars
|
|
389
|
-
total_count = len(self.dftable.df)
|
|
390
|
-
|
|
391
396
|
# Add column headers with sort indicators
|
|
392
397
|
columns = [
|
|
393
398
|
(column, "Value", 0),
|
|
@@ -415,7 +420,7 @@ class FrequencyScreen(TableScreen):
|
|
|
415
420
|
# Add rows to the frequency table
|
|
416
421
|
for row_idx, row in enumerate(self.df.rows()):
|
|
417
422
|
column, count = row
|
|
418
|
-
percentage = (count / total_count) * 100
|
|
423
|
+
percentage = (count / self.total_count) * 100
|
|
419
424
|
|
|
420
425
|
if column is None:
|
|
421
426
|
value = NULL_DISPLAY
|
|
@@ -432,7 +437,7 @@ class FrequencyScreen(TableScreen):
|
|
|
432
437
|
f"{count:,}" if self.thousand_separator else str(count), style=ds_int.style, justify=ds_int.justify
|
|
433
438
|
),
|
|
434
439
|
Text(
|
|
435
|
-
|
|
440
|
+
format_float(percentage, self.thousand_separator),
|
|
436
441
|
style=ds_float.style,
|
|
437
442
|
justify=ds_float.justify,
|
|
438
443
|
),
|
|
@@ -446,8 +451,16 @@ class FrequencyScreen(TableScreen):
|
|
|
446
451
|
# Add a total row
|
|
447
452
|
self.table.add_row(
|
|
448
453
|
Text("Total", style="bold", justify=dc.justify),
|
|
449
|
-
Text(
|
|
450
|
-
|
|
454
|
+
Text(
|
|
455
|
+
f"{self.total_count:,}" if self.thousand_separator else str(self.total_count),
|
|
456
|
+
style="bold",
|
|
457
|
+
justify="right",
|
|
458
|
+
),
|
|
459
|
+
Text(
|
|
460
|
+
format_float(100.0, self.thousand_separator),
|
|
461
|
+
style="bold",
|
|
462
|
+
justify="right",
|
|
463
|
+
),
|
|
451
464
|
Bar(
|
|
452
465
|
highlight_range=(0.0, 10),
|
|
453
466
|
width=10,
|
|
@@ -460,12 +473,10 @@ class FrequencyScreen(TableScreen):
|
|
|
460
473
|
row_idx, col_idx = self.table.cursor_coordinate
|
|
461
474
|
col_sort = col_idx if col_idx == 0 else 1
|
|
462
475
|
|
|
463
|
-
|
|
464
|
-
if sort_dir is not None:
|
|
476
|
+
if self.sorted_columns.get(col_sort) == descending:
|
|
465
477
|
# If already sorted in the same direction, do nothing
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return
|
|
478
|
+
# self.notify("Already sorted in that order", title="Sort", severity="warning")
|
|
479
|
+
return
|
|
469
480
|
|
|
470
481
|
self.sorted_columns.clear()
|
|
471
482
|
self.sorted_columns[col_sort] = descending
|
|
@@ -33,9 +33,11 @@ class YesNoScreen(ModalScreen):
|
|
|
33
33
|
min-width: 40;
|
|
34
34
|
max-width: 60;
|
|
35
35
|
height: auto;
|
|
36
|
-
border: heavy $
|
|
37
|
-
border-title-color: $
|
|
38
|
-
background: $
|
|
36
|
+
border: heavy $accent;
|
|
37
|
+
border-title-color: $accent;
|
|
38
|
+
border-title-background: $panel;
|
|
39
|
+
border-title-style: bold;
|
|
40
|
+
background: $background;
|
|
39
41
|
padding: 1 2;
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -241,6 +243,7 @@ class YesNoScreen(ModalScreen):
|
|
|
241
243
|
yield self.no
|
|
242
244
|
|
|
243
245
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
246
|
+
"""Handle button press events in the Yes/No screen."""
|
|
244
247
|
if event.button.id == "yes":
|
|
245
248
|
self._handle_yes()
|
|
246
249
|
elif event.button.id == "maybe":
|
|
@@ -249,6 +252,7 @@ class YesNoScreen(ModalScreen):
|
|
|
249
252
|
self.dismiss(None)
|
|
250
253
|
|
|
251
254
|
def on_key(self, event) -> None:
|
|
255
|
+
"""Handle key press events in the table screen."""
|
|
252
256
|
if event.key == "enter":
|
|
253
257
|
self._handle_yes()
|
|
254
258
|
event.stop()
|
|
@@ -298,7 +302,7 @@ class SaveFileScreen(YesNoScreen):
|
|
|
298
302
|
|
|
299
303
|
|
|
300
304
|
class ConfirmScreen(YesNoScreen):
|
|
301
|
-
"""Modal screen to
|
|
305
|
+
"""Modal screen to ask for confirmation."""
|
|
302
306
|
|
|
303
307
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "ConfirmScreen")
|
|
304
308
|
|
|
@@ -582,7 +586,7 @@ class AddColumnScreen(YesNoScreen):
|
|
|
582
586
|
title="Add Column",
|
|
583
587
|
label="Enter column name",
|
|
584
588
|
input="column name",
|
|
585
|
-
label2="Enter value or Polars expression, e.g., 123, NULL, $_ * 2",
|
|
589
|
+
label2="Enter value or Polars expression, e.g., abc, pl.lit(123), NULL, $_ * 2, $1 + $2, $_.str.to_uppercase(), pl.concat_str($_, pl.lit('-suffix'))",
|
|
586
590
|
input2="column value or expression",
|
|
587
591
|
on_yes_callback=self._get_input,
|
|
588
592
|
)
|
|
@@ -609,7 +613,7 @@ class AddColumnScreen(YesNoScreen):
|
|
|
609
613
|
return self.cidx, col_name, pl.lit(None)
|
|
610
614
|
elif tentative_expr(term):
|
|
611
615
|
try:
|
|
612
|
-
expr = validate_expr(term, self.df, self.cidx)
|
|
616
|
+
expr = validate_expr(term, self.df.columns, self.cidx)
|
|
613
617
|
return self.cidx, col_name, expr
|
|
614
618
|
except ValueError as e:
|
|
615
619
|
self.notify(f"Invalid expression [$error]{term}[/]: {str(e)}", title="Add Column", severity="error")
|
|
@@ -634,10 +638,10 @@ class FindReplaceScreen(YesNoScreen):
|
|
|
634
638
|
|
|
635
639
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "ReplaceScreen")
|
|
636
640
|
|
|
637
|
-
def __init__(self, dftable: DataFrameTable):
|
|
641
|
+
def __init__(self, dftable: "DataFrameTable", title: str = "Find and Replace"):
|
|
638
642
|
term_find = str(dftable.cursor_value)
|
|
639
643
|
super().__init__(
|
|
640
|
-
title=
|
|
644
|
+
title=title,
|
|
641
645
|
label="Find",
|
|
642
646
|
input=term_find,
|
|
643
647
|
label2="Replace with",
|