diff --git a/docs/versions.rst b/docs/versions.rst index 6d0704c..8b46b22 100644 --- a/docs/versions.rst +++ b/docs/versions.rst @@ -11,6 +11,14 @@ Versions and Change Log .. TODO * self.padl and r / 12 in make html width adj s/b elsewhere +5.4 +---- +* Can now pass a namedtuple as df; it is converted to a dataframe. + +5.3 +---- +* Passing a callable to formatters applies that function to **all** columns. + 5.2.1 ----- * Added ``SmartTitle`` class in utilites - eventually add capitalize option for index? diff --git a/greater_tables/__init__.py b/greater_tables/__init__.py index c813539..ad1e3d9 100644 --- a/greater_tables/__init__.py +++ b/greater_tables/__init__.py @@ -1,6 +1,6 @@ __project__ = 'greater_tables' __author__ = 'Stephen J Mildenhall' -__version__ = '5.2.1' +__version__ = '5.4' from . core import GT from . fabrications import * diff --git a/greater_tables/core.py b/greater_tables/core.py index 1657a09..aded50f 100644 --- a/greater_tables/core.py +++ b/greater_tables/core.py @@ -7,6 +7,7 @@ to HTML, plain text, or LaTeX output using a validated configuration model. This is the main entry point for rendering logic. See `gtconfig.py` for configuration schema. """ +from collections import namedtuple from decimal import InvalidOperation from io import StringIO from itertools import groupby @@ -62,6 +63,7 @@ class GT(object): **Input transformation** + * ``namedtuple`` converted to ``DataFrame`` * ``pd.Series`` converted to ``DataFrame`` * ``list`` converted to ``DataFrame``, optionally using row 0 as ``config.header_row`` @@ -124,7 +126,7 @@ class GT(object): **overrides, ): """ - Available keyword ``**overrides`: + Parameters: :param df: target DataFrame or list of lists or markdown table string :param caption: table caption, optional (GT will look for gt_caption @@ -133,8 +135,13 @@ class GT(object): tables with #tbl:... in the caption it is extracted automatically. :param aligners: None or dict (type or colname) -> left | center | right - :param formatters: None or dict (type or colname) -> format function - for the column; formatters trump ratio_cols + :param formatters: None or dict (type or colname) or callable -> + format function for the column; formatters trump ratio_cols; if + callable passed, it is applied to **all columns**. + :param tabs: None or list of column widths in characters or a common + int or float width. (It is converted into em; one character is about + 0.5em on average; digits are exactly 0.5em.) If None, will be calculated. + Default None. :param unbreakable: None or list of columns to be considered unbreakable :param ratio_cols: None, or "all" or list of column names treated as ratios. Set defaults in derived class suitable to application. @@ -146,6 +153,9 @@ class GT(object): :param raw_cols: None, or "all" or list of column names that are NOT cast to floats. Set defaults in derived class suitable to application. :param show_index: if True, show the index columns, default True + + Available keyword *overrides: + :param config.default_integer_str: format f-string for integers, default value '{x:,d}' :param config.default_float_str: format f-string for floats, default @@ -188,10 +198,6 @@ class GT(object): columns to floats :param config.header_row: True: use first row as headers; False no headings. Default True - :param config.tabs: None or list of column widths in characters or a common - int or float width. (It is converted into em; one character is about - 0.5em on average; digits are exactly 0.5em.) If None, will be calculated. - Default None. :param config.equal: if True, set all column widths config.equal. Default False. Maybe ignored, depending on computed ideal column widths. :param config.caption_align: for the caption @@ -224,6 +230,7 @@ class GT(object): if config: base_config = config elif config_path: + config_path = Path(config_path) try: raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) base_config = Configurator.model_validate(raw) @@ -266,6 +273,8 @@ class GT(object): else: df, aligners, caption, label = MD2DF.md_to_df(df) show_index = False + elif GT._is_namedtuple_instance(df): + df = GT._ntdf(df) else: raise ValueError( 'df must be a DataFrame, a list of lists, or a markdown table string') @@ -492,16 +501,20 @@ class GT(object): self.df_idx_aligners = self.df_aligners[:self.nindex] + self.default_formatters = {} if formatters is None: - self.default_formatters = {} + pass + elif callable(formatters): + # apply to all columns + for k in self.df.columns: + self.default_formatters[k] = formatters else: - self.default_formatters = {} for k, v in formatters.items(): if callable(v): self.default_formatters[k] = v - elif type(v) == str: + elif isinstance(v, str): self.default_formatters[k] = lambda x: v.format(x=x) - elif type(v) == int: + elif isinstance(v, int): fmt = f'{{x:.{v}f}}' self.default_formatters[k] = lambda x: fmt.format(x=x) else: @@ -526,7 +539,7 @@ class GT(object): self.tabs = None if self.config.padding_trbl is not None: - padding_trbl = self.config_padding_trbl + padding_trbl = self.config.padding_trbl elif self.config.padding_trbl is None: if self.config.spacing == 'tight': padding_trbl = (0, 5, 0, 5) @@ -1020,12 +1033,16 @@ class GT(object): if mode == 'text': df = self.df len_function = len + # no bold in text mode + bold_adjustment = 1.0 elif mode == 'html': df = self.df_html len_function = TextLength.text_display_len + bold_adjustment = 1.1 else: # mode == 'tex': df = self.df_tex len_function = TextLength.text_display_len + bold_adjustment = 1.1 n_row, n_col = df.shape @@ -1055,8 +1072,8 @@ class GT(object): ) # ensure is a tuple ctuple = col_name if isinstance(col_name, tuple) else (col_name, ) - header_natural[col_name] = max(map(len_function, ctuple)) - header_minimum[col_name] = min(len_function(part) for i in ctuple for part in re.split(pat, str(i))) + header_natural[col_name] = bold_adjustment * max(map(len_function, ctuple)) + header_minimum[col_name] = bold_adjustment * min(len_function(part) for i in ctuple for part in re.split(pat, str(i))) # begin to assemble the parts # ans will be the col_width_df; break_penalties needed by all methods @@ -2028,3 +2045,14 @@ class GT(object): display(f.width_report()) print(f.make_tikz()) return f + + @staticmethod + def _is_namedtuple_instance(x) -> bool: + """Heuristic: namedtuple instances are tuples whose class defines _fields.""" + return isinstance(x, tuple) and isinstance(getattr(type(x), "_fields", None), tuple) + + @staticmethod + def _ntdf(t): + """Convert named tuple to pandas dataframe to display.""" + return pd.Series(t, index=pd.Index(t._fields, name="Item")).to_frame('Value') + diff --git a/greater_tables/utilities.py b/greater_tables/utilities.py index 73701f7..1ef0870 100644 --- a/greater_tables/utilities.py +++ b/greater_tables/utilities.py @@ -192,7 +192,7 @@ class Escaping: class TextLength: - """Estimate length of displayed text.""" + """Estimate length in em of displayed text.""" # TeX control sequence display widths (heuristic) TEX_SIMPLE_GLYPHS = { 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta', @@ -203,109 +203,25 @@ class TextLength: } TEX_WIDE = {'frac', 'sqrt', 'sum', 'int', 'prod'} TEX_SPACING = {'quad', 'qquad', ',', ';', ' ', '!'} + _WIDTH_TABLE = {"a": 0.444, "b": 0.5, "c": 0.444, "d": 0.5, "e": 0.444, "f": 0.333, + "g": 0.5, "h": 0.5, "i": 0.278, "j": 0.278, "k": 0.5, "l": 0.278, "m": 0.778, + "n": 0.5, "o": 0.5, "p": 0.5, "q": 0.5, "r": 0.333, "s": 0.389, "t": 0.278, + "u": 0.5, "v": 0.5, "w": 0.722, "x": 0.5, "y": 0.5, "z": 0.444, "A": 0.722, + "B": 0.667, "C": 0.667, "D": 0.722, "E": 0.611, "F": 0.556, "G": 0.722, "H": 0.722, + "I": 0.333, "J": 0.389, "K": 0.722, "L": 0.611, "M": 0.889, "N": 0.722, "O": 0.722, + "P": 0.556, "Q": 0.722, "R": 0.667, "S": 0.556, "T": 0.611, "U": 0.722, "V": 0.722, + "W": 0.944, "X": 0.722, "Y": 0.722, "Z": 0.611, "0": 0.5, "1": 0.5, "2": 0.5, + "3": 0.5, "4": 0.5, "5": 0.5, "6": 0.5, "7": 0.5, "8": 0.5, "9": 0.5, + ".": 0.25, ",": 0.25, ":": 0.278, ";": 0.278, "(": 0.333, ")": 0.333, "[": 0.333, + "]": 0.333, "’": 0.333, '"': 0.444, "!": 0.333, "?": 0.444, " ": 0.25, "|": 0.2, + "‘": 0.333, "{": 0.48, "}": 0.48, "-": 0.5, # 0.333, + } + _DEFAULT_WIDTH = 0.6 @staticmethod def approximate_char_width_em(c: str) -> float: - width_table = { - "il.':|!`": 0.3, - "frtJ(){}[]*": 0.5, - "abcdeghknopqsuvxyz": 0.6, - "LCDEFHISTUZ": 0.7, - "ABGKNOPQRXYV": 0.8, - "mwMW": 0.9, - "0123456789": 0.6, - "-_=+<>": 0.5, - "#$%^&@~": 0.6, - ",;": 0.25, - '"': 0.4, - "/\\": 0.5, - "?": 0.6, - " ": 0.4, - } - width_table = { - "a": 0.444, - "b": 0.5, - "c": 0.444, - "d": 0.5, - "e": 0.444, - "f": 0.333, - "g": 0.5, - "h": 0.5, - "i": 0.278, - "j": 0.278, - "k": 0.5, - "l": 0.278, - "m": 0.778, - "n": 0.5, - "o": 0.5, - "p": 0.5, - "q": 0.5, - "r": 0.333, - "s": 0.389, - "t": 0.278, - "u": 0.5, - "v": 0.5, - "w": 0.722, - "x": 0.5, - "y": 0.5, - "z": 0.444, - "A": 0.722, - "B": 0.667, - "C": 0.667, - "D": 0.722, - "E": 0.611, - "F": 0.556, - "G": 0.722, - "H": 0.722, - "I": 0.333, - "J": 0.389, - "K": 0.722, - "L": 0.611, - "M": 0.889, - "N": 0.722, - "O": 0.722, - "P": 0.556, - "Q": 0.722, - "R": 0.667, - "S": 0.556, - "T": 0.611, - "U": 0.722, - "V": 0.722, - "W": 0.944, - "X": 0.722, - "Y": 0.722, - "Z": 0.611, - "0": 0.5, - "1": 0.5, - "2": 0.5, - "3": 0.5, - "4": 0.5, - "5": 0.5, - "6": 0.5, - "7": 0.5, - "8": 0.5, - "9": 0.5, - ".": 0.25, - ",": 0.25, - ":": 0.278, - ";": 0.278, - "(": 0.333, - ")": 0.333, - "[": 0.333, - "]": 0.333, - "’": 0.333, - '"': 0.444, - "!": 0.333, - "?": 0.444, - " ": 0.25, - "|": 0.2, - "‘": 0.333, - "{": 0.48, - "}": 0.48, - "-": 0.5, # 0.333, - } - char_width = {c: w for chars, w in width_table.items() for c in chars} - return char_width.get(c, 0.6) + """Estimate character width in em.""" + return TextLength._WIDTH_TABLE.get(c, TextLength._DEFAULT_WIDTH) @staticmethod def text_display_len(s: str) -> float: