"""User-facing table specification (semantics) and house style (opinions).
``TableSpec`` carries everything the *engine* needs to build a TableDoc —
semantic choices that ride with the table. ``HouseStyle`` carries renderer
opinions (rule weights, fonts, spacing) that must never enter the IR; it is
populated as renderers arrive.
"""
from __future__ import annotations
from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass, field
from typing import Any, Hashable, Literal
from .ir import FormatSpec
__all__ = ['ColSelector', 'HouseStyle', 'TableSpec']
# 'all' | regex string | explicit list of column labels | None
ColSelector = str | Sequence[Hashable] | None
[docs]
@dataclass
class TableSpec:
"""Semantic specification for one table.
Parameters
----------
caption, label, notes
Table metadata; ``label`` is the Quarto cross-reference id.
show_index
Show the row index as stub columns. None (default) auto-hides a
trivial unnamed RangeIndex.
header_row
For list input: promote the first row to column headers.
ratio_cols, year_cols, date_cols, raw_cols
Semantic column tags: a list of labels, ``'all'``, or a regex
matched against column names.
html_cols
String columns whose cells carry raw HTML (sets the cell
``html`` flag; renderers still gate on their own allow-html option).
unbreakable
Columns forced to ``wrap=False`` regardless of dtype.
aligners
``'lrc'``-style string (positional) or mapping label → 'l'/'r'/'c'.
formatters
Per-column format override, keyed by column label (a tuple label also
matches on its leaf name). Values: a FormatSpec, a mapping of
FormatSpec fields (the YAML form), a sugar string (csv-grid
``',.1f'`` or GT ``'{x:.1%}'`` style), an int (decimal digits, with
grouping), or a callable taking the value and returning a string.
A callable owns its own NaN handling and does not reach the IR, so a
client cannot re-render that column.
float_format, int_format, ratio_format, date_format
House defaults, one per kind — the 5.x ``default_*_str`` settings.
Each overrides only its own kind's built-in default and loses to an
explicit ``formatters`` entry. ``date_format`` covers the date tag
and both date and datetime dtypes. Setting ``float_format`` turns the
float *inference* off entirely, including the automatic
engineering/SI switch, so ``pef_*`` and ``eng_*`` stop applying.
Callables are rejected: these are house rules that must be
YAML-expressible.
table_float_format
The sledgehammer: every **untagged float** column, overriding even an
explicit ``formatters`` entry. Ratio, year and date columns have a
kind, so it never reaches them. Default None, and deliberately absent
from ``config.write_template`` — set it consciously at a call site,
never in a config file where it would silently defeat per-column
formats months later.
row_flags
Mapping row position (negative allowed) → flags, or a callable
``(position, row_series) -> flags``. Vocabulary: total, subtotal,
emphasis, muted.
cell_flags
Callable ``(position, column_label, value) -> flags`` for per-cell
emphasis/muted. The ``neg`` flag is stamped automatically.
include_raw
Carry raw values in body cells: False (default), True/'data'
(numeric/date/bool data columns), or an explicit column list.
**Data columns only** in every form — stub (index) columns never
carry raw values, so a client that must sort on an index level
wants ``df.reset_index()`` before ``build``.
max_rows
Truncate longer tables, appending a note. None = unlimited.
sparsify, sparsify_columns
Merge repeated stub values into rowspans / repeated header prefixes
into colspans.
pef_lower, pef_upper
Engineering-format bounds: a float column whose mean magnitude falls
outside ``[10**pef_lower, 10**pef_upper]`` renders in engineering
notation, as does one whose nonzero values span more than
``10**pef_upper`` between largest and smallest (column-level
decisions — 5.x switched per cell, mixing formats within a column).
eng_digits, eng_style
What engineering format looks like when it fires: decimals, and
``'si'`` suffixes (µ m k M G …) or ``'exp'`` aligned exponents.
Both are dead if ``float_format`` is set — that turns inference off.
"""
caption: str | None = None
label: str | None = None
notes: Sequence[str] = ()
show_index: bool | None = None
header_row: bool = False
ratio_cols: ColSelector = None
year_cols: ColSelector = None
date_cols: ColSelector = None
raw_cols: ColSelector = None
html_cols: ColSelector = None
unbreakable: ColSelector = None
aligners: str | Mapping[Hashable, str] | None = None
formatters: Mapping[Hashable, FormatSpec | Mapping | str | int
| Callable[[Any], str]] = field(default_factory=dict)
# --- house defaults, one per kind (5.x's default_*_str)
float_format: FormatSpec | Mapping | str | int | None = None
int_format: FormatSpec | Mapping | str | int | None = None
ratio_format: FormatSpec | Mapping | str | int | None = None
date_format: FormatSpec | Mapping | str | int | None = None
table_float_format: FormatSpec | Mapping | str | int | None = None
row_flags: Mapping[int, Sequence[str]] | Callable[[int, Any], Sequence[str]] | None = None
cell_flags: Callable[[int, Hashable, Any], Sequence[str]] | None = None
include_raw: bool | str | Sequence[Hashable] = False
max_rows: int | None = 200
sparsify: bool = True
sparsify_columns: bool = True
pef_lower: int = -3
pef_upper: int = 6
eng_digits: int = 3
eng_style: Literal['si', 'exp'] = 'si'
[docs]
@dataclass
class HouseStyle:
"""Renderer opinions — never serialized into the IR.
Rule weights are the greater_tables float scale, mapped to TikZ line
styles via ``render.tikz.W2TIKZ`` (0 = don't draw, 0.25 = ultra thin …
3 = ultra thick). HTML equivalents live as CSS custom properties in
``render.css.VARIABLES``.
Parameters
----------
max_rule_depth
How deep the hierarchy is ruled. ``'auto'`` (default) rules every
break except the innermost stub level — a change there is just "next
row", so ruling it draws a line between every row; that gives no
rules on a single-level index, depth 0 only on two levels, depths 0
and 1 on three. An **int is literal**: ``0`` rules only outermost
changes, and on a single-level index it rules every row whose stub
changed. None draws no internal rules at all.
max_vrule_depth
The same for column-group verticals (None = none). Only
``render.policy.N_VRULE_SLOTS`` weights exist, so deeper breaks reuse
the lightest.
max_prose_width_em
How wide a single *wrapping* column may get. TikZ reads it directly;
HTML emits it as ``--gt-prose-max``. One column, not the table — the
table is ``max_table_width_em``.
max_table_width_em
Total table width to aim for, in ems, or None (default) for no
target. **A target, not a guarantee**: columns that must not wrap —
numbers, dates, anything tagged or listed in ``TableSpec.unbreakable``
— hold their natural width whatever the budget says, so a table of
nothing but numbers can exceed it. What the budget can do is narrow
the prose columns, which it does with one shared quantile so every
one of them carries the same chance of wrapping
(``render.widths``). When even the floors do not fit, TikZ steps
``tikz_scale`` down a fixed ladder and says so through ``logging``;
HTML hands the number to the browser as a CSS ``max-width`` and lets
it measure. Ems because every other geometry field here is ems: at
11pt, 8 inches is about 52em.
tikz_engine
Which TikZ emitter renders the table. ``'tabular'`` (default) puts one
real ``tabular`` inside a single node, taking horizontal rules from
booktabs and drawing only the vertical rules in TikZ; its body is a
literal ``tabular``, so the ``.tex`` lifts straight into a document
that does not use TikZ, and it needs ``booktabs`` and ``array`` in the
preamble. ``'matrix'`` is the original ``matrix of nodes`` renderer:
one addressable pgf node per cell, and about 290 ms of TeX per table
against the tabular's 9 — materializing the nodes *is* the cost, which
is why swapping table packages does not help. Keep ``'matrix'`` for
output that must stay byte-identical to 6.1.0, or for
``tikz_post_process`` TeX that addresses cells by node name, which
only the matrix emits. The one thing neither can do is break across
pages — a node is one unbreakable box.
text_ascii_rules
Draw the text rendering with ASCII only — ``=`` and ``-`` for heavy
and light rules, ``|`` for the stub divider, ``<<flag>>`` for the row
gutter. Default False (box-drawing characters). Worth setting for a
LaTeX verbatim block, where Pygments lexes U+2501 as an error token
and styles it red, and for a console whose encoding or mono font
cannot carry box drawing. Covers what the *renderer* draws, not cell
text: values arrive already formatted, so ``FormatSpec.null`` (an em
dash) and the SI suffix ladder (µ) still come through. Change those
on the spec side if a build must be pure ASCII end to end.
"""
max_rule_depth: int | Literal['auto'] | None = 'auto'
max_vrule_depth: int | None = 1
# --- widths, shared by every renderer that has an opinion about them
max_prose_width_em: float = 18.0
max_table_width_em: float | None = None
# --- text renderer
text_ascii_rules: bool = False
# --- TikZ engine, geometry (em) and container
tikz_engine: Literal['matrix', 'tabular'] = 'tabular'
tikz_scale: float = 1.0
tikz_column_sep: float = 1.0
tikz_row_sep: float = 0.25
tikz_container_env: str = 'table' # 'table' | 'figure' | 'sidewaysfigure'
tikz_latex: str | None = None # placement, e.g. 'ht'
tikz_post_process: str = ''
# --- rule weights (w2tikz scale)
frame_rule: float = 1.0 # top and bottom of table
head_rule: float = 1.0 # under the header block
stub_rule: float = 1.0 # stub/body vertical divider
total_rule: float = 0.75 # above rows flagged total
spanner_rule: float = 0.5 # cmidrule under column-group labels
hrule_widths: tuple[float, float, float] = (0.5, 0.25, 0.25)
vrule_widths: tuple[float, float, float] = (0.25, 0.25, 0.25)