dataframe-textual 1.1.5__py3-none-any.whl → 1.3.9__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.
@@ -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 ContentTab, ContentTabs
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+Shift+S** - 💾 Save all tabs
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
- - **Ctrl+H** - ❓ Toggle this help panel
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
- ("ctrl+h", "toggle_help_panel", "Help"),
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+shift+s", "save_all_tabs", "Save All Tabs"),
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; /* Takes the remaining space below tabs */
69
+ height: 1fr;
73
70
  }
74
-
75
- TabbedContent ContentTab.active {
76
- background: $primary;
77
- color: $text;
71
+ ContentTab.-active {
72
+ background: $block-cursor-background; /* Same as underline */
78
73
  }
79
74
  """
80
75
 
@@ -178,12 +173,6 @@ class DataFrameViewer(App):
178
173
  if table.loaded_rows == 0:
179
174
  table._setup_table()
180
175
 
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
176
  def action_toggle_help_panel(self) -> None:
188
177
  """Toggle the help panel on or off.
189
178
 
@@ -305,15 +294,15 @@ class DataFrameViewer(App):
305
294
  try:
306
295
  n_tab = 0
307
296
  for lf, filename, tabname in load_file(filename, prefix_sheet=True):
308
- self._add_tab(lf.collect(), filename, tabname)
297
+ self._add_tab(lf, filename, tabname)
309
298
  n_tab += 1
310
- self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open")
299
+ # self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open")
311
300
  except Exception as e:
312
- self.notify(f"Error: {e}", title="Open", severity="error")
301
+ self.notify(f"Error loading [$error]{filename}[/]: {str(e)}", title="Open", severity="error")
313
302
  else:
314
303
  self.notify(f"File does not exist: [$warning]{filename}[/]", title="Open", severity="warning")
315
304
 
316
- def _add_tab(self, df: pl.DataFrame, filename: str, tabname: str) -> None:
305
+ def _add_tab(self, df: pl.DataFrame | pl.LazyFrame, filename: str, tabname: str) -> None:
317
306
  """Add new tab for the given DataFrame.
318
307
 
319
308
  Creates and adds a new tab with the provided DataFrame and configuration.
@@ -321,15 +310,18 @@ class DataFrameViewer(App):
321
310
  if this is no longer the only tab.
322
311
 
323
312
  Args:
324
- df: The Polars DataFrame to display in the new tab.
313
+ lf: The Polars DataFrame to display in the new tab.
325
314
  filename: The source filename for this data (used in table metadata).
326
315
  tabname: The display name for the tab.
327
316
 
328
317
  Returns:
329
318
  None
330
319
  """
331
- if any(tab.name == tabname for tab in self.tabs):
332
- tabname = f"{tabname}_{len(self.tabs) + 1}"
320
+ # Ensure unique tab names
321
+ counter = 1
322
+ while any(tab.name == tabname for tab in self.tabs):
323
+ tabname = f"{tabname}_{counter}"
324
+ counter += 1
333
325
 
334
326
  # Find an available tab index
335
327
  tab_idx = f"tab_{len(self.tabs) + 1}"
@@ -369,6 +361,6 @@ class DataFrameViewer(App):
369
361
  if active_pane := self.tabbed.active_pane:
370
362
  self.tabbed.remove_pane(active_pane.id)
371
363
  self.tabs.pop(active_pane)
372
- self.notify(f"Closed tab [$success]{active_pane.name}[/]", title="Close")
364
+ # self.notify(f"Closed tab [$success]{active_pane.name}[/]", title="Close")
373
365
  except NoMatches:
374
366
  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()
@@ -30,12 +30,14 @@ class TableScreen(ModalScreen):
30
30
 
31
31
  TableScreen > DataTable {
32
32
  width: auto;
33
- min-width: 20;
33
+ height: auto;
34
34
  border: solid $primary;
35
+ max-width: 100%;
36
+ overflow: auto;
35
37
  }
36
38
  """
37
39
 
38
- def __init__(self, dftable: DataFrameTable) -> None:
40
+ def __init__(self, dftable: "DataFrameTable") -> None:
39
41
  """Initialize the table screen.
40
42
 
41
43
  Sets up the base modal screen with reference to the main DataFrameTable widget
@@ -48,8 +50,8 @@ class TableScreen(ModalScreen):
48
50
  None
49
51
  """
50
52
  super().__init__()
51
- self.df: pl.DataFrame = dftable.df # Polars DataFrame
52
53
  self.dftable = dftable # DataFrameTable
54
+ self.df: pl.DataFrame = dftable.df # Polars DataFrame
53
55
  self.thousand_separator = False # Whether to use thousand separators in numbers
54
56
 
55
57
  def compose(self) -> ComposeResult:
@@ -223,7 +225,7 @@ class StatisticsScreen(TableScreen):
223
225
 
224
226
  CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "StatisticsScreen")
225
227
 
226
- def __init__(self, dftable: DataFrameTable, col_idx: int | None = None):
228
+ def __init__(self, dftable: "DataFrameTable", col_idx: int | None = None):
227
229
  super().__init__(dftable)
228
230
  self.col_idx = col_idx # None for dataframe statistics, otherwise column index
229
231
 
@@ -291,6 +293,10 @@ class StatisticsScreen(TableScreen):
291
293
  if False in self.dftable.visible_rows:
292
294
  lf = lf.filter(self.dftable.visible_rows)
293
295
 
296
+ # Apply only to non-hidden columns
297
+ if self.dftable.hidden_columns:
298
+ lf = lf.select(pl.exclude(self.dftable.hidden_columns))
299
+
294
300
  # Get dataframe statistics
295
301
  stats_df = lf.collect().describe()
296
302
 
@@ -338,15 +344,16 @@ class FrequencyScreen(TableScreen):
338
344
 
339
345
  CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "FrequencyScreen")
340
346
 
341
- def __init__(self, col_idx: int, dftable: DataFrameTable):
347
+ def __init__(self, col_idx: int, dftable: "DataFrameTable") -> None:
342
348
  super().__init__(dftable)
343
349
  self.col_idx = col_idx
344
350
  self.sorted_columns = {
345
351
  1: True, # Count
346
352
  }
347
- self.df: pl.DataFrame = (
348
- dftable.df[dftable.df.columns[self.col_idx]].value_counts(sort=True).sort("count", descending=True)
349
- )
353
+
354
+ df = dftable.df.filter(dftable.visible_rows) if False in dftable.visible_rows else dftable.df
355
+ self.total_count = len(df)
356
+ self.df: pl.DataFrame = df[df.columns[self.col_idx]].value_counts(sort=True).sort("count", descending=True)
350
357
 
351
358
  def on_mount(self) -> None:
352
359
  """Create the frequency table."""
@@ -379,9 +386,6 @@ class FrequencyScreen(TableScreen):
379
386
  dtype = self.dftable.df.dtypes[self.col_idx]
380
387
  dc = DtypeConfig(dtype)
381
388
 
382
- # Calculate frequencies using Polars
383
- total_count = len(self.dftable.df)
384
-
385
389
  # Add column headers with sort indicators
386
390
  columns = [
387
391
  (column, "Value", 0),
@@ -409,7 +413,7 @@ class FrequencyScreen(TableScreen):
409
413
  # Add rows to the frequency table
410
414
  for row_idx, row in enumerate(self.df.rows()):
411
415
  column, count = row
412
- percentage = (count / total_count) * 100
416
+ percentage = (count / self.total_count) * 100
413
417
 
414
418
  if column is None:
415
419
  value = NULL_DISPLAY
@@ -440,7 +444,7 @@ class FrequencyScreen(TableScreen):
440
444
  # Add a total row
441
445
  self.table.add_row(
442
446
  Text("Total", style="bold", justify=dc.justify),
443
- Text(f"{total_count:,}", style="bold", justify="right"),
447
+ Text(f"{self.total_count:,}", style="bold", justify="right"),
444
448
  Text("100.00", style="bold", justify="right"),
445
449
  Bar(
446
450
  highlight_range=(0.0, 10),
@@ -454,12 +458,10 @@ class FrequencyScreen(TableScreen):
454
458
  row_idx, col_idx = self.table.cursor_coordinate
455
459
  col_sort = col_idx if col_idx == 0 else 1
456
460
 
457
- sort_dir = self.sorted_columns.get(col_sort)
458
- if sort_dir is not None:
461
+ if self.sorted_columns.get(col_sort) == descending:
459
462
  # If already sorted in the same direction, do nothing
460
- if sort_dir == descending:
461
- self.notify("Already sorted in that order", title="Sort", severity="warning")
462
- return
463
+ # self.notify("Already sorted in that order", title="Sort", severity="warning")
464
+ return
463
465
 
464
466
  self.sorted_columns.clear()
465
467
  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 $primary;
37
- border-title-color: $primary-lighten-3;
38
- background: $surface;
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()
@@ -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,7 +638,7 @@ 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"):
638
642
  term_find = str(dftable.cursor_value)
639
643
  super().__init__(
640
644
  title="Find and Replace",