Skip to content

Instantly share code, notes, and snippets.

@digitalsignalperson
Last active March 24, 2026 07:26
Show Gist options
  • Select an option

  • Save digitalsignalperson/1c65f2e8e580e07c53702609662d06a5 to your computer and use it in GitHub Desktop.

Select an option

Save digitalsignalperson/1c65f2e8e580e07c53702609662d06a5 to your computer and use it in GitHub Desktop.
vibe-coded imgui table demo: 5k rows x 5k cols @ 120fps; custom drawlist rendering & IQR line plot conditional formatting
"""
Custom column-major DataFrame table renderer using imgui_bundle draw lists.
Key design constraints:
- All expensive work (stats, widths, col-to-array) is done ONCE at table creation.
- Per-frame work is O(visible_cells) only.
- Column settings and metadata are polars DataFrames.
- Settings UI shows one column at a time (search to select).
- Filtering uses polars/numpy expressions, not Python list comprehensions.
- Data generation uses numpy for bulk random data.
"""
from __future__ import annotations
import math
import string
import time
from dataclasses import dataclass, field
import numpy as np
import polars as pl
from imgui_bundle import imgui, immapp
from imgui_bundle import ImVec2, ImVec4
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
def col32(r: int, g: int, b: int, a: int = 255) -> int:
return imgui.IM_COL32(r, g, b, a)
CF_GREEN = col32( 60, 180, 60, 200)
CF_YELLOW = col32(200, 180, 30, 200)
CF_ORANGE = col32(220, 120, 20, 200)
CF_RED = col32(180, 40, 40, 200)
CF_NONE = col32( 40, 40, 40, 0)
TEXT_COLOR = col32(230, 230, 230)
HEADER_BG = col32( 50, 50, 70, 255)
HEADER_TEXT = col32(200, 210, 255)
CELL_BG_EVEN = col32( 35, 35, 40, 255)
CELL_BG_ODD = col32( 45, 45, 52, 255)
BORDER_COLOR = col32( 80, 80, 100, 160)
IQR_LINE_COL = col32(255, 200, 100, 128) # ~0.5 alpha amber
SEL_FILL_COL = col32(100, 180, 255, 55)
SEL_EDGE_COL = col32(100, 180, 255, 180)
ROW_HEIGHT = 22
LINE_HEIGHT = ROW_HEIGHT # height of one text line (= base row height)
HEADER_HEIGHT = 28
INDEX_WRAP_PX = 200 # index column max width
STR_COL_MAX_PX = 200
NUM_COL_PX = 90
PADDING = 6
MAX_COL_WIDTH = 256
# ---------------------------------------------------------------------------
# Test data generation parameters (defaults; runtime state overrides these)
# ---------------------------------------------------------------------------
GEN_INIT_ROWS = 100
GEN_INIT_COLS = 20
GEN_DEFAULT_ROWS = 5000
GEN_DEFAULT_COLS = 5000
GEN_DEFAULT_SEED = 42
GEN_DEFAULT_INT_MIN = -1_000_000
GEN_DEFAULT_INT_MAX = 1_000_000
# Float center: log10(|center|) uniform in [CENTER_LOG_LO, CENTER_LOG_HI], sign random
GEN_DEFAULT_FLOAT_CENTER_LOG_LO = -3.0
GEN_DEFAULT_FLOAT_CENTER_LOG_HI = 3.0
GEN_DEFAULT_FLOAT_SCALE_EXP_LO = -3.0 # log10 range for per-column std-dev
GEN_DEFAULT_FLOAT_SCALE_EXP_HI = 4.0
GEN_DEFAULT_STR_MIN_LEN = 1
GEN_DEFAULT_STR_MAX_LEN = 20
GEN_DEFAULT_INDEX_MIN_LEN = 5
GEN_DEFAULT_INDEX_MAX_LEN = 30
GEN_STR_POOL_SIZE = 100 # number of prototype strings in pool
NUM_FILTER_OPS = ["None", "<", "<=", ">", ">=", "=="]
# ---------------------------------------------------------------------------
# SI / sig-fig float formatting
# ---------------------------------------------------------------------------
_SI_SUFFIXES = [
(1e12, "T"), (1e9, "G"), (1e6, "M"), (1e3, "k"),
(1e0, ""), (1e-3,"m"), (1e-6,"µ"), (1e-9,"n"), (1e-12,"p"),
]
def _format_si(value: float, sf: int) -> str:
if math.isnan(value) or math.isinf(value):
return str(value)
if value == 0:
return "0"
abs_val = abs(value)
sign = "-" if value < 0 else ""
for threshold, suffix in _SI_SUFFIXES:
if abs_val >= threshold * 0.9999:
scaled = abs_val / threshold
mag = math.floor(math.log10(scaled)) if scaled > 0 else 0
d = max(0, sf - 1 - mag)
return f"{sign}{scaled:.{d}f}{suffix}"
mag = math.floor(math.log10(abs_val))
d = max(0, sf - 1 - mag)
return f"{value:.{d}f}"
def _format_fixed(value: float, decimals: int) -> str:
if math.isnan(value) or math.isinf(value):
return str(value)
return f"{value:.{decimals}f}"
# ---------------------------------------------------------------------------
# Test dataframe generation
# ---------------------------------------------------------------------------
_WORDS = (
"alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi "
"omicron pi rho sigma tau upsilon phi chi psi omega"
).split()
_STR_ALPHABET = np.frombuffer((string.ascii_lowercase + " ").encode(), dtype=np.uint8)
def _make_str_pool(min_len: int, max_len: int, np_rng: np.random.Generator) -> pl.Series:
lengths = np_rng.integers(min_len, max_len + 1, size=GEN_STR_POOL_SIZE, dtype=np.int32)
total = int(lengths.sum())
idx = np_rng.integers(0, len(_STR_ALPHABET), size=total, dtype=np.uint8)
flat = _STR_ALPHABET[idx]
raw = bytes(flat)
offsets = np.empty(GEN_STR_POOL_SIZE + 1, dtype=np.int32)
offsets[0] = 0
np.cumsum(lengths, out=offsets[1:])
pool = [raw[offsets[i]:offsets[i+1]].decode("ascii").strip() or "x"
for i in range(GEN_STR_POOL_SIZE)]
return pl.Series(pool, dtype=pl.Utf8)
def _make_str_series(name: str, n_rows: int, min_len: int, max_len: int,
np_rng: np.random.Generator) -> pl.Series:
pool = _make_str_pool(min_len, max_len, np_rng)
indices = np_rng.integers(0, GEN_STR_POOL_SIZE, size=n_rows, dtype=np.int32)
return pool.gather(indices).alias(name)
def _make_row_labels(n_rows: int, np_rng: np.random.Generator,
min_len: int = GEN_DEFAULT_INDEX_MIN_LEN,
max_len: int = GEN_DEFAULT_INDEX_MAX_LEN) -> pl.Series:
return _make_str_series("_index", n_rows, min_len, max_len, np_rng)
@dataclass
class GenParams:
rows: int = GEN_DEFAULT_ROWS
cols: int = GEN_DEFAULT_COLS
seed: int = GEN_DEFAULT_SEED
gen_ints: bool = True
gen_floats: bool = True
gen_strs: bool = True
int_min: int = GEN_DEFAULT_INT_MIN
int_max: int = GEN_DEFAULT_INT_MAX
float_center_log_lo: float = GEN_DEFAULT_FLOAT_CENTER_LOG_LO
float_center_log_hi: float = GEN_DEFAULT_FLOAT_CENTER_LOG_HI
float_scale_exp_lo: float = GEN_DEFAULT_FLOAT_SCALE_EXP_LO
float_scale_exp_hi: float = GEN_DEFAULT_FLOAT_SCALE_EXP_HI
str_min_len: int = GEN_DEFAULT_STR_MIN_LEN
str_max_len: int = GEN_DEFAULT_STR_MAX_LEN
index_min_len: int = GEN_DEFAULT_INDEX_MIN_LEN
index_max_len: int = GEN_DEFAULT_INDEX_MAX_LEN
def make_test_df(p: GenParams) -> tuple[pl.DataFrame, pl.Series]:
np_rng = np.random.default_rng(p.seed)
n_rows = p.rows
n_cols = p.cols
type_flags = [t for t in [p.gen_ints, p.gen_floats, p.gen_strs] if t]
n_types = len(type_flags)
if n_types == 0:
return pl.DataFrame(), _make_row_labels(n_rows, np_rng)
# distribute cols evenly across enabled types
types: list[str] = []
if p.gen_ints: types.append("int")
if p.gen_floats: types.append("float")
if p.gen_strs: types.append("str")
counts = {t: n_cols // n_types for t in types}
for i, t in enumerate(types):
if i < n_cols % n_types:
counts[t] += 1
int_series: list[pl.Series] = []
float_series: list[pl.Series] = []
str_series: list[pl.Series] = []
for i in range(counts.get("int", 0)):
lo, hi = (p.int_min, p.int_max) if p.int_min < p.int_max else (p.int_max, p.int_min)
arr = np_rng.integers(lo, hi, size=n_rows, dtype=np.int64)
int_series.append(pl.Series(f"int_{i}", arr, dtype=pl.Int64))
for i in range(counts.get("float", 0)):
log_mag = np_rng.uniform(p.float_center_log_lo, p.float_center_log_hi)
sign = np_rng.choice([-1.0, 1.0])
center = sign * (10.0 ** log_mag)
scale = float(10 ** np_rng.uniform(p.float_scale_exp_lo, p.float_scale_exp_hi))
arr = np_rng.normal(center, scale, size=n_rows)
float_series.append(pl.Series(f"float_{i}", arr, dtype=pl.Float64))
for i in range(counts.get("str", 0)):
lo, hi = (p.str_min_len, p.str_max_len) if p.str_min_len <= p.str_max_len else (p.str_max_len, p.str_min_len)
str_series.append(_make_str_series(f"str_{i}", n_rows, lo, hi, np_rng))
series_list: list[pl.Series] = []
iters = [iter(s) for s in [int_series, float_series, str_series] if s]
while True:
advanced = False
for it in iters:
try:
series_list.append(next(it))
advanced = True
except StopIteration:
pass
if not advanced:
break
df = pl.DataFrame(series_list) if series_list else pl.DataFrame()
row_labels = _make_row_labels(n_rows, np_rng, p.index_min_len, p.index_max_len)
return df, row_labels
# ---------------------------------------------------------------------------
# Precomputed metadata
# ---------------------------------------------------------------------------
_NUM_DTYPES = {
pl.Int8, pl.Int16, pl.Int32, pl.Int64,
pl.UInt8, pl.UInt16, pl.UInt32, pl.UInt64,
pl.Float32, pl.Float64,
}
_FLT_DTYPES = {pl.Float32, pl.Float64}
_STR_DTYPES = {pl.Utf8, pl.String}
def _build_col_meta(df: pl.DataFrame) -> pl.DataFrame:
col_names = df.columns
dtypes = df.dtypes
n = len(col_names)
dtype_names = [str(d) for d in dtypes]
is_numeric = [d in _NUM_DTYPES for d in dtypes]
is_float = [d in _FLT_DTYPES for d in dtypes]
is_str = [d in _STR_DTYPES for d in dtypes]
widths = np.full(n, NUM_COL_PX, dtype=np.int32)
medians = np.zeros(n, dtype=np.float64)
iqr1s = np.zeros(n, dtype=np.float64)
iqr2s = np.zeros(n, dtype=np.float64)
iqr3s = np.zeros(n, dtype=np.float64)
stat_min = np.full(n, float("nan"))
stat_max = np.full(n, float("nan"))
stat_mean = np.full(n, float("nan"))
stat_std = np.full(n, float("nan"))
n_unique = np.full(n, -1, dtype=np.int64)
most_common = [""] * n
for c, col_name in enumerate(col_names):
s = df[col_name]
if is_numeric[c]:
flt = s.drop_nulls().cast(pl.Float64)
if len(flt) > 0:
q25 = flt.quantile(0.25) or 0.0
q50 = flt.quantile(0.50) or 0.0
q75 = flt.quantile(0.75) or 0.0
iqr = q75 - q25
medians[c] = q50
iqr1s[c] = iqr
iqr2s[c] = 2.0 * iqr
iqr3s[c] = 3.0 * iqr
stat_min[c] = float(flt.min() or 0.0)
stat_max[c] = float(flt.max() or 0.0)
stat_mean[c] = float(flt.mean() or 0.0)
stat_std[c] = float(flt.std() or 0.0)
elif is_str[c]:
sample = s.drop_nulls()
if len(sample) > 500:
sample = sample.sample(500, seed=0)
max_len = int(sample.str.len_chars().max() or 10) if len(sample) > 0 else 10
w = min(max_len * 7 + PADDING * 2, STR_COL_MAX_PX)
widths[c] = max(w, 60)
n_unique[c] = s.n_unique()
vc = s.drop_nulls().value_counts(sort=True)
most_common[c] = str(vc[0, 0]) if len(vc) > 0 else ""
return pl.DataFrame({
"col_name": col_names,
"dtype_name": dtype_names,
"is_numeric": is_numeric,
"is_float": is_float,
"is_str": is_str,
"width_px": widths.tolist(),
"median": medians.tolist(),
"iqr1": iqr1s.tolist(),
"iqr2": iqr2s.tolist(),
"iqr3": iqr3s.tolist(),
"stat_min": stat_min.tolist(),
"stat_max": stat_max.tolist(),
"stat_mean": stat_mean.tolist(),
"stat_std": stat_std.tolist(),
"n_unique": n_unique.tolist(),
"most_common": most_common,
})
def _default_col_settings(n: int) -> pl.DataFrame:
return pl.DataFrame({
"cf_enabled": pl.Series(np.ones(n, dtype=bool)),
"use_fixed": pl.Series(np.ones(n, dtype=bool)),
"fixed_decimals": pl.Series(np.full(n, 2, dtype=np.int32)),
"use_sigfigs": pl.Series(np.zeros(n, dtype=bool)),
"sigfigs": pl.Series(np.full(n, 3, dtype=np.int32)),
"str_filter": pl.Series([""] * n, dtype=pl.Utf8),
"num_filter_op": pl.Series(np.zeros(n, dtype=np.int32)),
"num_filter_val": pl.Series(np.zeros(n, dtype=np.float64)),
"wrap_text": pl.Series(np.ones(n, dtype=bool)),
})
@dataclass
class TableCache:
df: pl.DataFrame
row_labels: pl.Series
col_meta: pl.DataFrame
col_settings: pl.DataFrame
col_arrays: dict[int, np.ndarray] = field(default_factory=dict)
col_arrays_range: tuple[int, int] = (0, 0)
visible_rows: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.int32))
visible_cols: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.int32))
index_col_width: int = 60
col_x_offsets: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.int32))
vis_col_offsets: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.int32))
# numpy views of col_meta
_meta_is_float: np.ndarray = field(default_factory=lambda: np.array([], dtype=bool))
_meta_is_numeric: np.ndarray = field(default_factory=lambda: np.array([], dtype=bool))
_meta_is_str: np.ndarray = field(default_factory=lambda: np.array([], dtype=bool))
_meta_median: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.float64))
_meta_iqr1: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.float64))
_meta_iqr2: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.float64))
_meta_iqr3: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.float64))
_meta_width: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.int32))
# numpy views of col_settings
_set_cf_enabled: np.ndarray = field(default_factory=lambda: np.array([], dtype=bool))
_set_use_fixed: np.ndarray = field(default_factory=lambda: np.array([], dtype=bool))
_set_fixed_decimals: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.int32))
_set_use_sigfigs: np.ndarray = field(default_factory=lambda: np.array([], dtype=bool))
_set_sigfigs: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.int32))
_set_wrap_text: np.ndarray = field(default_factory=lambda: np.array([], dtype=bool))
def refresh_numpy_views(self) -> None:
m = self.col_meta
s = self.col_settings
self._meta_is_float = m["is_float"].to_numpy()
self._meta_is_numeric = m["is_numeric"].to_numpy()
self._meta_is_str = m["is_str"].to_numpy()
self._meta_median = m["median"].to_numpy()
self._meta_iqr1 = m["iqr1"].to_numpy()
self._meta_iqr2 = m["iqr2"].to_numpy()
self._meta_iqr3 = m["iqr3"].to_numpy()
self._meta_width = m["width_px"].to_numpy()
self._set_cf_enabled = s["cf_enabled"].to_numpy()
self._set_use_fixed = s["use_fixed"].to_numpy()
self._set_fixed_decimals = s["fixed_decimals"].to_numpy()
self._set_use_sigfigs = s["use_sigfigs"].to_numpy()
self._set_sigfigs = s["sigfigs"].to_numpy()
self._set_wrap_text = s["wrap_text"].to_numpy()
def ensure_col_arrays(self, first_c: int, last_c: int) -> None:
cf, cl = self.col_arrays_range
if first_c >= cf and last_c <= cl:
return
n_cols = len(self.df.columns)
new_first = max(0, first_c - 10)
new_last = min(n_cols, last_c + 10)
cols = self.df.columns
for c in range(new_first, new_last):
if c not in self.col_arrays:
self.col_arrays[c] = self.df[cols[c]].to_numpy(allow_copy=True)
for c in list(self.col_arrays.keys()):
if c < new_first or c >= new_last:
del self.col_arrays[c]
self.col_arrays_range = (new_first, new_last)
def update_setting(self, col_idx: int, field_name: str, value: object) -> None:
s = self.col_settings
col_series = s[field_name].to_numpy(allow_copy=True)
col_series[col_idx] = value # type: ignore[index]
self.col_settings = s.with_columns(
pl.Series(field_name, col_series, dtype=s[field_name].dtype)
)
self.refresh_numpy_views()
def _build_cache(df: pl.DataFrame, row_labels: pl.Series) -> TableCache:
col_meta = _build_col_meta(df)
col_settings = _default_col_settings(len(df.columns))
max_lbl_len = int(row_labels.str.len_chars().max() or 10)
idx_w = max(min(max_lbl_len * 7 + PADDING * 2, INDEX_WRAP_PX), 40)
widths_arr = col_meta["width_px"].to_numpy().astype(np.int32)
col_x_offsets = np.empty(len(widths_arr) + 1, dtype=np.int32)
col_x_offsets[0] = idx_w
col_x_offsets[1:] = idx_w + np.cumsum(widths_arr + 1)
cache = TableCache(
df=df,
row_labels=row_labels,
col_meta=col_meta,
col_settings=col_settings,
visible_rows=np.arange(len(df), dtype=np.int32),
visible_cols=np.arange(len(df.columns), dtype=np.int32),
index_col_width=idx_w,
col_x_offsets=col_x_offsets,
vis_col_offsets=col_x_offsets.copy(),
)
cache.refresh_numpy_views()
return cache
# ---------------------------------------------------------------------------
# Filtering
# ---------------------------------------------------------------------------
def _rebuild_visible_rows(cache: TableCache) -> None:
df = cache.df
settings = cache.col_settings
meta = cache.col_meta
mask_series: pl.Series | None = None
active_str = (
settings.filter(pl.col("str_filter").str.len_chars() > 0)
.select(["str_filter"]).with_row_index("_c")
.with_columns(meta["col_name"].alias("col_name"))
)
active_num = (
settings.filter(pl.col("num_filter_op") > 0)
.select(["num_filter_op", "num_filter_val"]).with_row_index("_c")
.with_columns(meta["col_name"].alias("col_name"))
)
for row in active_str.iter_rows(named=True):
col_mask = df[row["col_name"]].str.to_lowercase().str.contains(
row["str_filter"].lower(), literal=True)
mask_series = col_mask if mask_series is None else (mask_series & col_mask)
for row in active_num.iter_rows(named=True):
op = NUM_FILTER_OPS[int(row["num_filter_op"])]
val = float(row["num_filter_val"])
flt = df[row["col_name"]].cast(pl.Float64)
if op == "<": col_mask = flt < val
elif op == "<=": col_mask = flt <= val
elif op == ">": col_mask = flt > val
elif op == ">=": col_mask = flt >= val
else: col_mask = flt == val
mask_series = col_mask if mask_series is None else (mask_series & col_mask)
cache.visible_rows = (
np.arange(len(df), dtype=np.int32) if mask_series is None
else mask_series.arg_true().to_numpy().astype(np.int32)
)
def _rebuild_visible_cols(cache: TableCache, st: "AppState") -> None:
"""Recompute visible_cols + vis_col_offsets from type filters and max_col_width."""
is_numeric = cache._meta_is_numeric
is_str = cache._meta_is_str
widths = cache._meta_width
idx_w = cache.index_col_width
max_w = max(1, st.max_col_width)
vis: list[int] = []
for c in range(len(cache.df.columns)):
if is_str[c] and not st.show_strs:
continue
if is_numeric[c]:
is_f = cache._meta_is_float[c]
if is_f and not st.show_floats:
continue
if not is_f and not st.show_ints:
continue
vis.append(c)
vc_arr = np.array(vis, dtype=np.int32)
effective_widths = np.minimum(widths[vc_arr] if len(vc_arr) else np.array([], dtype=np.int32),
max_w).astype(np.int32)
offsets = np.empty(len(vc_arr) + 1, dtype=np.int32)
offsets[0] = idx_w
if len(vc_arr):
offsets[1:] = idx_w + np.cumsum(effective_widths + 1)
cache.visible_cols = vc_arr
cache.vis_col_offsets = offsets
# ---------------------------------------------------------------------------
# Cell formatting
# ---------------------------------------------------------------------------
def _fmt_cell(value: object, c: int, cache: TableCache) -> str:
if value is None or (isinstance(value, float) and math.isnan(value)):
return "null"
if cache._meta_is_float[c]:
fv = float(value)
if cache._set_use_sigfigs[c]:
return _format_si(fv, int(cache._set_sigfigs[c]))
return _format_fixed(fv, int(cache._set_fixed_decimals[c]))
return str(value)
def _cf_color(value: float, c: int, cache: TableCache) -> int:
iqr1 = cache._meta_iqr1[c]
if iqr1 == 0:
return CF_NONE
dist = abs(value - cache._meta_median[c])
if dist <= iqr1: return CF_GREEN
elif dist <= cache._meta_iqr2[c]: return CF_YELLOW
elif dist <= cache._meta_iqr3[c]: return CF_ORANGE
return CF_RED
# ---------------------------------------------------------------------------
# Word wrap helpers
# ---------------------------------------------------------------------------
def _wrap_lines(text: str, max_width: int, char_w: float) -> list[str]:
"""Split text into lines fitting max_width pixels using char_w as avg char width."""
if not text:
return [""]
chars_per_line = max(1, int(max_width / char_w))
if len(text) <= chars_per_line:
return [text]
lines: list[str] = []
while len(text) > chars_per_line:
cut = text.rfind(" ", 0, chars_per_line)
cut = cut if cut > 0 else chars_per_line
lines.append(text[:cut])
text = text[cut:].lstrip(" ")
if text:
lines.append(text)
return lines or [""]
def _cell_height(lines: int) -> int:
return max(ROW_HEIGHT, lines * LINE_HEIGHT + PADDING)
# ---------------------------------------------------------------------------
# Draw helpers
# ---------------------------------------------------------------------------
def _draw_lines(
draw: imgui.ImDrawList,
cx: float, cy: float, cw: int, ch: int,
bg: int, lines: list[str],
clip_l: float, clip_r: float,
clip_t: float, clip_b: float,
char_color: int = TEXT_COLOR,
) -> None:
draw.add_rect_filled(ImVec2(cx, cy), ImVec2(cx + cw, cy + ch), bg)
draw.push_clip_rect(
ImVec2(max(cx, clip_l), max(cy, clip_t)),
ImVec2(min(cx + cw, clip_r), min(cy + ch, clip_b)), True)
tx = max(cx, clip_l) + PADDING
for i, line in enumerate(lines):
draw.add_text(ImVec2(tx, cy + PADDING // 2 + i * LINE_HEIGHT), char_color, line)
draw.pop_clip_rect()
def _draw_hdr_lines(
draw: imgui.ImDrawList,
cx: float, cy: float, cw: int, ch: int,
lines: list[str],
clip_l: float, clip_r: float,
clip_t: float, clip_b: float,
) -> None:
_draw_lines(draw, cx, cy, cw, ch, HEADER_BG, lines,
clip_l, clip_r, clip_t, clip_b, HEADER_TEXT)
# ---------------------------------------------------------------------------
# Column settings editor (shared between settings panel and right-click popup)
# ---------------------------------------------------------------------------
def _col_settings_editor(cache: TableCache, sel: int, st: AppState) -> None:
"""Render the settings widgets for column `sel`. Caller owns begin/end."""
meta = cache.col_meta
sdf = cache.col_settings
is_num = bool(cache._meta_is_numeric[sel])
is_flt = bool(cache._meta_is_float[sel])
is_str = bool(cache._meta_is_str[sel])
if is_num:
v = bool(cache._set_cf_enabled[sel])
ch, v = imgui.checkbox("Cond. formatting##cf", v)
if ch:
cache.update_setting(sel, "cf_enabled", v)
if is_flt:
imgui.text("Float format:")
v_fx = bool(cache._set_use_fixed[sel])
ch, v_fx = imgui.checkbox("Fixed decimals##fx", v_fx)
if ch:
cache.update_setting(sel, "use_fixed", v_fx)
if v_fx:
imgui.same_line()
imgui.set_next_item_width(60)
v_dec = int(cache._set_fixed_decimals[sel])
ch, v_dec = imgui.input_int("dec##dec", v_dec, 1, 1)
if ch:
cache.update_setting(sel, "fixed_decimals", max(0, min(10, v_dec)))
v_sf = bool(cache._set_use_sigfigs[sel])
ch, v_sf = imgui.checkbox("SI / sig figs##sf", v_sf)
if ch:
cache.update_setting(sel, "use_sigfigs", v_sf)
if v_sf:
imgui.same_line()
imgui.set_next_item_width(60)
v_sfc = int(cache._set_sigfigs[sel])
ch, v_sfc = imgui.input_int("sf##sfv", v_sfc, 1, 1)
if ch:
cache.update_setting(sel, "sigfigs", max(1, min(15, v_sfc)))
if is_str:
v_wt = bool(cache._set_wrap_text[sel])
ch, v_wt = imgui.checkbox("Word wrap##wt", v_wt)
if ch:
cache.update_setting(sel, "wrap_text", v_wt)
imgui.text("Filter:")
if is_str:
imgui.set_next_item_width(180)
ch, st._s_str_filter_buf[0] = imgui.input_text("##sfilt", st._s_str_filter_buf[0])
if ch:
cache.update_setting(sel, "str_filter", st._s_str_filter_buf[0])
_rebuild_visible_rows(cache)
if is_num:
v_op = int(sdf["num_filter_op"][sel])
imgui.set_next_item_width(80)
ch_op, v_op = imgui.combo("##nop", v_op, NUM_FILTER_OPS)
if v_op > 0:
imgui.same_line()
imgui.set_next_item_width(100)
ch_val, st._s_num_val_buf[0] = imgui.input_double("##nval", st._s_num_val_buf[0])
if ch_op or ch_val:
cache.update_setting(sel, "num_filter_op", v_op)
cache.update_setting(sel, "num_filter_val", st._s_num_val_buf[0])
_rebuild_visible_rows(cache)
elif ch_op:
cache.update_setting(sel, "num_filter_op", 0)
_rebuild_visible_rows(cache)
# ---------------------------------------------------------------------------
# App state
# ---------------------------------------------------------------------------
@dataclass
class AppState:
cache: TableCache | None = None
transposed: bool = False
show_settings: bool = True
show_generator: bool = True
show_fps: bool = True
global_cf: bool = True
# runtime wrap flags (mirror the constants but user-editable)
wrap_all: bool = False
wrap_headers: bool = True
wrap_index: bool = True
# column type visibility filters
show_ints: bool = True
show_floats: bool = True
show_strs: bool = True
max_col_width: int = STR_COL_MAX_PX
row_height: int = ROW_HEIGHT
# clipboard toast: time.perf_counter() when last copied, or 0
_clipboard_toast_t: float = 0.0
# generator params
gen: GenParams = field(default_factory=GenParams)
settings_search_buf: list = field(default_factory=lambda: [""])
settings_selected_col: int = -1
_s_str_filter_buf: list = field(default_factory=lambda: [""])
_s_num_val_buf: list = field(default_factory=lambda: [0.0])
# right-click popup
_popup_col: int = -1
# selection drag state
_sel_active: bool = False
_sel_start: tuple[float, float] | None = None
_sel_end: tuple[float, float] | None = None
_sel_row_range: tuple[int, int] = (0, 0)
_sel_col_range: tuple[int, int] = (0, 0)
_state = AppState()
def _init_state() -> None:
p = GenParams(rows=GEN_INIT_ROWS, cols=GEN_INIT_COLS)
df, labels = make_test_df(p)
_state.cache = _build_cache(df, labels)
_rebuild_visible_cols(_state.cache, _state)
# ---------------------------------------------------------------------------
# Settings panel
# ---------------------------------------------------------------------------
def show_settings_panel() -> None:
st = _state
cache = st.cache
if not st.show_settings or cache is None:
return
vp = imgui.get_main_viewport()
imgui.set_next_window_size(ImVec2(380, 420), imgui.Cond_.first_use_ever)
imgui.set_next_window_pos(
ImVec2(vp.pos.x + vp.size.x - 390, vp.pos.y + vp.size.y - 440),
imgui.Cond_.first_use_ever)
expanded, st.show_settings = imgui.begin("Column Settings", st.show_settings)
if not expanded:
imgui.end()
return
imgui.text("Search column:")
imgui.set_next_item_width(240)
_, st.settings_search_buf[0] = imgui.input_text("##col_search", st.settings_search_buf[0])
needle = st.settings_search_buf[0].lower().strip()
col_names = cache.col_meta["col_name"].to_list()
if needle:
matches = [(i, n) for i, n in enumerate(col_names) if needle in n.lower()]
imgui.text(f"{len(matches)} match(es):")
if imgui.begin_child("##csr", ImVec2(0, 100), imgui.ChildFlags_.borders):
for idx, name in matches[:50]:
sel = (idx == st.settings_selected_col)
if imgui.selectable(
f"{name} [{cache.col_meta['dtype_name'][idx]}]##{idx}", sel
)[1]:
st.settings_selected_col = idx
st.settings_search_buf[0] = ""
st._s_str_filter_buf[0] = cache.col_settings["str_filter"][idx]
st._s_num_val_buf[0] = float(cache.col_settings["num_filter_val"][idx])
imgui.end_child()
imgui.separator()
sel = st.settings_selected_col
if sel < 0 or sel >= len(col_names):
imgui.text_disabled("No column selected.")
imgui.end()
return
imgui.text(f"Editing: {col_names[sel]} [{cache.col_meta['dtype_name'][sel]}]")
imgui.spacing()
_col_settings_editor(cache, sel, st)
imgui.end()
# ---------------------------------------------------------------------------
# Generator window
# ---------------------------------------------------------------------------
def show_generator_window() -> None:
st = _state
if not st.show_generator:
return
vp = imgui.get_main_viewport()
imgui.set_next_window_size(ImVec2(340, 700), imgui.Cond_.first_use_ever)
imgui.set_next_window_pos(
ImVec2(vp.pos.x + vp.size.x - 350, vp.pos.y + 10),
imgui.Cond_.first_use_ever)
expanded, st.show_generator = imgui.begin("Generator & Global Settings", st.show_generator)
if not expanded:
imgui.end()
return
p = st.gen
W = 160 # widget width
# --- Generator section ---
imgui.text_colored(ImVec4(0.7, 0.9, 1.0, 1.0), "Data Generation")
imgui.separator()
imgui.set_next_item_width(W)
_, p.rows = imgui.input_int("Rows", p.rows, 100, 1000)
p.rows = max(1, min(500_000, p.rows))
imgui.set_next_item_width(W)
_, p.cols = imgui.input_int("Columns", p.cols, 1, 100)
p.cols = max(1, min(10_000, p.cols))
imgui.set_next_item_width(W)
_, p.seed = imgui.input_int("Seed", p.seed, 1, 10)
imgui.spacing()
_, p.gen_ints = imgui.checkbox("Integers", p.gen_ints)
imgui.same_line()
_, p.gen_floats = imgui.checkbox("Floats", p.gen_floats)
imgui.same_line()
_, p.gen_strs = imgui.checkbox("Strings", p.gen_strs)
if p.gen_ints:
imgui.set_next_item_width(W)
_, p.int_min = imgui.input_int("Int min", p.int_min, 1000, 100_000)
imgui.set_next_item_width(W)
_, p.int_max = imgui.input_int("Int max", p.int_max, 1000, 100_000)
if p.gen_floats:
imgui.set_next_item_width(W)
_, p.float_center_log_lo = imgui.slider_float(
"Center log lo", p.float_center_log_lo, -9.0, 0.0, "10^%.1f")
imgui.set_next_item_width(W)
_, p.float_center_log_hi = imgui.slider_float(
"Center log hi", p.float_center_log_hi, 0.0, 9.0, "10^%.1f")
imgui.set_next_item_width(W)
_, p.float_scale_exp_lo = imgui.slider_float(
"Scale exp lo", p.float_scale_exp_lo, -6.0, 0.0, "10^%.1f")
imgui.set_next_item_width(W)
_, p.float_scale_exp_hi = imgui.slider_float(
"Scale exp hi", p.float_scale_exp_hi, 0.0, 9.0, "10^%.1f")
if p.gen_strs:
imgui.set_next_item_width(W)
_, p.str_min_len = imgui.slider_int("Str min len", p.str_min_len, 1, 100)
imgui.set_next_item_width(W)
_, p.str_max_len = imgui.slider_int("Str max len", p.str_max_len, 1, 500)
imgui.set_next_item_width(W)
_, p.index_min_len = imgui.slider_int("Index min len", p.index_min_len, 1, 50)
imgui.set_next_item_width(W)
_, p.index_max_len = imgui.slider_int("Index max len", p.index_max_len, 1, 200)
imgui.spacing()
if imgui.button("Generate", ImVec2(120, 0)):
t0 = time.perf_counter()
df, labels = make_test_df(p)
st.cache = _build_cache(df, labels)
_rebuild_visible_cols(st.cache, st)
st.settings_selected_col = -1
dt = time.perf_counter() - t0
print(f"Generated {df.shape} df in {dt*1000:.1f} ms")
if st.cache is not None:
n_vis = len(st.cache.visible_rows)
n_total = len(st.cache.df)
imgui.same_line()
imgui.text(f"{n_vis}/{n_total} rows × {len(st.cache.df.columns)} cols")
imgui.spacing()
imgui.spacing()
# --- Global settings section ---
imgui.text_colored(ImVec4(0.7, 0.9, 1.0, 1.0), "Global Settings")
imgui.separator()
_, st.global_cf = imgui.checkbox("Conditional formatting", st.global_cf)
imgui.spacing()
imgui.text("Word wrap:")
_, st.wrap_all = imgui.checkbox("Enable all wrapping", st.wrap_all)
if st.wrap_all:
imgui.indent()
_, st.wrap_headers = imgui.checkbox("Headers", st.wrap_headers)
_, st.wrap_index = imgui.checkbox("Index column", st.wrap_index)
imgui.unindent()
imgui.spacing()
imgui.text("Show column types:")
needs_col_rebuild = False
ch_i, st.show_ints = imgui.checkbox("Int##show", st.show_ints)
imgui.same_line()
ch_f, st.show_floats = imgui.checkbox("Float##show", st.show_floats)
imgui.same_line()
ch_s, st.show_strs = imgui.checkbox("Str##show", st.show_strs)
if ch_i or ch_f or ch_s:
needs_col_rebuild = True
imgui.spacing()
imgui.set_next_item_width(W)
ch_mw, st.max_col_width = imgui.slider_int("Max col width", st.max_col_width, 1, MAX_COL_WIDTH, "%d px")
if ch_mw:
needs_col_rebuild = True
imgui.set_next_item_width(W)
_, st.row_height = imgui.slider_int("Row height", st.row_height, 1, ROW_HEIGHT, "%d px")
if needs_col_rebuild and st.cache is not None:
_rebuild_visible_cols(st.cache, st)
imgui.end()
# ---------------------------------------------------------------------------
# FPS window
# ---------------------------------------------------------------------------
def show_fps_window() -> None:
if not _state.show_fps:
return
vp = imgui.get_main_viewport()
imgui.set_next_window_size(ImVec2(160, 60), imgui.Cond_.first_use_ever)
imgui.set_next_window_pos(
ImVec2(vp.pos.x + 10, vp.pos.y + vp.size.y - 80),
imgui.Cond_.first_use_ever)
expanded, _state.show_fps = imgui.begin("FPS", _state.show_fps)
if expanded:
io = imgui.get_io()
imgui.text(f"FPS: {io.framerate:.1f}")
imgui.text(f"Frame: {io.delta_time * 1000:.2f} ms")
imgui.end()
# ---------------------------------------------------------------------------
# Table toolbar + popup host
# ---------------------------------------------------------------------------
def show_table_window() -> None:
st = _state
cache = st.cache
vp = imgui.get_main_viewport()
imgui.set_next_window_size(ImVec2(vp.size.x - 370, vp.size.y - 20), imgui.Cond_.first_use_ever)
imgui.set_next_window_pos(ImVec2(vp.pos.x + 10, vp.pos.y + 10), imgui.Cond_.first_use_ever)
if not imgui.begin("DataFrame Table"):
imgui.end()
return
if imgui.button("Transpose" if not st.transposed else "Un-Transpose"):
st.transposed = not st.transposed
imgui.same_line()
if imgui.button("Settings"):
st.show_settings = True
imgui.same_line()
if imgui.button("Generator"):
st.show_generator = True
imgui.same_line()
if imgui.button("FPS"):
st.show_fps = True
if cache is not None:
imgui.same_line()
imgui.text(f" {len(cache.visible_rows)}/{len(cache.df)} rows × {len(cache.df.columns)} cols")
imgui.spacing()
if cache is not None:
if not st.transposed:
_render_normal(cache, st)
else:
_render_transposed(cache, st)
# Right-click column settings popup (rendered in parent window scope)
if cache is not None and imgui.begin_popup("##colpop"):
c = st._popup_col
if 0 <= c < len(cache.df.columns):
imgui.text(f"{cache.col_meta['col_name'][c]} [{cache.col_meta['dtype_name'][c]}]")
imgui.separator()
_col_settings_editor(cache, c, st)
imgui.end_popup()
imgui.end()
# ---------------------------------------------------------------------------
# Column hover tooltip
# ---------------------------------------------------------------------------
def _col_hover_tooltip(cache: TableCache, c: int) -> None:
meta = cache.col_meta
imgui.begin_tooltip()
imgui.text(f"{meta['col_name'][c]} [{meta['dtype_name'][c]}]")
imgui.separator()
n_total = len(cache.df)
n_null = int(cache.df[meta["col_name"][c]].null_count())
imgui.text(f"n = {n_total} nulls = {n_null}")
if meta["is_numeric"][c]:
imgui.text(f"min = {meta['stat_min'][c]:.6g}")
imgui.text(f"max = {meta['stat_max'][c]:.6g}")
imgui.text(f"mean = {meta['stat_mean'][c]:.6g}")
imgui.text(f"std = {meta['stat_std'][c]:.6g}")
imgui.text(f"med = {meta['median'][c]:.6g}")
imgui.text(f"IQR = {meta['iqr1'][c]:.6g}")
elif meta["is_str"][c]:
imgui.text(f"unique = {meta['n_unique'][c]}")
imgui.text(f"top val = {meta['most_common'][c][:40]}")
imgui.end_tooltip()
# ---------------------------------------------------------------------------
# Selection finalisation
# ---------------------------------------------------------------------------
def _finalize_selection(
cache: TableCache, st: AppState,
scroll_x: float, scroll_y: float,
ox: float, oy: float, hdr_h: int,
offsets: np.ndarray,
vis_cols: np.ndarray,
) -> None:
if st._sel_start is None or st._sel_end is None:
return
x0 = min(st._sel_start[0], st._sel_end[0])
x1 = max(st._sel_start[0], st._sel_end[0])
y0 = min(st._sel_start[1], st._sel_end[1])
y1 = max(st._sel_start[1], st._sel_end[1])
tx0 = x0 - ox + scroll_x
tx1 = x1 - ox + scroll_x
n_vc = len(vis_cols)
first_vc = max(0, int(np.searchsorted(offsets, tx0, side="right")) - 1)
last_vc = min(n_vc - 1, int(np.searchsorted(offsets, tx1, side="right")) - 1)
ty0 = y0 - oy - hdr_h + scroll_y
ty1 = y1 - oy - hdr_h + scroll_y
n_rows = len(cache.visible_rows)
first_r = max(0, int(ty0 / (st.row_height + 1)))
last_r = min(n_rows - 1, int(ty1 / (st.row_height + 1)))
if first_vc > last_vc or first_r > last_r:
return
row_idx = cache.visible_rows[first_r : last_r + 1].tolist()
col_idxs = vis_cols[first_vc : last_vc + 1].tolist()
col_names = [cache.df.columns[i] for i in col_idxs]
sliced = cache.df[row_idx].select(col_names)
csv_text = sliced.write_csv()
imgui.set_clipboard_text(csv_text)
st._sel_row_range = (first_r, last_r)
st._sel_col_range = (first_vc, last_vc)
st._clipboard_toast_t = time.perf_counter()
print(f"Copied {sliced.shape} to clipboard as CSV")
# ---------------------------------------------------------------------------
# Normal orientation render
# ---------------------------------------------------------------------------
def _render_normal(cache: TableCache, st: AppState) -> None:
global_cf = st.global_cf
df = cache.df
offsets = cache.vis_col_offsets # offsets over visible columns
vis_cols = cache.visible_cols # df-col indices of visible columns
visible = cache.visible_rows
idx_w = cache.index_col_width
n_vis_cols = len(vis_cols)
n_rows = len(visible)
col_names = df.columns
total_w = int(offsets[n_vis_cols]) if n_vis_cols else idx_w
total_h = HEADER_HEIGHT + n_rows * (st.row_height + 1) # approximate
avail = imgui.get_content_region_avail()
win_flags = (
imgui.WindowFlags_.horizontal_scrollbar
| imgui.WindowFlags_.always_vertical_scrollbar
| imgui.WindowFlags_.no_move
)
if not imgui.begin_child("##tnorm", ImVec2(avail.x, max(avail.y - 4, 50)),
imgui.ChildFlags_.borders, win_flags):
imgui.end_child()
return
scroll_x = imgui.get_scroll_x()
scroll_y = imgui.get_scroll_y()
win_pos = imgui.get_window_pos()
view_w = imgui.get_window_width()
view_h = imgui.get_window_height()
draw = imgui.get_window_draw_list()
imgui.invisible_button("##area", ImVec2(total_w, total_h))
ox = win_pos.x
oy = win_pos.y
clip_r = ox + view_w
clip_b = oy + view_h
font_size = imgui.get_font_size()
char_w = font_size * 0.55 # approximate average char width
# Visible column range (indices into vis_cols)
first_vc = max(0, int(np.searchsorted(offsets, scroll_x, side="left")) - 1)
last_vc = min(n_vis_cols, int(np.searchsorted(offsets, scroll_x + view_w, side="right")) + 1)
if n_vis_cols:
cache.ensure_col_arrays(int(vis_cols[first_vc]), int(vis_cols[last_vc - 1]) + 1)
# --- Compute actual header height (may wrap) ---
hdr_h = HEADER_HEIGHT
wrap_headers = st.wrap_all and st.wrap_headers
wrap_index = st.wrap_all and st.wrap_index
if wrap_headers:
for vc in range(first_vc, last_vc):
c = int(vis_cols[vc])
cw = int(offsets[vc + 1] - offsets[vc] - 1)
nln = len(_wrap_lines(col_names[c], cw, char_w))
hdr_h = max(hdr_h, _cell_height(nln))
# Approximate first visible row from scroll
first_r = max(0, int((scroll_y - hdr_h) / (st.row_height + 1)) - 2)
# --- PHASE 1: data cells (clipped to data area) ---
data_clip_l = ox + idx_w
data_clip_t = oy + hdr_h
draw.push_clip_rect(ImVec2(data_clip_l, data_clip_t), ImVec2(clip_r, clip_b), True)
row_labels = cache.row_labels
cf_enabled = cache._set_cf_enabled
is_numeric = cache._meta_is_numeric
is_str_col = cache._meta_is_str
wrap_text = cache._set_wrap_text
cursor_y = oy + hdr_h + first_r * (st.row_height + 1) - scroll_y
ri = first_r
# store per-row y positions for selection highlight (keyed by ri)
row_y_map: dict[int, tuple[float, int]] = {} # ri -> (screen_y, height)
while ri < n_rows and cursor_y < clip_b:
actual_row = int(visible[ri])
row_bg = CELL_BG_EVEN if ri % 2 == 0 else CELL_BG_ODD
# Compute row height from wrapping across visible string columns
row_h = st.row_height
cell_lines: dict[int, list[str]] = {} # keyed by vc
for vc in range(first_vc, last_vc):
c = int(vis_cols[vc])
arr = cache.col_arrays.get(c)
val = arr[actual_row] if arr is not None else None
txt = _fmt_cell(val, c, cache)
if st.wrap_all and is_str_col[c] and wrap_text[c]:
cw = int(offsets[vc + 1] - offsets[vc] - 1)
lines = _wrap_lines(txt, cw, char_w)
row_h = max(row_h, _cell_height(len(lines)))
else:
lines = [txt]
cell_lines[vc] = lines
row_y_map[ri] = (cursor_y, row_h)
if cursor_y + row_h > data_clip_t: # at least partially visible
for vc in range(first_vc, last_vc):
c = int(vis_cols[vc])
cx = ox - scroll_x + int(offsets[vc])
cw = int(offsets[vc + 1] - offsets[vc] - 1)
arr = cache.col_arrays.get(c)
val = arr[actual_row] if arr is not None else None
lines = cell_lines[vc]
# Conditional formatting
if global_cf and cf_enabled[c] and is_numeric[c] and val is not None:
try:
fv = float(val)
if not math.isnan(fv):
cf = _cf_color(fv, c, cache)
_draw_lines(draw, cx, cursor_y, cw, row_h,
row_bg, lines, data_clip_l, clip_r,
data_clip_t, clip_b)
draw.add_rect_filled(
ImVec2(max(cx, data_clip_l), max(cursor_y, data_clip_t)),
ImVec2(min(cx + cw, clip_r), min(cursor_y + row_h, clip_b)), cf)
continue
except (TypeError, ValueError):
pass
_draw_lines(draw, cx, cursor_y, cw, row_h,
row_bg, lines, data_clip_l, clip_r, data_clip_t, clip_b)
cursor_y += row_h + 1
ri += 1
draw.pop_clip_rect()
# --- IQR distribution lines (numeric columns): connected center-to-center ---
# Each point is at (lx, cell_center_y); we draw lines between consecutive rows.
draw.push_clip_rect(ImVec2(data_clip_l, data_clip_t), ImVec2(clip_r, clip_b), True)
sorted_rows = sorted(row_y_map.items()) # [(ri, (y, h)), ...]
for vc in range(first_vc, last_vc):
c = int(vis_cols[vc])
if not is_numeric[c]:
continue
iqr4 = cache._meta_iqr1[c] * 4.0
if iqr4 == 0:
continue
median = cache._meta_median[c]
cx = ox - scroll_x + int(offsets[vc])
cw = int(offsets[vc + 1] - offsets[vc] - 1)
arr = cache.col_arrays.get(c)
if arr is None:
continue
prev_pt: tuple[float, float] | None = None
for ri2, (row_y2, row_h2) in sorted_rows:
actual_row2 = int(visible[ri2])
val = arr[actual_row2]
if val is None:
prev_pt = None
continue
try:
fv = float(val)
if math.isnan(fv):
prev_pt = None
continue
except (TypeError, ValueError):
prev_pt = None
continue
t = max(-1.0, min(1.0, (fv - median) / iqr4))
lx = cx + cw / 2.0 + t * (cw / 2.0 - 2)
lx = max(cx, min(cx + cw, lx))
cy2 = row_y2 + row_h2 / 2.0
if prev_pt is not None:
draw.add_line(ImVec2(prev_pt[0], prev_pt[1]), ImVec2(lx, cy2),
IQR_LINE_COL, 2.0)
prev_pt = (lx, cy2)
draw.pop_clip_rect()
# --- Selection highlight (drag rect) ---
if st._sel_active and st._sel_start is not None and st._sel_end is not None:
x0 = min(st._sel_start[0], st._sel_end[0])
x1 = max(st._sel_start[0], st._sel_end[0])
y0 = min(st._sel_start[1], st._sel_end[1])
y1 = max(st._sel_start[1], st._sel_end[1])
draw.add_rect_filled(ImVec2(x0, y0), ImVec2(x1, y1), SEL_FILL_COL)
draw.add_rect(ImVec2(x0, y0), ImVec2(x1, y1), SEL_EDGE_COL, 0.0, 0, 1.0)
elif st._sel_row_range != (0, 0) or st._sel_col_range != (0, 0):
# Draw thick border around the finalized selection cells
first_r2, last_r2 = st._sel_row_range
first_vc2, last_vc2 = st._sel_col_range
if (0 <= first_r2 <= last_r2 < n_rows and
0 <= first_vc2 <= last_vc2 < n_vis_cols):
# Find screen y extents from row_y_map
if first_r2 in row_y_map and last_r2 in row_y_map:
ry0, _ = row_y_map[first_r2]
ry1, rh = row_y_map[last_r2]
sx0 = ox - scroll_x + int(offsets[first_vc2])
sx1 = ox - scroll_x + int(offsets[last_vc2 + 1]) - 1
draw.add_rect_filled(ImVec2(sx0, ry0), ImVec2(sx1, ry1 + rh), SEL_FILL_COL)
draw.add_rect(ImVec2(sx0, ry0), ImVec2(sx1, ry1 + rh),
SEL_EDGE_COL, 0.0, 0, 2.5)
# --- PHASE 2: left index column (frozen) ---
draw.push_clip_rect(ImVec2(ox, data_clip_t), ImVec2(ox + idx_w, clip_b), True)
for ri2, (row_y2, row_h2) in row_y_map.items():
if row_y2 + row_h2 < data_clip_t or row_y2 > clip_b:
continue
actual_row2 = int(visible[ri2])
label = row_labels[actual_row2]
lines = _wrap_lines(label, idx_w - PADDING, char_w) if wrap_index else [label]
_draw_hdr_lines(draw, ox, row_y2, idx_w, row_h2, lines,
ox, ox + idx_w, data_clip_t, clip_b)
draw.pop_clip_rect()
# --- PHASE 3: column headers (frozen top) ---
# detect hovered column for tooltip and right-click
mouse = imgui.get_mouse_pos()
hovered_col = -1
in_header = (oy <= mouse.y <= oy + hdr_h)
if in_header:
mx_tbl = mouse.x - ox + scroll_x
hvc = int(np.searchsorted(offsets, mx_tbl, side="right")) - 1
if idx_w <= mx_tbl and 0 <= hvc < n_vis_cols:
hovered_col = int(vis_cols[hvc])
draw.push_clip_rect(ImVec2(ox + idx_w, oy), ImVec2(clip_r, oy + hdr_h), True)
for vc in range(first_vc, last_vc):
c = int(vis_cols[vc])
cx = ox - scroll_x + int(offsets[vc])
cw = int(offsets[vc + 1] - offsets[vc] - 1)
lines = _wrap_lines(col_names[c], cw, char_w) if wrap_headers else [col_names[c]]
bg = col32(70, 70, 100, 255) if c == hovered_col else HEADER_BG
draw.add_rect_filled(ImVec2(cx, oy), ImVec2(cx + cw, oy + hdr_h), bg)
draw.push_clip_rect(ImVec2(max(cx, ox + idx_w), oy),
ImVec2(min(cx + cw, clip_r), oy + hdr_h), True)
tx = max(cx, ox + idx_w) + PADDING
for i, line in enumerate(lines):
draw.add_text(ImVec2(tx, oy + PADDING // 2 + i * LINE_HEIGHT), HEADER_TEXT, line)
draw.pop_clip_rect()
draw.pop_clip_rect()
# --- PHASE 4: corner cell ---
draw.push_clip_rect(ImVec2(ox, oy), ImVec2(ox + idx_w, oy + hdr_h), True)
_draw_hdr_lines(draw, ox, oy, idx_w, hdr_h, ["#"],
ox, ox + idx_w, oy, oy + hdr_h)
draw.pop_clip_rect()
# Separator lines
draw.add_line(ImVec2(ox, oy + hdr_h), ImVec2(clip_r, oy + hdr_h), BORDER_COLOR, 1.0)
draw.add_line(ImVec2(ox + idx_w, oy), ImVec2(ox + idx_w, clip_b), BORDER_COLOR, 1.0)
# --- Hover tooltip ---
if hovered_col >= 0:
_col_hover_tooltip(cache, hovered_col)
if imgui.is_mouse_clicked(imgui.MouseButton_.right):
st._popup_col = hovered_col
st._s_str_filter_buf[0] = cache.col_settings["str_filter"][hovered_col]
st._s_num_val_buf[0] = float(cache.col_settings["num_filter_val"][hovered_col])
imgui.open_popup("##colpop")
# --- Drag selection ---
io = imgui.get_io()
in_data = (mouse.x >= ox + idx_w and mouse.x <= clip_r and
mouse.y >= oy + hdr_h and mouse.y <= clip_b)
if imgui.is_mouse_clicked(imgui.MouseButton_.left) and in_data:
st._sel_active = True
st._sel_start = (mouse.x, mouse.y)
st._sel_end = (mouse.x, mouse.y)
if st._sel_active:
if io.mouse_down[0]:
st._sel_end = (mouse.x, mouse.y)
else:
_finalize_selection(cache, st, scroll_x, scroll_y,
ox, oy, hdr_h, offsets, vis_cols)
st._sel_active = False
imgui.end_child()
# Clipboard toast
t_now = time.perf_counter()
if st._clipboard_toast_t > 0 and t_now - st._clipboard_toast_t < 2.0:
alpha = min(1.0, 2.0 - (t_now - st._clipboard_toast_t)) * 0.9
vp = imgui.get_main_viewport()
toast_x = vp.pos.x + vp.size.x * 0.5
toast_y = vp.pos.y + vp.size.y * 0.85
imgui.set_next_window_pos(ImVec2(toast_x, toast_y), imgui.Cond_.always,
ImVec2(0.5, 0.5))
imgui.set_next_window_bg_alpha(alpha)
flags = (imgui.WindowFlags_.no_decoration | imgui.WindowFlags_.no_inputs |
imgui.WindowFlags_.no_nav | imgui.WindowFlags_.always_auto_resize |
imgui.WindowFlags_.no_saved_settings | imgui.WindowFlags_.no_focus_on_appearing)
if imgui.begin("##toast", None, flags):
imgui.text(" Copied to clipboard ")
imgui.end()
# ---------------------------------------------------------------------------
# Transposed orientation render
# ---------------------------------------------------------------------------
def _render_transposed(cache: TableCache, st: AppState) -> None:
global_cf = st.global_cf
df = cache.df
meta = cache.col_meta
visible = cache.visible_rows
n_cols = len(df.columns)
n_rows = len(visible)
wrap_headers = st.wrap_all and st.wrap_headers
wrap_index = st.wrap_all and st.wrap_index
max_cn = int(meta["col_name"].str.len_chars().max() or 1)
row_hdr_w = max(min(max_cn * 7 + PADDING * 2, INDEX_WRAP_PX), 60)
# Each screen-column represents a df-row; natural width from data, capped by slider.
natural_w = int(np.median(cache._meta_width)) if len(cache._meta_width) else NUM_COL_PX
data_col_w = max(1, min(natural_w, st.max_col_width))
idx_row_h = HEADER_HEIGHT
row_h = st.row_height
total_w = row_hdr_w + n_rows * (data_col_w + 1)
total_h = idx_row_h + n_cols * (row_h + 1)
avail = imgui.get_content_region_avail()
win_flags = (
imgui.WindowFlags_.horizontal_scrollbar
| imgui.WindowFlags_.always_vertical_scrollbar
| imgui.WindowFlags_.no_move
)
if not imgui.begin_child("##ttrans", ImVec2(avail.x, max(avail.y - 4, 50)),
imgui.ChildFlags_.borders, win_flags):
imgui.end_child()
return
scroll_x = imgui.get_scroll_x()
scroll_y = imgui.get_scroll_y()
win_pos = imgui.get_window_pos()
view_w = imgui.get_window_width()
view_h = imgui.get_window_height()
draw = imgui.get_window_draw_list()
imgui.invisible_button("##areat", ImVec2(total_w, total_h))
ox = win_pos.x
oy = win_pos.y
clip_r = ox + view_w
clip_b = oy + view_h
font_size = imgui.get_font_size()
char_w = font_size * 0.55
first_dc = max(0, int((scroll_x - row_hdr_w) / (data_col_w + 1)) - 1)
last_dc = min(n_rows, first_dc + int(view_w / (data_col_w + 1)) + 3)
first_dr = max(0, int((scroll_y - idx_row_h) / (row_h + 1)) - 1)
last_dr = min(n_cols, first_dr + int(view_h / (row_h + 1)) + 3)
cache.ensure_col_arrays(first_dr, last_dr)
col_names = df.columns
row_labels = cache.row_labels
cf_enabled = cache._set_cf_enabled
is_numeric = cache._meta_is_numeric
data_clip_l = ox + row_hdr_w
data_clip_t = oy + idx_row_h
# --- PHASE 1: data cells ---
draw.push_clip_rect(ImVec2(data_clip_l, data_clip_t), ImVec2(clip_r, clip_b), True)
for dr in range(first_dr, last_dr):
row_y2 = oy + idx_row_h + dr * (row_h + 1) - scroll_y
row_bg = CELL_BG_EVEN if dr % 2 == 0 else CELL_BG_ODD
arr = cache.col_arrays.get(dr)
for dc in range(first_dc, last_dc):
actual_row = int(visible[dc])
cx = ox - scroll_x + row_hdr_w + dc * (data_col_w + 1)
val = arr[actual_row] if arr is not None else None
txt = _fmt_cell(val, dr, cache)
if global_cf and cf_enabled[dr] and is_numeric[dr] and val is not None:
try:
fv = float(val)
if not math.isnan(fv):
cf = _cf_color(fv, dr, cache)
_draw_lines(draw, cx, row_y2, data_col_w, row_h,
row_bg, [txt], data_clip_l, clip_r,
data_clip_t, clip_b)
draw.add_rect_filled(
ImVec2(max(cx, data_clip_l), max(row_y2, data_clip_t)),
ImVec2(min(cx+data_col_w, clip_r), min(row_y2+row_h, clip_b)), cf)
continue
except (TypeError, ValueError):
pass
_draw_lines(draw, cx, row_y2, data_col_w, row_h,
row_bg, [txt], data_clip_l, clip_r, data_clip_t, clip_b)
draw.pop_clip_rect()
# --- IQR lines (horizontal, per df-column row): connected center-to-center ---
draw.push_clip_rect(ImVec2(data_clip_l, data_clip_t), ImVec2(clip_r, clip_b), True)
for dr in range(first_dr, last_dr):
if not is_numeric[dr]:
continue
iqr4 = cache._meta_iqr1[dr] * 4.0
if iqr4 == 0:
continue
median = cache._meta_median[dr]
arr = cache.col_arrays.get(dr)
if arr is None:
continue
row_y2 = oy + idx_row_h + dr * (row_h + 1) - scroll_y
row_cy = row_y2 + row_h / 2.0
prev_pt: tuple[float, float] | None = None
for dc in range(first_dc, last_dc):
actual_row = int(visible[dc])
cx = ox - scroll_x + row_hdr_w + dc * (data_col_w + 1)
col_cx = cx + data_col_w / 2.0
val = arr[actual_row]
if val is None:
prev_pt = None
continue
try:
fv = float(val)
if math.isnan(fv):
prev_pt = None
continue
except (TypeError, ValueError):
prev_pt = None
continue
t = max(-1.0, min(1.0, (fv - median) / iqr4))
ly = row_cy + t * (row_h / 2.0 - 2)
ly = max(row_y2, min(row_y2 + row_h, ly))
if prev_pt is not None:
draw.add_line(ImVec2(prev_pt[0], prev_pt[1]), ImVec2(col_cx, ly),
IQR_LINE_COL, 2.0)
prev_pt = (col_cx, ly)
draw.pop_clip_rect()
# --- PHASE 2: row header column (frozen left) ---
draw.push_clip_rect(ImVec2(ox, data_clip_t), ImVec2(ox + row_hdr_w, clip_b), True)
for dr in range(first_dr, last_dr):
row_y2 = oy + idx_row_h + dr * (row_h + 1) - scroll_y
lines = _wrap_lines(col_names[dr], row_hdr_w - PADDING, char_w) if wrap_index else [col_names[dr]]
_draw_hdr_lines(draw, ox, row_y2, row_hdr_w, row_h, lines,
ox, ox + row_hdr_w, data_clip_t, clip_b)
draw.pop_clip_rect()
# --- PHASE 3: header row (row labels, frozen top) ---
mouse = imgui.get_mouse_pos()
hovered_col = -1
in_header = (oy <= mouse.y <= oy + idx_row_h)
if in_header:
mx_rel = mouse.x - ox + scroll_x - row_hdr_w
if mx_rel >= 0:
hc = int(mx_rel / (data_col_w + 1))
if 0 <= hc < n_rows:
hovered_col = hc # this is a visible-row index in transposed view
draw.push_clip_rect(ImVec2(data_clip_l, oy), ImVec2(clip_r, oy + idx_row_h), True)
for dc in range(first_dc, last_dc):
actual_row = int(visible[dc])
cx = ox - scroll_x + row_hdr_w + dc * (data_col_w + 1)
label = row_labels[actual_row]
lines = _wrap_lines(label, data_col_w - PADDING, char_w) if wrap_headers else [label]
bg = col32(70, 70, 100, 255) if dc == hovered_col else HEADER_BG
draw.add_rect_filled(ImVec2(cx, oy), ImVec2(cx + data_col_w, oy + idx_row_h), bg)
draw.push_clip_rect(ImVec2(max(cx, data_clip_l), oy),
ImVec2(min(cx + data_col_w, clip_r), oy + idx_row_h), True)
tx = max(cx, data_clip_l) + PADDING
for i, line in enumerate(lines):
draw.add_text(ImVec2(tx, oy + PADDING // 2 + i * LINE_HEIGHT), HEADER_TEXT, line)
draw.pop_clip_rect()
draw.pop_clip_rect()
# --- PHASE 4: corner ---
draw.push_clip_rect(ImVec2(ox, oy), ImVec2(ox + row_hdr_w, oy + idx_row_h), True)
_draw_hdr_lines(draw, ox, oy, row_hdr_w, idx_row_h, ["#"],
ox, ox + row_hdr_w, oy, oy + idx_row_h)
draw.pop_clip_rect()
# Separators
draw.add_line(ImVec2(ox, oy + idx_row_h), ImVec2(clip_r, oy + idx_row_h), BORDER_COLOR, 1.0)
draw.add_line(ImVec2(ox + row_hdr_w, oy), ImVec2(ox + row_hdr_w, clip_b), BORDER_COLOR, 1.0)
imgui.end_child()
# ---------------------------------------------------------------------------
# Main GUI
# ---------------------------------------------------------------------------
def gui() -> None:
if _state.cache is None:
_init_state()
show_table_window()
show_settings_panel()
show_generator_window()
show_fps_window()
def main() -> None:
_init_state()
from imgui_bundle import hello_imgui
runner_params = hello_imgui.RunnerParams()
runner_params.app_window_params.window_title = "ImGui DataFrame Viewer"
runner_params.app_window_params.window_geometry.size = (1400, 900)
runner_params.imgui_window_params.default_imgui_window_type = (
hello_imgui.DefaultImGuiWindowType.no_default_window
)
runner_params.callbacks.show_gui = gui
runner_params.ini_clear_previous_settings = True
def setup_imgui_config() -> None:
imgui.get_io().config_flags &= ~imgui.ConfigFlags_.docking_enable.value
runner_params.callbacks.setup_imgui_config = setup_imgui_config
immapp.run(runner_params=runner_params)
if __name__ == "__main__":
main()
@digitalsignalperson
Copy link
Copy Markdown
Author

image

@digitalsignalperson
Copy link
Copy Markdown
Author

Tranposed
image

Transposed and 1px col widths
image

Un-transposed and 1px row widths
image

Word wrap
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment