From 50878fd9e49f4341ba640ac1916c464f7fbc6cc9 Mon Sep 17 00:00:00 2001 From: Stephen Mildenhall Date: Sun, 15 Jun 2025 21:11:27 +0100 Subject: [PATCH] 3.3.0 improved readme and docs, tikz options to config regex search columns for year_cols etc. grtspacer renamed I to be thinner - helps tikz layout toml refresh --- README.md | 155 ++++++------ docs/img/simple-example.png | Bin 0 -> 68468 bytes docs/index.rst | 162 ++++++++++--- greater_tables/__init__.py | 2 +- greater_tables/gtconfig.py | 46 +++- greater_tables/gtcore.py | 466 +++++------------------------------- greater_tables/testdf.py | 3 +- greater_tables/tex_svg.py | 18 +- pyproject.toml | 8 +- 9 files changed, 319 insertions(+), 541 deletions(-) create mode 100644 docs/img/simple-example.png diff --git a/README.md b/README.md index f089bd2..ba10bc6 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,54 @@ # `greater_tables` Project -![GitHub commit activity](https://img.shields.io/github/commit-activity/y/mynl/greater_tables_project) - - -## TODO - -* Ratio cols with multi index columns -* % in tex output - never allow comments? -* center / left / right table output -> CSS -* ?Option to hide index -* Bring over the roll your own logger - - -## Greater Tables - -Display graphics vs. data tables - -no colors, sparklines, shading, ... - -Creating presentation quality tables is difficult. It is hard to left-align text and right-align numbers using pandas `display` or `df.to_html`. The `great_tables` package does a really nice job with pandas and polars dataframes but does not support indexes or TeX output. - -This package provides consistent HTML and TeX table output with flexible type-based formatting, and table rules. Neither output relies on the pandas `to_html` or `to_latex` functions. TeX output uses Tikz tables for very tight control over layout and grid lines. The package is designed for use in Jupyter Lab notebooks Quarto documents. - -Usage: the main class `GT` should be subclassed to set appropriate defaults for your project. `sGT` provides an example. - -The project is currently in **beta** status. HTML output is better developed than TeX. - -## The Name - -Obviously, the name is a play on the `great_tables` package. But, I have -been maintaining a set of macros called -[GREATools](https://www.mynl.com/old/GREAT/home.html) (generalized, -reusable, extensible actuarial tools) in VBA and Python since the late -1990s, and call all my macro packages "GREAT". - -## Documentation - +![](https://img.shields.io/github/commit-activity/y/mynl/greater_tables_project) +![](https://img.shields.io/pypi/format/greater_tables) ![](https://img.shields.io/readthedocs/greater_tables_project) -Available on -[readthedocs](https://greater-tables-project.readthedocs.io/en/latest). + +## Greater Tables + +Creating presentation quality tables is difficult. `greater_tables` provides +a flexible way to create consistent tables in HTML, LaTeX (PDF), and terminal +text outputs from Pandas dataframes. +It has many options but sensible defaults. It is designed +for use in Jupyter Lab and Quarto and will seamlessly return the correct format +for each output type. The basic usage is simply: + +```python +from greater_tables import GT +# ...create dataframe df... +GT(df) +``` + +or `display(GT(df))` if called within a Jupyter or Quarto code block. Once created `GT(df)` is immutable; to change options re-create. Presentation tables are small! They fit on one or two pages and, while `GT` does a lot of work to determine formating options, it still runs very quickly. + +`greater_tables` provides similar functionality to pandas `to_html`, `to_latex` and `to_markdown` +methods, without relying on them, and improves them in various ways. LaTeX output uses Tikz tables for very tight control over layout and grid lines. Arguments can be passed directly or set via a YAML configuration file. Validation is handled by `pydantic`. + +The package is tailored to more austere, black-and-white tables: no sparklines, colors or background shading. Tables can include a simple caption, but not more elaborate headers and footers. ## Installation -![](https://img.shields.io/pypi/format/greater_tables) - ```python pip install greater-tables ``` +## Documentation + +[ReadtheDocs](https://greater-tables-project.readthedocs.io/en/latest). + +## Source + +[GitHub](https://www.github.com/mynl/greater_tables_project). + +## Licence + +MIT. + ## Examples -The following example shows quite a hard table. It is formatted using -the `sGT` class, which is a subclass of `GT` with a few defaults set. +The following example shows a tricky hard table. ```python import pandas as pd @@ -63,49 +58,58 @@ level_1 = ["Group A", "Group A", "Group B", "Group B", 'Group C'] level_2 = ['Sub 1', 'Sub 2', 'Sub 2', 'Sub 3', 'Sub 3'] multi_index = pd.MultiIndex.from_arrays([level_1, level_2]) - -start = pd.Timestamp.today().normalize() # Today's date, normalized to midnight +start = pd.Timestamp.today().normalize() end = pd.Timestamp(f"{start.year}-12-31") # End of the year - -hard = pd.DataFrame( -{'x': np.arange(2020, 2025, dtype=int), +df = pd.DataFrame( +{'year': np.arange(2020, 2025, dtype=int), 'a': np.array((100, 105, 2000, 2025, 100000), dtype=int), 'b': 10. ** np.linspace(-9, 9, 5), 'c': np.linspace(601, 4000, 5), 'd': pd.date_range(start=start, end=end, periods=5), 'e': 'once upon a time, risk is hard to define, not in Kansas anymore, neutrinos are hard to detect, $\\int_\\infty^\\infty e^{-x^2/2}dx$ is a hard integral'.split(',') -}).set_index('x') -hard.columns = multi_index -sGT(hard, 'A hard table.') +}).set_index('year') +df.columns = multi_index +gtc.GT(df, caption='A simple GT table.', + year_cols='year', + vrule_widths=(1,.5, 0)) ``` -![HTML output.](img/hard-html.png) - -![TeX output.](img/hard-tex.png) +![](docs/img/simple-example.png) The output illustrates: -- Quarto or Jupyter automatically the class's `_repr_html_` method (or +- Quarto or Jupyter automatically calls the class's `_repr_html_` method (or `_repr_latex_` for pdf/TeX/Beamer output), providing seamless - integration across different output formats. -- Text is left-aligned, numbers are right-aligned. -- The index is displayed, was detected as likely years, and formatted - without a comma separator. -- The first column of integers does have a comma thousands separator. + integration across different output formats. `print()` produces fixed-pitch text output. +- Text is left-aligned, numbers are right-aligned, and dates are centered. +- The index is displayed, and formatted without a comma separator, being specified in `year_cols`. Columns specified in `ratio_col` use % formatting. Explicit control provided over all columns; these are just helpers. +- The first column of integers with a comma thousands separator and no decimals. - The second column of floats spans several orders of magnitude and is - formatted using Engineering format, n for nano through G for giga. + formatted using Engineering format, n for nano through k for kilo. - The third column of floats is formatted with a comma separator and two decimals, based on the average absolute value. -- The fourth column of date times is formatted as ISO standard dates - (not date times). -- The vertical lines separate the levels of the column multiindex. The - subgroups are a little tricky. +- The fourth column of date times is formatted as ISO standard dates. +- Text, in the last column, is sensibly wrapped and can include TeX. +- The vertical lines separate the levels of the column multiindex. -More coming soon. + + +## The Name + +Obviously, the name is a play on the `great_tables` package. I have +been maintaining a set of macros called +[GREATools](https://www.mynl.com/old/GREAT/home.html) (generalized, +reusable, extensible actuarial tools) in VBA and Python since the late +1990s, and call all my macro packages *GREAT*. ## History +3.3.0 +------- +* Added `tikz_` series of options to config: column and row separation, + container_env (for e.g., sidewaystable), hrule and vrule indices. + 3.2.0 ------- * Added more tex snippets! @@ -134,6 +138,7 @@ More coming soon. * Better column widths * Custom text output * Rich table output + 1.1.1 ------- * Added logo, updated docs. @@ -147,7 +152,6 @@ More coming soon. * Added ``caption_align='center'`` argument to set the caption alignment * Added ``large_ok=False`` argument, if ``False`` providing a dataframe with more than 100 rows throws an error. This function is expensive and is designed for small frames. - 1.0.0 ------ @@ -159,7 +163,6 @@ More coming soon. * Docs updated * Set tabs for width; use of width in HTML format. - 0.6.0 ------ @@ -172,10 +175,6 @@ Early development * tikz code from great.pres_manager - - - - ## 📁 Project Layout ``` @@ -222,17 +221,3 @@ greater_tables_project/ | hard-html.png | hard-tex.png ``` - - - -## 🧠 Design Principles You’re Following - -| Principle | Your Approach | -| ---------------------------- | ------------------------------------------- | -| Immutability | `GT(df, config)` is fixed once created | -| Separation of concerns | `GTConfigModel` holds defaults/types | -| Config as code/documentation | `config_template.yaml` generated from model | -| CLI-first mindset | `click` used to expose functionality | -| Linear Git workflow | Tags for rollback, no branches | - - diff --git a/docs/img/simple-example.png b/docs/img/simple-example.png new file mode 100644 index 0000000000000000000000000000000000000000..1baabe6c4d3df5801d9d75b176c782e281d6e882 GIT binary patch literal 68468 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 literal 0 HcmV?d00001 diff --git a/docs/index.rst b/docs/index.rst index ade6926..d9b2da2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,7 +4,128 @@ contain the root `toctree` directive. Welcome to greater_tables's documentation! -========================================== +============================================ + +|image1| |image2| |image3| + +Greater Tables +-------------- + +Creating presentation quality tables is difficult. ``greater_tables`` +provides a flexible way to create consistent tables in HTML, LaTeX +(PDF), and terminal text outputs from Pandas dataframes. It has many +options but sensible defaults. It is designed for use in Jupyter Lab and +Quarto and will seamlessly return the correct format for each output +type. The basic usage is simply: + +.. code:: python + + from greater_tables import GT + # ...create dataframe df... + GT(df) + +or ``display(GT(df))`` if called within a Jupyter or Quarto code block. +Once created ``GT(df)`` is immutable; to change options re-create. +Presentation tables are small! They fit on one or two pages and, while +``GT`` does a lot of work to determine formating options, it still runs +very quickly. + +``greater_tables`` provides similar functionality to pandas ``to_html``, +``to_latex`` and ``to_markdown`` methods, without relying on them, and +improves them in various ways. LaTeX output uses Tikz tables for very +tight control over layout and grid lines. Arguments can be passed +directly or set via a YAML configuration file. Validation is handled by +``pydantic``. + +The package is tailored to more austere, black-and-white tables: no +sparklines, colors or background shading. Tables can include a simple +caption, but not more elaborate headers and footers. + +Installation +------------ + +.. code:: python + + pip install greater-tables + +Documentation +------------- + +`ReadtheDocs `__. + +Source +------ + +`GitHub `__. + +Licence +------- + +MIT. + +Examples +-------- + +The following example shows a tricky hard table. + +.. code:: python + + import pandas as pd + import numpy as np + from greater_tables import sGT + level_1 = ["Group A", "Group A", "Group B", "Group B", 'Group C'] + level_2 = ['Sub 1', 'Sub 2', 'Sub 2', 'Sub 3', 'Sub 3'] + + multi_index = pd.MultiIndex.from_arrays([level_1, level_2]) + start = pd.Timestamp.today().normalize() + end = pd.Timestamp(f"{start.year}-12-31") # End of the year + df = pd.DataFrame( + {'year': np.arange(2020, 2025, dtype=int), + 'a': np.array((100, 105, 2000, 2025, 100000), dtype=int), + 'b': 10. ** np.linspace(-9, 9, 5), + 'c': np.linspace(601, 4000, 5), + 'd': pd.date_range(start=start, end=end, periods=5), + 'e': 'once upon a time, risk is hard to define, not in Kansas anymore, neutrinos are hard to detect, $\\int_\\infty^\\infty e^{-x^2/2}dx$ is a hard integral'.split(',') + }).set_index('year') + df.columns = multi_index + gtc.GT(df, caption='A simple GT table.', + year_cols='year', + vrule_widths=(1,.5, 0)) + +.. figure:: img/simple-example.png + + +The output illustrates: + +- Quarto or Jupyter automatically calls the class’s ``_repr_html_`` + method (or ``_repr_latex_`` for pdf/TeX/Beamer output), providing + seamless integration across different output formats. ``print()`` + produces fixed-pitch text output. +- Text is left-aligned, numbers are right-aligned, and dates are + centered. +- The index is displayed, and formatted without a comma separator, + being specified in ``year_cols``. Columns specified in ``ratio_col`` + use % formatting. Explicit control provided over all columns; these + are just helpers. +- The first column of integers with a comma thousands separator and no + decimals. +- The second column of floats spans several orders of magnitude and is + formatted using Engineering format, n for nano through k for kilo. +- The third column of floats is formatted with a comma separator and + two decimals, based on the average absolute value. +- The fourth column of date times is formatted as ISO standard dates. +- Text, in the last column, is sensibly wrapped and can include TeX. +- The vertical lines separate the levels of the column multiindex. + +The Name +-------- + +Obviously, the name is a play on the ``great_tables`` package. I have +been maintaining a set of macros called +`GREATools `__ (generalized, +reusable, extensible actuarial tools) in VBA and Python since the late +1990s, and call all my macro packages *GREAT*. + .. toctree:: :maxdepth: 2 @@ -14,40 +135,17 @@ Welcome to greater_tables's documentation! greater_tables.data -.. include:: ../README.md - :parser: myst_parser.sphinx_ - -Other -======= - -Auto doc files generated with:: - - sphinx-apidoc -o . ..\greater_tables\ - -File layout: - - C:\S\TELOS\PYTHON\GREATER_TABLES_PROJECT\GREATER_TABLES - | cli.py - | gtconfig.py - | gtcore.py - | gtenums.py - | gtformats.py - | hasher.py - | testdf.py - | tex_svg.py - | __init__.py - | - +---data - | | tex_list.csv - | | tex_list.py - | | words-12.md - | | __init__.py - - - Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` + + + + +.. |image1| image:: https://img.shields.io/github/commit-activity/y/mynl/greater_tables_project +.. |image2| image:: https://img.shields.io/pypi/format/greater_tables +.. |image3| image:: https://img.shields.io/readthedocs/greater_tables_project +.. |image4| image:: docs/img/simple-example.png diff --git a/greater_tables/__init__.py b/greater_tables/__init__.py index 907ee4c..334d15e 100644 --- a/greater_tables/__init__.py +++ b/greater_tables/__init__.py @@ -1,4 +1,4 @@ -__version__ = '3.2.0' +__version__ = '3.3.0' __project__ = 'greater_tables' __author__ = 'Stephen J Mildenhall' diff --git a/greater_tables/gtconfig.py b/greater_tables/gtconfig.py index 034f1ff..ec4d704 100644 --- a/greater_tables/gtconfig.py +++ b/greater_tables/gtconfig.py @@ -35,8 +35,11 @@ class GTConfigModel(BaseModel): :see also: ``GTConfig`` for loading from YAML with overrides. ``gt write-template`` CLI command to generate a default config file. """ - # immutable - model_config = ConfigDict(frozen=True) + model_config = ConfigDict( + # make model immutable (no attribute reassignment) + frozen=True, + extra="forbid" # raise error on unexpected/extra fields + ) default_integer_str: str = Field( "{x:,d}", description="Format f-string for integers. Example: '{x:,d}'" ) @@ -125,10 +128,6 @@ class GTConfigModel(BaseModel): caption_align: str = Field( "center", description="Alignment of the caption text" ) - large_ok: bool = Field( - False, description="If True, allow full rendering of large tables without truncation" - ) - max_str_length: int = Field( -1, description="Maximum length for stringified objects (e.g. nested DataFrames); -1 = unlimited" ) @@ -152,7 +151,40 @@ class GTConfigModel(BaseModel): table_width_header_relax: float = Field( 10.0, description="Extra characters allowed per column heading to help header wrapping" ) - debug: bool = Field(False, description="Run in debug mode with more reporting, include internal ID in caption and use colored output lines") + + # tikz specific options + tikz_column_sep: float = Field( + 0.5, description="Separation between columns") + tikz_row_sep: float = Field( + 0.125, description="Separation between rows") + tikz_container_env: Literal["table", "figure", "sidewaysfigure"] = Field( + default="table", + description="Type of element: 'table', 'figure', or 'sidewaysfigure'" + ) + tikz_extra_defs: str = Field( + '', description="TeX defintions and commands put at top of table, eg \\centering.") + tikz_hrule: Optional[list[int]] = Field( + default=None, + description="Optional, list of (0-based) integers for horizontal rules below each value; None means no lines." + ) + tikz_vrule: Optional[list[int]] = Field( + default=None, + description="Optional, list of integers for vertical rules right of each value; None means no lines." + ) + tikz_post_process: str = Field( + '', description="non-line commands put at bottom of table") + tikz_latex: Optional[str] = Field( + None, description="arguments at top of table \\begin{table}[tikz_latex]") + + # meta + debug: bool = Field( + False, description="Run in debug mode with more reporting, include internal ID in caption and use colored output lines") + large_ok: bool = Field( + False, description="If True, allow full rendering of large tables without truncation" + ) + large_warning: int = Field( + 50, description="Warn for dataframes longer then large_warning unless large_ok==True" + ) def write_template(self, path: Path): """Generate a clean default config file at the given path.""" diff --git a/greater_tables/gtcore.py b/greater_tables/gtcore.py index 8404aca..ee08fa0 100644 --- a/greater_tables/gtcore.py +++ b/greater_tables/gtcore.py @@ -264,7 +264,8 @@ class GT(object): # access through config # update and validate; need to merge to avoid repeated args - merged = dict(base_config.model_dump(), **overrides) + # merged = dict(base_config.model_dump(), **overrides) + merged = base_config.model_dump() | overrides self.config = GTConfigModel(**merged) # no validation # self.config = base_config.model_copy(update=overrides) @@ -298,7 +299,7 @@ class GT(object): raise ValueError( 'df must be a DataFrame, a list of lists, or a markdown table string') - if len(df) > 50 and not config.large_ok: + if len(df) > self.config.large_warning and not config.large_ok: raise ValueError( 'Large dataframe (>50 rows) and config.large_ok not set to true...do you know what you are doing?') @@ -657,8 +658,21 @@ class GT(object): return self._clean_tex def cols_from_regex(self, regex): - """Return columns of self.df matching regex""" - return [col for col in self.df.columns if isinstance(col, str) and re.search(regex, col)] + """ + Return columns matching a regex. + + For Index and MultiIndex. Operates on ``self.df`` and includes + index (if ``show_index``) and columns of input dataframe. Search + applies to any level of the index. Case sensitive. + """ + pattern = re.compile(regex) + matching_cols = [ + col for col in self.df.columns + if any(pattern.search(str(level)) + for level in (col if isinstance(col, tuple) else (col,))) + ] + return matching_cols + # return [col for col in self.df.columns if isinstance(col, str) and re.search(regex, col)] def cache_get(self, key): """Retrieve item from cache.""" @@ -977,6 +991,7 @@ class GT(object): self._column_width_df = self.make_column_width_df() tikz_colw, tabs, scaled_tabs = self.estimate_column_widths() self._column_width_df['tikz_colw'] = tikz_colw + self._column_width_df['tikz_colw'] += 2 # for \I spacer! self._column_width_df['estimated_tabs'] = tabs self._column_width_df['estimated_scaled_tabs'] = scaled_tabs if self.tabs is not None: @@ -1535,6 +1550,7 @@ class GT(object): # with tex adjustment tex_colw = dict.fromkeys(df.columns, 0) headw = dict.fromkeys(df.columns, 0) + tikz_headw = dict.fromkeys(df.columns, 0) tabs = [] scaled_tabs = [] mxmn = {} @@ -1544,12 +1560,15 @@ class GT(object): for i, c in enumerate(df.columns): # figure width of the column labels; if index c= str, if MI then c = tuple # cw is the width of the column header/title + # tzcw is for tikz - no wrapping and no tex adjustment if type(c) == str: if i < nl: cw = GT.text_display_len(c) + tzcw = len(c) else: # for data columns look at words rather than whole phrase cw = max(map(GT.text_display_len, c.split(' '))) + tzcw = len(c) # logger.info(f'leng col = {len(c)}, longest word = {cw}') else: # column name could be float etc. or if multi index a tuple @@ -1559,14 +1578,18 @@ class GT(object): words = ' '.join(c).split(' ') cw = max( map(lambda x: GT.text_display_len(str(x)), words)) + tzcw = max(map(len, words)) else: cw = max(map(lambda x: GT.text_display_len(str(x)), c)) + tzcw = max(map(len, c)) # print(f'{c}: {cw=} no error') except TypeError: # not a MI, float or something cw = GT.text_display_len(str(c)) + tzcw = len(str(c)) # print(f'{c}: {cw=} WITH error') headw[c] = cw + tikz_headw[c] = tzcw # now figure the width of the elements in the column # mxmn is used to determine whether to center the column (if all the same size) if df.dtypes.iloc[i] == object: @@ -1591,6 +1614,9 @@ class GT(object): mxmn[c] = (lens.max(), lens.min()) raw_lens = df.iloc[:, i].map(len) tikz_colw[c] = raw_lens.max() + # pick up long headers too + for c in df.columns: + tikz_colw[c] = max(tikz_colw[c], tikz_headw[c]) # print(tikz_colw) # now know all column widths...decide what to do # are all the data columns about the same width? @@ -2008,16 +2034,15 @@ class GT(object): return self._clean_html def make_tikz(self, - column_sep=4 / 8, # was 3/8 - row_sep=1 / 8, - container_env='table', - extra_defs='', - hrule=None, - vrule=None, - post_process='', - label='', - latex=None, - sparsify=1): + # column_sep=4 / 8, # was 3/8 + # row_sep=1 / 8, + # container_env='table', + # extra_defs='', + # hrule=None, + # vrule=None, + # post_process='', + # latex=None, + ): """ Write DataFrame to custom tikz matrix. @@ -2050,8 +2075,6 @@ class GT(object): never be a rule to the far right...it looks plebby; remember you must include the index columns! - config.sparsify number of cols of multi index to config.sparsify - Issue: column with floats and spaces or missing causes problems (VaR, TVaR, EPD, mean and CV table) @@ -2071,7 +2094,7 @@ class GT(object): lines lines below these rows, -1 for next to last row etc.; list of ints post_process e.g., non-line commands put at bottom of table - latex arguments after \begin{table}[latex] + latex arguments after \\begin{table}[latex] caption text for caption Previous version see great.pres_maker @@ -2085,6 +2108,16 @@ class GT(object): :param label: :return: """ + # pull out arguments (convert to local vars - these used to be arguments) + column_sep = self.config.tikz_column_sep + row_sep = self.config.tikz_row_sep + container_env = self.config.tikz_container_env + extra_defs = self.config.tikz_extra_defs + hrule = self.config.tikz_hrule + vrule = self.config.tikz_vrule + post_process = self.config.tikz_post_process + latex = self.config.tikz_latex + # local variable - with all formatters already applied df = self.df.copy() # self.apply_formatters(self.raw_df.copy(), mode='raw') caption = self.caption @@ -2217,8 +2250,16 @@ class GT(object): f'\trow {i}/.style={{nodes={{text=black, anchor=north, inner ysep=0, text height=0, text depth=0}}}},\n') for i in range(2, nr_columns + 2): sio.write( - f'\trow {i}/.style={{nodes={{text=black, anchor=south, inner ysep=.2em, minimum height=1.3em, font=\\bfseries}}}},\n') + f'\trow {i}/.style={{nodes={{text=black, anchor=south, inner ysep=.2em, minimum height=1.3em, font=\\bfseries, align=center}}}},\n') + # override for index columns headers + # probably ony need for the bottom row with a multiindex? + for i in range(2, nr_columns + 2): + for j in range(1, 1+nc_index): + sio.write( + f'\trow {i} column {j}/.style=' + '{nodes={font=\\bfseries\\itshape, align=left}},\n' + ) # write column spec for i, w, al in zip(range(1, len(align) + 1), tabs, align): # average char is only 0.48 of M @@ -2235,7 +2276,7 @@ class GT(object): f'nodes={{align={ad[al]:<6s}}}, nosep, text width={max(2, 0.6 * w):.2f}em}},\n') # extra col to right which enforces row height sio.write( - f'\tcolumn {i+1:>2d}/.style={{text height=0.9em, text depth=0.2em, nosep, text width=0em}}') + f'\tcolumn {i+1:>2d}/.style={{text height=0.9em, text depth=0.2em, nosep, text width=0em}}\n') sio.write('\t}]\n') sio.write("\\matrix ({matrix_name}) [table, ampersand replacement=\\&]{{\n".format( @@ -2262,7 +2303,7 @@ class GT(object): # c = wfloat_format(c) s = f'{nl} {{cell:{ad2[al]}{colw[cn]}s}} ' nl = '\\&' - sio.write(s.format(cell=c + '\\grtspacer')) + sio.write(s.format(cell=c + '\\I')) # include the blank extra last column sio.write('\\& \\\\\n') else: @@ -2271,7 +2312,7 @@ class GT(object): # c = wfloat_format(c) s = f'{nl} {{cell:{ad2[al]}{colw[c]}s}} ' nl = '\\&' - sio.write(s.format(cell=c + '\\grtspacer')) + sio.write(s.format(cell=c + '\\I')) sio.write('\\& \\\\\n') # write table entries @@ -2380,389 +2421,6 @@ class GT(object): return sio.getvalue() -# def make_tikz_original(self, -# column_sep=4 / 8, # was 3/8 -# row_sep=1 / 8, -# container_env='table', -# extra_defs='', -# hrule=None, -# vrule=None, -# post_process='', -# label='', -# latex=None, -# sparsify=1): -# """ -# Write DataFrame to custom tikz matrix to allow greater control of -# formatting and insertion of horizontal and vertical divider lines - -# Estimates tabs from text width of fields (not so great if includes -# a lot of TeX macros) with a manual override available. Tabs gives -# the widths of each field in em (width of M) - -# Standard row height = 1.5em seems to work - set in meta. - -# first and last thick rules -# others below (Python, zero-based) row number, excluding title row - -# keyword arguments : value (no newlines in value) escape back slashes! -# ``#keyword...`` rows ignored -# passed in as a string to facilitate using them with %%pmt? - -# **Rules** - -# * hrule at i means below row i of the table. (1-based) Top, bottom and -# below index lines are inserted automatically. Top and bottom lines -# are thicker. -# * vrule at i means to the left of table column i (1-based); there will -# never be a rule to the far right...it looks plebby; remember you must -# include the index columns! - -# config.sparsify number of cols of multi index to config.sparsify - -# Issue: column with floats and spaces or missing causes problems (VaR, -# TVaR, EPD, mean and CV table) - -# From great.pres_maker.df_to_tikz - -# keyword args: - -# scale picks up self.config.tikz_scale; scale applied to whole -# table - default 0.717 -# height row height, rec. 1 (em) -# column_sep col sep in em -# row_sep row sep in em -# container_env table, figure or sidewaysfigure -# color color for text boxes (helps config.debugging) -# extra_defs TeX defintions and commands put at top of table, -# e.g., \\centering -# lines lines below these rows, -1 for next to last row -# etc.; list of ints -# post_process e.g., non-line commands put at bottom of table -# latex arguments after \begin{table}[latex] -# caption text for caption - -# Previous version see great.pres_maker -# Original version see: C:\\S\\TELOS\\CAS\\AR_Min_Bias\\cvs_to_md.py - -# :param column_sep: -# :param row_sep: -# :param figure: -# :param extra_defs: -# :param post_process: -# :param label: -# :return: -# """ -# # local variable - with all formatters already applied -# df = self.apply_formatters(self.raw_df.copy(), mode='raw') -# caption = self.caption -# label = self.label -# # prepare label and caption -# if label == '': -# lt = '' -# label = '' -# else: -# lt = label -# label = f'\\label{{{label}}}' -# if caption == '': -# if lt != '': -# logger.info( -# f'You have a label but no caption; the label {label} will be ignored.') -# caption = '% caption placeholder' -# else: -# caption = f'\\caption{{{self.caption}}}\n{label}' - -# if not df.columns.is_unique: -# # possible index/body column interaction -# raise ValueError('tikz routine requires unique column names') -# # {extra_defs} -# # centering handled by quarto -# header = """ -# \\begin{{{container_env}}}{latex} -# {caption} -# \\centering{{ -# \\begin{{tikzpicture}}[ -# auto, -# transform shape, -# nosep/.style={{inner sep=0}}, -# table/.style={{ -# matrix of nodes, -# row sep={row_sep}em, -# column sep={column_sep}em, -# nodes in empty cells, -# nodes={{rectangle, scale={scale}, text badly ragged {debug}}}, -# """ -# # put draw=blue!10 or so in nodes to see the node - -# footer = """ -# {post_process} - -# \\end{{tikzpicture}} -# }} % close centering -# \\end{{{container_env}}} -# """ - -# # always a good idea to do this...need to deal with underscores, % -# # and it handles index types that are not strings -# df = GT.clean_index(df) -# if not np.all([i == 'object' for i in df.dtypes]) and not df.empty: -# logger.warning('cols of df not all objects (expect all obs at this ' -# 'point): ', df.dtypes, sep='\n') -# # make sure percents are escaped, but not if already escaped -# df = df.replace(r"(?', 'c': '^'} -# # use df_aligners, at this point the index has been reset -# align = [] -# for n, i in zip(df.columns, self.df_aligners): -# if i == 'grt-left': -# align.append('l') -# elif i == 'grt-right': -# align.append('r') -# elif i == 'grt-center': -# align.append('c') -# else: -# align.append('l') - -# # start writing -# sio = StringIO() -# if latex is None: -# latex = '' -# else: -# latex = f'[{latex}]' -# debug = '' -# if self.config.debug: -# # color all boxes -# debug = ', draw=blue!10' -# else: -# debug = '' -# sio.write(header.format(container_env=container_env, -# caption=caption, -# extra_defs=extra_defs, -# scale=self.config.tikz_scale, -# column_sep=column_sep, -# row_sep=row_sep, -# latex=latex, -# debug=debug)) - -# # table header -# # title rows, start with the empty spacer row -# i = 1 -# sio.write( -# f'\trow {i}/.style={{nodes={{text=black, anchor=north, inner ysep=0, text height=0, text depth=0}}}},\n') -# for i in range(2, nr_columns + 2): -# sio.write( -# f'\trow {i}/.style={{nodes={{text=black, anchor=south, inner ysep=.2em, minimum height=1.3em, font=\\bfseries}}}},\n') - -# # write column spec -# for i, w, al in zip(range(1, len(align) + 1), tabs, align): -# # average char is only 0.48 of M -# # https://en.wikipedia.org/wiki/Em_(gtypography) -# if i == 1: -# # first column sets row height for entire row -# sio.write(f'\tcolumn {i:>2d}/.style={{' -# f'nodes={{align={ad[al]:<6s}}}, ' -# 'text height=0.9em, text depth=0.2em, ' -# f'inner xsep={column_sep}em, inner ysep=0, ' -# f'text width={max(2, 0.6 * w):.2f}em}},\n') -# else: -# sio.write(f'\tcolumn {i:>2d}/.style={{' -# f'nodes={{align={ad[al]:<6s}}}, nosep, text width={max(2, 0.6 * w):.2f}em}},\n') -# # extra col to right which enforces row height -# sio.write( -# f'\tcolumn {i+1:>2d}/.style={{text height=0.9em, text depth=0.2em, nosep, text width=0em}}') -# sio.write('\t}]\n') - -# sio.write("\\matrix ({matrix_name}) [table, ampersand replacement=\\&]{{\n".format( -# matrix_name=matrix_name)) - -# # body of table, starting with the column headers -# # spacer row -# nl = '' -# for cn, al in zip(df.columns, align): -# s = f'{nl} {{cell:{ad2[al]}{colw[cn]}s}} ' -# nl = '\\&' -# sio.write(s.format(cell=' ')) -# # include the blank extra last column -# sio.write('\\& \\\\\n') -# # write header rows (again, issues with multi index) -# mi_vrules = {} -# sparse_columns = {} -# if isinstance(df.columns, pd.MultiIndex): -# for lvl in range(len(df.columns.levels)): -# nl = '' -# sparse_columns[lvl], mi_vrules[lvl] = GT.sparsify_mi(df.columns.get_level_values(lvl), -# lvl == len(df.columns.levels) - 1) -# for cn, c, al in zip(df.columns, sparse_columns[lvl], align): -# # c = wfloat_format(c) -# s = f'{nl} {{cell:{ad2[al]}{colw[cn]}s}} ' -# nl = '\\&' -# sio.write(s.format(cell=c + '\\grtspacer')) -# # include the blank extra last column -# sio.write('\\& \\\\\n') -# else: -# nl = '' -# for c, al in zip(df.columns, align): -# # c = wfloat_format(c) -# s = f'{nl} {{cell:{ad2[al]}{colw[c]}s}} ' -# nl = '\\&' -# sio.write(s.format(cell=c + '\\grtspacer')) -# sio.write('\\& \\\\\n') - -# # write table entries -# for idx, row in df.iterrows(): -# nl = '' -# for c, cell, al in zip(df.columns, row, align): -# # cell = wfloat_format(cell) -# s = f'{nl} {{cell:{ad2[al]}{colw[c]}s}} ' -# nl = '\\&' -# sio.write(s.format(cell=cell)) -# # if c=='p': -# # print('COLp', cell, type(cell), s, s.format(cell=cell)) -# sio.write('\\& \\\\\n') -# sio.write(f'}};\n\n') - -# # decorations and post processing - horizontal and vertical lines -# nr, nc = df.shape -# # add for the index and the last row plus 1 for the added spacer row at the top -# nr += nr_columns + 1 -# # always include top and bottom -# # you input a table row number and get a line below it; it is implemented as a line ABOVE the next row -# # function to convert row numbers to TeX table format (edge case on last row -1 is nr and is caught, -2 -# # is below second to last row = above last row) -# # shift down extra 1 for the spacer row at the top -# def python_2_tex(x): return x + nr_columns + \ -# 2 if x >= 0 else nr + x + 3 -# tb_rules = [nr_columns + 1, python_2_tex(-1)] -# if hrule: -# hrule = set(map(python_2_tex, hrule)).union(tb_rules) -# else: -# hrule = list(tb_rules) -# logger.debug(f'hlines: {hrule}') - -# # why -# yshift = row_sep / 2 -# xshift = -column_sep / 2 -# descender_proportion = 0.25 - -# # top rule is special -# ls = 'thick' -# ln = 1 -# sio.write( -# f'\\path[draw, {ls}] ({matrix_name}-{ln}-1.south west) -- ({matrix_name}-{ln}-{nc+1}.south east);\n') - -# for ln in hrule: -# ls = 'thick' if ln == nr + nr_columns + \ -# 1 else ('semithick' if ln == 1 + nr_columns else 'very thin') -# if ln < nr: -# # line above TeX row ln+1 that exists -# sio.write(f'\\path[draw, {ls}] ([yshift={-yshift}em]{matrix_name}-{ln}-1.south west) -- ' -# f'([yshift={-yshift}em]{matrix_name}-{ln}-{nc+1}.south east);\n') -# else: -# # line above row below bottom = line below last row -# # descenders are 200 to 300 below baseline -# ln = nr -# sio.write(f'\\path[draw, thick] ([yshift={-descender_proportion-yshift}em]{matrix_name}-{ln}-1.base west) -- ' -# f'([yshift={-descender_proportion-yshift}em]{matrix_name}-{ln}-{nc+1}.base east);\n') - -# # if multi index put in lines within the index TODO make this better! -# if nr_columns > 1: -# for ln in range(2, nr_columns + 1): -# sio.write(f'\\path[draw, very thin] ([xshift={xshift}em, yshift={-yshift}em]' -# f'{matrix_name}-{ln}-{nc_index+1}.south west) -- ' -# f'([yshift={-yshift}em]{matrix_name}-{ln}-{nc+1}.south east);\n') - -# written = set(range(1, nc_index + 1)) -# if vrule and self.show_index: -# # to left of col, 1 based, includes index -# # write these first -# # TODO fix madness vrule is to the left, mi_vrules are to the right... -# ls = 'very thin' -# for cn in vrule: -# if cn not in written: -# sio.write(f'\\path[draw, {ls}] ([xshift={xshift}em]{matrix_name}-1-{cn}.south west) -- ' -# f'([yshift={-descender_proportion-yshift}em, xshift={xshift}em]{matrix_name}-{nr}-{cn}.base west);\n') -# written.add(cn - 1) - -# if len(mi_vrules) > 0: -# logger.debug( -# f'Generated vlines {mi_vrules}; already written {written}') -# # vertical rules for the multi index -# # these go to the RIGHT of the relevant column and reflect the index columns already -# # mi_vrules = {level of index: [list of vrule columns] -# # written keeps track of which vrules have been done already; start by cutting out the index columns -# ls = 'ultra thin' -# for k, cols in mi_vrules.items(): -# # don't write the lowest level -# if k == len(mi_vrules) - 1: -# break -# for cn in cols: -# if cn in written: -# pass -# else: -# written.add(cn) -# top = k + 1 -# if top == 0: -# sio.write(f'\\path[draw, {ls}] ([xshift={-xshift}em]{matrix_name}-{top}-{cn}.south east) -- ' -# f'([yshift={-descender_proportion-yshift}em, xshift={-xshift}em]{matrix_name}-{nr}-{cn}.base east);\n') -# else: -# sio.write(f'\\path[draw, {ls}] ([xshift={-xshift}em, yshift={-yshift}em]{matrix_name}-{top}-{cn}.south east) -- ' -# f'([yshift={-descender_proportion-yshift}em, xshift={-xshift}em]{matrix_name}-{nr}-{cn}.base east);\n') - -# sio.write(footer.format(container_env=container_env, -# post_process=post_process)) - -# return sio.getvalue() - @staticmethod def sparsify(df, cs): out = df.copy() diff --git a/greater_tables/testdf.py b/greater_tables/testdf.py index e4927d0..6782a7b 100644 --- a/greater_tables/testdf.py +++ b/greater_tables/testdf.py @@ -251,7 +251,8 @@ class TestDataFrameFactory: return pd.Series(self.rng.normal(loc=0.5, scale=0.35, size=n)) if dtype == 'l': # log float (greater range) - return pd.Series(np.exp(self.rng.normal(loc=-4 / 2 + 4, scale=4, size=n))) + scale = 10. + return pd.Series(np.exp(self.rng.normal(loc=-scale**2 / 2 + 15, scale=scale, size=n))) if dtype == 'v': # log float (greater range) sc = 5 diff --git a/greater_tables/tex_svg.py b/greater_tables/tex_svg.py index bb044e8..3b8bfdd 100644 --- a/greater_tables/tex_svg.py +++ b/greater_tables/tex_svg.py @@ -19,6 +19,8 @@ class TikzProcessor: # Full TeX preamble to generate a .fmt if needed _tex_template_full = r"""\documentclass[10pt, border=5mm]{standalone} \usepackage{amsfonts} +\usepackage{amsmath} +\usepackage{mathrsfs} \usepackage{url} \usepackage{tikz} \usepackage{color} @@ -33,7 +35,7 @@ class TikzProcessor: # Minimal template to embed user tikz _tex_template = r""" -\newcommand{{\grtspacer}}{{\vphantom{{lp}}}} +\newcommand{{\I}}{{\vphantom{{lp}}}} % fka grtspacer \def\dfrac{{\displaystyle\frac}} \def\dint{{\displaystyle\int}} \begin{{document}} @@ -65,13 +67,17 @@ class TikzProcessor: print('TikzProcessor: building TeX format fmt file...', end ='') tmp = self.out_path / 'tikz_format.tex' tmp.write_text(self._tex_template_full, encoding='utf-8') - self.run_command([ + cmd = [ 'pdflatex', f'-ini', f'-jobname={self.format_file.stem}', '&pdflatex', tmp.name, - ], raise_on_error=True, cwd=self.out_path) + ] + print(f'Running {" ".join(cmd)} to build format file...') + (self.file_path.parent / 'make_format.bat').write_text(" ".join(cmd), encoding='utf-8') + self.run_command(cmd, raise_on_error=True, cwd=self.out_path) + # tidy up ... to some extent # tmp.unlink() (self.out_path / f'{self.format_file.stem}.log').unlink() print('...success...format file built', self.format_file.resolve()) @@ -100,14 +106,12 @@ class TikzProcessor: ] if self.debug: print("Running:", " ".join(tex_cmd)) + (tex_path.parent / 'make_tikz.bat').write_text(" ".join(tex_cmd), encoding='utf-8') if self.run_command(tex_cmd): raise ValueError('TeX failed to compile, not pdf or svg output.') # no tidying up else: - # continue - - (tex_path.parent / 'make_tikz.bat').write_text(" ".join(tex_cmd), encoding='utf-8') - + # no error: continue svg_cmd = [ # 'C:\\temp\\pdf2svg-windows\\dist-64bits\\pdf2svg', 'pdf2svg', diff --git a/pyproject.toml b/pyproject.toml index 70fba1d..5757a28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "wheel"] # Tools needed to build the package +requires = ["setuptools", "build"] build-backend = "setuptools.build_meta" [project] @@ -8,7 +8,8 @@ dynamic = ["version"] description = "Perfect tables from pandas dataframes." authors = [{name = "Stephen J Mildehall", email = "mynl@me.com" }] readme = {file = "README.md", content-type = "text/markdown"} -license = { text = "MIT" } +license = "MIT" +license-files = ["LICENSE"] requires-python = ">=3.10" dependencies = [ "bs4", @@ -21,14 +22,13 @@ dependencies = [ ] classifiers = [ "Development Status :: 4 - Beta", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Topic :: Office/Business" ] [tool.setuptools.packages.find] -include = ["greater_tables"] +include = ["greater_tables", "greater_tables.data"] exclude = ["img", "tests", "docs"] [tool.setuptools.package-data]