diff --git a/src/dt_browser/browser.py b/src/dt_browser/browser.py
index 4574d7d..46947d5 100644
--- a/src/dt_browser/browser.py
+++ b/src/dt_browser/browser.py
@@ -28,6 +28,7 @@
SelectFromTable,
)
from dt_browser.bookmarks import Bookmarks
+from dt_browser.column_metadata import ColumnMetadata
from dt_browser.column_selector import ColumnSelector
from dt_browser.custom_table import CustomTable, _color_name, polars_list_to_string
from dt_browser.expression_box import ExpressionBox
@@ -248,9 +249,8 @@ def compute_active_search_idx_display(self):
class RowDetail(Widget, can_focus=False, can_focus_children=False):
DEFAULT_CSS = """
RowDetail {
- width: auto;
- max_width: 50%;
- min_width: 30%;
+ width: 100%;
+ height: 1fr;
padding: 0 1;
border: tall $primary;
}
@@ -288,14 +288,43 @@ def watch_row_df(self):
assert self._schema is not None
display_df = display_df.join(self._schema, on=["Field"]).select(["Field", "dtype", "Value"])
self._dt.set_dt(display_df, display_df.with_row_index(name=INDEX_COL).select([INDEX_COL]))
- self.styles.width = self._dt.virtual_size.width + self.gutter.width + 1
self._dt.refresh()
+ if isinstance(self.parent, DetailPanel):
+ self.parent.update_width()
# self._dt.go_to_cell(coord)
+ @property
+ def content_width(self) -> int:
+ return self._dt.virtual_size.width + self.gutter.width + 1
+
def compose(self):
yield self._dt
+class DetailPanel(Widget, can_focus=False):
+ DEFAULT_CSS = """
+DetailPanel {
+ max-width: 50%;
+ min-width: 30%;
+ layout: vertical;
+}
+"""
+
+ def __init__(self, row_detail: RowDetail, column_metadata: ColumnMetadata, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._row_detail = row_detail
+ self._column_metadata = column_metadata
+
+ def update_width(self) -> None:
+ row_detail_width = self._row_detail.content_width if not self._row_detail.row_df.is_empty() else 0
+ meta_width = self._column_metadata.content_size.width + self._column_metadata.gutter.width
+ self.styles.width = max(row_detail_width, meta_width)
+
+ def compose(self):
+ yield self._row_detail
+ yield self._column_metadata
+
+
def from_file_path(path: pathlib.Path, has_header: bool = True) -> pl.DataFrame:
if path.suffix in [".arrow", ".feather"]:
@@ -342,6 +371,7 @@ class DtBrowser(Widget): # pylint: disable=too-many-public-methods,too-many-ins
current_filter = reactive[str | None](None)
cur_row = reactive(0)
+ cur_col = reactive(0)
cur_total_rows = reactive(0)
total_rows = reactive(0)
@@ -400,6 +430,9 @@ def __init__(
self._ts_col_selector.styles.width = 1
self._row_detail = RowDetail()
+ self._column_metadata = ColumnMetadata()
+ self._column_metadata.set_source_df(self._filtered_dt)
+ self._detail_panel = DetailPanel(self._row_detail, self._column_metadata)
self._color_by_cache: LRUCache[tuple[str, ...], pl.Series] = LRUCache(5)
self._last_message_ts = time.time()
@@ -623,10 +656,10 @@ async def action_show_save(self):
async def watch_show_row_detail(self):
if not self.show_row_detail:
- if existing := self.query(RowDetail):
+ if existing := self.query(DetailPanel):
existing.remove()
elif not self._display_dt.is_empty():
- await self.query_one("#main_hori", Horizontal).mount(self._row_detail)
+ await self.query_one("#main_hori", Horizontal).mount(self._detail_panel)
async def action_show_bookmarks(self):
await self.mount(self._bookmarks, before=self.query_one(TableFooter))
@@ -644,6 +677,8 @@ async def action_timestamp_selector(self):
def _set_filtered_dt(self, filtered_dt: pl.DataFrame, filtered_meta: pl.DataFrame, **kwargs):
self._filtered_dt = filtered_dt
self._meta_dt = filtered_meta
+ self._column_metadata.set_source_df(self._filtered_dt)
+ self._column_metadata.invalidate_cache()
self._set_active_dt(self._filtered_dt, **kwargs)
def _set_active_dt(self, active_dt: pl.DataFrame, new_row: int | None = None):
@@ -714,10 +749,20 @@ def enable_select_from_table(self, event: SelectFromTable):
@on(CustomTable.CellHighlighted, selector="#main_table")
async def handle_cell_highlight(self, event: CustomTable.CellHighlighted):
self.cur_row = event.coordinate.row
+ col = event.coordinate.column
+ if col != self.cur_col:
+ self.cur_col = col
def watch_cur_row(self):
self._row_detail.row_df = self._display_dt[self.cur_row]
+ def watch_cur_col(self):
+ if self._display_dt.is_empty() or self.cur_col >= len(self._display_dt.columns):
+ return
+ col_name = self._display_dt.columns[self.cur_col]
+ dtype = self._display_dt.schema[col_name]
+ self._column_metadata.column_info = (col_name, dtype)
+
@on(CustomTable.CellSelected, selector="#main_table")
def handle_cell_select(self, event: CustomTable.CellSelected):
if self._select_interest:
@@ -857,6 +902,9 @@ def on_mount(self):
self.cur_total_rows = len(self._display_dt)
self.total_rows = len(self._original_dt)
self._row_detail.row_df = self._display_dt[0]
+ if not self._display_dt.is_empty():
+ col_name = self._display_dt.columns[0]
+ self._column_metadata.column_info = (col_name, self._display_dt.schema[col_name])
if self.removed_cols:
err_str = ", ".join(f"{k}: {v}" for k, v in self.removed_cols.items())
self.notify(
diff --git a/src/dt_browser/column_metadata.py b/src/dt_browser/column_metadata.py
new file mode 100644
index 0000000..8d10f95
--- /dev/null
+++ b/src/dt_browser/column_metadata.py
@@ -0,0 +1,134 @@
+import polars as pl
+from rich.table import Table as RichTable
+from textual import work
+from textual.reactive import reactive
+from textual.widget import Widget
+from textual.widgets import Static
+
+
+def _categorical_stats(series: pl.Series) -> list[tuple[str, str]]:
+ n_unique = series.n_unique()
+ stats: list[tuple[str, str]] = [("Unique values", str(n_unique))]
+ val_col = series.name
+ vc = series.value_counts().sort(["count", val_col], descending=[True, False]).head(10)
+ for row in vc.iter_rows(named=True):
+ stats.append((f" {row[val_col]}", str(row["count"])))
+ return stats
+
+
+def _numeric_stats(series: pl.Series) -> list[tuple[str, str]]:
+ s = series.drop_nulls()
+ if s.is_empty():
+ return [("", "No data")]
+ stats = [
+ ("Min", str(s.min())),
+ ("Q1", str(s.quantile(0.25))),
+ ("Median", str(s.median())),
+ ("Q3", str(s.quantile(0.75))),
+ ("Max", str(s.max())),
+ ]
+ if s.dtype.is_float():
+ nan_count = s.is_nan().sum()
+ if nan_count > 0:
+ stats.append(("NaN", str(nan_count)))
+ return stats
+
+
+def _temporal_stats(series: pl.Series) -> list[tuple[str, str]]:
+ s = series.drop_nulls()
+ if s.is_empty():
+ return [("", "No data")]
+ return [
+ ("Min", str(s.min())),
+ ("Max", str(s.max())),
+ ]
+
+
+def _boolean_stats(series: pl.Series) -> list[tuple[str, str]]:
+ true_count = series.sum()
+ null_count = series.null_count()
+ false_count = len(series) - (true_count or 0) - null_count
+ return [
+ ("True", str(true_count)),
+ ("False", str(false_count)),
+ ]
+
+
+def compute_column_stats(series: pl.Series) -> list[tuple[str, str]]:
+ dtype = series.dtype
+ if dtype == pl.Categorical:
+ stats = _categorical_stats(series)
+ elif dtype.is_numeric():
+ stats = _numeric_stats(series)
+ elif dtype.is_temporal():
+ stats = _temporal_stats(series)
+ elif dtype.is_(pl.Boolean):
+ stats = _boolean_stats(series)
+ else:
+ return []
+ null_count = series.null_count()
+ if null_count > 0:
+ stats.append(("Null", str(null_count)))
+ return stats
+
+
+class ColumnMetadata(Widget, can_focus=False, can_focus_children=False):
+ DEFAULT_CSS = """
+ColumnMetadata {
+ width: 100%;
+ height: auto;
+ padding: 0 1;
+ border: tall $primary;
+}
+"""
+ column_info: reactive[tuple[str, pl.DataType] | None] = reactive(None)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.border_title = "Column Metadata"
+ self._source_df: pl.DataFrame = pl.DataFrame()
+ self._cache: dict[str, list[tuple[str, str]]] = {}
+ self._static = Static("")
+
+ def set_source_df(self, df: pl.DataFrame) -> None:
+ self._source_df = df
+
+ def invalidate_cache(self) -> None:
+ self._cache.clear()
+
+ def _render_stats(self, col_name: str, stats: list[tuple[str, str]]) -> None:
+ self.border_title = f"Column: {col_name}"
+ if not stats:
+ self._static.update("")
+ return
+ table = RichTable(show_header=False, box=None, padding=(0, 1), expand=True)
+ table.add_column("Stat", no_wrap=True)
+ table.add_column("Value", no_wrap=True, justify="right")
+ for label, value in stats:
+ table.add_row(label, value)
+ self._static.update(table)
+ if self.parent is not None and hasattr(self.parent, "update_width"):
+ self.parent.update_width()
+
+ def watch_column_info(self) -> None:
+ if self.column_info is None or self._source_df.is_empty():
+ return
+ col_name, _ = self.column_info
+ if col_name not in self._source_df.columns:
+ return
+ if col_name in self._cache:
+ self._render_stats(col_name, self._cache[col_name])
+ else:
+ self.border_title = f"Column: {col_name}"
+ self._static.update("Computing...")
+ self._compute_stats(col_name)
+
+ @work(exclusive=True)
+ async def _compute_stats(self, col_name: str) -> None:
+ series = self._source_df[col_name]
+ stats = compute_column_stats(series)
+ self._cache[col_name] = stats
+ self._render_stats(col_name, stats)
+
+ def compose(self):
+ yield self._static
diff --git a/tests/__snapshots__/test_column_metadata/test_snap_column_metadata_boolean.svg b/tests/__snapshots__/test_column_metadata/test_snap_column_metadata_boolean.svg
new file mode 100644
index 0000000..2fba790
--- /dev/null
+++ b/tests/__snapshots__/test_column_metadata/test_snap_column_metadata_boolean.svg
@@ -0,0 +1,181 @@
+
diff --git a/tests/__snapshots__/test_column_metadata/test_snap_column_metadata_categorical.svg b/tests/__snapshots__/test_column_metadata/test_snap_column_metadata_categorical.svg
new file mode 100644
index 0000000..06ad954
--- /dev/null
+++ b/tests/__snapshots__/test_column_metadata/test_snap_column_metadata_categorical.svg
@@ -0,0 +1,181 @@
+
diff --git a/tests/__snapshots__/test_column_metadata/test_snap_column_metadata_numeric.svg b/tests/__snapshots__/test_column_metadata/test_snap_column_metadata_numeric.svg
new file mode 100644
index 0000000..9710c26
--- /dev/null
+++ b/tests/__snapshots__/test_column_metadata/test_snap_column_metadata_numeric.svg
@@ -0,0 +1,181 @@
+
diff --git a/tests/__snapshots__/test_column_metadata/test_snap_detail_panel_layout.svg b/tests/__snapshots__/test_column_metadata/test_snap_detail_panel_layout.svg
new file mode 100644
index 0000000..5791943
--- /dev/null
+++ b/tests/__snapshots__/test_column_metadata/test_snap_detail_panel_layout.svg
@@ -0,0 +1,181 @@
+
diff --git a/tests/__snapshots__/test_snapshots/test_snap_column_hidden.svg b/tests/__snapshots__/test_snapshots/test_snap_column_hidden.svg
index 0e96097..dcfa102 100644
--- a/tests/__snapshots__/test_snapshots/test_snap_column_hidden.svg
+++ b/tests/__snapshots__/test_snapshots/test_snap_column_hidden.svg
@@ -43,9 +43,8 @@
.terminal-r9 { fill: #242f38 }
.terminal-r10 { fill: #8ad4a1 }
.terminal-r11 { fill: #000f18 }
-.terminal-r12 { fill: #003054 }
-.terminal-r13 { fill: #ffa62b;font-weight: bold }
-.terminal-r14 { fill: #495259 }
+.terminal-r12 { fill: #ffa62b;font-weight: bold }
+.terminal-r13 { fill: #495259 }
@@ -149,38 +148,38 @@
-
+
- Row # name value category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▎▊▔ Show/Hide/Reorder Columns ▔▔▔▔▔▎
- 1 item_0 0 cat_0 ▊ Field dtype ▎▊▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▎
- 2 item_1 1 cat_1 ▊ Row # UInt32 ▎▊▊Type to filter columns ▎▎
- 3 item_2 2 cat_2 ▊ name String ▎▊▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
- 4 item_3 3 cat_3 ▊ value Int64 ▎▊▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▎
- 5 item_4 4 cat_4 ▊ score Float64 ▎▊▊▐X▌ Row #▎▎
- 6 item_5 5 cat_0 ▊ category String ▎▊▊▐X▌ name▎▎
- 7 item_6 6 cat_1 ▊▎▊▊▐X▌ value▎▎
- 8 item_7 7 cat_2 ▊▎▊▊▐X▌ score▎▎
- 9 item_8 8 cat_3 ▊▎▊▊▐X▌ category▎▎
- 10 item_9 9 cat_4 ▊▎▊▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
- 11 item_10 10 cat_0 ▊▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- 12 item_11 11 cat_1 ▊▎
- 13 item_12 12 cat_2 ▊▎
- 14 item_13 13 cat_3 ▊▎
- 15 item_14 14 cat_4 ▊▎
- 16 item_15 15 cat_0 ▊▎
- 17 item_16 16 cat_1 ▊▎
- 18 item_17 17 cat_2 ▊▎
- 19 item_18 18 cat_3 ▊▎
- 20 item_19 19 cat_4 ▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▊▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- esc Close and Apply ctrl+a Apply ctrl+s Select All ctrl+x Deselect All 1 / 20▏^p palette
+ Row # name value category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔ Show/Hide/Reorder Columns ▔▔▔▔▔▎
+ 1 item_0 0 cat_0 ▊ Field dtype Value ▎▊▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▎
+ 2 item_1 1 cat_1 ▊ Row # UInt32 1 ▎▊▊Type to filter columns ▎▎
+ 3 item_2 2 cat_2 ▊ name String item_0 ▎▊▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
+ 4 item_3 3 cat_3 ▊ value Int64 0 ▎▊▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▎
+ 5 item_4 4 cat_4 ▊ score Float64 0.0 ▎▊▊▐X▌ Row #▎▎
+ 6 item_5 5 cat_0 ▊ category String cat_0 ▎▊▊▐X▌ name▎▎
+ 7 item_6 6 cat_1 ▊▎▊▊▐X▌ value▎▎
+ 8 item_7 7 cat_2 ▊▎▊▊▐X▌ score▎▎
+ 9 item_8 8 cat_3 ▊▎▊▊▐X▌ category▎▎
+ 10 item_9 9 cat_4 ▊▎▊▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
+ 11 item_10 10 cat_0 ▊▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ 12 item_11 11 cat_1 ▊▎
+ 13 item_12 12 cat_2 ▊▎
+ 14 item_13 13 cat_3 ▊▎
+ 15 item_14 14 cat_4 ▊▎
+ 16 item_15 15 cat_0 ▊▎
+ 17 item_16 16 cat_1 ▊▎
+ 18 item_17 17 cat_2 ▊▎
+ 19 item_18 18 cat_3 ▊▎
+ 20 item_19 19 cat_4 ▊▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔ Column: Row # ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊ Min 1 ▎
+▊ Q1 6.0 ▎
+▊ Median 10.5 ▎
+▊ Q3 15.0 ▎
+▊ Max 20 ▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ esc Close and Apply ctrl+a Apply ctrl+s Select All ctrl+x Deselect All 1 / 20▏^p palette
diff --git a/tests/__snapshots__/test_snapshots/test_snap_column_selector_open.svg b/tests/__snapshots__/test_snapshots/test_snap_column_selector_open.svg
index cfcf083..7f3aad8 100644
--- a/tests/__snapshots__/test_snapshots/test_snap_column_selector_open.svg
+++ b/tests/__snapshots__/test_snapshots/test_snap_column_selector_open.svg
@@ -42,9 +42,8 @@
.terminal-r8 { fill: #e0e0e0 }
.terminal-r9 { fill: #242f38 }
.terminal-r10 { fill: #8ad4a1 }
-.terminal-r11 { fill: #003054 }
-.terminal-r12 { fill: #ffa62b;font-weight: bold }
-.terminal-r13 { fill: #495259 }
+.terminal-r11 { fill: #ffa62b;font-weight: bold }
+.terminal-r12 { fill: #495259 }
@@ -148,38 +147,38 @@
-
+
- Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▎▊▔ Show/Hide/Reorder Columns ▔▔▔▔▔▎
- 1 item_0 0 0.0 cat_0 ▊ Field dtype ▎▊▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▎
- 2 item_1 1 1.5 cat_1 ▊ Row # UInt32 ▎▊▊Type to filter columns ▎▎
- 3 item_2 2 3.0 cat_2 ▊ name String ▎▊▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
- 4 item_3 3 4.5 cat_3 ▊ value Int64 ▎▊▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▎
- 5 item_4 4 6.0 cat_4 ▊ score Float64 ▎▊▊▐X▌ Row #▎▎
- 6 item_5 5 7.5 cat_0 ▊ category String ▎▊▊▐X▌ name▎▎
- 7 item_6 6 9.0 cat_1 ▊▎▊▊▐X▌ value▎▎
- 8 item_7 7 10.5 cat_2 ▊▎▊▊▐X▌ score▎▎
- 9 item_8 8 12.0 cat_3 ▊▎▊▊▐X▌ category▎▎
- 10 item_9 9 13.5 cat_4 ▊▎▊▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
- 11 item_10 10 15.0 cat_0 ▊▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- 12 item_11 11 16.5 cat_1 ▊▎
- 13 item_12 12 18.0 cat_2 ▊▎
- 14 item_13 13 19.5 cat_3 ▊▎
- 15 item_14 14 21.0 cat_4 ▊▎
- 16 item_15 15 22.5 cat_0 ▊▎
- 17 item_16 16 24.0 cat_1 ▊▎
- 18 item_17 17 25.5 cat_2 ▊▎
- 19 item_18 18 27.0 cat_3 ▊▎
- 20 item_19 19 28.5 cat_4 ▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▊▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- esc Close and Apply ctrl+a Apply ctrl+s Select All ctrl+x Deselect All 1 / 20▏^p palette
+ Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔ Show/Hide/Reorder Columns ▔▔▔▔▔▎
+ 1 item_0 0 0.0 cat_0 ▊ Field dtype Value ▎▊▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▎
+ 2 item_1 1 1.5 cat_1 ▊ Row # UInt32 1 ▎▊▊Type to filter columns ▎▎
+ 3 item_2 2 3.0 cat_2 ▊ name String item_0 ▎▊▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
+ 4 item_3 3 4.5 cat_3 ▊ value Int64 0 ▎▊▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▎
+ 5 item_4 4 6.0 cat_4 ▊ score Float64 0.0 ▎▊▊▐X▌ Row #▎▎
+ 6 item_5 5 7.5 cat_0 ▊ category String cat_0 ▎▊▊▐X▌ name▎▎
+ 7 item_6 6 9.0 cat_1 ▊▎▊▊▐X▌ value▎▎
+ 8 item_7 7 10.5 cat_2 ▊▎▊▊▐X▌ score▎▎
+ 9 item_8 8 12.0 cat_3 ▊▎▊▊▐X▌ category▎▎
+ 10 item_9 9 13.5 cat_4 ▊▎▊▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
+ 11 item_10 10 15.0 cat_0 ▊▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ 12 item_11 11 16.5 cat_1 ▊▎
+ 13 item_12 12 18.0 cat_2 ▊▎
+ 14 item_13 13 19.5 cat_3 ▊▎
+ 15 item_14 14 21.0 cat_4 ▊▎
+ 16 item_15 15 22.5 cat_0 ▊▎
+ 17 item_16 16 24.0 cat_1 ▊▎
+ 18 item_17 17 25.5 cat_2 ▊▎
+ 19 item_18 18 27.0 cat_3 ▊▎
+ 20 item_19 19 28.5 cat_4 ▊▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔ Column: Row # ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊ Min 1 ▎
+▊ Q1 6.0 ▎
+▊ Median 10.5 ▎
+▊ Q3 15.0 ▎
+▊ Max 20 ▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ esc Close and Apply ctrl+a Apply ctrl+s Select All ctrl+x Deselect All 1 / 20▏^p palette
diff --git a/tests/__snapshots__/test_snapshots/test_snap_cursor_moved.svg b/tests/__snapshots__/test_snapshots/test_snap_cursor_moved.svg
index bc4fb13..5f0e508 100644
--- a/tests/__snapshots__/test_snapshots/test_snap_cursor_moved.svg
+++ b/tests/__snapshots__/test_snapshots/test_snap_cursor_moved.svg
@@ -37,8 +37,8 @@
.terminal-r3 { fill: #0178d4 }
.terminal-r4 { fill: #c5c8c6 }
.terminal-r5 { fill: #dde6ed }
-.terminal-r6 { fill: #ffa62b;font-weight: bold }
-.terminal-r7 { fill: #e0e0e0 }
+.terminal-r6 { fill: #e0e0e0 }
+.terminal-r7 { fill: #ffa62b;font-weight: bold }
.terminal-r8 { fill: #495259 }
@@ -143,38 +143,38 @@
-
+
- Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- 1 item_0 0 0.0 cat_0 ▊ Field dtype Value ▎
- 2 item_1 1 1.5 cat_1 ▊ Row # UInt32 4 ▎
- 3 item_2 2 3.0 cat_2 ▊ name String item_3 ▎
- 4 item_3 3 4.5 cat_3 ▊ value Int64 3 ▎
- 5 item_4 4 6.0 cat_4 ▊ score Float64 4.5 ▎
- 6 item_5 5 7.5 cat_0 ▊ category String cat_3 ▎
- 7 item_6 6 9.0 cat_1 ▊▎
- 8 item_7 7 10.5 cat_2 ▊▎
- 9 item_8 8 12.0 cat_3 ▊▎
- 10 item_9 9 13.5 cat_4 ▊▎
- 11 item_10 10 15.0 cat_0 ▊▎
- 12 item_11 11 16.5 cat_1 ▊▎
- 13 item_12 12 18.0 cat_2 ▊▎
- 14 item_13 13 19.5 cat_3 ▊▎
- 15 item_14 14 21.0 cat_4 ▊▎
- 16 item_15 15 22.5 cat_0 ▊▎
- 17 item_16 16 24.0 cat_1 ▊▎
- 18 item_17 17 25.5 cat_2 ▊▎
- 19 item_18 18 27.0 cat_3 ▊▎
- 20 item_19 19 28.5 cat_4 ▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- f Filter rows / Search b Add/Del Bookmark x Compute Expressions... c Columns... r Toggle Row Detail s▏^p palette
+ Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ 1 item_0 0 0.0 cat_0 ▊ Field dtype Value ▎
+ 2 item_1 1 1.5 cat_1 ▊ Row # UInt32 4 ▎
+ 3 item_2 2 3.0 cat_2 ▊ name String item_3 ▎
+ 4 item_3 3 4.5 cat_3 ▊ value Int64 3 ▎
+ 5 item_4 4 6.0 cat_4 ▊ score Float64 4.5 ▎
+ 6 item_5 5 7.5 cat_0 ▊ category String cat_3 ▎
+ 7 item_6 6 9.0 cat_1 ▊▎
+ 8 item_7 7 10.5 cat_2 ▊▎
+ 9 item_8 8 12.0 cat_3 ▊▎
+ 10 item_9 9 13.5 cat_4 ▊▎
+ 11 item_10 10 15.0 cat_0 ▊▎
+ 12 item_11 11 16.5 cat_1 ▊▎
+ 13 item_12 12 18.0 cat_2 ▊▎
+ 14 item_13 13 19.5 cat_3 ▊▎
+ 15 item_14 14 21.0 cat_4 ▊▎
+ 16 item_15 15 22.5 cat_0 ▊▎
+ 17 item_16 16 24.0 cat_1 ▊▎
+ 18 item_17 17 25.5 cat_2 ▊▎
+ 19 item_18 18 27.0 cat_3 ▊▎
+ 20 item_19 19 28.5 cat_4 ▊▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔ Column: value ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊ Min 0 ▎
+▊ Q1 5.0 ▎
+▊ Median 9.5 ▎
+▊ Q3 14.0 ▎
+▊ Max 19 ▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ f Filter rows / Search b Add/Del Bookmark x Compute Expressions... c Columns... r Toggle Row Detail s▏^p palette
diff --git a/tests/__snapshots__/test_snapshots/test_snap_filter_applied.svg b/tests/__snapshots__/test_snapshots/test_snap_filter_applied.svg
index f877bb8..73dd962 100644
--- a/tests/__snapshots__/test_snapshots/test_snap_filter_applied.svg
+++ b/tests/__snapshots__/test_snapshots/test_snap_filter_applied.svg
@@ -37,11 +37,12 @@
.terminal-r3 { fill: #0178d4 }
.terminal-r4 { fill: #c5c8c6 }
.terminal-r5 { fill: #dde6ed }
-.terminal-r6 { fill: #ffffff }
+.terminal-r6 { fill: #000000 }
.terminal-r7 { fill: #e0e0e0 }
-.terminal-r8 { fill: #004578 }
-.terminal-r9 { fill: #ffa62b;font-weight: bold }
-.terminal-r10 { fill: #495259 }
+.terminal-r8 { fill: #ffffff }
+.terminal-r9 { fill: #004578 }
+.terminal-r10 { fill: #ffa62b;font-weight: bold }
+.terminal-r11 { fill: #495259 }
@@ -145,38 +146,38 @@
-
+
- Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- 12 item_11 11 16.5 cat_1 ▊ Field dtype Value ▎
- 13 item_12 12 18.0 cat_2 ▊ Row # UInt32 1 ▎
- 14 item_13 13 19.5 cat_3 ▊ name String item_0 ▎
- 15 item_14 14 21.0 cat_4 ▊ value Int64 0 ▎
- 16 item_15 15 22.5 cat_0 ▊ score Float64 0.0 ▎
- 17 item_16 16 24.0 cat_1 ▊ category String cat_0 ▎
- 18 item_17 17 25.5 cat_2 ▊▎
- 19 item_18 18 27.0 cat_3 ▊▎
- 20 item_19 19 28.5 cat_4 ▊▎
-▊▎
-▊▎
-▊▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-▊▔ Filter dataframe ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▎
-▊▊value > 10▎▎
-▊▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
-▊▎
-▊──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────▎
-▊▎
-▊value > 10▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- ^t Select/copy value from table esc Close 1 / 9 (Filtered from 20)▏^p palette
+ Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ 12 item_11 11 16.5 cat_1 ▊ Field dtype Value ▎
+ 13 item_12 12 18.0 cat_2 ▊ Row # UInt32 1 ▎
+ 14 item_13 13 19.5 cat_3 ▊ name String item_0 ▎
+ 15 item_14 14 21.0 cat_4 ▊ value Int64 0 ▎
+ 16 item_15 15 22.5 cat_0 ▊ score Float64 0.0 ▆▆▎
+ 17 item_16 16 24.0 cat_1 ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ 18 item_17 17 25.5 cat_2 ▊▔ Column: Row # ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ 19 item_18 18 27.0 cat_3 ▊ Min 1 ▎
+ 20 item_19 19 28.5 cat_4 ▊ Q1 6.0 ▎
+▊ Median 10.5 ▎
+▊ Q3 15.0 ▎
+▊ Max 20 ▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔ Filter dataframe ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▎
+▊▊value > 10▎▎
+▊▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
+▊▎
+▊──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────▎
+▊▎
+▊value > 10▎
+▊▎
+▊▎
+▊▎
+▊▎
+▊▎
+▊▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ ^t Select/copy value from table esc Close 1 / 9 (Filtered from 20)▏^p palette
diff --git a/tests/__snapshots__/test_snapshots/test_snap_initial_view.svg b/tests/__snapshots__/test_snapshots/test_snap_initial_view.svg
index bb68455..2db7562 100644
--- a/tests/__snapshots__/test_snapshots/test_snap_initial_view.svg
+++ b/tests/__snapshots__/test_snapshots/test_snap_initial_view.svg
@@ -37,8 +37,8 @@
.terminal-r3 { fill: #0178d4 }
.terminal-r4 { fill: #c5c8c6 }
.terminal-r5 { fill: #dde6ed }
-.terminal-r6 { fill: #ffa62b;font-weight: bold }
-.terminal-r7 { fill: #e0e0e0 }
+.terminal-r6 { fill: #e0e0e0 }
+.terminal-r7 { fill: #ffa62b;font-weight: bold }
.terminal-r8 { fill: #495259 }
@@ -143,38 +143,38 @@
-
+
- Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- 1 item_0 0 0.0 cat_0 ▊ Field dtype Value ▎
- 2 item_1 1 1.5 cat_1 ▊ Row # UInt32 1 ▎
- 3 item_2 2 3.0 cat_2 ▊ name String item_0 ▎
- 4 item_3 3 4.5 cat_3 ▊ value Int64 0 ▎
- 5 item_4 4 6.0 cat_4 ▊ score Float64 0.0 ▎
- 6 item_5 5 7.5 cat_0 ▊ category String cat_0 ▎
- 7 item_6 6 9.0 cat_1 ▊▎
- 8 item_7 7 10.5 cat_2 ▊▎
- 9 item_8 8 12.0 cat_3 ▊▎
- 10 item_9 9 13.5 cat_4 ▊▎
- 11 item_10 10 15.0 cat_0 ▊▎
- 12 item_11 11 16.5 cat_1 ▊▎
- 13 item_12 12 18.0 cat_2 ▊▎
- 14 item_13 13 19.5 cat_3 ▊▎
- 15 item_14 14 21.0 cat_4 ▊▎
- 16 item_15 15 22.5 cat_0 ▊▎
- 17 item_16 16 24.0 cat_1 ▊▎
- 18 item_17 17 25.5 cat_2 ▊▎
- 19 item_18 18 27.0 cat_3 ▊▎
- 20 item_19 19 28.5 cat_4 ▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- f Filter rows / Search b Add/Del Bookmark x Compute Expressions... c Columns... r Toggle Row Detail s▏^p palette
+ Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ 1 item_0 0 0.0 cat_0 ▊ Field dtype Value ▎
+ 2 item_1 1 1.5 cat_1 ▊ Row # UInt32 1 ▎
+ 3 item_2 2 3.0 cat_2 ▊ name String item_0 ▎
+ 4 item_3 3 4.5 cat_3 ▊ value Int64 0 ▎
+ 5 item_4 4 6.0 cat_4 ▊ score Float64 0.0 ▎
+ 6 item_5 5 7.5 cat_0 ▊ category String cat_0 ▎
+ 7 item_6 6 9.0 cat_1 ▊▎
+ 8 item_7 7 10.5 cat_2 ▊▎
+ 9 item_8 8 12.0 cat_3 ▊▎
+ 10 item_9 9 13.5 cat_4 ▊▎
+ 11 item_10 10 15.0 cat_0 ▊▎
+ 12 item_11 11 16.5 cat_1 ▊▎
+ 13 item_12 12 18.0 cat_2 ▊▎
+ 14 item_13 13 19.5 cat_3 ▊▎
+ 15 item_14 14 21.0 cat_4 ▊▎
+ 16 item_15 15 22.5 cat_0 ▊▎
+ 17 item_16 16 24.0 cat_1 ▊▎
+ 18 item_17 17 25.5 cat_2 ▊▎
+ 19 item_18 18 27.0 cat_3 ▊▎
+ 20 item_19 19 28.5 cat_4 ▊▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔ Column: Row # ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊ Min 1 ▎
+▊ Q1 6.0 ▎
+▊ Median 10.5 ▎
+▊ Q3 15.0 ▎
+▊ Max 20 ▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ f Filter rows / Search b Add/Del Bookmark x Compute Expressions... c Columns... r Toggle Row Detail s▏^p palette
diff --git a/tests/__snapshots__/test_snapshots/test_snap_narrow_terminal.svg b/tests/__snapshots__/test_snapshots/test_snap_narrow_terminal.svg
index c6b2a22..d2db595 100644
--- a/tests/__snapshots__/test_snapshots/test_snap_narrow_terminal.svg
+++ b/tests/__snapshots__/test_snapshots/test_snap_narrow_terminal.svg
@@ -37,10 +37,9 @@
.terminal-r3 { fill: #0178d4 }
.terminal-r4 { fill: #c5c8c6 }
.terminal-r5 { fill: #dde6ed }
-.terminal-r6 { fill: #003054 }
-.terminal-r7 { fill: #e0e0e0 }
-.terminal-r8 { fill: #ffa62b;font-weight: bold }
-.terminal-r9 { fill: #495259 }
+.terminal-r6 { fill: #e0e0e0 }
+.terminal-r7 { fill: #ffa62b;font-weight: bold }
+.terminal-r8 { fill: #495259 }
@@ -144,38 +143,38 @@
-
+
- Row # name value score category▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▎
- 1 item_0 0 0.0 cat_0 ▊ Field dtype
- 2 item_1 1 1.5 cat_1 ▊ Row # UInt32
- 3 item_2 2 3.0 cat_2 ▊ name String
- 4 item_3 3 4.5 cat_3 ▊ value Int64
- 5 item_4 4 6.0 cat_4 ▊ score Float64
- 6 item_5 5 7.5 cat_0 ▊ category String
- 7 item_6 6 9.0 cat_1 ▊
- 8 item_7 7 10.5 cat_2 ▊
- 9 item_8 8 12.0 cat_3 ▊
- 10 item_9 9 13.5 cat_4 ▊
- 11 item_10 10 15.0 cat_0 ▊
- 12 item_11 11 16.5 cat_1 ▊
- 13 item_12 12 18.0 cat_2 ▊
- 14 item_13 13 19.5 cat_3 ▊
- 15 item_14 14 21.0 cat_4 ▊
- 16 item_15 15 22.5 cat_0 ▊
- 17 item_16 16 24.0 cat_1 ▊
- 18 item_17 17 25.5 cat_2 ▊
- 19 item_18 18 27.0 cat_3 ▊
- 20 item_19 19 28.5 cat_4 ▊
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▊▎
-▏▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- f Filter rows / Search b Add/Del Bookmark x ▏^p palette
+ Row # name value score ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ 1 item_0 0 0.0 ▊ Field dtype Value ▎
+ 2 item_1 1 1.5 ▊ Row # UInt32 1 ▎
+ 3 item_2 2 3.0 ▊ name String item_0 ▎
+ 4 item_3 3 4.5 ▊ value Int64 0 ▎
+ 5 item_4 4 6.0 ▊ score Float64 0.0 ▎
+ 6 item_5 5 7.5 ▊ category String cat_0 ▎
+ 7 item_6 6 9.0 ▊▎
+ 8 item_7 7 10.5 ▊▎
+ 9 item_8 8 12.0 ▊▎
+ 10 item_9 9 13.5 ▊▎
+ 11 item_10 10 15.0 ▊▎
+ 12 item_11 11 16.5 ▊▎
+ 13 item_12 12 18.0 ▊▎
+ 14 item_13 13 19.5 ▊▎
+ 15 item_14 14 21.0 ▊▎
+ 16 item_15 15 22.5 ▊▎
+ 17 item_16 16 24.0 ▊▎
+ 18 item_17 17 25.5 ▊▎
+ 19 item_18 18 27.0 ▊▎
+ 20 item_19 19 28.5 ▊▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔ Column: Row # ▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊ Min 1 ▎
+▊ Q1 6.0 ▎
+▊ Median 10.5 ▎
+▊ Q3 15.0 ▎
+▊ Max 20 ▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ f Filter rows / Search b Add/Del Bookmark x ▏^p palette
diff --git a/tests/__snapshots__/test_snapshots/test_snap_row_detail.svg b/tests/__snapshots__/test_snapshots/test_snap_row_detail.svg
index 65e86e7..d06f2a3 100644
--- a/tests/__snapshots__/test_snapshots/test_snap_row_detail.svg
+++ b/tests/__snapshots__/test_snapshots/test_snap_row_detail.svg
@@ -37,8 +37,8 @@
.terminal-r3 { fill: #0178d4 }
.terminal-r4 { fill: #c5c8c6 }
.terminal-r5 { fill: #dde6ed }
-.terminal-r6 { fill: #ffa62b;font-weight: bold }
-.terminal-r7 { fill: #e0e0e0 }
+.terminal-r6 { fill: #e0e0e0 }
+.terminal-r7 { fill: #ffa62b;font-weight: bold }
.terminal-r8 { fill: #495259 }
@@ -143,38 +143,38 @@
-
+
- Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- 1 item_0 0 0.0 cat_0 ▊ Field dtype Value ▎
- 2 item_1 1 1.5 cat_1 ▊ Row # UInt32 3 ▎
- 3 item_2 2 3.0 cat_2 ▊ name String item_2 ▎
- 4 item_3 3 4.5 cat_3 ▊ value Int64 2 ▎
- 5 item_4 4 6.0 cat_4 ▊ score Float64 3.0 ▎
- 6 item_5 5 7.5 cat_0 ▊ category String cat_2 ▎
- 7 item_6 6 9.0 cat_1 ▊▎
- 8 item_7 7 10.5 cat_2 ▊▎
- 9 item_8 8 12.0 cat_3 ▊▎
- 10 item_9 9 13.5 cat_4 ▊▎
- 11 item_10 10 15.0 cat_0 ▊▎
- 12 item_11 11 16.5 cat_1 ▊▎
- 13 item_12 12 18.0 cat_2 ▊▎
- 14 item_13 13 19.5 cat_3 ▊▎
- 15 item_14 14 21.0 cat_4 ▊▎
- 16 item_15 15 22.5 cat_0 ▊▎
- 17 item_16 16 24.0 cat_1 ▊▎
- 18 item_17 17 25.5 cat_2 ▊▎
- 19 item_18 18 27.0 cat_3 ▊▎
- 20 item_19 19 28.5 cat_4 ▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- f Filter rows / Search b Add/Del Bookmark x Compute Expressions... c Columns... r Toggle Row Detail s▏^p palette
+ Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ 1 item_0 0 0.0 cat_0 ▊ Field dtype Value ▎
+ 2 item_1 1 1.5 cat_1 ▊ Row # UInt32 3 ▎
+ 3 item_2 2 3.0 cat_2 ▊ name String item_2 ▎
+ 4 item_3 3 4.5 cat_3 ▊ value Int64 2 ▎
+ 5 item_4 4 6.0 cat_4 ▊ score Float64 3.0 ▎
+ 6 item_5 5 7.5 cat_0 ▊ category String cat_2 ▎
+ 7 item_6 6 9.0 cat_1 ▊▎
+ 8 item_7 7 10.5 cat_2 ▊▎
+ 9 item_8 8 12.0 cat_3 ▊▎
+ 10 item_9 9 13.5 cat_4 ▊▎
+ 11 item_10 10 15.0 cat_0 ▊▎
+ 12 item_11 11 16.5 cat_1 ▊▎
+ 13 item_12 12 18.0 cat_2 ▊▎
+ 14 item_13 13 19.5 cat_3 ▊▎
+ 15 item_14 14 21.0 cat_4 ▊▎
+ 16 item_15 15 22.5 cat_0 ▊▎
+ 17 item_16 16 24.0 cat_1 ▊▎
+ 18 item_17 17 25.5 cat_2 ▊▎
+ 19 item_18 18 27.0 cat_3 ▊▎
+ 20 item_19 19 28.5 cat_4 ▊▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔ Column: Row # ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊ Min 1 ▎
+▊ Q1 6.0 ▎
+▊ Median 10.5 ▎
+▊ Q3 15.0 ▎
+▊ Max 20 ▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ f Filter rows / Search b Add/Del Bookmark x Compute Expressions... c Columns... r Toggle Row Detail s▏^p palette
diff --git a/tests/__snapshots__/test_snapshots/test_snap_search_results.svg b/tests/__snapshots__/test_snapshots/test_snap_search_results.svg
index 46098c3..b5c2068 100644
--- a/tests/__snapshots__/test_snapshots/test_snap_search_results.svg
+++ b/tests/__snapshots__/test_snapshots/test_snap_search_results.svg
@@ -37,8 +37,8 @@
.terminal-r3 { fill: #0178d4 }
.terminal-r4 { fill: #c5c8c6 }
.terminal-r5 { fill: #dde6ed }
-.terminal-r6 { fill: #ffa62b;font-weight: bold }
-.terminal-r7 { fill: #e0e0e0 }
+.terminal-r6 { fill: #e0e0e0 }
+.terminal-r7 { fill: #ffa62b;font-weight: bold }
.terminal-r8 { fill: #495259 }
@@ -143,38 +143,38 @@
-
+
- Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- 1 item_0 0 0.0 cat_0 ▊ Field dtype Value ▎
- 2 item_1 1 1.5 cat_1 ▊ Row # UInt32 7 ▎
- 3 item_2 2 3.0 cat_2 ▊ name String item_6 ▎
- 4 item_3 3 4.5 cat_3 ▊ value Int64 6 ▎
- 5 item_4 4 6.0 cat_4 ▊ score Float64 9.0 ▎
- 6 item_5 5 7.5 cat_0 ▊ category String cat_1 ▎
- 7 item_6 6 9.0 cat_1 ▊▎
- 8 item_7 7 10.5 cat_2 ▊▎
- 9 item_8 8 12.0 cat_3 ▊▎
- 10 item_9 9 13.5 cat_4 ▊▎
- 11 item_10 10 15.0 cat_0 ▊▎
- 12 item_11 11 16.5 cat_1 ▊▎
- 13 item_12 12 18.0 cat_2 ▊▎
- 14 item_13 13 19.5 cat_3 ▊▎
- 15 item_14 14 21.0 cat_4 ▊▎
- 16 item_15 15 22.5 cat_0 ▊▎
- 17 item_16 16 24.0 cat_1 ▊▎
- 18 item_17 17 25.5 cat_2 ▊▎
- 19 item_18 18 27.0 cat_3 ▊▎
- 20 item_19 19 28.5 cat_4 ▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- f Filter rows / Search n Next b Add/Del Bookmark x Compute Expressions... c Columns... r Toggle Row D▏^p palette
+ Row # name value score category ▊▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ 1 item_0 0 0.0 cat_0 ▊ Field dtype Value ▎
+ 2 item_1 1 1.5 cat_1 ▊ Row # UInt32 7 ▎
+ 3 item_2 2 3.0 cat_2 ▊ name String item_6 ▎
+ 4 item_3 3 4.5 cat_3 ▊ value Int64 6 ▎
+ 5 item_4 4 6.0 cat_4 ▊ score Float64 9.0 ▎
+ 6 item_5 5 7.5 cat_0 ▊ category String cat_1 ▎
+ 7 item_6 6 9.0 cat_1 ▊▎
+ 8 item_7 7 10.5 cat_2 ▊▎
+ 9 item_8 8 12.0 cat_3 ▊▎
+ 10 item_9 9 13.5 cat_4 ▊▎
+ 11 item_10 10 15.0 cat_0 ▊▎
+ 12 item_11 11 16.5 cat_1 ▊▎
+ 13 item_12 12 18.0 cat_2 ▊▎
+ 14 item_13 13 19.5 cat_3 ▊▎
+ 15 item_14 14 21.0 cat_4 ▊▎
+ 16 item_15 15 22.5 cat_0 ▊▎
+ 17 item_16 16 24.0 cat_1 ▊▎
+ 18 item_17 17 25.5 cat_2 ▊▎
+ 19 item_18 18 27.0 cat_3 ▊▎
+ 20 item_19 19 28.5 cat_4 ▊▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔ Column: Row # ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊ Min 1 ▎
+▊ Q1 6.0 ▎
+▊ Median 10.5 ▎
+▊ Q3 15.0 ▎
+▊ Max 20 ▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ f Filter rows / Search n Next b Add/Del Bookmark x Compute Expressions... c Columns... r Toggle Row D▏^p palette
diff --git a/tests/test_column_metadata.py b/tests/test_column_metadata.py
new file mode 100644
index 0000000..23b5a78
--- /dev/null
+++ b/tests/test_column_metadata.py
@@ -0,0 +1,352 @@
+import datetime
+
+import polars as pl
+from textual.pilot import Pilot
+
+from dt_browser.browser import DtBrowserApp
+from dt_browser.column_metadata import ColumnMetadata, compute_column_stats
+from dt_browser.custom_table import CustomTable
+
+
+# --- Unit tests for stats computation ---
+
+
+def test_numeric_stats():
+ series = pl.Series("val", [1.0, 2.0, 3.0, 4.0, 5.0])
+ stats = compute_column_stats(series)
+ labels = [s[0] for s in stats]
+ assert labels == ["Min", "Q1", "Median", "Q3", "Max"]
+ assert stats[0][1] == "1.0"
+ assert stats[4][1] == "5.0"
+
+
+def test_numeric_stats_integers():
+ series = pl.Series("val", [10, 20, 30, 40, 50])
+ stats = compute_column_stats(series)
+ labels = [s[0] for s in stats]
+ assert labels == ["Min", "Q1", "Median", "Q3", "Max"]
+ assert stats[0][1] == "10"
+ assert stats[4][1] == "50"
+
+
+def test_numeric_stats_empty():
+ series = pl.Series("val", [], dtype=pl.Float64)
+ stats = compute_column_stats(series)
+ assert stats == [("", "No data")]
+
+
+def test_categorical_stats():
+ series = pl.Series("cat", ["a", "b", "a", "c", "b", "a"]).cast(pl.Categorical)
+ stats = compute_column_stats(series)
+ assert stats[0] == ("Unique values", "3")
+ # Top entries should be sorted by count descending
+ assert stats[1] == (" a", "3")
+ assert stats[2] == (" b", "2")
+ assert stats[3] == (" c", "1")
+
+
+def test_categorical_stats_top_10():
+ values = [f"cat_{i}" for i in range(20)] * 2
+ series = pl.Series("cat", values).cast(pl.Categorical)
+ stats = compute_column_stats(series)
+ assert stats[0][0] == "Unique values"
+ # Should have at most 10 variant rows + 1 header
+ assert len(stats) <= 11
+
+
+def test_temporal_stats():
+ series = pl.Series("ts", [datetime.datetime(2024, 1, 1), datetime.datetime(2024, 6, 15), datetime.datetime(2024, 12, 31)])
+ stats = compute_column_stats(series)
+ labels = [s[0] for s in stats]
+ assert labels == ["Min", "Max"]
+ assert "2024-01-01" in stats[0][1]
+ assert "2024-12-31" in stats[1][1]
+
+
+def test_temporal_stats_empty():
+ series = pl.Series("ts", [], dtype=pl.Datetime)
+ stats = compute_column_stats(series)
+ assert stats == [("", "No data")]
+
+
+def test_boolean_stats():
+ series = pl.Series("flag", [True, True, False, True, False])
+ stats = compute_column_stats(series)
+ labels = [s[0] for s in stats]
+ assert "True" in labels
+ assert "False" in labels
+ true_val = next(s[1] for s in stats if s[0] == "True")
+ false_val = next(s[1] for s in stats if s[0] == "False")
+ assert true_val == "3"
+ assert false_val == "2"
+
+
+def test_boolean_stats_with_nulls():
+ series = pl.Series("flag", [True, None, False, None])
+ stats = compute_column_stats(series)
+ labels = [s[0] for s in stats]
+ assert "Null" in labels
+ null_val = next(s[1] for s in stats if s[0] == "Null")
+ assert null_val == "2"
+
+
+def test_numeric_stats_with_nulls():
+ series = pl.Series("val", [1.0, None, 3.0, None, 5.0])
+ stats = compute_column_stats(series)
+ labels = [s[0] for s in stats]
+ assert "Null" in labels
+ null_val = next(s[1] for s in stats if s[0] == "Null")
+ assert null_val == "2"
+
+
+def test_numeric_stats_with_nans():
+ series = pl.Series("val", [1.0, float("nan"), 3.0, float("nan"), 5.0])
+ stats = compute_column_stats(series)
+ labels = [s[0] for s in stats]
+ assert "NaN" in labels
+ nan_val = next(s[1] for s in stats if s[0] == "NaN")
+ assert nan_val == "2"
+
+
+def test_numeric_integer_no_nan_row():
+ series = pl.Series("val", [1, 2, 3])
+ stats = compute_column_stats(series)
+ labels = [s[0] for s in stats]
+ assert "NaN" not in labels
+
+
+def test_temporal_stats_with_nulls():
+ series = pl.Series("ts", [datetime.datetime(2024, 1, 1), None, datetime.datetime(2024, 12, 31)])
+ stats = compute_column_stats(series)
+ labels = [s[0] for s in stats]
+ assert "Null" in labels
+ null_val = next(s[1] for s in stats if s[0] == "Null")
+ assert null_val == "1"
+
+
+def test_categorical_stats_with_nulls():
+ series = pl.Series("cat", ["a", None, "b", None]).cast(pl.Categorical)
+ stats = compute_column_stats(series)
+ labels = [s[0] for s in stats]
+ assert "Null" in labels
+ null_val = next(s[1] for s in stats if s[0] == "Null")
+ assert null_val == "2"
+
+
+def test_unsupported_dtype_returns_empty():
+ series = pl.Series("text", ["hello", "world"])
+ stats = compute_column_stats(series)
+ assert stats == []
+
+
+# --- Integration tests ---
+
+
+def _make_mixed_app(num_rows: int = 30) -> DtBrowserApp:
+ df = pl.DataFrame(
+ {
+ "id": list(range(num_rows)),
+ "score": [round(i * 1.5, 1) for i in range(num_rows)],
+ "category": pl.Series([f"cat_{i % 5}" for i in range(num_rows)]).cast(pl.Categorical),
+ "active": [i % 3 == 0 for i in range(num_rows)],
+ "name": [f"item_{i}" for i in range(num_rows)],
+ }
+ )
+ return DtBrowserApp("test", df)
+
+
+async def test_column_metadata_visible_on_start():
+ """Column metadata panel should be visible on app start and within viewport."""
+ app = _make_mixed_app()
+ async with app.run_test(size=(120, 30)) as pilot:
+ await pilot.pause()
+ metadata = app.query_one(ColumnMetadata)
+ assert metadata is not None
+ # Should show stats for the first column (Row # which is integer)
+ assert metadata.column_info is not None
+ # Must be within the visible area
+ assert metadata.region.y + metadata.region.height <= 30, (
+ f"ColumnMetadata at y={metadata.region.y} h={metadata.region.height} is off-screen"
+ )
+ assert metadata.region.height > 0
+
+
+async def test_column_metadata_displays_numeric_stats():
+ """Verify numeric column stats are actually rendered in the widget."""
+ app = _make_mixed_app()
+ async with app.run_test(size=(120, 30)) as pilot:
+ await pilot.pause()
+ metadata = app.query_one(ColumnMetadata)
+ # First column is Row # (numeric) — stats should be cached
+ col_name = metadata.column_info[0]
+ assert col_name in metadata._cache
+ stats = metadata._cache[col_name]
+ labels = [s[0] for s in stats]
+ assert "Min" in labels
+ assert "Median" in labels
+ assert "Max" in labels
+ # Border title should show column name
+ assert col_name in metadata.border_title
+
+
+async def test_column_metadata_displays_categorical_stats():
+ """Verify categorical column stats are rendered with value counts."""
+ app = _make_mixed_app()
+ async with app.run_test(size=(120, 30)) as pilot:
+ await pilot.pause()
+ metadata = app.query_one(ColumnMetadata)
+ # Navigate to 'category' column (Row#, id, score, category)
+ for _ in range(3):
+ await pilot.press("right")
+ await pilot.pause()
+ assert metadata.column_info[0] == "category"
+ stats = metadata._cache["category"]
+ assert stats[0] == ("Unique values", "5")
+ # Should have value count entries
+ assert len(stats) == 6 # 1 header + 5 categories
+ assert "category" in metadata.border_title
+
+
+async def test_column_metadata_displays_boolean_stats():
+ """Verify boolean column stats show True/False counts."""
+ app = _make_mixed_app()
+ async with app.run_test(size=(120, 30)) as pilot:
+ await pilot.pause()
+ metadata = app.query_one(ColumnMetadata)
+ # Navigate to 'active' column (Row#, id, score, category, active)
+ for _ in range(4):
+ await pilot.press("right")
+ await pilot.pause()
+ assert metadata.column_info[0] == "active"
+ stats = metadata._cache["active"]
+ labels = [s[0] for s in stats]
+ assert "True" in labels
+ assert "False" in labels
+
+
+async def test_column_metadata_updates_on_cursor_move():
+ """Moving cursor to a different column updates column_info."""
+ app = _make_mixed_app()
+ async with app.run_test(size=(120, 30)) as pilot:
+ await pilot.pause()
+ metadata = app.query_one(ColumnMetadata)
+ initial_info = metadata.column_info
+
+ await pilot.press("right")
+ await pilot.pause()
+
+ assert metadata.column_info != initial_info
+
+
+async def test_column_metadata_cache_works():
+ """Moving to a column, away, and back should use cached stats."""
+ app = _make_mixed_app()
+ async with app.run_test(size=(120, 30)) as pilot:
+ await pilot.pause()
+ metadata = app.query_one(ColumnMetadata)
+
+ # Move to column 1
+ await pilot.press("right")
+ await pilot.pause()
+ col_name_1 = metadata.column_info[0]
+ assert col_name_1 in metadata._cache
+
+ # Move to column 2
+ await pilot.press("right")
+ await pilot.pause()
+
+ # Move back to column 1
+ await pilot.press("left")
+ await pilot.pause()
+ # Cache should still have the entry
+ assert col_name_1 in metadata._cache
+
+
+async def test_column_metadata_cache_invalidated_on_filter():
+ """Applying a filter should clear the column metadata cache."""
+ app = _make_mixed_app()
+ async with app.run_test(size=(120, 30)) as pilot:
+ await pilot.pause()
+ metadata = app.query_one(ColumnMetadata)
+
+ # Move to trigger cache population
+ await pilot.press("right")
+ await pilot.pause()
+ assert len(metadata._cache) > 0
+
+ # Apply a filter
+ await pilot.press("f")
+ await pilot.pause()
+ await pilot.press(*list("id > 10"))
+ await pilot.press("enter")
+ await pilot.app.workers.wait_for_complete()
+ await pilot.pause()
+
+ assert len(metadata._cache) == 0
+
+
+async def test_column_metadata_hidden_with_row_detail():
+ """Toggling row detail off should also hide column metadata."""
+ app = _make_mixed_app()
+ async with app.run_test(size=(120, 30)) as pilot:
+ await pilot.pause()
+ assert len(app.query(ColumnMetadata)) == 1
+
+ await pilot.press("r")
+ await pilot.pause()
+
+ # Both should be gone (inside DetailPanel)
+ assert len(app.query(ColumnMetadata)) == 0
+
+
+# --- Snapshot tests ---
+
+
+def test_snap_column_metadata_numeric(snap_compare):
+ """Snapshot showing numeric column stats (id column)."""
+ assert snap_compare(
+ _make_mixed_app(),
+ press=["right"],
+ terminal_size=(120, 30),
+ )
+
+
+def test_snap_column_metadata_categorical(snap_compare):
+ """Snapshot showing categorical column stats."""
+
+ async def run_before(pilot: Pilot) -> None:
+ # Navigate to the 'category' column (index 3: Row#, id, score, category)
+ for _ in range(3):
+ await pilot.press("right")
+ await pilot.pause()
+
+ assert snap_compare(
+ _make_mixed_app(),
+ run_before=run_before,
+ terminal_size=(120, 30),
+ )
+
+
+def test_snap_column_metadata_boolean(snap_compare):
+ """Snapshot showing boolean column stats."""
+
+ async def run_before(pilot: Pilot) -> None:
+ # Navigate to the 'active' column (index 4: Row#, id, score, category, active)
+ for _ in range(4):
+ await pilot.press("right")
+ await pilot.pause()
+
+ assert snap_compare(
+ _make_mixed_app(),
+ run_before=run_before,
+ terminal_size=(120, 30),
+ )
+
+
+def test_snap_detail_panel_layout(snap_compare):
+ """Snapshot showing full detail panel with both row detail and column metadata."""
+ assert snap_compare(
+ _make_mixed_app(),
+ press=["down", "down"],
+ terminal_size=(120, 30),
+ )