Last active
March 24, 2026 07:26
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| 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() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Tranposed

Transposed and 1px col widths

Un-transposed and 1px row widths

Word wrap
