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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataframe-textual
3
- Version: 0.3.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dataframe-textual"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Interactive CSV/Excel viewer for the terminal (Textual TUI)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -577,7 +577,7 @@ class DataFrameTable(DataTable):
577
577
 
578
578
  # Add to history
579
579
  self._add_history(
580
- f"Pinned [on $primary]{fixed_rows}[/] rows and [on $primary]{fixed_columns}[/] columns"
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 [on $primary]{fixed_rows}[/] rows and [on $primary]{fixed_columns}[/] columns",
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 [on $success]{match_count}[/] matches for [on $primary]{term}[/] across all columns",
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 [on $primary]{new_selected_count}[/] rows",
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 [on $primary]{selected_count}[/] selected rows", title="Clear"
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 [on $primary]{selected_count}[/] rows",
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 [on $primary]{matched_count}[/] matching rows",
1256
+ f"Filtered to [$accent]{matched_count}[/] matching rows",
1257
1257
  title="Filter",
1258
1258
  )
1259
1259
 
@@ -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(f"Switched to theme: [$primary]{self.theme}[/]", title="Theme")
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
- tab = TabPane(tabname, table, name=tabname, id=f"tab_{len(self.tabs) + 1}")
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 BOOLS, DtypeConfig, _format_row
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
- column = self.df.columns[self.col_idx]
185
- dtype = str(self.df.dtypes[self.col_idx])
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
- freq_df = self.df[column].value_counts(sort=True).sort("count", descending=True)
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(freq_df.rows()):
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
- col_idx = freq_table.cursor_column
261
- col_dtype = "String"
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(col_idx)
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[col_idx] = descending
274
-
275
- if col_idx == 0:
276
- col_name = self.df.columns[self.col_idx]
277
- col_dtype = str(self.df.dtypes[self.col_idx])
278
- elif col_idx == 1:
279
- col_name = "Count"
280
- col_dtype = "Int64"
281
- elif col_idx == 2:
282
- col_name = "%"
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
- self.dismiss(True)
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
- # Dismiss with the new value
234
- self.dismiss((self.row_key, self.col_idx, new_value))
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
- # Dismiss with the search term
269
- self.dismiss((term, self.col_dtype, self.col_name))
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
- # Dismiss with the expression
307
- self.dismiss((expr, expr_str))
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
- self.dismiss(None)
316
+
317
+ return None
320
318
 
321
319
 
322
320
  class FreezeScreen(YesNoScreen):
@@ -171,7 +171,7 @@ wheels = [
171
171
 
172
172
  [[package]]
173
173
  name = "dataframe-textual"
174
- version = "0.3.1"
174
+ version = "0.3.2"
175
175
  source = { editable = "." }
176
176
  dependencies = [
177
177
  { name = "polars" },