From b56073d7a23a3513ee3fa47e105e3de96ddd8e6a Mon Sep 17 00:00:00 2001 From: jrycw Date: Sat, 27 Jul 2024 10:00:01 +0800 Subject: [PATCH 001/150] Consolidate ordered list code using new `_create_ordered_list()` function --- great_tables/_gt_data.py | 6 ++++-- great_tables/_spanners.py | 6 +++--- great_tables/_tbl_data.py | 17 ++++++----------- great_tables/_utils.py | 6 +++++- tests/test_utils.py | 14 ++++++++++++++ 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index 12e37529d..e8fb38160 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -24,7 +24,7 @@ to_list, validate_frame, ) -from ._utils import _str_detect +from ._utils import _str_detect, _create_ordered_list if TYPE_CHECKING: from ._helpers import Md, Html, UnitStr, Text @@ -577,7 +577,9 @@ def from_data(cls, data, rowname_col: str | None = None, groupname_col: str | No row_info = [RowInfo(*i) for i in zip(row_indices, group_id, row_names)] # create groups, and ensure they're ordered by first observed - group_names = list({row.group_id: True for row in row_info if row.group_id is not None}) + group_names = _create_ordered_list( + row.group_id for row in row_info if row.group_id is not None + ) group_rows = GroupRows(data, group_key=groupname_col).reorder(group_names) return cls(row_info, group_rows) diff --git a/great_tables/_spanners.py b/great_tables/_spanners.py index 51f804d3c..414a577b0 100644 --- a/great_tables/_spanners.py +++ b/great_tables/_spanners.py @@ -7,6 +7,7 @@ from ._locations import resolve_cols_c from ._tbl_data import SelectExpr from ._text import Text +from ._utils import _create_ordered_list if TYPE_CHECKING: from ._gt_data import Boxhead @@ -171,10 +172,9 @@ def tab_spanner( # get column names associated with selected spanners ---- _vars = [span.vars for span in data._spanners if span.spanner_id in spanner_ids] - spanner_column_names = list({k: True for k in itertools.chain(*_vars)}) - - column_names = list({k: True for k in [*selected_column_names, *spanner_column_names]}) + spanner_column_names = _create_ordered_list(itertools.chain(*_vars)) + column_names = _create_ordered_list([*selected_column_names, *spanner_column_names]) # combine columns names and those from spanners ---- # get spanner level ---- diff --git a/great_tables/_tbl_data.py b/great_tables/_tbl_data.py index 03ff916ba..a62a28f59 100644 --- a/great_tables/_tbl_data.py +++ b/great_tables/_tbl_data.py @@ -323,6 +323,7 @@ def _( def _(data: PlDataFrame, expr: Union[list[str], _selector_proxy_], strict: bool = True) -> _NamePos: # TODO: how to annotate type of a polars selector? # Seems to be polars.selectors._selector_proxy_. + from ._utils import _create_ordered_list import polars.selectors as cs from polars import Expr @@ -352,20 +353,14 @@ def _(data: PlDataFrame, expr: Union[list[str], _selector_proxy_], strict: bool # validate all entries ---- _validate_selector_list(all_selectors, **expand_opts) - # perform selection ---- - # use a dictionary, with values set to True, as an ordered list. - selection_set = {} - # this should be equivalent to reducing selectors using an "or" operator, # which isn't possible when there are selectors mixed with expressions # like pl.col("some_col") - for sel in all_selectors: - new_cols = cs.expand_selector(data, sel, **expand_opts) - for col_name in new_cols: - selection_set[col_name] = True - - final_columns = list(selection_set) - + final_columns = _create_ordered_list( + col_name + for sel in all_selectors + for col_name in cs.expand_selector(data, sel, **expand_opts) + ) else: if not isinstance(expr, (cls_selector, Expr)): raise TypeError(f"Unsupported selection expr type: {type(expr)}") diff --git a/great_tables/_utils.py b/great_tables/_utils.py index ad364445c..cb4ecea21 100644 --- a/great_tables/_utils.py +++ b/great_tables/_utils.py @@ -85,7 +85,11 @@ def _str_scalar_to_list(x: str) -> list[str]: def _unique_set(x: list[Any] | None) -> list[Any] | None: if x is None: return None - return list({k: True for k in x}) + return _create_ordered_list(x) + + +def _create_ordered_list(x: Iterable[Any]) -> list[Any]: + return list(dict.fromkeys(x).keys()) def _as_css_font_family_attr(fonts: list[str], value_only: bool = False) -> str: diff --git a/tests/test_utils.py b/tests/test_utils.py index 0c3ea4111..30ebef98c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,6 +6,7 @@ _assert_str_list, _assert_str_scalar, _collapse_list_elements, + _create_ordered_list, _insert_into_list, _match_arg, _str_scalar_to_list, @@ -118,6 +119,19 @@ def test_unique_set(): assert len(result) == 2 +@pytest.mark.parametrize( + "iterable, ordered_list", + [ + (["1", "2", "3"], ["1", "2", "3"]), + (["1", "3", "2", "3", "1"], ["1", "3", "2"]), + ((1, 3, 2, 3, 1, 1, 3, 2, 2), [1, 3, 2]), + (iter("223311"), ["2", "3", "1"]), + ], +) +def test_create_ordered_list(iterable, ordered_list): + assert _create_ordered_list(iterable) == ordered_list + + def test_collapse_list_elements(): lst = ["a", "b", "c"] assert _collapse_list_elements(lst) == "abc" From 200fd724c30aacae96f069764da3f503c07ed157 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 14 May 2024 11:11:54 -0400 Subject: [PATCH 002/150] WIP allow for granular section restyling via convenience api --- great_tables/_gt_data.py | 48 ++++- great_tables/_locations.py | 280 ++++++++++++++++++++++++----- great_tables/_utils_render_html.py | 177 +++++++++--------- great_tables/loc.py | 44 ++++- 4 files changed, 400 insertions(+), 149 deletions(-) diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index 12e37529d..fbe4b98d8 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from dataclasses import dataclass, field, replace from enum import Enum, auto -from typing import Any, Callable, Tuple, TypeVar, overload, TYPE_CHECKING +from typing import Any, Callable, Literal, Tuple, TypeVar, Union, overload, TYPE_CHECKING from typing_extensions import Self, TypeAlias @@ -847,10 +847,52 @@ class FootnotePlacement(Enum): right = auto() auto = auto() +LocHeaderName = Literal[ + "header", + "title", + "subtitle", +] +LocStubheadName = Literal[ + "stubhead", + "stubhead_label", +] +LocColumnLabelsName = Literal[ + "column_labels", + "spanner_label", + "column_label", +] +LocStubName = Literal[ + "stub", + "row_group_label", + "row_label", + "summary_label", +] +LocBodyName = Literal[ + "body", + "cell", + "summary" +] +LocFooterName = Literal[ + "footer", + "footnotes", + "source_notes", +] +LocUnknownName = Literal[ + "none", +] +LocName = Union[ + LocHeaderName, + LocStubheadName, + LocColumnLabelsName, + LocStubName, + LocBodyName, + LocFooterName, + LocUnknownName, +] @dataclass(frozen=True) class FootnoteInfo: - locname: str | None = None + locname: LocName | None = None grpname: str | None = None colname: str | None = None locnum: int | None = None @@ -867,7 +909,7 @@ class FootnoteInfo: @dataclass(frozen=True) class StyleInfo: - locname: str + locname: LocName locnum: int grpname: str | None = None colname: str | None = None diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 617bd7ecf..16f899b95 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -1,16 +1,16 @@ from __future__ import annotations import itertools -from dataclasses import dataclass +from dataclasses import dataclass, field from functools import singledispatch -from typing import TYPE_CHECKING, Any, Callable, Literal +from typing import TYPE_CHECKING, Any, Callable, Literal, Union from typing_extensions import TypeAlias # note that types like Spanners are only used in annotations for concretes of the # resolve generic, but we need to import at runtime, due to singledispatch looking # up annotations -from ._gt_data import ColInfoTypeEnum, FootnoteInfo, FootnotePlacement, GTData, Spanners, StyleInfo +from ._gt_data import ColInfoTypeEnum, FootnoteInfo, FootnotePlacement, GTData, Spanners, StyleInfo, LocName, LocHeaderName, LocStubheadName, LocColumnLabelsName, LocStubName, LocBodyName, LocFooterName, LocUnknownName from ._styles import CellStyle from ._tbl_data import PlDataFrame, PlExpr, eval_select, eval_transform @@ -35,51 +35,84 @@ class CellPos: column: int row: int colname: str + rowname: str | None = None @dataclass class Loc: """A location.""" + groups: LocName @dataclass -class LocTitle(Loc): +class LocHeader(Loc): """A location for targeting the table title and subtitle.""" - groups: Literal["title", "subtitle"] + groups: LocHeaderName = "header" @dataclass -class LocStubhead(Loc): - groups: Literal["stubhead"] = "stubhead" +class LocTitle(Loc): + groups: Literal["title"] = "title" @dataclass -class LocColumnSpanners(Loc): - """A location for column spanners.""" +class LocSubTitle(Loc): + groups: Literal["subtitle"] = "subtitle" + - # TODO: these can also be tidy selectors - ids: list[str] +@dataclass +class LocStubhead(Loc): + """A location for targeting the table stubhead and stubhead label.""" + groups: LocStubheadName = "stubhead" + + +@dataclass +class LocStubheadLabel(Loc): + groups: Literal["stubhead_label"] = "stubhead_label" @dataclass class LocColumnLabels(Loc): - # TODO: these can be tidyselectors - columns: list[str] + """A location for column spanners and column labels.""" + + groups: LocColumnLabelsName = "column_labels" @dataclass -class LocRowGroups(Loc): - # TODO: these can be tidyselectors - groups: list[str] +class LocColumnLabel(Loc): + groups: Literal["column_label"] = "column_label" + columns: SelectExpr = None +@dataclass +class LocSpannerLabel(Loc): + """A location for column spanners.""" + groups: Literal["spanner_label"] = "spanner_label" + ids: SelectExpr = None + @dataclass class LocStub(Loc): - # TODO: these can be tidyselectors - # TODO: can this take integers? - rows: list[str] + """A location for targeting the table stub, row group labels, summary labels, and body.""" + + groups: Literal["stub"] = "stub" + rows: RowSelectExpr = None + +@dataclass +class LocRowGroupLabel(Loc): + groups: Literal["row_group_label"] = "row_group_label" + rows: RowSelectExpr = None + +@dataclass +class LocRowLabel(Loc): + groups: Literal["row_label"] = "row_label" + rows: RowSelectExpr = None + +@dataclass +class LocSummaryLabel(Loc): + groups: Literal["summary_label"] = "summary_label" + rows: RowSelectExpr = None @dataclass class LocBody(Loc): @@ -108,6 +141,8 @@ class LocBody(Loc): ------ See [`GT.tab_style()`](`great_tables.GT.tab_style`). """ + groups: LocBodyName = "body" + columns: SelectExpr = None rows: RowSelectExpr = None @@ -115,28 +150,14 @@ class LocBody(Loc): @dataclass class LocSummary(Loc): # TODO: these can be tidyselectors - groups: list[str] - columns: list[str] - rows: list[str] - - -@dataclass -class LocGrandSummary(Loc): - # TODO: these can be tidyselectors - columns: list[str] - rows: list[str] - - -@dataclass -class LocStubSummary(Loc): - # TODO: these can be tidyselectors - groups: list[str] - rows: list[str] + groups: LocName = "summary" + columns: SelectExpr = None + rows: RowSelectExpr = None @dataclass -class LocStubGrandSummary(Loc): - rows: list[str] +class LocFooter(Loc): + groups: LocFooterName = "footer" @dataclass @@ -355,7 +376,7 @@ def resolve(loc: Loc, *args: Any, **kwargs: Any) -> Loc | list[CellPos]: @resolve.register -def _(loc: LocColumnSpanners, spanners: Spanners) -> LocColumnSpanners: +def _(loc: LocSpannerLabel, spanners: Spanners) -> LocSpannerLabel: # unique labels (with order preserved) spanner_ids = [span.spanner_id for span in spanners] @@ -363,7 +384,21 @@ def _(loc: LocColumnSpanners, spanners: Spanners) -> LocColumnSpanners: resolved_spanners = [spanner_ids[idx] for idx in resolved_spanners_idx] # Create a list object - return LocColumnSpanners(ids=resolved_spanners) + return LocSpannerLabel(ids=resolved_spanners) + + +@resolve.register +def _(loc: LocColumnLabel, data: GTData) -> list[CellPos]: + cols = resolve_cols_i(data=data, expr=loc.columns) + cell_pos = [CellPos(col[1], 0, colname=col[0]) for col in cols] + return cell_pos + + +@resolve.register +def _(loc: LocRowLabel, data: GTData) -> list[CellPos]: + rows = resolve_rows_i(data=data, expr=loc.rows) + cell_pos = [CellPos(0, row[1], colname="", rowname=row[0]) for row in rows] + return cell_pos @resolve.register @@ -383,6 +418,24 @@ def _(loc: LocBody, data: GTData) -> list[CellPos]: # Style generic ======================================================================== +# LocHeader +# LocTitle +# LocSubTitle +# LocStubhead +# LocStubheadLabel +# LocColumnLabels +# LocColumnLabel +# LocSpannerLabel +# LocStub +# LocRowGroupLabel +# LocRowLabel +# LocSummaryLabel +# LocBody +# LocSummary +# LocFooter +# LocFootnotes +# LocSourceNotes + @singledispatch def set_style(loc: Loc, data: GTData, style: list[str]) -> GTData: """Set style for location.""" @@ -390,20 +443,121 @@ def set_style(loc: Loc, data: GTData, style: list[str]) -> GTData: @set_style.register -def _(loc: LocTitle, data: GTData, style: list[CellStyle]) -> GTData: +def _(loc: LocHeader, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) # set ---- - if loc.groups == "title": - info = StyleInfo(locname="title", locnum=1, styles=style) + if loc.groups == "header": + info = StyleInfo(locname="header", locnum=1, styles=style) + elif loc.groups == "title": + info = StyleInfo(locname="title", locnum=2, styles=style) elif loc.groups == "subtitle": - info = StyleInfo(locname="subtitle", locnum=2, styles=style) + info = StyleInfo(locname="subtitle", locnum=3, styles=style) else: raise ValueError(f"Unknown title group: {loc.groups}") + return data._replace(_styles=data._styles + [info]) + + +@set_style.register +def _(loc: LocTitle, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + return data._replace(_styles=data._styles + [StyleInfo(locname="title", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocSubTitle, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + return data._replace(_styles=data._styles + [StyleInfo(locname="subtitle", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocStubhead, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + return data._replace(_styles=data._styles + [StyleInfo(locname="stubhead", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocStubheadLabel, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + return data._replace(_styles=data._styles + [StyleInfo(locname="stubhead_label", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocColumnLabels, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + return data._replace(_styles=data._styles + [StyleInfo(locname="column_labels", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocColumnLabel, data: GTData, style: list[CellStyle]) -> GTData: + positions: list[CellPos] = resolve(loc, data) + + # evaluate any column expressions in styles + styles = [entry._evaluate_expressions(data._tbl_data) for entry in style] + + all_info: list[StyleInfo] = [] + for col_pos in positions: + crnt_info = StyleInfo( + locname="column_label", locnum=2, colname=col_pos.colname, rownum=col_pos.row, styles=styles + ) + all_info.append(crnt_info) + return data._replace(_styles=data._styles + all_info) - return data._styles.append(info) + +@set_style.register +def _(loc: LocSpannerLabel, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + # TODO resolve + return data._replace(_styles=data._styles + [StyleInfo(locname="column_labels", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocStub, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + return data._replace(_styles=data._styles + [StyleInfo(locname="stub", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocRowGroupLabel, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + # TODO resolve + return data._replace(_styles=data._styles + [StyleInfo(locname="row_group_label", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocRowLabel, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + # TODO resolve + return data._replace(_styles=data._styles + [StyleInfo(locname="row_label", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocSummaryLabel, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + # TODO resolve + return data._replace(_styles=data._styles + [StyleInfo(locname="summary_label", locnum=1, styles=style)]) @set_style.register @@ -417,13 +571,47 @@ def _(loc: LocBody, data: GTData, style: list[CellStyle]) -> GTData: for col_pos in positions: row_styles = [entry._from_row(data._tbl_data, col_pos.row) for entry in style_ready] crnt_info = StyleInfo( - locname="data", locnum=5, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles + locname="cell", locnum=5, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles ) all_info.append(crnt_info) return data._replace(_styles=data._styles + all_info) +@set_style.register +def _(loc: LocSummary, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + return data._replace(_styles=data._styles + [StyleInfo(locname="summary", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocFooter, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + return data._replace(_styles=data._styles + [StyleInfo(locname="footer", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocFootnotes, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + # TODO resolve + return data._replace(_styles=data._styles + [StyleInfo(locname="footnotes", locnum=1, styles=style)]) + + +@set_style.register +def _(loc: LocSourceNotes, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + # TODO resolve + return data._replace(_styles=data._styles + [StyleInfo(locname="source_notes", locnum=1, styles=style)]) + + # Set footnote generic ================================================================= diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 5bacdc1e7..c90f2001a 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -1,17 +1,32 @@ from __future__ import annotations -from itertools import chain +from itertools import chain, groupby +from math import isnan from typing import Any, cast from great_tables._spanners import spanners_print_matrix from htmltools import HTML, TagList, css, tags -from ._gt_data import GTData +from ._gt_data import GTData, Styles from ._tbl_data import _get_cell, cast_frame_to_string, n_rows, replace_null_frame from ._text import StringBuilder, _process_text, _process_text_id from ._utils import heading_has_subtitle, heading_has_title, seq_groups +def _flatten_styles(styles: Styles, wrap: bool = False) -> str: + # flatten all StyleInfo.styles lists + style_entries = list(chain(*[x.styles for x in styles])) + rendered_styles = [el._to_html_style() for el in style_entries] + + # TODO dedupe rendered styles in sequence + + if wrap: + style_to_return = f'style="{" ".join(rendered_styles)}"' + " " + else: + style_to_return = " ".join(rendered_styles) + return style_to_return + + def create_heading_component_h(data: GTData) -> StringBuilder: result = StringBuilder() @@ -32,6 +47,14 @@ def create_heading_component_h(data: GTData) -> StringBuilder: title = _process_text(title) subtitle = _process_text(subtitle) + # Filter list of StyleInfo for the various header components + styles_header = [x for x in data._styles if x.locname == "header"] + styles_title = [x for x in data._styles if x.locname == "title"] + styles_subtitle = [x for x in data._styles if x.locname == "subtitle"] + header_style = _flatten_styles(styles_header, wrap=True) if styles_header else "" + title_style = _flatten_styles(styles_title, wrap=True) if styles_title else "" + subtitle_style = _flatten_styles(styles_subtitle, wrap=True) if styles_subtitle else "" + # Get the effective number of columns, which is number of columns # that will finally be rendered accounting for the stub layout n_cols_total = data._boxhead._get_effective_number_of_columns( @@ -41,20 +64,20 @@ def create_heading_component_h(data: GTData) -> StringBuilder: if has_subtitle: heading = f""" - {title} + {title} - {subtitle} + {subtitle} """ else: heading = f""" - {title} + {title} """ result.append(heading) - return StringBuilder('', result, "\n") + return StringBuilder(f"""""", result, "\n") def create_columns_component_h(data: GTData) -> str: @@ -70,8 +93,6 @@ def create_columns_component_h(data: GTData) -> str: # Get necessary data objects for composing the column labels and spanners stubh = data._stubhead - # TODO: skipping styles for now - # styles_tbl = dt_styles_get(data = data) boxhead = data._boxhead # TODO: The body component of the table is only needed for determining RTL alignment @@ -100,13 +121,12 @@ def create_columns_component_h(data: GTData) -> str: # Get the column headings headings_info = boxhead._get_default_columns() - # TODO: Skipping styles for now - # Get the style attrs for the stubhead label - # stubhead_style_attrs = subset(styles_tbl, locname == "stubhead") - # Get the style attrs for the spanner column headings - # spanner_style_attrs = subset(styles_tbl, locname == "columns_groups") - # Get the style attrs for the spanner column headings - # column_style_attrs = subset(styles_tbl, locname == "columns_columns") + # Filter list of StyleInfo for the various stubhead and column labels components + styles_stubhead = [x for x in data._styles if x.locname == "stubhead"] + styles_stubhead_label = [x for x in data._styles if x.locname == "stubhead_label"] + styles_column_labels = [x for x in data._styles if x.locname == "column_labels"] + styles_spanner_label = [x for x in data._styles if x.locname == "spanner_label"] + styles_column_label = [x for x in data._styles if x.locname == "column_label"] # If columns are present in the stub, then replace with a set stubhead label or nothing if len(stub_layout) > 0 and stubh is not None: @@ -127,18 +147,13 @@ def create_columns_component_h(data: GTData) -> str: if spanner_row_count == 0: # Create the cell for the stubhead label if len(stub_layout) > 0: - stubhead_style = None - # FIXME: Ignore styles for now - # if stubhead_style_attrs is not None and len(stubhead_style_attrs) > 0: - # stubhead_style = stubhead_style_attrs[0].html_style - table_col_headings.append( tags.th( HTML(_process_text(stub_label)), class_=f"gt_col_heading gt_columns_bottom_border gt_{stubhead_label_alignment}", rowspan="1", colspan=len(stub_layout), - style=stubhead_style, + style=_flatten_styles(styles_stubhead + styles_stubhead_label), scope="colgroup" if len(stub_layout) > 1 else "col", id=_process_text_id(stub_label), ) @@ -146,13 +161,8 @@ def create_columns_component_h(data: GTData) -> str: # Create the headings in the case where there are no spanners at all ------------------------- for info in headings_info: - # NOTE: Ignore styles for now - # styles_column = subset(column_style_attrs, colnum == i) - # - # Convert the code above this comment from R to valid python - # if len(styles_column) > 0: - # column_style = styles_column[0].html_style - column_style = None + # Filter by column label / id, join with overall column labels style + styles_i = [x for x in styles_column_label if x.colname == info.var] table_col_headings.append( tags.th( @@ -160,7 +170,7 @@ def create_columns_component_h(data: GTData) -> str: class_=f"gt_col_heading gt_columns_bottom_border gt_{info.defaulted_align}", rowspan=1, colspan=1, - style=column_style, + style=_flatten_styles(styles_column_labels + styles_i), scope="col", id=_process_text_id(info.column_label), ) @@ -199,20 +209,13 @@ def create_columns_component_h(data: GTData) -> str: # Create the cell for the stubhead label if len(stub_layout) > 0: - # NOTE: Ignore styles for now - # if len(stubhead_style_attrs) > 0: - # stubhead_style = stubhead_style_attrs.html_style - # else: - # stubhead_style = None - stubhead_style = None - level_1_spanners.append( tags.th( HTML(_process_text(stub_label)), class_=f"gt_col_heading gt_columns_bottom_border gt_{str(stubhead_label_alignment)}", rowspan=2, colspan=len(stub_layout), - style=stubhead_style, + style=_flatten_styles(styles_stubhead + styles_stubhead_label), scope="colgroup" if len(stub_layout) > 1 else "col", id=_process_text_id(stub_label), ) @@ -232,14 +235,8 @@ def create_columns_component_h(data: GTData) -> str: for ii, (span_key, h_info) in enumerate(zip(spanner_col_names, headings_info)): if spanner_ids[level_1_index][span_key] is None: - # NOTE: Ignore styles for now - # styles_heading = filter( - # lambda x: x.get('locname') == "columns_columns" and x.get('colname') == headings_vars[i], - # styles_tbl if 'styles_tbl' in locals() else [] - # ) - # - # heading_style = next(styles_heading, {}).get('html_style', None) - heading_style = None + # Filter by column label / id, join with overall column labels style + styles_i = [x for x in styles_column_label if x.colname == h_info.var] # Get the alignment values for the first set of column labels first_set_alignment = h_info.defaulted_align @@ -251,7 +248,7 @@ def create_columns_component_h(data: GTData) -> str: class_=f"gt_col_heading gt_columns_bottom_border gt_{str(first_set_alignment)}", rowspan=2, colspan=1, - style=heading_style, + style=_flatten_styles(styles_column_labels + styles_i), scope="col", id=_process_text_id(h_info.column_label), ) @@ -261,21 +258,9 @@ def create_columns_component_h(data: GTData) -> str: # If colspans[i] == 0, it means that a previous cell's # `colspan` will cover us if colspans[ii] > 0: - # NOTE: Ignore styles for now - # FIXME: this needs to be rewritten - # styles_spanners = filter( - # spanner_style_attrs, - # locname == "columns_groups", - # grpname == spanner_ids[level_1_index, ][i] - # ) - # - # spanner_style = - # if (nrow(styles_spanners) > 0) { - # styles_spanners$html_style - # } else { - # NULL - # } - spanner_style = None + # Filter by column label / id, join with overall column labels style + # TODO check this filter logic + styles_i = [x for x in styles_spanner_label if x.grpname == spanner_ids_level_1_index[ii]] level_1_spanners.append( tags.th( @@ -286,7 +271,7 @@ def create_columns_component_h(data: GTData) -> str: class_="gt_center gt_columns_top_border gt_column_spanner_outer", rowspan=1, colspan=colspans[ii], - style=spanner_style, + style=_flatten_styles(styles_column_labels + styles_i), scope="colgroup" if colspans[ii] > 1 else "col", id=_process_text_id(spanner_ids_level_1_index[ii]), ) @@ -304,18 +289,9 @@ def create_columns_component_h(data: GTData) -> str: spanned_column_labels = [] for j in range(len(remaining_headings)): - # Skip styles for now - # styles_remaining = styles_tbl[ - # (styles_tbl["locname"] == "columns_columns") & - # (styles_tbl["colname"] == remaining_headings[j]) - # ] - # - # remaining_style = ( - # styles_remaining["html_style"].values[0] - # if len(styles_remaining) > 0 - # else None - # ) - remaining_style = None + # Filter by column label / id, join with overall column labels style + # TODO check this filter logic + styles_i = [x for x in styles_column_label if x.colname == remaining_headings[j]] remaining_alignment = boxhead._get_boxhead_get_alignment_by_var( var=remaining_headings[j] @@ -327,7 +303,7 @@ def create_columns_component_h(data: GTData) -> str: class_=f"gt_col_heading gt_columns_bottom_border gt_{remaining_alignment}", rowspan=1, colspan=1, - style=remaining_style, + style=_flatten_styles(styles_column_labels + styles_i), scope="col", id=_process_text_id(remaining_headings_labels[j]), ) @@ -362,18 +338,9 @@ def create_columns_component_h(data: GTData) -> str: for colspan, span_label in zip(colspans, spanners_row.values()): if colspan > 0: - # Skip styles for now - # styles_spanners = styles_tbl[ - # (styles_tbl["locname"] == "columns_groups") & - # (styles_tbl["grpname"] in spanners_vars) - # ] - # - # spanner_style = ( - # styles_spanners["html_style"].values[0] - # if len(styles_spanners) > 0 - # else None - # ) - spanner_style = None + # Filter by column label / id, join with overall column labels style + # TODO check this filter logic + styles_i = [x for x in styles_column_label if x.grpname in (colspan, span_label)] if span_label: span = tags.span( @@ -389,7 +356,7 @@ def create_columns_component_h(data: GTData) -> str: class_="gt_center gt_columns_bottom_border gt_columns_top_border gt_column_spanner_outer", rowspan=1, colspan=colspan, - style=spanner_style, + style=_flatten_styles(styles_column_labels + styles_i), scope="colgroup" if colspan > 1 else "col", ) ) @@ -403,6 +370,8 @@ def create_columns_component_h(data: GTData) -> str: rowspan=1, colspan=len(stub_layout), scope="colgroup" if len(stub_layout) > 1 else "col", + # TODO check if ok to just use base styling? + style=_flatten_styles(styles_column_labels), ), ) @@ -412,6 +381,8 @@ def create_columns_component_h(data: GTData) -> str: tags.tr( level_i_spanners, class_="gt_col_headings gt_spanner_row", + # TODO check if ok to just use base styling? + style=_flatten_styles(styles_column_labels), ) ), ) @@ -429,8 +400,17 @@ def create_body_component_h(data: GTData) -> str: _str_orig_data = cast_frame_to_string(data._tbl_data) tbl_data = replace_null_frame(data._body.body, _str_orig_data) - # Filter list of StyleInfo to only those that apply to the body (where locname="data") - styles_body = [x for x in data._styles if x.locname == "data"] + # Filter list of StyleInfo to only those that apply to the stub + styles_stub = [x for x in data._styles if x.locname == "stub"] + styles_row_group_label = [x for x in data._styles if x.locname == "row_group_label"] + styles_row_label = [x for x in data._styles if x.locname == "row_label"] + styles_summary_label = [x for x in data._styles if x.locname == "summary_label"] + stub_style = _flatten_styles(styles_stub, wrap=True)if styles_stub else "" + + # Filter list of StyleInfo to only those that apply to the body + styles_cells = [x for x in data._styles if x.locname == "cell"] + styles_body = [x for x in data._styles if x.locname == "body"] + styles_summary = [x for x in data._styles if x.locname == "summary"] # Get the default column vars column_vars = data._boxhead._get_default_columns() @@ -493,20 +473,17 @@ def create_body_component_h(data: GTData) -> str: cell_alignment = colinfo.defaulted_align # Get the style attributes for the current cell by filtering the - # `styles_body` list for the current row and column - styles_i = [x for x in styles_body if x.rownum == i and x.colname == colinfo.var] + # `styles_cells` list for the current row and column + styles_i = [x for x in styles_cells if x.rownum == i and x.colname == colinfo.var] # Develop the `style` attribute for the current cell if len(styles_i) > 0: - # flatten all StyleInfo.styles lists - style_entries = list(chain(*[x.styles for x in styles_i])) - rendered_styles = [el._to_html_style() for el in style_entries] - cell_styles = f'style="{" ".join(rendered_styles)}"' + " " + cell_styles = _flatten_styles(styles_i, wrap=True) else: cell_styles = "" if is_stub_cell: - body_cells.append(f""" {cell_str}""") + body_cells.append(f""" {cell_str}""") else: body_cells.append( f""" {cell_str}""" @@ -525,6 +502,9 @@ def create_body_component_h(data: GTData) -> str: def create_source_notes_component_h(data: GTData) -> str: source_notes = data._source_notes + # Filter list of StyleInfo to only those that apply to the source notes + styles_source_notes = [x for x in data._styles if x.locname == "source_notes"] + # If there are no source notes, then return an empty string if source_notes == []: return "" @@ -591,6 +571,9 @@ def create_source_notes_component_h(data: GTData) -> str: def create_footnotes_component_h(data: GTData): + # Filter list of StyleInfo to only those that apply to the footnotes + styles_footnotes = [x for x in data._styles if x.locname == "footnotes"] + return "" diff --git a/great_tables/loc.py b/great_tables/loc.py index eec0149b7..6c4e68951 100644 --- a/great_tables/loc.py +++ b/great_tables/loc.py @@ -1,9 +1,47 @@ from __future__ import annotations from ._locations import ( - LocBody as body, - LocStub as stub, + # Header elements + LocHeader as header, + LocTitle as title, + LocSubTitle as subtitle, + # Stubhead elements + LocStubhead as stubhead, + LocStubheadLabel as stubhead_label, + # Column Labels elements LocColumnLabels as column_labels, + LocSpannerLabel as spanner_label, + LocColumnLabel as column_label, + # Stub elements + LocStub as stub, + LocRowGroupLabel as row_group_label, + LocRowLabel as row_label, + LocSummaryLabel as summary_label, + # Body elements + LocBody as body, + LocSummary as summary, + # Footer elements + LocFooter as footer, + LocFootnotes as footnotes, + LocSourceNotes as source_notes, ) -__all__ = ("body", "stub", "column_labels") +__all__ = ( + "header", + "title", + "subtitle", + "stubhead", + "stubhead_label", + "column_labels", + "spanner_label", + "column_label", + "stub", + "row_group_label", + "row_label", + "summary_label", + "body", + "summary", + "footer", + "footnotes", + "source_notes", +) From f80e1cde64d767789ef7c5898c2d038949a54b9f Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Wed, 15 May 2024 17:46:11 -0400 Subject: [PATCH 003/150] fix lint --- great_tables/_gt_data.py | 12 ++--- great_tables/_locations.py | 83 ++++++++++++++++++++++++------ great_tables/_utils_render_html.py | 16 ++++-- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index fbe4b98d8..2fa250e82 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -847,6 +847,7 @@ class FootnotePlacement(Enum): right = auto() auto = auto() + LocHeaderName = Literal[ "header", "title", @@ -867,19 +868,13 @@ class FootnotePlacement(Enum): "row_label", "summary_label", ] -LocBodyName = Literal[ - "body", - "cell", - "summary" -] +LocBodyName = Literal["body", "cell", "summary"] LocFooterName = Literal[ "footer", "footnotes", "source_notes", ] -LocUnknownName = Literal[ - "none", -] +LocUnknownName = Literal["none",] LocName = Union[ LocHeaderName, LocStubheadName, @@ -890,6 +885,7 @@ class FootnotePlacement(Enum): LocUnknownName, ] + @dataclass(frozen=True) class FootnoteInfo: locname: LocName | None = None diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 16f899b95..8aa2cd22c 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -10,7 +10,22 @@ # note that types like Spanners are only used in annotations for concretes of the # resolve generic, but we need to import at runtime, due to singledispatch looking # up annotations -from ._gt_data import ColInfoTypeEnum, FootnoteInfo, FootnotePlacement, GTData, Spanners, StyleInfo, LocName, LocHeaderName, LocStubheadName, LocColumnLabelsName, LocStubName, LocBodyName, LocFooterName, LocUnknownName +from ._gt_data import ( + ColInfoTypeEnum, + FootnoteInfo, + FootnotePlacement, + GTData, + Spanners, + StyleInfo, + LocName, + LocHeaderName, + LocStubheadName, + LocColumnLabelsName, + LocStubName, + LocBodyName, + LocFooterName, + LocUnknownName, +) from ._styles import CellStyle from ._tbl_data import PlDataFrame, PlExpr, eval_select, eval_transform @@ -41,6 +56,7 @@ class CellPos: @dataclass class Loc: """A location.""" + groups: LocName @@ -64,6 +80,7 @@ class LocSubTitle(Loc): @dataclass class LocStubhead(Loc): """A location for targeting the table stubhead and stubhead label.""" + groups: LocStubheadName = "stubhead" @@ -88,9 +105,11 @@ class LocColumnLabel(Loc): @dataclass class LocSpannerLabel(Loc): """A location for column spanners.""" + groups: Literal["spanner_label"] = "spanner_label" ids: SelectExpr = None + @dataclass class LocStub(Loc): """A location for targeting the table stub, row group labels, summary labels, and body.""" @@ -98,6 +117,7 @@ class LocStub(Loc): groups: Literal["stub"] = "stub" rows: RowSelectExpr = None + @dataclass class LocRowGroupLabel(Loc): groups: Literal["row_group_label"] = "row_group_label" @@ -109,11 +129,13 @@ class LocRowLabel(Loc): groups: Literal["row_label"] = "row_label" rows: RowSelectExpr = None + @dataclass class LocSummaryLabel(Loc): groups: Literal["summary_label"] = "summary_label" rows: RowSelectExpr = None + @dataclass class LocBody(Loc): # TODO: these can be tidyselectors @@ -436,6 +458,7 @@ def _(loc: LocBody, data: GTData) -> list[CellPos]: # LocFootnotes # LocSourceNotes + @singledispatch def set_style(loc: Loc, data: GTData, style: list[str]) -> GTData: """Set style for location.""" @@ -465,7 +488,9 @@ def _(loc: LocTitle, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname="title", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="title", locnum=1, styles=style)] + ) @set_style.register @@ -473,7 +498,9 @@ def _(loc: LocSubTitle, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname="subtitle", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="subtitle", locnum=1, styles=style)] + ) @set_style.register @@ -481,7 +508,9 @@ def _(loc: LocStubhead, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname="stubhead", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="stubhead", locnum=1, styles=style)] + ) @set_style.register @@ -489,7 +518,9 @@ def _(loc: LocStubheadLabel, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname="stubhead_label", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="stubhead_label", locnum=1, styles=style)] + ) @set_style.register @@ -497,7 +528,9 @@ def _(loc: LocColumnLabels, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname="column_labels", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="column_labels", locnum=1, styles=style)] + ) @set_style.register @@ -510,7 +543,11 @@ def _(loc: LocColumnLabel, data: GTData, style: list[CellStyle]) -> GTData: all_info: list[StyleInfo] = [] for col_pos in positions: crnt_info = StyleInfo( - locname="column_label", locnum=2, colname=col_pos.colname, rownum=col_pos.row, styles=styles + locname="column_label", + locnum=2, + colname=col_pos.colname, + rownum=col_pos.row, + styles=styles, ) all_info.append(crnt_info) return data._replace(_styles=data._styles + all_info) @@ -522,7 +559,9 @@ def _(loc: LocSpannerLabel, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname="column_labels", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="column_labels", locnum=1, styles=style)] + ) @set_style.register @@ -539,7 +578,9 @@ def _(loc: LocRowGroupLabel, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname="row_group_label", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="row_group_label", locnum=1, styles=style)] + ) @set_style.register @@ -548,7 +589,9 @@ def _(loc: LocRowLabel, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname="row_label", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="row_label", locnum=1, styles=style)] + ) @set_style.register @@ -557,7 +600,9 @@ def _(loc: LocSummaryLabel, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname="summary_label", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="summary_label", locnum=1, styles=style)] + ) @set_style.register @@ -583,7 +628,9 @@ def _(loc: LocSummary, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname="summary", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="summary", locnum=1, styles=style)] + ) @set_style.register @@ -591,7 +638,9 @@ def _(loc: LocFooter, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname="footer", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="footer", locnum=1, styles=style)] + ) @set_style.register @@ -600,7 +649,9 @@ def _(loc: LocFootnotes, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname="footnotes", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="footnotes", locnum=1, styles=style)] + ) @set_style.register @@ -609,7 +660,9 @@ def _(loc: LocSourceNotes, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname="source_notes", locnum=1, styles=style)]) + return data._replace( + _styles=data._styles + [StyleInfo(locname="source_notes", locnum=1, styles=style)] + ) # Set footnote generic ================================================================= diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index c90f2001a..1ec9436e8 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -260,7 +260,11 @@ def create_columns_component_h(data: GTData) -> str: if colspans[ii] > 0: # Filter by column label / id, join with overall column labels style # TODO check this filter logic - styles_i = [x for x in styles_spanner_label if x.grpname == spanner_ids_level_1_index[ii]] + styles_i = [ + x + for x in styles_spanner_label + if x.grpname == spanner_ids_level_1_index[ii] + ] level_1_spanners.append( tags.th( @@ -340,7 +344,9 @@ def create_columns_component_h(data: GTData) -> str: if colspan > 0: # Filter by column label / id, join with overall column labels style # TODO check this filter logic - styles_i = [x for x in styles_column_label if x.grpname in (colspan, span_label)] + styles_i = [ + x for x in styles_column_label if x.grpname in (colspan, span_label) + ] if span_label: span = tags.span( @@ -405,7 +411,7 @@ def create_body_component_h(data: GTData) -> str: styles_row_group_label = [x for x in data._styles if x.locname == "row_group_label"] styles_row_label = [x for x in data._styles if x.locname == "row_label"] styles_summary_label = [x for x in data._styles if x.locname == "summary_label"] - stub_style = _flatten_styles(styles_stub, wrap=True)if styles_stub else "" + stub_style = _flatten_styles(styles_stub, wrap=True) if styles_stub else "" # Filter list of StyleInfo to only those that apply to the body styles_cells = [x for x in data._styles if x.locname == "cell"] @@ -483,7 +489,9 @@ def create_body_component_h(data: GTData) -> str: cell_styles = "" if is_stub_cell: - body_cells.append(f""" {cell_str}""") + body_cells.append( + f""" {cell_str}""" + ) else: body_cells.append( f""" {cell_str}""" From 5c18797d40d2dffec11e7e09d0ded9c5a8deb7bb Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Wed, 15 May 2024 18:18:19 -0400 Subject: [PATCH 004/150] working on tests --- great_tables/_utils_render_html.py | 25 ++++++++++++++++--------- tests/test_locations.py | 8 ++++---- tests/test_tab_create_modify.py | 2 +- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 1ec9436e8..de056535b 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -21,10 +21,17 @@ def _flatten_styles(styles: Styles, wrap: bool = False) -> str: # TODO dedupe rendered styles in sequence if wrap: - style_to_return = f'style="{" ".join(rendered_styles)}"' + " " - else: - style_to_return = " ".join(rendered_styles) - return style_to_return + if rendered_styles: + # return style html attribute + return f'style="{" ".join(rendered_styles)}"' + " " + # if no rendered styles, just return a blank + return "" + if rendered_styles: + # return space-separated list of rendered styles + return " ".join(rendered_styles) + # if not wrapping the styles for html element, + # return None so htmltools omits a style attribute + return None def create_heading_component_h(data: GTData) -> StringBuilder: @@ -64,20 +71,20 @@ def create_heading_component_h(data: GTData) -> StringBuilder: if has_subtitle: heading = f""" - {title} + {title} - {subtitle} + {subtitle} """ else: heading = f""" - {title} + {title} """ result.append(heading) - return StringBuilder(f"""""", result, "\n") + return StringBuilder(f"""""", result, "\n") def create_columns_component_h(data: GTData) -> str: @@ -490,7 +497,7 @@ def create_body_component_h(data: GTData) -> str: if is_stub_cell: body_cells.append( - f""" {cell_str}""" + f""" {cell_str}""" ) else: body_cells.append( diff --git a/tests/test_locations.py b/tests/test_locations.py index 55006b41e..334600410 100644 --- a/tests/test_locations.py +++ b/tests/test_locations.py @@ -6,7 +6,7 @@ from great_tables._locations import ( CellPos, LocBody, - LocColumnSpanners, + LocSpannerLabel, LocTitle, resolve, resolve_cols_i, @@ -137,7 +137,7 @@ def test_resolve_column_spanners_simple(): ids = ["a", "b", "c"] spanners = Spanners.from_ids(ids) - loc = LocColumnSpanners(ids=["a", "c"]) + loc = LocSpannerLabel(ids=["a", "c"]) new_loc = resolve(loc, spanners) @@ -150,7 +150,7 @@ def test_resolve_column_spanners_error_missing(): ids = ["a", "b", "c"] spanners = Spanners.from_ids(ids) - loc = LocColumnSpanners(ids=["a", "d"]) + loc = LocSpannerLabel(ids=["a", "d"]) with pytest.raises(ValueError): resolve(loc, spanners) @@ -178,7 +178,7 @@ def test_set_style_loc_body_from_column(expr): new_gt = set_style(loc, gt_df, [style]) # 1 style info added - assert len(new_gt._styles) == 1 + assert len(new_gt._styles) == 2 cell_info = new_gt._styles[0] # style info has single cell style, with new color diff --git a/tests/test_tab_create_modify.py b/tests/test_tab_create_modify.py index f88ae4fea..12ad0275c 100644 --- a/tests/test_tab_create_modify.py +++ b/tests/test_tab_create_modify.py @@ -16,7 +16,7 @@ def test_tab_style(gt: GT): new_gt = tab_style(gt, style, LocBody(["x"], [0])) assert len(gt._styles) == 0 - assert len(new_gt._styles) == 1 + assert len(new_gt._styles) == 2 assert len(new_gt._styles[0].styles) == 1 assert new_gt._styles[0].styles[0] is style From 4ea4921b8163cf9532bd89f97879c1b8097f632c Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 27 Aug 2024 12:48:26 -0400 Subject: [PATCH 005/150] Add `google_font()` helper function --- great_tables/__init__.py | 2 ++ great_tables/_helpers.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/great_tables/__init__.py b/great_tables/__init__.py index 0ce2e57d7..582bd0e2f 100644 --- a/great_tables/__init__.py +++ b/great_tables/__init__.py @@ -19,6 +19,7 @@ pct, md, html, + google_font, random_id, system_fonts, define_units, @@ -35,6 +36,7 @@ "pct", "md", "html", + "google_font", "system_fonts", "define_units", "nanoplot_options", diff --git a/great_tables/_helpers.py b/great_tables/_helpers.py index 40e40aee4..e001844d0 100644 --- a/great_tables/_helpers.py +++ b/great_tables/_helpers.py @@ -281,6 +281,27 @@ def LETTERS() -> list[str]: return list(string.ascii_uppercase) +def google_font(name: str) -> dict[str, str]: + """Specify a font from the *Google Fonts* service. + + Parameters + ---------- + name + The name of the Google Font to use. + + Returns + ------- + str + The name of the Google Font. This is the name of the font as it appears in the Google Fonts + service (e.g., `"Roboto"`). Please refer to the Google Fonts website for the full list of + available fonts (https://fonts.google.com/). + """ + + import_stmt = f"@import url('https://fonts.googleapis.com/css2?family={name.replace(' ', '+')}&display=swap');" + + return dict(name=name, import_stmt=import_stmt) + + def system_fonts(name: FontStackName = "system-ui") -> list[str]: """Get a themed font stack that works well across systems. From bd3584764dcbb5b9ce535bf69c8e5995ec60913a Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 27 Aug 2024 12:48:56 -0400 Subject: [PATCH 006/150] Enable use of `table_additional_css` option --- great_tables/_gt_data.py | 2 +- great_tables/_options.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index 12e37529d..cb44556a9 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -984,7 +984,7 @@ class Options: table_margin_left: OptionsInfo = OptionsInfo(True, "table", "px", "auto") table_margin_right: OptionsInfo = OptionsInfo(True, "table", "px", "auto") table_background_color: OptionsInfo = OptionsInfo(True, "table", "value", "#FFFFFF") - # table_additional_css: OptionsInfo = OptionsInfo(False, "table", "values", None) + table_additional_css: OptionsInfo = OptionsInfo(False, "table", "values", None) table_font_names: OptionsInfo = OptionsInfo(False, "table", "values", default_fonts_list) table_font_size: OptionsInfo = OptionsInfo(True, "table", "px", "16px") table_font_weight: OptionsInfo = OptionsInfo(True, "table", "value", "normal") diff --git a/great_tables/_options.py b/great_tables/_options.py index b1e7c11b2..f78b24f78 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -23,7 +23,7 @@ def tab_options( table_margin_left: str | None = None, table_margin_right: str | None = None, table_background_color: str | None = None, - # table_additional_css: str | None = None, + table_additional_css: str | None = None, table_font_names: str | list[str] | None = None, table_font_size: str | None = None, table_font_weight: str | int | float | None = None, From 0b9418e35aeb6c770bdb0f56c79a4b3e589d567f Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 27 Aug 2024 12:49:27 -0400 Subject: [PATCH 007/150] Add docs for `table_additional_css` --- great_tables/_options.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/great_tables/_options.py b/great_tables/_options.py index f78b24f78..0917b0c99 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -207,6 +207,9 @@ def tab_options( table_background_color The background color for the table. A color name or a hexadecimal color code should be provided. + table_additional_css + Additional CSS that can be added to the table. This can be used to add any custom CSS + that is not covered by the other options. table_font_names The names of the fonts used for the table. This should be provided as a list of font names. If the first font isn't available, then the next font is tried (and so on). From 22997da19112e2684bf4de333c9c6e72f40e764f Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 27 Aug 2024 12:50:09 -0400 Subject: [PATCH 008/150] Store data for Google font in dict (in opt_table_font()) --- great_tables/_options.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index 0917b0c99..7e98d2649 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -1060,7 +1060,7 @@ def opt_table_outline( def opt_table_font( self: GTSelf, - font: str | list[str] | None = None, + font: str | list[str] | dict[str, str] | None = None, stack: FontStackName | None = None, weight: str | int | float | None = None, style: str | None = None, @@ -1200,6 +1200,14 @@ def opt_table_font( if isinstance(font, str): font = [font] + # If `font` is a dictionary, then it is a Google Font definition + if isinstance(font, dict): + + font_import_stmt = font["import_stmt"] + font = [font["name"]] + + res = tab_options(res, table_additional_css=font_import_stmt) + else: font = [] From 7dfc327e4e792b3a0ddf31ae2e58b17c52689917 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 27 Aug 2024 12:50:54 -0400 Subject: [PATCH 009/150] Add support for Google fonts in `compile_scss()` --- great_tables/_scss.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/great_tables/_scss.py b/great_tables/_scss.py index d5deafffa..1f01d782e 100644 --- a/great_tables/_scss.py +++ b/great_tables/_scss.py @@ -137,7 +137,20 @@ def compile_scss( # Generate styles ---- gt_table_open_str = f"#{id} table" if has_id else ".gt_table" - gt_table_class_str = f"""{gt_table_open_str} {{ + # Prepend any additional CSS ---- + additional_css = data._options.table_additional_css.value + + # Determine if there are any additional CSS statements + has_additional_css = additional_css is not None + + # Combine any additional CSS statements and separate with `\n` + if has_additional_css: + table_additional_css = additional_css + # "\n".join(additional_css) + else: + table_additional_css = "" + + gt_table_class_str = f"""{table_additional_css}{gt_table_open_str} {{ {font_family_attr} -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; From aae7765dc2919e1dd9d5a8dded8ed8fb2d9f8b0d Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 27 Aug 2024 12:54:37 -0400 Subject: [PATCH 010/150] Add TODO; remove commented code --- great_tables/_scss.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/great_tables/_scss.py b/great_tables/_scss.py index 1f01d782e..b46c27359 100644 --- a/great_tables/_scss.py +++ b/great_tables/_scss.py @@ -143,10 +143,9 @@ def compile_scss( # Determine if there are any additional CSS statements has_additional_css = additional_css is not None - # Combine any additional CSS statements and separate with `\n` + # TODO: Combine any additional CSS statements and separate with `\n` if has_additional_css: table_additional_css = additional_css - # "\n".join(additional_css) else: table_additional_css = "" From bac743e3a3bc26673cacc3fa09221b3217d19377 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 27 Aug 2024 15:02:25 -0400 Subject: [PATCH 011/150] Refactor conditional statements --- great_tables/_options.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index 7e98d2649..c9712193b 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -1196,16 +1196,15 @@ def opt_table_font( if font is not None: - # If `font` is a string, convert it to a list if isinstance(font, str): + # Case where `font=` is a string; here, it's converted to a list font = [font] - - # If `font` is a dictionary, then it is a Google Font definition - if isinstance(font, dict): - + elif isinstance(font, dict): + # Case where `font=` is a dictionary, then it is assumed to be Google Font definition font_import_stmt = font["import_stmt"] font = [font["name"]] + # Add the import statement to the `table_additional_css` option res = tab_options(res, table_additional_css=font_import_stmt) else: From 148322c97a3f0f788fc2f629374a3e48e64c080e Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Wed, 28 Aug 2024 14:07:37 -0400 Subject: [PATCH 012/150] Ensure `table_additional_css` init'd as a list --- great_tables/_gt_data.py | 2 +- great_tables/_options.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index cb44556a9..f596c7e92 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -984,7 +984,7 @@ class Options: table_margin_left: OptionsInfo = OptionsInfo(True, "table", "px", "auto") table_margin_right: OptionsInfo = OptionsInfo(True, "table", "px", "auto") table_background_color: OptionsInfo = OptionsInfo(True, "table", "value", "#FFFFFF") - table_additional_css: OptionsInfo = OptionsInfo(False, "table", "values", None) + table_additional_css: OptionsInfo = OptionsInfo(False, "table", "values", []) table_font_names: OptionsInfo = OptionsInfo(False, "table", "values", default_fonts_list) table_font_size: OptionsInfo = OptionsInfo(True, "table", "px", "16px") table_font_weight: OptionsInfo = OptionsInfo(True, "table", "value", "normal") diff --git a/great_tables/_options.py b/great_tables/_options.py index c9712193b..9965359a0 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -23,7 +23,7 @@ def tab_options( table_margin_left: str | None = None, table_margin_right: str | None = None, table_background_color: str | None = None, - table_additional_css: str | None = None, + table_additional_css: list[str] | None = None, table_font_names: str | list[str] | None = None, table_font_size: str | None = None, table_font_weight: str | int | float | None = None, From 72db8cb1166b2b0934ae8af0bfa5c91355834cd7 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Wed, 28 Aug 2024 14:08:47 -0400 Subject: [PATCH 013/150] Create dataclass GoogleFont to work w/ helper --- great_tables/_helpers.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/great_tables/_helpers.py b/great_tables/_helpers.py index e001844d0..ba580c947 100644 --- a/great_tables/_helpers.py +++ b/great_tables/_helpers.py @@ -281,7 +281,21 @@ def LETTERS() -> list[str]: return list(string.ascii_uppercase) -def google_font(name: str) -> dict[str, str]: +@dataclass +class GoogleFont: + font: str + + def __repr__(self) -> str: + return f"GoogleFont({self.font})" + + def make_import_stmt(self) -> str: + return f"@import url('https://fonts.googleapis.com/css2?family={self.font.replace(' ', '+')}&display=swap');" + + def get_font_name(self) -> str: + return self.font + + +def google_font(name: str) -> GoogleFont: """Specify a font from the *Google Fonts* service. Parameters @@ -297,9 +311,7 @@ def google_font(name: str) -> dict[str, str]: available fonts (https://fonts.google.com/). """ - import_stmt = f"@import url('https://fonts.googleapis.com/css2?family={name.replace(' ', '+')}&display=swap');" - - return dict(name=name, import_stmt=import_stmt) + return GoogleFont(font=name) def system_fonts(name: FontStackName = "system-ui") -> list[str]: From adc875d2f70b1718d7fda933a24455c9407b2dda Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Wed, 28 Aug 2024 14:09:42 -0400 Subject: [PATCH 014/150] Detect and use GoogleFont class + methods --- great_tables/_options.py | 17 ++++++++++------- great_tables/_styles.py | 15 +++++++++++++-- great_tables/_tab_create_modify.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index 9965359a0..4d17901fa 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, ClassVar, cast from great_tables import _utils -from great_tables._helpers import FontStackName +from great_tables._helpers import FontStackName, GoogleFont if TYPE_CHECKING: @@ -1199,13 +1199,16 @@ def opt_table_font( if isinstance(font, str): # Case where `font=` is a string; here, it's converted to a list font = [font] - elif isinstance(font, dict): - # Case where `font=` is a dictionary, then it is assumed to be Google Font definition - font_import_stmt = font["import_stmt"] - font = [font["name"]] + elif isinstance(font, GoogleFont): + # Case where `font=` is a GoogleFont object + font_import_stmt = font.make_import_stmt() + font = [font.get_font_name()] - # Add the import statement to the `table_additional_css` option - res = tab_options(res, table_additional_css=font_import_stmt) + # Append the import statement to the `table_additional_css` list + existing_additional_css = self._options.table_additional_css.value + [font_import_stmt] + + # Add revised CSS list via the `tab_options()` method + res = tab_options(res, table_additional_css=existing_additional_css) else: font = [] diff --git a/great_tables/_styles.py b/great_tables/_styles.py index 90f8a8fb0..0411cbb28 100644 --- a/great_tables/_styles.py +++ b/great_tables/_styles.py @@ -5,7 +5,7 @@ from typing_extensions import Self, TypeAlias -from ._helpers import px +from ._helpers import px, GoogleFont from ._tbl_data import PlExpr, TblData, _get_cell, eval_transform if TYPE_CHECKING: @@ -220,12 +220,23 @@ class CellStyleText(CellStyle): ) = None def _to_html_style(self) -> str: + rendered = "" if self.color: rendered += f"color: {self.color};" if self.font: - rendered += f"font-family: {self.font};" + font = self.font + if isinstance(font, str) or isinstance(font, FromColumn): + # Case where `font=` is a string or a FromColumn expression + font_name = font + elif isinstance(font, GoogleFont): + # Case where `font=` is a GoogleFont + font_name = font.get_font_name() + else: + # Case where font is of an invalid type + raise ValueError(f"Invalid font type '{type(font)}' provided.") + rendered += f"font-family: {font_name};" if self.size: rendered += f"font-size: {self.size};" if self.align: diff --git a/great_tables/_tab_create_modify.py b/great_tables/_tab_create_modify.py index a427a7d77..6c1025e38 100644 --- a/great_tables/_tab_create_modify.py +++ b/great_tables/_tab_create_modify.py @@ -4,6 +4,7 @@ from ._locations import Loc, PlacementOptions, set_footnote, set_style from ._styles import CellStyle +from ._helpers import GoogleFont if TYPE_CHECKING: @@ -118,6 +119,34 @@ def tab_style( locations = [locations] new_data = self + + # Intercept `font` in CellStyleText to capture Google Fonts and: + # 1. transform dictionary to string (with Google Font name) + # 2. add Google Font import statement via tab_options(table_additional_css) + if any(isinstance(s, CellStyle) for s in style): + + for s in style: + if ( + isinstance(s, CellStyle) + and hasattr(s, "font") + and s.font is not None + and isinstance(s.font, GoogleFont) + ): + # Obtain font name and import statement as local variables + font_name = s.font.get_font_name() + font_import_stmt = s.font.make_import_stmt() + + # Replace GoogleFont class with font name + s.font = font_name + + # Append the import statement to the `table_additional_css` list + existing_additional_css = self._options.table_additional_css.value + [ + font_import_stmt + ] + + # Add revised CSS list via the `tab_options()` method + new_data = new_data.tab_options(table_additional_css=existing_additional_css) + for loc in locations: new_data = set_style(loc, new_data, style) From b1802a4975761ffdc704dac392d9420447fafe23 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Wed, 28 Aug 2024 14:10:10 -0400 Subject: [PATCH 015/150] Handle `additional_css` as a list of strings --- great_tables/_scss.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/great_tables/_scss.py b/great_tables/_scss.py index b46c27359..cbad96c47 100644 --- a/great_tables/_scss.py +++ b/great_tables/_scss.py @@ -141,11 +141,15 @@ def compile_scss( additional_css = data._options.table_additional_css.value # Determine if there are any additional CSS statements - has_additional_css = additional_css is not None + has_additional_css = ( + additional_css is not None and isinstance(additional_css, list) and len(additional_css) > 0 + ) - # TODO: Combine any additional CSS statements and separate with `\n` + # Ensure that list items in `additional_css` are unique and then combine statements while + # separating with `\n`; use an empty string if list is empty or value is None if has_additional_css: - table_additional_css = additional_css + additional_css_unique = _unique_set(additional_css) + table_additional_css = "\n".join(additional_css_unique) + "\n" else: table_additional_css = "" From 8739dcb29ac965114928be6908add3dce5146040 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Wed, 28 Aug 2024 16:35:43 -0400 Subject: [PATCH 016/150] Allow lists with mix of local and Google fonts --- great_tables/_options.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index 4d17901fa..edf2a0c6a 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -1196,19 +1196,34 @@ def opt_table_font( if font is not None: - if isinstance(font, str): - # Case where `font=` is a string; here, it's converted to a list + # If font is a string or GoogleFont object, convert to a list + if isinstance(font, str) or isinstance(font, GoogleFont): font = [font] - elif isinstance(font, GoogleFont): - # Case where `font=` is a GoogleFont object - font_import_stmt = font.make_import_stmt() - font = [font.get_font_name()] - # Append the import statement to the `table_additional_css` list - existing_additional_css = self._options.table_additional_css.value + [font_import_stmt] + new_font_list: list[str] = [] - # Add revised CSS list via the `tab_options()` method - res = tab_options(res, table_additional_css=existing_additional_css) + for item in font: + + if isinstance(item, str): + # Case where list item is a string; here, it's converted to a list + new_font_list.append(item) + + elif isinstance(item, GoogleFont): + # Case where the list item is a GoogleFont object + new_font_list.append(item.get_font_name()) + + # Append the import statement to the `table_additional_css` list + existing_additional_css = self._options.table_additional_css.value + [ + item.make_import_stmt() + ] + + # Add revised CSS list via the `tab_options()` method + res = tab_options(res, table_additional_css=existing_additional_css) + + else: + raise TypeError("`font=` must be a string or a list of strings.") + + font = new_font_list else: font = [] From dc54ba4fb3adef4e11ab830b15f477ea9fdb48a3 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Wed, 28 Aug 2024 17:18:17 -0400 Subject: [PATCH 017/150] Update docs for `google_font()` --- great_tables/_helpers.py | 53 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/great_tables/_helpers.py b/great_tables/_helpers.py index ba580c947..2525f848a 100644 --- a/great_tables/_helpers.py +++ b/great_tables/_helpers.py @@ -298,6 +298,12 @@ def get_font_name(self) -> str: def google_font(name: str) -> GoogleFont: """Specify a font from the *Google Fonts* service. + The `google_font()` helper function can be used wherever a font name might be specified. There + are two instances where this helper can be used: + + 1. `opt_table_font(font=...)` (for setting a table font) + 2. `style.text(font=...)` (itself used in `tab_style()`) + Parameters ---------- name @@ -305,10 +311,49 @@ def google_font(name: str) -> GoogleFont: Returns ------- - str - The name of the Google Font. This is the name of the font as it appears in the Google Fonts - service (e.g., `"Roboto"`). Please refer to the Google Fonts website for the full list of - available fonts (https://fonts.google.com/). + GoogleFont: + A GoogleFont object, which contains the name of the font and methods for incorporating the + font in HTML output tables. + + Examples + -------- + Let's use the `exibble` dataset to create a table of two columns and eight rows. We'll replace + missing values with em dashes using `sub_missing()`. For text in the time column, we will use + the font called `"IBM Plex Mono"` which is available from Google Fonts. This is defined inside + the `google_font()` call, itself within the `style.text()` method that's applied to the `style=` + parameter of `tab_style()`. + + ```{python} + + from great_tables import GT, exibble, style, loc, google_font + + ( + GT(exibble[["char", "time"]]) + .sub_missing() + .tab_style( + style=style.text(font=google_font(name="IBM Plex Mono")), + locations=loc.body(columns="time") + ) + ) + ``` + + We can use a subset of the `sp500` dataset to create a small table. With `fmt_currency()`, we + can display values as monetary values. Then, we'll set a larger font size for the table and opt + to use the `"Merriweather"` font by calling `google_font()` within `opt_table_font()`. In cases + where that font may not materialize, we include two font fallbacks: `"Cochin"` and the catchall + `"Serif"` group. + + ```{python} + from great_tables import GT, google_font + from great_tables.data import sp500 + + ( + GT(sp500.drop(columns=["volume", "adj_close"]).head(10)) + .fmt_currency(columns=["open", "high", "low", "close"]) + .tab_options(table_font_size="20px") + .opt_table_font(font=[google_font("Merriweather"), "Cochin", "Serif"]) + ) + ``` """ return GoogleFont(font=name) From d3562a46b5c5b74c254e7d1aa5399e0fe9d2e21a Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 29 Aug 2024 12:28:01 -0400 Subject: [PATCH 018/150] Make adjustments to `google_font()` docs --- great_tables/_helpers.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/great_tables/_helpers.py b/great_tables/_helpers.py index 2525f848a..358bf2f53 100644 --- a/great_tables/_helpers.py +++ b/great_tables/_helpers.py @@ -302,7 +302,7 @@ def google_font(name: str) -> GoogleFont: are two instances where this helper can be used: 1. `opt_table_font(font=...)` (for setting a table font) - 2. `style.text(font=...)` (itself used in `tab_style()`) + 2. `style.text(font=...)` (itself used in [`tab_style()`](`great_tables.GT.tab_style`)) Parameters ---------- @@ -311,20 +311,20 @@ def google_font(name: str) -> GoogleFont: Returns ------- - GoogleFont: + GoogleFont A GoogleFont object, which contains the name of the font and methods for incorporating the font in HTML output tables. Examples -------- Let's use the `exibble` dataset to create a table of two columns and eight rows. We'll replace - missing values with em dashes using `sub_missing()`. For text in the time column, we will use - the font called `"IBM Plex Mono"` which is available from Google Fonts. This is defined inside - the `google_font()` call, itself within the `style.text()` method that's applied to the `style=` - parameter of `tab_style()`. + missing values with em dashes using [`sub_missing()`](`great_tables.GT.sub_missing`). For text + in the time column, we will use the font called `"IBM Plex Mono"` which is available from Google + Fonts. This is defined inside the `google_font()` call, itself within the + [`style.text()`](`great_tables.style.text`) method that's applied to the `style=` parameter of + [`tab_style()`](`great_tables.GT.tab_style`). ```{python} - from great_tables import GT, exibble, style, loc, google_font ( @@ -337,9 +337,10 @@ def google_font(name: str) -> GoogleFont: ) ``` - We can use a subset of the `sp500` dataset to create a small table. With `fmt_currency()`, we - can display values as monetary values. Then, we'll set a larger font size for the table and opt - to use the `"Merriweather"` font by calling `google_font()` within `opt_table_font()`. In cases + We can use a subset of the `sp500` dataset to create a small table. With + [`fmt_currency()`](`great_tables.GT.fmt_currency`), we can display values as monetary values. + Then, we'll set a larger font size for the table and opt to use the `"Merriweather"` font by + calling `google_font()` within [`opt_table_font()`](`great_tables.GT.opt_table_font`). In cases where that font may not materialize, we include two font fallbacks: `"Cochin"` and the catchall `"Serif"` group. @@ -351,7 +352,7 @@ def google_font(name: str) -> GoogleFont: GT(sp500.drop(columns=["volume", "adj_close"]).head(10)) .fmt_currency(columns=["open", "high", "low", "close"]) .tab_options(table_font_size="20px") - .opt_table_font(font=[google_font("Merriweather"), "Cochin", "Serif"]) + .opt_table_font(font=[google_font(name="Merriweather"), "Cochin", "Serif"]) ) ``` """ From 036df2423737d617ae483506a825bf4b80ebb6ef Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 29 Aug 2024 16:38:08 -0400 Subject: [PATCH 019/150] Add tests for `google_font()` in two methods --- tests/test_options.py | 17 ++++++++++++++++- tests/test_tab_create_modify.py | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/tests/test_options.py b/tests/test_options.py index a92422325..73720cb1c 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,6 +1,6 @@ import pandas as pd import pytest -from great_tables import GT, exibble, md +from great_tables import GT, exibble, md, google_font from great_tables._scss import compile_scss from great_tables._gt_data import default_fonts_list @@ -362,6 +362,21 @@ def test_opt_table_font_use_stack_and_system_font(): assert gt_tbl._options.table_font_names.value[-1] == "Noto Color Emoji" +def test_opt_table_font_google_font(): + + gt_tbl = GT(exibble).opt_table_font(font=google_font(name="IBM Plex Mono")) + + rendered_html = gt_tbl.as_raw_html() + + assert rendered_html.find( + "@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap');" + ) + + assert rendered_html.find( + "font-family: 'IBM Plex Mono', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', 'Fira Sans', 'Droid Sans', Arial, sans-serif;" + ) + + def test_opt_table_font_raises(): # Both `font` and `stack` cannot be `None` diff --git a/tests/test_tab_create_modify.py b/tests/test_tab_create_modify.py index f88ae4fea..bae225ee9 100644 --- a/tests/test_tab_create_modify.py +++ b/tests/test_tab_create_modify.py @@ -1,6 +1,6 @@ import pandas as pd import pytest -from great_tables import GT +from great_tables import GT, style, loc, google_font from great_tables._locations import LocBody from great_tables._styles import CellStyleFill from great_tables._tab_create_modify import tab_style @@ -30,3 +30,19 @@ def test_tab_style_multiple_columns(gt: GT): assert len(new_gt._styles[0].styles) == 1 assert new_gt._styles[0].styles[0] is style + + +def test_tab_style_google_font(gt: GT): + + new_gt = tab_style( + gt, + style=style.text(font=google_font(name="IBM Plex Mono")), + locations=loc.body(columns="time"), + ) + + rendered_html = new_gt.as_raw_html() + + assert rendered_html.find( + "@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap');" + ) + assert rendered_html.find("font-family: IBM Plex Mono;") From 79b183b0f3f7dcc978933a5235189dff549a8da3 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 29 Aug 2024 16:47:07 -0400 Subject: [PATCH 020/150] Add tests for `google_font()` and GoogleFont --- tests/test_helpers.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index df9c70a6a..4a6ea91a4 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -4,6 +4,7 @@ pct, px, random_id, + google_font, _get_font_stack, define_units, FONT_STACKS, @@ -13,6 +14,7 @@ _units_html_sub_super, _replace_units_symbol, _units_symbol_replacements, + GoogleFont, UnitStr, UnitDefinition, UnitDefinitionList, @@ -80,6 +82,27 @@ def test_uppercases(): assert set(bad_letters).difference(uppercases) +def test_google_font(): + font_name = "Roboto" + font = google_font(font_name) + assert isinstance(font, GoogleFont) + assert font.get_font_name() == font_name + assert ( + font.make_import_stmt() + == "@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');" + ) + + +def test_google_font_class(): + font_name = "Roboto" + font = GoogleFont(font_name) + assert font.get_font_name() == font_name + assert ( + font.make_import_stmt() + == "@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');" + ) + + def test_get_font_stack_raises(): name = "fake_name" with pytest.raises(ValueError) as exc_info: From 600480c7ed336d94bb5e4d0bc6d49dd9e959a1e8 Mon Sep 17 00:00:00 2001 From: jrycw Date: Fri, 30 Aug 2024 09:10:48 +0800 Subject: [PATCH 021/150] Create OrderedSet class --- great_tables/_utils.py | 34 +++++++++++++++++++++++++++++++++- tests/test_utils.py | 13 +++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/great_tables/_utils.py b/great_tables/_utils.py index cb4ecea21..c27e8ce33 100644 --- a/great_tables/_utils.py +++ b/great_tables/_utils.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Set import importlib import itertools import json @@ -89,7 +90,38 @@ def _unique_set(x: list[Any] | None) -> list[Any] | None: def _create_ordered_list(x: Iterable[Any]) -> list[Any]: - return list(dict.fromkeys(x).keys()) + return OrderedSet(x).as_list() + + +class OrderedSet(Set): + def __init__(self, d: Iterable = ()): + self._d = self._create(d) + + def _create(self, d: Iterable): + return {k: True for k in d} + + def as_set(self): + return set(self._d) + + def as_list(self): + return list(self._d) + + def as_dict(self): + return dict(self._d) + + def __contains__(self, k): + return k in self._d + + def __iter__(self): + return iter(self._d) + + def __len__(self): + return len(self._d) + + def __repr__(self): + cls_name = type(self).__name__ + lst = self.as_list() + return f"{cls_name}({lst!r})" def _as_css_font_family_attr(fonts: list[str], value_only: bool = False) -> str: diff --git a/tests/test_utils.py b/tests/test_utils.py index 30ebef98c..3dea8cf57 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -9,6 +9,7 @@ _create_ordered_list, _insert_into_list, _match_arg, + OrderedSet, _str_scalar_to_list, _unique_set, heading_has_subtitle, @@ -119,6 +120,18 @@ def test_unique_set(): assert len(result) == 2 +def test_orderedSet(): + o = OrderedSet([1, 2, "x", "y", 1, 2]) + + assert all(x in o for x in [1, 2, "x", "y"]) + assert len(o) == 4 + assert list(o) == [1, 2, "x", "y"] + assert o.as_list() == [1, 2, "x", "y"] + assert o.as_set() == {1, 2, "x", "y"} + assert o.as_dict() == {1: True, 2: True, "x": True, "y": True} + assert repr(o) == "OrderedSet([1, 2, 'x', 'y'])" + + @pytest.mark.parametrize( "iterable, ordered_list", [ From 0d0a4aa1cd0ab2cb1cc0987c9010c4e6903a4d15 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 30 Aug 2024 11:00:11 -0400 Subject: [PATCH 022/150] Update documentation for `tab_spanner()` --- great_tables/_spanners.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/great_tables/_spanners.py b/great_tables/_spanners.py index 51f804d3c..3a0360f70 100644 --- a/great_tables/_spanners.py +++ b/great_tables/_spanners.py @@ -40,6 +40,16 @@ def tab_spanner( `level` as space permits) and with `replace`, which allows for full or partial spanner replacement. + Labels may use either of three types of input: + + 1. plain text + 2. Markdown or HTML text through use of the [`md()`](`great_tables.md`) or + [`html()`](`great_tables.html`) helpers, respectively. + 3. Text set in curly braces for applying special formatting, called unit notation. For example, + "area ({{ft^2}})" would appear as "area (ft²)". + + See [`define_units()`](`great_tables.define_units`) for details on unit notation. + Parameters ---------- label From fe684d46cd2fc053dd433d94c4445d205ff6da66 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 5 Sep 2024 10:56:53 -0400 Subject: [PATCH 023/150] fix: use `isinstance()` instead of `hasattr()` --- great_tables/_spanners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/great_tables/_spanners.py b/great_tables/_spanners.py index 51f804d3c..11b1e0bec 100644 --- a/great_tables/_spanners.py +++ b/great_tables/_spanners.py @@ -130,7 +130,7 @@ def tab_spanner( if id is None: # The label may contain HTML or Markdown, so we need to extract # it from the Text object - if hasattr(label, "text"): + if isinstance(label, Text): id = label.text else: id = label From cd0bebff1983b5f19d53f9fd2ad75af5dbc29267 Mon Sep 17 00:00:00 2001 From: Jerry Wu Date: Tue, 10 Sep 2024 00:03:29 +0800 Subject: [PATCH 024/150] Standardize the `GTSelf` signature (#431) --- great_tables/_source_notes.py | 4 +-- great_tables/_spanners.py | 62 +++++++++++++++++------------------ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/great_tables/_source_notes.py b/great_tables/_source_notes.py index 154a3af54..8c549ad80 100644 --- a/great_tables/_source_notes.py +++ b/great_tables/_source_notes.py @@ -8,7 +8,7 @@ from ._types import GTSelf -def tab_source_note(data: GTSelf, source_note: str | Text) -> GTSelf: +def tab_source_note(self: GTSelf, source_note: str | Text) -> GTSelf: """ Add a source note citation. @@ -51,4 +51,4 @@ def tab_source_note(data: GTSelf, source_note: str | Text) -> GTSelf: ``` """ - return data._replace(_source_notes=data._source_notes + [source_note]) + return self._replace(_source_notes=self._source_notes + [source_note]) diff --git a/great_tables/_spanners.py b/great_tables/_spanners.py index 51f804d3c..49535f4eb 100644 --- a/great_tables/_spanners.py +++ b/great_tables/_spanners.py @@ -17,7 +17,7 @@ def tab_spanner( - data: GTSelf, + self: GTSelf, label: str | Text, columns: SelectExpr = None, spanners: str | list[str] | None = None, @@ -125,7 +125,7 @@ def tab_spanner( """ from great_tables._helpers import UnitStr - crnt_spanner_ids = set([span.spanner_id for span in data._spanners]) + crnt_spanner_ids = set([span.spanner_id for span in self._spanners]) if id is None: # The label may contain HTML or Markdown, so we need to extract @@ -153,7 +153,7 @@ def tab_spanner( # select columns ---- - selected_column_names = resolve_cols_c(data=data, expr=columns, null_means="nothing") or [] + selected_column_names = resolve_cols_c(data=self, expr=columns, null_means="nothing") or [] # select spanner ids ---- # TODO: this supports tidyselect @@ -170,7 +170,7 @@ def tab_spanner( raise NotImplementedError("columns/spanners must be specified") # get column names associated with selected spanners ---- - _vars = [span.vars for span in data._spanners if span.spanner_id in spanner_ids] + _vars = [span.vars for span in self._spanners if span.spanner_id in spanner_ids] spanner_column_names = list({k: True for k in itertools.chain(*_vars)}) column_names = list({k: True for k in [*selected_column_names, *spanner_column_names]}) @@ -179,7 +179,7 @@ def tab_spanner( # get spanner level ---- if level is None: - level = data._spanners.next_level(column_names) + level = self._spanners.next_level(column_names) # get spanner units and labels ---- # TODO: grep units from {{.*}}, may need to switch delimiters @@ -213,8 +213,8 @@ def tab_spanner( spanner_label=new_label, ) - spanners = data._spanners.append_entry(new_span) - new_data = data._replace(_spanners=spanners) + spanners = self._spanners.append_entry(new_span) + new_data = self._replace(_spanners=spanners) if gather and not len(spanner_ids) and level == 0 and column_names: return cols_move(new_data, columns=column_names, after=column_names[0]) @@ -229,7 +229,7 @@ def _validate_sel_cols(sel_cols: list[str], col_vars: list[str]) -> None: raise ValueError("All `columns` must exist and be visible in the input `data` table.") -def cols_move(data: GTSelf, columns: SelectExpr, after: str) -> GTSelf: +def cols_move(self: GTSelf, columns: SelectExpr, after: str) -> GTSelf: """Move one or more columns. On those occasions where you need to move columns this way or that way, we can make use of the @@ -287,11 +287,11 @@ def cols_move(data: GTSelf, columns: SelectExpr, after: str) -> GTSelf: if isinstance(columns, str): columns = [columns] - sel_cols = resolve_cols_c(data=data, expr=columns) + sel_cols = resolve_cols_c(data=self, expr=columns) - sel_after = resolve_cols_c(data=data, expr=[after]) + sel_after = resolve_cols_c(data=self, expr=[after]) - col_vars = [col.var for col in data._boxhead] + col_vars = [col.var for col in self._boxhead] if not len(sel_after): raise ValueError(f"Column {after} not found in table.") @@ -308,11 +308,11 @@ def cols_move(data: GTSelf, columns: SelectExpr, after: str) -> GTSelf: indx = other_columns.index(after) final_vars = [*other_columns[: indx + 1], *moving_columns, *other_columns[indx + 1 :]] - new_boxhead = data._boxhead.reorder(final_vars) - return data._replace(_boxhead=new_boxhead) + new_boxhead = self._boxhead.reorder(final_vars) + return self._replace(_boxhead=new_boxhead) -def cols_move_to_start(data: GTSelf, columns: SelectExpr) -> GTSelf: +def cols_move_to_start(self: GTSelf, columns: SelectExpr) -> GTSelf: """Move one or more columns to the start. We can easily move set of columns to the beginning of the column series and we only need to @@ -368,9 +368,9 @@ def cols_move_to_start(data: GTSelf, columns: SelectExpr) -> GTSelf: if isinstance(columns, str): columns = [columns] - sel_cols = resolve_cols_c(data=data, expr=columns) + sel_cols = resolve_cols_c(data=self, expr=columns) - col_vars = [col.var for col in data._boxhead] + col_vars = [col.var for col in self._boxhead] _validate_sel_cols(sel_cols, col_vars) @@ -379,11 +379,11 @@ def cols_move_to_start(data: GTSelf, columns: SelectExpr) -> GTSelf: final_vars = [*moving_columns, *other_columns] - new_boxhead = data._boxhead.reorder(final_vars) - return data._replace(_boxhead=new_boxhead) + new_boxhead = self._boxhead.reorder(final_vars) + return self._replace(_boxhead=new_boxhead) -def cols_move_to_end(data: GTSelf, columns: SelectExpr) -> GTSelf: +def cols_move_to_end(self: GTSelf, columns: SelectExpr) -> GTSelf: """Move one or more columns to the end. We can easily move set of columns to the beginning of the column series and we only need to @@ -434,9 +434,9 @@ def cols_move_to_end(data: GTSelf, columns: SelectExpr) -> GTSelf: if isinstance(columns, str): columns = [columns] - sel_cols = resolve_cols_c(data=data, expr=columns) + sel_cols = resolve_cols_c(data=self, expr=columns) - col_vars = [col.var for col in data._boxhead] + col_vars = [col.var for col in self._boxhead] _validate_sel_cols(sel_cols, col_vars) @@ -445,11 +445,11 @@ def cols_move_to_end(data: GTSelf, columns: SelectExpr) -> GTSelf: final_vars = [*other_columns, *moving_columns] - new_boxhead = data._boxhead.reorder(final_vars) - return data._replace(_boxhead=new_boxhead) + new_boxhead = self._boxhead.reorder(final_vars) + return self._replace(_boxhead=new_boxhead) -def cols_hide(data: GTSelf, columns: SelectExpr) -> GTSelf: +def cols_hide(self: GTSelf, columns: SelectExpr) -> GTSelf: """Hide one or more columns. The `cols_hide()` method allows us to hide one or more columns from appearing in the final @@ -502,16 +502,16 @@ def cols_hide(data: GTSelf, columns: SelectExpr) -> GTSelf: if isinstance(columns, str): columns = [columns] - sel_cols = resolve_cols_c(data=data, expr=columns) + sel_cols = resolve_cols_c(data=self, expr=columns) - col_vars = [col.var for col in data._boxhead] + col_vars = [col.var for col in self._boxhead] _validate_sel_cols(sel_cols, col_vars) # New boxhead with hidden columns - new_boxhead = data._boxhead.set_cols_hidden(sel_cols) + new_boxhead = self._boxhead.set_cols_hidden(sel_cols) - return data._replace(_boxhead=new_boxhead) + return self._replace(_boxhead=new_boxhead) def spanners_print_matrix( @@ -580,7 +580,7 @@ def empty_spanner_matrix( return [{var: var for var in vars}], vars -def cols_width(data: GTSelf, cases: dict[str, str]) -> GTSelf: +def cols_width(self: GTSelf, cases: dict[str, str]) -> GTSelf: """Set the widths of columns. Manual specifications of column widths can be performed using the `cols_width()` method. We @@ -684,9 +684,9 @@ def cols_width(data: GTSelf, cases: dict[str, str]) -> GTSelf: previous example). """ - curr_boxhead = data._boxhead + curr_boxhead = self._boxhead for col, width in cases.items(): curr_boxhead = curr_boxhead._set_column_width(col, width) - return data._replace(_boxhead=curr_boxhead) + return self._replace(_boxhead=curr_boxhead) From bacda7e29772acc38c20c13d71b22a22ad32ab55 Mon Sep 17 00:00:00 2001 From: Jerry Wu Date: Tue, 10 Sep 2024 00:10:36 +0800 Subject: [PATCH 025/150] Update import statement in the `GT.data_color()` example (#432) --- great_tables/_data_color/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/great_tables/_data_color/base.py b/great_tables/_data_color/base.py index ffc25e45a..e76a61064 100644 --- a/great_tables/_data_color/base.py +++ b/great_tables/_data_color/base.py @@ -132,9 +132,10 @@ def data_color( do this with the `exibble` dataset: ```{python} - import great_tables as gt + from great_tables import GT + from great_tables.data import exibble - gt.GT(gt.data.exibble).data_color() + GT(exibble).data_color() ``` What's happened is that `data_color()` applies background colors to all cells of every column @@ -146,8 +147,7 @@ def data_color( supply `palette=` values of `"red"` and `"green"`. ```{python} - - gt.GT(gt.data.exibble).data_color( + GT(exibble).data_color( columns=["num", "currency"], palette=["red", "green"] ) @@ -164,7 +164,7 @@ def data_color( (so we'll set that to `"lightgray"`). ```{python} - gt.GT(gt.data.exibble).data_color( + GT(exibble).data_color( columns="currency", palette=["red", "green"], domain=[0, 50], From 169b5ddf3087e91d75ab9ae1aab970bc32150304 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Mon, 9 Sep 2024 12:23:52 -0400 Subject: [PATCH 026/150] refactor: use OrderedSet directly; rm _unique_set function --- great_tables/_gt_data.py | 6 ++---- great_tables/_scss.py | 8 ++++++-- great_tables/_spanners.py | 8 ++++---- great_tables/_tbl_data.py | 6 +++--- great_tables/_utils.py | 10 ---------- tests/test_utils.py | 15 +-------------- 6 files changed, 16 insertions(+), 37 deletions(-) diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index e8fb38160..de717931d 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -24,7 +24,7 @@ to_list, validate_frame, ) -from ._utils import _str_detect, _create_ordered_list +from ._utils import _str_detect, OrderedSet if TYPE_CHECKING: from ._helpers import Md, Html, UnitStr, Text @@ -577,9 +577,7 @@ def from_data(cls, data, rowname_col: str | None = None, groupname_col: str | No row_info = [RowInfo(*i) for i in zip(row_indices, group_id, row_names)] # create groups, and ensure they're ordered by first observed - group_names = _create_ordered_list( - row.group_id for row in row_info if row.group_id is not None - ) + group_names = OrderedSet(row.group_id for row in row_info if row.group_id is not None) group_rows = GroupRows(data, group_key=groupname_col).reorder(group_names) return cls(row_info, group_rows) diff --git a/great_tables/_scss.py b/great_tables/_scss.py index d5deafffa..bdd383d08 100644 --- a/great_tables/_scss.py +++ b/great_tables/_scss.py @@ -10,7 +10,7 @@ from ._data_color.base import _html_color, _ideal_fgnd_color from ._gt_data import GTData from ._helpers import pct, px -from ._utils import _as_css_font_family_attr, _unique_set +from ._utils import _as_css_font_family_attr, OrderedSet DEFAULTS_TABLE_BACKGROUND = ( "heading_background_color", @@ -126,7 +126,11 @@ def compile_scss( # Handle fonts ---- # Get the unique list of fonts from `gt_options_dict` - font_list = _unique_set(data._options.table_font_names.value) + _font_names = data._options.table_font_names.value + if _font_names is not None: + font_list = OrderedSet(_font_names).as_list() + else: + font_list = None # Generate a `font-family` string if font_list is not None: diff --git a/great_tables/_spanners.py b/great_tables/_spanners.py index 414a577b0..0b5563acc 100644 --- a/great_tables/_spanners.py +++ b/great_tables/_spanners.py @@ -7,7 +7,7 @@ from ._locations import resolve_cols_c from ._tbl_data import SelectExpr from ._text import Text -from ._utils import _create_ordered_list +from ._utils import OrderedSet if TYPE_CHECKING: from ._gt_data import Boxhead @@ -172,14 +172,14 @@ def tab_spanner( # get column names associated with selected spanners ---- _vars = [span.vars for span in data._spanners if span.spanner_id in spanner_ids] - spanner_column_names = _create_ordered_list(itertools.chain(*_vars)) + spanner_column_names = OrderedSet(itertools.chain(*_vars)) - column_names = _create_ordered_list([*selected_column_names, *spanner_column_names]) + column_names = list(OrderedSet([*selected_column_names, *spanner_column_names])) # combine columns names and those from spanners ---- # get spanner level ---- if level is None: - level = data._spanners.next_level(column_names) + level = data._spanners.next_level(list(column_names)) # get spanner units and labels ---- # TODO: grep units from {{.*}}, may need to switch delimiters diff --git a/great_tables/_tbl_data.py b/great_tables/_tbl_data.py index a62a28f59..3ca7d0ce9 100644 --- a/great_tables/_tbl_data.py +++ b/great_tables/_tbl_data.py @@ -323,7 +323,7 @@ def _( def _(data: PlDataFrame, expr: Union[list[str], _selector_proxy_], strict: bool = True) -> _NamePos: # TODO: how to annotate type of a polars selector? # Seems to be polars.selectors._selector_proxy_. - from ._utils import _create_ordered_list + from ._utils import OrderedSet import polars.selectors as cs from polars import Expr @@ -356,11 +356,11 @@ def _(data: PlDataFrame, expr: Union[list[str], _selector_proxy_], strict: bool # this should be equivalent to reducing selectors using an "or" operator, # which isn't possible when there are selectors mixed with expressions # like pl.col("some_col") - final_columns = _create_ordered_list( + final_columns = OrderedSet( col_name for sel in all_selectors for col_name in cs.expand_selector(data, sel, **expand_opts) - ) + ).as_list() else: if not isinstance(expr, (cls_selector, Expr)): raise TypeError(f"Unsupported selection expr type: {type(expr)}") diff --git a/great_tables/_utils.py b/great_tables/_utils.py index c27e8ce33..5cdfa85e9 100644 --- a/great_tables/_utils.py +++ b/great_tables/_utils.py @@ -83,16 +83,6 @@ def _str_scalar_to_list(x: str) -> list[str]: return [x] -def _unique_set(x: list[Any] | None) -> list[Any] | None: - if x is None: - return None - return _create_ordered_list(x) - - -def _create_ordered_list(x: Iterable[Any]) -> list[Any]: - return OrderedSet(x).as_list() - - class OrderedSet(Set): def __init__(self, d: Iterable = ()): self._d = self._create(d) diff --git a/tests/test_utils.py b/tests/test_utils.py index 3dea8cf57..df6323703 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,12 +6,10 @@ _assert_str_list, _assert_str_scalar, _collapse_list_elements, - _create_ordered_list, _insert_into_list, _match_arg, OrderedSet, _str_scalar_to_list, - _unique_set, heading_has_subtitle, heading_has_title, seq_groups, @@ -109,17 +107,6 @@ def test_str_scalar_to_list(): assert x[0] == "x" -def test_unique_set_None(): - assert _unique_set(None) is None - - -def test_unique_set(): - x = ["a", "a", "b"] - result = _unique_set(x) - assert isinstance(result, list) - assert len(result) == 2 - - def test_orderedSet(): o = OrderedSet([1, 2, "x", "y", 1, 2]) @@ -142,7 +129,7 @@ def test_orderedSet(): ], ) def test_create_ordered_list(iterable, ordered_list): - assert _create_ordered_list(iterable) == ordered_list + assert OrderedSet(iterable).as_list() == ordered_list def test_collapse_list_elements(): From 62731aa26545678f9cf5cf5cb4a9a5dc8feff21c Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Mon, 9 Sep 2024 14:43:50 -0400 Subject: [PATCH 027/150] chore: explicitly cast OrderedSet to list --- great_tables/_gt_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index de717931d..0b891198b 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -577,7 +577,7 @@ def from_data(cls, data, rowname_col: str | None = None, groupname_col: str | No row_info = [RowInfo(*i) for i in zip(row_indices, group_id, row_names)] # create groups, and ensure they're ordered by first observed - group_names = OrderedSet(row.group_id for row in row_info if row.group_id is not None) + group_names = list(OrderedSet(row.group_id for row in row_info if row.group_id is not None)) group_rows = GroupRows(data, group_key=groupname_col).reorder(group_names) return cls(row_info, group_rows) From a6d85c4949101dcf9786b07d079ed679294fea8f Mon Sep 17 00:00:00 2001 From: jrycw Date: Thu, 12 Sep 2024 21:52:00 +0800 Subject: [PATCH 028/150] Introduce `_intify_scaled_px()` --- great_tables/_helpers.py | 4 ++++ tests/test_helpers.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/great_tables/_helpers.py b/great_tables/_helpers.py index 40e40aee4..2c4f351a9 100644 --- a/great_tables/_helpers.py +++ b/great_tables/_helpers.py @@ -531,6 +531,10 @@ def _generate_tokens_list(units_notation: str) -> list[str]: return tokens_list +def _intify_scaled_px(v: str, scale: float) -> int: + return int(float(v.removesuffix("px")) * scale) + + @dataclass class UnitDefinition: token: str diff --git a/tests/test_helpers.py b/tests/test_helpers.py index df9c70a6a..d67854f57 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -7,6 +7,7 @@ _get_font_stack, define_units, FONT_STACKS, + _intify_scaled_px, _generate_tokens_list, _units_to_subscript, _units_to_superscript, @@ -345,3 +346,10 @@ def test_unit_str_unmatched_brackets(): assert isinstance(res[1], UnitDefinitionList) assert res[1] == define_units(units_notation="m s^-1 and acceleration {{m s^-2") assert res[2] == "" + + +@pytest.mark.parametrize( + "value, scale, expected", [("0.5px", 0.5, 0), ["1px", 1, 1], ["2.1px", 2.1, 4]] +) +def test_intify_scaled_px(value: str, scale: float, expected: int): + assert _intify_scaled_px(value, scale) == expected From 059275c417082b8c592661d3361e9e06c20d8548 Mon Sep 17 00:00:00 2001 From: jrycw Date: Thu, 12 Sep 2024 21:54:50 +0800 Subject: [PATCH 029/150] Update `opt_vertical_padding()` and `opt_horizontal_padding()` --- great_tables/_options.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index b1e7c11b2..2bc848e26 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, ClassVar, cast from great_tables import _utils -from great_tables._helpers import FontStackName +from great_tables._helpers import FontStackName, _intify_scaled_px, px if TYPE_CHECKING: @@ -765,9 +765,7 @@ def opt_vertical_padding(self: GTSelf, scale: float = 1.0) -> GTSelf: # then reattach the units after the multiplication # TODO: a current limitation is that the padding values must be in pixels and not percentages # TODO: another limitation is that the returned values must be in integer pixel values - new_vertical_padding_vals = [ - str(int(float(v.split("px")[0]) * scale)) + "px" for v in vertical_padding_vals - ] + new_vertical_padding_vals = [px(_intify_scaled_px(v, scale)) for v in vertical_padding_vals] return tab_options(self, **dict(zip(vertical_padding_params, new_vertical_padding_vals))) @@ -864,9 +862,7 @@ def opt_horizontal_padding(self: GTSelf, scale: float = 1.0) -> GTSelf: # then reattach the units after the multiplication # TODO: a current limitation is that the padding values must be in pixels and not percentages # TODO: another limitation is that the returned values must be in integer pixel values - new_horizontal_padding_vals = [ - str(int(float(v.split("px")[0]) * scale)) + "px" for v in horizontal_padding_vals - ] + new_horizontal_padding_vals = [px(_intify_scaled_px(v, scale)) for v in horizontal_padding_vals] return tab_options(self, **dict(zip(horizontal_padding_params, new_horizontal_padding_vals))) From 5cb1de110e61653054d6cc73dafa635bf0e8e23e Mon Sep 17 00:00:00 2001 From: jrycw Date: Thu, 12 Sep 2024 21:55:49 +0800 Subject: [PATCH 030/150] Improve test coverage for test_options.py --- tests/test_options.py | 58 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/test_options.py b/tests/test_options.py index a92422325..a22f11a3b 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -3,6 +3,7 @@ from great_tables import GT, exibble, md from great_tables._scss import compile_scss from great_tables._gt_data import default_fonts_list +from great_tables._helpers import _intify_scaled_px def test_options_overwrite(): @@ -369,3 +370,60 @@ def test_opt_table_font_raises(): GT(exibble).opt_table_font(font=None, stack=None) assert "Either `font=` or `stack=` must be provided." in exc_info.value.args[0] + + +@pytest.mark.parametrize("align", ["left", "center", "right"]) +def test_opt_align_table_header(gt_tbl: GT, align: list[str]): + tbl = gt_tbl.opt_align_table_header(align=align) + + assert tbl._options.heading_align.value == align + + +@pytest.mark.parametrize("scale, expected", [(0.7, "3px"), (1.0, "5px"), (2.1, "10px")]) +def test_opt_vertical_padding(gt_tbl: GT, scale: float, expected: int): + """ + css_length_val_small = "5px" + => int(0.7 * 5) = 3 + => int(1.0 * 5) = 5 + => int(2.1 * 5) = 10 + """ + tbl = gt_tbl.opt_vertical_padding(scale=scale) + + assert tbl._options.heading_padding.value == expected + assert tbl._options.column_labels_padding.value == expected + assert tbl._options.data_row_padding.value == expected + assert tbl._options.row_group_padding.value == expected + assert tbl._options.source_notes_padding.value == expected + + +@pytest.mark.parametrize("scale", [-0.2, 3.2]) +def test_opt_vertical_padding_raises(gt_tbl: GT, scale: float): + with pytest.raises(ValueError) as exc_info: + gt_tbl.opt_vertical_padding(scale=scale) + + assert "`scale` must be a value between `0` and `3`." in exc_info.value.args[0] + + +@pytest.mark.parametrize("scale, expected", [(0.1, "0px"), (1.0, "5px"), (2.2, "11px")]) +def test_opt_horizontal_padding(gt_tbl: GT, scale: float, expected: int): + """ + css_length_val_small = "5px" + => int(0.1 * 5) = 0 + => int(1.0 * 5) = 5 + => int(2.2 * 5) = 11 + """ + tbl = gt_tbl.opt_horizontal_padding(scale=scale) + + assert tbl._options.heading_padding_horizontal.value == expected + assert tbl._options.column_labels_padding_horizontal.value == expected + assert tbl._options.data_row_padding_horizontal.value == expected + assert tbl._options.row_group_padding_horizontal.value == expected + assert tbl._options.source_notes_padding_horizontal.value == expected + + +@pytest.mark.parametrize("scale", [-0.2, 3.2]) +def test_opt_horizontal_padding_raises(gt_tbl: GT, scale: float): + with pytest.raises(ValueError) as exc_info: + gt_tbl.opt_horizontal_padding(scale=scale) + + assert "`scale` must be a value between `0` and `3`." in exc_info.value.args[0] From 83e5ae3f2c31e9318bfae60604c31c91674778ad Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 10:57:31 -0400 Subject: [PATCH 031/150] Show tables with style and color values applied --- docs/get-started/table-theme-premade.qmd | 91 +++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/docs/get-started/table-theme-premade.qmd b/docs/get-started/table-theme-premade.qmd index 747788a2a..c4e1dfd83 100644 --- a/docs/get-started/table-theme-premade.qmd +++ b/docs/get-started/table-theme-premade.qmd @@ -50,8 +50,97 @@ gt_ex.opt_stylize(style = 2, color = "red") Notice that first table (blue) emphasizes the row labels with a solid background color. The second table (red) emphasizes the column labels, and uses solid lines to separate the body cell values. -There are six styles available, each emphasizing different table parts. The styles are currently numbered 1-6. The options for colors are `"blue"`, `"cyan"`, `"pink"`, `"green"`, `"red"`, and `"gray"`. +There are six styles available, each emphasizing different table parts. The `style=` values are numbered from `1` to `6`: + +```{=html} + +``` + +```{python} +# | echo: false +# | output: asis + +from great_tables import GT, exibble, loc, style, md + +print(":::{.grid}") + +for ii in range(1, 7): + + gt_tbl = ( + GT( + exibble[["num", "char", "currency", "row", "group"]], + rowname_col="row", + groupname_col="group", + ) + .tab_header( + title=md("Data listing from **exibble**"), + subtitle=md("This is a **Great Tables** dataset."), + ) + .fmt_number(columns="num") + .fmt_currency(columns="currency") + .tab_source_note(source_note="This is only a subset of the dataset.") + .opt_vertical_padding(scale=0.5) + .opt_stylize(style=ii) + .as_raw_html() + ) + + print( + ":::{.g-col-lg-4 .g-col-12 .shrink-example}", + f"
{ii}
", + gt_tbl, + ":::", + sep="\n\n" + ) + +print(":::") +``` +
+ +There are six `color=` options `"blue"`, `"cyan"`, `"pink"`, `"green"`, `"red"`, and `"gray"`: + +```{python} +# | echo: false +# | output: asis + +from great_tables import GT, exibble, loc, style, md + +print(":::{.grid}") + +for color_option in ["blue", "cyan", "pink", "green", "red", "gray"]: + + gt_tbl = ( + GT( + exibble[["num", "char", "currency", "row", "group"]], + rowname_col="row", + groupname_col="group", + ) + .tab_header( + title=md("Data listing from **exibble**"), + subtitle=md("This is a **Great Tables** dataset."), + ) + .fmt_number(columns="num") + .fmt_currency(columns="currency") + .tab_source_note(source_note="This is only a subset of the dataset.") + .opt_vertical_padding(scale=0.5) + .opt_stylize(style=1, color=color_option) + .as_raw_html() + ) + + print( + ":::{.g-col-lg-4 .g-col-12 .shrink-example}", + f"
{color_option}
", + gt_tbl, + ":::", + sep="\n\n" + ) + +print(":::") +``` ## `opt_*()` convenience methods From 46f8adf9824bfd9880dd311065e74b2d84586008 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 11:06:51 -0400 Subject: [PATCH 032/150] Modify the wording of the final sentence --- docs/get-started/table-theme-premade.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/get-started/table-theme-premade.qmd b/docs/get-started/table-theme-premade.qmd index c4e1dfd83..fde2a745b 100644 --- a/docs/get-started/table-theme-premade.qmd +++ b/docs/get-started/table-theme-premade.qmd @@ -101,7 +101,7 @@ print(":::")
-There are six `color=` options `"blue"`, `"cyan"`, `"pink"`, `"green"`, `"red"`, and `"gray"`: +There six `color=` options are `"blue"`, `"cyan"`, `"pink"`, `"green"`, `"red"`, and `"gray"`: ```{python} # | echo: false From a3d58474aaf658f4cba2019797406cb521b4716e Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 11:15:18 -0400 Subject: [PATCH 033/150] Consolidate import statements --- docs/get-started/table-theme-premade.qmd | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/get-started/table-theme-premade.qmd b/docs/get-started/table-theme-premade.qmd index fde2a745b..d7349dbb2 100644 --- a/docs/get-started/table-theme-premade.qmd +++ b/docs/get-started/table-theme-premade.qmd @@ -14,7 +14,7 @@ We'll use the basic GT object below for most examples, since it marks some of th ```{python} -from great_tables import GT, exibble +from great_tables import GT, exibble, md lil_exibble = exibble.head(5)[["num", "char", "row", "group"]] @@ -64,8 +64,6 @@ There are six styles available, each emphasizing different table parts. The `sty # | echo: false # | output: asis -from great_tables import GT, exibble, loc, style, md - print(":::{.grid}") for ii in range(1, 7): @@ -107,8 +105,6 @@ There six `color=` options are `"blue"`, `"cyan"`, `"pink"`, `"green"`, `"red"`, # | echo: false # | output: asis -from great_tables import GT, exibble, loc, style, md - print(":::{.grid}") for color_option in ["blue", "cyan", "pink", "green", "red", "gray"]: From c812efc1767eabb21a9408d3c4c711df80f1f8d7 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 11:15:55 -0400 Subject: [PATCH 034/150] Add code snippet for user experimentation --- docs/get-started/table-theme-premade.qmd | 27 +++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/get-started/table-theme-premade.qmd b/docs/get-started/table-theme-premade.qmd index d7349dbb2..d716dccdc 100644 --- a/docs/get-started/table-theme-premade.qmd +++ b/docs/get-started/table-theme-premade.qmd @@ -132,12 +132,37 @@ for color_option in ["blue", "cyan", "pink", "green", "red", "gray"]: f"
{color_option}
", gt_tbl, ":::", - sep="\n\n" + sep="\n\n", ) print(":::") ``` +
+ +You can experiment with the different `style=` and `color=` options with the following table code (used in the previous examples). + +```python +from great_tables import GT, exibble, md + +( + GT( + exibble[["num", "char", "currency", "row", "group"]], + rowname_col="row", + groupname_col="group", + ) + .tab_header( + title=md("Data listing from **exibble**"), + subtitle=md("This is a **Great Tables** dataset."), + ) + .fmt_number(columns="num") + .fmt_currency(columns="currency") + .tab_source_note(source_note="This is only a subset of the dataset.") + .opt_vertical_padding(scale=0.5) + .opt_stylize(style=1, color="blue") +) +``` + ## `opt_*()` convenience methods This section shows the different `GT.opt_*()` methods available. They serve as convenience methods for common `GT.tab_options()` tasks. From 73f6e8c97881cf54a03e7579f7cc5d78fb7f768b Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 12:35:22 -0400 Subject: [PATCH 035/150] Simplify docs in .tab_spanner() --- great_tables/_spanners.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/great_tables/_spanners.py b/great_tables/_spanners.py index 3a0360f70..3e13b83ff 100644 --- a/great_tables/_spanners.py +++ b/great_tables/_spanners.py @@ -40,22 +40,13 @@ def tab_spanner( `level` as space permits) and with `replace`, which allows for full or partial spanner replacement. - Labels may use either of three types of input: - - 1. plain text - 2. Markdown or HTML text through use of the [`md()`](`great_tables.md`) or - [`html()`](`great_tables.html`) helpers, respectively. - 3. Text set in curly braces for applying special formatting, called unit notation. For example, - "area ({{ft^2}})" would appear as "area (ft²)". - - See [`define_units()`](`great_tables.define_units`) for details on unit notation. - Parameters ---------- label The text to use for the spanner label. We can optionally use the [`md()`](`great_tables.md`) and [`html()`](`great_tables.html`) helper functions to style the text as Markdown or to - retain HTML elements in the text. + retain HTML elements in the text. Alternatively, units notation can be used (see + [`define_units()`](`great_tables.define_units`) for details.) columns The columns to target. Can either be a single column name or a series of column names provided in a list. From 37772beeb6d49b1b5a7627ebf79090d4ff2e3536 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 12:36:36 -0400 Subject: [PATCH 036/150] Update _spanners.py --- great_tables/_spanners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/great_tables/_spanners.py b/great_tables/_spanners.py index 3e13b83ff..5ae726575 100644 --- a/great_tables/_spanners.py +++ b/great_tables/_spanners.py @@ -46,7 +46,7 @@ def tab_spanner( The text to use for the spanner label. We can optionally use the [`md()`](`great_tables.md`) and [`html()`](`great_tables.html`) helper functions to style the text as Markdown or to retain HTML elements in the text. Alternatively, units notation can be used (see - [`define_units()`](`great_tables.define_units`) for details.) + [`define_units()`](`great_tables.define_units`) for details). columns The columns to target. Can either be a single column name or a series of column names provided in a list. From 1382d163643d7b696ec12fd816f2ae8fc301774c Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 13:19:33 -0400 Subject: [PATCH 037/150] Refine docs for .opt_stylize() --- docs/get-started/table-theme-premade.qmd | 106 +++-------------------- 1 file changed, 14 insertions(+), 92 deletions(-) diff --git a/docs/get-started/table-theme-premade.qmd b/docs/get-started/table-theme-premade.qmd index d716dccdc..788fa142a 100644 --- a/docs/get-started/table-theme-premade.qmd +++ b/docs/get-started/table-theme-premade.qmd @@ -12,7 +12,6 @@ There are two important kinds of `GT.opt_*()` methods: We'll use the basic GT object below for most examples, since it marks some of the table parts. - ```{python} from great_tables import GT, exibble, md @@ -20,15 +19,20 @@ lil_exibble = exibble.head(5)[["num", "char", "row", "group"]] gt_ex = ( GT(lil_exibble, rowname_col="row", groupname_col="group") - .tab_header("THE HEADING", "(a subtitle)") - .tab_stubhead("THE STUBHEAD") - .tab_source_note("THE SOURCE NOTE") + .tab_header( + title=md("Data listing from exibble"), + subtitle=md("This is a **Great Tables** dataset."), + ) + .tab_stubhead(label="row") + .fmt_number(columns="num") + .fmt_currency(columns="currency") + .tab_source_note(source_note="This is only a portion of the dataset.") + .opt_vertical_padding(scale=0.5) ) gt_ex ``` - ## `opt_stylize()`: premade themes Below are the first two premade styles. The first uses `color="blue"`, and the second uses `color="red"`. @@ -37,18 +41,18 @@ Below are the first two premade styles. The first uses `color="blue"`, and the s :::{.g-col-lg-6 .g-col-12} ```{python} -gt_ex.opt_stylize(style = 1, color = "blue") +gt_ex.opt_stylize(style=1, color="blue") ``` ::: :::{.g-col-lg-6 .g-col-12} ```{python} -gt_ex.opt_stylize(style = 2, color = "red") +gt_ex.opt_stylize(style=2, color="red") ``` ::: :::::: -Notice that first table (blue) emphasizes the row labels with a solid background color. The second table (red) emphasizes the column labels, and uses solid lines to separate the body cell values. +Notice that first table (blue) emphasizes the row labels with a solid background color. The second table (red) emphasizes the column labels, and uses solid lines to separate the body cell values. There are six options for the `color=` argument: `"blue"`, `"cyan"`, `"pink"`, `"green"`, `"red"`, and `"gray"`. There are six styles available, each emphasizing different table parts. The `style=` values are numbered from `1` to `6`: @@ -68,28 +72,12 @@ print(":::{.grid}") for ii in range(1, 7): - gt_tbl = ( - GT( - exibble[["num", "char", "currency", "row", "group"]], - rowname_col="row", - groupname_col="group", - ) - .tab_header( - title=md("Data listing from **exibble**"), - subtitle=md("This is a **Great Tables** dataset."), - ) - .fmt_number(columns="num") - .fmt_currency(columns="currency") - .tab_source_note(source_note="This is only a subset of the dataset.") - .opt_vertical_padding(scale=0.5) - .opt_stylize(style=ii) - .as_raw_html() - ) + gt_html = gt_ex.opt_stylize(style=ii).as_raw_html() print( ":::{.g-col-lg-4 .g-col-12 .shrink-example}", f"
{ii}
", - gt_tbl, + gt_html, ":::", sep="\n\n" ) @@ -97,72 +85,6 @@ for ii in range(1, 7): print(":::") ``` -
- -There six `color=` options are `"blue"`, `"cyan"`, `"pink"`, `"green"`, `"red"`, and `"gray"`: - -```{python} -# | echo: false -# | output: asis - -print(":::{.grid}") - -for color_option in ["blue", "cyan", "pink", "green", "red", "gray"]: - - gt_tbl = ( - GT( - exibble[["num", "char", "currency", "row", "group"]], - rowname_col="row", - groupname_col="group", - ) - .tab_header( - title=md("Data listing from **exibble**"), - subtitle=md("This is a **Great Tables** dataset."), - ) - .fmt_number(columns="num") - .fmt_currency(columns="currency") - .tab_source_note(source_note="This is only a subset of the dataset.") - .opt_vertical_padding(scale=0.5) - .opt_stylize(style=1, color=color_option) - .as_raw_html() - ) - - print( - ":::{.g-col-lg-4 .g-col-12 .shrink-example}", - f"
{color_option}
", - gt_tbl, - ":::", - sep="\n\n", - ) - -print(":::") -``` - -
- -You can experiment with the different `style=` and `color=` options with the following table code (used in the previous examples). - -```python -from great_tables import GT, exibble, md - -( - GT( - exibble[["num", "char", "currency", "row", "group"]], - rowname_col="row", - groupname_col="group", - ) - .tab_header( - title=md("Data listing from **exibble**"), - subtitle=md("This is a **Great Tables** dataset."), - ) - .fmt_number(columns="num") - .fmt_currency(columns="currency") - .tab_source_note(source_note="This is only a subset of the dataset.") - .opt_vertical_padding(scale=0.5) - .opt_stylize(style=1, color="blue") -) -``` - ## `opt_*()` convenience methods This section shows the different `GT.opt_*()` methods available. They serve as convenience methods for common `GT.tab_options()` tasks. From e9540ec1520017c07533b7a3d4c354ce9a720a5c Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 16:35:56 -0400 Subject: [PATCH 038/150] Link to Get Started article from opt_stylize() --- great_tables/_options.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index b1e7c11b2..ffc90d706 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -1250,7 +1250,9 @@ def opt_stylize(self: GTSelf, style: int = 1, color: str = "blue") -> GTSelf: have vertical lines. In addition to choosing a `style` preset, there are six `color` variations that each use a range of five color tints. Each of the color tints have been fine-tuned to maximize the contrast between text and its background. There are 36 combinations of `style` and - `color` to choose from. + `color` to choose from. For examples of each style, see the + [*Table Theme Options*](../docs/user_guide/table_theme_options.md) section of the + **Get Started** guide. Parameters ---------- From 8721eae272370d1598640aa52a825733ab939952 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 16:36:35 -0400 Subject: [PATCH 039/150] Link to opt_stylize() docs page from guide --- docs/get-started/table-theme-premade.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/get-started/table-theme-premade.qmd b/docs/get-started/table-theme-premade.qmd index 788fa142a..6f55da3fe 100644 --- a/docs/get-started/table-theme-premade.qmd +++ b/docs/get-started/table-theme-premade.qmd @@ -52,7 +52,7 @@ gt_ex.opt_stylize(style=2, color="red") ::: :::::: -Notice that first table (blue) emphasizes the row labels with a solid background color. The second table (red) emphasizes the column labels, and uses solid lines to separate the body cell values. There are six options for the `color=` argument: `"blue"`, `"cyan"`, `"pink"`, `"green"`, `"red"`, and `"gray"`. +Notice that first table (blue) emphasizes the row labels with a solid background color. The second table (red) emphasizes the column labels, and uses solid lines to separate the body cell values. See [`.opt_stylize()`](`great_tables.GT.opt_stylize`) for all available color options. There are six styles available, each emphasizing different table parts. The `style=` values are numbered from `1` to `6`: From cc43aceaaa839ebab2609277068024ec7e5ede85 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 16:39:25 -0400 Subject: [PATCH 040/150] Modify link text --- docs/get-started/table-theme-premade.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/get-started/table-theme-premade.qmd b/docs/get-started/table-theme-premade.qmd index 6f55da3fe..edae776d2 100644 --- a/docs/get-started/table-theme-premade.qmd +++ b/docs/get-started/table-theme-premade.qmd @@ -52,7 +52,7 @@ gt_ex.opt_stylize(style=2, color="red") ::: :::::: -Notice that first table (blue) emphasizes the row labels with a solid background color. The second table (red) emphasizes the column labels, and uses solid lines to separate the body cell values. See [`.opt_stylize()`](`great_tables.GT.opt_stylize`) for all available color options. +Notice that first table (blue) emphasizes the row labels with a solid background color. The second table (red) emphasizes the column labels, and uses solid lines to separate the body cell values. See [`opt_stylize()`](`great_tables.GT.opt_stylize`) for all available color options. There are six styles available, each emphasizing different table parts. The `style=` values are numbered from `1` to `6`: From 896f21cefed01d9240e5779a3d756d439a44e410 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 17:17:56 -0400 Subject: [PATCH 041/150] Make correction to link --- great_tables/_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index ffc90d706..a6f0781b4 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -1251,7 +1251,7 @@ def opt_stylize(self: GTSelf, style: int = 1, color: str = "blue") -> GTSelf: that each use a range of five color tints. Each of the color tints have been fine-tuned to maximize the contrast between text and its background. There are 36 combinations of `style` and `color` to choose from. For examples of each style, see the - [*Table Theme Options*](../docs/user_guide/table_theme_options.md) section of the + [*Table Theme Options*](https://posit-dev.github.io/great-tables/get-started/table-theme-options.html) section of the **Get Started** guide. Parameters From 2b0b2d457e0fe60134e64b8e6a5420937fa81085 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 12 Sep 2024 17:25:43 -0400 Subject: [PATCH 042/150] Link to correct page in Get Started --- great_tables/_options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index a6f0781b4..cf169fdc1 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -1251,8 +1251,8 @@ def opt_stylize(self: GTSelf, style: int = 1, color: str = "blue") -> GTSelf: that each use a range of five color tints. Each of the color tints have been fine-tuned to maximize the contrast between text and its background. There are 36 combinations of `style` and `color` to choose from. For examples of each style, see the - [*Table Theme Options*](https://posit-dev.github.io/great-tables/get-started/table-theme-options.html) section of the - **Get Started** guide. + [*Premade Themes*](https://posit-dev.github.io/great-tables/get-started/table-theme-premade.html) + section of the **Get Started** guide. Parameters ---------- From 8fa2fd4ce5b305238ba8399dd500024789e9e24d Mon Sep 17 00:00:00 2001 From: jrycw Date: Mon, 16 Sep 2024 14:47:02 +0800 Subject: [PATCH 043/150] Update FmtImage.to_html() --- great_tables/_formats.py | 4 +--- tests/test_formats.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 645ca947e..68882e30e 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -3492,7 +3492,6 @@ class FmtImage: SPAN_TEMPLATE: ClassVar = '{}' def to_html(self, val: Any): - import re from pathlib import Path # TODO: are we assuming val is a string? (or coercing?) @@ -3526,9 +3525,8 @@ def to_html(self, val: Any): for file in full_files: # Case 1: from url if self.path and (self.path.startswith("http://") or self.path.startswith("https://")): - norm_path = re.sub(r"/\s+$", self.path) + norm_path = self.path.rstrip().removesuffix("/") uri = f"{norm_path}/{file}" - # Case 2: else: filename = (Path(self.path or "") / file).expanduser().absolute() diff --git a/tests/test_formats.py b/tests/test_formats.py index b021c4964..88e7650e3 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1489,6 +1489,20 @@ def test_fmt_image_path(): assert 'src="/a/b/c"' in strip_windows_drive(res) +@pytest.mark.parametrize( + "url", ["http://posit.co/", "http://posit.co", "https://posit.co/", "https://posit.co"] +) +def test_fmt_image_path_http(url: str): + formatter = FmtImage(encode=False, height=30, path=url) + res = formatter.to_html("c") + dst_img = ''.format( + url.removesuffix("/") + ) + dst = formatter.SPAN_TEMPLATE.format(dst_img) + + assert strip_windows_drive(res) == dst + + @pytest.mark.parametrize( "src,dst", [ From 6f55a84348a92457199374105ca7eea63befc561 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Mon, 16 Sep 2024 09:59:08 -0400 Subject: [PATCH 044/150] fix: missed variable renaming in git merge --- great_tables/_spanners.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/great_tables/_spanners.py b/great_tables/_spanners.py index 9220584fb..e0ffb6cb8 100644 --- a/great_tables/_spanners.py +++ b/great_tables/_spanners.py @@ -171,7 +171,7 @@ def tab_spanner( raise NotImplementedError("columns/spanners must be specified") # get column names associated with selected spanners ---- - _vars = [span.vars for span in data._spanners if span.spanner_id in spanner_ids] + _vars = [span.vars for span in self._spanners if span.spanner_id in spanner_ids] spanner_column_names = OrderedSet(itertools.chain(*_vars)) column_names = list(OrderedSet([*selected_column_names, *spanner_column_names])) @@ -179,7 +179,7 @@ def tab_spanner( # get spanner level ---- if level is None: - level = data._spanners.next_level(list(column_names)) + level = self._spanners.next_level(list(column_names)) # get spanner units and labels ---- # TODO: grep units from {{.*}}, may need to switch delimiters From ae77aa6973e1b88d2ef8b31d5568392daa92ef1a Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 16 Sep 2024 12:26:53 -0400 Subject: [PATCH 045/150] Use Quarto linking --- great_tables/_options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index cf169fdc1..20179207a 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -1251,8 +1251,8 @@ def opt_stylize(self: GTSelf, style: int = 1, color: str = "blue") -> GTSelf: that each use a range of five color tints. Each of the color tints have been fine-tuned to maximize the contrast between text and its background. There are 36 combinations of `style` and `color` to choose from. For examples of each style, see the - [*Premade Themes*](https://posit-dev.github.io/great-tables/get-started/table-theme-premade.html) - section of the **Get Started** guide. + [*Premade Themes*](/docs/get-started/table-theme-premade.qmd) section of the **Get Started** + guide. Parameters ---------- From 49a46377c182437a759c1c62135837e300d0e936 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 16 Sep 2024 13:02:57 -0400 Subject: [PATCH 046/150] Use relative path for quarto link --- great_tables/_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index 20179207a..3887f8184 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -1251,7 +1251,7 @@ def opt_stylize(self: GTSelf, style: int = 1, color: str = "blue") -> GTSelf: that each use a range of five color tints. Each of the color tints have been fine-tuned to maximize the contrast between text and its background. There are 36 combinations of `style` and `color` to choose from. For examples of each style, see the - [*Premade Themes*](/docs/get-started/table-theme-premade.qmd) section of the **Get Started** + [*Premade Themes*](../get-started/table-theme-premade.qmd) section of the **Get Started** guide. Parameters From 97dba1c518ff33c1670403406629f737261d39a7 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 16 Sep 2024 16:43:36 -0400 Subject: [PATCH 047/150] Make updates to PyCon blog post --- docs/blog/pycon-2024-great-tables-are-possible/index.qmd | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/blog/pycon-2024-great-tables-are-possible/index.qmd b/docs/blog/pycon-2024-great-tables-are-possible/index.qmd index 73f24172e..a80f0ba38 100644 --- a/docs/blog/pycon-2024-great-tables-are-possible/index.qmd +++ b/docs/blog/pycon-2024-great-tables-are-possible/index.qmd @@ -9,7 +9,7 @@ freeze: true The Great Tables crew is excited to share that we'll be presenting on tables at PyCon 2024! If you're around and want to meet, be sure to stop by the Posit Booth, or reach out on linkedin to [Rich](https://www.linkedin.com/in/richard-iannone-a5640017/) or [Michael](https://www.linkedin.com/in/michael-a-chow/)! -The talk, Making Beautiful, Publication Quality Tables is Possible in 2024 is [10:45am Friday](https://us.pycon.org/2024/schedule/presentation/65/). +The talk, Making Beautiful, Publication Quality Tables is Possible in 2024 happened on [May 17, 2024](https://us.pycon.org/2024/schedule/presentation/65/). You can watch the [recording on YouTube](https://youtu.be/08yLWPpFdo4?si=vBK9h-ObXNKp9tHH). In addition to the talk, there are two other events worth mentioning: @@ -79,9 +79,10 @@ We’ll cover the following: Check out these resources to learn more about the wild and beautiful life of display tables: -* [Great Tables example gallery](/docs/examples) -* [The Design Philosophy of Great Tables (blog post)](http://localhost:6877/blog/design-philosophy/) +* [Great Tables example gallery](../examples/index.html) +* [The Design Philosophy of Great Tables (blog post)](blog/design-philosophy/) * [20 Minute Table Tutorial by Albert Rapp](https://youtu.be/ESyWcOFuMQc?si=1_bBRZEKENFKVNpB) +* [PyCon talk: Making Beautiful, Publication Quality Tables is Possible in 2024](https://youtu.be/08yLWPpFdo4?si=vBK9h-ObXNKp9tHH) ## Hope all your tables are great! From d5489ff498b3770e46447e569ed34c061c81faa0 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 16 Sep 2024 16:54:04 -0400 Subject: [PATCH 048/150] Update SciPy 2024 talk information --- docs/blog/tables-for-scientific-publishing/index.qmd | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/blog/tables-for-scientific-publishing/index.qmd b/docs/blog/tables-for-scientific-publishing/index.qmd index 3faefe869..279aa6607 100644 --- a/docs/blog/tables-for-scientific-publishing/index.qmd +++ b/docs/blog/tables-for-scientific-publishing/index.qmd @@ -21,9 +21,9 @@ In this post, we'll review the big pieces that scientific tables need: We've added **six new datasets**, to help quickly show off scientific publishing! We'll use the new `reactions` and `gibraltar` datasets to create examples in the fields of Atmospheric Chemistry and Meteorology, respectively. :::{.callout-tip} -Rich will be speaking on this at SciPy! +Rich presented on this topic at SciPy 2024! -If you're at SciPy 2024 in Tacoma, WA, Rich's talk is scheduled for July 11, 2024 (16:30–17:00 PT). The talk is called *Great Tables for Everyone* and it's sure to be both exciting and educational. If you're not attending that's okay, the talk is available [in GitHub](https://github.com/rich-iannone/presentations/tree/main/2024-07-11-SciPy-talk-GT). +At SciPy 2024 (July 11, 2024, Tacoma, WA), Rich delivered a talk called *Great Tables for Everyone* and it presented some of the tables shown in this post. If you weren't in attendence that's okay, you can [watch the recorded talk](https://youtu.be/uvH-Z39ZUj0?si=3NVipMteXaeO3vb1) and the materials are available [in GitHub](https://github.com/rich-iannone/presentations/tree/main/2024-07-11-SciPy-talk-GT). ::: ## Unit and scientific notation @@ -160,5 +160,3 @@ Units notation is ever useful and it is applied in one of the column labels of t ## Hope all your (science-y) tables are great! We did scientific work pretty heavily in the past and so we understand that great tables in the realm of science publication is something that could and should be possible. We'll keep doing more to make this even better in upcoming releases. - -Hope to see you at SciPy 2024! From 5b06a222c2aa4b3ef5975a93e0a09263f2e6a3e9 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 16 Sep 2024 16:55:36 -0400 Subject: [PATCH 049/150] Make tweak to conference information --- docs/blog/tables-for-scientific-publishing/index.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blog/tables-for-scientific-publishing/index.qmd b/docs/blog/tables-for-scientific-publishing/index.qmd index 279aa6607..e48057186 100644 --- a/docs/blog/tables-for-scientific-publishing/index.qmd +++ b/docs/blog/tables-for-scientific-publishing/index.qmd @@ -23,7 +23,7 @@ We've added **six new datasets**, to help quickly show off scientific publishing :::{.callout-tip} Rich presented on this topic at SciPy 2024! -At SciPy 2024 (July 11, 2024, Tacoma, WA), Rich delivered a talk called *Great Tables for Everyone* and it presented some of the tables shown in this post. If you weren't in attendence that's okay, you can [watch the recorded talk](https://youtu.be/uvH-Z39ZUj0?si=3NVipMteXaeO3vb1) and the materials are available [in GitHub](https://github.com/rich-iannone/presentations/tree/main/2024-07-11-SciPy-talk-GT). +At SciPy 2024 (on July 11, 2024), Rich delivered a talk called *Great Tables for Everyone* and it presented some of the tables shown in this post. If you weren't in attendence that's okay, you can [watch the recorded talk](https://youtu.be/uvH-Z39ZUj0?si=3NVipMteXaeO3vb1) and the materials are available [in GitHub](https://github.com/rich-iannone/presentations/tree/main/2024-07-11-SciPy-talk-GT). ::: ## Unit and scientific notation From 029ffb026bd06a7f71e5e3c684dcec567d082ef7 Mon Sep 17 00:00:00 2001 From: jrycw Date: Tue, 17 Sep 2024 19:30:34 +0800 Subject: [PATCH 050/150] Favor `OrderedSet().as_list()` --- great_tables/_gt_data.py | 4 +++- great_tables/_spanners.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index 0b891198b..64ec6d314 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -577,7 +577,9 @@ def from_data(cls, data, rowname_col: str | None = None, groupname_col: str | No row_info = [RowInfo(*i) for i in zip(row_indices, group_id, row_names)] # create groups, and ensure they're ordered by first observed - group_names = list(OrderedSet(row.group_id for row in row_info if row.group_id is not None)) + group_names = OrderedSet( + row.group_id for row in row_info if row.group_id is not None + ).as_list() group_rows = GroupRows(data, group_key=groupname_col).reorder(group_names) return cls(row_info, group_rows) diff --git a/great_tables/_spanners.py b/great_tables/_spanners.py index 592a896f4..24d888fef 100644 --- a/great_tables/_spanners.py +++ b/great_tables/_spanners.py @@ -173,9 +173,9 @@ def tab_spanner( # get column names associated with selected spanners ---- _vars = [span.vars for span in self._spanners if span.spanner_id in spanner_ids] - spanner_column_names = OrderedSet(itertools.chain(*_vars)) + spanner_column_names = OrderedSet(itertools.chain(*_vars)).as_list() - column_names = list(OrderedSet([*selected_column_names, *spanner_column_names])) + column_names = OrderedSet([*selected_column_names, *spanner_column_names]).as_list() # combine columns names and those from spanners ---- # get spanner level ---- From 2ba634b8e870520c8e821887e83adf595922e865 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 17 Sep 2024 13:06:09 -0400 Subject: [PATCH 051/150] Make updates to PyCon 2024 post --- .../index.qmd | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/docs/blog/pycon-2024-great-tables-are-possible/index.qmd b/docs/blog/pycon-2024-great-tables-are-possible/index.qmd index a80f0ba38..81fca7578 100644 --- a/docs/blog/pycon-2024-great-tables-are-possible/index.qmd +++ b/docs/blog/pycon-2024-great-tables-are-possible/index.qmd @@ -42,31 +42,25 @@ Note three important pieces: 3. **The nanoplot** on the right shows a tiny bargraph for monthly sales over the past year. This makes it easy to spot trends, and can be hovered over to get exact values. -Critically, the code for this table used the DataFrame library [Polars](https://pola.rs/), which -makes it really [easy to select rows and columns for styling](../polars-styling). +Critically, the code for this table used the DataFrame library [Polars](https://pola.rs/), which makes it really [easy to select rows and columns for styling](../polars-styling/index.qmd). ## What's next? ### The 2024 Table Contest -The world's premier display table contest---the [4th annual Table Contest](https://posit.co/blog/announcing-the-2024-table-contest/) draws competitors from near and far, -to showcase the latest and greatest examples in table presentation. -The contest is happening now, with **submissions due by May 31st, 2024**. +The world's premier display table contest---the [4th annual Table Contest](https://posit.co/blog/announcing-the-2024-table-contest/) draws competitors from near and far, to showcase the latest and greatest examples in table presentation. -For inspiration, see these resources: - -* [Contest announcement](https://posit.co/blog/announcing-the-2024-table-contest/) -* [2022 winners and honorable mentions](https://posit.co/blog/winners-of-the-2022-table-contest/) +The contest was a great success! On July 1, 2024 we announced the [2024 winners and honorable mentions](https://posit.co/blog/2024-table-contest-winners/). Check 'em out! ### posit::conf() workshop -We're planning a posit::conf() 2024 workshop in August, called [Making Tables with gt and Great Tables](https://reg.conf.posit.co/flow/posit/positconf24/publiccatalog/page/publiccatalog/session/1707334049004001S0l2). +We held a posit::conf() 2024 workshop, and you can find the materials at the [Making Tables with gt and Great Tables repo](https://github.com/posit-conf-2024/tables). -If you're curious about making beautiful, publication quality tables in Python or R, we'd love to have you! +If you're curious about making beautiful, publication quality tables in Python or R, do have a look at the resources at that GitHub repository. -We’ll cover the following: +We covered the following table topics: * Create table components and put them together (e.g., header, footer, stub, etc.) * Format cell values (numeric/scientific, date/datetime, etc.) @@ -79,13 +73,11 @@ We’ll cover the following: Check out these resources to learn more about the wild and beautiful life of display tables: -* [Great Tables example gallery](../examples/index.html) -* [The Design Philosophy of Great Tables (blog post)](blog/design-philosophy/) +* [Great Tables example gallery](../../examples/index.qmd) +* [The Design Philosophy of Great Tables (blog post)](../design-philosophy/index.qmd) * [20 Minute Table Tutorial by Albert Rapp](https://youtu.be/ESyWcOFuMQc?si=1_bBRZEKENFKVNpB) * [PyCon talk: Making Beautiful, Publication Quality Tables is Possible in 2024](https://youtu.be/08yLWPpFdo4?si=vBK9h-ObXNKp9tHH) ## Hope all your tables are great! -A huge thanks to all the people who have contributed to Great Tables over the past year. -It's been a really incredible journey. -Hope to see you at PyCon 2024! +A huge thanks to all the people who have contributed to Great Tables over the past year. It's been a really incredible journey! From a7f5b4f4f754e74bc814a1fcef097cfee7784f52 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 17 Sep 2024 14:50:12 -0400 Subject: [PATCH 052/150] Use `OrderedSet.as_list()` to get unique CSS rules --- great_tables/_scss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/great_tables/_scss.py b/great_tables/_scss.py index d57a8065a..12c06caad 100644 --- a/great_tables/_scss.py +++ b/great_tables/_scss.py @@ -152,7 +152,7 @@ def compile_scss( # Ensure that list items in `additional_css` are unique and then combine statements while # separating with `\n`; use an empty string if list is empty or value is None if has_additional_css: - additional_css_unique = _unique_set(additional_css) + additional_css_unique = OrderedSet(additional_css).as_list() table_additional_css = "\n".join(additional_css_unique) + "\n" else: table_additional_css = "" From dc859f0d5a1c696d489e2418a480e69ec30379b3 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 17 Sep 2024 16:58:52 -0400 Subject: [PATCH 053/150] Allow `font` to take a GoogleFont input --- great_tables/_styles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/great_tables/_styles.py b/great_tables/_styles.py index 0411cbb28..c14cb1937 100644 --- a/great_tables/_styles.py +++ b/great_tables/_styles.py @@ -188,7 +188,7 @@ class CellStyleText(CellStyle): """ color: str | ColumnExpr | None = None - font: str | ColumnExpr | None = None + font: str | ColumnExpr | GoogleFont | None = None size: str | ColumnExpr | None = None align: Literal["center", "left", "right", "justify"] | ColumnExpr | None = None v_align: Literal["middle", "top", "bottom"] | ColumnExpr | None = None From a043032b8a5d1a77b322e12c8b1c7913b6d5470b Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 17 Sep 2024 16:59:28 -0400 Subject: [PATCH 054/150] Add tests for various `cell_text(font=)` inputs --- tests/test_tab_create_modify.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/test_tab_create_modify.py b/tests/test_tab_create_modify.py index bae225ee9..646b4f7fe 100644 --- a/tests/test_tab_create_modify.py +++ b/tests/test_tab_create_modify.py @@ -1,6 +1,7 @@ import pandas as pd +import polars as pl import pytest -from great_tables import GT, style, loc, google_font +from great_tables import GT, style, loc, google_font, from_column from great_tables._locations import LocBody from great_tables._styles import CellStyleFill from great_tables._tab_create_modify import tab_style @@ -37,7 +38,7 @@ def test_tab_style_google_font(gt: GT): new_gt = tab_style( gt, style=style.text(font=google_font(name="IBM Plex Mono")), - locations=loc.body(columns="time"), + locations=loc.body(columns="x"), ) rendered_html = new_gt.as_raw_html() @@ -46,3 +47,30 @@ def test_tab_style_google_font(gt: GT): "@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap');" ) assert rendered_html.find("font-family: IBM Plex Mono;") + + +def test_tab_style_font_local(gt: GT): + + new_gt = tab_style( + gt, + style=style.text(font="Courier"), + locations=loc.body(columns="x"), + ) + + rendered_html = new_gt.as_raw_html() + + assert rendered_html.find('1') + + +def test_tab_style_font_from_column(): + + tbl = pl.DataFrame({"x": [1, 2], "font": ["Helvetica", "Courier"]}) + + gt_tbl = GT(tbl).tab_style( + style=style.text(font=from_column(column="font")), locations=loc.body(columns="x") + ) + + rendered_html = gt_tbl.as_raw_html() + + assert rendered_html.find('1') + assert rendered_html.find('2') From 10d6cd96ddbf4ce1657f9251aa50bba8142f5728 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 17 Sep 2024 16:59:31 -0400 Subject: [PATCH 055/150] refactor: prepare to use loc classes directly, not strings --- great_tables/_locations.py | 40 ++++++++++++++-------------- great_tables/_utils_render_html.py | 42 ++++++++++++++++++------------ great_tables/loc.py | 17 +++++++----- tests/test_locations.py | 4 +-- tests/test_tab_create_modify.py | 2 +- 5 files changed, 59 insertions(+), 46 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 8aa2cd22c..2da44a81c 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -3,7 +3,7 @@ import itertools from dataclasses import dataclass, field from functools import singledispatch -from typing import TYPE_CHECKING, Any, Callable, Literal, Union +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, Union from typing_extensions import TypeAlias @@ -57,48 +57,48 @@ class CellPos: class Loc: """A location.""" - groups: LocName + groups: ClassVar[LocName] @dataclass class LocHeader(Loc): """A location for targeting the table title and subtitle.""" - groups: LocHeaderName = "header" + groups: ClassVar[LocHeaderName] = "header" @dataclass class LocTitle(Loc): - groups: Literal["title"] = "title" + groups: ClassVar[Literal["title"]] = "title" @dataclass class LocSubTitle(Loc): - groups: Literal["subtitle"] = "subtitle" + groups: ClassVar[Literal["subtitle"]] = "subtitle" @dataclass class LocStubhead(Loc): """A location for targeting the table stubhead and stubhead label.""" - groups: LocStubheadName = "stubhead" + groups: ClassVar[LocStubheadName] = "stubhead" @dataclass class LocStubheadLabel(Loc): - groups: Literal["stubhead_label"] = "stubhead_label" + groups: ClassVar[Literal["stubhead_label"]] = "stubhead_label" @dataclass class LocColumnLabels(Loc): """A location for column spanners and column labels.""" - groups: LocColumnLabelsName = "column_labels" + groups: ClassVar[LocColumnLabelsName] = "column_labels" @dataclass class LocColumnLabel(Loc): - groups: Literal["column_label"] = "column_label" + groups: ClassVar[Literal["column_label"]] = "column_label" columns: SelectExpr = None @@ -106,7 +106,7 @@ class LocColumnLabel(Loc): class LocSpannerLabel(Loc): """A location for column spanners.""" - groups: Literal["spanner_label"] = "spanner_label" + groups: ClassVar[Literal["spanner_label"]] = "spanner_label" ids: SelectExpr = None @@ -114,25 +114,25 @@ class LocSpannerLabel(Loc): class LocStub(Loc): """A location for targeting the table stub, row group labels, summary labels, and body.""" - groups: Literal["stub"] = "stub" + groups: ClassVar[Literal["stub"]] = "stub" rows: RowSelectExpr = None @dataclass class LocRowGroupLabel(Loc): - groups: Literal["row_group_label"] = "row_group_label" + groups: ClassVar[Literal["row_group_label"]] = "row_group_label" rows: RowSelectExpr = None @dataclass class LocRowLabel(Loc): - groups: Literal["row_label"] = "row_label" + groups: ClassVar[Literal["row_label"]] = "row_label" rows: RowSelectExpr = None @dataclass class LocSummaryLabel(Loc): - groups: Literal["summary_label"] = "summary_label" + groups: ClassVar[Literal["summary_label"]] = "summary_label" rows: RowSelectExpr = None @@ -163,7 +163,7 @@ class LocBody(Loc): ------ See [`GT.tab_style()`](`great_tables.GT.tab_style`). """ - groups: LocBodyName = "body" + groups: ClassVar[LocBodyName] = "data" columns: SelectExpr = None rows: RowSelectExpr = None @@ -172,19 +172,19 @@ class LocBody(Loc): @dataclass class LocSummary(Loc): # TODO: these can be tidyselectors - groups: LocName = "summary" + groups: ClassVar[LocName] = "summary" columns: SelectExpr = None rows: RowSelectExpr = None @dataclass class LocFooter(Loc): - groups: LocFooterName = "footer" + groups: ClassVar[LocFooterName] = "footer" @dataclass class LocFootnotes(Loc): - groups: Literal["footnotes"] = "footnotes" + groups: ClassVar[Literal["footnotes"]] = "footnotes" @dataclass @@ -192,7 +192,7 @@ class LocSourceNotes(Loc): # This dataclass in R has a `groups` field, which is a literal value. # In python, we can use an isinstance check to determine we're seeing an # instance of this class - groups: Literal["source_notes"] = "source_notes" + groups: ClassVar[Literal["source_notes"]] = "source_notes" # Utils ================================================================================ @@ -616,7 +616,7 @@ def _(loc: LocBody, data: GTData, style: list[CellStyle]) -> GTData: for col_pos in positions: row_styles = [entry._from_row(data._tbl_data, col_pos.row) for entry in style_ready] crnt_info = StyleInfo( - locname="cell", locnum=5, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles + locname="data", locnum=5, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles ) all_info.append(crnt_info) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index cbdf32654..2469596d3 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -11,6 +11,14 @@ from ._tbl_data import _get_cell, cast_frame_to_string, n_rows, replace_null_frame from ._text import _process_text, _process_text_id from ._utils import heading_has_subtitle, heading_has_title, seq_groups +from . import _locations as loc + + +def _is_loc(loc: str | loc.Loc, cls: type[loc.Loc]): + if isinstance(loc, str): + return loc == cls.groups + + return isinstance(loc, cls) def _flatten_styles(styles: Styles, wrap: bool = False) -> str: @@ -53,9 +61,9 @@ def create_heading_component_h(data: GTData) -> str: subtitle = _process_text(subtitle) # Filter list of StyleInfo for the various header components - styles_header = [x for x in data._styles if x.locname == "header"] - styles_title = [x for x in data._styles if x.locname == "title"] - styles_subtitle = [x for x in data._styles if x.locname == "subtitle"] + styles_header = [x for x in data._styles if _is_loc(x.locname, loc.LocHeader)] + styles_title = [x for x in data._styles if _is_loc(x.locname, loc.LocTitle)] + styles_subtitle = [x for x in data._styles if _is_loc(x.locname, loc.LocSubTitle)] header_style = _flatten_styles(styles_header, wrap=True) if styles_header else "" title_style = _flatten_styles(styles_title, wrap=True) if styles_title else "" subtitle_style = _flatten_styles(styles_subtitle, wrap=True) if styles_subtitle else "" @@ -125,11 +133,11 @@ def create_columns_component_h(data: GTData) -> str: headings_info = boxhead._get_default_columns() # Filter list of StyleInfo for the various stubhead and column labels components - styles_stubhead = [x for x in data._styles if x.locname == "stubhead"] - styles_stubhead_label = [x for x in data._styles if x.locname == "stubhead_label"] - styles_column_labels = [x for x in data._styles if x.locname == "column_labels"] - styles_spanner_label = [x for x in data._styles if x.locname == "spanner_label"] - styles_column_label = [x for x in data._styles if x.locname == "column_label"] + styles_stubhead = [x for x in data._styles if _is_loc(x.locname, loc.LocStubhead)] + styles_stubhead_label = [x for x in data._styles if _is_loc(x.locname, loc.LocStubheadLabel)] + styles_column_labels = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnLabels)] + styles_spanner_label = [x for x in data._styles if _is_loc(x.locname, loc.LocSpannerLabel)] + styles_column_label = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnLabel)] # If columns are present in the stub, then replace with a set stubhead label or nothing if len(stub_layout) > 0 and stubh is not None: @@ -410,16 +418,16 @@ def create_body_component_h(data: GTData) -> str: tbl_data = replace_null_frame(data._body.body, _str_orig_data) # Filter list of StyleInfo to only those that apply to the stub - styles_stub = [x for x in data._styles if x.locname == "stub"] - styles_row_group_label = [x for x in data._styles if x.locname == "row_group_label"] - styles_row_label = [x for x in data._styles if x.locname == "row_label"] - styles_summary_label = [x for x in data._styles if x.locname == "summary_label"] + styles_stub = [x for x in data._styles if _is_loc(x.locname, loc.LocStub)] + styles_row_group_label = [x for x in data._styles if _is_loc(x.locname, loc.LocRowGroupLabel)] + styles_row_label = [x for x in data._styles if _is_loc(x.locname, loc.LocRowLabel)] + styles_summary_label = [x for x in data._styles if _is_loc(x.locname, loc.LocSummaryLabel)] stub_style = _flatten_styles(styles_stub, wrap=True) if styles_stub else "" # Filter list of StyleInfo to only those that apply to the body - styles_cells = [x for x in data._styles if x.locname == "cell"] - styles_body = [x for x in data._styles if x.locname == "body"] - styles_summary = [x for x in data._styles if x.locname == "summary"] + styles_cells = [x for x in data._styles if _is_loc(x.locname, loc.LocBody)] + # styles_body = [x for x in data._styles if _is_loc(x.locname, loc.LocBody2)] + # styles_summary = [x for x in data._styles if _is_loc(x.locname, loc.LocSummary)] # Get the default column vars column_vars = data._boxhead._get_default_columns() @@ -514,7 +522,7 @@ def create_source_notes_component_h(data: GTData) -> str: source_notes = data._source_notes # Filter list of StyleInfo to only those that apply to the source notes - styles_source_notes = [x for x in data._styles if x.locname == "source_notes"] + styles_source_notes = [x for x in data._styles if _is_loc(x.locname, loc.LocSourceNotes)] # If there are no source notes, then return an empty string if source_notes == []: @@ -583,7 +591,7 @@ def create_source_notes_component_h(data: GTData) -> str: def create_footnotes_component_h(data: GTData): # Filter list of StyleInfo to only those that apply to the footnotes - styles_footnotes = [x for x in data._styles if x.locname == "footnotes"] + styles_footnotes = [x for x in data._styles if _is_loc(x.locname, loc.LocFootnotes)] return "" diff --git a/great_tables/loc.py b/great_tables/loc.py index 6c4e68951..07c637a6f 100644 --- a/great_tables/loc.py +++ b/great_tables/loc.py @@ -1,26 +1,31 @@ from __future__ import annotations from ._locations import ( - # Header elements + # Header ---- LocHeader as header, LocTitle as title, LocSubTitle as subtitle, - # Stubhead elements + # + # Stubhead ---- LocStubhead as stubhead, LocStubheadLabel as stubhead_label, - # Column Labels elements + # + # Column Labels ---- LocColumnLabels as column_labels, LocSpannerLabel as spanner_label, LocColumnLabel as column_label, - # Stub elements + # + # Stub ---- LocStub as stub, LocRowGroupLabel as row_group_label, LocRowLabel as row_label, LocSummaryLabel as summary_label, - # Body elements + # + # Body ---- LocBody as body, LocSummary as summary, - # Footer elements + # + # Footer ---- LocFooter as footer, LocFootnotes as footnotes, LocSourceNotes as source_notes, diff --git a/tests/test_locations.py b/tests/test_locations.py index 334600410..bb0988327 100644 --- a/tests/test_locations.py +++ b/tests/test_locations.py @@ -178,7 +178,7 @@ def test_set_style_loc_body_from_column(expr): new_gt = set_style(loc, gt_df, [style]) # 1 style info added - assert len(new_gt._styles) == 2 + assert len(new_gt._styles) == 1 cell_info = new_gt._styles[0] # style info has single cell style, with new color @@ -190,7 +190,7 @@ def test_set_style_loc_body_from_column(expr): def test_set_style_loc_title_from_column_error(snapshot): df = pd.DataFrame({"x": [1, 2], "color": ["red", "blue"]}) gt_df = GT(df) - loc = LocTitle("title") + loc = LocTitle() style = CellStyleText(color=FromColumn("color")) with pytest.raises(TypeError) as exc_info: diff --git a/tests/test_tab_create_modify.py b/tests/test_tab_create_modify.py index 12ad0275c..f88ae4fea 100644 --- a/tests/test_tab_create_modify.py +++ b/tests/test_tab_create_modify.py @@ -16,7 +16,7 @@ def test_tab_style(gt: GT): new_gt = tab_style(gt, style, LocBody(["x"], [0])) assert len(gt._styles) == 0 - assert len(new_gt._styles) == 2 + assert len(new_gt._styles) == 1 assert len(new_gt._styles[0].styles) == 1 assert new_gt._styles[0].styles[0] is style From 39902a06ba63acee3bea42b318c769e98ea00b61 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 17 Sep 2024 17:19:31 -0400 Subject: [PATCH 056/150] refactor: clean up last use of .locname --- great_tables/_locations.py | 68 +++++++++++------------------------- great_tables/_modify_rows.py | 6 +++- 2 files changed, 26 insertions(+), 48 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 2da44a81c..7196f5e8a 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -473,11 +473,11 @@ def _(loc: LocHeader, data: GTData, style: list[CellStyle]) -> GTData: # set ---- if loc.groups == "header": - info = StyleInfo(locname="header", locnum=1, styles=style) + info = StyleInfo(locname=loc, locnum=1, styles=style) elif loc.groups == "title": - info = StyleInfo(locname="title", locnum=2, styles=style) + info = StyleInfo(locname=loc, locnum=2, styles=style) elif loc.groups == "subtitle": - info = StyleInfo(locname="subtitle", locnum=3, styles=style) + info = StyleInfo(locname=loc, locnum=3, styles=style) else: raise ValueError(f"Unknown title group: {loc.groups}") return data._replace(_styles=data._styles + [info]) @@ -488,9 +488,7 @@ def _(loc: LocTitle, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace( - _styles=data._styles + [StyleInfo(locname="title", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -498,9 +496,7 @@ def _(loc: LocSubTitle, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace( - _styles=data._styles + [StyleInfo(locname="subtitle", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -508,9 +504,7 @@ def _(loc: LocStubhead, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace( - _styles=data._styles + [StyleInfo(locname="stubhead", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -518,9 +512,7 @@ def _(loc: LocStubheadLabel, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace( - _styles=data._styles + [StyleInfo(locname="stubhead_label", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -528,9 +520,7 @@ def _(loc: LocColumnLabels, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace( - _styles=data._styles + [StyleInfo(locname="column_labels", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -543,7 +533,7 @@ def _(loc: LocColumnLabel, data: GTData, style: list[CellStyle]) -> GTData: all_info: list[StyleInfo] = [] for col_pos in positions: crnt_info = StyleInfo( - locname="column_label", + locname=loc, locnum=2, colname=col_pos.colname, rownum=col_pos.row, @@ -559,9 +549,7 @@ def _(loc: LocSpannerLabel, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace( - _styles=data._styles + [StyleInfo(locname="column_labels", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -569,7 +557,7 @@ def _(loc: LocStub, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname="stub", locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -578,9 +566,7 @@ def _(loc: LocRowGroupLabel, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace( - _styles=data._styles + [StyleInfo(locname="row_group_label", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -589,9 +575,7 @@ def _(loc: LocRowLabel, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace( - _styles=data._styles + [StyleInfo(locname="row_label", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -600,9 +584,7 @@ def _(loc: LocSummaryLabel, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace( - _styles=data._styles + [StyleInfo(locname="summary_label", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -616,7 +598,7 @@ def _(loc: LocBody, data: GTData, style: list[CellStyle]) -> GTData: for col_pos in positions: row_styles = [entry._from_row(data._tbl_data, col_pos.row) for entry in style_ready] crnt_info = StyleInfo( - locname="data", locnum=5, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles + locname=loc, locnum=5, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles ) all_info.append(crnt_info) @@ -628,9 +610,7 @@ def _(loc: LocSummary, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace( - _styles=data._styles + [StyleInfo(locname="summary", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -638,9 +618,7 @@ def _(loc: LocFooter, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace( - _styles=data._styles + [StyleInfo(locname="footer", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -649,9 +627,7 @@ def _(loc: LocFootnotes, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace( - _styles=data._styles + [StyleInfo(locname="footnotes", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) @set_style.register @@ -660,9 +636,7 @@ def _(loc: LocSourceNotes, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace( - _styles=data._styles + [StyleInfo(locname="source_notes", locnum=1, styles=style)] - ) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) # Set footnote generic ================================================================= @@ -688,9 +662,9 @@ def _(loc: LocTitle, data: GTData, footnote: str, placement: PlacementOptions) - # can be a list of strings. place = FootnotePlacement[placement] if loc.groups == "title": - info = FootnoteInfo(locname="title", locnum=1, footnotes=[footnote], placement=place) + info = FootnoteInfo(locname=loc, locnum=1, footnotes=[footnote], placement=place) elif loc.groups == "subtitle": - info = FootnoteInfo(locname="subtitle", locnum=2, footnotes=[footnote], placement=place) + info = FootnoteInfo(locname=loc, locnum=2, footnotes=[footnote], placement=place) else: raise ValueError(f"Unknown title group: {loc.groups}") diff --git a/great_tables/_modify_rows.py b/great_tables/_modify_rows.py index 013fe458e..72e553091 100644 --- a/great_tables/_modify_rows.py +++ b/great_tables/_modify_rows.py @@ -15,8 +15,12 @@ def row_group_order(self: GTSelf, groups: RowGroups) -> GTSelf: def _remove_from_body_styles(styles: Styles, column: str) -> Styles: + # TODO: refactor + from ._utils_render_html import _is_loc + from ._locations import LocBody + new_styles = [ - info for info in styles if not (info.locname == "data" and info.colname == column) + info for info in styles if not (_is_loc(info.locname, LocBody) and info.colname == column) ] return new_styles From a5bab23aea84d9fa25a576cc0d33a9dff25b92b4 Mon Sep 17 00:00:00 2001 From: jrycw Date: Wed, 18 Sep 2024 07:04:11 +0800 Subject: [PATCH 057/150] Refactor import statements in `_formats.py` --- great_tables/_formats.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 68882e30e..3744bad50 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -1,6 +1,8 @@ from __future__ import annotations import math +import re +from dataclasses import dataclass from datetime import date, datetime, time from decimal import Decimal from pathlib import Path @@ -3476,9 +3478,6 @@ def fmt_image( return fmt(self, fns=formatter.to_html, columns=columns, rows=rows) -from dataclasses import dataclass - - @dataclass class FmtImage: dispatch_on: DataFrameLike | Agnostic = Agnostic() @@ -3492,8 +3491,6 @@ class FmtImage: SPAN_TEMPLATE: ClassVar = '{}' def to_html(self, val: Any): - from pathlib import Path - # TODO: are we assuming val is a string? (or coercing?) # otherwise... @@ -3561,8 +3558,6 @@ def _get_image_uri(cls, filename: str) -> str: @staticmethod def _get_mime_type(filename: str) -> str: - from pathlib import Path - # note that we strip off the leading "." suffix = Path(filename).suffix[1:] @@ -3969,8 +3964,6 @@ def _generate_data_vals( list[Any]: A list of data values. """ - import re - if is_series(data_vals): data_vals = to_list(data_vals) @@ -4063,8 +4056,6 @@ def _process_number_stream(data_vals: str) -> list[float]: list[float]: A list of numeric values. """ - import re - number_stream = re.sub(r"[;,]", " ", data_vals) number_stream = re.sub(r"\\[|\\]", " ", number_stream) number_stream = re.sub(r"^\\s+|\\s+$", "", number_stream) @@ -4075,9 +4066,6 @@ def _process_number_stream(data_vals: str) -> list[float]: return number_stream -import re - - def _process_time_stream(data_vals: str) -> list[float]: """ Process a string of time values and convert to a list of floats. From e91bbaa873450b256d51c74e4cb49642af8b46c9 Mon Sep 17 00:00:00 2001 From: jrycw Date: Wed, 18 Sep 2024 07:27:08 +0800 Subject: [PATCH 058/150] Add `Returns` section to the doc for `GT.fmt_image()` and `GT.fmt_nanoplot()` --- great_tables/_formats.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 3744bad50..317aaa029 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -3428,6 +3428,12 @@ def fmt_image( The option to always use Base64 encoding for image paths that are determined to be local. By default, this is `True`. + Returns + ------- + GT + The GT object is returned. This is the same object that the method is called on so that we + can facilitate method chaining. + Examples -------- Using a small portion of `metro` dataset, let's create a new table. We will only include a few @@ -3656,6 +3662,12 @@ def fmt_nanoplot( By using the [`nanoplot_options()`](`great_tables.nanoplot_options`) helper function here, you can alter the layout and styling of the nanoplots in the new column. + Returns + ------- + GT + The GT object is returned. This is the same object that the method is called on so that we + can facilitate method chaining. + Details ------- Nanoplots try to show individual data with reasonably good visibility. Interactivity is included From f076015f4daf87a13c75ab6fe69a415858ea4234 Mon Sep 17 00:00:00 2001 From: jrycw Date: Wed, 18 Sep 2024 08:11:08 +0800 Subject: [PATCH 059/150] Add `val_fmt_image()` --- great_tables/_formats_vals.py | 65 +++++++++++++++++++++++++++++++++++ great_tables/vals.py | 1 + 2 files changed, 66 insertions(+) diff --git a/great_tables/_formats_vals.py b/great_tables/_formats_vals.py index 6dd8cac5b..9c5670dd5 100644 --- a/great_tables/_formats_vals.py +++ b/great_tables/_formats_vals.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any +from pathlib import Path from great_tables import GT from great_tables.gt import GT, _get_column_of_values @@ -931,3 +932,67 @@ def val_fmt_markdown( vals_fmt = _get_column_of_values(gt=gt_obj_fmt, column_name="x", context="html") return vals_fmt + + +def val_fmt_image( + x: X, + height: str | int | None = None, + width: str | int | None = None, + sep: str = " ", + path: str | Path | None = None, + file_pattern: str = "{}", + encode: bool = True, +) -> list[str]: + """Format image paths to generate images in cells. + + To more easily insert graphics into body cells, we can use the `fmt_image()` method. This allows + for one or more images to be placed in the targeted cells. The cells need to contain some + reference to an image file, either: (1) complete http/https or local paths to the files; (2) the + file names, where a common path can be provided via `path=`; or (3) a fragment of the file name, + where the `file_pattern=` argument helps to compose the entire file name and `path=` provides + the path information. This should be expressly used on columns that contain *only* references to + image files (i.e., no image references as part of a larger block of text). Multiple images can + be included per cell by separating image references by commas. The `sep=` argument allows for a + common separator to be applied between images. + + Parameters + ---------- + x + A list of values to be formatted. + height + The height of the rendered images. + width + The width of the rendered images. + sep + In the output of images within a body cell, `sep=` provides the separator between each + image. + path + An optional path to local image files (this is combined with all filenames). + file_pattern + The pattern to use for mapping input values in the body cells to the names of the graphics + files. The string supplied should use `"{}"` in the pattern to map filename fragments to + input strings. + encode + The option to always use Base64 encoding for image paths that are determined to be local. By + default, this is `True`. + + Returns + ------- + list[str] + A list of formatted values is returned. + """ + gt_obj: GTData = _make_one_col_table(vals=x) + + gt_obj_fmt = gt_obj.fmt_image( + columns="x", + height=height, + width=width, + sep=sep, + path=path, + file_pattern=file_pattern, + encode=encode, + ) + + vals_fmt = _get_column_of_values(gt=gt_obj_fmt, column_name="x", context="html") + + return vals_fmt diff --git a/great_tables/vals.py b/great_tables/vals.py index 92cd22280..8027ec908 100644 --- a/great_tables/vals.py +++ b/great_tables/vals.py @@ -11,4 +11,5 @@ val_fmt_date as fmt_date, val_fmt_time as fmt_time, val_fmt_markdown as fmt_markdown, + val_fmt_image as fmt_image, ) From 95708a5fed871615f50e548ecad42b03d1002107 Mon Sep 17 00:00:00 2001 From: jrycw Date: Wed, 18 Sep 2024 09:11:15 +0800 Subject: [PATCH 060/150] Unify `GT.cols_label()` and `GT.cols_width()` to accept both `cases` and `**kwargs` --- great_tables/_boxhead.py | 23 +++++++++++++++-------- great_tables/_spanners.py | 24 +++++++++++++++++++++--- tests/test_boxhead.py | 9 +++++++++ tests/test_spanners.py | 9 +++++++++ 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/great_tables/_boxhead.py b/great_tables/_boxhead.py index 7f1435c24..32b1b827b 100644 --- a/great_tables/_boxhead.py +++ b/great_tables/_boxhead.py @@ -11,7 +11,9 @@ from ._types import GTSelf -def cols_label(self: GTSelf, **kwargs: str | Text) -> GTSelf: +def cols_label( + self: GTSelf, cases: dict[str, str | Text] | None = None, **kwargs: str | Text +) -> GTSelf: """ Relabel one or more columns. @@ -26,10 +28,13 @@ def cols_label(self: GTSelf, **kwargs: str | Text) -> GTSelf: Parameters ---------- + cases + A dictionary where the keys are column names and the values are the labels. Labels may use + [`md()`](`great_tables.md`) or [`html()`](`great_tables.html`) helpers for formatting. + **kwargs - New labels expressed as keyword arguments. Column names should be the keyword (left-hand side). - Labels may use [`md()`](`great_tables.md`) or [`html()`](`great_tables.html`) helpers for - formatting. + Keyword arguments to specify column labels. Each keyword corresponds to a column name, with + its value indicating the new label. Returns ------- @@ -111,14 +116,16 @@ def cols_label(self: GTSelf, **kwargs: str | Text) -> GTSelf: """ from great_tables._helpers import UnitStr + cases = cases if cases is not None else {} + new_cases = cases | kwargs + # If nothing is provided, return `data` unchanged - if len(kwargs) == 0: + if len(new_cases) == 0: return self - mod_columns = list(kwargs.keys()) - # Get the full list of column names for the data column_names = self._boxhead._get_columns() + mod_columns = list(new_cases.keys()) # Stop function if any of the column names specified are not in `cols_labels` # msg: "All column names provided must exist in the input `.data` table." @@ -127,7 +134,7 @@ def cols_label(self: GTSelf, **kwargs: str | Text) -> GTSelf: # Handle units syntax in labels (e.g., "Density ({{ppl / mi^2}})") new_kwargs: dict[str, UnitStr | str | Text] = {} - for k, v in kwargs.items(): + for k, v in new_cases.items(): if isinstance(v, str): diff --git a/great_tables/_spanners.py b/great_tables/_spanners.py index 24d888fef..c854c97a5 100644 --- a/great_tables/_spanners.py +++ b/great_tables/_spanners.py @@ -7,7 +7,7 @@ from ._locations import resolve_cols_c from ._tbl_data import SelectExpr from ._text import Text -from ._utils import OrderedSet +from ._utils import OrderedSet, _assert_list_is_subset if TYPE_CHECKING: from ._gt_data import Boxhead @@ -581,7 +581,7 @@ def empty_spanner_matrix( return [{var: var for var in vars}], vars -def cols_width(self: GTSelf, cases: dict[str, str]) -> GTSelf: +def cols_width(self: GTSelf, cases: dict[str, str] | None = None, **kwargs: str) -> GTSelf: """Set the widths of columns. Manual specifications of column widths can be performed using the `cols_width()` method. We @@ -595,6 +595,10 @@ def cols_width(self: GTSelf, cases: dict[str, str]) -> GTSelf: A dictionary where the keys are column names and the values are the widths. Widths can be specified in pixels (e.g., `"50px"`) or as percentages (e.g., `"20%"`). + **kwargs + Keyword arguments to specify column widths. Each keyword corresponds to a column name, with + its value indicating the width in pixels or percentages. + Returns ------- GT @@ -684,10 +688,24 @@ def cols_width(self: GTSelf, cases: dict[str, str]) -> GTSelf: column widths based on the content (and you wouldn't get the overflowing behavior seen in the previous example). """ + cases = cases if cases is not None else {} + new_cases = cases | kwargs + + # If nothing is provided, return `data` unchanged + if len(new_cases) == 0: + return self curr_boxhead = self._boxhead - for col, width in cases.items(): + # Get the full list of column names for the data + column_names = curr_boxhead._get_columns() + mod_columns = list(new_cases.keys()) + + # Stop function if any of the column names specified are not in `cols_width` + # msg: "All column names provided must exist in the input `.data` table." + _assert_list_is_subset(mod_columns, set_list=column_names) + + for col, width in new_cases.items(): curr_boxhead = curr_boxhead._set_column_width(col, width) return self._replace(_boxhead=curr_boxhead) diff --git a/tests/test_boxhead.py b/tests/test_boxhead.py index 1bf2c5172..1f20d7bd7 100644 --- a/tests/test_boxhead.py +++ b/tests/test_boxhead.py @@ -13,6 +13,15 @@ def test_cols_label(): assert x == y +def test_cols_label_mix_cases_kwargs(): + df = pd.DataFrame({"x": [1.234, 2.345], "y": [3.456, 4.567], "z": [5.678, 6.789]}) + gt = GT(df).cols_label({"x": "ex"}, **{"y": "why"}, z="Zee") + + x = _get_column_labels(gt=gt, context="html") + y = ["ex", "why", "Zee"] + assert x == y + + def test_cols_label_units_text(): df = pd.DataFrame({"x": [1.234, 2.345], "y": [3.456, 4.567], "z": [5.678, 6.789]}) gt = GT(df).cols_label(x="Area ({{m^2}})", y="Density ({{kg / m^3}})", z="Zee") diff --git a/tests/test_spanners.py b/tests/test_spanners.py index 4e66ea335..c908539c4 100644 --- a/tests/test_spanners.py +++ b/tests/test_spanners.py @@ -207,6 +207,15 @@ def test_cols_width_fully_set(): assert gt_tbl._boxhead[2].column_width == "30px" +def test_cols_width_mix_cases_kwargs(): + df = pd.DataFrame({"a": [1, 2], "b": [3, 4], "c": [5, 6]}) + gt_tbl = GT(df).cols_width({"a": "10px"}, **{"b": "20px"}, c="30px") + + assert gt_tbl._boxhead[0].column_width == "10px" + assert gt_tbl._boxhead[1].column_width == "20px" + assert gt_tbl._boxhead[2].column_width == "30px" + + def test_cols_width_partial_set_pct(): df = pd.DataFrame({"a": [1, 2], "b": [3, 4], "c": [5, 6]}) gt_tbl = GT(df).cols_width({"a": "20%"}) From e96bbd60198f87b9d08b58d491e9e7cad34d820b Mon Sep 17 00:00:00 2001 From: jrycw Date: Wed, 18 Sep 2024 13:38:54 +0800 Subject: [PATCH 061/150] Add a preview section for the built-in datasets --- great_tables/data/__init__.py | 420 +++++++++++++++++++++++++++++++++- 1 file changed, 417 insertions(+), 3 deletions(-) diff --git a/great_tables/data/__init__.py b/great_tables/data/__init__.py index e6c1c729b..34ba0fce7 100644 --- a/great_tables/data/__init__.py +++ b/great_tables/data/__init__.py @@ -308,6 +308,17 @@ - `year`: The year for the population estimate. - `population`: The population estimate, midway through the year. +Preview +------- +``` + country_name country_code_2 country_code_3 year population +0 Aruba AW ABW 1960 54608 +1 Aruba AW ABW 1961 55811 +2 Aruba AW ABW 1962 56682 +3 Aruba AW ABW 1963 57475 +4 Aruba AW ABW 1964 58178 +``` + Source ------ @@ -343,6 +354,17 @@ - `sza`: The solar zenith angle in degrees, where missing values indicate that sunrise hadn't yet occurred by the `tst` value. +Preview +------- +``` + latitude month tst sza +0 20 jan 0400 NaN +1 20 jan 0430 NaN +2 20 jan 0500 NaN +3 20 jan 0530 NaN +4 20 jan 0600 NaN +``` + Source ------ Calculated Actinic Fluxes (290 - 700 nm) for Air Pollution Photochemistry Applications (Peterson, @@ -383,6 +405,25 @@ between both types (`"am"`), or, direct drive (`"dd"`) - `ctry_origin`: The country name for where the vehicle manufacturer is headquartered. - `msrp`: Manufacturer's suggested retail price in U.S. dollars (USD). + +Preview +------- +``` + mfr model year trim bdy_style hp hp_rpm \ +0 Ford GT 2017.0 Base Coupe coupe 647.0 6250.0 +1 Ferrari 458 Speciale 2015.0 Base Coupe coupe 597.0 9000.0 +2 Ferrari 458 Spider 2015.0 Base convertible 562.0 9000.0 +3 Ferrari 458 Italia 2014.0 Base Coupe coupe 562.0 9000.0 +4 Ferrari 488 GTB 2016.0 Base Coupe coupe 661.0 8000.0 + + trq trq_rpm mpg_c mpg_h drivetrain trsmn ctry_origin msrp +0 550.0 5900.0 11.0 18.0 rwd 7a United States 447000.0 +1 398.0 6000.0 13.0 17.0 rwd 7a Italy 291744.0 +2 398.0 6000.0 13.0 17.0 rwd 7a Italy 263553.0 +3 398.0 6000.0 13.0 17.0 rwd 7a Italy 233509.0 +4 561.0 3000.0 15.0 22.0 rwd 7a Italy 245400.0 +``` + """ sp500: pd.DataFrame = pd.read_csv(_sp500_fname, dtype=_sp500_dtype) # type: ignore @@ -402,6 +443,25 @@ `close` price is adjusted for splits. - `volume`: The number of trades for the given `date`. - `adj_close`: The close price adjusted for both dividends and splits. + +Preview +------- +``` + date open high low close volume \ +0 2015-12-31 2060.5901 2062.5400 2043.62 2043.9399 2.655330e+09 +1 2015-12-30 2077.3401 2077.3401 2061.97 2063.3601 2.367430e+09 +2 2015-12-29 2060.5400 2081.5601 2060.54 2078.3601 2.542000e+09 +3 2015-12-28 2057.7700 2057.7700 2044.20 2056.5000 2.492510e+09 +4 2015-12-24 2063.5200 2067.3601 2058.73 2060.9900 1.411860e+09 + + adj_close +0 2043.9399 +1 2063.3601 +2 2078.3601 +3 2056.5000 +4 2060.9900 +``` + """ pizzaplace: pd.DataFrame = pd.read_csv(_pizzaplace_fname, dtype=_pizzaplace_dtype) # type: ignore @@ -504,6 +564,18 @@ - `type`: The category or type of pizza, which can either be `"classic"`, `"chicken"`, `"supreme"`, or `"veggie"`. - `price`: The price of the pizza and the amount that it sold for (in USD). + +Preview +------- +``` + id date time name size type price +0 2015-000001 2015-01-01 11:38:36 hawaiian M classic 13.25 +1 2015-000002 2015-01-01 11:57:40 classic_dlx M classic 16.00 +2 2015-000002 2015-01-01 11:57:40 mexicana M veggie 16.00 +3 2015-000002 2015-01-01 11:57:40 thai_ckn L chicken 20.75 +4 2015-000002 2015-01-01 11:57:40 five_cheese L veggie 18.50 +``` + """ exibble: pd.DataFrame = pd.read_csv(_exibble_fname, dtype=_exibble_dtype) # type: ignore @@ -530,6 +602,25 @@ in a table stub. - `group`: A string-based column with four `"grp_a"` values and four `"grp_b"` values which can be useful for testing tables that contain row groups. + +Preview +------- +``` + num char fctr date time datetime currency \ +0 0.1111 apricot one 2015-01-15 13:35 2018-01-01 02:22 49.95 +1 2.2220 banana two 2015-02-15 14:40 2018-02-02 14:33 17.95 +2 33.3300 coconut three 2015-03-15 15:45 2018-03-03 03:44 1.39 +3 444.4000 durian four 2015-04-15 16:50 2018-04-04 15:55 65100.00 +4 5550.0000 NaN five 2015-05-15 17:55 2018-05-05 04:00 1325.81 + + row group +0 row_1 grp_a +1 row_2 grp_a +2 row_3 grp_a +3 row_4 grp_a +4 row_5 grp_b +``` + """ towny: pd.DataFrame = pd.read_csv(_towny_fname, dtype=_towny_dtype) # type: ignore @@ -573,6 +664,60 @@ - `pop_change_1996_2001_pct`, `pop_change_2001_2006_pct`, `pop_change_2006_2011_pct`, `pop_change_2011_2016_pct`, `pop_change_2016_2021_pct`: Population changes between adjacent pairs of census years, from 1996 to 2021. + +Preview +------- +``` + name website status csd_type \ +0 Addington Highlands https://addingtonhighlands.ca lower-tier township +1 Adelaide Metcalfe https://adelaidemetcalfe.on.ca lower-tier township +2 Adjala-Tosorontio https://www.adjtos.ca lower-tier township +3 Admaston/Bromley https://admastonbromley.com lower-tier township +4 Ajax https://www.ajax.ca lower-tier town + + census_div latitude longitude land_area_km2 population_1996 \ +0 Lennox and Addington 45.000000 -77.250000 1293.99 2429 +1 Middlesex 42.950000 -81.700000 331.11 3128 +2 Simcoe 44.133333 -79.933333 371.53 9359 +3 Renfrew 45.529167 -76.896944 519.59 2837 +4 Durham 43.858333 -79.036389 66.64 64430 + + population_2001 population_2006 population_2011 population_2016 \ +0 2402 2512 2517 2318 +1 3149 3135 3028 2990 +2 10082 10695 10603 10975 +3 2824 2716 2844 2935 +4 73753 90167 109600 119677 + + population_2021 density_1996 density_2001 density_2006 density_2011 \ +0 2534 1.88 1.86 1.94 1.95 +1 3011 9.45 9.51 9.47 9.14 +2 10989 25.19 27.14 28.79 28.54 +3 2995 5.46 5.44 5.23 5.47 +4 126666 966.84 1106.74 1353.05 1644.66 + + density_2016 density_2021 pop_change_1996_2001_pct \ +0 1.79 1.96 -0.0111 +1 9.03 9.09 0.0067 +2 29.54 29.58 0.0773 +3 5.65 5.76 -0.0046 +4 1795.87 1900.75 0.1447 + + pop_change_2001_2006_pct pop_change_2006_2011_pct \ +0 0.0458 0.0020 +1 -0.0044 -0.0341 +2 0.0608 -0.0086 +3 -0.0382 0.0471 +4 0.2226 0.2155 + + pop_change_2011_2016_pct pop_change_2016_2021_pct +0 -0.0791 0.0932 +1 -0.0125 0.0070 +2 0.0351 0.0013 +3 0.0320 0.0204 +4 0.0919 0.0584 +``` + """ peeps: pd.DataFrame = pd.read_csv(_peeps_fname, dtype=_peeps_dtype) # type: ignore @@ -607,6 +752,32 @@ - `dob`: The individual's date of birth (DOB) in the ISO 8601 form of `YYYY-MM-DD`. - `height_cm`, `weight_kg`: The height and weight of the individual in centimeters (cm) and kilograms (kg), respectively. + +Preview +------- +``` + name_given name_family address city \ +0 Ruth Conte 4299 Bobcat Drive Baileys Crossroads +1 Peter Möller 3705 Hidden Pond Road Red Boiling Springs +2 Fanette Gadbois 4200 Swick Hill Street New Orleans +3 Judyta Borkowska 2287 Cherry Ridge Drive Oakfield +4 Leonard Jacobs 1496 Hillhaven Drive Los Angeles + + state_prov postcode country email_addr phone_number \ +0 MD 22041 USA rcconte@example.com 240-783-7630 +1 TN 37150 USA pmoeller@example.com 615-699-3517 +2 LA 70112 USA fan_gadbois@example.com 985-205-2970 +3 NY 14125 USA jdtabork@example.com 585-948-7790 +4 CA 90036 USA leojacobs@example.com 323-857-6576 + + country_code gender dob height_cm weight_kg +0 1 female 1949-03-16 153 76.4 +1 1 male 1939-11-22 175 74.9 +2 1 female 1970-12-20 167 61.6 +3 1 female 1965-07-19 156 54.5 +4 1 male 1985-10-01 177 113.2 +``` + """ films: pd.DataFrame = pd.read_csv(_films_fname, dtype=_films_dtype) # type: ignore @@ -637,6 +808,32 @@ - `run_time`: The run time of the film in hours and minutes. This is given as a string in the format `h m`. - `imdb_url`: The URL of the film's information page in the Internet Movie Database (IMDB). + +Preview +------- +``` + year title original_title director \ +0 1946 The Lovers Amanti in fuga Giacomo Gentilomo +1 1946 Anna and the King of Siam NaN John Cromwell +2 1946 Blood and Fire Blod och eld Anders Henrikson +3 1946 Letter from the Dead Brevet fra afdøde Johan Jacobsen +4 1946 Brief Encounter NaN David Lean + + languages countries_of_origin run_time \ +0 it IT 1h 30m +1 en US 2h 8m +2 sv SE 1h 40m +3 da DK 1h 18m +4 en,fr GB 1h 26m + + imdb_url +0 https://www.imdb.com/title/tt0038297/ +1 https://www.imdb.com/title/tt0038303/ +2 https://www.imdb.com/title/tt0037544/ +3 https://www.imdb.com/title/tt0124300/ +4 https://www.imdb.com/title/tt0037558/ +``` + """ metro: pd.DataFrame = pd.read_csv(_metro_fname, dtype=_metro_dtype) # type: ignore @@ -671,8 +868,8 @@ series of line names. - `connect_rer`: Station connections with the RER. The RER system has five lines (A, B, C, D, and E) with 257 stations and several interchanges with the Metro. -- `connect_tram`: Connections with tramway lines. This system has twelve lines in operation (T1, T2, -T3a, T3b, T4, T5, T6, T7, T8, T9, T11, and T13) with 235 stations. +- `connect_tramway`: Connections with tramway lines. This system has twelve lines in operation (T1, +T2, T3a, T3b, T4, T5, T6, T7, T8, T9, T11, and T13) with 235 stations. - `connect_transilien`: Connections with Transilien lines. This system has eight lines in operation (H, J, K, L, N, P, R, and U). - `connect_other`: Other connections with transportation infrastructure such as regional, intercity, @@ -684,6 +881,32 @@ series. - `passengers`: The total number of Metro station entries during 2021. Some of the newest stations in the Metro system do not have this data, thus they show as missing values. + +Preview +------- +``` + name caption lines connect_rer \ +0 Argentine NaN 1 NaN +1 Bastille NaN 1, 5, 8 NaN +2 Bérault NaN 1 NaN +3 Champs-Élysées—Clemenceau Grand Palais 1, 13 NaN +4 Charles de Gaulle—Étoile NaN 1, 2, 6 A + + connect_tramway connect_transilien connect_other passengers latitude \ +0 NaN NaN NaN 2079212 48.875278 +1 NaN NaN NaN 8069243 48.853082 +2 NaN NaN NaN 2106827 48.845278 +3 NaN NaN NaN 1909005 48.867500 +4 NaN NaN NaN 4291663 48.873889 + + longitude location +0 2.290000 Paris 16th, Paris 17th +1 2.369077 Paris 4th, Paris 11th, Paris 12th +2 2.428333 Saint-Mandé, Vincennes +3 2.313500 Paris 8th +4 2.295000 Paris 8th, Paris 16th, Paris 17th +``` + """ gibraltar: pd.DataFrame = pd.read_csv(_gibraltar_fname, dtype=_gibraltar_dtype) # type: ignore @@ -708,6 +931,25 @@ is recorded as m/s values (otherwise the value is `0`). - `pressure`: The atmospheric pressure in hectopascals (hPa). - `condition`: The weather condition. + +Preview +------- +``` + date time temp dew_point humidity wind_dir wind_speed \ +0 2023-05-01 00:20 18.9 12.8 0.68 W 6.7 +1 2023-05-01 00:50 18.9 13.9 0.73 WSW 7.2 +2 2023-05-01 01:20 17.8 13.9 0.77 W 6.7 +3 2023-05-01 01:50 18.9 13.9 0.73 W 6.7 +4 2023-05-01 02:20 18.9 12.8 0.68 WSW 6.7 + + wind_gust pressure condition +0 0.0 1015.2 Fair +1 0.0 1015.2 Fair +2 0.0 1014.6 Fair +3 0.0 1014.6 Fair +4 0.0 1014.6 Fair +``` + """ constants: pd.DataFrame = pd.read_csv(_constants_fname, dtype=_constants_dtype) # type: ignore @@ -724,7 +966,7 @@ Details ------- -This is a dataset with 354 rows and 4 columns. +This is a dataset with 354 rows and 6 columns. - `name`: The name of the constant. - `value`: The value of the constant. @@ -733,6 +975,25 @@ - `sf_value`, `sf_uncert`: The number of significant figures associated with the value and any uncertainty value. - `units`: The units associated with the constant. + +Preview +------- +``` + name value uncert \ +0 alpha particle-electron mass ratio 7.294300e+03 2.400000e-07 +1 alpha particle mass 6.644657e-27 2.000000e-36 +2 alpha particle mass energy equivalent 5.971920e-10 1.800000e-19 +3 alpha particle mass energy equivalent in MeV 3.727379e+03 1.100000e-06 +4 alpha particle mass in u 4.001506e+00 6.300000e-11 + + sf_value sf_uncert units +0 12 2 NaN +1 11 2 kg +2 11 2 J +3 11 2 MeV +4 13 2 u +``` + """ illness: pd.DataFrame = pd.read_csv(_illness_fname, dtype=_illness_dtype) # type: ignore @@ -799,6 +1060,25 @@ each test administered from days 3 to 9. A missing value indicates that the test could not be performed that day. - `norm_l`, `norm_u`: Lower and upper bounds for the normal range associated with the test. + +Preview +------- +``` + test units day_3 day_4 day_5 day_6 day_7 \ +0 Viral load copies per mL 12000.00 4200.00 1600.00 830.00 760.00 +1 WBC x10^9 / L 5.26 4.26 9.92 10.49 24.77 +2 Neutrophils x10^9 / L 4.87 4.72 7.92 18.21 22.08 +3 RBC x10^12 / L 5.72 5.98 4.23 4.83 4.12 +4 Hb g / L 153.00 135.00 126.00 115.00 75.00 + + day_8 day_9 norm_l norm_u +0 520.00 250.00 NaN NaN +1 30.26 19.03 4.0 10.0 +2 27.17 16.59 2.0 8.0 +3 2.68 3.32 4.0 5.5 +4 87.00 95.00 120.0 160.0 +``` + """ reactions: pd.DataFrame = pd.read_csv(_reactions_fname, dtype=_reactions_dtype) # type: ignore @@ -869,6 +1149,60 @@ that data is not available. - `Cl_t_low`, `Cl_t_high`: The low and high temperature boundaries (in units of K) for which the `Cl_a`, `Cl_b`, and `Cl_n` parameters are valid. + +Preview +------- +``` + cmpd_name cmpd_mwt cmpd_formula cmpd_type cmpd_smiles \ +0 methane 16.04 CH4 normal alkane C +1 formaldehyde 30.03 CH2O aldehyde C=O +2 methanol 32.04 CH4O alcohol or glycol CO +3 fluoromethane 34.03 CH3F haloalkane (separated) CF +4 formic acid 46.03 CH2O2 carboxylic acid OC=O + + cmpd_inchi cmpd_inchikey \ +0 InChI=1S/CH4/h1H4 VNWKTOKETHGBQD-UHFFFAOYSA-N +1 InChI=1S/CH2O/c1-2/h1H2 WSFSSNUMVMOOMR-UHFFFAOYSA-N +2 InChI=1S/CH4O/c1-2/h2H,1H3 OKKJLVBELUTLKV-UHFFFAOYSA-N +3 InChI=1S/CH3F/c1-2/h1H3 NBVXSUQYWXRMNV-UHFFFAOYSA-N +4 InChI=1S/CH2O2/c2-1-3/h1H,(H,2,3) BDAGIHXWWSANSR-UHFFFAOYSA-N + + OH_k298 OH_uncert OH_u_fac OH_A OH_B OH_n \ +0 6.360000e-15 0.1 NaN 3.620000e-13 1200.348660 2.179936 +1 8.500000e-12 0.2 NaN 5.400000e-12 -135.000000 NaN +2 8.780000e-13 0.1 NaN 2.320000e-13 -402.000000 2.720000 +3 1.970000e-14 0.1 NaN 1.990000e-13 685.420421 2.040182 +4 4.500000e-13 NaN 1.4 4.500000e-13 NaN NaN + + OH_t_low OH_t_high O3_k298 O3_uncert O3_u_fac O3_A O3_B O3_n \ +0 200.0 2025.0 NaN NaN NaN NaN NaN NaN +1 200.0 300.0 NaN NaN NaN NaN NaN NaN +2 210.0 1344.0 NaN NaN NaN NaN NaN NaN +3 240.0 1800.0 NaN NaN NaN NaN NaN NaN +4 290.0 450.0 NaN NaN NaN NaN NaN NaN + + O3_t_low O3_t_high NO3_k298 NO3_uncert NO3_u_fac NO3_A \ +0 NaN NaN NaN NaN NaN NaN +1 NaN NaN 5.500000e-16 NaN 1.6 NaN +2 NaN NaN 1.300000e-16 NaN 3.0 9.400000e-13 +3 NaN NaN NaN NaN NaN NaN +4 NaN NaN NaN NaN NaN NaN + + NO3_B NO3_n NO3_t_low NO3_t_high Cl_k298 Cl_uncert Cl_u_fac \ +0 NaN NaN NaN NaN 1.000000e-13 0.15 NaN +1 NaN NaN NaN NaN 7.200000e-11 0.15 NaN +2 2650.0 NaN 250.0 370.0 5.100000e-11 0.20 NaN +3 NaN NaN NaN NaN 3.600000e-13 NaN 1.4 +4 NaN NaN NaN NaN 1.900000e-13 NaN 1.4 + + Cl_A Cl_B Cl_n Cl_t_low Cl_t_high +0 6.600000e-12 1240.0 NaN 200.0 300.0 +1 8.100000e-11 34.0 NaN 200.0 500.0 +2 5.100000e-11 0.0 NaN 225.0 950.0 +3 4.900000e-12 781.0 NaN 200.0 300.0 +4 NaN NaN NaN NaN NaN +``` + """ photolysis: pd.DataFrame = pd.read_csv(_photolysis_fname, dtype=_photolysis_dtype) # type: ignore @@ -904,6 +1238,39 @@ photoabsorption data for the compound undergoing photolysis. The values in `wavelength_nm` provide the wavelength of light in nanometer units; the `sigma_298_cm2` values are paired with the `wavelength_nm` values and they are in units of `cm^2 molecule^-1`. + +Preview +------- +``` + cmpd_name cmpd_formula products type \ +0 ozone O3 -> O(^1D) + O2 inorganic reactions +1 ozone O3 -> O(^3P) + O2 inorganic reactions +2 hydrogen peroxide H2O2 -> OH + OH inorganic reactions +3 nitrogen dioxide NO2 -> NO + O(^3P) inorganic reactions +4 nitrate radical NO3 -> NO + O2 inorganic reactions + + l m n quantum_yield \ +0 0.000061 1.743 0.474 NaN +1 0.000478 0.298 0.080 NaN +2 0.000010 0.723 0.279 1.0 +3 0.011650 0.244 0.267 NaN +4 0.024850 0.168 0.108 1.0 + + wavelength_nm \ +0 290,291,292,293,294,295,296,297,298,299,300,30... +1 290,291,292,293,294,295,296,297,298,299,300,30... +2 190,195,200,205,210,215,220,225,230,235,240,24... +3 400,401,402,403,404,405,406,407,408,409,410,41... +4 400,401,402,403,404,405,406,407,408,409,410,41... + + sigma_298_cm2 +0 1.43E-18,1.27E-18,1.11E-18,9.94E-19,8.68E-19,7... +1 1.43E-18,1.27E-18,1.11E-18,9.94E-19,8.68E-19,7... +2 6.72E-19,5.63E-19,4.75E-19,4.08E-19,3.57E-19,3... +3 0,0,0,2.00E-20,0,3.00E-20,2.00E-20,1.00E-20,3.... +4 0,0,0,2.00E-22,0,3.00E-22,2.00E-22,1.00E-22,3.... +``` + """ nuclides: pd.DataFrame = pd.read_csv(_nuclides_fname, dtype=_nuclides_dtype) # type: ignore @@ -942,6 +1309,53 @@ micro AMU. - `mass_excess`, `mass_excess_uncert`: The mass excess and its associated uncertainty. In units of keV. + +Preview +------- +``` + nuclide z n element radius radius_uncert abundance \ +0 ^{1}_{1}H0 1 0 H 0.8783 0.0086 0.999855 +1 ^{2}_{1}H1 1 1 H 2.1421 0.0088 0.000145 +2 ^{3}_{1}H2 1 2 H 1.7591 0.0363 NaN +3 ^{4}_{1}H3 1 3 H NaN NaN NaN +4 ^{5}_{1}H4 1 4 H NaN NaN NaN + + abundance_uncert is_stable half_life half_life_uncert isospin decay_1 \ +0 0.000078 TRUE NaN NaN NaN NaN +1 0.000078 TRUE NaN NaN NaN NaN +2 NaN FALSE 3.887813e+08 6.311385e+05 NaN B- +3 NaN FALSE NaN NaN 1 N +4 NaN FALSE 8.608259e-23 6.496799e-24 NaN 2N + + decay_1_pct decay_1_pct_uncert decay_2 decay_2_pct decay_2_pct_uncert \ +0 NaN NaN NaN NaN NaN +1 NaN NaN NaN NaN NaN +2 1.0 NaN NaN NaN NaN +3 1.0 NaN NaN NaN NaN +4 1.0 NaN NaN NaN NaN + + decay_3 decay_3_pct decay_3_pct_uncert magnetic_dipole \ +0 NaN NaN NaN 2.792847 +1 NaN NaN NaN 0.857438 +2 NaN NaN NaN 2.978962 +3 NaN NaN NaN NaN +4 NaN NaN NaN NaN + + magnetic_dipole_uncert electric_quadrupole electric_quadrupole_uncert \ +0 9.000000e-09 NaN NaN +1 5.000000e-09 0.002858 3.000000e-07 +2 1.400000e-08 NaN NaN +3 NaN NaN NaN +4 NaN NaN NaN + + atomic_mass atomic_mass_uncert mass_excess mass_excess_uncert +0 1.007825e+06 0.000014 7288.971064 0.000013 +1 2.014102e+06 0.000015 13135.722895 0.000015 +2 3.016049e+06 0.000080 14949.810900 0.000080 +3 4.026432e+06 107.354000 24621.129000 100.000000 +4 5.035311e+06 96.020000 32892.447000 89.443000 +``` + """ islands: pd.DataFrame = pd.read_csv(_islands_fname) # type: ignore From 779361df08ae8bb0c5984d2c9ba60227bcf7df2f Mon Sep 17 00:00:00 2001 From: jrycw Date: Wed, 18 Sep 2024 18:06:08 +0800 Subject: [PATCH 062/150] Update `GT.save()` --- great_tables/_export.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/great_tables/_export.py b/great_tables/_export.py index 54189d26b..4ec833443 100644 --- a/great_tables/_export.py +++ b/great_tables/_export.py @@ -185,6 +185,7 @@ def save( web_driver: WebDrivers = "chrome", window_size: tuple[int, int] = (6000, 6000), debug_port: None | int = None, + encoding: str = "utf-8", _debug_dump: DebugDumpOptions | None = None, ) -> None: """ @@ -215,6 +216,8 @@ def save( to capture a table, but may affect the tables appearance. debug_port Port number to use for debugging. By default no debugging port is opened. + encoding + The encoding used when writing temporary files. _debug_dump Whether the saved image should be a big browser window, with key elements outlined. This is helpful for debugging this function's resizing, cropping heuristics. This is an internal @@ -255,11 +258,8 @@ def save( if selector != "table": raise NotImplementedError("Currently, only selector='table' is supported.") - # Get the file extension from the file name - file_extension = file.split(".")[-1] - # If there is no file extension, add the .png extension - if len(file_extension) == len(file): + if not Path(file).suffix: file += ".png" # Get the HTML content from the displayed output @@ -306,11 +306,11 @@ def save( ): # Write the HTML content to the temp file - with open(f"{tmp_dir}/table.html", "w") as temp_file: + with open(f"{tmp_dir}/table.html", "w", encoding=encoding) as temp_file: temp_file.write(html_content) # Open the HTML file in the headless browser - headless_browser.set_window_size(window_size[0], window_size[1]) + headless_browser.set_window_size(*window_size) headless_browser.get("file://" + temp_file.name) _save_screenshot(headless_browser, scale, file, debug=_debug_dump) From 20fa355b868462f5a7765a1f909545e69456ce2b Mon Sep 17 00:00:00 2001 From: jrycw Date: Thu, 19 Sep 2024 09:26:36 +0800 Subject: [PATCH 063/150] Use `polars.DataFrame.glimpse` to generate dataset previews --- great_tables/data/__init__.py | 574 +++++++++++++++------------------- 1 file changed, 254 insertions(+), 320 deletions(-) diff --git a/great_tables/data/__init__.py b/great_tables/data/__init__.py index 34ba0fce7..e96f18112 100644 --- a/great_tables/data/__init__.py +++ b/great_tables/data/__init__.py @@ -311,12 +311,13 @@ Preview ------- ``` - country_name country_code_2 country_code_3 year population -0 Aruba AW ABW 1960 54608 -1 Aruba AW ABW 1961 55811 -2 Aruba AW ABW 1962 56682 -3 Aruba AW ABW 1963 57475 -4 Aruba AW ABW 1964 58178 +Rows: 13545 +Columns: 5 +$ country_name 'Aruba', 'Aruba', 'Aruba' +$ country_code_2 'AW', 'AW', 'AW' +$ country_code_3 'ABW', 'ABW', 'ABW' +$ year 1960, 1961, 1962 +$ population 54608, 55811, 56682 ``` Source @@ -357,12 +358,12 @@ Preview ------- ``` - latitude month tst sza -0 20 jan 0400 NaN -1 20 jan 0430 NaN -2 20 jan 0500 NaN -3 20 jan 0530 NaN -4 20 jan 0600 NaN +Rows: 816 +Columns: 4 +$ latitude '20', '20', '20' +$ month 'jan', 'jan', 'jan' +$ tst '0400', '0430', '0500' +$ sza None, None, None ``` Source @@ -409,19 +410,23 @@ Preview ------- ``` - mfr model year trim bdy_style hp hp_rpm \ -0 Ford GT 2017.0 Base Coupe coupe 647.0 6250.0 -1 Ferrari 458 Speciale 2015.0 Base Coupe coupe 597.0 9000.0 -2 Ferrari 458 Spider 2015.0 Base convertible 562.0 9000.0 -3 Ferrari 458 Italia 2014.0 Base Coupe coupe 562.0 9000.0 -4 Ferrari 488 GTB 2016.0 Base Coupe coupe 661.0 8000.0 - - trq trq_rpm mpg_c mpg_h drivetrain trsmn ctry_origin msrp -0 550.0 5900.0 11.0 18.0 rwd 7a United States 447000.0 -1 398.0 6000.0 13.0 17.0 rwd 7a Italy 291744.0 -2 398.0 6000.0 13.0 17.0 rwd 7a Italy 263553.0 -3 398.0 6000.0 13.0 17.0 rwd 7a Italy 233509.0 -4 561.0 3000.0 15.0 22.0 rwd 7a Italy 245400.0 +Rows: 47 +Columns: 15 +$ mfr 'Ford', 'Ferrari', 'Ferrari' +$ model 'GT', '458 Speciale', '458 Spider' +$ year 2017.0, 2015.0, 2015.0 +$ trim 'Base Coupe', 'Base Coupe', 'Base' +$ bdy_style 'coupe', 'coupe', 'convertible' +$ hp 647.0, 597.0, 562.0 +$ hp_rpm 6250.0, 9000.0, 9000.0 +$ trq 550.0, 398.0, 398.0 +$ trq_rpm 5900.0, 6000.0, 6000.0 +$ mpg_c 11.0, 13.0, 13.0 +$ mpg_h 18.0, 17.0, 17.0 +$ drivetrain 'rwd', 'rwd', 'rwd' +$ trsmn '7a', '7a', '7a' +$ ctry_origin 'United States', 'Italy', 'Italy' +$ msrp 447000.0, 291744.0, 263553.0 ``` """ @@ -447,19 +452,15 @@ Preview ------- ``` - date open high low close volume \ -0 2015-12-31 2060.5901 2062.5400 2043.62 2043.9399 2.655330e+09 -1 2015-12-30 2077.3401 2077.3401 2061.97 2063.3601 2.367430e+09 -2 2015-12-29 2060.5400 2081.5601 2060.54 2078.3601 2.542000e+09 -3 2015-12-28 2057.7700 2057.7700 2044.20 2056.5000 2.492510e+09 -4 2015-12-24 2063.5200 2067.3601 2058.73 2060.9900 1.411860e+09 - - adj_close -0 2043.9399 -1 2063.3601 -2 2078.3601 -3 2056.5000 -4 2060.9900 +Rows: 16607 +Columns: 7 +$ date '2015-12-31', '2015-12-30', '2015-12-29' +$ open 2060.5901, 2077.3401, 2060.54 +$ high 2062.54, 2077.3401, 2081.5601 +$ low 2043.62, 2061.97, 2060.54 +$ close 2043.9399, 2063.3601, 2078.3601 +$ volume 2655330000.0, 2367430000.0, 2542000000.0 +$ adj_close 2043.9399, 2063.3601, 2078.3601 ``` """ @@ -568,12 +569,15 @@ Preview ------- ``` - id date time name size type price -0 2015-000001 2015-01-01 11:38:36 hawaiian M classic 13.25 -1 2015-000002 2015-01-01 11:57:40 classic_dlx M classic 16.00 -2 2015-000002 2015-01-01 11:57:40 mexicana M veggie 16.00 -3 2015-000002 2015-01-01 11:57:40 thai_ckn L chicken 20.75 -4 2015-000002 2015-01-01 11:57:40 five_cheese L veggie 18.50 +Rows: 49574 +Columns: 7 +$ id '2015-000001', '2015-000002', '2015-000002' +$ date '2015-01-01', '2015-01-01', '2015-01-01' +$ time '11:38:36', '11:57:40', '11:57:40' +$ name 'hawaiian', 'classic_dlx', 'mexicana' +$ size 'M', 'M', 'M' +$ type 'classic', 'classic', 'veggie' +$ price 13.25, 16.0, 16.0 ``` """ @@ -606,19 +610,17 @@ Preview ------- ``` - num char fctr date time datetime currency \ -0 0.1111 apricot one 2015-01-15 13:35 2018-01-01 02:22 49.95 -1 2.2220 banana two 2015-02-15 14:40 2018-02-02 14:33 17.95 -2 33.3300 coconut three 2015-03-15 15:45 2018-03-03 03:44 1.39 -3 444.4000 durian four 2015-04-15 16:50 2018-04-04 15:55 65100.00 -4 5550.0000 NaN five 2015-05-15 17:55 2018-05-05 04:00 1325.81 - - row group -0 row_1 grp_a -1 row_2 grp_a -2 row_3 grp_a -3 row_4 grp_a -4 row_5 grp_b +Rows: 8 +Columns: 9 +$ num 0.1111, 2.222, 33.33 +$ char 'apricot', 'banana', 'coconut' +$ fctr 'one', 'two', 'three' +$ date '2015-01-15', '2015-02-15', '2015-03-15' +$ time '13:35', '14:40', '15:45' +$ datetime '2018-01-01 02:22', '2018-02-02 14:33', '2018-03-03 03:44' +$ currency 49.95, 17.95, 1.39 +$ row 'row_1', 'row_2', 'row_3' +$ group 'grp_a', 'grp_a', 'grp_a' ``` """ @@ -668,54 +670,35 @@ Preview ------- ``` - name website status csd_type \ -0 Addington Highlands https://addingtonhighlands.ca lower-tier township -1 Adelaide Metcalfe https://adelaidemetcalfe.on.ca lower-tier township -2 Adjala-Tosorontio https://www.adjtos.ca lower-tier township -3 Admaston/Bromley https://admastonbromley.com lower-tier township -4 Ajax https://www.ajax.ca lower-tier town - - census_div latitude longitude land_area_km2 population_1996 \ -0 Lennox and Addington 45.000000 -77.250000 1293.99 2429 -1 Middlesex 42.950000 -81.700000 331.11 3128 -2 Simcoe 44.133333 -79.933333 371.53 9359 -3 Renfrew 45.529167 -76.896944 519.59 2837 -4 Durham 43.858333 -79.036389 66.64 64430 - - population_2001 population_2006 population_2011 population_2016 \ -0 2402 2512 2517 2318 -1 3149 3135 3028 2990 -2 10082 10695 10603 10975 -3 2824 2716 2844 2935 -4 73753 90167 109600 119677 - - population_2021 density_1996 density_2001 density_2006 density_2011 \ -0 2534 1.88 1.86 1.94 1.95 -1 3011 9.45 9.51 9.47 9.14 -2 10989 25.19 27.14 28.79 28.54 -3 2995 5.46 5.44 5.23 5.47 -4 126666 966.84 1106.74 1353.05 1644.66 - - density_2016 density_2021 pop_change_1996_2001_pct \ -0 1.79 1.96 -0.0111 -1 9.03 9.09 0.0067 -2 29.54 29.58 0.0773 -3 5.65 5.76 -0.0046 -4 1795.87 1900.75 0.1447 - - pop_change_2001_2006_pct pop_change_2006_2011_pct \ -0 0.0458 0.0020 -1 -0.0044 -0.0341 -2 0.0608 -0.0086 -3 -0.0382 0.0471 -4 0.2226 0.2155 - - pop_change_2011_2016_pct pop_change_2016_2021_pct -0 -0.0791 0.0932 -1 -0.0125 0.0070 -2 0.0351 0.0013 -3 0.0320 0.0204 -4 0.0919 0.0584 +Rows: 414 +Columns: 25 +$ name 'Addington Highlands', 'Adelaide Metcalfe', 'Adjala-Tosorontio' +$ website 'https://addingtonhighlands.ca', + 'https://adelaidemetcalfe.on.ca', + 'https://www.adjtos.ca' +$ status 'lower-tier', 'lower-tier', 'lower-tier' +$ csd_type 'township', 'township', 'township' +$ census_div 'Lennox and Addington', 'Middlesex', 'Simcoe' +$ latitude 45.0, 42.95, 44.133333 +$ longitude -77.25, -81.7, -79.933333 +$ land_area_km2 1293.99, 331.11, 371.53 +$ population_1996 2429, 3128, 9359 +$ population_2001 2402, 3149, 10082 +$ population_2006 2512, 3135, 10695 +$ population_2011 2517, 3028, 10603 +$ population_2016 2318, 2990, 10975 +$ population_2021 2534, 3011, 10989 +$ density_1996 1.88, 9.45, 25.19 +$ density_2001 1.86, 9.51, 27.14 +$ density_2006 1.94, 9.47, 28.79 +$ density_2011 1.95, 9.14, 28.54 +$ density_2016 1.79, 9.03, 29.54 +$ density_2021 1.96, 9.09, 29.58 +$ pop_change_1996_2001_pct -0.0111, 0.0067, 0.0773 +$ pop_change_2001_2006_pct 0.0458, -0.0044, 0.0608 +$ pop_change_2006_2011_pct 0.002, -0.0341, -0.0086 +$ pop_change_2011_2016_pct -0.0791, -0.0125, 0.0351 +$ pop_change_2016_2021_pct 0.0932, 0.007, 0.0013 ``` """ @@ -756,26 +739,22 @@ Preview ------- ``` - name_given name_family address city \ -0 Ruth Conte 4299 Bobcat Drive Baileys Crossroads -1 Peter Möller 3705 Hidden Pond Road Red Boiling Springs -2 Fanette Gadbois 4200 Swick Hill Street New Orleans -3 Judyta Borkowska 2287 Cherry Ridge Drive Oakfield -4 Leonard Jacobs 1496 Hillhaven Drive Los Angeles - - state_prov postcode country email_addr phone_number \ -0 MD 22041 USA rcconte@example.com 240-783-7630 -1 TN 37150 USA pmoeller@example.com 615-699-3517 -2 LA 70112 USA fan_gadbois@example.com 985-205-2970 -3 NY 14125 USA jdtabork@example.com 585-948-7790 -4 CA 90036 USA leojacobs@example.com 323-857-6576 - - country_code gender dob height_cm weight_kg -0 1 female 1949-03-16 153 76.4 -1 1 male 1939-11-22 175 74.9 -2 1 female 1970-12-20 167 61.6 -3 1 female 1965-07-19 156 54.5 -4 1 male 1985-10-01 177 113.2 +Rows: 100 +Columns: 14 +$ name_given 'Ruth', 'Peter', 'Fanette' +$ name_family 'Conte', 'Möller', 'Gadbois' +$ address '4299 Bobcat Drive', '3705 Hidden Pond Road', '4200 Swick Hill Street' +$ city 'Baileys Crossroads', 'Red Boiling Springs', 'New Orleans' +$ state_prov 'MD', 'TN', 'LA' +$ postcode '22041', '37150', '70112' +$ country 'USA', 'USA', 'USA' +$ email_addr 'rcconte@example.com', 'pmoeller@example.com', 'fan_gadbois@example.com' +$ phone_number '240-783-7630', '615-699-3517', '985-205-2970' +$ country_code '1', '1', '1' +$ gender 'female', 'male', 'female' +$ dob '1949-03-16', '1939-11-22', '1970-12-20' +$ height_cm 153, 175, 167 +$ weight_kg 76.4, 74.9, 61.6 ``` """ @@ -812,26 +791,18 @@ Preview ------- ``` - year title original_title director \ -0 1946 The Lovers Amanti in fuga Giacomo Gentilomo -1 1946 Anna and the King of Siam NaN John Cromwell -2 1946 Blood and Fire Blod och eld Anders Henrikson -3 1946 Letter from the Dead Brevet fra afdøde Johan Jacobsen -4 1946 Brief Encounter NaN David Lean - - languages countries_of_origin run_time \ -0 it IT 1h 30m -1 en US 2h 8m -2 sv SE 1h 40m -3 da DK 1h 18m -4 en,fr GB 1h 26m - - imdb_url -0 https://www.imdb.com/title/tt0038297/ -1 https://www.imdb.com/title/tt0038303/ -2 https://www.imdb.com/title/tt0037544/ -3 https://www.imdb.com/title/tt0124300/ -4 https://www.imdb.com/title/tt0037558/ +Rows: 1851 +Columns: 8 +$ year 1946, 1946, 1946 +$ title 'The Lovers', 'Anna and the King of Siam', 'Blood and Fire' +$ original_title 'Amanti in fuga', None, 'Blod och eld' +$ director 'Giacomo Gentilomo', 'John Cromwell', 'Anders Henrikson' +$ languages 'it', 'en', 'sv' +$ countries_of_origin 'IT', 'US', 'SE' +$ run_time '1h 30m', '2h 8m', '1h 40m' +$ imdb_url 'https://www.imdb.com/title/tt0038297/', + 'https://www.imdb.com/title/tt0038303/', + 'https://www.imdb.com/title/tt0037544/' ``` """ @@ -885,26 +856,21 @@ Preview ------- ``` - name caption lines connect_rer \ -0 Argentine NaN 1 NaN -1 Bastille NaN 1, 5, 8 NaN -2 Bérault NaN 1 NaN -3 Champs-Élysées—Clemenceau Grand Palais 1, 13 NaN -4 Charles de Gaulle—Étoile NaN 1, 2, 6 A - - connect_tramway connect_transilien connect_other passengers latitude \ -0 NaN NaN NaN 2079212 48.875278 -1 NaN NaN NaN 8069243 48.853082 -2 NaN NaN NaN 2106827 48.845278 -3 NaN NaN NaN 1909005 48.867500 -4 NaN NaN NaN 4291663 48.873889 - - longitude location -0 2.290000 Paris 16th, Paris 17th -1 2.369077 Paris 4th, Paris 11th, Paris 12th -2 2.428333 Saint-Mandé, Vincennes -3 2.313500 Paris 8th -4 2.295000 Paris 8th, Paris 16th, Paris 17th +Rows: 314 +Columns: 11 +$ name 'Argentine', 'Bastille', 'Bérault' +$ caption None, None, None +$ lines '1', '1, 5, 8', '1' +$ connect_rer None, None, None +$ connect_tramway None, None, None +$ connect_transilien None, None, None +$ connect_other None, None, None +$ passengers 2079212, 8069243, 2106827 +$ latitude 48.875278, 48.853082, 48.845278 +$ longitude 2.29, 2.369077, 2.428333 +$ location 'Paris 16th, Paris 17th', + 'Paris 4th, Paris 11th, Paris 12th', + 'Saint-Mandé, Vincennes' ``` """ @@ -935,19 +901,18 @@ Preview ------- ``` - date time temp dew_point humidity wind_dir wind_speed \ -0 2023-05-01 00:20 18.9 12.8 0.68 W 6.7 -1 2023-05-01 00:50 18.9 13.9 0.73 WSW 7.2 -2 2023-05-01 01:20 17.8 13.9 0.77 W 6.7 -3 2023-05-01 01:50 18.9 13.9 0.73 W 6.7 -4 2023-05-01 02:20 18.9 12.8 0.68 WSW 6.7 - - wind_gust pressure condition -0 0.0 1015.2 Fair -1 0.0 1015.2 Fair -2 0.0 1014.6 Fair -3 0.0 1014.6 Fair -4 0.0 1014.6 Fair +Rows: 1431 +Columns: 10 +$ date '2023-05-01', '2023-05-01', '2023-05-01' +$ time '00:20', '00:50', '01:20' +$ temp 18.9, 18.9, 17.8 +$ dew_point 12.8, 13.9, 13.9 +$ humidity 0.68, 0.73, 0.77 +$ wind_dir 'W', 'WSW', 'W' +$ wind_speed 6.7, 7.2, 6.7 +$ wind_gust 0.0, 0.0, 0.0 +$ pressure 1015.2, 1015.2, 1014.6 +$ condition 'Fair', 'Fair', 'Fair' ``` """ @@ -979,19 +944,16 @@ Preview ------- ``` - name value uncert \ -0 alpha particle-electron mass ratio 7.294300e+03 2.400000e-07 -1 alpha particle mass 6.644657e-27 2.000000e-36 -2 alpha particle mass energy equivalent 5.971920e-10 1.800000e-19 -3 alpha particle mass energy equivalent in MeV 3.727379e+03 1.100000e-06 -4 alpha particle mass in u 4.001506e+00 6.300000e-11 - - sf_value sf_uncert units -0 12 2 NaN -1 11 2 kg -2 11 2 J -3 11 2 MeV -4 13 2 u +Rows: 354 +Columns: 6 +$ name 'alpha particle-electron mass ratio', + 'alpha particle mass', + 'alpha particle mass energy equivalent' +$ value 7294.29954142, 6.6446573357e-27, 5.9719201914e-10 +$ uncert 2.4e-07, 2e-36, 1.8e-19 +$ sf_value 12, 11, 11 +$ sf_uncert 2, 2, 2 +$ units None, 'kg', 'J' ``` """ @@ -1064,19 +1026,19 @@ Preview ------- ``` - test units day_3 day_4 day_5 day_6 day_7 \ -0 Viral load copies per mL 12000.00 4200.00 1600.00 830.00 760.00 -1 WBC x10^9 / L 5.26 4.26 9.92 10.49 24.77 -2 Neutrophils x10^9 / L 4.87 4.72 7.92 18.21 22.08 -3 RBC x10^12 / L 5.72 5.98 4.23 4.83 4.12 -4 Hb g / L 153.00 135.00 126.00 115.00 75.00 - - day_8 day_9 norm_l norm_u -0 520.00 250.00 NaN NaN -1 30.26 19.03 4.0 10.0 -2 27.17 16.59 2.0 8.0 -3 2.68 3.32 4.0 5.5 -4 87.00 95.00 120.0 160.0 +Rows: 39 +Columns: 11 +$ test 'Viral load', 'WBC', 'Neutrophils' +$ units 'copies per mL', 'x10^9 / L', 'x10^9 / L' +$ day_3 12000.0, 5.26, 4.87 +$ day_4 4200.0, 4.26, 4.72 +$ day_5 1600.0, 9.92, 7.92 +$ day_6 830.0, 10.49, 18.21 +$ day_7 760.0, 24.77, 22.08 +$ day_8 520.0, 30.26, 27.17 +$ day_9 250.0, 19.03, 16.59 +$ norm_l None, 4.0, 2.0 +$ norm_u None, 10.0, 8.0 ``` """ @@ -1153,54 +1115,49 @@ Preview ------- ``` - cmpd_name cmpd_mwt cmpd_formula cmpd_type cmpd_smiles \ -0 methane 16.04 CH4 normal alkane C -1 formaldehyde 30.03 CH2O aldehyde C=O -2 methanol 32.04 CH4O alcohol or glycol CO -3 fluoromethane 34.03 CH3F haloalkane (separated) CF -4 formic acid 46.03 CH2O2 carboxylic acid OC=O - - cmpd_inchi cmpd_inchikey \ -0 InChI=1S/CH4/h1H4 VNWKTOKETHGBQD-UHFFFAOYSA-N -1 InChI=1S/CH2O/c1-2/h1H2 WSFSSNUMVMOOMR-UHFFFAOYSA-N -2 InChI=1S/CH4O/c1-2/h2H,1H3 OKKJLVBELUTLKV-UHFFFAOYSA-N -3 InChI=1S/CH3F/c1-2/h1H3 NBVXSUQYWXRMNV-UHFFFAOYSA-N -4 InChI=1S/CH2O2/c2-1-3/h1H,(H,2,3) BDAGIHXWWSANSR-UHFFFAOYSA-N - - OH_k298 OH_uncert OH_u_fac OH_A OH_B OH_n \ -0 6.360000e-15 0.1 NaN 3.620000e-13 1200.348660 2.179936 -1 8.500000e-12 0.2 NaN 5.400000e-12 -135.000000 NaN -2 8.780000e-13 0.1 NaN 2.320000e-13 -402.000000 2.720000 -3 1.970000e-14 0.1 NaN 1.990000e-13 685.420421 2.040182 -4 4.500000e-13 NaN 1.4 4.500000e-13 NaN NaN - - OH_t_low OH_t_high O3_k298 O3_uncert O3_u_fac O3_A O3_B O3_n \ -0 200.0 2025.0 NaN NaN NaN NaN NaN NaN -1 200.0 300.0 NaN NaN NaN NaN NaN NaN -2 210.0 1344.0 NaN NaN NaN NaN NaN NaN -3 240.0 1800.0 NaN NaN NaN NaN NaN NaN -4 290.0 450.0 NaN NaN NaN NaN NaN NaN - - O3_t_low O3_t_high NO3_k298 NO3_uncert NO3_u_fac NO3_A \ -0 NaN NaN NaN NaN NaN NaN -1 NaN NaN 5.500000e-16 NaN 1.6 NaN -2 NaN NaN 1.300000e-16 NaN 3.0 9.400000e-13 -3 NaN NaN NaN NaN NaN NaN -4 NaN NaN NaN NaN NaN NaN - - NO3_B NO3_n NO3_t_low NO3_t_high Cl_k298 Cl_uncert Cl_u_fac \ -0 NaN NaN NaN NaN 1.000000e-13 0.15 NaN -1 NaN NaN NaN NaN 7.200000e-11 0.15 NaN -2 2650.0 NaN 250.0 370.0 5.100000e-11 0.20 NaN -3 NaN NaN NaN NaN 3.600000e-13 NaN 1.4 -4 NaN NaN NaN NaN 1.900000e-13 NaN 1.4 - - Cl_A Cl_B Cl_n Cl_t_low Cl_t_high -0 6.600000e-12 1240.0 NaN 200.0 300.0 -1 8.100000e-11 34.0 NaN 200.0 500.0 -2 5.100000e-11 0.0 NaN 225.0 950.0 -3 4.900000e-12 781.0 NaN 200.0 300.0 -4 NaN NaN NaN NaN NaN +Rows: 1683 +Columns: 39 +$ cmpd_name 'methane', 'formaldehyde', 'methanol' +$ cmpd_mwt 16.04, 30.03, 32.04 +$ cmpd_formula 'CH4', 'CH2O', 'CH4O' +$ cmpd_type 'normal alkane', 'aldehyde', 'alcohol or glycol' +$ cmpd_smiles 'C', 'C=O', 'CO' +$ cmpd_inchi 'InChI=1S/CH4/h1H4', 'InChI=1S/CH2O/c1-2/h1H2', 'InChI=1S/CH4O/c1-2/h2H,1H3' +$ cmpd_inchikey 'VNWKTOKETHGBQD-UHFFFAOYSA-N', + 'WSFSSNUMVMOOMR-UHFFFAOYSA-N', + 'OKKJLVBELUTLKV-UHFFFAOYSA-N' +$ OH_k298 6.36e-15, 8.5e-12, 8.78e-13 +$ OH_uncert 0.1, 0.2, 0.1 +$ OH_u_fac None, None, None +$ OH_A 3.62e-13, 5.4e-12, 2.32e-13 +$ OH_B 1200.34866000493, -135.0, -402.0 +$ OH_n 2.17993581535803, None, 2.72 +$ OH_t_low 200.0, 200.0, 210.0 +$ OH_t_high 2025.0, 300.0, 1344.0 +$ O3_k298 None, None, None +$ O3_uncert None, None, None +$ O3_u_fac None, None, None +$ O3_A None, None, None +$ O3_B None, None, None +$ O3_n None, None, None +$ O3_t_low None, None, None +$ O3_t_high None, None, None +$ NO3_k298 None, 5.5e-16, 1.3e-16 +$ NO3_uncert None, None, None +$ NO3_u_fac None, 1.6, 3.0 +$ NO3_A None, None, 9.4e-13 +$ NO3_B None, None, 2650.0 +$ NO3_n None, None, None +$ NO3_t_low None, None, 250.0 +$ NO3_t_high None, None, 370.0 +$ Cl_k298 1e-13, 7.2e-11, 5.1e-11 +$ Cl_uncert 0.15, 0.15, 0.2 +$ Cl_u_fac None, None, None +$ Cl_A 6.6e-12, 8.1e-11, 5.1e-11 +$ Cl_B 1240.0, 34.0, 0.0 +$ Cl_n None, None, None +$ Cl_t_low 200.0, 200.0, 225.0 +$ Cl_t_high 300.0, 500.0, 950.0 ``` """ @@ -1242,33 +1199,20 @@ Preview ------- ``` - cmpd_name cmpd_formula products type \ -0 ozone O3 -> O(^1D) + O2 inorganic reactions -1 ozone O3 -> O(^3P) + O2 inorganic reactions -2 hydrogen peroxide H2O2 -> OH + OH inorganic reactions -3 nitrogen dioxide NO2 -> NO + O(^3P) inorganic reactions -4 nitrate radical NO3 -> NO + O2 inorganic reactions - - l m n quantum_yield \ -0 0.000061 1.743 0.474 NaN -1 0.000478 0.298 0.080 NaN -2 0.000010 0.723 0.279 1.0 -3 0.011650 0.244 0.267 NaN -4 0.024850 0.168 0.108 1.0 - - wavelength_nm \ -0 290,291,292,293,294,295,296,297,298,299,300,30... -1 290,291,292,293,294,295,296,297,298,299,300,30... -2 190,195,200,205,210,215,220,225,230,235,240,24... -3 400,401,402,403,404,405,406,407,408,409,410,41... -4 400,401,402,403,404,405,406,407,408,409,410,41... - - sigma_298_cm2 -0 1.43E-18,1.27E-18,1.11E-18,9.94E-19,8.68E-19,7... -1 1.43E-18,1.27E-18,1.11E-18,9.94E-19,8.68E-19,7... -2 6.72E-19,5.63E-19,4.75E-19,4.08E-19,3.57E-19,3... -3 0,0,0,2.00E-20,0,3.00E-20,2.00E-20,1.00E-20,3.... -4 0,0,0,2.00E-22,0,3.00E-22,2.00E-22,1.00E-22,3.... +Rows: 34 +Columns: 10 +$ cmpd_name 'ozone', 'ozone', 'hydrogen peroxide' +$ cmpd_formula 'O3', 'O3', 'H2O2' +$ products '-> O(^1D) + O2', '-> O(^3P) + O2', '-> OH + OH' +$ type 'inorganic reactions', 'inorganic reactions', 'inorganic reactions' +$ l 6.073e-05, 0.0004775, 1.041e-05 +$ m 1.743, 0.298, 0.723 +$ n 0.474, 0.08, 0.279 +$ quantum_yield None, None, 1.0 +$ wavelength_nm '290,291,292,...', '290,291,292,...', '190,195,200,...' +$ sigma_298_cm2 '1.43E-18,1.27E-18,1.11E-18,...', + '1.43E-18,1.27E-18,1.11E-18,...', + '6.72E-19,5.63E-19,4.75E-19,...' ``` """ @@ -1313,47 +1257,37 @@ Preview ------- ``` - nuclide z n element radius radius_uncert abundance \ -0 ^{1}_{1}H0 1 0 H 0.8783 0.0086 0.999855 -1 ^{2}_{1}H1 1 1 H 2.1421 0.0088 0.000145 -2 ^{3}_{1}H2 1 2 H 1.7591 0.0363 NaN -3 ^{4}_{1}H3 1 3 H NaN NaN NaN -4 ^{5}_{1}H4 1 4 H NaN NaN NaN - - abundance_uncert is_stable half_life half_life_uncert isospin decay_1 \ -0 0.000078 TRUE NaN NaN NaN NaN -1 0.000078 TRUE NaN NaN NaN NaN -2 NaN FALSE 3.887813e+08 6.311385e+05 NaN B- -3 NaN FALSE NaN NaN 1 N -4 NaN FALSE 8.608259e-23 6.496799e-24 NaN 2N - - decay_1_pct decay_1_pct_uncert decay_2 decay_2_pct decay_2_pct_uncert \ -0 NaN NaN NaN NaN NaN -1 NaN NaN NaN NaN NaN -2 1.0 NaN NaN NaN NaN -3 1.0 NaN NaN NaN NaN -4 1.0 NaN NaN NaN NaN - - decay_3 decay_3_pct decay_3_pct_uncert magnetic_dipole \ -0 NaN NaN NaN 2.792847 -1 NaN NaN NaN 0.857438 -2 NaN NaN NaN 2.978962 -3 NaN NaN NaN NaN -4 NaN NaN NaN NaN - - magnetic_dipole_uncert electric_quadrupole electric_quadrupole_uncert \ -0 9.000000e-09 NaN NaN -1 5.000000e-09 0.002858 3.000000e-07 -2 1.400000e-08 NaN NaN -3 NaN NaN NaN -4 NaN NaN NaN - - atomic_mass atomic_mass_uncert mass_excess mass_excess_uncert -0 1.007825e+06 0.000014 7288.971064 0.000013 -1 2.014102e+06 0.000015 13135.722895 0.000015 -2 3.016049e+06 0.000080 14949.810900 0.000080 -3 4.026432e+06 107.354000 24621.129000 100.000000 -4 5.035311e+06 96.020000 32892.447000 89.443000 +Rows: 3383 +Columns: 29 +$ nuclide '^{1}_{1}H0', '^{2}_{1}H1', '^{3}_{1}H2' +$ z 1, 1, 1 +$ n 0, 1, 2 +$ element 'H', 'H', 'H' +$ radius 0.8783, 2.1421, 1.7591 +$ radius_uncert 0.0086, 0.0088, 0.0363 +$ abundance 0.999855, 0.000145, None +$ abundance_uncert 7.8e-05, 7.8e-05, None +$ is_stable 'TRUE', 'TRUE', 'FALSE' +$ half_life None, None, 388781328.00697297 +$ half_life_uncert None, None, 631138.51949184 +$ isospin None, None, None +$ decay_1 None, None, 'B-' +$ decay_1_pct None, None, 1.0 +$ decay_1_pct_uncert None, None, None +$ decay_2 None, None, None +$ decay_2_pct None, None, None +$ decay_2_pct_uncert None, None, None +$ decay_3 None, None, None +$ decay_3_pct None, None, None +$ decay_3_pct_uncert None, None, None +$ magnetic_dipole 2.792847351, 0.857438231, 2.97896246 +$ magnetic_dipole_uncert 9e-09, 5e-09, 1.4e-08 +$ electric_quadrupole None, 0.0028578, None +$ electric_quadrupole_uncert None, 3e-07, None +$ atomic_mass 1007825.031898, 2014101.777844, 3016049.28132 +$ atomic_mass_uncert 1.4e-05, 1.5e-05, 8e-05 +$ mass_excess 7288.971064, 13135.722895, 14949.8109 +$ mass_excess_uncert 1.3e-05, 1.5e-05, 8e-05 ``` """ From aa0302811328ded0a0f5bcce07ecacf913fe0cc6 Mon Sep 17 00:00:00 2001 From: jrycw Date: Thu, 19 Sep 2024 12:57:43 +0800 Subject: [PATCH 064/150] Add tests for `val_fmt_image()` --- tests/test_formats_vals.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/test_formats_vals.py diff --git a/tests/test_formats_vals.py b/tests/test_formats_vals.py new file mode 100644 index 000000000..794d41350 --- /dev/null +++ b/tests/test_formats_vals.py @@ -0,0 +1,30 @@ +import base64 +from pathlib import Path +from importlib_resources import files + +import pytest +from great_tables import vals + + +@pytest.fixture +def img_paths(): + return files("great_tables") / "data/metro_images" + + +def test_locate_val_fmt_image(img_paths: Path): + imgs = vals.fmt_image("1", path=img_paths, file_pattern="metro_{}.svg") + with open(img_paths / "metro_1.svg", "rb") as f: + encoded = base64.b64encode(f.read()).decode() + assert encoded in imgs[0] + + +def test_val_fmt_image_single(img_paths: Path): + imgs = vals.fmt_image("1", path=img_paths, file_pattern="metro_{}.svg") + assert "data:image/svg+xml" in imgs[0] + + +def test_val_fmt_image_multiple(img_paths: Path): + img1, img2 = vals.fmt_image(["1", "2"], path=img_paths, file_pattern="metro_{}.svg") + + assert "data:image/svg+xml;base64" in img1 + assert "data:image/svg+xml;base64" in img2 From 5340b99fbad48930e233a40d41a95adee87e1130 Mon Sep 17 00:00:00 2001 From: jrycw Date: Thu, 19 Sep 2024 15:38:19 +0800 Subject: [PATCH 065/150] Strengthen assertion assumptions --- tests/test_formats_vals.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_formats_vals.py b/tests/test_formats_vals.py index 794d41350..660426a56 100644 --- a/tests/test_formats_vals.py +++ b/tests/test_formats_vals.py @@ -15,16 +15,17 @@ def test_locate_val_fmt_image(img_paths: Path): imgs = vals.fmt_image("1", path=img_paths, file_pattern="metro_{}.svg") with open(img_paths / "metro_1.svg", "rb") as f: encoded = base64.b64encode(f.read()).decode() + assert encoded in imgs[0] def test_val_fmt_image_single(img_paths: Path): imgs = vals.fmt_image("1", path=img_paths, file_pattern="metro_{}.svg") - assert "data:image/svg+xml" in imgs[0] + assert 'img src="data:image/svg+xml;base64' in imgs[0] def test_val_fmt_image_multiple(img_paths: Path): img1, img2 = vals.fmt_image(["1", "2"], path=img_paths, file_pattern="metro_{}.svg") - assert "data:image/svg+xml;base64" in img1 - assert "data:image/svg+xml;base64" in img2 + assert 'img src="data:image/svg+xml;base64' in img1 + assert 'img src="data:image/svg+xml;base64' in img2 From 6627b6a33b41b346a9b99c0ff82701f54c401094 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Thu, 19 Sep 2024 15:51:11 -0400 Subject: [PATCH 066/150] fix: support spanner label targeting --- docs/_quarto.yml | 1 + great_tables/_locations.py | 7 ++++++- great_tables/_utils_render_html.py | 14 +++++++++----- tests/test_locations.py | 28 ++++++++++++++++++++++++---- tests/test_utils_render_html.py | 15 ++++++++++++++- 5 files changed, 54 insertions(+), 11 deletions(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index a714db5e4..3ce34e255 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -46,6 +46,7 @@ website: - get-started/column-selection.qmd - get-started/row-selection.qmd - get-started/nanoplots.qmd + - get-started/targeted-styles.qmd format: html: diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 7196f5e8a..d0555b818 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -549,7 +549,12 @@ def _(loc: LocSpannerLabel, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + + new_loc = resolve(loc, data._spanners) + return data._replace( + _styles=data._styles + + [StyleInfo(locname=new_loc, locnum=1, grpname=new_loc.ids, styles=style)] + ) @set_style.register diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 2469596d3..fb721cbe7 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -188,9 +188,9 @@ def create_columns_component_h(data: GTData) -> str: ) # Join the cells into a string and begin each with a newline - th_cells = "\n" + "\n".join([" " + str(tag) for tag in table_col_headings]) + "\n" + # th_cells = "\n" + "\n".join([" " + str(tag) for tag in table_col_headings]) + "\n" - table_col_headings = tags.tr(HTML(th_cells), class_="gt_col_headings") + table_col_headings = tags.tr(*table_col_headings, class_="gt_col_headings") # # Create the spanners and column labels in the case where there *are* spanners ------------- @@ -274,7 +274,8 @@ def create_columns_component_h(data: GTData) -> str: styles_i = [ x for x in styles_spanner_label - if x.grpname == spanner_ids_level_1_index[ii] + # TODO: refactor use of set + if set(x.grpname) & set([spanner_ids_level_1_index[ii]]) ] level_1_spanners.append( @@ -356,7 +357,10 @@ def create_columns_component_h(data: GTData) -> str: # Filter by column label / id, join with overall column labels style # TODO check this filter logic styles_i = [ - x for x in styles_column_label if x.grpname in (colspan, span_label) + x + for x in styles_column_label + # TODO: refactor use of set + if set(x.grpname) & set([colspan, span_label]) ] if span_label: @@ -408,7 +412,7 @@ def create_columns_component_h(data: GTData) -> str: higher_spanner_rows, table_col_headings, ) - return str(table_col_headings) + return table_col_headings def create_body_component_h(data: GTData) -> str: diff --git a/tests/test_locations.py b/tests/test_locations.py index bb0988327..ce535bb88 100644 --- a/tests/test_locations.py +++ b/tests/test_locations.py @@ -1,5 +1,6 @@ import pandas as pd import polars as pl +import polars.selectors as cs import pytest from great_tables import GT from great_tables._gt_data import Spanners @@ -116,6 +117,9 @@ def test_resolve_rows_i_raises(bad_expr): assert "a callable that takes a DataFrame and returns a boolean Series" in expected +# Resolve Loc tests -------------------------------------------------------------------------------- + + def test_resolve_loc_body(): gt = GT(pd.DataFrame({"x": [1, 2], "y": [3, 4]})) @@ -132,20 +136,36 @@ def test_resolve_loc_body(): assert pos.colname == "x" -def test_resolve_column_spanners_simple(): +@pytest.mark.xfail +def test_resolve_loc_spanners_label_single(): + spanners = Spanners.from_ids(["a", "b"]) + loc = LocSpannerLabel(ids="a") + + new_loc = resolve(loc, spanners) + + assert new_loc.ids == ["a"] + + +@pytest.mark.parametrize( + "expr", + [ + ["a", "c"], + pytest.param(cs.by_name("a", "c"), marks=pytest.mark.xfail), + ], +) +def test_resolve_loc_spanners_label(expr): # note that this essentially a no-op ids = ["a", "b", "c"] spanners = Spanners.from_ids(ids) - loc = LocSpannerLabel(ids=["a", "c"]) + loc = LocSpannerLabel(ids=expr) new_loc = resolve(loc, spanners) - assert new_loc == loc assert new_loc.ids == ["a", "c"] -def test_resolve_column_spanners_error_missing(): +def test_resolve_loc_spanner_label_error_missing(): # note that this essentially a no-op ids = ["a", "b", "c"] diff --git a/tests/test_utils_render_html.py b/tests/test_utils_render_html.py index da0241a75..5167e5c9b 100644 --- a/tests/test_utils_render_html.py +++ b/tests/test_utils_render_html.py @@ -29,7 +29,7 @@ def assert_rendered_columns(snapshot, gt): built = gt._build_data("html") columns = create_columns_component_h(built) - assert snapshot == columns + assert snapshot == str(columns) def assert_rendered_body(snapshot, gt): @@ -191,3 +191,16 @@ def test_multiple_spanners_pads_for_stubhead_label(snapshot): ) assert_rendered_columns(snapshot, gt) + + +# Location style rendering ------------------------------------------------------------------------- +# these tests focus on location classes being correctly picked up +def test_loc_column_label(): + gt = GT(pl.DataFrame({"x": [1], "y": [2]})) + + new_gt = gt.tab_style(style.fill("yellow"), loc.column_label(columns=["x"])) + el = create_columns_component_h(new_gt._build_data("html")) + + assert el.name == "tr" + assert el.children[0].attrs["style"] == "background-color: yellow;" + assert "style" not in el.children[1].attrs From 85f096740d785988bb818a42f0b167c3445dd80d Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Thu, 19 Sep 2024 16:08:08 -0400 Subject: [PATCH 067/150] feat: add style.css for raw css --- great_tables/_styles.py | 8 ++++++++ great_tables/style.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/great_tables/_styles.py b/great_tables/_styles.py index 90f8a8fb0..9e028188b 100644 --- a/great_tables/_styles.py +++ b/great_tables/_styles.py @@ -125,6 +125,14 @@ def _raise_if_requires_data(self, loc: Loc): ) +@dataclass +class CellStyleCss(CellStyle): + rule: str + + def _to_html_style(self): + return self.rule + + @dataclass class CellStyleText(CellStyle): """A style specification for cell text. diff --git a/great_tables/style.py b/great_tables/style.py index e6b4c480e..7bd85d96e 100644 --- a/great_tables/style.py +++ b/great_tables/style.py @@ -4,6 +4,7 @@ CellStyleText as text, CellStyleFill as fill, CellStyleBorders as borders, + CellStyleCss as css, ) -__all__ = ("text", "fill", "borders") +__all__ = ("text", "fill", "borders", "css") From eced05568b5b2167d359c2712df6cb33c6acee90 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Thu, 19 Sep 2024 16:08:47 -0400 Subject: [PATCH 068/150] tests: add kitchen sink location snapshot test --- tests/test_utils_render_html.py | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_utils_render_html.py b/tests/test_utils_render_html.py index 5167e5c9b..bea74acd1 100644 --- a/tests/test_utils_render_html.py +++ b/tests/test_utils_render_html.py @@ -204,3 +204,39 @@ def test_loc_column_label(): assert el.name == "tr" assert el.children[0].attrs["style"] == "background-color: yellow;" assert "style" not in el.children[1].attrs + + +def test_loc_kitchen_sink(snapshot): + gt = ( + GT(exibble.loc[[0], ["num", "char", "fctr", "row", "group"]]) + .tab_header("title", "subtitle") + .tab_stub(rowname_col="row", groupname_col="group") + .tab_source_note("yo") + .tab_spanner("spanner", ["char", "fctr"]) + .tab_stubhead("stubhead") + ) + + new_gt = ( + gt.tab_style(style.css("BODY"), loc.body()) + # Columns ----------- + .tab_style(style.css("COLUMN_LABEL"), loc.column_label(columns="num")) + .tab_style(style.css("COLUMN_LABELS"), loc.column_labels()) + .tab_style(style.css("SPANNER_LABEL"), loc.spanner_label(ids=["spanner"])) + # Header ----------- + .tab_style(style.css("HEADER"), loc.header()) + .tab_style(style.css("SUBTITLE"), loc.subtitle()) + .tab_style(style.css("TITLE"), loc.title()) + # Footer ----------- + # .tab_style(style.css("AAA"), loc.source_notes()) + # .tab_style(style.css("AAA"), loc.footnotes()) + # .tab_style(style.css("AAA"), loc.footer()) + # Stub -------------- + .tab_style(style.css("GROUP_LABEL"), loc.row_group_label()) + .tab_style(style.css("ROW_LABEL"), loc.row_label(rows=1)) + .tab_style(style.css("STUB"), loc.stub()) + .tab_style(style.css("STUBHEAD"), loc.stubhead()) + ) + + html = new_gt.as_raw_html() + cleaned = html[html.index(" Date: Thu, 19 Sep 2024 16:09:11 -0400 Subject: [PATCH 069/150] tests: update snapshots --- .../__snapshots__/test_utils_render_html.ambr | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/__snapshots__/test_utils_render_html.ambr b/tests/__snapshots__/test_utils_render_html.ambr index f671a3d3a..2076533af 100644 --- a/tests/__snapshots__/test_utils_render_html.ambr +++ b/tests/__snapshots__/test_utils_render_html.ambr @@ -35,6 +35,54 @@ ''' # --- +# name: test_loc_kitchen_sink + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
title
subtitle
stubheadnum + spanner +
charfctr
grp_a
row_10.1111apricotone
yo
+ + + + ''' +# --- # name: test_multiple_spanners_pads_for_stubhead_label ''' From 17b990a834ad95daa918cca976532cdcdb9b346b Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Thu, 19 Sep 2024 17:56:51 -0400 Subject: [PATCH 070/150] fix: title style tag needs space before --- great_tables/_utils_render_html.py | 6 +++--- tests/__snapshots__/test_utils_render_html.ambr | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index fb721cbe7..58e9c9c70 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -31,7 +31,7 @@ def _flatten_styles(styles: Styles, wrap: bool = False) -> str: if wrap: if rendered_styles: # return style html attribute - return f'style="{" ".join(rendered_styles)}"' + " " + return f' style="{" ".join(rendered_styles)}"' # if no rendered styles, just return a blank return "" if rendered_styles: @@ -505,11 +505,11 @@ def create_body_component_h(data: GTData) -> str: if is_stub_cell: body_cells.append( - f""" {cell_str}""" + f""" {cell_str}""" ) else: body_cells.append( - f""" {cell_str}""" + f""" {cell_str}""" ) prev_group_label = group_label diff --git a/tests/__snapshots__/test_utils_render_html.ambr b/tests/__snapshots__/test_utils_render_html.ambr index 2076533af..184743a8e 100644 --- a/tests/__snapshots__/test_utils_render_html.ambr +++ b/tests/__snapshots__/test_utils_render_html.ambr @@ -41,10 +41,10 @@ - title + title - subtitle + subtitle stubhead From 58436b16cef9ba13de214c6e13b93e09acef7be4 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Mon, 23 Sep 2024 14:05:04 -0400 Subject: [PATCH 071/150] fix: use full html page in show() for correct utf-8 display --- great_tables/_export.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/great_tables/_export.py b/great_tables/_export.py index 54189d26b..cddce58e1 100644 --- a/great_tables/_export.py +++ b/great_tables/_export.py @@ -97,22 +97,23 @@ def show( """ - html = self._repr_html_() - if target == "auto": target = _infer_render_target() if target == "notebook": from IPython.core.display import display_html + html = self._repr_html_() + # https://github.com/ipython/ipython/pull/10962 display_html( # pyright: ignore[reportUnknownVariableType] html, raw=True, metadata={"text/html": {"isolated": True}} ) elif target == "browser": + html = self.as_raw_html(make_page=True) with tempfile.TemporaryDirectory() as tmp_dir: f_path = Path(tmp_dir) / "index.html" - f_path.write_text(html) + f_path.write_text(html, encoding="utf-8") # create a server that closes after 1 request ---- server = _create_temp_file_server(f_path) From f79e7e6a8a77c1381b637e9fcd4287c107dc199d Mon Sep 17 00:00:00 2001 From: jrycw Date: Tue, 24 Sep 2024 09:58:49 +0800 Subject: [PATCH 072/150] update `_save_screenshot()` --- great_tables/_export.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/great_tables/_export.py b/great_tables/_export.py index 4ec833443..0c4a145f7 100644 --- a/great_tables/_export.py +++ b/great_tables/_export.py @@ -404,12 +404,7 @@ def _save_screenshot( from PIL import Image # convert to other formats (e.g. pdf, bmp) using PIL - with tempfile.TemporaryDirectory() as tmp_dir: - fname = f"{tmp_dir}/image.png" - el.screenshot(fname) - - with open(fname, "rb") as f: - Image.open(fp=BytesIO(f.read())).save(fp=path) + Image.open(fp=BytesIO(el.screenshot_as_png)).save(fp=path) def _dump_debug_screenshot(driver, path): From a5c533af5ccd4b965e1bd2690bc195acaab72996 Mon Sep 17 00:00:00 2001 From: jrycw Date: Tue, 24 Sep 2024 16:43:21 +0800 Subject: [PATCH 073/150] Update `superbowl` example to align with the new version of Polars --- docs/blog/superbowl-squares/_code.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/blog/superbowl-squares/_code.py b/docs/blog/superbowl-squares/_code.py index 5a06dd135..42b3bb6b1 100644 --- a/docs/blog/superbowl-squares/_code.py +++ b/docs/blog/superbowl-squares/_code.py @@ -41,10 +41,10 @@ def team_final_digits(game: pl.DataFrame, team_code: str) -> pl.DataFrame: # Cross and multiply p(digit | team=KC)p(digit | team=SF) to get # the joint probability p(digit_KC, digit_SF | KC, SF) joint = ( - home.join(away, on="final_digit", how="cross") + home.join(away, how="cross") .with_columns(joint=pl.col("prop") * pl.col("prop_right")) .sort("final_digit", "final_digit_right") - .pivot(values="joint", columns="final_digit_right", index="final_digit") + .pivot(values="joint", on="final_digit_right", index="final_digit") .with_columns((cs.exclude("final_digit") * 100).round(1)) ) From e4b6b6c745c04b967237814d1d2acd96ca149c2d Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 24 Sep 2024 13:38:27 -0400 Subject: [PATCH 074/150] fix: resolve_rows_i can always handle None expr --- great_tables/_locations.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index d0555b818..319d53305 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -332,17 +332,17 @@ def resolve_rows_i( expr: list[str | int] = [expr] if isinstance(data, GTData): - if expr is None: - if null_means == "everything": - return [(row.rowname, ii) for ii, row in enumerate(data._stub)] - else: - return [] - row_names = [row.rowname for row in data._stub] else: row_names = data - if isinstance(expr, list): + if expr is None: + if null_means == "everything": + return [(row.rowname, ii) for ii, row in enumerate(data._stub)] + else: + return [] + + elif isinstance(expr, list): # TODO: manually doing row selection here for now target_names = set(x for x in expr if isinstance(x, str)) target_pos = set( From 978bce425cdb11515d0df24ff5bc99a348c5ca29 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 24 Sep 2024 13:42:11 -0400 Subject: [PATCH 075/150] feat: implement LocRowGroupLabel styles --- great_tables/_gt_data.py | 6 ++--- great_tables/_locations.py | 16 +++++++++-- great_tables/_utils_render_html.py | 27 ++++++++++--------- .../__snapshots__/test_utils_render_html.ambr | 2 +- tests/test_gt_data.py | 3 ++- 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index 2fa250e82..bfe4bb7cb 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -608,7 +608,7 @@ def order_groups(self, group_order: RowGroups): # TODO: validate return self.__class__(self.rows, self.group_rows.reorder(group_order)) - def group_indices_map(self) -> list[tuple[int, str | None]]: + def group_indices_map(self) -> list[tuple[int, GroupRowInfo | None]]: return self.group_rows.indices_map(len(self.rows)) def __iter__(self): @@ -738,7 +738,7 @@ def reorder(self, group_ids: list[str | MISSING_GROUP]) -> Self: return self.__class__(reordered) - def indices_map(self, n: int) -> list[tuple[int, str | None]]: + def indices_map(self, n: int) -> list[tuple[int, GroupRowInfo]]: """Return pairs of row index, group label for all rows in data. Note that when no groupings exist, n is used to return from range(n). @@ -749,7 +749,7 @@ def indices_map(self, n: int) -> list[tuple[int, str | None]]: if not len(self._d): return [(ii, None) for ii in range(n)] - return [(ind, info.defaulted_label()) for info in self for ind in info.indices] + return [(ind, info) for info in self for ind in info.indices] # Spanners ---- diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 319d53305..229572c30 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -416,6 +416,13 @@ def _(loc: LocColumnLabel, data: GTData) -> list[CellPos]: return cell_pos +@resolve.register +def _(loc: LocRowGroupLabel, data: GTData) -> set[int]: + # TODO: what are the rules for matching row groups? + group_pos = set(pos for _, pos in resolve_rows_i(data, loc.rows)) + return list(group_pos) + + @resolve.register def _(loc: LocRowLabel, data: GTData) -> list[CellPos]: rows = resolve_rows_i(data=data, expr=loc.rows) @@ -570,8 +577,11 @@ def _(loc: LocRowGroupLabel, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + + row_groups = resolve(loc, data) + return data._replace( + _styles=data._styles + [StyleInfo(locname=loc, locnum=1, grpname=row_groups, styles=style)] + ) @set_style.register @@ -580,6 +590,8 @@ def _(loc: LocRowLabel, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve + cells = resolve(loc, data) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 58e9c9c70..7b748cbb6 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -7,7 +7,7 @@ from great_tables._spanners import spanners_print_matrix from htmltools import HTML, TagList, css, tags -from ._gt_data import GTData, Styles +from ._gt_data import GTData, Styles, GroupRowInfo from ._tbl_data import _get_cell, cast_frame_to_string, n_rows, replace_null_frame from ._text import _process_text, _process_text_id from ._utils import heading_has_subtitle, heading_has_title, seq_groups @@ -451,34 +451,36 @@ def create_body_component_h(data: GTData) -> str: body_rows: list[str] = [] # iterate over rows (ordered by groupings) - prev_group_label = None + prev_group_info = None - ordered_index = data._stub.group_indices_map() + ordered_index: list[tuple[int, GroupRowInfo]] = data._stub.group_indices_map() - for i, group_label in ordered_index: + for i, group_info in ordered_index: body_cells: list[str] = [] + # Create row for group (if applicable) if has_stub_column and has_groups and not has_two_col_stub: colspan_value = data._boxhead._get_effective_number_of_columns( stub=data._stub, options=data._options ) - # Generate a row that contains the row group label (this spans the entire row) but - # only if `i` indicates there should be a row group label - if group_label != prev_group_label: + # Only create if this is the first row within the group + if group_info is not prev_group_info: + group_label = group_info.defaulted_label() group_class = ( "gt_empty_group_heading" if group_label == "" else "gt_group_heading_row" ) + _styles = [style for style in styles_row_group_label if i in style.grpname] + group_styles = _flatten_styles(_styles, wrap=True) + print(group_styles) group_row = f""" - {group_label} + {group_label} """ - prev_group_label = group_label - body_rows.append(group_row) - # Create a single cell and append result to `body_cells` + # Create row cells for colinfo in column_vars: cell_content: Any = _get_cell(tbl_data, i, colinfo.var) cell_str: str = str(cell_content) @@ -512,7 +514,8 @@ def create_body_component_h(data: GTData) -> str: f""" {cell_str}""" ) - prev_group_label = group_label + prev_group_info = group_info + body_rows.append(" \n" + "\n".join(body_cells) + "\n ") all_body_rows = "\n".join(body_rows) diff --git a/tests/__snapshots__/test_utils_render_html.ambr b/tests/__snapshots__/test_utils_render_html.ambr index 184743a8e..6949c792c 100644 --- a/tests/__snapshots__/test_utils_render_html.ambr +++ b/tests/__snapshots__/test_utils_render_html.ambr @@ -60,7 +60,7 @@ - grp_a + grp_a row_1 diff --git a/tests/test_gt_data.py b/tests/test_gt_data.py index ee89b3d70..9feea05d0 100644 --- a/tests/test_gt_data.py +++ b/tests/test_gt_data.py @@ -31,7 +31,8 @@ def test_stub_order_groups(): stub2 = stub.order_groups(["c", "a", "b"]) assert stub2.group_ids == ["c", "a", "b"] - assert stub2.group_indices_map() == [(3, "c"), (1, "a"), (0, "b"), (2, "b")] + indice_labels = [(ii, info.defaulted_label()) for ii, info in stub2.group_indices_map()] + assert indice_labels == [(3, "c"), (1, "a"), (0, "b"), (2, "b")] def test_boxhead_reorder(): From 2fab1505e2ecb87dbba813824b9d0cfc7c68e7a7 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 24 Sep 2024 14:16:42 -0400 Subject: [PATCH 076/150] feat: support LocRowLabel styles --- great_tables/_locations.py | 9 ++++++--- great_tables/_utils_render_html.py | 21 +++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 229572c30..924395709 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -419,14 +419,16 @@ def _(loc: LocColumnLabel, data: GTData) -> list[CellPos]: @resolve.register def _(loc: LocRowGroupLabel, data: GTData) -> set[int]: # TODO: what are the rules for matching row groups? + # TODO: resolve_rows_i will match a list expr to row names (not group names) group_pos = set(pos for _, pos in resolve_rows_i(data, loc.rows)) return list(group_pos) @resolve.register -def _(loc: LocRowLabel, data: GTData) -> list[CellPos]: +def _(loc: LocRowLabel, data: GTData) -> set[int]: + # TODO: what are the rules for matching row groups? rows = resolve_rows_i(data=data, expr=loc.rows) - cell_pos = [CellPos(0, row[1], colname="", rowname=row[0]) for row in rows] + cell_pos = set(row[1] for row in rows) return cell_pos @@ -592,7 +594,8 @@ def _(loc: LocRowLabel, data: GTData, style: list[CellStyle]) -> GTData: # TODO resolve cells = resolve(loc, data) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + new_styles = [StyleInfo(locname=loc, locnum=1, rownum=rownum, styles=style) for rownum in cells] + return data._replace(_styles=data._styles + new_styles) @set_style.register diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 7b748cbb6..7d85be82a 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -458,13 +458,13 @@ def create_body_component_h(data: GTData) -> str: for i, group_info in ordered_index: body_cells: list[str] = [] - # Create row for group (if applicable) + # Create table row specifically for group (if applicable) if has_stub_column and has_groups and not has_two_col_stub: colspan_value = data._boxhead._get_effective_number_of_columns( stub=data._stub, options=data._options ) - # Only create if this is the first row within the group + # Only create if this is the first row of data within the group if group_info is not prev_group_info: group_label = group_info.defaulted_label() group_class = ( @@ -497,19 +497,20 @@ def create_body_component_h(data: GTData) -> str: # Get the style attributes for the current cell by filtering the # `styles_cells` list for the current row and column - styles_i = [x for x in styles_cells if x.rownum == i and x.colname == colinfo.var] - - # Develop the `style` attribute for the current cell - if len(styles_i) > 0: - cell_styles = _flatten_styles(styles_i, wrap=True) - else: - cell_styles = "" + _body_styles = [x for x in styles_cells if x.rownum == i and x.colname == colinfo.var] if is_stub_cell: + # TODO: what order should styles for these parts be combined in? + _rowname_styles = [x for x in styles_row_label if x.rownum == i] + cell_styles = _flatten_styles( + styles_stub + _body_styles + _rowname_styles, + wrap=True, + ) body_cells.append( - f""" {cell_str}""" + f""" {cell_str}""" ) else: + cell_styles = _flatten_styles(_body_styles, wrap=True) body_cells.append( f""" {cell_str}""" ) From 7cccdafb758b08f2b6a2ff0f805a7d02220361c4 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 24 Sep 2024 14:29:10 -0400 Subject: [PATCH 077/150] feat: support LocSourceNotes styles --- great_tables/_utils_render_html.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 7d85be82a..c873cf11b 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -556,10 +556,11 @@ def create_source_notes_component_h(data: GTData) -> str: for note in source_notes: note_str = _process_text(note) + _styles = _flatten_styles(styles_source_notes, wrap=True) source_notes_tr.append( f""" - {note_str} + {note_str} """ ) From 8f5dadad5ff33687fc71fe56a1b478f60da646da Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 24 Sep 2024 14:32:53 -0400 Subject: [PATCH 078/150] tests: add source note to styles test, update snapshot --- tests/__snapshots__/test_utils_render_html.ambr | 2 +- tests/test_utils_render_html.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/__snapshots__/test_utils_render_html.ambr b/tests/__snapshots__/test_utils_render_html.ambr index 6949c792c..7246203a1 100644 --- a/tests/__snapshots__/test_utils_render_html.ambr +++ b/tests/__snapshots__/test_utils_render_html.ambr @@ -72,7 +72,7 @@ - yo + yo diff --git a/tests/test_utils_render_html.py b/tests/test_utils_render_html.py index bea74acd1..71afe8ca0 100644 --- a/tests/test_utils_render_html.py +++ b/tests/test_utils_render_html.py @@ -227,7 +227,7 @@ def test_loc_kitchen_sink(snapshot): .tab_style(style.css("SUBTITLE"), loc.subtitle()) .tab_style(style.css("TITLE"), loc.title()) # Footer ----------- - # .tab_style(style.css("AAA"), loc.source_notes()) + .tab_style(style.css("SOURCE_NOTES"), loc.source_notes()) # .tab_style(style.css("AAA"), loc.footnotes()) # .tab_style(style.css("AAA"), loc.footer()) # Stub -------------- From b549c7e9f89334d7bac260f25106dda05e8b55fa Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 24 Sep 2024 14:36:56 -0400 Subject: [PATCH 079/150] tests: correctly target row of data in snapshot --- tests/__snapshots__/test_utils_render_html.ambr | 2 +- tests/test_utils_render_html.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/__snapshots__/test_utils_render_html.ambr b/tests/__snapshots__/test_utils_render_html.ambr index 7246203a1..10dcd1515 100644 --- a/tests/__snapshots__/test_utils_render_html.ambr +++ b/tests/__snapshots__/test_utils_render_html.ambr @@ -63,7 +63,7 @@ grp_a - row_1 + row_1 0.1111 apricot one diff --git a/tests/test_utils_render_html.py b/tests/test_utils_render_html.py index 71afe8ca0..e4a9e2bc7 100644 --- a/tests/test_utils_render_html.py +++ b/tests/test_utils_render_html.py @@ -232,7 +232,7 @@ def test_loc_kitchen_sink(snapshot): # .tab_style(style.css("AAA"), loc.footer()) # Stub -------------- .tab_style(style.css("GROUP_LABEL"), loc.row_group_label()) - .tab_style(style.css("ROW_LABEL"), loc.row_label(rows=1)) + .tab_style(style.css("ROW_LABEL"), loc.row_label(rows=[0])) .tab_style(style.css("STUB"), loc.stub()) .tab_style(style.css("STUBHEAD"), loc.stubhead()) ) From 8b4b81136f812b6d1f2bd48ff41d0d1a32328834 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 24 Sep 2024 14:37:52 -0400 Subject: [PATCH 080/150] refactor: remove LocStubheadLabel --- great_tables/_utils_render_html.py | 5 ++--- great_tables/loc.py | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index c873cf11b..6fe43eaba 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -134,7 +134,6 @@ def create_columns_component_h(data: GTData) -> str: # Filter list of StyleInfo for the various stubhead and column labels components styles_stubhead = [x for x in data._styles if _is_loc(x.locname, loc.LocStubhead)] - styles_stubhead_label = [x for x in data._styles if _is_loc(x.locname, loc.LocStubheadLabel)] styles_column_labels = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnLabels)] styles_spanner_label = [x for x in data._styles if _is_loc(x.locname, loc.LocSpannerLabel)] styles_column_label = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnLabel)] @@ -164,7 +163,7 @@ def create_columns_component_h(data: GTData) -> str: class_=f"gt_col_heading gt_columns_bottom_border gt_{stubhead_label_alignment}", rowspan="1", colspan=len(stub_layout), - style=_flatten_styles(styles_stubhead + styles_stubhead_label), + style=_flatten_styles(styles_stubhead), scope="colgroup" if len(stub_layout) > 1 else "col", id=_process_text_id(stub_label), ) @@ -226,7 +225,7 @@ def create_columns_component_h(data: GTData) -> str: class_=f"gt_col_heading gt_columns_bottom_border gt_{str(stubhead_label_alignment)}", rowspan=2, colspan=len(stub_layout), - style=_flatten_styles(styles_stubhead + styles_stubhead_label), + style=_flatten_styles(styles_stubhead), scope="colgroup" if len(stub_layout) > 1 else "col", id=_process_text_id(stub_label), ) diff --git a/great_tables/loc.py b/great_tables/loc.py index 07c637a6f..9fec1f4c1 100644 --- a/great_tables/loc.py +++ b/great_tables/loc.py @@ -8,7 +8,6 @@ # # Stubhead ---- LocStubhead as stubhead, - LocStubheadLabel as stubhead_label, # # Column Labels ---- LocColumnLabels as column_labels, @@ -19,14 +18,17 @@ LocStub as stub, LocRowGroupLabel as row_group_label, LocRowLabel as row_label, + # TODO: remove for now LocSummaryLabel as summary_label, # # Body ---- LocBody as body, + # TODO: remove for now LocSummary as summary, # # Footer ---- LocFooter as footer, + # TODO: remove for now LocFootnotes as footnotes, LocSourceNotes as source_notes, ) @@ -36,7 +38,6 @@ "title", "subtitle", "stubhead", - "stubhead_label", "column_labels", "spanner_label", "column_label", From 033ef5bf0e2a32a6597f4fa0f0f33de9055d2d4a Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 24 Sep 2024 14:39:17 -0400 Subject: [PATCH 081/150] docs: add targeted styles page to get-started --- docs/get-started/targeted-styles.qmd | 124 +++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 docs/get-started/targeted-styles.qmd diff --git a/docs/get-started/targeted-styles.qmd b/docs/get-started/targeted-styles.qmd new file mode 100644 index 000000000..3a76d4ae0 --- /dev/null +++ b/docs/get-started/targeted-styles.qmd @@ -0,0 +1,124 @@ +--- +title: Targeted styles +jupyter: python3 +--- + +## Kitchen sink + +```{python} +from great_tables import GT, exibble, loc, style + +COLOR = "yellow" + +# https://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12 +brewer_colors = [ + "#a6cee3", + "#1f78b4", + "#b2df8a", + "#33a02c", + "#fb9a99", + "#e31a1c", + "#fdbf6f", + "#ff7f00", + "#cab2d6", + "#6a3d9a", + "#ffff99", + "#b15928", +] + +c = iter(brewer_colors) + +gt = ( + GT(exibble.loc[[0, 1, 4], ["num", "char", "fctr", "row", "group"]]) + .tab_header("title", "subtitle") + .tab_stub(rowname_col="row", groupname_col="group") + .tab_source_note("yo") + .tab_spanner("spanner", ["char", "fctr"]) + .tab_stubhead("stubhead") +) + +( + gt.tab_style(style.fill(next(c)), loc.body()) + # Columns ----------- + # TODO: appears in browser, but not vs code + .tab_style(style.fill(next(c)), loc.column_label(columns="num")) + .tab_style(style.fill(next(c)), loc.column_labels()) + .tab_style(style.fill(next(c)), loc.spanner_label(ids=["spanner"])) + # Header ----------- + .tab_style(style.fill(next(c)), loc.header()) + .tab_style(style.fill(next(c)), loc.subtitle()) + .tab_style(style.fill(next(c)), loc.title()) + # Footer ----------- + .tab_style(style.fill(next(c)), loc.source_notes()) + # .tab_style(style.fill(next(c)), loc.footnotes()) + # .tab_style(style.fill(next(c)), loc.footer()) + # Stub -------------- + .tab_style(style.fill(next(c)), loc.row_group_label()) + .tab_style(style.fill(next(c)), loc.row_label(rows=1)) + .tab_style(style.fill(next(c)), loc.stub()) + .tab_style(style.fill(next(c)), loc.stubhead()) +) +``` + +## Body + +```{python} +gt.tab_style(style.fill(COLOR), loc.body()) +``` + +## Column labels + +```{python} +( + gt + .tab_style(style.fill(COLOR), loc.column_labels()) + .tab_style(style.fill("blue"), loc.column_label(columns="num")) + .tab_style(style.fill("red"), loc.spanner_label(ids=["spanner"])) +) + +``` + + + +## Header + +```{python} +( + gt.tab_style(style.fill(COLOR), loc.header()) + .tab_style(style.fill("blue"), loc.title()) + .tab_style(style.fill("red"), loc.subtitle()) +) +``` + +## Footer + +```{python} +( + gt.tab_style( + style.fill(COLOR), + loc.source_notes(), + ).tab_style( + style.fill(COLOR), + loc.footer(), + ) +) +``` + +## Stub + +```{python} +( + gt.tab_style(style.fill(COLOR), loc.stub()) + .tab_style(style.fill("blue"), loc.row_group_label()) + .tab_style( + style.borders(style="dashed", weight="3px", color="red"), + loc.row_label(rows=[1]), + ) +) +``` + +## Stubhead + +```{python} +gt.tab_style(style.fill(COLOR), loc.stubhead()) +``` From a3bf5dfee6af81bc74870480e1bdbc8286457c46 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 24 Sep 2024 14:57:12 -0400 Subject: [PATCH 082/150] chore: remove print statement --- great_tables/_utils_render_html.py | 1 - 1 file changed, 1 deletion(-) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 6fe43eaba..dbba4d0f1 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -472,7 +472,6 @@ def create_body_component_h(data: GTData) -> str: _styles = [style for style in styles_row_group_label if i in style.grpname] group_styles = _flatten_styles(_styles, wrap=True) - print(group_styles) group_row = f""" {group_label} """ From e5526e1a2aec64f74b1330e85beb432730b3aafa Mon Sep 17 00:00:00 2001 From: jrycw Date: Wed, 25 Sep 2024 14:19:17 +0800 Subject: [PATCH 083/150] Fix deprecation warning in the `superbowl` example. --- docs/blog/superbowl-squares/index.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blog/superbowl-squares/index.qmd b/docs/blog/superbowl-squares/index.qmd index c6f0b1808..4694752d6 100644 --- a/docs/blog/superbowl-squares/index.qmd +++ b/docs/blog/superbowl-squares/index.qmd @@ -47,7 +47,7 @@ df = ( .with_columns( z=pl.when((pl.col("x") == 7) & (pl.col("y") == 4)).then(pl.lit("4/7")).otherwise("z") ) - .pivot(index="x", values="z", columns="y") + .pivot(index="x", values="z", on="y") .with_row_index() ) From 5548da17de57e08a3f5a30fb03a52f2bf29fd8f2 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Wed, 25 Sep 2024 15:39:33 -0400 Subject: [PATCH 084/150] feat: implement row striping options (#461) * Implement row striping in HTML table * Improve documentation for `opt_row_striping()` * Add example to `opt_row_striping()` * Add tests for row striping (including snapshot) * Add `opt_row_striping()` to reference docs * Modify tests based on code review comments * Consolidate table body generation code * Use separate snapshots for striping of stub & body --- docs/_quarto.yml | 1 + great_tables/_gt_data.py | 10 +-- great_tables/_options.py | 46 ++++++++++++-- great_tables/_utils_render_html.py | 38 +++++++++-- great_tables/css/gt_styles_default.scss | 4 ++ tests/__snapshots__/test_export.ambr | 1 + tests/__snapshots__/test_options.ambr | 83 +++++++++++++++++++++++++ tests/__snapshots__/test_repr.ambr | 5 ++ tests/__snapshots__/test_scss.ambr | 4 ++ tests/test_options.py | 54 ++++++++++++++++ 10 files changed, 231 insertions(+), 15 deletions(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index a714db5e4..d69673cc4 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -171,6 +171,7 @@ quartodoc: having to use `tab_options()` directly. contents: - GT.opt_align_table_header + - GT.opt_row_striping - GT.opt_all_caps - GT.opt_vertical_padding - GT.opt_horizontal_padding diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index ba57fb614..58355a435 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -1160,11 +1160,11 @@ class Options: ) source_notes_multiline: OptionsInfo = OptionsInfo(False, "source_notes", "boolean", True) source_notes_sep: OptionsInfo = OptionsInfo(False, "source_notes", "value", " ") - # row_striping_background_color: OptionsInfo = OptionsInfo( - # True, "row", "value", "rgba(128,128,128,0.05)" - # ) - # row_striping_include_stub: OptionsInfo = OptionsInfo(False, "row", "boolean", False) - # row_striping_include_table_body: OptionsInfo = OptionsInfo(False, "row", "boolean", False) + row_striping_background_color: OptionsInfo = OptionsInfo( + True, "row", "value", "rgba(128,128,128,0.05)" + ) + row_striping_include_stub: OptionsInfo = OptionsInfo(False, "row", "boolean", False) + row_striping_include_table_body: OptionsInfo = OptionsInfo(False, "row", "boolean", False) container_width: OptionsInfo = OptionsInfo(False, "container", "px", "auto") container_height: OptionsInfo = OptionsInfo(False, "container", "px", "auto") container_padding_x: OptionsInfo = OptionsInfo(False, "container", "px", "0px") diff --git a/great_tables/_options.py b/great_tables/_options.py index 16d90a897..2d5f8c528 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -161,9 +161,9 @@ def tab_options( source_notes_border_lr_color: str | None = None, source_notes_multiline: bool | None = None, source_notes_sep: str | None = None, - # row_striping_background_color: str | None = None, - # row_striping_include_stub: bool | None = None, - # row_striping_include_table_body: bool | None = None, + row_striping_background_color: str | None = None, + row_striping_include_stub: bool | None = None, + row_striping_include_table_body: bool | None = None, ) -> GTSelf: """ Modify the table output options. @@ -466,6 +466,13 @@ def tab_options( this border is larger, then it will be the visible border. source_notes_border_lr_color The color of the left and right borders of the `source_notes` location. + row_striping_background_color + The background color for striped table body rows. A color name or a hexadecimal color code + should be provided. + row_striping_include_stub + An option for whether to include the stub when striping rows. + row_striping_include_table_body + An option for whether to include the table body when striping rows. Returns ------- @@ -603,9 +610,9 @@ def opt_row_striping(self: GTSelf, row_striping: bool = True) -> GTSelf: """ Option to add or remove row striping. - By default, a gt*table does not have row striping enabled. However, this method allows us to - easily enable or disable striped rows in the table body. It's a convenient shortcut for - `gt.tab_options(row_striping_include_table_body=)`. + By default, a table does not have row striping enabled. However, this method allows us to easily + enable or disable striped rows in the table body. It's a convenient shortcut for + `tab_options(row_striping_include_table_body=)`. Parameters ---------- @@ -618,7 +625,34 @@ def opt_row_striping(self: GTSelf, row_striping: bool = True) -> GTSelf: GT The GT object is returned. This is the same object that the method is called on so that we can facilitate method chaining. + + Examples + -------- + Using only a few columns from the `exibble` dataset, let's create a table with a number of + components added. Following that, we'll add row striping to every second row with the + `opt_row_striping()` method. + + ```{python} + from great_tables import GT, exibble, md + + ( + GT( + exibble[["num", "char", "currency", "row", "group"]], + rowname_col="row", + groupname_col="group" + ) + .tab_header( + title=md("Data listing from **exibble**"), + subtitle=md("`exibble` is a **Great Tables** dataset.") + ) + .fmt_number(columns="num") + .fmt_currency(columns="currency") + .tab_source_note(source_note="This is only a subset of the dataset.") + .opt_row_striping() + ) + ``` """ + return tab_options(self, row_striping_include_table_body=row_striping) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index f12d59828..4f8489a6a 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -444,6 +444,12 @@ def create_body_component_h(data: GTData) -> str: if stub_var is not None: column_vars = [stub_var] + column_vars + # Is the stub to be striped? + table_stub_striped = data._options.row_striping_include_stub.value + + # Are the rows in the table body to be striped? + table_body_striped = data._options.row_striping_include_table_body.value + body_rows: list[str] = [] # iterate over rows (ordered by groupings) @@ -452,6 +458,12 @@ def create_body_component_h(data: GTData) -> str: ordered_index = data._stub.group_indices_map() for i, group_label in ordered_index: + + # For table striping we want to add a striping CSS class to the even-numbered + # rows in the rendered table; to target these rows, determine if `i` in the current + # row render is an odd number + odd_i_row = i % 2 == 1 + body_cells: list[str] = [] if has_stub_column and has_groups and not has_two_col_stub: @@ -503,11 +515,29 @@ def create_body_component_h(data: GTData) -> str: cell_styles = "" if is_stub_cell: - body_cells.append(f""" {cell_str}""") + + el_name = "th" + + classes = ["gt_row", "gt_left", "gt_stub"] + + if table_stub_striped and odd_i_row: + classes.append("gt_striped") + else: - body_cells.append( - f""" {cell_str}""" - ) + + el_name = "td" + + classes = ["gt_row", f"gt_{cell_alignment}"] + + if table_body_striped and odd_i_row: + classes.append("gt_striped") + + # Ensure that `classes` becomes a space-separated string + classes = " ".join(classes) + + body_cells.append( + f""" <{el_name} {cell_styles}class="{classes}">{cell_str}""" + ) prev_group_label = group_label body_rows.append(" \n" + "\n".join(body_cells) + "\n ") diff --git a/great_tables/css/gt_styles_default.scss b/great_tables/css/gt_styles_default.scss index 429f3221b..1fba67e43 100644 --- a/great_tables/css/gt_styles_default.scss +++ b/great_tables/css/gt_styles_default.scss @@ -263,6 +263,10 @@ p { border-top-width: $row_group_border_top_width; } +.gt_striped { + background-color: $row_striping_background_color; +} + .gt_table_body { border-top-style: $table_body_border_top_style; border-top-width: $table_body_border_top_width; diff --git a/tests/__snapshots__/test_export.ambr b/tests/__snapshots__/test_export.ambr index 55f06da4a..aa967589e 100644 --- a/tests/__snapshots__/test_export.ambr +++ b/tests/__snapshots__/test_export.ambr @@ -34,6 +34,7 @@ #test_table .gt_stub_row_group { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; vertical-align: top; } #test_table .gt_row_group_first td { border-top-width: 2px; } #test_table .gt_row_group_first th { border-top-width: 2px; } + #test_table .gt_striped { background-color: rgba(128,128,128,0.05); } #test_table .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; } #test_table .gt_sourcenotes { color: #333333; background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; } #test_table .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; } diff --git a/tests/__snapshots__/test_options.ambr b/tests/__snapshots__/test_options.ambr index ed08250d4..b7a5463e4 100644 --- a/tests/__snapshots__/test_options.ambr +++ b/tests/__snapshots__/test_options.ambr @@ -272,6 +272,10 @@ border-top-width: 5px; } + #abc .gt_striped { + background-color: rgba(128,128,128,0.05); + } + #abc .gt_table_body { border-top-style: solid; border-top-width: 5px; @@ -619,6 +623,10 @@ border-top-width: 2px; } + #abc .gt_striped { + background-color: rgba(128,128,128,0.05); + } + #abc .gt_table_body { border-top-style: solid; border-top-width: 2px; @@ -693,3 +701,78 @@ ''' # --- +# name: test_tab_options_striping_body_snap + ''' + + + grp_a + + + row_1 + apricot + + + row_2 + banana + + + row_3 + coconut + + + row_4 + durian + + + ''' +# --- +# name: test_tab_options_striping_snap + ''' + + + grp_a + + + row_1 + apricot + + + row_2 + banana + + + row_3 + coconut + + + row_4 + durian + + + ''' +# --- +# name: test_tab_options_striping_stub_snap + ''' + + + grp_a + + + row_1 + apricot + + + row_2 + banana + + + row_3 + coconut + + + row_4 + durian + + + ''' +# --- diff --git a/tests/__snapshots__/test_repr.ambr b/tests/__snapshots__/test_repr.ambr index 1319d6e2b..c793901eb 100644 --- a/tests/__snapshots__/test_repr.ambr +++ b/tests/__snapshots__/test_repr.ambr @@ -34,6 +34,7 @@ #test .gt_stub_row_group { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; vertical-align: top; } #test .gt_row_group_first td { border-top-width: 2px; } #test .gt_row_group_first th { border-top-width: 2px; } + #test .gt_striped { background-color: rgba(128,128,128,0.05); } #test .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; } #test .gt_sourcenotes { color: #333333; background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; } #test .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; } @@ -109,6 +110,7 @@ #test .gt_stub_row_group { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; vertical-align: top; } #test .gt_row_group_first td { border-top-width: 2px; } #test .gt_row_group_first th { border-top-width: 2px; } + #test .gt_striped { background-color: rgba(128,128,128,0.05); } #test .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; } #test .gt_sourcenotes { color: #333333; background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; } #test .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; } @@ -190,6 +192,7 @@ #test .gt_stub_row_group { color: #333333 !important; background-color: #FFFFFF !important; font-size: 100% !important; font-weight: initial !important; text-transform: inherit !important; border-right-style: solid !important; border-right-width: 2px !important; border-right-color: #D3D3D3 !important; padding-left: 5px !important; padding-right: 5px !important; vertical-align: top !important; } #test .gt_row_group_first td { border-top-width: 2px !important; } #test .gt_row_group_first th { border-top-width: 2px !important; } + #test .gt_striped { background-color: rgba(128,128,128,0.05) !important; } #test .gt_table_body { border-top-style: solid !important; border-top-width: 2px !important; border-top-color: #D3D3D3 !important; border-bottom-style: solid !important; border-bottom-width: 2px !important; border-bottom-color: #D3D3D3 !important; } #test .gt_sourcenotes { color: #333333 !important; background-color: #FFFFFF !important; border-bottom-style: none !important; border-bottom-width: 2px !important; border-bottom-color: #D3D3D3 !important; border-left-style: none !important; border-left-width: 2px !important; border-left-color: #D3D3D3 !important; border-right-style: none !important; border-right-width: 2px !important; border-right-color: #D3D3D3 !important; } #test .gt_sourcenote { font-size: 90% !important; padding-top: 4px !important; padding-bottom: 4px !important; padding-left: 5px !important; padding-right: 5px !important; text-align: left !important; } @@ -268,6 +271,7 @@ #test .gt_stub_row_group { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; vertical-align: top; } #test .gt_row_group_first td { border-top-width: 2px; } #test .gt_row_group_first th { border-top-width: 2px; } + #test .gt_striped { background-color: rgba(128,128,128,0.05); } #test .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; } #test .gt_sourcenotes { color: #333333; background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; } #test .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; } @@ -343,6 +347,7 @@ #test .gt_stub_row_group { color: #333333 !important; background-color: #FFFFFF !important; font-size: 100% !important; font-weight: initial !important; text-transform: inherit !important; border-right-style: solid !important; border-right-width: 2px !important; border-right-color: #D3D3D3 !important; padding-left: 5px !important; padding-right: 5px !important; vertical-align: top !important; } #test .gt_row_group_first td { border-top-width: 2px !important; } #test .gt_row_group_first th { border-top-width: 2px !important; } + #test .gt_striped { background-color: rgba(128,128,128,0.05) !important; } #test .gt_table_body { border-top-style: solid !important; border-top-width: 2px !important; border-top-color: #D3D3D3 !important; border-bottom-style: solid !important; border-bottom-width: 2px !important; border-bottom-color: #D3D3D3 !important; } #test .gt_sourcenotes { color: #333333 !important; background-color: #FFFFFF !important; border-bottom-style: none !important; border-bottom-width: 2px !important; border-bottom-color: #D3D3D3 !important; border-left-style: none !important; border-left-width: 2px !important; border-left-color: #D3D3D3 !important; border-right-style: none !important; border-right-width: 2px !important; border-right-color: #D3D3D3 !important; } #test .gt_sourcenote { font-size: 90% !important; padding-top: 4px !important; padding-bottom: 4px !important; padding-left: 5px !important; padding-right: 5px !important; text-align: left !important; } diff --git a/tests/__snapshots__/test_scss.ambr b/tests/__snapshots__/test_scss.ambr index 5b55bc81d..00b3549db 100644 --- a/tests/__snapshots__/test_scss.ambr +++ b/tests/__snapshots__/test_scss.ambr @@ -272,6 +272,10 @@ border-top-width: 2px; } + #abc .gt_striped { + background-color: rgba(128,128,128,0.05); + } + #abc .gt_table_body { border-top-style: solid; border-top-width: 2px; diff --git a/tests/test_options.py b/tests/test_options.py index 2ba4c5f57..1364849db 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -4,6 +4,7 @@ from great_tables._scss import compile_scss from great_tables._gt_data import default_fonts_list from great_tables._helpers import _intify_scaled_px +from great_tables._utils_render_html import create_body_component_h def test_options_overwrite(): @@ -387,6 +388,59 @@ def test_opt_table_font_raises(): assert "Either `font=` or `stack=` must be provided." in exc_info.value.args[0] +def test_opt_row_striping(): + + gt_tbl_0 = GT(exibble) + gt_tbl_1 = GT(exibble).opt_row_striping() + gt_tbl_2 = GT(exibble).opt_row_striping().opt_row_striping(row_striping=False) + + assert gt_tbl_0._options.row_striping_include_table_body.value == False + assert gt_tbl_1._options.row_striping_include_table_body.value == True + assert gt_tbl_2._options.row_striping_include_table_body.value == False + + +def test_tab_options_striping(): + + gt_tbl_tab_opts = GT(exibble).tab_options(row_striping_include_table_body=True) + gt_tbl_opt_stri = GT(exibble).opt_row_striping() + + assert gt_tbl_tab_opts._options.row_striping_include_table_body.value == True + assert gt_tbl_tab_opts._options.row_striping_include_stub.value == False + + assert gt_tbl_opt_stri._options.row_striping_include_table_body.value == True + assert gt_tbl_opt_stri._options.row_striping_include_stub.value == False + + +def test_tab_options_striping_body_snap(snapshot): + + gt_tbl = GT( + exibble[["row", "group", "char"]].head(4), rowname_col="row", groupname_col="group" + ).tab_options( + row_striping_include_table_body=True, + row_striping_background_color="lightblue", + ) + + built = gt_tbl._build_data("html") + body = create_body_component_h(built) + + assert snapshot == body + + +def test_tab_options_striping_stub_snap(snapshot): + + gt_tbl = GT( + exibble[["row", "group", "char"]].head(4), rowname_col="row", groupname_col="group" + ).tab_options( + row_striping_include_stub=True, + row_striping_background_color="lightblue", + ) + + built = gt_tbl._build_data("html") + body = create_body_component_h(built) + + assert snapshot == body + + @pytest.mark.parametrize("align", ["left", "center", "right"]) def test_opt_align_table_header(gt_tbl: GT, align: list[str]): tbl = gt_tbl.opt_align_table_header(align=align) From 3bafd96c87e3593f51e2344f3da4c01c4112913b Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 27 Sep 2024 12:13:37 -0400 Subject: [PATCH 085/150] feat!: add missing styling (including row striping) in `opt_stylize()` (#463) * Add `add_row_striping` arg to `opt_stylize()` * Add description of new argument in docs * Remove items from `omit_keys` * Add `row_striping_background_color` to StyleMapper * Remove `table_outline_color` from styles dict * Ensure row striping param is added to params dict * Add missing table border for styles 2, 4, and 5 * Add tests of `opt_stylize()` w/ and w/o striping * Add test to verify border props in all style vals --- great_tables/_options.py | 55 +- tests/__snapshots__/test_options.ambr | 1512 +++++++++++++++++++++++++ tests/test_options.py | 32 + 3 files changed, 1575 insertions(+), 24 deletions(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index 2d5f8c528..71757d533 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -1297,7 +1297,9 @@ def opt_table_font( return res -def opt_stylize(self: GTSelf, style: int = 1, color: str = "blue") -> GTSelf: +def opt_stylize( + self: GTSelf, style: int = 1, color: str = "blue", add_row_striping: bool = True +) -> GTSelf: """ Stylize your table with a colorful look. @@ -1320,6 +1322,8 @@ def opt_stylize(self: GTSelf, style: int = 1, color: str = "blue") -> GTSelf: color The color scheme of the table. The default value is `"blue"`. The valid values are `"blue"`, `"cyan"`, `"pink"`, `"green"`, `"red"`, and `"gray"`. + add_row_striping + An option to enable row striping in the table body for the style chosen. Returns ------- @@ -1377,14 +1381,11 @@ def opt_stylize(self: GTSelf, style: int = 1, color: str = "blue") -> GTSelf: # Omit keys that are not needed for the `tab_options()` method # TODO: the omitted keys are for future use when: - # (1) row striping is implemented - # (2) summary rows are implemented - # (3) grand summary rows are implemented + # (1) summary rows are implemented + # (2) grand summary rows are implemented omit_keys = { "summary_row_background_color", "grand_summary_row_background_color", - "row_striping_background_color", - "table_outline_color", } def dict_omit_keys(dict: dict[str, str], omit_keys: set[str]) -> dict[str, str]: @@ -1394,6 +1395,28 @@ def dict_omit_keys(dict: dict[str, str], omit_keys: set[str]) -> dict[str, str]: mapped_params = StyleMapper(**params).map_all() + # Add the `add_row_striping` parameter to the `mapped_params` dictionary + if add_row_striping: + mapped_params["row_striping_include_table_body"] = ["True"] + + if style in [2, 4, 5]: + # For styles 2, 4, and 5 we need to set the border colors and widths + + # Use a dictionary comprehension to generate the border parameters + directions = ["top", "bottom", "left", "right"] + attributes = ["color", "width", "style"] + + border_params: dict[str, str] = { + f"table_border_{d}_{a}": ( + "#D5D5D5" if a == "color" else "3px" if a == "width" else "solid" + ) + for d in directions + for a in attributes + } + + # Append the border parameters to the `mapped_params` dictionary + mapped_params.update(border_params) + # Apply the style parameters to the table using the `tab_options()` method res = tab_options(self=self, **mapped_params) @@ -1412,6 +1435,7 @@ class StyleMapper: data_hlines_color: str data_vlines_style: str data_vlines_color: str + row_striping_background_color: str mappings: ClassVar[dict[str, list[str]]] = { "table_hlines_color": ["table_border_top_color", "table_border_bottom_color"], @@ -1432,6 +1456,7 @@ class StyleMapper: "data_hlines_color": ["table_body_hlines_color"], "data_vlines_style": ["table_body_vlines_style"], "data_vlines_color": ["table_body_vlines_color"], + "row_striping_background_color": ["row_striping_background_color"], } def map_entry(self, name: str) -> dict[str, list[str]]: @@ -1549,7 +1574,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#D5D5D5", "grand_summary_row_background_color": "#929292", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "blue-2": { "table_hlines_color": "#D5D5D5", @@ -1565,7 +1589,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#89D3FE", "grand_summary_row_background_color": "#00A1FF", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "cyan-2": { "table_hlines_color": "#D5D5D5", @@ -1581,7 +1604,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#A5FEF2", "grand_summary_row_background_color": "#7FE9DB", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "pink-2": { "table_hlines_color": "#D5D5D5", @@ -1597,7 +1619,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#FFC6E3", "grand_summary_row_background_color": "#EF5FA7", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "green-2": { "table_hlines_color": "#D5D5D5", @@ -1613,7 +1634,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#CAFFAF", "grand_summary_row_background_color": "#89FD61", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "red-2": { "table_hlines_color": "#D5D5D5", @@ -1629,7 +1649,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#FFCCC7", "grand_summary_row_background_color": "#FF644E", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "gray-3": { "table_hlines_color": "#929292", @@ -1735,7 +1754,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#FFFFFF", "grand_summary_row_background_color": "#5F5F5F", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "blue-4": { "table_hlines_color": "#D5D5D5", @@ -1751,7 +1769,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#FFFFFF", "grand_summary_row_background_color": "#0076BA", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "cyan-4": { "table_hlines_color": "#D5D5D5", @@ -1767,7 +1784,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#FFFFFF", "grand_summary_row_background_color": "#01837B", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "pink-4": { "table_hlines_color": "#D5D5D5", @@ -1783,7 +1799,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#FFFFFF", "grand_summary_row_background_color": "#CB2A7B", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "green-4": { "table_hlines_color": "#D5D5D5", @@ -1799,7 +1814,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#FFFFFF", "grand_summary_row_background_color": "#038901", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "red-4": { "table_hlines_color": "#D5D5D5", @@ -1815,7 +1829,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#FFFFFF", "grand_summary_row_background_color": "#E4220C", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "gray-5": { "table_hlines_color": "#D5D5D5", @@ -1831,7 +1844,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#5F5F5F", "grand_summary_row_background_color": "#929292", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "blue-5": { "table_hlines_color": "#D5D5D5", @@ -1847,7 +1859,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#5F5F5F", "grand_summary_row_background_color": "#929292", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "cyan-5": { "table_hlines_color": "#D5D5D5", @@ -1863,7 +1874,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#5F5F5F", "grand_summary_row_background_color": "#929292", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "pink-5": { "table_hlines_color": "#D5D5D5", @@ -1879,7 +1889,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#5F5F5F", "grand_summary_row_background_color": "#929292", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "green-5": { "table_hlines_color": "#D5D5D5", @@ -1895,7 +1904,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#5F5F5F", "grand_summary_row_background_color": "#929292", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "red-5": { "table_hlines_color": "#D5D5D5", @@ -1911,7 +1919,6 @@ def map_all(self) -> dict[str, list[str]]: "summary_row_background_color": "#5F5F5F", "grand_summary_row_background_color": "#929292", "row_striping_background_color": "#F4F4F4", - "table_outline_color": "#D5D5D5", }, "gray-6": { "table_hlines_color": "#5F5F5F", diff --git a/tests/__snapshots__/test_options.ambr b/tests/__snapshots__/test_options.ambr index b7a5463e4..fd988cd91 100644 --- a/tests/__snapshots__/test_options.ambr +++ b/tests/__snapshots__/test_options.ambr @@ -1,4 +1,1516 @@ # serializer version: 1 +# name: test_opt_stylize + ''' + #abc table { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', 'Fira Sans', 'Droid Sans', Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + #abc thead, + tbody, + tfoot, + tr, + td, + th { + border-style: none; + } + + tr { + background-color: transparent; + } + + #abc p { + margin: 0; + padding: 0; + } + + #abc .gt_table { + display: table; + border-collapse: collapse; + line-height: normal; + margin-left: auto; + margin-right: auto; + color: #333333; + font-size: 16px; + font-weight: normal; + font-style: normal; + background-color: #FFFFFF; + width: auto; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #004D80; + border-right-style: none; + border-right-width: 2px; + border-right-color: #D3D3D3; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #004D80; + border-left-style: none; + border-left-width: 2px; + border-left-color: #D3D3D3; + } + + #abc .gt_caption { + padding-top: 4px; + padding-bottom: 4px; + } + + #abc .gt_title { + color: #333333; + font-size: 125%; + font-weight: initial; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 5px; + padding-right: 5px; + border-bottom-color: #FFFFFF; + border-bottom-width: 0; + } + + #abc .gt_subtitle { + color: #333333; + font-size: 85%; + font-weight: initial; + padding-top: 3px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + border-top-color: #FFFFFF; + border-top-width: 0; + } + + #abc .gt_heading { + background-color: #FFFFFF; + text-align: center; + border-bottom-color: #FFFFFF; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + } + + #abc .gt_bottom_border { + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + } + + #abc .gt_col_headings { + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + } + + #abc .gt_col_heading { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: normal; + text-transform: inherit; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + overflow-x: hidden; + } + + #abc .gt_column_spanner_outer { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: normal; + text-transform: inherit; + padding-top: 0; + padding-bottom: 0; + padding-left: 4px; + padding-right: 4px; + } + + #abc .gt_column_spanner_outer:first-child { + padding-left: 0; + } + + #abc .gt_column_spanner_outer:last-child { + padding-right: 0; + } + + #abc .gt_column_spanner { + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 5px; + overflow-x: hidden; + display: inline-block; + width: 100%; + } + + #abc .gt_spanner_row { + border-bottom-style: hidden; + } + + #abc .gt_group_heading { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 5px; + padding-right: 5px; + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + vertical-align: middle; + text-align: left; + } + + #abc .gt_empty_group_heading { + padding: 0.5px; + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + vertical-align: middle; + } + + #abc .gt_from_md> :first-child { + margin-top: 0; + } + + #abc .gt_from_md> :last-child { + margin-bottom: 0; + } + + #abc .gt_row { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 5px; + padding-right: 5px; + margin: 10px; + border-top-style: none; + border-top-width: 1px; + border-top-color: #89D3FE; + border-left-style: none; + border-left-width: 1px; + border-left-color: #89D3FE; + border-right-style: none; + border-right-width: 1px; + border-right-color: #89D3FE; + vertical-align: middle; + overflow-x: hidden; + } + + #abc .gt_stub { + color: #FFFFFF; + background-color: #0076BA; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-right-style: solid; + border-right-width: 2px; + border-right-color: #0076BA; + padding-left: 5px; + padding-right: 5px; + } + + #abc .gt_stub_row_group { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-right-style: solid; + border-right-width: 2px; + border-right-color: #D3D3D3; + padding-left: 5px; + padding-right: 5px; + vertical-align: top; + } + + #abc .gt_row_group_first td { + border-top-width: 2px; + } + + #abc .gt_row_group_first th { + border-top-width: 2px; + } + + #abc .gt_striped { + background-color: #F4F4F4; + } + + #abc .gt_table_body { + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + } + + #abc .gt_sourcenotes { + color: #333333; + background-color: #FFFFFF; + border-bottom-style: none; + border-bottom-width: 2px; + border-bottom-color: #D3D3D3; + border-left-style: none; + border-left-width: 2px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 2px; + border-right-color: #D3D3D3; + } + + #abc .gt_sourcenote { + font-size: 90%; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 5px; + padding-right: 5px; + text-align: left; + } + + #abc .gt_left { + text-align: left; + } + + #abc .gt_center { + text-align: center; + } + + #abc .gt_right { + text-align: right; + font-variant-numeric: tabular-nums; + } + + #abc .gt_font_normal { + font-weight: normal; + } + + #abc .gt_font_bold { + font-weight: bold; + } + + #abc .gt_font_italic { + font-style: italic; + } + + #abc .gt_super { + font-size: 65%; + } + + #abc .gt_footnote_marks { + font-size: 75%; + vertical-align: 0.4em; + position: initial; + } + + #abc .gt_asterisk { + font-size: 100%; + vertical-align: 0; + } + + ''' +# --- +# name: test_opt_stylize.1 + ''' + #abc table { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', 'Fira Sans', 'Droid Sans', Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + #abc thead, + tbody, + tfoot, + tr, + td, + th { + border-style: none; + } + + tr { + background-color: transparent; + } + + #abc p { + margin: 0; + padding: 0; + } + + #abc .gt_table { + display: table; + border-collapse: collapse; + line-height: normal; + margin-left: auto; + margin-right: auto; + color: #333333; + font-size: 16px; + font-weight: normal; + font-style: normal; + background-color: #FFFFFF; + width: auto; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #004D80; + border-right-style: none; + border-right-width: 2px; + border-right-color: #D3D3D3; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #004D80; + border-left-style: none; + border-left-width: 2px; + border-left-color: #D3D3D3; + } + + #abc .gt_caption { + padding-top: 4px; + padding-bottom: 4px; + } + + #abc .gt_title { + color: #333333; + font-size: 125%; + font-weight: initial; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 5px; + padding-right: 5px; + border-bottom-color: #FFFFFF; + border-bottom-width: 0; + } + + #abc .gt_subtitle { + color: #333333; + font-size: 85%; + font-weight: initial; + padding-top: 3px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + border-top-color: #FFFFFF; + border-top-width: 0; + } + + #abc .gt_heading { + background-color: #FFFFFF; + text-align: center; + border-bottom-color: #FFFFFF; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + } + + #abc .gt_bottom_border { + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + } + + #abc .gt_col_headings { + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + } + + #abc .gt_col_heading { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: normal; + text-transform: inherit; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + overflow-x: hidden; + } + + #abc .gt_column_spanner_outer { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: normal; + text-transform: inherit; + padding-top: 0; + padding-bottom: 0; + padding-left: 4px; + padding-right: 4px; + } + + #abc .gt_column_spanner_outer:first-child { + padding-left: 0; + } + + #abc .gt_column_spanner_outer:last-child { + padding-right: 0; + } + + #abc .gt_column_spanner { + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 5px; + overflow-x: hidden; + display: inline-block; + width: 100%; + } + + #abc .gt_spanner_row { + border-bottom-style: hidden; + } + + #abc .gt_group_heading { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 5px; + padding-right: 5px; + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + vertical-align: middle; + text-align: left; + } + + #abc .gt_empty_group_heading { + padding: 0.5px; + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + vertical-align: middle; + } + + #abc .gt_from_md> :first-child { + margin-top: 0; + } + + #abc .gt_from_md> :last-child { + margin-bottom: 0; + } + + #abc .gt_row { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 5px; + padding-right: 5px; + margin: 10px; + border-top-style: none; + border-top-width: 1px; + border-top-color: #89D3FE; + border-left-style: none; + border-left-width: 1px; + border-left-color: #89D3FE; + border-right-style: none; + border-right-width: 1px; + border-right-color: #89D3FE; + vertical-align: middle; + overflow-x: hidden; + } + + #abc .gt_stub { + color: #FFFFFF; + background-color: #0076BA; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-right-style: solid; + border-right-width: 2px; + border-right-color: #0076BA; + padding-left: 5px; + padding-right: 5px; + } + + #abc .gt_stub_row_group { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-right-style: solid; + border-right-width: 2px; + border-right-color: #D3D3D3; + padding-left: 5px; + padding-right: 5px; + vertical-align: top; + } + + #abc .gt_row_group_first td { + border-top-width: 2px; + } + + #abc .gt_row_group_first th { + border-top-width: 2px; + } + + #abc .gt_striped { + background-color: #F4F4F4; + } + + #abc .gt_table_body { + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + } + + #abc .gt_sourcenotes { + color: #333333; + background-color: #FFFFFF; + border-bottom-style: none; + border-bottom-width: 2px; + border-bottom-color: #D3D3D3; + border-left-style: none; + border-left-width: 2px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 2px; + border-right-color: #D3D3D3; + } + + #abc .gt_sourcenote { + font-size: 90%; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 5px; + padding-right: 5px; + text-align: left; + } + + #abc .gt_left { + text-align: left; + } + + #abc .gt_center { + text-align: center; + } + + #abc .gt_right { + text-align: right; + font-variant-numeric: tabular-nums; + } + + #abc .gt_font_normal { + font-weight: normal; + } + + #abc .gt_font_bold { + font-weight: bold; + } + + #abc .gt_font_italic { + font-style: italic; + } + + #abc .gt_super { + font-size: 65%; + } + + #abc .gt_footnote_marks { + font-size: 75%; + vertical-align: 0.4em; + position: initial; + } + + #abc .gt_asterisk { + font-size: 100%; + vertical-align: 0; + } + + ''' +# --- +# name: test_opt_stylize_default + ''' + #abc table { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', 'Fira Sans', 'Droid Sans', Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + #abc thead, + tbody, + tfoot, + tr, + td, + th { + border-style: none; + } + + tr { + background-color: transparent; + } + + #abc p { + margin: 0; + padding: 0; + } + + #abc .gt_table { + display: table; + border-collapse: collapse; + line-height: normal; + margin-left: auto; + margin-right: auto; + color: #333333; + font-size: 16px; + font-weight: normal; + font-style: normal; + background-color: #FFFFFF; + width: auto; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #004D80; + border-right-style: none; + border-right-width: 2px; + border-right-color: #D3D3D3; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #004D80; + border-left-style: none; + border-left-width: 2px; + border-left-color: #D3D3D3; + } + + #abc .gt_caption { + padding-top: 4px; + padding-bottom: 4px; + } + + #abc .gt_title { + color: #333333; + font-size: 125%; + font-weight: initial; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 5px; + padding-right: 5px; + border-bottom-color: #FFFFFF; + border-bottom-width: 0; + } + + #abc .gt_subtitle { + color: #333333; + font-size: 85%; + font-weight: initial; + padding-top: 3px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + border-top-color: #FFFFFF; + border-top-width: 0; + } + + #abc .gt_heading { + background-color: #FFFFFF; + text-align: center; + border-bottom-color: #FFFFFF; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + } + + #abc .gt_bottom_border { + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + } + + #abc .gt_col_headings { + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + } + + #abc .gt_col_heading { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: normal; + text-transform: inherit; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + overflow-x: hidden; + } + + #abc .gt_column_spanner_outer { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: normal; + text-transform: inherit; + padding-top: 0; + padding-bottom: 0; + padding-left: 4px; + padding-right: 4px; + } + + #abc .gt_column_spanner_outer:first-child { + padding-left: 0; + } + + #abc .gt_column_spanner_outer:last-child { + padding-right: 0; + } + + #abc .gt_column_spanner { + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 5px; + overflow-x: hidden; + display: inline-block; + width: 100%; + } + + #abc .gt_spanner_row { + border-bottom-style: hidden; + } + + #abc .gt_group_heading { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 5px; + padding-right: 5px; + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + vertical-align: middle; + text-align: left; + } + + #abc .gt_empty_group_heading { + padding: 0.5px; + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + vertical-align: middle; + } + + #abc .gt_from_md> :first-child { + margin-top: 0; + } + + #abc .gt_from_md> :last-child { + margin-bottom: 0; + } + + #abc .gt_row { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 5px; + padding-right: 5px; + margin: 10px; + border-top-style: none; + border-top-width: 1px; + border-top-color: #89D3FE; + border-left-style: none; + border-left-width: 1px; + border-left-color: #89D3FE; + border-right-style: none; + border-right-width: 1px; + border-right-color: #89D3FE; + vertical-align: middle; + overflow-x: hidden; + } + + #abc .gt_stub { + color: #FFFFFF; + background-color: #0076BA; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-right-style: solid; + border-right-width: 2px; + border-right-color: #0076BA; + padding-left: 5px; + padding-right: 5px; + } + + #abc .gt_stub_row_group { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-right-style: solid; + border-right-width: 2px; + border-right-color: #D3D3D3; + padding-left: 5px; + padding-right: 5px; + vertical-align: top; + } + + #abc .gt_row_group_first td { + border-top-width: 2px; + } + + #abc .gt_row_group_first th { + border-top-width: 2px; + } + + #abc .gt_striped { + background-color: #F4F4F4; + } + + #abc .gt_table_body { + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + } + + #abc .gt_sourcenotes { + color: #333333; + background-color: #FFFFFF; + border-bottom-style: none; + border-bottom-width: 2px; + border-bottom-color: #D3D3D3; + border-left-style: none; + border-left-width: 2px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 2px; + border-right-color: #D3D3D3; + } + + #abc .gt_sourcenote { + font-size: 90%; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 5px; + padding-right: 5px; + text-align: left; + } + + #abc .gt_left { + text-align: left; + } + + #abc .gt_center { + text-align: center; + } + + #abc .gt_right { + text-align: right; + font-variant-numeric: tabular-nums; + } + + #abc .gt_font_normal { + font-weight: normal; + } + + #abc .gt_font_bold { + font-weight: bold; + } + + #abc .gt_font_italic { + font-style: italic; + } + + #abc .gt_super { + font-size: 65%; + } + + #abc .gt_footnote_marks { + font-size: 75%; + vertical-align: 0.4em; + position: initial; + } + + #abc .gt_asterisk { + font-size: 100%; + vertical-align: 0; + } + + ''' +# --- +# name: test_opt_stylize_no_striping + ''' + #abc table { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', 'Fira Sans', 'Droid Sans', Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + #abc thead, + tbody, + tfoot, + tr, + td, + th { + border-style: none; + } + + tr { + background-color: transparent; + } + + #abc p { + margin: 0; + padding: 0; + } + + #abc .gt_table { + display: table; + border-collapse: collapse; + line-height: normal; + margin-left: auto; + margin-right: auto; + color: #333333; + font-size: 16px; + font-weight: normal; + font-style: normal; + background-color: #FFFFFF; + width: auto; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #004D80; + border-right-style: none; + border-right-width: 2px; + border-right-color: #D3D3D3; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #004D80; + border-left-style: none; + border-left-width: 2px; + border-left-color: #D3D3D3; + } + + #abc .gt_caption { + padding-top: 4px; + padding-bottom: 4px; + } + + #abc .gt_title { + color: #333333; + font-size: 125%; + font-weight: initial; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 5px; + padding-right: 5px; + border-bottom-color: #FFFFFF; + border-bottom-width: 0; + } + + #abc .gt_subtitle { + color: #333333; + font-size: 85%; + font-weight: initial; + padding-top: 3px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + border-top-color: #FFFFFF; + border-top-width: 0; + } + + #abc .gt_heading { + background-color: #FFFFFF; + text-align: center; + border-bottom-color: #FFFFFF; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + } + + #abc .gt_bottom_border { + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + } + + #abc .gt_col_headings { + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + } + + #abc .gt_col_heading { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: normal; + text-transform: inherit; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + overflow-x: hidden; + } + + #abc .gt_column_spanner_outer { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: normal; + text-transform: inherit; + padding-top: 0; + padding-bottom: 0; + padding-left: 4px; + padding-right: 4px; + } + + #abc .gt_column_spanner_outer:first-child { + padding-left: 0; + } + + #abc .gt_column_spanner_outer:last-child { + padding-right: 0; + } + + #abc .gt_column_spanner { + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 5px; + overflow-x: hidden; + display: inline-block; + width: 100%; + } + + #abc .gt_spanner_row { + border-bottom-style: hidden; + } + + #abc .gt_group_heading { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 5px; + padding-right: 5px; + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + border-left-style: none; + border-left-width: 1px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 1px; + border-right-color: #D3D3D3; + vertical-align: middle; + text-align: left; + } + + #abc .gt_empty_group_heading { + padding: 0.5px; + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + vertical-align: middle; + } + + #abc .gt_from_md> :first-child { + margin-top: 0; + } + + #abc .gt_from_md> :last-child { + margin-bottom: 0; + } + + #abc .gt_row { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 5px; + padding-right: 5px; + margin: 10px; + border-top-style: none; + border-top-width: 1px; + border-top-color: #89D3FE; + border-left-style: none; + border-left-width: 1px; + border-left-color: #89D3FE; + border-right-style: none; + border-right-width: 1px; + border-right-color: #89D3FE; + vertical-align: middle; + overflow-x: hidden; + } + + #abc .gt_stub { + color: #FFFFFF; + background-color: #0076BA; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-right-style: solid; + border-right-width: 2px; + border-right-color: #0076BA; + padding-left: 5px; + padding-right: 5px; + } + + #abc .gt_stub_row_group { + color: #333333; + background-color: #FFFFFF; + font-size: 100%; + font-weight: initial; + text-transform: inherit; + border-right-style: solid; + border-right-width: 2px; + border-right-color: #D3D3D3; + padding-left: 5px; + padding-right: 5px; + vertical-align: top; + } + + #abc .gt_row_group_first td { + border-top-width: 2px; + } + + #abc .gt_row_group_first th { + border-top-width: 2px; + } + + #abc .gt_striped { + background-color: #F4F4F4; + } + + #abc .gt_table_body { + border-top-style: solid; + border-top-width: 2px; + border-top-color: #0076BA; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #0076BA; + } + + #abc .gt_sourcenotes { + color: #333333; + background-color: #FFFFFF; + border-bottom-style: none; + border-bottom-width: 2px; + border-bottom-color: #D3D3D3; + border-left-style: none; + border-left-width: 2px; + border-left-color: #D3D3D3; + border-right-style: none; + border-right-width: 2px; + border-right-color: #D3D3D3; + } + + #abc .gt_sourcenote { + font-size: 90%; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 5px; + padding-right: 5px; + text-align: left; + } + + #abc .gt_left { + text-align: left; + } + + #abc .gt_center { + text-align: center; + } + + #abc .gt_right { + text-align: right; + font-variant-numeric: tabular-nums; + } + + #abc .gt_font_normal { + font-weight: normal; + } + + #abc .gt_font_bold { + font-weight: bold; + } + + #abc .gt_font_italic { + font-style: italic; + } + + #abc .gt_super { + font-size: 65%; + } + + #abc .gt_footnote_marks { + font-size: 75%; + vertical-align: 0.4em; + position: initial; + } + + #abc .gt_asterisk { + font-size: 100%; + vertical-align: 0; + } + + ''' +# --- +# name: test_opt_stylize_outline_present[1] + ''' + + border-top-style: solid; + border-top-width: 2px; + border-top-color: #004D80; + border-right-style: none; + border-right-width: 2px; + border-right-color: #D3D3D3; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #004D80; + border-left-style: none; + border-left-width: 2px; + border-left-color: #D3D3D3; + + ''' +# --- +# name: test_opt_stylize_outline_present[2] + ''' + + border-top-style: solid; + border-top-width: 3px; + border-top-color: #D5D5D5; + border-right-style: solid; + border-right-width: 3px; + border-right-color: #D5D5D5; + border-bottom-style: solid; + border-bottom-width: 3px; + border-bottom-color: #D5D5D5; + border-left-style: solid; + border-left-width: 3px; + border-left-color: #D5D5D5; + + ''' +# --- +# name: test_opt_stylize_outline_present[3] + ''' + + border-top-style: solid; + border-top-width: 2px; + border-top-color: #929292; + border-right-style: none; + border-right-width: 2px; + border-right-color: #D3D3D3; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #929292; + border-left-style: none; + border-left-width: 2px; + border-left-color: #D3D3D3; + + ''' +# --- +# name: test_opt_stylize_outline_present[4] + ''' + + border-top-style: solid; + border-top-width: 3px; + border-top-color: #D5D5D5; + border-right-style: solid; + border-right-width: 3px; + border-right-color: #D5D5D5; + border-bottom-style: solid; + border-bottom-width: 3px; + border-bottom-color: #D5D5D5; + border-left-style: solid; + border-left-width: 3px; + border-left-color: #D5D5D5; + + ''' +# --- +# name: test_opt_stylize_outline_present[5] + ''' + + border-top-style: solid; + border-top-width: 3px; + border-top-color: #D5D5D5; + border-right-style: solid; + border-right-width: 3px; + border-right-color: #D5D5D5; + border-bottom-style: solid; + border-bottom-width: 3px; + border-bottom-color: #D5D5D5; + border-left-style: solid; + border-left-width: 3px; + border-left-color: #D5D5D5; + + ''' +# --- +# name: test_opt_stylize_outline_present[6] + ''' + + border-top-style: solid; + border-top-width: 2px; + border-top-color: #5F5F5F; + border-right-style: none; + border-right-width: 2px; + border-right-color: #D3D3D3; + border-bottom-style: solid; + border-bottom-width: 2px; + border-bottom-color: #5F5F5F; + border-left-style: none; + border-left-width: 2px; + border-left-color: #D3D3D3; + + ''' +# --- # name: test_scss_default_generated ''' #abc table { diff --git a/tests/test_options.py b/tests/test_options.py index 1364849db..b5320b7f5 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,3 +1,5 @@ +import re + import pandas as pd import pytest from great_tables import GT, exibble, md, google_font @@ -441,6 +443,36 @@ def test_tab_options_striping_stub_snap(snapshot): assert snapshot == body +def test_opt_stylize_default(snapshot): + + gt_tbl = GT(exibble, rowname_col="row", groupname_col="group").opt_stylize() + + assert snapshot == compile_scss(gt_tbl, id="abc", compress=False) + + +def test_opt_stylize_no_striping(snapshot): + + gt_tbl = GT(exibble, rowname_col="row", groupname_col="group").opt_stylize( + add_row_striping=False + ) + + assert snapshot == compile_scss(gt_tbl, id="abc", compress=False) + + +@pytest.mark.parametrize("style", [1, 2, 3, 4, 5, 6]) +def test_opt_stylize_outline_present(style, snapshot): + + gt_tbl = GT(exibble, rowname_col="row", groupname_col="group").opt_stylize(style=style) + + css = compile_scss(gt_tbl, id="abc", compress=False) + + css_gt_table_cls = re.sub(r"^.*?#abc \.gt_table \{\n(.*?)\}.*$", r"\1", css, flags=re.DOTALL) + + css_gt_table_border = re.sub(r".*?width: auto;(.*)", r"\1", css_gt_table_cls, flags=re.DOTALL) + + assert snapshot == css_gt_table_border + + @pytest.mark.parametrize("align", ["left", "center", "right"]) def test_opt_align_table_header(gt_tbl: GT, align: list[str]): tbl = gt_tbl.opt_align_table_header(align=align) From 8865f623d6ca92d3e2ac136173f6c78407c38500 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 27 Sep 2024 12:35:20 -0400 Subject: [PATCH 086/150] Add WIP post --- docs/blog/introduction-0.12.0/index.qmd | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/blog/introduction-0.12.0/index.qmd diff --git a/docs/blog/introduction-0.12.0/index.qmd b/docs/blog/introduction-0.12.0/index.qmd new file mode 100644 index 000000000..63fed358e --- /dev/null +++ b/docs/blog/introduction-0.12.0/index.qmd @@ -0,0 +1,39 @@ +--- +title: "Great Tables `v0.12.0`: Styling all over the place (and so much more)" +html-table-processing: none +author: Rich Iannone +date: 2024-09-27 +freeze: true +jupyter: python3 +--- + +In Great Tables `0.12.0` we did something that is sure to please those who obsess over styling, and this is the ability to style virtually any part (i.e., *location*) of a table. Also, the use of *Google Fonts* is now integrated into the package (with lots of font choices there). Lastly, we incorporated row striping as an option so that you can get that zebra stripe look in your table. In this post, we'll present a few examples in the following big features: + +- using `tab_style()` in more `locations=` +- putting some Google Fonts into a table with `tab_style()`/`opt_table_font()` + `google_font()` +- adding row stripes with `tab_options()` or `opt_row_striping()` + +### Styles all over the table with an enhanced `loc` module + + + +```{python} +from great_tables import GT, md +from great_tables.data import illness +import polars as pl + + +``` + + +### Using fonts from Google Fonts + + + +### Striping rows in your table + + + +### Wrapping up + +We got a lot of feedback on how limiting table styling was so we're happy that enhanced styling is now released with `v0.12.0`. If you're new to the wide world of table styling, check out the [*Get Started* guide on styling the table](https://posit-dev.github.io/great-tables/get-started/basic-styling.html) for primer on the subject. As ever, please let us know through [GitHub Issues](https://github.com/posit-dev/great-tables/issues) whether you ran into problems with any feature (new or old), or, if you have suggestions for further improvement! From 3ba7818f10158a876c754071d85314c01b80783d Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 12:42:33 -0400 Subject: [PATCH 087/150] feat: implement loc.headers --- great_tables/_utils_render_html.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index dbba4d0f1..3c31ca0ea 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -64,9 +64,8 @@ def create_heading_component_h(data: GTData) -> str: styles_header = [x for x in data._styles if _is_loc(x.locname, loc.LocHeader)] styles_title = [x for x in data._styles if _is_loc(x.locname, loc.LocTitle)] styles_subtitle = [x for x in data._styles if _is_loc(x.locname, loc.LocSubTitle)] - header_style = _flatten_styles(styles_header, wrap=True) if styles_header else "" - title_style = _flatten_styles(styles_title, wrap=True) if styles_title else "" - subtitle_style = _flatten_styles(styles_subtitle, wrap=True) if styles_subtitle else "" + title_style = _flatten_styles(styles_header + styles_title, wrap=True) + subtitle_style = _flatten_styles(styles_header + styles_subtitle, wrap=True) # Get the effective number of columns, which is number of columns # that will finally be rendered accounting for the stub layout From 90095a4ed46c207dff69c051f600ca63a19b263f Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 13:32:54 -0400 Subject: [PATCH 088/150] feat: implement LocFooter styles --- great_tables/_utils_render_html.py | 3 ++- tests/test_utils_render_html.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 83d2660a1..ac50d1de7 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -556,6 +556,7 @@ def create_source_notes_component_h(data: GTData) -> str: source_notes = data._source_notes # Filter list of StyleInfo to only those that apply to the source notes + styles_footer = [x for x in data._styles if _is_loc(x.locname, loc.LocFooter)] styles_source_notes = [x for x in data._styles if _is_loc(x.locname, loc.LocSourceNotes)] # If there are no source notes, then return an empty string @@ -579,10 +580,10 @@ def create_source_notes_component_h(data: GTData) -> str: source_notes_tr: list[str] = [] + _styles = _flatten_styles(styles_footer + styles_source_notes, wrap=True) for note in source_notes: note_str = _process_text(note) - _styles = _flatten_styles(styles_source_notes, wrap=True) source_notes_tr.append( f""" diff --git a/tests/test_utils_render_html.py b/tests/test_utils_render_html.py index e4a9e2bc7..04a6ed7f3 100644 --- a/tests/test_utils_render_html.py +++ b/tests/test_utils_render_html.py @@ -227,9 +227,9 @@ def test_loc_kitchen_sink(snapshot): .tab_style(style.css("SUBTITLE"), loc.subtitle()) .tab_style(style.css("TITLE"), loc.title()) # Footer ----------- + .tab_style(style.css("FOOTER"), loc.footer()) .tab_style(style.css("SOURCE_NOTES"), loc.source_notes()) # .tab_style(style.css("AAA"), loc.footnotes()) - # .tab_style(style.css("AAA"), loc.footer()) # Stub -------------- .tab_style(style.css("GROUP_LABEL"), loc.row_group_label()) .tab_style(style.css("ROW_LABEL"), loc.row_label(rows=[0])) From 9b746e9f7f8a639434693f2df07acc62b85bf8e4 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 13:33:08 -0400 Subject: [PATCH 089/150] tests: update snapshots --- tests/__snapshots__/test_utils_render_html.ambr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/__snapshots__/test_utils_render_html.ambr b/tests/__snapshots__/test_utils_render_html.ambr index 10dcd1515..9d37303e5 100644 --- a/tests/__snapshots__/test_utils_render_html.ambr +++ b/tests/__snapshots__/test_utils_render_html.ambr @@ -41,10 +41,10 @@ - title + title - subtitle + subtitle stubhead @@ -72,7 +72,7 @@ - yo + yo From 805c15507310632ad668f7dfb98839ee60d92953 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 13:50:09 -0400 Subject: [PATCH 090/150] fix: extra space in body html tag --- great_tables/_utils_render_html.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index ac50d1de7..d24bd3de6 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -513,8 +513,8 @@ def create_body_component_h(data: GTData) -> str: el_name = "th" classes = ["gt_row", "gt_left", "gt_stub"] - - _rowname_styles = [x for x in styles_row_label if x.rownum == i] + + _rowname_styles = styles_stub + [x for x in styles_row_label if x.rownum == i] if table_stub_striped and odd_i_row: classes.append("gt_striped") @@ -524,7 +524,7 @@ def create_body_component_h(data: GTData) -> str: el_name = "td" classes = ["gt_row", f"gt_{cell_alignment}"] - + _rowname_styles = [] if table_body_striped and odd_i_row: @@ -533,12 +533,12 @@ def create_body_component_h(data: GTData) -> str: # Ensure that `classes` becomes a space-separated string classes = " ".join(classes) cell_styles = _flatten_styles( - styles_stub + _body_styles + _rowname_styles, + _body_styles + _rowname_styles, wrap=True, ) body_cells.append( - f""" <{el_name} {cell_styles}class="{classes}">{cell_str}""" + f""" <{el_name}{cell_styles} class="{classes}">{cell_str}""" ) prev_group_info = group_info From 9e623e08ef5439f4df3914849fada1a22a6bca1d Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 13:50:40 -0400 Subject: [PATCH 091/150] refactor: remove groups attr from Loc classes --- great_tables/_locations.py | 54 ++++++-------------------------------- 1 file changed, 8 insertions(+), 46 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 924395709..9ea6f289b 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -57,48 +57,39 @@ class CellPos: class Loc: """A location.""" - groups: ClassVar[LocName] - @dataclass class LocHeader(Loc): """A location for targeting the table title and subtitle.""" - groups: ClassVar[LocHeaderName] = "header" - @dataclass class LocTitle(Loc): - groups: ClassVar[Literal["title"]] = "title" + """A location for targeting the title.""" @dataclass class LocSubTitle(Loc): - groups: ClassVar[Literal["subtitle"]] = "subtitle" + """A location for targeting the subtitle.""" @dataclass class LocStubhead(Loc): """A location for targeting the table stubhead and stubhead label.""" - groups: ClassVar[LocStubheadName] = "stubhead" - @dataclass class LocStubheadLabel(Loc): - groups: ClassVar[Literal["stubhead_label"]] = "stubhead_label" + """A location for targetting the stubhead.""" @dataclass class LocColumnLabels(Loc): """A location for column spanners and column labels.""" - groups: ClassVar[LocColumnLabelsName] = "column_labels" - @dataclass class LocColumnLabel(Loc): - groups: ClassVar[Literal["column_label"]] = "column_label" columns: SelectExpr = None @@ -106,7 +97,6 @@ class LocColumnLabel(Loc): class LocSpannerLabel(Loc): """A location for column spanners.""" - groups: ClassVar[Literal["spanner_label"]] = "spanner_label" ids: SelectExpr = None @@ -114,25 +104,21 @@ class LocSpannerLabel(Loc): class LocStub(Loc): """A location for targeting the table stub, row group labels, summary labels, and body.""" - groups: ClassVar[Literal["stub"]] = "stub" rows: RowSelectExpr = None @dataclass class LocRowGroupLabel(Loc): - groups: ClassVar[Literal["row_group_label"]] = "row_group_label" rows: RowSelectExpr = None @dataclass class LocRowLabel(Loc): - groups: ClassVar[Literal["row_label"]] = "row_label" rows: RowSelectExpr = None @dataclass class LocSummaryLabel(Loc): - groups: ClassVar[Literal["summary_label"]] = "summary_label" rows: RowSelectExpr = None @@ -163,7 +149,6 @@ class LocBody(Loc): ------ See [`GT.tab_style()`](`great_tables.GT.tab_style`). """ - groups: ClassVar[LocBodyName] = "data" columns: SelectExpr = None rows: RowSelectExpr = None @@ -172,27 +157,23 @@ class LocBody(Loc): @dataclass class LocSummary(Loc): # TODO: these can be tidyselectors - groups: ClassVar[LocName] = "summary" columns: SelectExpr = None rows: RowSelectExpr = None @dataclass class LocFooter(Loc): - groups: ClassVar[LocFooterName] = "footer" + """A location for targeting the footer.""" @dataclass class LocFootnotes(Loc): - groups: ClassVar[Literal["footnotes"]] = "footnotes" + """A location for targeting footnotes.""" @dataclass class LocSourceNotes(Loc): - # This dataclass in R has a `groups` field, which is a literal value. - # In python, we can use an isinstance check to determine we're seeing an - # instance of this class - groups: ClassVar[Literal["source_notes"]] = "source_notes" + """A location for targeting source notes.""" # Utils ================================================================================ @@ -480,16 +461,7 @@ def _(loc: LocHeader, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) - # set ---- - if loc.groups == "header": - info = StyleInfo(locname=loc, locnum=1, styles=style) - elif loc.groups == "title": - info = StyleInfo(locname=loc, locnum=2, styles=style) - elif loc.groups == "subtitle": - info = StyleInfo(locname=loc, locnum=3, styles=style) - else: - raise ValueError(f"Unknown title group: {loc.groups}") - return data._replace(_styles=data._styles + [info]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=3, styles=style)]) @set_style.register @@ -678,14 +650,4 @@ def _(loc: None, data: GTData, footnote: str, placement: PlacementOptions) -> GT @set_footnote.register def _(loc: LocTitle, data: GTData, footnote: str, placement: PlacementOptions) -> GTData: - # TODO: note that footnote here is annotated as a string, but I think that in R it - # can be a list of strings. - place = FootnotePlacement[placement] - if loc.groups == "title": - info = FootnoteInfo(locname=loc, locnum=1, footnotes=[footnote], placement=place) - elif loc.groups == "subtitle": - info = FootnoteInfo(locname=loc, locnum=2, footnotes=[footnote], placement=place) - else: - raise ValueError(f"Unknown title group: {loc.groups}") - - return data._replace(_footnotes=data._footnotes + [info]) + raise NotImplementedError() From 1a2e9d43dff13374beec130a7bf3c8c2386ebc6f Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 13:59:01 -0400 Subject: [PATCH 092/150] refactor: remove StyleInfo.locnum attr, .loc always a Loc class --- great_tables/_gt_data.py | 44 +++----------------------------------- great_tables/_locations.py | 44 +++++++++++++++----------------------- 2 files changed, 20 insertions(+), 68 deletions(-) diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index cc6c0499e..697c2404b 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -28,6 +28,7 @@ if TYPE_CHECKING: from ._helpers import Md, Html, UnitStr, Text + from ._locations import Loc T = TypeVar("T") @@ -850,47 +851,9 @@ class FootnotePlacement(Enum): auto = auto() -LocHeaderName = Literal[ - "header", - "title", - "subtitle", -] -LocStubheadName = Literal[ - "stubhead", - "stubhead_label", -] -LocColumnLabelsName = Literal[ - "column_labels", - "spanner_label", - "column_label", -] -LocStubName = Literal[ - "stub", - "row_group_label", - "row_label", - "summary_label", -] -LocBodyName = Literal["body", "cell", "summary"] -LocFooterName = Literal[ - "footer", - "footnotes", - "source_notes", -] -LocUnknownName = Literal["none",] -LocName = Union[ - LocHeaderName, - LocStubheadName, - LocColumnLabelsName, - LocStubName, - LocBodyName, - LocFooterName, - LocUnknownName, -] - - @dataclass(frozen=True) class FootnoteInfo: - locname: LocName | None = None + locname: Loc | None = None grpname: str | None = None colname: str | None = None locnum: int | None = None @@ -907,8 +870,7 @@ class FootnoteInfo: @dataclass(frozen=True) class StyleInfo: - locname: LocName - locnum: int + locname: Loc grpname: str | None = None colname: str | None = None rownum: int | None = None diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 9ea6f289b..9fa5b724a 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -17,14 +17,6 @@ GTData, Spanners, StyleInfo, - LocName, - LocHeaderName, - LocStubheadName, - LocColumnLabelsName, - LocStubName, - LocBodyName, - LocFooterName, - LocUnknownName, ) from ._styles import CellStyle from ._tbl_data import PlDataFrame, PlExpr, eval_select, eval_transform @@ -461,7 +453,7 @@ def _(loc: LocHeader, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=3, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @set_style.register @@ -469,7 +461,7 @@ def _(loc: LocTitle, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @set_style.register @@ -477,7 +469,7 @@ def _(loc: LocSubTitle, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @set_style.register @@ -485,7 +477,7 @@ def _(loc: LocStubhead, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @set_style.register @@ -493,7 +485,7 @@ def _(loc: LocStubheadLabel, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @set_style.register @@ -501,7 +493,7 @@ def _(loc: LocColumnLabels, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @set_style.register @@ -515,7 +507,6 @@ def _(loc: LocColumnLabel, data: GTData, style: list[CellStyle]) -> GTData: for col_pos in positions: crnt_info = StyleInfo( locname=loc, - locnum=2, colname=col_pos.colname, rownum=col_pos.row, styles=styles, @@ -533,8 +524,7 @@ def _(loc: LocSpannerLabel, data: GTData, style: list[CellStyle]) -> GTData: new_loc = resolve(loc, data._spanners) return data._replace( - _styles=data._styles - + [StyleInfo(locname=new_loc, locnum=1, grpname=new_loc.ids, styles=style)] + _styles=data._styles + [StyleInfo(locname=new_loc, grpname=new_loc.ids, styles=style)] ) @@ -543,7 +533,7 @@ def _(loc: LocStub, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @set_style.register @@ -554,7 +544,7 @@ def _(loc: LocRowGroupLabel, data: GTData, style: list[CellStyle]) -> GTData: row_groups = resolve(loc, data) return data._replace( - _styles=data._styles + [StyleInfo(locname=loc, locnum=1, grpname=row_groups, styles=style)] + _styles=data._styles + [StyleInfo(locname=loc, grpname=row_groups, styles=style)] ) @@ -566,7 +556,7 @@ def _(loc: LocRowLabel, data: GTData, style: list[CellStyle]) -> GTData: # TODO resolve cells = resolve(loc, data) - new_styles = [StyleInfo(locname=loc, locnum=1, rownum=rownum, styles=style) for rownum in cells] + new_styles = [StyleInfo(locname=loc, rownum=rownum, styles=style) for rownum in cells] return data._replace(_styles=data._styles + new_styles) @@ -576,7 +566,7 @@ def _(loc: LocSummaryLabel, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @set_style.register @@ -590,7 +580,7 @@ def _(loc: LocBody, data: GTData, style: list[CellStyle]) -> GTData: for col_pos in positions: row_styles = [entry._from_row(data._tbl_data, col_pos.row) for entry in style_ready] crnt_info = StyleInfo( - locname=loc, locnum=5, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles + locname=loc, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles ) all_info.append(crnt_info) @@ -602,7 +592,7 @@ def _(loc: LocSummary, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @set_style.register @@ -610,7 +600,7 @@ def _(loc: LocFooter, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @set_style.register @@ -619,7 +609,7 @@ def _(loc: LocFootnotes, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @set_style.register @@ -628,7 +618,7 @@ def _(loc: LocSourceNotes, data: GTData, style: list[CellStyle]) -> GTData: for entry in style: entry._raise_if_requires_data(loc) # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, locnum=1, styles=style)]) + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) # Set footnote generic ================================================================= @@ -643,7 +633,7 @@ def set_footnote(loc: Loc, data: GTData, footnote: str, placement: PlacementOpti @set_footnote.register(type(None)) def _(loc: None, data: GTData, footnote: str, placement: PlacementOptions) -> GTData: place = FootnotePlacement[placement] - info = FootnoteInfo(locname="none", locnum=0, footnotes=[footnote], placement=place) + info = FootnoteInfo(locname="none", footnotes=[footnote], placement=place) return data._replace(_footnotes=data._footnotes + [info]) From e2bea6a683627fb5e10d584ec11d5276899d6c12 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 14:05:10 -0400 Subject: [PATCH 093/150] refactor: remove redundant or unimplemented set_style concretes --- great_tables/_locations.py | 117 ++++++++----------------------------- 1 file changed, 24 insertions(+), 93 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 9fa5b724a..74c4a8225 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -447,52 +447,34 @@ def set_style(loc: Loc, data: GTData, style: list[str]) -> GTData: raise NotImplementedError(f"Unsupported location type: {type(loc)}") -@set_style.register -def _(loc: LocHeader, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - - -@set_style.register -def _(loc: LocTitle, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - - -@set_style.register -def _(loc: LocSubTitle, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - - -@set_style.register -def _(loc: LocStubhead, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - - -@set_style.register -def _(loc: LocStubheadLabel, data: GTData, style: list[CellStyle]) -> GTData: +@set_style.register(LocHeader) +@set_style.register(LocTitle) +@set_style.register(LocSubTitle) +@set_style.register(LocStubhead) +@set_style.register(LocStubheadLabel) +@set_style.register(LocColumnLabels) +@set_style.register(LocStub) +@set_style.register(LocFooter) +@set_style.register(LocSourceNotes) +def _( + loc: ( + LocHeader + | LocTitle + | LocSubTitle + | LocStubhead + | LocStubheadLabel + | LocColumnLabels + | LocStub + | LocFooter + | LocSourceNotes + ), + data: GTData, + style: list[CellStyle], +) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - -@set_style.register -def _(loc: LocColumnLabels, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) @@ -528,14 +510,6 @@ def _(loc: LocSpannerLabel, data: GTData, style: list[CellStyle]) -> GTData: ) -@set_style.register -def _(loc: LocStub, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - - @set_style.register def _(loc: LocRowGroupLabel, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- @@ -560,15 +534,6 @@ def _(loc: LocRowLabel, data: GTData, style: list[CellStyle]) -> GTData: return data._replace(_styles=data._styles + new_styles) -@set_style.register -def _(loc: LocSummaryLabel, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - - @set_style.register def _(loc: LocBody, data: GTData, style: list[CellStyle]) -> GTData: positions: list[CellPos] = resolve(loc, data) @@ -587,40 +552,6 @@ def _(loc: LocBody, data: GTData, style: list[CellStyle]) -> GTData: return data._replace(_styles=data._styles + all_info) -@set_style.register -def _(loc: LocSummary, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - - -@set_style.register -def _(loc: LocFooter, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - - -@set_style.register -def _(loc: LocFootnotes, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - - -@set_style.register -def _(loc: LocSourceNotes, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - # TODO resolve - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - - # Set footnote generic ================================================================= From cbc39bde1133896aa8baf2fe2a2fcf7a6604b6d0 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 14:20:58 -0400 Subject: [PATCH 094/150] refactor!: rename or remove locations based on Rich feedback --- great_tables/_locations.py | 31 ++++++++++++------------------ great_tables/_utils_render_html.py | 12 +++++------- great_tables/loc.py | 10 ++++------ tests/test_locations.py | 8 ++++---- tests/test_utils_render_html.py | 12 ++++++------ 5 files changed, 31 insertions(+), 42 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 74c4a8225..7f855b841 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -76,17 +76,17 @@ class LocStubheadLabel(Loc): @dataclass -class LocColumnLabels(Loc): +class LocColumnHeader(Loc): """A location for column spanners and column labels.""" @dataclass -class LocColumnLabel(Loc): +class LocColumnLabels(Loc): columns: SelectExpr = None @dataclass -class LocSpannerLabel(Loc): +class LocSpannerLabels(Loc): """A location for column spanners.""" ids: SelectExpr = None @@ -104,11 +104,6 @@ class LocRowGroupLabel(Loc): rows: RowSelectExpr = None -@dataclass -class LocRowLabel(Loc): - rows: RowSelectExpr = None - - @dataclass class LocSummaryLabel(Loc): rows: RowSelectExpr = None @@ -371,7 +366,7 @@ def resolve(loc: Loc, *args: Any, **kwargs: Any) -> Loc | list[CellPos]: @resolve.register -def _(loc: LocSpannerLabel, spanners: Spanners) -> LocSpannerLabel: +def _(loc: LocSpannerLabels, spanners: Spanners) -> LocSpannerLabels: # unique labels (with order preserved) spanner_ids = [span.spanner_id for span in spanners] @@ -379,11 +374,11 @@ def _(loc: LocSpannerLabel, spanners: Spanners) -> LocSpannerLabel: resolved_spanners = [spanner_ids[idx] for idx in resolved_spanners_idx] # Create a list object - return LocSpannerLabel(ids=resolved_spanners) + return LocSpannerLabels(ids=resolved_spanners) @resolve.register -def _(loc: LocColumnLabel, data: GTData) -> list[CellPos]: +def _(loc: LocColumnLabels, data: GTData) -> list[CellPos]: cols = resolve_cols_i(data=data, expr=loc.columns) cell_pos = [CellPos(col[1], 0, colname=col[0]) for col in cols] return cell_pos @@ -398,7 +393,7 @@ def _(loc: LocRowGroupLabel, data: GTData) -> set[int]: @resolve.register -def _(loc: LocRowLabel, data: GTData) -> set[int]: +def _(loc: LocStub, data: GTData) -> set[int]: # TODO: what are the rules for matching row groups? rows = resolve_rows_i(data=data, expr=loc.rows) cell_pos = set(row[1] for row in rows) @@ -452,8 +447,7 @@ def set_style(loc: Loc, data: GTData, style: list[str]) -> GTData: @set_style.register(LocSubTitle) @set_style.register(LocStubhead) @set_style.register(LocStubheadLabel) -@set_style.register(LocColumnLabels) -@set_style.register(LocStub) +@set_style.register(LocColumnHeader) @set_style.register(LocFooter) @set_style.register(LocSourceNotes) def _( @@ -463,8 +457,7 @@ def _( | LocSubTitle | LocStubhead | LocStubheadLabel - | LocColumnLabels - | LocStub + | LocColumnHeader | LocFooter | LocSourceNotes ), @@ -479,7 +472,7 @@ def _( @set_style.register -def _(loc: LocColumnLabel, data: GTData, style: list[CellStyle]) -> GTData: +def _(loc: LocColumnLabels, data: GTData, style: list[CellStyle]) -> GTData: positions: list[CellPos] = resolve(loc, data) # evaluate any column expressions in styles @@ -498,7 +491,7 @@ def _(loc: LocColumnLabel, data: GTData, style: list[CellStyle]) -> GTData: @set_style.register -def _(loc: LocSpannerLabel, data: GTData, style: list[CellStyle]) -> GTData: +def _(loc: LocSpannerLabels, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) @@ -523,7 +516,7 @@ def _(loc: LocRowGroupLabel, data: GTData, style: list[CellStyle]) -> GTData: @set_style.register -def _(loc: LocRowLabel, data: GTData, style: list[CellStyle]) -> GTData: +def _(loc: LocStub, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index d24bd3de6..447cdbb77 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -133,9 +133,9 @@ def create_columns_component_h(data: GTData) -> str: # Filter list of StyleInfo for the various stubhead and column labels components styles_stubhead = [x for x in data._styles if _is_loc(x.locname, loc.LocStubhead)] - styles_column_labels = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnLabels)] - styles_spanner_label = [x for x in data._styles if _is_loc(x.locname, loc.LocSpannerLabel)] - styles_column_label = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnLabel)] + styles_column_labels = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnHeader)] + styles_spanner_label = [x for x in data._styles if _is_loc(x.locname, loc.LocSpannerLabels)] + styles_column_label = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnLabels)] # If columns are present in the stub, then replace with a set stubhead label or nothing if len(stub_layout) > 0 and stubh is not None: @@ -420,11 +420,9 @@ def create_body_component_h(data: GTData) -> str: tbl_data = replace_null_frame(data._body.body, _str_orig_data) # Filter list of StyleInfo to only those that apply to the stub - styles_stub = [x for x in data._styles if _is_loc(x.locname, loc.LocStub)] styles_row_group_label = [x for x in data._styles if _is_loc(x.locname, loc.LocRowGroupLabel)] - styles_row_label = [x for x in data._styles if _is_loc(x.locname, loc.LocRowLabel)] + styles_row_label = [x for x in data._styles if _is_loc(x.locname, loc.LocStub)] styles_summary_label = [x for x in data._styles if _is_loc(x.locname, loc.LocSummaryLabel)] - stub_style = _flatten_styles(styles_stub, wrap=True) if styles_stub else "" # Filter list of StyleInfo to only those that apply to the body styles_cells = [x for x in data._styles if _is_loc(x.locname, loc.LocBody)] @@ -514,7 +512,7 @@ def create_body_component_h(data: GTData) -> str: classes = ["gt_row", "gt_left", "gt_stub"] - _rowname_styles = styles_stub + [x for x in styles_row_label if x.rownum == i] + _rowname_styles = [x for x in styles_row_label if x.rownum == i] if table_stub_striped and odd_i_row: classes.append("gt_striped") diff --git a/great_tables/loc.py b/great_tables/loc.py index 9fec1f4c1..fe849e2c9 100644 --- a/great_tables/loc.py +++ b/great_tables/loc.py @@ -10,14 +10,13 @@ LocStubhead as stubhead, # # Column Labels ---- + LocColumnHeader as column_header, + LocSpannerLabels as spanner_labels, LocColumnLabels as column_labels, - LocSpannerLabel as spanner_label, - LocColumnLabel as column_label, # # Stub ---- LocStub as stub, LocRowGroupLabel as row_group_label, - LocRowLabel as row_label, # TODO: remove for now LocSummaryLabel as summary_label, # @@ -38,12 +37,11 @@ "title", "subtitle", "stubhead", + "column_header", + "spanner_labels", "column_labels", - "spanner_label", - "column_label", "stub", "row_group_label", - "row_label", "summary_label", "body", "summary", diff --git a/tests/test_locations.py b/tests/test_locations.py index ce535bb88..cd10f7940 100644 --- a/tests/test_locations.py +++ b/tests/test_locations.py @@ -7,7 +7,7 @@ from great_tables._locations import ( CellPos, LocBody, - LocSpannerLabel, + LocSpannerLabels, LocTitle, resolve, resolve_cols_i, @@ -139,7 +139,7 @@ def test_resolve_loc_body(): @pytest.mark.xfail def test_resolve_loc_spanners_label_single(): spanners = Spanners.from_ids(["a", "b"]) - loc = LocSpannerLabel(ids="a") + loc = LocSpannerLabels(ids="a") new_loc = resolve(loc, spanners) @@ -158,7 +158,7 @@ def test_resolve_loc_spanners_label(expr): ids = ["a", "b", "c"] spanners = Spanners.from_ids(ids) - loc = LocSpannerLabel(ids=expr) + loc = LocSpannerLabels(ids=expr) new_loc = resolve(loc, spanners) @@ -170,7 +170,7 @@ def test_resolve_loc_spanner_label_error_missing(): ids = ["a", "b", "c"] spanners = Spanners.from_ids(ids) - loc = LocSpannerLabel(ids=["a", "d"]) + loc = LocSpannerLabels(ids=["a", "d"]) with pytest.raises(ValueError): resolve(loc, spanners) diff --git a/tests/test_utils_render_html.py b/tests/test_utils_render_html.py index 04a6ed7f3..549769358 100644 --- a/tests/test_utils_render_html.py +++ b/tests/test_utils_render_html.py @@ -195,10 +195,10 @@ def test_multiple_spanners_pads_for_stubhead_label(snapshot): # Location style rendering ------------------------------------------------------------------------- # these tests focus on location classes being correctly picked up -def test_loc_column_label(): +def test_loc_column_labels(): gt = GT(pl.DataFrame({"x": [1], "y": [2]})) - new_gt = gt.tab_style(style.fill("yellow"), loc.column_label(columns=["x"])) + new_gt = gt.tab_style(style.fill("yellow"), loc.column_labels(columns=["x"])) el = create_columns_component_h(new_gt._build_data("html")) assert el.name == "tr" @@ -219,9 +219,9 @@ def test_loc_kitchen_sink(snapshot): new_gt = ( gt.tab_style(style.css("BODY"), loc.body()) # Columns ----------- - .tab_style(style.css("COLUMN_LABEL"), loc.column_label(columns="num")) - .tab_style(style.css("COLUMN_LABELS"), loc.column_labels()) - .tab_style(style.css("SPANNER_LABEL"), loc.spanner_label(ids=["spanner"])) + .tab_style(style.css("COLUMN_LABEL"), loc.column_labels(columns="num")) + .tab_style(style.css("COLUMN_HEADER"), loc.column_header()) + .tab_style(style.css("SPANNER_LABEL"), loc.spanner_labels(ids=["spanner"])) # Header ----------- .tab_style(style.css("HEADER"), loc.header()) .tab_style(style.css("SUBTITLE"), loc.subtitle()) @@ -232,8 +232,8 @@ def test_loc_kitchen_sink(snapshot): # .tab_style(style.css("AAA"), loc.footnotes()) # Stub -------------- .tab_style(style.css("GROUP_LABEL"), loc.row_group_label()) - .tab_style(style.css("ROW_LABEL"), loc.row_label(rows=[0])) .tab_style(style.css("STUB"), loc.stub()) + .tab_style(style.css("ROW_LABEL"), loc.stub(rows=[0])) .tab_style(style.css("STUBHEAD"), loc.stubhead()) ) From e87ab10ef3650566e5273d7e0c3b3abcd7364bb3 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 14:21:12 -0400 Subject: [PATCH 095/150] tests: update snapshots --- tests/__snapshots__/test_utils_render_html.ambr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/__snapshots__/test_utils_render_html.ambr b/tests/__snapshots__/test_utils_render_html.ambr index 9d37303e5..77598d1a8 100644 --- a/tests/__snapshots__/test_utils_render_html.ambr +++ b/tests/__snapshots__/test_utils_render_html.ambr @@ -48,14 +48,14 @@ stubhead - num - + num + spanner - char - fctr + char + fctr From bdbe223d346417ede8c9994cfd2e1f56c4671a67 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 14:24:43 -0400 Subject: [PATCH 096/150] refactor!: rename loc.row_group_labels to row_groups --- great_tables/_locations.py | 6 +++--- great_tables/_utils_render_html.py | 2 +- great_tables/loc.py | 13 ++----------- tests/test_utils_render_html.py | 2 +- 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 7f855b841..3274c2450 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -100,7 +100,7 @@ class LocStub(Loc): @dataclass -class LocRowGroupLabel(Loc): +class LocRowGroups(Loc): rows: RowSelectExpr = None @@ -385,7 +385,7 @@ def _(loc: LocColumnLabels, data: GTData) -> list[CellPos]: @resolve.register -def _(loc: LocRowGroupLabel, data: GTData) -> set[int]: +def _(loc: LocRowGroups, data: GTData) -> set[int]: # TODO: what are the rules for matching row groups? # TODO: resolve_rows_i will match a list expr to row names (not group names) group_pos = set(pos for _, pos in resolve_rows_i(data, loc.rows)) @@ -504,7 +504,7 @@ def _(loc: LocSpannerLabels, data: GTData, style: list[CellStyle]) -> GTData: @set_style.register -def _(loc: LocRowGroupLabel, data: GTData, style: list[CellStyle]) -> GTData: +def _(loc: LocRowGroups, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 447cdbb77..4abd55ec7 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -420,7 +420,7 @@ def create_body_component_h(data: GTData) -> str: tbl_data = replace_null_frame(data._body.body, _str_orig_data) # Filter list of StyleInfo to only those that apply to the stub - styles_row_group_label = [x for x in data._styles if _is_loc(x.locname, loc.LocRowGroupLabel)] + styles_row_group_label = [x for x in data._styles if _is_loc(x.locname, loc.LocRowGroups)] styles_row_label = [x for x in data._styles if _is_loc(x.locname, loc.LocStub)] styles_summary_label = [x for x in data._styles if _is_loc(x.locname, loc.LocSummaryLabel)] diff --git a/great_tables/loc.py b/great_tables/loc.py index fe849e2c9..e463ab132 100644 --- a/great_tables/loc.py +++ b/great_tables/loc.py @@ -16,19 +16,13 @@ # # Stub ---- LocStub as stub, - LocRowGroupLabel as row_group_label, - # TODO: remove for now - LocSummaryLabel as summary_label, + LocRowGroups as row_groups, # # Body ---- LocBody as body, - # TODO: remove for now - LocSummary as summary, # # Footer ---- LocFooter as footer, - # TODO: remove for now - LocFootnotes as footnotes, LocSourceNotes as source_notes, ) @@ -41,11 +35,8 @@ "spanner_labels", "column_labels", "stub", - "row_group_label", - "summary_label", + "row_groups", "body", - "summary", "footer", - "footnotes", "source_notes", ) diff --git a/tests/test_utils_render_html.py b/tests/test_utils_render_html.py index 549769358..0d84ee314 100644 --- a/tests/test_utils_render_html.py +++ b/tests/test_utils_render_html.py @@ -231,7 +231,7 @@ def test_loc_kitchen_sink(snapshot): .tab_style(style.css("SOURCE_NOTES"), loc.source_notes()) # .tab_style(style.css("AAA"), loc.footnotes()) # Stub -------------- - .tab_style(style.css("GROUP_LABEL"), loc.row_group_label()) + .tab_style(style.css("GROUP_LABEL"), loc.row_groups()) .tab_style(style.css("STUB"), loc.stub()) .tab_style(style.css("ROW_LABEL"), loc.stub(rows=[0])) .tab_style(style.css("STUBHEAD"), loc.stubhead()) From 2f1e1f1278efe5c51a004ba65c99a14596edb732 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 27 Sep 2024 14:26:02 -0400 Subject: [PATCH 097/150] Update post --- docs/blog/introduction-0.12.0/index.qmd | 58 +++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/docs/blog/introduction-0.12.0/index.qmd b/docs/blog/introduction-0.12.0/index.qmd index 63fed358e..a0f0c7962 100644 --- a/docs/blog/introduction-0.12.0/index.qmd +++ b/docs/blog/introduction-0.12.0/index.qmd @@ -15,17 +15,69 @@ In Great Tables `0.12.0` we did something that is sure to please those who obses ### Styles all over the table with an enhanced `loc` module +Before `v0.12.0` we were able to style parts of the table with `tab_style()` but that was limited only to the body of the table by having only `loc.body()` being usable in `tab_style()`'s `locations=` argument. Now, we have quite a few locations that cover all locations in the table! Here's the complete set: +- `loc.body()` - the table body +- ... + + +Let's start with this small, yet unstyled table. It has a few locations included thanks to its use of several `tab_*()` methods. ```{python} -from great_tables import GT, md -from great_tables.data import illness -import polars as pl + +#| code-fold: true +#| code-summary: "Show the code" + +from great_tables import GT, exibble, style, loc + + +gt_tbl = ( + GT(exibble.head(), rowname_col="row", groupname_col="group") + .cols_hide(columns=["fctr", "date", "time"]) + .tab_header( + title="A small piece of the exibble dataset", + subtitle="Displaying the first five rows (of eight)", + ) + .tab_source_note( + source_note="This dataset is included in Great Tables." + ) +) + +gt_tbl +``` +```{python} +from great_tables import GT, exibble, style, loc + +( + gt_tbl + .tab_style( + style=style.fill(color="lightblue"), + locations=loc.body(columns="num", rows=[1, 2]), + ) + .tab_style( + style=[ + style.text(color="white", weight="bold"), + style.fill(color="olivedrab") + ], + locations=loc.body(columns="currency") + ) + #.tab_style( + # style=style.fill(color="aqua"), + # locations=[ + # loc.column_labels(columns=["num", "currency"]), + # loc.title(), + # loc.subtitle() + # ] + #) +) ``` + + + ### Using fonts from Google Fonts From aebb417c6160ec2bc6f9efe5afdebfccd3d2cd44 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 27 Sep 2024 14:45:58 -0400 Subject: [PATCH 098/150] Include `google_font()` helper fn in API reference (#464) --- docs/_quarto.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index d69673cc4..15a937b15 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -162,6 +162,7 @@ quartodoc: - md - html - from_column + - google_font - system_fonts - define_units - nanoplot_options From 367194dca4f89ed7dacb11bd6064b411815329ad Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 15:02:24 -0400 Subject: [PATCH 099/150] docs: get targeted styles working again --- docs/get-started/targeted-styles.qmd | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/get-started/targeted-styles.qmd b/docs/get-started/targeted-styles.qmd index 3a76d4ae0..9853f08ec 100644 --- a/docs/get-started/targeted-styles.qmd +++ b/docs/get-started/targeted-styles.qmd @@ -41,9 +41,9 @@ gt = ( gt.tab_style(style.fill(next(c)), loc.body()) # Columns ----------- # TODO: appears in browser, but not vs code - .tab_style(style.fill(next(c)), loc.column_label(columns="num")) - .tab_style(style.fill(next(c)), loc.column_labels()) - .tab_style(style.fill(next(c)), loc.spanner_label(ids=["spanner"])) + .tab_style(style.fill(next(c)), loc.column_labels(columns="num")) + .tab_style(style.fill(next(c)), loc.column_header()) + .tab_style(style.fill(next(c)), loc.spanner_labels(ids=["spanner"])) # Header ----------- .tab_style(style.fill(next(c)), loc.header()) .tab_style(style.fill(next(c)), loc.subtitle()) @@ -53,8 +53,8 @@ gt = ( # .tab_style(style.fill(next(c)), loc.footnotes()) # .tab_style(style.fill(next(c)), loc.footer()) # Stub -------------- - .tab_style(style.fill(next(c)), loc.row_group_label()) - .tab_style(style.fill(next(c)), loc.row_label(rows=1)) + .tab_style(style.fill(next(c)), loc.row_groups()) + .tab_style(style.fill(next(c)), loc.stub(rows=1)) .tab_style(style.fill(next(c)), loc.stub()) .tab_style(style.fill(next(c)), loc.stubhead()) ) @@ -71,9 +71,9 @@ gt.tab_style(style.fill(COLOR), loc.body()) ```{python} ( gt - .tab_style(style.fill(COLOR), loc.column_labels()) - .tab_style(style.fill("blue"), loc.column_label(columns="num")) - .tab_style(style.fill("red"), loc.spanner_label(ids=["spanner"])) + .tab_style(style.fill(COLOR), loc.column_header()) + .tab_style(style.fill("blue"), loc.column_labels(columns="num")) + .tab_style(style.fill("red"), loc.spanner_labels(ids=["spanner"])) ) ``` @@ -109,10 +109,10 @@ gt.tab_style(style.fill(COLOR), loc.body()) ```{python} ( gt.tab_style(style.fill(COLOR), loc.stub()) - .tab_style(style.fill("blue"), loc.row_group_label()) + .tab_style(style.fill("blue"), loc.row_groups()) .tab_style( style.borders(style="dashed", weight="3px", color="red"), - loc.row_label(rows=[1]), + loc.stub(rows=[1]), ) ) ``` From febbd701f821ae8c3d37fb8a0c45d9bb7d74b56c Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 27 Sep 2024 15:14:56 -0400 Subject: [PATCH 100/150] docs: flesh out targeted styles a bit --- docs/get-started/targeted-styles.qmd | 33 +++++++++++++++++----------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/get-started/targeted-styles.qmd b/docs/get-started/targeted-styles.qmd index 9853f08ec..a3394ec75 100644 --- a/docs/get-started/targeted-styles.qmd +++ b/docs/get-started/targeted-styles.qmd @@ -3,13 +3,21 @@ title: Targeted styles jupyter: python3 --- +In [Styling the Table Body](./basic-styling), we discussed styling table data with `.tab_style()`. +In this article we'll cover how the same method can be used to style many other parts of the table, like the header, specific spanner labels, the footer, and more. + +:::{.callout-warning} +This feature is currently a work in progress, and not yet released. Great Tables must be installed from github in order to try it. +::: + + ## Kitchen sink +Below is a big example that shows all possible `loc` specifiers being used. + ```{python} from great_tables import GT, exibble, loc, style -COLOR = "yellow" - # https://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12 brewer_colors = [ "#a6cee3", @@ -49,12 +57,11 @@ gt = ( .tab_style(style.fill(next(c)), loc.subtitle()) .tab_style(style.fill(next(c)), loc.title()) # Footer ----------- - .tab_style(style.fill(next(c)), loc.source_notes()) - # .tab_style(style.fill(next(c)), loc.footnotes()) - # .tab_style(style.fill(next(c)), loc.footer()) + .tab_style(style.borders(weight="3px"), loc.source_notes()) + .tab_style(style.fill(next(c)), loc.footer()) # Stub -------------- .tab_style(style.fill(next(c)), loc.row_groups()) - .tab_style(style.fill(next(c)), loc.stub(rows=1)) + .tab_style(style.borders(weight="3px"), loc.stub(rows=1)) .tab_style(style.fill(next(c)), loc.stub()) .tab_style(style.fill(next(c)), loc.stubhead()) ) @@ -63,7 +70,7 @@ gt = ( ## Body ```{python} -gt.tab_style(style.fill(COLOR), loc.body()) +gt.tab_style(style.fill("yellow"), loc.body()) ``` ## Column labels @@ -71,7 +78,7 @@ gt.tab_style(style.fill(COLOR), loc.body()) ```{python} ( gt - .tab_style(style.fill(COLOR), loc.column_header()) + .tab_style(style.fill("yellow"), loc.column_header()) .tab_style(style.fill("blue"), loc.column_labels(columns="num")) .tab_style(style.fill("red"), loc.spanner_labels(ids=["spanner"])) ) @@ -84,7 +91,7 @@ gt.tab_style(style.fill(COLOR), loc.body()) ```{python} ( - gt.tab_style(style.fill(COLOR), loc.header()) + gt.tab_style(style.fill("yellow"), loc.header()) .tab_style(style.fill("blue"), loc.title()) .tab_style(style.fill("red"), loc.subtitle()) ) @@ -95,10 +102,10 @@ gt.tab_style(style.fill(COLOR), loc.body()) ```{python} ( gt.tab_style( - style.fill(COLOR), + style.fill("yellow"), loc.source_notes(), ).tab_style( - style.fill(COLOR), + style.borders(weight="3px"), loc.footer(), ) ) @@ -108,7 +115,7 @@ gt.tab_style(style.fill(COLOR), loc.body()) ```{python} ( - gt.tab_style(style.fill(COLOR), loc.stub()) + gt.tab_style(style.fill("yellow"), loc.stub()) .tab_style(style.fill("blue"), loc.row_groups()) .tab_style( style.borders(style="dashed", weight="3px", color="red"), @@ -120,5 +127,5 @@ gt.tab_style(style.fill(COLOR), loc.body()) ## Stubhead ```{python} -gt.tab_style(style.fill(COLOR), loc.stubhead()) +gt.tab_style(style.fill("yellow"), loc.stubhead()) ``` From b2da03cee5a9d3f2e311069750ef8f1bc25dad84 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Fri, 27 Sep 2024 22:45:45 +0200 Subject: [PATCH 101/150] Allow for granular section restyling via convenience api (#341) * WIP allow for granular section restyling via convenience api * fix lint * working on tests * refactor: prepare to use loc classes directly, not strings * refactor: clean up last use of .locname * fix: support spanner label targeting * feat: add style.css for raw css * tests: add kitchen sink location snapshot test * tests: update snapshots * fix: title style tag needs space before * fix: resolve_rows_i can always handle None expr * feat: implement LocRowGroupLabel styles * feat: support LocRowLabel styles * feat: support LocSourceNotes styles * tests: add source note to styles test, update snapshot * tests: correctly target row of data in snapshot * refactor: remove LocStubheadLabel * docs: add targeted styles page to get-started * chore: remove print statement * feat: implement loc.headers * feat: implement LocFooter styles * tests: update snapshots * fix: extra space in body html tag * refactor: remove groups attr from Loc classes * refactor: remove StyleInfo.locnum attr, .loc always a Loc class * refactor: remove redundant or unimplemented set_style concretes * refactor!: rename or remove locations based on Rich feedback * tests: update snapshots * refactor!: rename loc.row_group_labels to row_groups * docs: get targeted styles working again * docs: flesh out targeted styles a bit --------- Co-authored-by: Michael Chow --- docs/_quarto.yml | 1 + docs/get-started/targeted-styles.qmd | 131 +++++++++ great_tables/_gt_data.py | 14 +- great_tables/_locations.py | 257 +++++++++++++----- great_tables/_modify_rows.py | 6 +- great_tables/_styles.py | 8 + great_tables/_utils_render_html.py | 243 +++++++++-------- great_tables/loc.py | 39 ++- great_tables/style.py | 3 +- .../__snapshots__/test_utils_render_html.ambr | 48 ++++ tests/test_gt_data.py | 3 +- tests/test_locations.py | 34 ++- tests/test_utils_render_html.py | 51 +++- 13 files changed, 627 insertions(+), 211 deletions(-) create mode 100644 docs/get-started/targeted-styles.qmd diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 15a937b15..55f19de95 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -46,6 +46,7 @@ website: - get-started/column-selection.qmd - get-started/row-selection.qmd - get-started/nanoplots.qmd + - get-started/targeted-styles.qmd format: html: diff --git a/docs/get-started/targeted-styles.qmd b/docs/get-started/targeted-styles.qmd new file mode 100644 index 000000000..a3394ec75 --- /dev/null +++ b/docs/get-started/targeted-styles.qmd @@ -0,0 +1,131 @@ +--- +title: Targeted styles +jupyter: python3 +--- + +In [Styling the Table Body](./basic-styling), we discussed styling table data with `.tab_style()`. +In this article we'll cover how the same method can be used to style many other parts of the table, like the header, specific spanner labels, the footer, and more. + +:::{.callout-warning} +This feature is currently a work in progress, and not yet released. Great Tables must be installed from github in order to try it. +::: + + +## Kitchen sink + +Below is a big example that shows all possible `loc` specifiers being used. + +```{python} +from great_tables import GT, exibble, loc, style + +# https://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12 +brewer_colors = [ + "#a6cee3", + "#1f78b4", + "#b2df8a", + "#33a02c", + "#fb9a99", + "#e31a1c", + "#fdbf6f", + "#ff7f00", + "#cab2d6", + "#6a3d9a", + "#ffff99", + "#b15928", +] + +c = iter(brewer_colors) + +gt = ( + GT(exibble.loc[[0, 1, 4], ["num", "char", "fctr", "row", "group"]]) + .tab_header("title", "subtitle") + .tab_stub(rowname_col="row", groupname_col="group") + .tab_source_note("yo") + .tab_spanner("spanner", ["char", "fctr"]) + .tab_stubhead("stubhead") +) + +( + gt.tab_style(style.fill(next(c)), loc.body()) + # Columns ----------- + # TODO: appears in browser, but not vs code + .tab_style(style.fill(next(c)), loc.column_labels(columns="num")) + .tab_style(style.fill(next(c)), loc.column_header()) + .tab_style(style.fill(next(c)), loc.spanner_labels(ids=["spanner"])) + # Header ----------- + .tab_style(style.fill(next(c)), loc.header()) + .tab_style(style.fill(next(c)), loc.subtitle()) + .tab_style(style.fill(next(c)), loc.title()) + # Footer ----------- + .tab_style(style.borders(weight="3px"), loc.source_notes()) + .tab_style(style.fill(next(c)), loc.footer()) + # Stub -------------- + .tab_style(style.fill(next(c)), loc.row_groups()) + .tab_style(style.borders(weight="3px"), loc.stub(rows=1)) + .tab_style(style.fill(next(c)), loc.stub()) + .tab_style(style.fill(next(c)), loc.stubhead()) +) +``` + +## Body + +```{python} +gt.tab_style(style.fill("yellow"), loc.body()) +``` + +## Column labels + +```{python} +( + gt + .tab_style(style.fill("yellow"), loc.column_header()) + .tab_style(style.fill("blue"), loc.column_labels(columns="num")) + .tab_style(style.fill("red"), loc.spanner_labels(ids=["spanner"])) +) + +``` + + + +## Header + +```{python} +( + gt.tab_style(style.fill("yellow"), loc.header()) + .tab_style(style.fill("blue"), loc.title()) + .tab_style(style.fill("red"), loc.subtitle()) +) +``` + +## Footer + +```{python} +( + gt.tab_style( + style.fill("yellow"), + loc.source_notes(), + ).tab_style( + style.borders(weight="3px"), + loc.footer(), + ) +) +``` + +## Stub + +```{python} +( + gt.tab_style(style.fill("yellow"), loc.stub()) + .tab_style(style.fill("blue"), loc.row_groups()) + .tab_style( + style.borders(style="dashed", weight="3px", color="red"), + loc.stub(rows=[1]), + ) +) +``` + +## Stubhead + +```{python} +gt.tab_style(style.fill("yellow"), loc.stubhead()) +``` diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index 58355a435..697c2404b 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from dataclasses import dataclass, field, replace from enum import Enum, auto -from typing import Any, Callable, Tuple, TypeVar, overload, TYPE_CHECKING +from typing import Any, Callable, Literal, Tuple, TypeVar, Union, overload, TYPE_CHECKING from typing_extensions import Self, TypeAlias @@ -28,6 +28,7 @@ if TYPE_CHECKING: from ._helpers import Md, Html, UnitStr, Text + from ._locations import Loc T = TypeVar("T") @@ -610,7 +611,7 @@ def order_groups(self, group_order: RowGroups): # TODO: validate return self.__class__(self.rows, self.group_rows.reorder(group_order)) - def group_indices_map(self) -> list[tuple[int, str | None]]: + def group_indices_map(self) -> list[tuple[int, GroupRowInfo | None]]: return self.group_rows.indices_map(len(self.rows)) def __iter__(self): @@ -740,7 +741,7 @@ def reorder(self, group_ids: list[str | MISSING_GROUP]) -> Self: return self.__class__(reordered) - def indices_map(self, n: int) -> list[tuple[int, str | None]]: + def indices_map(self, n: int) -> list[tuple[int, GroupRowInfo]]: """Return pairs of row index, group label for all rows in data. Note that when no groupings exist, n is used to return from range(n). @@ -751,7 +752,7 @@ def indices_map(self, n: int) -> list[tuple[int, str | None]]: if not len(self._d): return [(ii, None) for ii in range(n)] - return [(ind, info.defaulted_label()) for info in self for ind in info.indices] + return [(ind, info) for info in self for ind in info.indices] # Spanners ---- @@ -852,7 +853,7 @@ class FootnotePlacement(Enum): @dataclass(frozen=True) class FootnoteInfo: - locname: str | None = None + locname: Loc | None = None grpname: str | None = None colname: str | None = None locnum: int | None = None @@ -869,8 +870,7 @@ class FootnoteInfo: @dataclass(frozen=True) class StyleInfo: - locname: str - locnum: int + locname: Loc grpname: str | None = None colname: str | None = None rownum: int | None = None diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 617bd7ecf..3274c2450 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -1,16 +1,23 @@ from __future__ import annotations import itertools -from dataclasses import dataclass +from dataclasses import dataclass, field from functools import singledispatch -from typing import TYPE_CHECKING, Any, Callable, Literal +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, Union from typing_extensions import TypeAlias # note that types like Spanners are only used in annotations for concretes of the # resolve generic, but we need to import at runtime, due to singledispatch looking # up annotations -from ._gt_data import ColInfoTypeEnum, FootnoteInfo, FootnotePlacement, GTData, Spanners, StyleInfo +from ._gt_data import ( + ColInfoTypeEnum, + FootnoteInfo, + FootnotePlacement, + GTData, + Spanners, + StyleInfo, +) from ._styles import CellStyle from ._tbl_data import PlDataFrame, PlExpr, eval_select, eval_transform @@ -35,6 +42,7 @@ class CellPos: column: int row: int colname: str + rowname: str | None = None @dataclass @@ -43,42 +51,62 @@ class Loc: @dataclass -class LocTitle(Loc): +class LocHeader(Loc): """A location for targeting the table title and subtitle.""" - groups: Literal["title", "subtitle"] + +@dataclass +class LocTitle(Loc): + """A location for targeting the title.""" + + +@dataclass +class LocSubTitle(Loc): + """A location for targeting the subtitle.""" @dataclass class LocStubhead(Loc): - groups: Literal["stubhead"] = "stubhead" + """A location for targeting the table stubhead and stubhead label.""" @dataclass -class LocColumnSpanners(Loc): - """A location for column spanners.""" +class LocStubheadLabel(Loc): + """A location for targetting the stubhead.""" + - # TODO: these can also be tidy selectors - ids: list[str] +@dataclass +class LocColumnHeader(Loc): + """A location for column spanners and column labels.""" @dataclass class LocColumnLabels(Loc): - # TODO: these can be tidyselectors - columns: list[str] + columns: SelectExpr = None @dataclass -class LocRowGroups(Loc): - # TODO: these can be tidyselectors - groups: list[str] +class LocSpannerLabels(Loc): + """A location for column spanners.""" + + ids: SelectExpr = None @dataclass class LocStub(Loc): - # TODO: these can be tidyselectors - # TODO: can this take integers? - rows: list[str] + """A location for targeting the table stub, row group labels, summary labels, and body.""" + + rows: RowSelectExpr = None + + +@dataclass +class LocRowGroups(Loc): + rows: RowSelectExpr = None + + +@dataclass +class LocSummaryLabel(Loc): + rows: RowSelectExpr = None @dataclass @@ -108,6 +136,7 @@ class LocBody(Loc): ------ See [`GT.tab_style()`](`great_tables.GT.tab_style`). """ + columns: SelectExpr = None rows: RowSelectExpr = None @@ -115,41 +144,23 @@ class LocBody(Loc): @dataclass class LocSummary(Loc): # TODO: these can be tidyselectors - groups: list[str] - columns: list[str] - rows: list[str] - - -@dataclass -class LocGrandSummary(Loc): - # TODO: these can be tidyselectors - columns: list[str] - rows: list[str] - - -@dataclass -class LocStubSummary(Loc): - # TODO: these can be tidyselectors - groups: list[str] - rows: list[str] + columns: SelectExpr = None + rows: RowSelectExpr = None @dataclass -class LocStubGrandSummary(Loc): - rows: list[str] +class LocFooter(Loc): + """A location for targeting the footer.""" @dataclass class LocFootnotes(Loc): - groups: Literal["footnotes"] = "footnotes" + """A location for targeting footnotes.""" @dataclass class LocSourceNotes(Loc): - # This dataclass in R has a `groups` field, which is a literal value. - # In python, we can use an isinstance check to determine we're seeing an - # instance of this class - groups: Literal["source_notes"] = "source_notes" + """A location for targeting source notes.""" # Utils ================================================================================ @@ -289,17 +300,17 @@ def resolve_rows_i( expr: list[str | int] = [expr] if isinstance(data, GTData): - if expr is None: - if null_means == "everything": - return [(row.rowname, ii) for ii, row in enumerate(data._stub)] - else: - return [] - row_names = [row.rowname for row in data._stub] else: row_names = data - if isinstance(expr, list): + if expr is None: + if null_means == "everything": + return [(row.rowname, ii) for ii, row in enumerate(data._stub)] + else: + return [] + + elif isinstance(expr, list): # TODO: manually doing row selection here for now target_names = set(x for x in expr if isinstance(x, str)) target_pos = set( @@ -355,7 +366,7 @@ def resolve(loc: Loc, *args: Any, **kwargs: Any) -> Loc | list[CellPos]: @resolve.register -def _(loc: LocColumnSpanners, spanners: Spanners) -> LocColumnSpanners: +def _(loc: LocSpannerLabels, spanners: Spanners) -> LocSpannerLabels: # unique labels (with order preserved) spanner_ids = [span.spanner_id for span in spanners] @@ -363,7 +374,30 @@ def _(loc: LocColumnSpanners, spanners: Spanners) -> LocColumnSpanners: resolved_spanners = [spanner_ids[idx] for idx in resolved_spanners_idx] # Create a list object - return LocColumnSpanners(ids=resolved_spanners) + return LocSpannerLabels(ids=resolved_spanners) + + +@resolve.register +def _(loc: LocColumnLabels, data: GTData) -> list[CellPos]: + cols = resolve_cols_i(data=data, expr=loc.columns) + cell_pos = [CellPos(col[1], 0, colname=col[0]) for col in cols] + return cell_pos + + +@resolve.register +def _(loc: LocRowGroups, data: GTData) -> set[int]: + # TODO: what are the rules for matching row groups? + # TODO: resolve_rows_i will match a list expr to row names (not group names) + group_pos = set(pos for _, pos in resolve_rows_i(data, loc.rows)) + return list(group_pos) + + +@resolve.register +def _(loc: LocStub, data: GTData) -> set[int]: + # TODO: what are the rules for matching row groups? + rows = resolve_rows_i(data=data, expr=loc.rows) + cell_pos = set(row[1] for row in rows) + return cell_pos @resolve.register @@ -383,27 +417,114 @@ def _(loc: LocBody, data: GTData) -> list[CellPos]: # Style generic ======================================================================== +# LocHeader +# LocTitle +# LocSubTitle +# LocStubhead +# LocStubheadLabel +# LocColumnLabels +# LocColumnLabel +# LocSpannerLabel +# LocStub +# LocRowGroupLabel +# LocRowLabel +# LocSummaryLabel +# LocBody +# LocSummary +# LocFooter +# LocFootnotes +# LocSourceNotes + + @singledispatch def set_style(loc: Loc, data: GTData, style: list[str]) -> GTData: """Set style for location.""" raise NotImplementedError(f"Unsupported location type: {type(loc)}") +@set_style.register(LocHeader) +@set_style.register(LocTitle) +@set_style.register(LocSubTitle) +@set_style.register(LocStubhead) +@set_style.register(LocStubheadLabel) +@set_style.register(LocColumnHeader) +@set_style.register(LocFooter) +@set_style.register(LocSourceNotes) +def _( + loc: ( + LocHeader + | LocTitle + | LocSubTitle + | LocStubhead + | LocStubheadLabel + | LocColumnHeader + | LocFooter + | LocSourceNotes + ), + data: GTData, + style: list[CellStyle], +) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + + return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) + + @set_style.register -def _(loc: LocTitle, data: GTData, style: list[CellStyle]) -> GTData: +def _(loc: LocColumnLabels, data: GTData, style: list[CellStyle]) -> GTData: + positions: list[CellPos] = resolve(loc, data) + + # evaluate any column expressions in styles + styles = [entry._evaluate_expressions(data._tbl_data) for entry in style] + + all_info: list[StyleInfo] = [] + for col_pos in positions: + crnt_info = StyleInfo( + locname=loc, + colname=col_pos.colname, + rownum=col_pos.row, + styles=styles, + ) + all_info.append(crnt_info) + return data._replace(_styles=data._styles + all_info) + + +@set_style.register +def _(loc: LocSpannerLabels, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) + # TODO resolve - # set ---- - if loc.groups == "title": - info = StyleInfo(locname="title", locnum=1, styles=style) - elif loc.groups == "subtitle": - info = StyleInfo(locname="subtitle", locnum=2, styles=style) - else: - raise ValueError(f"Unknown title group: {loc.groups}") + new_loc = resolve(loc, data._spanners) + return data._replace( + _styles=data._styles + [StyleInfo(locname=new_loc, grpname=new_loc.ids, styles=style)] + ) - return data._styles.append(info) + +@set_style.register +def _(loc: LocRowGroups, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + + row_groups = resolve(loc, data) + return data._replace( + _styles=data._styles + [StyleInfo(locname=loc, grpname=row_groups, styles=style)] + ) + + +@set_style.register +def _(loc: LocStub, data: GTData, style: list[CellStyle]) -> GTData: + # validate ---- + for entry in style: + entry._raise_if_requires_data(loc) + # TODO resolve + cells = resolve(loc, data) + + new_styles = [StyleInfo(locname=loc, rownum=rownum, styles=style) for rownum in cells] + return data._replace(_styles=data._styles + new_styles) @set_style.register @@ -417,7 +538,7 @@ def _(loc: LocBody, data: GTData, style: list[CellStyle]) -> GTData: for col_pos in positions: row_styles = [entry._from_row(data._tbl_data, col_pos.row) for entry in style_ready] crnt_info = StyleInfo( - locname="data", locnum=5, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles + locname=loc, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles ) all_info.append(crnt_info) @@ -436,21 +557,11 @@ def set_footnote(loc: Loc, data: GTData, footnote: str, placement: PlacementOpti @set_footnote.register(type(None)) def _(loc: None, data: GTData, footnote: str, placement: PlacementOptions) -> GTData: place = FootnotePlacement[placement] - info = FootnoteInfo(locname="none", locnum=0, footnotes=[footnote], placement=place) + info = FootnoteInfo(locname="none", footnotes=[footnote], placement=place) return data._replace(_footnotes=data._footnotes + [info]) @set_footnote.register def _(loc: LocTitle, data: GTData, footnote: str, placement: PlacementOptions) -> GTData: - # TODO: note that footnote here is annotated as a string, but I think that in R it - # can be a list of strings. - place = FootnotePlacement[placement] - if loc.groups == "title": - info = FootnoteInfo(locname="title", locnum=1, footnotes=[footnote], placement=place) - elif loc.groups == "subtitle": - info = FootnoteInfo(locname="subtitle", locnum=2, footnotes=[footnote], placement=place) - else: - raise ValueError(f"Unknown title group: {loc.groups}") - - return data._replace(_footnotes=data._footnotes + [info]) + raise NotImplementedError() diff --git a/great_tables/_modify_rows.py b/great_tables/_modify_rows.py index 013fe458e..72e553091 100644 --- a/great_tables/_modify_rows.py +++ b/great_tables/_modify_rows.py @@ -15,8 +15,12 @@ def row_group_order(self: GTSelf, groups: RowGroups) -> GTSelf: def _remove_from_body_styles(styles: Styles, column: str) -> Styles: + # TODO: refactor + from ._utils_render_html import _is_loc + from ._locations import LocBody + new_styles = [ - info for info in styles if not (info.locname == "data" and info.colname == column) + info for info in styles if not (_is_loc(info.locname, LocBody) and info.colname == column) ] return new_styles diff --git a/great_tables/_styles.py b/great_tables/_styles.py index c14cb1937..867c3f5e1 100644 --- a/great_tables/_styles.py +++ b/great_tables/_styles.py @@ -125,6 +125,14 @@ def _raise_if_requires_data(self, loc: Loc): ) +@dataclass +class CellStyleCss(CellStyle): + rule: str + + def _to_html_style(self): + return self.rule + + @dataclass class CellStyleText(CellStyle): """A style specification for cell text. diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 4f8489a6a..4abd55ec7 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -1,19 +1,48 @@ from __future__ import annotations -from itertools import chain +from itertools import chain, groupby +from math import isnan from typing import Any, cast from great_tables._spanners import spanners_print_matrix from htmltools import HTML, TagList, css, tags -from ._gt_data import GTData +from ._gt_data import GTData, Styles, GroupRowInfo from ._tbl_data import _get_cell, cast_frame_to_string, n_rows, replace_null_frame from ._text import _process_text, _process_text_id from ._utils import heading_has_subtitle, heading_has_title, seq_groups +from . import _locations as loc -def create_heading_component_h(data: GTData) -> str: +def _is_loc(loc: str | loc.Loc, cls: type[loc.Loc]): + if isinstance(loc, str): + return loc == cls.groups + + return isinstance(loc, cls) + +def _flatten_styles(styles: Styles, wrap: bool = False) -> str: + # flatten all StyleInfo.styles lists + style_entries = list(chain(*[x.styles for x in styles])) + rendered_styles = [el._to_html_style() for el in style_entries] + + # TODO dedupe rendered styles in sequence + + if wrap: + if rendered_styles: + # return style html attribute + return f' style="{" ".join(rendered_styles)}"' + # if no rendered styles, just return a blank + return "" + if rendered_styles: + # return space-separated list of rendered styles + return " ".join(rendered_styles) + # if not wrapping the styles for html element, + # return None so htmltools omits a style attribute + return None + + +def create_heading_component_h(data: GTData) -> str: title = data._heading.title subtitle = data._heading.subtitle @@ -31,6 +60,13 @@ def create_heading_component_h(data: GTData) -> str: title = _process_text(title) subtitle = _process_text(subtitle) + # Filter list of StyleInfo for the various header components + styles_header = [x for x in data._styles if _is_loc(x.locname, loc.LocHeader)] + styles_title = [x for x in data._styles if _is_loc(x.locname, loc.LocTitle)] + styles_subtitle = [x for x in data._styles if _is_loc(x.locname, loc.LocSubTitle)] + title_style = _flatten_styles(styles_header + styles_title, wrap=True) + subtitle_style = _flatten_styles(styles_header + styles_subtitle, wrap=True) + # Get the effective number of columns, which is number of columns # that will finally be rendered accounting for the stub layout n_cols_total = data._boxhead._get_effective_number_of_columns( @@ -40,15 +76,15 @@ def create_heading_component_h(data: GTData) -> str: if has_subtitle: heading = f""" - {title} + {title} - {subtitle} + {subtitle} """ else: heading = f""" - {title} + {title} """ return heading @@ -67,8 +103,6 @@ def create_columns_component_h(data: GTData) -> str: # Get necessary data objects for composing the column labels and spanners stubh = data._stubhead - # TODO: skipping styles for now - # styles_tbl = dt_styles_get(data = data) boxhead = data._boxhead # TODO: The body component of the table is only needed for determining RTL alignment @@ -97,13 +131,11 @@ def create_columns_component_h(data: GTData) -> str: # Get the column headings headings_info = boxhead._get_default_columns() - # TODO: Skipping styles for now - # Get the style attrs for the stubhead label - # stubhead_style_attrs = subset(styles_tbl, locname == "stubhead") - # Get the style attrs for the spanner column headings - # spanner_style_attrs = subset(styles_tbl, locname == "columns_groups") - # Get the style attrs for the spanner column headings - # column_style_attrs = subset(styles_tbl, locname == "columns_columns") + # Filter list of StyleInfo for the various stubhead and column labels components + styles_stubhead = [x for x in data._styles if _is_loc(x.locname, loc.LocStubhead)] + styles_column_labels = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnHeader)] + styles_spanner_label = [x for x in data._styles if _is_loc(x.locname, loc.LocSpannerLabels)] + styles_column_label = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnLabels)] # If columns are present in the stub, then replace with a set stubhead label or nothing if len(stub_layout) > 0 and stubh is not None: @@ -124,18 +156,13 @@ def create_columns_component_h(data: GTData) -> str: if spanner_row_count == 0: # Create the cell for the stubhead label if len(stub_layout) > 0: - stubhead_style = None - # FIXME: Ignore styles for now - # if stubhead_style_attrs is not None and len(stubhead_style_attrs) > 0: - # stubhead_style = stubhead_style_attrs[0].html_style - table_col_headings.append( tags.th( HTML(_process_text(stub_label)), class_=f"gt_col_heading gt_columns_bottom_border gt_{stubhead_label_alignment}", rowspan="1", colspan=len(stub_layout), - style=stubhead_style, + style=_flatten_styles(styles_stubhead), scope="colgroup" if len(stub_layout) > 1 else "col", id=_process_text_id(stub_label), ) @@ -143,13 +170,8 @@ def create_columns_component_h(data: GTData) -> str: # Create the headings in the case where there are no spanners at all ------------------------- for info in headings_info: - # NOTE: Ignore styles for now - # styles_column = subset(column_style_attrs, colnum == i) - # - # Convert the code above this comment from R to valid python - # if len(styles_column) > 0: - # column_style = styles_column[0].html_style - column_style = None + # Filter by column label / id, join with overall column labels style + styles_i = [x for x in styles_column_label if x.colname == info.var] table_col_headings.append( tags.th( @@ -157,16 +179,16 @@ def create_columns_component_h(data: GTData) -> str: class_=f"gt_col_heading gt_columns_bottom_border gt_{info.defaulted_align}", rowspan=1, colspan=1, - style=column_style, + style=_flatten_styles(styles_column_labels + styles_i), scope="col", id=_process_text_id(info.column_label), ) ) # Join the cells into a string and begin each with a newline - th_cells = "\n" + "\n".join([" " + str(tag) for tag in table_col_headings]) + "\n" + # th_cells = "\n" + "\n".join([" " + str(tag) for tag in table_col_headings]) + "\n" - table_col_headings = tags.tr(HTML(th_cells), class_="gt_col_headings") + table_col_headings = tags.tr(*table_col_headings, class_="gt_col_headings") # # Create the spanners and column labels in the case where there *are* spanners ------------- @@ -196,20 +218,13 @@ def create_columns_component_h(data: GTData) -> str: # Create the cell for the stubhead label if len(stub_layout) > 0: - # NOTE: Ignore styles for now - # if len(stubhead_style_attrs) > 0: - # stubhead_style = stubhead_style_attrs.html_style - # else: - # stubhead_style = None - stubhead_style = None - level_1_spanners.append( tags.th( HTML(_process_text(stub_label)), class_=f"gt_col_heading gt_columns_bottom_border gt_{str(stubhead_label_alignment)}", rowspan=2, colspan=len(stub_layout), - style=stubhead_style, + style=_flatten_styles(styles_stubhead), scope="colgroup" if len(stub_layout) > 1 else "col", id=_process_text_id(stub_label), ) @@ -229,14 +244,8 @@ def create_columns_component_h(data: GTData) -> str: for ii, (span_key, h_info) in enumerate(zip(spanner_col_names, headings_info)): if spanner_ids[level_1_index][span_key] is None: - # NOTE: Ignore styles for now - # styles_heading = filter( - # lambda x: x.get('locname') == "columns_columns" and x.get('colname') == headings_vars[i], - # styles_tbl if 'styles_tbl' in locals() else [] - # ) - # - # heading_style = next(styles_heading, {}).get('html_style', None) - heading_style = None + # Filter by column label / id, join with overall column labels style + styles_i = [x for x in styles_column_label if x.colname == h_info.var] # Get the alignment values for the first set of column labels first_set_alignment = h_info.defaulted_align @@ -248,7 +257,7 @@ def create_columns_component_h(data: GTData) -> str: class_=f"gt_col_heading gt_columns_bottom_border gt_{str(first_set_alignment)}", rowspan=2, colspan=1, - style=heading_style, + style=_flatten_styles(styles_column_labels + styles_i), scope="col", id=_process_text_id(h_info.column_label), ) @@ -258,21 +267,14 @@ def create_columns_component_h(data: GTData) -> str: # If colspans[i] == 0, it means that a previous cell's # `colspan` will cover us if colspans[ii] > 0: - # NOTE: Ignore styles for now - # FIXME: this needs to be rewritten - # styles_spanners = filter( - # spanner_style_attrs, - # locname == "columns_groups", - # grpname == spanner_ids[level_1_index, ][i] - # ) - # - # spanner_style = - # if (nrow(styles_spanners) > 0) { - # styles_spanners$html_style - # } else { - # NULL - # } - spanner_style = None + # Filter by column label / id, join with overall column labels style + # TODO check this filter logic + styles_i = [ + x + for x in styles_spanner_label + # TODO: refactor use of set + if set(x.grpname) & set([spanner_ids_level_1_index[ii]]) + ] level_1_spanners.append( tags.th( @@ -283,7 +285,7 @@ def create_columns_component_h(data: GTData) -> str: class_="gt_center gt_columns_top_border gt_column_spanner_outer", rowspan=1, colspan=colspans[ii], - style=spanner_style, + style=_flatten_styles(styles_column_labels + styles_i), scope="colgroup" if colspans[ii] > 1 else "col", id=_process_text_id(spanner_ids_level_1_index[ii]), ) @@ -301,18 +303,9 @@ def create_columns_component_h(data: GTData) -> str: spanned_column_labels = [] for j in range(len(remaining_headings)): - # Skip styles for now - # styles_remaining = styles_tbl[ - # (styles_tbl["locname"] == "columns_columns") & - # (styles_tbl["colname"] == remaining_headings[j]) - # ] - # - # remaining_style = ( - # styles_remaining["html_style"].values[0] - # if len(styles_remaining) > 0 - # else None - # ) - remaining_style = None + # Filter by column label / id, join with overall column labels style + # TODO check this filter logic + styles_i = [x for x in styles_column_label if x.colname == remaining_headings[j]] remaining_alignment = boxhead._get_boxhead_get_alignment_by_var( var=remaining_headings[j] @@ -324,7 +317,7 @@ def create_columns_component_h(data: GTData) -> str: class_=f"gt_col_heading gt_columns_bottom_border gt_{remaining_alignment}", rowspan=1, colspan=1, - style=remaining_style, + style=_flatten_styles(styles_column_labels + styles_i), scope="col", id=_process_text_id(remaining_headings_labels[j]), ) @@ -359,18 +352,14 @@ def create_columns_component_h(data: GTData) -> str: for colspan, span_label in zip(colspans, spanners_row.values()): if colspan > 0: - # Skip styles for now - # styles_spanners = styles_tbl[ - # (styles_tbl["locname"] == "columns_groups") & - # (styles_tbl["grpname"] in spanners_vars) - # ] - # - # spanner_style = ( - # styles_spanners["html_style"].values[0] - # if len(styles_spanners) > 0 - # else None - # ) - spanner_style = None + # Filter by column label / id, join with overall column labels style + # TODO check this filter logic + styles_i = [ + x + for x in styles_column_label + # TODO: refactor use of set + if set(x.grpname) & set([colspan, span_label]) + ] if span_label: span = tags.span( @@ -386,7 +375,7 @@ def create_columns_component_h(data: GTData) -> str: class_="gt_center gt_columns_bottom_border gt_columns_top_border gt_column_spanner_outer", rowspan=1, colspan=colspan, - style=spanner_style, + style=_flatten_styles(styles_column_labels + styles_i), scope="colgroup" if colspan > 1 else "col", ) ) @@ -400,6 +389,8 @@ def create_columns_component_h(data: GTData) -> str: rowspan=1, colspan=len(stub_layout), scope="colgroup" if len(stub_layout) > 1 else "col", + # TODO check if ok to just use base styling? + style=_flatten_styles(styles_column_labels), ), ) @@ -409,6 +400,8 @@ def create_columns_component_h(data: GTData) -> str: tags.tr( level_i_spanners, class_="gt_col_headings gt_spanner_row", + # TODO check if ok to just use base styling? + style=_flatten_styles(styles_column_labels), ) ), ) @@ -417,7 +410,7 @@ def create_columns_component_h(data: GTData) -> str: higher_spanner_rows, table_col_headings, ) - return str(table_col_headings) + return table_col_headings def create_body_component_h(data: GTData) -> str: @@ -426,8 +419,15 @@ def create_body_component_h(data: GTData) -> str: _str_orig_data = cast_frame_to_string(data._tbl_data) tbl_data = replace_null_frame(data._body.body, _str_orig_data) - # Filter list of StyleInfo to only those that apply to the body (where locname="data") - styles_body = [x for x in data._styles if x.locname == "data"] + # Filter list of StyleInfo to only those that apply to the stub + styles_row_group_label = [x for x in data._styles if _is_loc(x.locname, loc.LocRowGroups)] + styles_row_label = [x for x in data._styles if _is_loc(x.locname, loc.LocStub)] + styles_summary_label = [x for x in data._styles if _is_loc(x.locname, loc.LocSummaryLabel)] + + # Filter list of StyleInfo to only those that apply to the body + styles_cells = [x for x in data._styles if _is_loc(x.locname, loc.LocBody)] + # styles_body = [x for x in data._styles if _is_loc(x.locname, loc.LocBody2)] + # styles_summary = [x for x in data._styles if _is_loc(x.locname, loc.LocSummary)] # Get the default column vars column_vars = data._boxhead._get_default_columns() @@ -453,11 +453,11 @@ def create_body_component_h(data: GTData) -> str: body_rows: list[str] = [] # iterate over rows (ordered by groupings) - prev_group_label = None + prev_group_info = None - ordered_index = data._stub.group_indices_map() + ordered_index: list[tuple[int, GroupRowInfo]] = data._stub.group_indices_map() - for i, group_label in ordered_index: + for i, group_info in ordered_index: # For table striping we want to add a striping CSS class to the even-numbered # rows in the rendered table; to target these rows, determine if `i` in the current @@ -466,27 +466,28 @@ def create_body_component_h(data: GTData) -> str: body_cells: list[str] = [] + # Create table row specifically for group (if applicable) if has_stub_column and has_groups and not has_two_col_stub: colspan_value = data._boxhead._get_effective_number_of_columns( stub=data._stub, options=data._options ) - # Generate a row that contains the row group label (this spans the entire row) but - # only if `i` indicates there should be a row group label - if group_label != prev_group_label: + # Only create if this is the first row of data within the group + if group_info is not prev_group_info: + group_label = group_info.defaulted_label() group_class = ( "gt_empty_group_heading" if group_label == "" else "gt_group_heading_row" ) + _styles = [style for style in styles_row_group_label if i in style.grpname] + group_styles = _flatten_styles(_styles, wrap=True) group_row = f""" - {group_label} + {group_label} """ - prev_group_label = group_label - body_rows.append(group_row) - # Create a single cell and append result to `body_cells` + # Create row cells for colinfo in column_vars: cell_content: Any = _get_cell(tbl_data, i, colinfo.var) cell_str: str = str(cell_content) @@ -502,17 +503,8 @@ def create_body_component_h(data: GTData) -> str: cell_alignment = colinfo.defaulted_align # Get the style attributes for the current cell by filtering the - # `styles_body` list for the current row and column - styles_i = [x for x in styles_body if x.rownum == i and x.colname == colinfo.var] - - # Develop the `style` attribute for the current cell - if len(styles_i) > 0: - # flatten all StyleInfo.styles lists - style_entries = list(chain(*[x.styles for x in styles_i])) - rendered_styles = [el._to_html_style() for el in style_entries] - cell_styles = f'style="{" ".join(rendered_styles)}"' + " " - else: - cell_styles = "" + # `styles_cells` list for the current row and column + _body_styles = [x for x in styles_cells if x.rownum == i and x.colname == colinfo.var] if is_stub_cell: @@ -520,6 +512,8 @@ def create_body_component_h(data: GTData) -> str: classes = ["gt_row", "gt_left", "gt_stub"] + _rowname_styles = [x for x in styles_row_label if x.rownum == i] + if table_stub_striped and odd_i_row: classes.append("gt_striped") @@ -529,17 +523,24 @@ def create_body_component_h(data: GTData) -> str: classes = ["gt_row", f"gt_{cell_alignment}"] + _rowname_styles = [] + if table_body_striped and odd_i_row: classes.append("gt_striped") # Ensure that `classes` becomes a space-separated string classes = " ".join(classes) + cell_styles = _flatten_styles( + _body_styles + _rowname_styles, + wrap=True, + ) body_cells.append( - f""" <{el_name} {cell_styles}class="{classes}">{cell_str}""" + f""" <{el_name}{cell_styles} class="{classes}">{cell_str}""" ) - prev_group_label = group_label + prev_group_info = group_info + body_rows.append(" \n" + "\n".join(body_cells) + "\n ") all_body_rows = "\n".join(body_rows) @@ -552,6 +553,10 @@ def create_body_component_h(data: GTData) -> str: def create_source_notes_component_h(data: GTData) -> str: source_notes = data._source_notes + # Filter list of StyleInfo to only those that apply to the source notes + styles_footer = [x for x in data._styles if _is_loc(x.locname, loc.LocFooter)] + styles_source_notes = [x for x in data._styles if _is_loc(x.locname, loc.LocSourceNotes)] + # If there are no source notes, then return an empty string if source_notes == []: return "" @@ -573,13 +578,14 @@ def create_source_notes_component_h(data: GTData) -> str: source_notes_tr: list[str] = [] + _styles = _flatten_styles(styles_footer + styles_source_notes, wrap=True) for note in source_notes: note_str = _process_text(note) source_notes_tr.append( f""" - {note_str} + {note_str} """ ) @@ -618,6 +624,9 @@ def create_source_notes_component_h(data: GTData) -> str: def create_footnotes_component_h(data: GTData): + # Filter list of StyleInfo to only those that apply to the footnotes + styles_footnotes = [x for x in data._styles if _is_loc(x.locname, loc.LocFootnotes)] + return "" diff --git a/great_tables/loc.py b/great_tables/loc.py index eec0149b7..e463ab132 100644 --- a/great_tables/loc.py +++ b/great_tables/loc.py @@ -1,9 +1,42 @@ from __future__ import annotations from ._locations import ( - LocBody as body, - LocStub as stub, + # Header ---- + LocHeader as header, + LocTitle as title, + LocSubTitle as subtitle, + # + # Stubhead ---- + LocStubhead as stubhead, + # + # Column Labels ---- + LocColumnHeader as column_header, + LocSpannerLabels as spanner_labels, LocColumnLabels as column_labels, + # + # Stub ---- + LocStub as stub, + LocRowGroups as row_groups, + # + # Body ---- + LocBody as body, + # + # Footer ---- + LocFooter as footer, + LocSourceNotes as source_notes, ) -__all__ = ("body", "stub", "column_labels") +__all__ = ( + "header", + "title", + "subtitle", + "stubhead", + "column_header", + "spanner_labels", + "column_labels", + "stub", + "row_groups", + "body", + "footer", + "source_notes", +) diff --git a/great_tables/style.py b/great_tables/style.py index e6b4c480e..7bd85d96e 100644 --- a/great_tables/style.py +++ b/great_tables/style.py @@ -4,6 +4,7 @@ CellStyleText as text, CellStyleFill as fill, CellStyleBorders as borders, + CellStyleCss as css, ) -__all__ = ("text", "fill", "borders") +__all__ = ("text", "fill", "borders", "css") diff --git a/tests/__snapshots__/test_utils_render_html.ambr b/tests/__snapshots__/test_utils_render_html.ambr index f671a3d3a..77598d1a8 100644 --- a/tests/__snapshots__/test_utils_render_html.ambr +++ b/tests/__snapshots__/test_utils_render_html.ambr @@ -35,6 +35,54 @@ ''' # --- +# name: test_loc_kitchen_sink + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
title
subtitle
stubheadnum + spanner +
charfctr
grp_a
row_10.1111apricotone
yo
+ + + + ''' +# --- # name: test_multiple_spanners_pads_for_stubhead_label ''' diff --git a/tests/test_gt_data.py b/tests/test_gt_data.py index ee89b3d70..9feea05d0 100644 --- a/tests/test_gt_data.py +++ b/tests/test_gt_data.py @@ -31,7 +31,8 @@ def test_stub_order_groups(): stub2 = stub.order_groups(["c", "a", "b"]) assert stub2.group_ids == ["c", "a", "b"] - assert stub2.group_indices_map() == [(3, "c"), (1, "a"), (0, "b"), (2, "b")] + indice_labels = [(ii, info.defaulted_label()) for ii, info in stub2.group_indices_map()] + assert indice_labels == [(3, "c"), (1, "a"), (0, "b"), (2, "b")] def test_boxhead_reorder(): diff --git a/tests/test_locations.py b/tests/test_locations.py index 55006b41e..cd10f7940 100644 --- a/tests/test_locations.py +++ b/tests/test_locations.py @@ -1,12 +1,13 @@ import pandas as pd import polars as pl +import polars.selectors as cs import pytest from great_tables import GT from great_tables._gt_data import Spanners from great_tables._locations import ( CellPos, LocBody, - LocColumnSpanners, + LocSpannerLabels, LocTitle, resolve, resolve_cols_i, @@ -116,6 +117,9 @@ def test_resolve_rows_i_raises(bad_expr): assert "a callable that takes a DataFrame and returns a boolean Series" in expected +# Resolve Loc tests -------------------------------------------------------------------------------- + + def test_resolve_loc_body(): gt = GT(pd.DataFrame({"x": [1, 2], "y": [3, 4]})) @@ -132,25 +136,41 @@ def test_resolve_loc_body(): assert pos.colname == "x" -def test_resolve_column_spanners_simple(): +@pytest.mark.xfail +def test_resolve_loc_spanners_label_single(): + spanners = Spanners.from_ids(["a", "b"]) + loc = LocSpannerLabels(ids="a") + + new_loc = resolve(loc, spanners) + + assert new_loc.ids == ["a"] + + +@pytest.mark.parametrize( + "expr", + [ + ["a", "c"], + pytest.param(cs.by_name("a", "c"), marks=pytest.mark.xfail), + ], +) +def test_resolve_loc_spanners_label(expr): # note that this essentially a no-op ids = ["a", "b", "c"] spanners = Spanners.from_ids(ids) - loc = LocColumnSpanners(ids=["a", "c"]) + loc = LocSpannerLabels(ids=expr) new_loc = resolve(loc, spanners) - assert new_loc == loc assert new_loc.ids == ["a", "c"] -def test_resolve_column_spanners_error_missing(): +def test_resolve_loc_spanner_label_error_missing(): # note that this essentially a no-op ids = ["a", "b", "c"] spanners = Spanners.from_ids(ids) - loc = LocColumnSpanners(ids=["a", "d"]) + loc = LocSpannerLabels(ids=["a", "d"]) with pytest.raises(ValueError): resolve(loc, spanners) @@ -190,7 +210,7 @@ def test_set_style_loc_body_from_column(expr): def test_set_style_loc_title_from_column_error(snapshot): df = pd.DataFrame({"x": [1, 2], "color": ["red", "blue"]}) gt_df = GT(df) - loc = LocTitle("title") + loc = LocTitle() style = CellStyleText(color=FromColumn("color")) with pytest.raises(TypeError) as exc_info: diff --git a/tests/test_utils_render_html.py b/tests/test_utils_render_html.py index da0241a75..0d84ee314 100644 --- a/tests/test_utils_render_html.py +++ b/tests/test_utils_render_html.py @@ -29,7 +29,7 @@ def assert_rendered_columns(snapshot, gt): built = gt._build_data("html") columns = create_columns_component_h(built) - assert snapshot == columns + assert snapshot == str(columns) def assert_rendered_body(snapshot, gt): @@ -191,3 +191,52 @@ def test_multiple_spanners_pads_for_stubhead_label(snapshot): ) assert_rendered_columns(snapshot, gt) + + +# Location style rendering ------------------------------------------------------------------------- +# these tests focus on location classes being correctly picked up +def test_loc_column_labels(): + gt = GT(pl.DataFrame({"x": [1], "y": [2]})) + + new_gt = gt.tab_style(style.fill("yellow"), loc.column_labels(columns=["x"])) + el = create_columns_component_h(new_gt._build_data("html")) + + assert el.name == "tr" + assert el.children[0].attrs["style"] == "background-color: yellow;" + assert "style" not in el.children[1].attrs + + +def test_loc_kitchen_sink(snapshot): + gt = ( + GT(exibble.loc[[0], ["num", "char", "fctr", "row", "group"]]) + .tab_header("title", "subtitle") + .tab_stub(rowname_col="row", groupname_col="group") + .tab_source_note("yo") + .tab_spanner("spanner", ["char", "fctr"]) + .tab_stubhead("stubhead") + ) + + new_gt = ( + gt.tab_style(style.css("BODY"), loc.body()) + # Columns ----------- + .tab_style(style.css("COLUMN_LABEL"), loc.column_labels(columns="num")) + .tab_style(style.css("COLUMN_HEADER"), loc.column_header()) + .tab_style(style.css("SPANNER_LABEL"), loc.spanner_labels(ids=["spanner"])) + # Header ----------- + .tab_style(style.css("HEADER"), loc.header()) + .tab_style(style.css("SUBTITLE"), loc.subtitle()) + .tab_style(style.css("TITLE"), loc.title()) + # Footer ----------- + .tab_style(style.css("FOOTER"), loc.footer()) + .tab_style(style.css("SOURCE_NOTES"), loc.source_notes()) + # .tab_style(style.css("AAA"), loc.footnotes()) + # Stub -------------- + .tab_style(style.css("GROUP_LABEL"), loc.row_groups()) + .tab_style(style.css("STUB"), loc.stub()) + .tab_style(style.css("ROW_LABEL"), loc.stub(rows=[0])) + .tab_style(style.css("STUBHEAD"), loc.stubhead()) + ) + + html = new_gt.as_raw_html() + cleaned = html[html.index(" Date: Fri, 27 Sep 2024 16:49:50 -0400 Subject: [PATCH 102/150] Revert "Allow for granular section restyling via convenience api (#341)" This reverts commit b2da03cee5a9d3f2e311069750ef8f1bc25dad84. --- docs/_quarto.yml | 1 - docs/get-started/targeted-styles.qmd | 131 --------- great_tables/_gt_data.py | 14 +- great_tables/_locations.py | 257 +++++------------- great_tables/_modify_rows.py | 6 +- great_tables/_styles.py | 8 - great_tables/_utils_render_html.py | 243 ++++++++--------- great_tables/loc.py | 39 +-- great_tables/style.py | 3 +- .../__snapshots__/test_utils_render_html.ambr | 48 ---- tests/test_gt_data.py | 3 +- tests/test_locations.py | 34 +-- tests/test_utils_render_html.py | 51 +--- 13 files changed, 211 insertions(+), 627 deletions(-) delete mode 100644 docs/get-started/targeted-styles.qmd diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 55f19de95..15a937b15 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -46,7 +46,6 @@ website: - get-started/column-selection.qmd - get-started/row-selection.qmd - get-started/nanoplots.qmd - - get-started/targeted-styles.qmd format: html: diff --git a/docs/get-started/targeted-styles.qmd b/docs/get-started/targeted-styles.qmd deleted file mode 100644 index a3394ec75..000000000 --- a/docs/get-started/targeted-styles.qmd +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: Targeted styles -jupyter: python3 ---- - -In [Styling the Table Body](./basic-styling), we discussed styling table data with `.tab_style()`. -In this article we'll cover how the same method can be used to style many other parts of the table, like the header, specific spanner labels, the footer, and more. - -:::{.callout-warning} -This feature is currently a work in progress, and not yet released. Great Tables must be installed from github in order to try it. -::: - - -## Kitchen sink - -Below is a big example that shows all possible `loc` specifiers being used. - -```{python} -from great_tables import GT, exibble, loc, style - -# https://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12 -brewer_colors = [ - "#a6cee3", - "#1f78b4", - "#b2df8a", - "#33a02c", - "#fb9a99", - "#e31a1c", - "#fdbf6f", - "#ff7f00", - "#cab2d6", - "#6a3d9a", - "#ffff99", - "#b15928", -] - -c = iter(brewer_colors) - -gt = ( - GT(exibble.loc[[0, 1, 4], ["num", "char", "fctr", "row", "group"]]) - .tab_header("title", "subtitle") - .tab_stub(rowname_col="row", groupname_col="group") - .tab_source_note("yo") - .tab_spanner("spanner", ["char", "fctr"]) - .tab_stubhead("stubhead") -) - -( - gt.tab_style(style.fill(next(c)), loc.body()) - # Columns ----------- - # TODO: appears in browser, but not vs code - .tab_style(style.fill(next(c)), loc.column_labels(columns="num")) - .tab_style(style.fill(next(c)), loc.column_header()) - .tab_style(style.fill(next(c)), loc.spanner_labels(ids=["spanner"])) - # Header ----------- - .tab_style(style.fill(next(c)), loc.header()) - .tab_style(style.fill(next(c)), loc.subtitle()) - .tab_style(style.fill(next(c)), loc.title()) - # Footer ----------- - .tab_style(style.borders(weight="3px"), loc.source_notes()) - .tab_style(style.fill(next(c)), loc.footer()) - # Stub -------------- - .tab_style(style.fill(next(c)), loc.row_groups()) - .tab_style(style.borders(weight="3px"), loc.stub(rows=1)) - .tab_style(style.fill(next(c)), loc.stub()) - .tab_style(style.fill(next(c)), loc.stubhead()) -) -``` - -## Body - -```{python} -gt.tab_style(style.fill("yellow"), loc.body()) -``` - -## Column labels - -```{python} -( - gt - .tab_style(style.fill("yellow"), loc.column_header()) - .tab_style(style.fill("blue"), loc.column_labels(columns="num")) - .tab_style(style.fill("red"), loc.spanner_labels(ids=["spanner"])) -) - -``` - - - -## Header - -```{python} -( - gt.tab_style(style.fill("yellow"), loc.header()) - .tab_style(style.fill("blue"), loc.title()) - .tab_style(style.fill("red"), loc.subtitle()) -) -``` - -## Footer - -```{python} -( - gt.tab_style( - style.fill("yellow"), - loc.source_notes(), - ).tab_style( - style.borders(weight="3px"), - loc.footer(), - ) -) -``` - -## Stub - -```{python} -( - gt.tab_style(style.fill("yellow"), loc.stub()) - .tab_style(style.fill("blue"), loc.row_groups()) - .tab_style( - style.borders(style="dashed", weight="3px", color="red"), - loc.stub(rows=[1]), - ) -) -``` - -## Stubhead - -```{python} -gt.tab_style(style.fill("yellow"), loc.stubhead()) -``` diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index 697c2404b..58355a435 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from dataclasses import dataclass, field, replace from enum import Enum, auto -from typing import Any, Callable, Literal, Tuple, TypeVar, Union, overload, TYPE_CHECKING +from typing import Any, Callable, Tuple, TypeVar, overload, TYPE_CHECKING from typing_extensions import Self, TypeAlias @@ -28,7 +28,6 @@ if TYPE_CHECKING: from ._helpers import Md, Html, UnitStr, Text - from ._locations import Loc T = TypeVar("T") @@ -611,7 +610,7 @@ def order_groups(self, group_order: RowGroups): # TODO: validate return self.__class__(self.rows, self.group_rows.reorder(group_order)) - def group_indices_map(self) -> list[tuple[int, GroupRowInfo | None]]: + def group_indices_map(self) -> list[tuple[int, str | None]]: return self.group_rows.indices_map(len(self.rows)) def __iter__(self): @@ -741,7 +740,7 @@ def reorder(self, group_ids: list[str | MISSING_GROUP]) -> Self: return self.__class__(reordered) - def indices_map(self, n: int) -> list[tuple[int, GroupRowInfo]]: + def indices_map(self, n: int) -> list[tuple[int, str | None]]: """Return pairs of row index, group label for all rows in data. Note that when no groupings exist, n is used to return from range(n). @@ -752,7 +751,7 @@ def indices_map(self, n: int) -> list[tuple[int, GroupRowInfo]]: if not len(self._d): return [(ii, None) for ii in range(n)] - return [(ind, info) for info in self for ind in info.indices] + return [(ind, info.defaulted_label()) for info in self for ind in info.indices] # Spanners ---- @@ -853,7 +852,7 @@ class FootnotePlacement(Enum): @dataclass(frozen=True) class FootnoteInfo: - locname: Loc | None = None + locname: str | None = None grpname: str | None = None colname: str | None = None locnum: int | None = None @@ -870,7 +869,8 @@ class FootnoteInfo: @dataclass(frozen=True) class StyleInfo: - locname: Loc + locname: str + locnum: int grpname: str | None = None colname: str | None = None rownum: int | None = None diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 3274c2450..617bd7ecf 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -1,23 +1,16 @@ from __future__ import annotations import itertools -from dataclasses import dataclass, field +from dataclasses import dataclass from functools import singledispatch -from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, Union +from typing import TYPE_CHECKING, Any, Callable, Literal from typing_extensions import TypeAlias # note that types like Spanners are only used in annotations for concretes of the # resolve generic, but we need to import at runtime, due to singledispatch looking # up annotations -from ._gt_data import ( - ColInfoTypeEnum, - FootnoteInfo, - FootnotePlacement, - GTData, - Spanners, - StyleInfo, -) +from ._gt_data import ColInfoTypeEnum, FootnoteInfo, FootnotePlacement, GTData, Spanners, StyleInfo from ._styles import CellStyle from ._tbl_data import PlDataFrame, PlExpr, eval_select, eval_transform @@ -42,7 +35,6 @@ class CellPos: column: int row: int colname: str - rowname: str | None = None @dataclass @@ -50,63 +42,43 @@ class Loc: """A location.""" -@dataclass -class LocHeader(Loc): - """A location for targeting the table title and subtitle.""" - - @dataclass class LocTitle(Loc): - """A location for targeting the title.""" - + """A location for targeting the table title and subtitle.""" -@dataclass -class LocSubTitle(Loc): - """A location for targeting the subtitle.""" + groups: Literal["title", "subtitle"] @dataclass class LocStubhead(Loc): - """A location for targeting the table stubhead and stubhead label.""" - - -@dataclass -class LocStubheadLabel(Loc): - """A location for targetting the stubhead.""" - - -@dataclass -class LocColumnHeader(Loc): - """A location for column spanners and column labels.""" + groups: Literal["stubhead"] = "stubhead" @dataclass -class LocColumnLabels(Loc): - columns: SelectExpr = None - - -@dataclass -class LocSpannerLabels(Loc): +class LocColumnSpanners(Loc): """A location for column spanners.""" - ids: SelectExpr = None + # TODO: these can also be tidy selectors + ids: list[str] @dataclass -class LocStub(Loc): - """A location for targeting the table stub, row group labels, summary labels, and body.""" - - rows: RowSelectExpr = None +class LocColumnLabels(Loc): + # TODO: these can be tidyselectors + columns: list[str] @dataclass class LocRowGroups(Loc): - rows: RowSelectExpr = None + # TODO: these can be tidyselectors + groups: list[str] @dataclass -class LocSummaryLabel(Loc): - rows: RowSelectExpr = None +class LocStub(Loc): + # TODO: these can be tidyselectors + # TODO: can this take integers? + rows: list[str] @dataclass @@ -136,7 +108,6 @@ class LocBody(Loc): ------ See [`GT.tab_style()`](`great_tables.GT.tab_style`). """ - columns: SelectExpr = None rows: RowSelectExpr = None @@ -144,23 +115,41 @@ class LocBody(Loc): @dataclass class LocSummary(Loc): # TODO: these can be tidyselectors - columns: SelectExpr = None - rows: RowSelectExpr = None + groups: list[str] + columns: list[str] + rows: list[str] + + +@dataclass +class LocGrandSummary(Loc): + # TODO: these can be tidyselectors + columns: list[str] + rows: list[str] + + +@dataclass +class LocStubSummary(Loc): + # TODO: these can be tidyselectors + groups: list[str] + rows: list[str] @dataclass -class LocFooter(Loc): - """A location for targeting the footer.""" +class LocStubGrandSummary(Loc): + rows: list[str] @dataclass class LocFootnotes(Loc): - """A location for targeting footnotes.""" + groups: Literal["footnotes"] = "footnotes" @dataclass class LocSourceNotes(Loc): - """A location for targeting source notes.""" + # This dataclass in R has a `groups` field, which is a literal value. + # In python, we can use an isinstance check to determine we're seeing an + # instance of this class + groups: Literal["source_notes"] = "source_notes" # Utils ================================================================================ @@ -300,17 +289,17 @@ def resolve_rows_i( expr: list[str | int] = [expr] if isinstance(data, GTData): + if expr is None: + if null_means == "everything": + return [(row.rowname, ii) for ii, row in enumerate(data._stub)] + else: + return [] + row_names = [row.rowname for row in data._stub] else: row_names = data - if expr is None: - if null_means == "everything": - return [(row.rowname, ii) for ii, row in enumerate(data._stub)] - else: - return [] - - elif isinstance(expr, list): + if isinstance(expr, list): # TODO: manually doing row selection here for now target_names = set(x for x in expr if isinstance(x, str)) target_pos = set( @@ -366,7 +355,7 @@ def resolve(loc: Loc, *args: Any, **kwargs: Any) -> Loc | list[CellPos]: @resolve.register -def _(loc: LocSpannerLabels, spanners: Spanners) -> LocSpannerLabels: +def _(loc: LocColumnSpanners, spanners: Spanners) -> LocColumnSpanners: # unique labels (with order preserved) spanner_ids = [span.spanner_id for span in spanners] @@ -374,30 +363,7 @@ def _(loc: LocSpannerLabels, spanners: Spanners) -> LocSpannerLabels: resolved_spanners = [spanner_ids[idx] for idx in resolved_spanners_idx] # Create a list object - return LocSpannerLabels(ids=resolved_spanners) - - -@resolve.register -def _(loc: LocColumnLabels, data: GTData) -> list[CellPos]: - cols = resolve_cols_i(data=data, expr=loc.columns) - cell_pos = [CellPos(col[1], 0, colname=col[0]) for col in cols] - return cell_pos - - -@resolve.register -def _(loc: LocRowGroups, data: GTData) -> set[int]: - # TODO: what are the rules for matching row groups? - # TODO: resolve_rows_i will match a list expr to row names (not group names) - group_pos = set(pos for _, pos in resolve_rows_i(data, loc.rows)) - return list(group_pos) - - -@resolve.register -def _(loc: LocStub, data: GTData) -> set[int]: - # TODO: what are the rules for matching row groups? - rows = resolve_rows_i(data=data, expr=loc.rows) - cell_pos = set(row[1] for row in rows) - return cell_pos + return LocColumnSpanners(ids=resolved_spanners) @resolve.register @@ -417,114 +383,27 @@ def _(loc: LocBody, data: GTData) -> list[CellPos]: # Style generic ======================================================================== -# LocHeader -# LocTitle -# LocSubTitle -# LocStubhead -# LocStubheadLabel -# LocColumnLabels -# LocColumnLabel -# LocSpannerLabel -# LocStub -# LocRowGroupLabel -# LocRowLabel -# LocSummaryLabel -# LocBody -# LocSummary -# LocFooter -# LocFootnotes -# LocSourceNotes - - @singledispatch def set_style(loc: Loc, data: GTData, style: list[str]) -> GTData: """Set style for location.""" raise NotImplementedError(f"Unsupported location type: {type(loc)}") -@set_style.register(LocHeader) -@set_style.register(LocTitle) -@set_style.register(LocSubTitle) -@set_style.register(LocStubhead) -@set_style.register(LocStubheadLabel) -@set_style.register(LocColumnHeader) -@set_style.register(LocFooter) -@set_style.register(LocSourceNotes) -def _( - loc: ( - LocHeader - | LocTitle - | LocSubTitle - | LocStubhead - | LocStubheadLabel - | LocColumnHeader - | LocFooter - | LocSourceNotes - ), - data: GTData, - style: list[CellStyle], -) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - - return data._replace(_styles=data._styles + [StyleInfo(locname=loc, styles=style)]) - - @set_style.register -def _(loc: LocColumnLabels, data: GTData, style: list[CellStyle]) -> GTData: - positions: list[CellPos] = resolve(loc, data) - - # evaluate any column expressions in styles - styles = [entry._evaluate_expressions(data._tbl_data) for entry in style] - - all_info: list[StyleInfo] = [] - for col_pos in positions: - crnt_info = StyleInfo( - locname=loc, - colname=col_pos.colname, - rownum=col_pos.row, - styles=styles, - ) - all_info.append(crnt_info) - return data._replace(_styles=data._styles + all_info) - - -@set_style.register -def _(loc: LocSpannerLabels, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - # TODO resolve - - new_loc = resolve(loc, data._spanners) - return data._replace( - _styles=data._styles + [StyleInfo(locname=new_loc, grpname=new_loc.ids, styles=style)] - ) - - -@set_style.register -def _(loc: LocRowGroups, data: GTData, style: list[CellStyle]) -> GTData: +def _(loc: LocTitle, data: GTData, style: list[CellStyle]) -> GTData: # validate ---- for entry in style: entry._raise_if_requires_data(loc) - row_groups = resolve(loc, data) - return data._replace( - _styles=data._styles + [StyleInfo(locname=loc, grpname=row_groups, styles=style)] - ) - - -@set_style.register -def _(loc: LocStub, data: GTData, style: list[CellStyle]) -> GTData: - # validate ---- - for entry in style: - entry._raise_if_requires_data(loc) - # TODO resolve - cells = resolve(loc, data) + # set ---- + if loc.groups == "title": + info = StyleInfo(locname="title", locnum=1, styles=style) + elif loc.groups == "subtitle": + info = StyleInfo(locname="subtitle", locnum=2, styles=style) + else: + raise ValueError(f"Unknown title group: {loc.groups}") - new_styles = [StyleInfo(locname=loc, rownum=rownum, styles=style) for rownum in cells] - return data._replace(_styles=data._styles + new_styles) + return data._styles.append(info) @set_style.register @@ -538,7 +417,7 @@ def _(loc: LocBody, data: GTData, style: list[CellStyle]) -> GTData: for col_pos in positions: row_styles = [entry._from_row(data._tbl_data, col_pos.row) for entry in style_ready] crnt_info = StyleInfo( - locname=loc, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles + locname="data", locnum=5, colname=col_pos.colname, rownum=col_pos.row, styles=row_styles ) all_info.append(crnt_info) @@ -557,11 +436,21 @@ def set_footnote(loc: Loc, data: GTData, footnote: str, placement: PlacementOpti @set_footnote.register(type(None)) def _(loc: None, data: GTData, footnote: str, placement: PlacementOptions) -> GTData: place = FootnotePlacement[placement] - info = FootnoteInfo(locname="none", footnotes=[footnote], placement=place) + info = FootnoteInfo(locname="none", locnum=0, footnotes=[footnote], placement=place) return data._replace(_footnotes=data._footnotes + [info]) @set_footnote.register def _(loc: LocTitle, data: GTData, footnote: str, placement: PlacementOptions) -> GTData: - raise NotImplementedError() + # TODO: note that footnote here is annotated as a string, but I think that in R it + # can be a list of strings. + place = FootnotePlacement[placement] + if loc.groups == "title": + info = FootnoteInfo(locname="title", locnum=1, footnotes=[footnote], placement=place) + elif loc.groups == "subtitle": + info = FootnoteInfo(locname="subtitle", locnum=2, footnotes=[footnote], placement=place) + else: + raise ValueError(f"Unknown title group: {loc.groups}") + + return data._replace(_footnotes=data._footnotes + [info]) diff --git a/great_tables/_modify_rows.py b/great_tables/_modify_rows.py index 72e553091..013fe458e 100644 --- a/great_tables/_modify_rows.py +++ b/great_tables/_modify_rows.py @@ -15,12 +15,8 @@ def row_group_order(self: GTSelf, groups: RowGroups) -> GTSelf: def _remove_from_body_styles(styles: Styles, column: str) -> Styles: - # TODO: refactor - from ._utils_render_html import _is_loc - from ._locations import LocBody - new_styles = [ - info for info in styles if not (_is_loc(info.locname, LocBody) and info.colname == column) + info for info in styles if not (info.locname == "data" and info.colname == column) ] return new_styles diff --git a/great_tables/_styles.py b/great_tables/_styles.py index 867c3f5e1..c14cb1937 100644 --- a/great_tables/_styles.py +++ b/great_tables/_styles.py @@ -125,14 +125,6 @@ def _raise_if_requires_data(self, loc: Loc): ) -@dataclass -class CellStyleCss(CellStyle): - rule: str - - def _to_html_style(self): - return self.rule - - @dataclass class CellStyleText(CellStyle): """A style specification for cell text. diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 4abd55ec7..4f8489a6a 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -1,48 +1,19 @@ from __future__ import annotations -from itertools import chain, groupby -from math import isnan +from itertools import chain from typing import Any, cast from great_tables._spanners import spanners_print_matrix from htmltools import HTML, TagList, css, tags -from ._gt_data import GTData, Styles, GroupRowInfo +from ._gt_data import GTData from ._tbl_data import _get_cell, cast_frame_to_string, n_rows, replace_null_frame from ._text import _process_text, _process_text_id from ._utils import heading_has_subtitle, heading_has_title, seq_groups -from . import _locations as loc - - -def _is_loc(loc: str | loc.Loc, cls: type[loc.Loc]): - if isinstance(loc, str): - return loc == cls.groups - - return isinstance(loc, cls) - - -def _flatten_styles(styles: Styles, wrap: bool = False) -> str: - # flatten all StyleInfo.styles lists - style_entries = list(chain(*[x.styles for x in styles])) - rendered_styles = [el._to_html_style() for el in style_entries] - - # TODO dedupe rendered styles in sequence - - if wrap: - if rendered_styles: - # return style html attribute - return f' style="{" ".join(rendered_styles)}"' - # if no rendered styles, just return a blank - return "" - if rendered_styles: - # return space-separated list of rendered styles - return " ".join(rendered_styles) - # if not wrapping the styles for html element, - # return None so htmltools omits a style attribute - return None def create_heading_component_h(data: GTData) -> str: + title = data._heading.title subtitle = data._heading.subtitle @@ -60,13 +31,6 @@ def create_heading_component_h(data: GTData) -> str: title = _process_text(title) subtitle = _process_text(subtitle) - # Filter list of StyleInfo for the various header components - styles_header = [x for x in data._styles if _is_loc(x.locname, loc.LocHeader)] - styles_title = [x for x in data._styles if _is_loc(x.locname, loc.LocTitle)] - styles_subtitle = [x for x in data._styles if _is_loc(x.locname, loc.LocSubTitle)] - title_style = _flatten_styles(styles_header + styles_title, wrap=True) - subtitle_style = _flatten_styles(styles_header + styles_subtitle, wrap=True) - # Get the effective number of columns, which is number of columns # that will finally be rendered accounting for the stub layout n_cols_total = data._boxhead._get_effective_number_of_columns( @@ -76,15 +40,15 @@ def create_heading_component_h(data: GTData) -> str: if has_subtitle: heading = f""" - {title} + {title} - {subtitle} + {subtitle} """ else: heading = f""" - {title} + {title} """ return heading @@ -103,6 +67,8 @@ def create_columns_component_h(data: GTData) -> str: # Get necessary data objects for composing the column labels and spanners stubh = data._stubhead + # TODO: skipping styles for now + # styles_tbl = dt_styles_get(data = data) boxhead = data._boxhead # TODO: The body component of the table is only needed for determining RTL alignment @@ -131,11 +97,13 @@ def create_columns_component_h(data: GTData) -> str: # Get the column headings headings_info = boxhead._get_default_columns() - # Filter list of StyleInfo for the various stubhead and column labels components - styles_stubhead = [x for x in data._styles if _is_loc(x.locname, loc.LocStubhead)] - styles_column_labels = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnHeader)] - styles_spanner_label = [x for x in data._styles if _is_loc(x.locname, loc.LocSpannerLabels)] - styles_column_label = [x for x in data._styles if _is_loc(x.locname, loc.LocColumnLabels)] + # TODO: Skipping styles for now + # Get the style attrs for the stubhead label + # stubhead_style_attrs = subset(styles_tbl, locname == "stubhead") + # Get the style attrs for the spanner column headings + # spanner_style_attrs = subset(styles_tbl, locname == "columns_groups") + # Get the style attrs for the spanner column headings + # column_style_attrs = subset(styles_tbl, locname == "columns_columns") # If columns are present in the stub, then replace with a set stubhead label or nothing if len(stub_layout) > 0 and stubh is not None: @@ -156,13 +124,18 @@ def create_columns_component_h(data: GTData) -> str: if spanner_row_count == 0: # Create the cell for the stubhead label if len(stub_layout) > 0: + stubhead_style = None + # FIXME: Ignore styles for now + # if stubhead_style_attrs is not None and len(stubhead_style_attrs) > 0: + # stubhead_style = stubhead_style_attrs[0].html_style + table_col_headings.append( tags.th( HTML(_process_text(stub_label)), class_=f"gt_col_heading gt_columns_bottom_border gt_{stubhead_label_alignment}", rowspan="1", colspan=len(stub_layout), - style=_flatten_styles(styles_stubhead), + style=stubhead_style, scope="colgroup" if len(stub_layout) > 1 else "col", id=_process_text_id(stub_label), ) @@ -170,8 +143,13 @@ def create_columns_component_h(data: GTData) -> str: # Create the headings in the case where there are no spanners at all ------------------------- for info in headings_info: - # Filter by column label / id, join with overall column labels style - styles_i = [x for x in styles_column_label if x.colname == info.var] + # NOTE: Ignore styles for now + # styles_column = subset(column_style_attrs, colnum == i) + # + # Convert the code above this comment from R to valid python + # if len(styles_column) > 0: + # column_style = styles_column[0].html_style + column_style = None table_col_headings.append( tags.th( @@ -179,16 +157,16 @@ def create_columns_component_h(data: GTData) -> str: class_=f"gt_col_heading gt_columns_bottom_border gt_{info.defaulted_align}", rowspan=1, colspan=1, - style=_flatten_styles(styles_column_labels + styles_i), + style=column_style, scope="col", id=_process_text_id(info.column_label), ) ) # Join the cells into a string and begin each with a newline - # th_cells = "\n" + "\n".join([" " + str(tag) for tag in table_col_headings]) + "\n" + th_cells = "\n" + "\n".join([" " + str(tag) for tag in table_col_headings]) + "\n" - table_col_headings = tags.tr(*table_col_headings, class_="gt_col_headings") + table_col_headings = tags.tr(HTML(th_cells), class_="gt_col_headings") # # Create the spanners and column labels in the case where there *are* spanners ------------- @@ -218,13 +196,20 @@ def create_columns_component_h(data: GTData) -> str: # Create the cell for the stubhead label if len(stub_layout) > 0: + # NOTE: Ignore styles for now + # if len(stubhead_style_attrs) > 0: + # stubhead_style = stubhead_style_attrs.html_style + # else: + # stubhead_style = None + stubhead_style = None + level_1_spanners.append( tags.th( HTML(_process_text(stub_label)), class_=f"gt_col_heading gt_columns_bottom_border gt_{str(stubhead_label_alignment)}", rowspan=2, colspan=len(stub_layout), - style=_flatten_styles(styles_stubhead), + style=stubhead_style, scope="colgroup" if len(stub_layout) > 1 else "col", id=_process_text_id(stub_label), ) @@ -244,8 +229,14 @@ def create_columns_component_h(data: GTData) -> str: for ii, (span_key, h_info) in enumerate(zip(spanner_col_names, headings_info)): if spanner_ids[level_1_index][span_key] is None: - # Filter by column label / id, join with overall column labels style - styles_i = [x for x in styles_column_label if x.colname == h_info.var] + # NOTE: Ignore styles for now + # styles_heading = filter( + # lambda x: x.get('locname') == "columns_columns" and x.get('colname') == headings_vars[i], + # styles_tbl if 'styles_tbl' in locals() else [] + # ) + # + # heading_style = next(styles_heading, {}).get('html_style', None) + heading_style = None # Get the alignment values for the first set of column labels first_set_alignment = h_info.defaulted_align @@ -257,7 +248,7 @@ def create_columns_component_h(data: GTData) -> str: class_=f"gt_col_heading gt_columns_bottom_border gt_{str(first_set_alignment)}", rowspan=2, colspan=1, - style=_flatten_styles(styles_column_labels + styles_i), + style=heading_style, scope="col", id=_process_text_id(h_info.column_label), ) @@ -267,14 +258,21 @@ def create_columns_component_h(data: GTData) -> str: # If colspans[i] == 0, it means that a previous cell's # `colspan` will cover us if colspans[ii] > 0: - # Filter by column label / id, join with overall column labels style - # TODO check this filter logic - styles_i = [ - x - for x in styles_spanner_label - # TODO: refactor use of set - if set(x.grpname) & set([spanner_ids_level_1_index[ii]]) - ] + # NOTE: Ignore styles for now + # FIXME: this needs to be rewritten + # styles_spanners = filter( + # spanner_style_attrs, + # locname == "columns_groups", + # grpname == spanner_ids[level_1_index, ][i] + # ) + # + # spanner_style = + # if (nrow(styles_spanners) > 0) { + # styles_spanners$html_style + # } else { + # NULL + # } + spanner_style = None level_1_spanners.append( tags.th( @@ -285,7 +283,7 @@ def create_columns_component_h(data: GTData) -> str: class_="gt_center gt_columns_top_border gt_column_spanner_outer", rowspan=1, colspan=colspans[ii], - style=_flatten_styles(styles_column_labels + styles_i), + style=spanner_style, scope="colgroup" if colspans[ii] > 1 else "col", id=_process_text_id(spanner_ids_level_1_index[ii]), ) @@ -303,9 +301,18 @@ def create_columns_component_h(data: GTData) -> str: spanned_column_labels = [] for j in range(len(remaining_headings)): - # Filter by column label / id, join with overall column labels style - # TODO check this filter logic - styles_i = [x for x in styles_column_label if x.colname == remaining_headings[j]] + # Skip styles for now + # styles_remaining = styles_tbl[ + # (styles_tbl["locname"] == "columns_columns") & + # (styles_tbl["colname"] == remaining_headings[j]) + # ] + # + # remaining_style = ( + # styles_remaining["html_style"].values[0] + # if len(styles_remaining) > 0 + # else None + # ) + remaining_style = None remaining_alignment = boxhead._get_boxhead_get_alignment_by_var( var=remaining_headings[j] @@ -317,7 +324,7 @@ def create_columns_component_h(data: GTData) -> str: class_=f"gt_col_heading gt_columns_bottom_border gt_{remaining_alignment}", rowspan=1, colspan=1, - style=_flatten_styles(styles_column_labels + styles_i), + style=remaining_style, scope="col", id=_process_text_id(remaining_headings_labels[j]), ) @@ -352,14 +359,18 @@ def create_columns_component_h(data: GTData) -> str: for colspan, span_label in zip(colspans, spanners_row.values()): if colspan > 0: - # Filter by column label / id, join with overall column labels style - # TODO check this filter logic - styles_i = [ - x - for x in styles_column_label - # TODO: refactor use of set - if set(x.grpname) & set([colspan, span_label]) - ] + # Skip styles for now + # styles_spanners = styles_tbl[ + # (styles_tbl["locname"] == "columns_groups") & + # (styles_tbl["grpname"] in spanners_vars) + # ] + # + # spanner_style = ( + # styles_spanners["html_style"].values[0] + # if len(styles_spanners) > 0 + # else None + # ) + spanner_style = None if span_label: span = tags.span( @@ -375,7 +386,7 @@ def create_columns_component_h(data: GTData) -> str: class_="gt_center gt_columns_bottom_border gt_columns_top_border gt_column_spanner_outer", rowspan=1, colspan=colspan, - style=_flatten_styles(styles_column_labels + styles_i), + style=spanner_style, scope="colgroup" if colspan > 1 else "col", ) ) @@ -389,8 +400,6 @@ def create_columns_component_h(data: GTData) -> str: rowspan=1, colspan=len(stub_layout), scope="colgroup" if len(stub_layout) > 1 else "col", - # TODO check if ok to just use base styling? - style=_flatten_styles(styles_column_labels), ), ) @@ -400,8 +409,6 @@ def create_columns_component_h(data: GTData) -> str: tags.tr( level_i_spanners, class_="gt_col_headings gt_spanner_row", - # TODO check if ok to just use base styling? - style=_flatten_styles(styles_column_labels), ) ), ) @@ -410,7 +417,7 @@ def create_columns_component_h(data: GTData) -> str: higher_spanner_rows, table_col_headings, ) - return table_col_headings + return str(table_col_headings) def create_body_component_h(data: GTData) -> str: @@ -419,15 +426,8 @@ def create_body_component_h(data: GTData) -> str: _str_orig_data = cast_frame_to_string(data._tbl_data) tbl_data = replace_null_frame(data._body.body, _str_orig_data) - # Filter list of StyleInfo to only those that apply to the stub - styles_row_group_label = [x for x in data._styles if _is_loc(x.locname, loc.LocRowGroups)] - styles_row_label = [x for x in data._styles if _is_loc(x.locname, loc.LocStub)] - styles_summary_label = [x for x in data._styles if _is_loc(x.locname, loc.LocSummaryLabel)] - - # Filter list of StyleInfo to only those that apply to the body - styles_cells = [x for x in data._styles if _is_loc(x.locname, loc.LocBody)] - # styles_body = [x for x in data._styles if _is_loc(x.locname, loc.LocBody2)] - # styles_summary = [x for x in data._styles if _is_loc(x.locname, loc.LocSummary)] + # Filter list of StyleInfo to only those that apply to the body (where locname="data") + styles_body = [x for x in data._styles if x.locname == "data"] # Get the default column vars column_vars = data._boxhead._get_default_columns() @@ -453,11 +453,11 @@ def create_body_component_h(data: GTData) -> str: body_rows: list[str] = [] # iterate over rows (ordered by groupings) - prev_group_info = None + prev_group_label = None - ordered_index: list[tuple[int, GroupRowInfo]] = data._stub.group_indices_map() + ordered_index = data._stub.group_indices_map() - for i, group_info in ordered_index: + for i, group_label in ordered_index: # For table striping we want to add a striping CSS class to the even-numbered # rows in the rendered table; to target these rows, determine if `i` in the current @@ -466,28 +466,27 @@ def create_body_component_h(data: GTData) -> str: body_cells: list[str] = [] - # Create table row specifically for group (if applicable) if has_stub_column and has_groups and not has_two_col_stub: colspan_value = data._boxhead._get_effective_number_of_columns( stub=data._stub, options=data._options ) - # Only create if this is the first row of data within the group - if group_info is not prev_group_info: - group_label = group_info.defaulted_label() + # Generate a row that contains the row group label (this spans the entire row) but + # only if `i` indicates there should be a row group label + if group_label != prev_group_label: group_class = ( "gt_empty_group_heading" if group_label == "" else "gt_group_heading_row" ) - _styles = [style for style in styles_row_group_label if i in style.grpname] - group_styles = _flatten_styles(_styles, wrap=True) group_row = f""" - {group_label} + {group_label} """ + prev_group_label = group_label + body_rows.append(group_row) - # Create row cells + # Create a single cell and append result to `body_cells` for colinfo in column_vars: cell_content: Any = _get_cell(tbl_data, i, colinfo.var) cell_str: str = str(cell_content) @@ -503,8 +502,17 @@ def create_body_component_h(data: GTData) -> str: cell_alignment = colinfo.defaulted_align # Get the style attributes for the current cell by filtering the - # `styles_cells` list for the current row and column - _body_styles = [x for x in styles_cells if x.rownum == i and x.colname == colinfo.var] + # `styles_body` list for the current row and column + styles_i = [x for x in styles_body if x.rownum == i and x.colname == colinfo.var] + + # Develop the `style` attribute for the current cell + if len(styles_i) > 0: + # flatten all StyleInfo.styles lists + style_entries = list(chain(*[x.styles for x in styles_i])) + rendered_styles = [el._to_html_style() for el in style_entries] + cell_styles = f'style="{" ".join(rendered_styles)}"' + " " + else: + cell_styles = "" if is_stub_cell: @@ -512,8 +520,6 @@ def create_body_component_h(data: GTData) -> str: classes = ["gt_row", "gt_left", "gt_stub"] - _rowname_styles = [x for x in styles_row_label if x.rownum == i] - if table_stub_striped and odd_i_row: classes.append("gt_striped") @@ -523,24 +529,17 @@ def create_body_component_h(data: GTData) -> str: classes = ["gt_row", f"gt_{cell_alignment}"] - _rowname_styles = [] - if table_body_striped and odd_i_row: classes.append("gt_striped") # Ensure that `classes` becomes a space-separated string classes = " ".join(classes) - cell_styles = _flatten_styles( - _body_styles + _rowname_styles, - wrap=True, - ) body_cells.append( - f""" <{el_name}{cell_styles} class="{classes}">{cell_str}""" + f""" <{el_name} {cell_styles}class="{classes}">{cell_str}""" ) - prev_group_info = group_info - + prev_group_label = group_label body_rows.append(" \n" + "\n".join(body_cells) + "\n ") all_body_rows = "\n".join(body_rows) @@ -553,10 +552,6 @@ def create_body_component_h(data: GTData) -> str: def create_source_notes_component_h(data: GTData) -> str: source_notes = data._source_notes - # Filter list of StyleInfo to only those that apply to the source notes - styles_footer = [x for x in data._styles if _is_loc(x.locname, loc.LocFooter)] - styles_source_notes = [x for x in data._styles if _is_loc(x.locname, loc.LocSourceNotes)] - # If there are no source notes, then return an empty string if source_notes == []: return "" @@ -578,14 +573,13 @@ def create_source_notes_component_h(data: GTData) -> str: source_notes_tr: list[str] = [] - _styles = _flatten_styles(styles_footer + styles_source_notes, wrap=True) for note in source_notes: note_str = _process_text(note) source_notes_tr.append( f""" - {note_str} + {note_str} """ ) @@ -624,9 +618,6 @@ def create_source_notes_component_h(data: GTData) -> str: def create_footnotes_component_h(data: GTData): - # Filter list of StyleInfo to only those that apply to the footnotes - styles_footnotes = [x for x in data._styles if _is_loc(x.locname, loc.LocFootnotes)] - return "" diff --git a/great_tables/loc.py b/great_tables/loc.py index e463ab132..eec0149b7 100644 --- a/great_tables/loc.py +++ b/great_tables/loc.py @@ -1,42 +1,9 @@ from __future__ import annotations from ._locations import ( - # Header ---- - LocHeader as header, - LocTitle as title, - LocSubTitle as subtitle, - # - # Stubhead ---- - LocStubhead as stubhead, - # - # Column Labels ---- - LocColumnHeader as column_header, - LocSpannerLabels as spanner_labels, - LocColumnLabels as column_labels, - # - # Stub ---- - LocStub as stub, - LocRowGroups as row_groups, - # - # Body ---- LocBody as body, - # - # Footer ---- - LocFooter as footer, - LocSourceNotes as source_notes, + LocStub as stub, + LocColumnLabels as column_labels, ) -__all__ = ( - "header", - "title", - "subtitle", - "stubhead", - "column_header", - "spanner_labels", - "column_labels", - "stub", - "row_groups", - "body", - "footer", - "source_notes", -) +__all__ = ("body", "stub", "column_labels") diff --git a/great_tables/style.py b/great_tables/style.py index 7bd85d96e..e6b4c480e 100644 --- a/great_tables/style.py +++ b/great_tables/style.py @@ -4,7 +4,6 @@ CellStyleText as text, CellStyleFill as fill, CellStyleBorders as borders, - CellStyleCss as css, ) -__all__ = ("text", "fill", "borders", "css") +__all__ = ("text", "fill", "borders") diff --git a/tests/__snapshots__/test_utils_render_html.ambr b/tests/__snapshots__/test_utils_render_html.ambr index 77598d1a8..f671a3d3a 100644 --- a/tests/__snapshots__/test_utils_render_html.ambr +++ b/tests/__snapshots__/test_utils_render_html.ambr @@ -35,54 +35,6 @@ ''' # --- -# name: test_loc_kitchen_sink - ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
title
subtitle
stubheadnum - spanner -
charfctr
grp_a
row_10.1111apricotone
yo
- - - - ''' -# --- # name: test_multiple_spanners_pads_for_stubhead_label ''' diff --git a/tests/test_gt_data.py b/tests/test_gt_data.py index 9feea05d0..ee89b3d70 100644 --- a/tests/test_gt_data.py +++ b/tests/test_gt_data.py @@ -31,8 +31,7 @@ def test_stub_order_groups(): stub2 = stub.order_groups(["c", "a", "b"]) assert stub2.group_ids == ["c", "a", "b"] - indice_labels = [(ii, info.defaulted_label()) for ii, info in stub2.group_indices_map()] - assert indice_labels == [(3, "c"), (1, "a"), (0, "b"), (2, "b")] + assert stub2.group_indices_map() == [(3, "c"), (1, "a"), (0, "b"), (2, "b")] def test_boxhead_reorder(): diff --git a/tests/test_locations.py b/tests/test_locations.py index cd10f7940..55006b41e 100644 --- a/tests/test_locations.py +++ b/tests/test_locations.py @@ -1,13 +1,12 @@ import pandas as pd import polars as pl -import polars.selectors as cs import pytest from great_tables import GT from great_tables._gt_data import Spanners from great_tables._locations import ( CellPos, LocBody, - LocSpannerLabels, + LocColumnSpanners, LocTitle, resolve, resolve_cols_i, @@ -117,9 +116,6 @@ def test_resolve_rows_i_raises(bad_expr): assert "a callable that takes a DataFrame and returns a boolean Series" in expected -# Resolve Loc tests -------------------------------------------------------------------------------- - - def test_resolve_loc_body(): gt = GT(pd.DataFrame({"x": [1, 2], "y": [3, 4]})) @@ -136,41 +132,25 @@ def test_resolve_loc_body(): assert pos.colname == "x" -@pytest.mark.xfail -def test_resolve_loc_spanners_label_single(): - spanners = Spanners.from_ids(["a", "b"]) - loc = LocSpannerLabels(ids="a") - - new_loc = resolve(loc, spanners) - - assert new_loc.ids == ["a"] - - -@pytest.mark.parametrize( - "expr", - [ - ["a", "c"], - pytest.param(cs.by_name("a", "c"), marks=pytest.mark.xfail), - ], -) -def test_resolve_loc_spanners_label(expr): +def test_resolve_column_spanners_simple(): # note that this essentially a no-op ids = ["a", "b", "c"] spanners = Spanners.from_ids(ids) - loc = LocSpannerLabels(ids=expr) + loc = LocColumnSpanners(ids=["a", "c"]) new_loc = resolve(loc, spanners) + assert new_loc == loc assert new_loc.ids == ["a", "c"] -def test_resolve_loc_spanner_label_error_missing(): +def test_resolve_column_spanners_error_missing(): # note that this essentially a no-op ids = ["a", "b", "c"] spanners = Spanners.from_ids(ids) - loc = LocSpannerLabels(ids=["a", "d"]) + loc = LocColumnSpanners(ids=["a", "d"]) with pytest.raises(ValueError): resolve(loc, spanners) @@ -210,7 +190,7 @@ def test_set_style_loc_body_from_column(expr): def test_set_style_loc_title_from_column_error(snapshot): df = pd.DataFrame({"x": [1, 2], "color": ["red", "blue"]}) gt_df = GT(df) - loc = LocTitle() + loc = LocTitle("title") style = CellStyleText(color=FromColumn("color")) with pytest.raises(TypeError) as exc_info: diff --git a/tests/test_utils_render_html.py b/tests/test_utils_render_html.py index 0d84ee314..da0241a75 100644 --- a/tests/test_utils_render_html.py +++ b/tests/test_utils_render_html.py @@ -29,7 +29,7 @@ def assert_rendered_columns(snapshot, gt): built = gt._build_data("html") columns = create_columns_component_h(built) - assert snapshot == str(columns) + assert snapshot == columns def assert_rendered_body(snapshot, gt): @@ -191,52 +191,3 @@ def test_multiple_spanners_pads_for_stubhead_label(snapshot): ) assert_rendered_columns(snapshot, gt) - - -# Location style rendering ------------------------------------------------------------------------- -# these tests focus on location classes being correctly picked up -def test_loc_column_labels(): - gt = GT(pl.DataFrame({"x": [1], "y": [2]})) - - new_gt = gt.tab_style(style.fill("yellow"), loc.column_labels(columns=["x"])) - el = create_columns_component_h(new_gt._build_data("html")) - - assert el.name == "tr" - assert el.children[0].attrs["style"] == "background-color: yellow;" - assert "style" not in el.children[1].attrs - - -def test_loc_kitchen_sink(snapshot): - gt = ( - GT(exibble.loc[[0], ["num", "char", "fctr", "row", "group"]]) - .tab_header("title", "subtitle") - .tab_stub(rowname_col="row", groupname_col="group") - .tab_source_note("yo") - .tab_spanner("spanner", ["char", "fctr"]) - .tab_stubhead("stubhead") - ) - - new_gt = ( - gt.tab_style(style.css("BODY"), loc.body()) - # Columns ----------- - .tab_style(style.css("COLUMN_LABEL"), loc.column_labels(columns="num")) - .tab_style(style.css("COLUMN_HEADER"), loc.column_header()) - .tab_style(style.css("SPANNER_LABEL"), loc.spanner_labels(ids=["spanner"])) - # Header ----------- - .tab_style(style.css("HEADER"), loc.header()) - .tab_style(style.css("SUBTITLE"), loc.subtitle()) - .tab_style(style.css("TITLE"), loc.title()) - # Footer ----------- - .tab_style(style.css("FOOTER"), loc.footer()) - .tab_style(style.css("SOURCE_NOTES"), loc.source_notes()) - # .tab_style(style.css("AAA"), loc.footnotes()) - # Stub -------------- - .tab_style(style.css("GROUP_LABEL"), loc.row_groups()) - .tab_style(style.css("STUB"), loc.stub()) - .tab_style(style.css("ROW_LABEL"), loc.stub(rows=[0])) - .tab_style(style.css("STUBHEAD"), loc.stubhead()) - ) - - html = new_gt.as_raw_html() - cleaned = html[html.index(" Date: Fri, 27 Sep 2024 17:26:23 -0400 Subject: [PATCH 103/150] Update index.qmd --- docs/blog/introduction-0.12.0/index.qmd | 63 ++++++++++--------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/docs/blog/introduction-0.12.0/index.qmd b/docs/blog/introduction-0.12.0/index.qmd index a0f0c7962..285181607 100644 --- a/docs/blog/introduction-0.12.0/index.qmd +++ b/docs/blog/introduction-0.12.0/index.qmd @@ -7,21 +7,21 @@ freeze: true jupyter: python3 --- -In Great Tables `0.12.0` we did something that is sure to please those who obsess over styling, and this is the ability to style virtually any part (i.e., *location*) of a table. Also, the use of *Google Fonts* is now integrated into the package (with lots of font choices there). Lastly, we incorporated row striping as an option so that you can get that zebra stripe look in your table. In this post, we'll present a few examples in the following big features: +In Great Tables `0.12.0` we added two features that provide more options for customizing the appearance of a table: we incorporated row striping as an option so that you can get that zebra stripe look in your table. In this post, we'll present a few examples in the following big features: -- using `tab_style()` in more `locations=` -- putting some Google Fonts into a table with `tab_style()`/`opt_table_font()` + `google_font()` -- adding row stripes with `tab_options()` or `opt_row_striping()` +- using typefaces from Google Fonts via `tab_style()` or `opt_table_font()` +- adding table striping via `tab_options()` and `opt_row_striping()` -### Styles all over the table with an enhanced `loc` module +Let's have a look at how these new features can be used! -Before `v0.12.0` we were able to style parts of the table with `tab_style()` but that was limited only to the body of the table by having only `loc.body()` being usable in `tab_style()`'s `locations=` argument. Now, we have quite a few locations that cover all locations in the table! Here's the complete set: +### Using fonts from *Google Fonts* -- `loc.body()` - the table body -- ... +Google Fonts is a free service that allows use of hosted typefaces in your own websites. In Great Tables, we added the `google_font()` helper function to easily incorporate such fonts in your tables. There are two ways to go about this: +1. use `google_font()` with `opt_table_font()` to set a Google Font for the entire table +2. invoke `google_font()` within `tab_style(styles=style.text(font=...))` to set the font within a location -Let's start with this small, yet unstyled table. It has a few locations included thanks to its use of several `tab_*()` methods. +Let's start with this small table that uses the default set of fonts for the entire table. ```{python} @@ -30,10 +30,9 @@ Let's start with this small, yet unstyled table. It has a few locations included from great_tables import GT, exibble, style, loc - gt_tbl = ( GT(exibble.head(), rowname_col="row", groupname_col="group") - .cols_hide(columns=["fctr", "date", "time"]) + .cols_hide(columns=["char", "fctr", "date", "time"]) .tab_header( title="A small piece of the exibble dataset", subtitle="Displaying the first five rows (of eight)", @@ -46,41 +45,30 @@ gt_tbl = ( gt_tbl ``` +Now, with `opt_table_font()` + `google_font()`, we'll change the table's font to one from Google Fonts. I like [`Noto Serif`](https://fonts.google.com/noto/specimen/Noto+Serif) so let's use that here! ```{python} -from great_tables import GT, exibble, style, loc +from great_tables import GT, exibble, style, loc, google_font ( gt_tbl - .tab_style( - style=style.fill(color="lightblue"), - locations=loc.body(columns="num", rows=[1, 2]), - ) - .tab_style( - style=[ - style.text(color="white", weight="bold"), - style.fill(color="olivedrab") - ], - locations=loc.body(columns="currency") - ) - #.tab_style( - # style=style.fill(color="aqua"), - # locations=[ - # loc.column_labels(columns=["num", "currency"]), - # loc.title(), - # loc.subtitle() - # ] - #) + .opt_table_font(font=google_font(name="Noto Serif")) ) ``` +Looking good! And we don't have to apply the font to the entire table. We might just wanted to use a Google Font in the table body. For that use case, `tab_style()` is the preferred method. Here's an example that uses the [`IBM Plex Mono`](https://fonts.google.com/specimen/IBM+Plex+Mono) typeface. +```{python} +( + gt_tbl + .tab_style( + style=style.text(font=google_font(name="IBM Plex Mono")), + locations=loc.body() + ) +) +``` - - -### Using fonts from Google Fonts - - +Nice! And it's refreshing to see tables with fonts different from default set, as good as it might be. We kept the `google_font()` helper function as simple as possible, requiring only the font name in its `name=` argument. There are hundreds of fonts hosted on [Google Fonts](https://fonts.google.com) so look through the site, experiment, and find the fonts that you think look best in your tables! ### Striping rows in your table @@ -88,4 +76,5 @@ from great_tables import GT, exibble, style, loc ### Wrapping up -We got a lot of feedback on how limiting table styling was so we're happy that enhanced styling is now released with `v0.12.0`. If you're new to the wide world of table styling, check out the [*Get Started* guide on styling the table](https://posit-dev.github.io/great-tables/get-started/basic-styling.html) for primer on the subject. As ever, please let us know through [GitHub Issues](https://github.com/posit-dev/great-tables/issues) whether you ran into problems with any feature (new or old), or, if you have suggestions for further improvement! + +If you're new to the wide world of table styling, check out the [*Get Started* guide on styling the table](https://posit-dev.github.io/great-tables/get-started/basic-styling.html) for primer on the subject. As ever, please let us know through [GitHub Issues](https://github.com/posit-dev/great-tables/issues) whether you ran into problems with any feature (new or old), or, if you have suggestions for further improvement! From 8bbc7bda0e21fc9a0da7e3ffffaa4ce1b427067d Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 27 Sep 2024 19:48:32 -0400 Subject: [PATCH 104/150] Update index.qmd --- docs/blog/introduction-0.12.0/index.qmd | 31 ++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/blog/introduction-0.12.0/index.qmd b/docs/blog/introduction-0.12.0/index.qmd index 285181607..895759b20 100644 --- a/docs/blog/introduction-0.12.0/index.qmd +++ b/docs/blog/introduction-0.12.0/index.qmd @@ -72,9 +72,38 @@ Nice! And it's refreshing to see tables with fonts different from default set, a ### Striping rows in your table +Some people like having row striping (a.k.a. zebra stripes) in their display tables. We get that, and that's why we now have that option in the package. There are two ways to enable this: +1. invoking `opt_row_striping()` to quickly set row stripes in the table body +2. using some combination of three `row_striping_*` arguments in `tab_options()` -### Wrapping up +Let's use that example table with `opt_row_striping()`. + +```{python} +gt_tbl.opt_row_striping() +``` + +It's somewhat subtle but there is an alternating, slightly gray background (starting on the `"row_2"` row). The color is `#808080` but with an alpha (transparency) value of `0.05`. + +If this is not exactly what you want, there is an alternative to this. The `tab_options()` method has three new arguments: + +- `row_striping_background_color`: color to use for row striping +- `row_striping_include_stub`: should striping include cells in the stub? +- `row_striping_include_table_body`: should striping include cells in the body? + +With these new options, we can choose to stripe the *entire* row (stub cells + body cells) and use a darker color like `"lightblue"`. +```{python} +( + gt_tbl + .tab_options( + row_striping_background_color="lightblue", + row_striping_include_stub=True, + row_striping_include_table_body=True, + ) +) +``` + +### Wrapping up If you're new to the wide world of table styling, check out the [*Get Started* guide on styling the table](https://posit-dev.github.io/great-tables/get-started/basic-styling.html) for primer on the subject. As ever, please let us know through [GitHub Issues](https://github.com/posit-dev/great-tables/issues) whether you ran into problems with any feature (new or old), or, if you have suggestions for further improvement! From 2e5a9bfffdc354c2a6dbf35c0ce5749fcc107b9c Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Fri, 27 Sep 2024 20:13:37 -0400 Subject: [PATCH 105/150] Update index.qmd --- docs/blog/introduction-0.12.0/index.qmd | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/blog/introduction-0.12.0/index.qmd b/docs/blog/introduction-0.12.0/index.qmd index 895759b20..1ca8fe7ec 100644 --- a/docs/blog/introduction-0.12.0/index.qmd +++ b/docs/blog/introduction-0.12.0/index.qmd @@ -1,20 +1,20 @@ --- -title: "Great Tables `v0.12.0`: Styling all over the place (and so much more)" +title: "Great Tables `v0.12.0`: Google Fonts and zebra stripes" html-table-processing: none author: Rich Iannone -date: 2024-09-27 +date: 2024-09-30 freeze: true jupyter: python3 --- -In Great Tables `0.12.0` we added two features that provide more options for customizing the appearance of a table: we incorporated row striping as an option so that you can get that zebra stripe look in your table. In this post, we'll present a few examples in the following big features: +In Great Tables `0.12.0` we focused on adding options for customizing the appearance of a table. In this post, we'll present two new features: - using typefaces from Google Fonts via `tab_style()` or `opt_table_font()` - adding table striping via `tab_options()` and `opt_row_striping()` Let's have a look at how these new features can be used! -### Using fonts from *Google Fonts* +### Using fonts from Google Fonts Google Fonts is a free service that allows use of hosted typefaces in your own websites. In Great Tables, we added the `google_font()` helper function to easily incorporate such fonts in your tables. There are two ways to go about this: @@ -72,7 +72,7 @@ Nice! And it's refreshing to see tables with fonts different from default set, a ### Striping rows in your table -Some people like having row striping (a.k.a. zebra stripes) in their display tables. We get that, and that's why we now have that option in the package. There are two ways to enable this: +Some people like having row striping (a.k.a. zebra stripes) in their display tables. We also know that some [advise against the practice](https://www.darkhorseanalytics.com/blog/clear-off-the-table/). We understand it's a controversial table issue, however, we also want to give you the creative freedom to just include the stripes. To that end, we now have that option in the package. There are two ways to enable this look: 1. invoking `opt_row_striping()` to quickly set row stripes in the table body 2. using some combination of three `row_striping_*` arguments in `tab_options()` @@ -104,6 +104,8 @@ With these new options, we can choose to stripe the *entire* row (stub cells + b ) ``` +These alternating fills can be a good idea in some table display circumstances. Now, you can make that call and the functionality is there to support your decision. + ### Wrapping up If you're new to the wide world of table styling, check out the [*Get Started* guide on styling the table](https://posit-dev.github.io/great-tables/get-started/basic-styling.html) for primer on the subject. As ever, please let us know through [GitHub Issues](https://github.com/posit-dev/great-tables/issues) whether you ran into problems with any feature (new or old), or, if you have suggestions for further improvement! From a53ebdd94e60f620d326402161f1a85e578c61ce Mon Sep 17 00:00:00 2001 From: jrycw Date: Sat, 28 Sep 2024 19:27:09 +0800 Subject: [PATCH 106/150] Update `opt_table_font()` --- great_tables/_options.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/great_tables/_options.py b/great_tables/_options.py index 71757d533..e2d62101a 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, fields, replace -from typing import TYPE_CHECKING, ClassVar, cast +from typing import TYPE_CHECKING, ClassVar, cast, Iterable from great_tables import _utils from great_tables._helpers import FontStackName, GoogleFont, _intify_scaled_px, px @@ -1090,7 +1090,7 @@ def opt_table_outline( def opt_table_font( self: GTSelf, - font: str | list[str] | dict[str, str] | None = None, + font: str | list[str] | dict[str, str] | GoogleFont | None = None, stack: FontStackName | None = None, weight: str | int | float | None = None, style: str | None = None, @@ -1110,9 +1110,10 @@ def opt_table_font( Parameters ---------- font - One or more font names available on the user system. This can be a string or a list of - strings. The default value is `None` since you could instead opt to use `stack` to define - a list of fonts. + One or more font names available on the user's system. This can be provided as a string or + a list of strings. Alternatively, you can specify font names using the `google_font()` + helper function. The default value is `None` since you could instead opt to use `stack` to + define a list of fonts. stack A name that is representative of a font stack (obtained via internally via the `system_fonts()` helper function. If provided, this new stack will replace any defined fonts @@ -1227,8 +1228,16 @@ def opt_table_font( if font is not None: # If font is a string or GoogleFont object, convert to a list - if isinstance(font, str) or isinstance(font, GoogleFont): - font = [font] + if isinstance(font, (str, GoogleFont)): + font: list[str | GoogleFont] = [font] + + if not isinstance(font, Iterable): + # We need to raise an exception here. Otherwise, if the provided `font` is not iterable, + # the `for item in font` loop will raise a `TypeError` with a message stating that the + # object is not iterable. + raise TypeError( + "`font=` must be a string/GoogleFont object or a list of strings/GoogleFont objects." + ) new_font_list: list[str] = [] @@ -1251,9 +1260,11 @@ def opt_table_font( res = tab_options(res, table_additional_css=existing_additional_css) else: - raise TypeError("`font=` must be a string or a list of strings.") + raise TypeError( + "`font=` must be a string/GoogleFont object or a list of strings/GoogleFont objects." + ) - font = new_font_list + font: list[str] = new_font_list else: font = [] @@ -1278,7 +1289,7 @@ def opt_table_font( if weight is not None: - if isinstance(weight, int) or isinstance(weight, float): + if isinstance(weight, (int, float)): weight = str(round(weight)) From d6d346a96466fbd4982bd79c0524c25aeea269a3 Mon Sep 17 00:00:00 2001 From: jrycw Date: Sat, 28 Sep 2024 19:27:22 +0800 Subject: [PATCH 107/150] Add tests for `opt_table_font()` --- tests/test_options.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_options.py b/tests/test_options.py index b5320b7f5..fbafa1dad 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -390,6 +390,27 @@ def test_opt_table_font_raises(): assert "Either `font=` or `stack=` must be provided." in exc_info.value.args[0] +@pytest.mark.parametrize("font", [1, [1]]) +def test_opt_table_font_raises_font(font): + with pytest.raises(TypeError) as exc_info: + GT(exibble).opt_table_font(font=font) + + assert ( + "`font=` must be a string/GoogleFont object or a list of strings/GoogleFont objects." + in exc_info.value.args[0] + ) + + +def test_opt_table_font_raises_weight(): + with pytest.raises(TypeError) as exc_info: + GT(exibble).opt_table_font(stack="humanist", weight=(1, 2)) + + assert ( + "`weight=` must be a numeric value between 1 and 1000 or a text-based keyword." + in exc_info.value.args[0] + ) + + def test_opt_row_striping(): gt_tbl_0 = GT(exibble) From 8ae332623f951c9507f21f70d0add546de7f1310 Mon Sep 17 00:00:00 2001 From: jrycw Date: Sat, 28 Sep 2024 21:38:14 +0800 Subject: [PATCH 108/150] Update `__repr__()` for `GoogleFont`, `UnitStr` and `UnitDefinitionList` --- great_tables/_helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/great_tables/_helpers.py b/great_tables/_helpers.py index 3eeb8eb99..0e27c82fe 100644 --- a/great_tables/_helpers.py +++ b/great_tables/_helpers.py @@ -286,7 +286,7 @@ class GoogleFont: font: str def __repr__(self) -> str: - return f"GoogleFont({self.font})" + return f"{type(self).__name__}({self.font})" def make_import_stmt(self) -> str: return f"@import url('https://fonts.googleapis.com/css2?family={self.font.replace(' ', '+')}&display=swap');" @@ -837,7 +837,7 @@ def __init__(self, units_str: list[str | UnitDefinitionList]): self.units_str = units_str def __repr__(self) -> str: - return f"UnitStr({self.units_str})" + return f"{type(self).__name__}({self.units_str})" def to_html(self) -> str: @@ -890,7 +890,7 @@ class UnitDefinitionList: units_list: list[UnitDefinition] def __repr__(self) -> str: - return f"UnitDefinitionList({self.units_list})" + return f"{type(self).__name__}({self.units_list})" def __len__(self) -> int: return len(self.units_list) From 5b7298b680b2239aca755a26c53a26bf92240a9c Mon Sep 17 00:00:00 2001 From: jrycw Date: Sat, 28 Sep 2024 21:43:00 +0800 Subject: [PATCH 109/150] Update tests for `GoogleFont`, `UnitStr` and `UnitDefinitionList` --- tests/test_helpers.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 8e7828680..5b77db8b6 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -92,6 +92,7 @@ def test_google_font(): font.make_import_stmt() == "@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');" ) + assert str(font) == repr(font) == f"GoogleFont({font_name})" def test_google_font_class(): @@ -320,11 +321,19 @@ def test_unit_definition_class_construction(): def test_unit_definition_list_class_construction(): unit_def_list = UnitDefinitionList([UnitDefinition(token="m^2", unit="m", exponent="2")]) assert unit_def_list.units_list == [UnitDefinition(token="m^2", unit="m", exponent="2")] + assert ( + str(unit_def_list) + == repr(unit_def_list) + == "UnitDefinitionList([UnitDefinition(" + + "token='m^2', unit='m', unit_subscript=None, exponent='2', sub_super_overstrike=False, " + + "chemical_formula=False, built=None)])" + ) def test_unit_str_class_construction(): unit_str = UnitStr(["a b"]) assert unit_str.units_str == ["a b"] + assert len(unit_str) == 1 def test_unit_str_from_str_single_unit(): From f3c69c03e5bae329d7b8cd42fcb5e7fb02e97e52 Mon Sep 17 00:00:00 2001 From: jrycw Date: Sat, 28 Sep 2024 21:56:48 +0800 Subject: [PATCH 110/150] Update test for `UnitStr` --- tests/test_helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 5b77db8b6..d0ef6215f 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -333,6 +333,7 @@ def test_unit_definition_list_class_construction(): def test_unit_str_class_construction(): unit_str = UnitStr(["a b"]) assert unit_str.units_str == ["a b"] + assert str(unit_str) == repr(unit_str) == "UnitStr(['a b'])" assert len(unit_str) == 1 From dbea7f193f0281fb6ab37e4926fe260c84cdfc36 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 28 Sep 2024 11:29:28 -0400 Subject: [PATCH 111/150] Update index.qmd --- docs/blog/introduction-0.12.0/index.qmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/blog/introduction-0.12.0/index.qmd b/docs/blog/introduction-0.12.0/index.qmd index 1ca8fe7ec..f28826419 100644 --- a/docs/blog/introduction-0.12.0/index.qmd +++ b/docs/blog/introduction-0.12.0/index.qmd @@ -9,7 +9,7 @@ jupyter: python3 In Great Tables `0.12.0` we focused on adding options for customizing the appearance of a table. In this post, we'll present two new features: -- using typefaces from Google Fonts via `tab_style()` or `opt_table_font()` +- using typefaces from Google Fonts via `tab_style()` and `opt_table_font()` - adding table striping via `tab_options()` and `opt_row_striping()` Let's have a look at how these new features can be used! @@ -108,4 +108,4 @@ These alternating fills can be a good idea in some table display circumstances. ### Wrapping up -If you're new to the wide world of table styling, check out the [*Get Started* guide on styling the table](https://posit-dev.github.io/great-tables/get-started/basic-styling.html) for primer on the subject. As ever, please let us know through [GitHub Issues](https://github.com/posit-dev/great-tables/issues) whether you ran into problems with any feature (new or old), or, if you have suggestions for further improvement! +We are excited that this new functionality is now available in Great Tables. As ever, please let us know through [GitHub Issues](https://github.com/posit-dev/great-tables/issues) whether you ran into problems with any feature (new or old), or, if you have suggestions for further improvement! From 6ce209058168f7553c8667ed71ffec224de8d433 Mon Sep 17 00:00:00 2001 From: jrycw Date: Sun, 29 Sep 2024 12:08:03 +0800 Subject: [PATCH 112/150] Fix deprecated warning for `pl.DataFrame.pivot()` --- docs/blog/introduction-0.2.0/index.qmd | 2 +- docs/get-started/colorizing-with-data.qmd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/blog/introduction-0.2.0/index.qmd b/docs/blog/introduction-0.2.0/index.qmd index 981a0365e..83d6a0b14 100644 --- a/docs/blog/introduction-0.2.0/index.qmd +++ b/docs/blog/introduction-0.2.0/index.qmd @@ -59,7 +59,7 @@ wide_pops = ( pl.col("country_code_2").is_in(["FM", "GU", "KI", "MH", "MP", "NR", "PW"]) & pl.col("year").is_in([2000, 2010, 2020]) ) - .pivot(index="country_name", columns="year", values="population") + .pivot(index="country_name", on="year", values="population") .sort("2020", descending=True) ) diff --git a/docs/get-started/colorizing-with-data.qmd b/docs/get-started/colorizing-with-data.qmd index 1ccc726cb..446df635d 100644 --- a/docs/get-started/colorizing-with-data.qmd +++ b/docs/get-started/colorizing-with-data.qmd @@ -80,7 +80,7 @@ sza_pivot = ( .filter((pl.col("latitude") == "20") & (pl.col("tst") <= "1200")) .select(pl.col("*").exclude("latitude")) .drop_nulls() - .pivot(values="sza", index="month", columns="tst", sort_columns=True) + .pivot(values="sza", index="month", on="tst", sort_columns=True) ) ( From 4c07fbe68dab5c9f07c5a74771f252b6a3069543 Mon Sep 17 00:00:00 2001 From: jrycw Date: Sun, 29 Sep 2024 18:24:50 +0800 Subject: [PATCH 113/150] Fix global `locale` not being respected in `fmt_*()` functions --- great_tables/_formats.py | 48 ++++++++-------------------------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 317aaa029..d061162d4 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -279,11 +279,6 @@ def fmt_number( Take a look at the functional version of this method: [`val_fmt_number()`](`great_tables._formats_vals.val_fmt_number`). """ - - # Stop if `locale` does not have a valid value; normalize locale and resolve one - # that might be set globally - _validate_locale(locale=locale) - locale = _normalize_locale(locale=locale) locale = _resolve_locale(self, locale=locale) # Use locale-based marks if a locale ID is provided @@ -458,10 +453,7 @@ def fmt_integer( [`val_fmt_integer()`](`great_tables._formats_vals.val_fmt_integer`). """ - # Stop if `locale` does not have a valid value; normalize locale and resolve one - # that might be set globally - _validate_locale(locale=locale) - locale = _normalize_locale(locale=locale) + locale = _resolve_locale(self, locale=locale) # Use locale-based marks if a locale ID is provided sep_mark = _get_locale_sep_mark(default=sep_mark, use_seps=use_seps, locale=locale) @@ -665,10 +657,7 @@ def fmt_scientific( # large exponent values use_seps = True - # Stop if `locale` does not have a valid value; normalize locale and resolve one - # that might be set globally - _validate_locale(locale=locale) - locale = _normalize_locale(locale=locale) + locale = _resolve_locale(self, locale=locale) # Use locale-based marks if a locale ID is provided sep_mark = _get_locale_sep_mark(default=sep_mark, use_seps=use_seps, locale=locale) @@ -919,10 +908,7 @@ def fmt_percent( single numerical value (or a list of them). """ - # Stop if `locale` does not have a valid value; normalize locale and resolve one - # that might be set globally - _validate_locale(locale=locale) - locale = _normalize_locale(locale=locale) + locale = _resolve_locale(self, locale=locale) # Use locale-based marks if a locale ID is provided sep_mark = _get_locale_sep_mark(default=sep_mark, use_seps=use_seps, locale=locale) @@ -1143,10 +1129,7 @@ def fmt_currency( single numerical value (or a list of them). """ - # Stop if `locale` does not have a valid value; normalize locale and resolve one - # that might be set globally - _validate_locale(locale=locale) - locale = _normalize_locale(locale=locale) + locale = _resolve_locale(self, locale=locale) # Use locale-based marks if a locale ID is provided sep_mark = _get_locale_sep_mark(default=sep_mark, use_seps=use_seps, locale=locale) @@ -1483,10 +1466,7 @@ def fmt_bytes( numerical value (or a list of them). """ - # Stop if `locale` does not have a valid value; normalize locale and resolve one - # that might be set globally - _validate_locale(locale=locale) - locale = _normalize_locale(locale=locale) + locale = _resolve_locale(self, locale=locale) # Use locale-based marks if a locale ID is provided sep_mark = _get_locale_sep_mark(default=sep_mark, use_seps=use_seps, locale=locale) @@ -1692,10 +1672,7 @@ def fmt_date( numerical value (or a list of them). """ - # Stop if `locale` does not have a valid value; normalize locale and resolve one - # that might be set globally - _validate_locale(locale=locale) - locale = _normalize_locale(locale=locale) + locale = _resolve_locale(self, locale=locale) # Get the date format string based on the `date_style` value date_format_str = _get_date_format(date_style=date_style) @@ -1826,10 +1803,7 @@ def fmt_time( numerical value (or a list of them). """ - # Stop if `locale` does not have a valid value; normalize locale and resolve one - # that might be set globally - _validate_locale(locale=locale) - locale = _normalize_locale(locale=locale) + locale = _resolve_locale(self, locale=locale) # Get the time format string based on the `time_style` value time_format_str = _get_time_format(time_style=time_style) @@ -1976,10 +1950,7 @@ def fmt_datetime( ``` """ - # Stop if `locale` does not have a valid value; normalize locale and resolve one - # that might be set globally - _validate_locale(locale=locale) - locale = _normalize_locale(locale=locale) + locale = _resolve_locale(self, locale=locale) # Get the date format string based on the `date_style` value date_format_str = _get_date_format(date_style=date_style) @@ -2899,9 +2870,8 @@ def _resolve_locale(x: GTData, locale: str | None = None) -> str | None: # TODO: why do both the normalize and validate functions convert # underscores to hyphens? Should we remove from validate locale? - locale = _normalize_locale(locale=locale) - _validate_locale(locale=locale) + locale = _normalize_locale(locale=locale) return locale From 7b91e38037e726768f6305873e9950a308fd8d37 Mon Sep 17 00:00:00 2001 From: jrycw Date: Mon, 30 Sep 2024 02:32:33 +0800 Subject: [PATCH 114/150] Add tests for `GT.fmt_*()` with `locale` support --- tests/test_formats.py | 90 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/test_formats.py b/tests/test_formats.py index 88e7650e3..17941a3c6 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -2103,3 +2103,93 @@ def test_get_currency_str(): def test_get_currency_str_no_match_raises(): with pytest.raises(Exception): _get_currency_str("NOT_A_CURRENCY") + + +@pytest.mark.parametrize( + "src, fn", + [ + (1, "fmt_number"), + (2, "fmt_integer"), + (3, "fmt_scientific"), + (4, "fmt_percent"), + (5, "fmt_currency"), + (6, "fmt_bytes"), + ("2023-12-31", "fmt_date"), + ("12:34:56", "fmt_time"), + ("2023-12-31 12:34:56", "fmt_datetime"), + ], +) +def test_fmt_with_locale1(src, fn): + df = pd.DataFrame({"x": [src]}) + global_locale = local_locale = "en" + + # w/o global locale, w/o local locale => use default locale => "en" + gt1 = getattr(GT(df), fn)() + x1 = _get_column_of_values(gt1, column_name="x", context="html") + + # w global locale, w/o local locale => use global locale => "en" + gt2 = getattr(GT(df, locale=global_locale), fn)() + x2 = _get_column_of_values(gt2, column_name="x", context="html") + + # w/o global locale, w local locale => use local locale => "en" + gt3 = getattr(GT(df), fn)(locale=local_locale) + x3 = _get_column_of_values(gt3, column_name="x", context="html") + + assert x1 == x2 == x3 + + +@pytest.mark.parametrize( + "src, fn", + [ + (1, "fmt_number"), + (2, "fmt_integer"), + (3, "fmt_scientific"), + (4, "fmt_percent"), + (5, "fmt_currency"), + (6, "fmt_bytes"), + ("2023-12-31", "fmt_date"), + ("12:34:56", "fmt_time"), + ("2023-12-31 12:34:56", "fmt_datetime"), + ], +) +def test_fmt_with_locale2(src, fn): + df = pd.DataFrame({"x": [src]}) + global_locale = local_locale = "ja" + + # w global locale, w/o local locale => use global locale => "ja" + gt1 = getattr(GT(df, locale=global_locale), fn)() + x1 = _get_column_of_values(gt1, column_name="x", context="html") + + # w/o global locale, w local locale => use local locale => "ja" + gt2 = getattr(GT(df), fn)(locale=local_locale) + x2 = _get_column_of_values(gt2, column_name="x", context="html") + + assert x1 == x2 + + +@pytest.mark.parametrize( + "src, fn", + [ + (1, "fmt_number"), + (2, "fmt_integer"), + (3, "fmt_scientific"), + (4, "fmt_percent"), + (5, "fmt_currency"), + (6, "fmt_bytes"), + ("2023-12-31", "fmt_date"), + ("12:34:56", "fmt_time"), + ("2023-12-31 12:34:56", "fmt_datetime"), + ], +) +def test_fmt_with_locale3(src, fn): + df = pd.DataFrame({"x": [src]}) + global_locale, local_locale = "ja", "de" + + # w global locale, w local locale => use local locale => "de" + gt = getattr(GT(df, locale=global_locale), fn)(locale=local_locale) + x = _get_column_of_values(gt, column_name="x", context="html") + + gt_de = getattr(GT(df, locale="de"), fn)() + x_de = _get_column_of_values(gt_de, column_name="x", context="html") + + assert x == x_de From b0edc731c5cba4261d75bc293165e032f5ac7f23 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 13:10:33 -0400 Subject: [PATCH 115/150] Add docstring for LocSourceNotes --- great_tables/_locations.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 3274c2450..81aec501c 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -160,7 +160,40 @@ class LocFootnotes(Loc): @dataclass class LocSourceNotes(Loc): - """A location for targeting source notes.""" + """A location specification for targeting the source notes. + + The `loc.source_notes()` class is used to target the source notes in the table. The class can be + used to apply custom styling with the [`tab_style()`](`great_tables.GT.tab_style`) method. That + method has a `locations=` argument and this class should be used there to perform the targeting. + The 'source_notes' location is generated by + [`tab_source_note()`](`great_tables.GT.tab_source_note`). + + Returns + ------- + LocSourceNotes + A `LocSourceNotes` object, which is used for a `locations=` argument if specifying the + source notes. + + Examples + -------- + Let's use a subset of the [`gtcars`] dataset in a new table. Add a source note (with + [`tab_source_note()`](`great_tables.GT.tab_source_note`) and style the source notes section + inside [`tab_style()`](`great_tables.GT.tab_style`) with `locations=loc.source_notes()`. + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT(gtcars[["mfr", "model", "msrp"]].head(5)) + .tab_source_note(source_note="From edmunds.com") + .tab_style( + style=style.text(color="blue", size="small", weight="bold"), + locations=loc.source_notes() + ) + ) + ``` + """ # Utils ================================================================================ From 326e5237e21d154e0f0d7d3acb76ed0c29688497 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 13:35:55 -0400 Subject: [PATCH 116/150] Add `loc.source_note` to API reference --- docs/_quarto.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 55f19de95..1029c660c 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -146,6 +146,7 @@ quartodoc: specification of the styling properties to be applied to the targeted locations. contents: - loc.body + - loc.source_note - style.fill - style.text - style.borders From 3d3f708b0f11697a6997fc97a888d68f3b7c7632 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 13:41:33 -0400 Subject: [PATCH 117/150] Make correction to loc name --- docs/_quarto.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 1029c660c..8a4904d6f 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -146,7 +146,7 @@ quartodoc: specification of the styling properties to be applied to the targeted locations. contents: - loc.body - - loc.source_note + - loc.source_notes - style.fill - style.text - style.borders From a203fbc921390f814d15b050501ec28c6baa8b20 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 14:06:48 -0400 Subject: [PATCH 118/150] Revise text in some Loc* docstrings --- great_tables/_locations.py | 50 +++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 81aec501c..c4c36f093 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -114,9 +114,9 @@ class LocBody(Loc): # TODO: these can be tidyselectors """A location specification for targeting data cells in the table body. - The `loc.body()` class is used to target the data cells in the table body. The class can be used - to apply custom styling with the `tab_style()` method. That method has a `locations` argument - and this class should be used there to perform the targeting. + With `loc.body()`, we can target the data cells in the table body. This is useful for applying + custom styling with the [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a + `locations=` argument and this class should be used there to perform the targeting. Parameters ---------- @@ -130,7 +130,7 @@ class LocBody(Loc): Returns ------- LocBody - A LocBody object, which is used for a `locations` argument if specifying the table body. + A LocBody object, which is used for a `locations=` argument if specifying the table body. Examples ------ @@ -150,7 +150,39 @@ class LocSummary(Loc): @dataclass class LocFooter(Loc): - """A location for targeting the footer.""" + """A location specification for targeting the table footer. + + With `loc.footer()` we can target the table's footer. This is useful when applying custom + styling with the [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a + `locations=` argument and this class should be used there to perform the targeting. The 'footer' + location is generated by [`tab_source_note()`](`great_tables.GT.tab_source_note`). + + Returns + ------- + LocFooter + A `LocFooter` object, which is used for a `locations=` argument if specifying the footer of + the table. + + Examples + -------- + Let's use a subset of the [`gtcars`] dataset in a new table. Add a source note (with + [`tab_source_note()`](`great_tables.GT.tab_source_note`) and style this footer section inside of + [`tab_style()`](`great_tables.GT.tab_style`) with `locations=loc.footer()`. + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT(gtcars[["mfr", "model", "msrp"]].head(5)) + .tab_source_note(source_note="From edmunds.com") + .tab_style( + style=style.text(color="blue", size="small", weight="bold"), + locations=loc.footer() + ) + ) + ``` + """ @dataclass @@ -162,10 +194,10 @@ class LocFootnotes(Loc): class LocSourceNotes(Loc): """A location specification for targeting the source notes. - The `loc.source_notes()` class is used to target the source notes in the table. The class can be - used to apply custom styling with the [`tab_style()`](`great_tables.GT.tab_style`) method. That - method has a `locations=` argument and this class should be used there to perform the targeting. - The 'source_notes' location is generated by + With `loc.source_notes()`, we can target the source notes in the table. This is useful when + applying custom with the [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a + `locations=` argument and this class should be used there to perform the targeting. The + 'source_notes' location is generated by [`tab_source_note()`](`great_tables.GT.tab_source_note`). Returns From 6f7559f7e061df9a192ef7190ca8215792a914eb Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 14:17:20 -0400 Subject: [PATCH 119/150] Add docstring for LocColumnLabels --- great_tables/_locations.py | 44 +++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index c4c36f093..1c3547ebe 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -82,6 +82,44 @@ class LocColumnHeader(Loc): @dataclass class LocColumnLabels(Loc): + """A location specification for targeting column labels. + + With `loc.column_labels()`, we can target the cells containing the column labels. This is useful + for applying custom styling with the [`tab_style()`](`great_tables.GT.tab_style`) method. That + method has a `locations=` argument and this class should be used there to perform the targeting. + + Parameters + ---------- + columns + The columns to target. Can either be a single column name or a series of column names + provided in a list. If no columns are specified, all columns are targeted. + + Returns + ------- + LocBody + A LocBody object, which is used for a `locations=` argument if specifying the table body. + + Examples + -------- + Let's use a subset of the `gtcars` dataset in a new table. We will style all three of the column + labels by using `locations=loc.column_labels()` within + [`tab_style()`](`great_tables.GT.tab_style`). Note that no specification of `columns=` is needed + here because we want to target all columns. + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT(gtcars[["mfr", "model", "msrp"]].head(5)) + .tab_style( + style=style.text(color="blue", size="large", weight="bold"), + locations=loc.column_labels() + ) + ) + ``` + """ + columns: SelectExpr = None @@ -133,7 +171,7 @@ class LocBody(Loc): A LocBody object, which is used for a `locations=` argument if specifying the table body. Examples - ------ + -------- See [`GT.tab_style()`](`great_tables.GT.tab_style`). """ @@ -165,7 +203,7 @@ class LocFooter(Loc): Examples -------- - Let's use a subset of the [`gtcars`] dataset in a new table. Add a source note (with + Let's use a subset of the `gtcars` dataset in a new table. Add a source note (with [`tab_source_note()`](`great_tables.GT.tab_source_note`) and style this footer section inside of [`tab_style()`](`great_tables.GT.tab_style`) with `locations=loc.footer()`. @@ -208,7 +246,7 @@ class LocSourceNotes(Loc): Examples -------- - Let's use a subset of the [`gtcars`] dataset in a new table. Add a source note (with + Let's use a subset of the `gtcars` dataset in a new table. Add a source note (with [`tab_source_note()`](`great_tables.GT.tab_source_note`) and style the source notes section inside [`tab_style()`](`great_tables.GT.tab_style`) with `locations=loc.source_notes()`. From 87f74072a50513c2e4509fe11b429e7d71abbef0 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 14:17:49 -0400 Subject: [PATCH 120/150] Add loc.column_labels to API reference --- docs/_quarto.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 8a4904d6f..d039feee7 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -146,6 +146,7 @@ quartodoc: specification of the styling properties to be applied to the targeted locations. contents: - loc.body + - loc.column_labels - loc.source_notes - style.fill - style.text From e5255a938e66124c7a6190cb8d9aff768d68cfa1 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 14:18:11 -0400 Subject: [PATCH 121/150] Add loc.footer to API reference --- docs/_quarto.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index d039feee7..dc41d261d 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -147,6 +147,7 @@ quartodoc: contents: - loc.body - loc.column_labels + - loc.footer - loc.source_notes - style.fill - style.text From 9a9c3d6068e8ce8b55d9316eea370814563f8ae3 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 14:37:26 -0400 Subject: [PATCH 122/150] Make correction to 'Returns' section in docstring --- great_tables/_locations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 1c3547ebe..5640e6177 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -167,8 +167,9 @@ class LocBody(Loc): Returns ------- - LocBody - A LocBody object, which is used for a `locations=` argument if specifying the table body. + LocColumnLabels + A LocColumnLabels object, which is used for a `locations=` argument if specifying the + table's column labels. Examples -------- From c4c281f8ed407ceed60c5f4c17248338831ec68b Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 14:37:40 -0400 Subject: [PATCH 123/150] Add docstring for the LocTitle class --- docs/_quarto.yml | 1 + great_tables/_locations.py | 48 +++++++++++++++++++++++++++++++++----- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index dc41d261d..62107ead1 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -146,6 +146,7 @@ quartodoc: specification of the styling properties to be applied to the targeted locations. contents: - loc.body + - loc.title - loc.column_labels - loc.footer - loc.source_notes diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 5640e6177..5c9476587 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -57,7 +57,43 @@ class LocHeader(Loc): @dataclass class LocTitle(Loc): - """A location for targeting the title.""" + """A location specification for targeting the table title. + + With `loc.title()`, we can target the part of table containing the title (within the table + header). This is useful for applying custom styling with the + [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a `locations=` argument and + this class should be used there to perform the targeting. + + Returns + ------- + LocTitle + A LocTitle object, which is used for a `locations=` argument if specifying the title of the + table. + + Examples + -------- + Let's use a subset of the `gtcars` dataset in a new table. We will style only the 'title' part + of the table header (leaving the 'subtitle' part unaffected). This can be done by using + `locations=loc.title()` within [`tab_style()`](`great_tables.GT.tab_style`). + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT(gtcars[["mfr", "model", "msrp"]].head(5)) + .tab_header( + title="Select Cars from the gtcars Dataset", + subtitle="Only the first five cars are displayed" + ) + .tab_style( + style=style.text(color="blue", size="large", weight="bold"), + locations=loc.title() + ) + .fmt_currency(columns="msrp", decimals=0) + ) + ``` + """ @dataclass @@ -96,8 +132,9 @@ class LocColumnLabels(Loc): Returns ------- - LocBody - A LocBody object, which is used for a `locations=` argument if specifying the table body. + LocColumnLabels + A LocColumnLabels object, which is used for a `locations=` argument if specifying the + table's column labels. Examples -------- @@ -167,9 +204,8 @@ class LocBody(Loc): Returns ------- - LocColumnLabels - A LocColumnLabels object, which is used for a `locations=` argument if specifying the - table's column labels. + LocBody + A LocBody object, which is used for a `locations=` argument if specifying the table body. Examples -------- From 617ac1010b85a1360390889d3a58c72c0d3c66fa Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 14:49:35 -0400 Subject: [PATCH 124/150] Add docstring for LocSubTitle class --- docs/_quarto.yml | 1 + great_tables/_locations.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 62107ead1..2e5f88731 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -147,6 +147,7 @@ quartodoc: contents: - loc.body - loc.title + - loc.subtitle - loc.column_labels - loc.footer - loc.source_notes diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 5c9476587..96ca98968 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -98,7 +98,43 @@ class LocTitle(Loc): @dataclass class LocSubTitle(Loc): - """A location for targeting the subtitle.""" + """A location specification for targeting the table subtitle. + + With `loc.subtitle()`, we can target the part of table containing the subtitle (within the table + header). This is useful for applying custom styling with the + [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a `locations=` argument and + this class should be used there to perform the targeting. + + Returns + ------- + LocSubTitle + A LocSubTitle object, which is used for a `locations=` argument if specifying the subtitle + of the table. + + Examples + -------- + Let's use a subset of the `gtcars` dataset in a new table. We will style only the 'subtitle' + part of the table header (leaving the 'title' part unaffected). This can be done by using + `locations=loc.subtitle()` within [`tab_style()`](`great_tables.GT.tab_style`). + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT(gtcars[["mfr", "model", "msrp"]].head(5)) + .tab_header( + title="Select Cars from the gtcars Dataset", + subtitle="Only the first five cars are displayed" + ) + .tab_style( + style=style.fill(color="lightblue"), + locations=loc.subtitle() + ) + .fmt_currency(columns="msrp", decimals=0) + ) + ``` + """ @dataclass From b807c1427b3455e589f9b491ca11358690e661e5 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 15:05:27 -0400 Subject: [PATCH 125/150] Add docstring for LocHeader class --- docs/_quarto.yml | 1 + great_tables/_locations.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 2e5f88731..cb2a40650 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -148,6 +148,7 @@ quartodoc: - loc.body - loc.title - loc.subtitle + - loc.header - loc.column_labels - loc.footer - loc.source_notes diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 96ca98968..ae9eeef23 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -52,7 +52,43 @@ class Loc: @dataclass class LocHeader(Loc): - """A location for targeting the table title and subtitle.""" + """A location specification for targeting the table header (title and subtitle). + + With `loc.header()`, we can target the table header which contains the title and the subtitle. + This is useful for applying custom styling with the + [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a `locations=` argument and + this class should be used there to perform the targeting. + + Returns + ------- + LocHeader + A LocHeader object, which is used for a `locations=` argument if specifying the title of the + table. + + Examples + -------- + Let's use a subset of the `gtcars` dataset in a new table. We will style the entire table header + (the 'title' and 'subtitle' parts. This can be done by using `locations=loc.header()` within + [`tab_style()`](`great_tables.GT.tab_style`). + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT(gtcars[["mfr", "model", "msrp"]].head(5)) + .tab_header( + title="Select Cars from the gtcars Dataset", + subtitle="Only the first five cars are displayed" + ) + .tab_style( + style=style.fill(color="lightblue"), + locations=loc.header() + ) + .fmt_currency(columns="msrp", decimals=0) + ) + ``` + """ @dataclass From 1d5a414c3336070e354416dc17b7a01343d00237 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Mon, 30 Sep 2024 16:18:46 -0400 Subject: [PATCH 126/150] docs: do not document GT members inline on its reference page --- docs/_quarto.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 55f19de95..418e901fd 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -82,7 +82,9 @@ quartodoc: class, we supply the input data table and some basic options for creating a stub and row groups (with the `rowname_col=` and `groupname_col=` arguments). contents: - - GT + - name: GT + members: [] + - title: Creating or modifying parts of a table desc: > A table can contain a few useful components for conveying additional information. These From a407a98278df67462af90c1633ced781973dca7b Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Mon, 30 Sep 2024 16:23:25 -0400 Subject: [PATCH 127/150] docs: link to method docs inline on GT reference page --- docs/_quarto.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 418e901fd..a2dfd5ff7 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -83,7 +83,8 @@ quartodoc: groups (with the `rowname_col=` and `groupname_col=` arguments). contents: - name: GT - members: [] + children: linked + - title: Creating or modifying parts of a table desc: > From 871ca1ca749a1e6c0d97f20907f8be73b07d136c Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Mon, 30 Sep 2024 16:35:08 -0400 Subject: [PATCH 128/150] ci: use latest quartodoc commit for now --- .github/workflows/ci-docs.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-docs.yaml b/.github/workflows/ci-docs.yaml index a48bf5437..f4c9cf153 100644 --- a/.github/workflows/ci-docs.yaml +++ b/.github/workflows/ci-docs.yaml @@ -2,7 +2,7 @@ on: push: branches: - main - - 'docs-preview-**' + - "docs-preview-**" pull_request: branches: - main @@ -20,6 +20,7 @@ jobs: - name: Install dependencies run: | python -m pip install ".[all]" + pip install git+https://github.com/machow/quartodoc.git@a98bf47004a76578e6893b4ba6d9447322137a3c - uses: quarto-dev/quarto-actions/setup@v2 - name: Build docs run: | From b89d94ec9ba3048ba1044b121c44b0399d083d7d Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Mon, 30 Sep 2024 16:35:34 -0400 Subject: [PATCH 129/150] chore: format _quarto.yml --- docs/_quarto.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index a2dfd5ff7..c0e3b385f 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -84,8 +84,8 @@ quartodoc: contents: - name: GT children: linked - - + + - title: Creating or modifying parts of a table desc: > A table can contain a few useful components for conveying additional information. These From a453212339b8959e38aba1fd5f75a378baede80f Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 17:05:46 -0400 Subject: [PATCH 130/150] Add docstring for LocSpannerLabels class --- docs/_quarto.yml | 1 + great_tables/_locations.py | 51 +++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index cb2a40650..8d6d629a8 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -150,6 +150,7 @@ quartodoc: - loc.subtitle - loc.header - loc.column_labels + - loc.spanner_labels - loc.footer - loc.source_notes - style.fill diff --git a/great_tables/_locations.py b/great_tables/_locations.py index ae9eeef23..b21672bf3 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -234,7 +234,56 @@ class LocColumnLabels(Loc): @dataclass class LocSpannerLabels(Loc): - """A location for column spanners.""" + """A location specification for targeting spanner labels. + + With `loc.spanner_labels()`, we can target the cells containing the spanner labels. This is + useful for applying custom styling with the [`tab_style()`](`great_tables.GT.tab_style`) method. + That method has a `locations=` argument and this class should be used there to perform the + targeting. + + Parameters + ---------- + ids: + The ID values for the spanner labels to target. A list of one or more ID values is required. + + Returns + ------- + LocSpannerLabels + A LocSpannerLabels object, which is used for a `locations=` argument if specifying the + table's spanner labels. + + Examples + -------- + Let's use a subset of the `gtcars` dataset in a new table. We create two spanner labels through + two separate calls of the [`tab_spanner()`](`great_tables.GT.tab_spanner`) method. In each of + those, the text supplied to `label=` argument is used as the ID value (though they have be + explicitly set via the `id=` argument). We will style only the spanner label with the text + `"performance"` by using `locations=loc.spanner_labels(ids=["performance"])` within + [`tab_style()`](`great_tables.GT.tab_style`). + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT(gtcars[["mfr", "model", "hp", "trq", "msrp"]].head(5)) + .tab_spanner( + label="performance", + columns=["hp", "trq"] + ) + .tab_spanner( + label="make and model", + columns=["mfr", "model"] + ) + .tab_style( + style=style.text(color="blue", weight="bold"), + locations=loc.spanner_labels(ids=["performance"]) + ) + .fmt_integer(columns=["hp", "trq"]) + .fmt_currency(columns="msrp", decimals=0) + ) + ``` + """ ids: SelectExpr = None From d1a357c01c8c7d876d759a9a5510fb546fd91484 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 30 Sep 2024 17:16:45 -0400 Subject: [PATCH 131/150] Make adjustments to text introducing example --- great_tables/_locations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index b21672bf3..8d7348e18 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -256,8 +256,8 @@ class LocSpannerLabels(Loc): -------- Let's use a subset of the `gtcars` dataset in a new table. We create two spanner labels through two separate calls of the [`tab_spanner()`](`great_tables.GT.tab_spanner`) method. In each of - those, the text supplied to `label=` argument is used as the ID value (though they have be - explicitly set via the `id=` argument). We will style only the spanner label with the text + those, the text supplied to `label=` argument is used as the ID value (though they have to be + explicitly set via the `id=` argument). We will style only the spanner label having the text `"performance"` by using `locations=loc.spanner_labels(ids=["performance"])` within [`tab_style()`](`great_tables.GT.tab_style`). From cc53a163c18df8dfb4befc3bb95949befa84409a Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 1 Oct 2024 09:32:22 -0400 Subject: [PATCH 132/150] Add docstring for LocColumnHeader --- docs/_quarto.yml | 1 + great_tables/_locations.py | 50 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 8d6d629a8..d689d678b 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -151,6 +151,7 @@ quartodoc: - loc.header - loc.column_labels - loc.spanner_labels + - loc.column_header - loc.footer - loc.source_notes - style.fill diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 8d7348e18..96ce186c2 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -180,12 +180,58 @@ class LocStubhead(Loc): @dataclass class LocStubheadLabel(Loc): - """A location for targetting the stubhead.""" + """A location specification for targetting the stubhead.""" @dataclass class LocColumnHeader(Loc): - """A location for column spanners and column labels.""" + """A location specification for column spanners and column labels. + + With `loc.column_header()`, we can target the column header which contains all of the column + labels and any spanner labels that are present. This is useful for applying custom styling with + the [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a `locations=` argument + and this class should be used there to perform the targeting. + + Returns + ------- + LocColumnHeader + A LocColumnHeader object, which is used for a `locations=` argument if specifying the column + header of the table. + + Examples + -------- + Let's use a subset of the `gtcars` dataset in a new table. We create spanner labels through + use of the [`tab_spanner()`](`great_tables.GT.tab_spanner`) method; this gives us a column + header with a mix of column labels and spanner labels. We will style the entire column header at + once by using `locations=loc.column_header()` within + [`tab_style()`](`great_tables.GT.tab_style`). + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT(gtcars[["mfr", "model", "hp", "trq", "msrp"]].head(5)) + .tab_spanner( + label="performance", + columns=["hp", "trq"] + ) + .tab_spanner( + label="make and model", + columns=["mfr", "model"] + ) + .tab_style( + style=[ + style.text(color="white", weight="bold"), + style.fill(color="steelblue") + ], + locations=loc.column_header() + ) + .fmt_integer(columns=["hp", "trq"]) + .fmt_currency(columns="msrp", decimals=0) + ) + ``` + """ @dataclass From 164cea6d83a90b5f076ba10db0bfaf182aebd348 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 1 Oct 2024 09:49:29 -0400 Subject: [PATCH 133/150] Add docstring for LocStubheadLabel --- docs/_quarto.yml | 1 + great_tables/_locations.py | 41 +++++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index f275c1e9d..fb29bfd18 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -155,6 +155,7 @@ quartodoc: - loc.column_labels - loc.spanner_labels - loc.column_header + - loc.stubhead - loc.footer - loc.source_notes - style.fill diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 96ce186c2..922d6b569 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -180,7 +180,46 @@ class LocStubhead(Loc): @dataclass class LocStubheadLabel(Loc): - """A location specification for targetting the stubhead.""" + """A location specification for targeting the stubhead. + + With `loc.stubhead_label()`, we can target the part of table that resides both at the top of the + stub and also beside the column header. This is useful for applying custom styling with the + [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a `locations=` argument and + this class should be used there to perform the targeting. + + Returns + ------- + LocStubheadLabel + A LocStubheadLabel object, which is used for a `locations=` argument if specifying the + stubhead of the table. + + Examples + -------- + Let's use a subset of the `gtcars` dataset in a new table. This table contains a stub (produced + by setting `rowname_col="model"` in the initial `GT()` call). The stubhead is given a label by + way of the [`tab_stubhead()`](`great_tables.GT.tab_stubhead`) method and this label can be + styled by using `locations=loc.stubhead()` within [`tab_style()`](`great_tables.GT.tab_style`). + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT( + gtcars[["mfr", "model", "hp", "trq", "msrp"]].head(5), + rowname_col="model", + groupname_col="mfr" + ) + .tab_stubhead(label="car") + .tab_style( + style=style.text(color="red", weight="bold"), + locations=loc.stubhead() + ) + .fmt_integer(columns=["hp", "trq"]) + .fmt_currency(columns="msrp", decimals=0) + ) + ``` + """ @dataclass From 0e91b1dfe217cfbf30efdad07788dbc5fba60004 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 1 Oct 2024 10:21:52 -0400 Subject: [PATCH 134/150] Document LocStubhead instead of LocStubheadLabel --- great_tables/_locations.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 922d6b569..903217785 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -175,23 +175,18 @@ class LocSubTitle(Loc): @dataclass class LocStubhead(Loc): - """A location for targeting the table stubhead and stubhead label.""" - - -@dataclass -class LocStubheadLabel(Loc): """A location specification for targeting the stubhead. - With `loc.stubhead_label()`, we can target the part of table that resides both at the top of the + With `loc.stubhead()`, we can target the part of table that resides both at the top of the stub and also beside the column header. This is useful for applying custom styling with the [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a `locations=` argument and this class should be used there to perform the targeting. Returns ------- - LocStubheadLabel - A LocStubheadLabel object, which is used for a `locations=` argument if specifying the - stubhead of the table. + LocStubhead + A LocStubhead object, which is used for a `locations=` argument if specifying the stubhead + of the table. Examples -------- @@ -222,6 +217,11 @@ class LocStubheadLabel(Loc): """ +@dataclass +class LocStubheadLabel(Loc): + """A location for targeting the stubhead label""" + + @dataclass class LocColumnHeader(Loc): """A location specification for column spanners and column labels. From 4f14979ba592af857e58eee158e5826ed3dff509 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 1 Oct 2024 10:28:28 -0400 Subject: [PATCH 135/150] Add docstring for LocStub class --- docs/_quarto.yml | 1 + great_tables/_locations.py | 48 +++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index fb29bfd18..a42ef00de 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -156,6 +156,7 @@ quartodoc: - loc.spanner_labels - loc.column_header - loc.stubhead + - loc.stub - loc.footer - loc.source_notes - style.fill diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 903217785..e549af3ea 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -375,7 +375,53 @@ class LocSpannerLabels(Loc): @dataclass class LocStub(Loc): - """A location for targeting the table stub, row group labels, summary labels, and body.""" + """A location specification for targeting rows of the table stub. + + With `loc.stub()` we can target the cells containing the row labels, which reside in the table + stub. This is useful for applying custom styling with the + [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a `locations=` argument and + this class should be used there to perform the targeting. + + Parameters + ---------- + rows + The rows to target within the stub. Can either be a single row name or a series of row names + provided in a list. If no rows are specified, all rows are targeted. + + Returns + ------- + LocStub + A LocStub object, which is used for a `locations=` argument if specifying the table's stub. + + Examples + -------- + Let's use a subset of the `gtcars` dataset in a new table. We will style the entire table stub + (the row labels) by using `locations=loc.stub()` within + [`tab_style()`](`great_tables.GT.tab_style`). + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT( + gtcars[["mfr", "model", "hp", "trq", "msrp"]].head(5), + rowname_col="model", + groupname_col="mfr" + ) + .tab_stubhead(label="car") + .tab_style( + style=[ + style.text(color="crimson", weight="bold"), + style.fill(color="lightgray") + ], + locations=loc.stub() + ) + .fmt_integer(columns=["hp", "trq"]) + .fmt_currency(columns="msrp", decimals=0) + ) + ``` + """ rows: RowSelectExpr = None From 8c82aefa63db1bef74c60dad56e6525b1a0d33f6 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 1 Oct 2024 10:36:02 -0400 Subject: [PATCH 136/150] Add docstring for LocRowGroups class --- docs/_quarto.yml | 1 + great_tables/_locations.py | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index a42ef00de..605c95e45 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -157,6 +157,7 @@ quartodoc: - loc.column_header - loc.stubhead - loc.stub + - loc.row_groups - loc.footer - loc.source_notes - style.fill diff --git a/great_tables/_locations.py b/great_tables/_locations.py index e549af3ea..d9923e1c8 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -428,6 +428,55 @@ class LocStub(Loc): @dataclass class LocRowGroups(Loc): + """A location specification for targeting row groups. + + With `loc.row_groups()` we can target the cells containing the row group labels, which span + across the table body. This is useful for applying custom styling with the + [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a `locations=` argument and + this class should be used there to perform the targeting. + + Parameters + ---------- + rows + The row groups to target. Can either be a single group name or a series of group names + provided in a list. If no groups are specified, all are targeted. + + Returns + ------- + LocRowGroups + A LocRowGroups object, which is used for a `locations=` argument if specifying the table's + row groups. + + Examples + -------- + Let's use a subset of the `gtcars` dataset in a new table. We will style all of the cells + comprising the row group labels by using `locations=loc.row_groups()` within + [`tab_style()`](`great_tables.GT.tab_style`). + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT( + gtcars[["mfr", "model", "hp", "trq", "msrp"]].head(5), + rowname_col="model", + groupname_col="mfr" + ) + .tab_stubhead(label="car") + .tab_style( + style=[ + style.text(color="crimson", weight="bold"), + style.fill(color="lightgray") + ], + locations=loc.row_groups() + ) + .fmt_integer(columns=["hp", "trq"]) + .fmt_currency(columns="msrp", decimals=0) + ) + ``` + """ + rows: RowSelectExpr = None From c1fcebe5b5029fb25a8dbb247e8a1bb58891b2fe Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 1 Oct 2024 10:41:33 -0400 Subject: [PATCH 137/150] Add example to LocBody docstring --- great_tables/_locations.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index d9923e1c8..05373254f 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -510,7 +510,31 @@ class LocBody(Loc): Examples -------- - See [`GT.tab_style()`](`great_tables.GT.tab_style`). + Let's use a subset of the `gtcars` dataset in a new table. We will style all of the body cells + by using `locations=loc.body()` within [`tab_style()`](`great_tables.GT.tab_style`). + + ```{python} + from great_tables import GT, style, loc + from great_tables.data import gtcars + + ( + GT( + gtcars[["mfr", "model", "hp", "trq", "msrp"]].head(5), + rowname_col="model", + groupname_col="mfr" + ) + .tab_stubhead(label="car") + .tab_style( + style=[ + style.text(color="darkblue", weight="bold"), + style.fill(color="gainsboro") + ], + locations=loc.body() + ) + .fmt_integer(columns=["hp", "trq"]) + .fmt_currency(columns="msrp", decimals=0) + ) + ``` """ columns: SelectExpr = None From 606cf68943f44efb2519953ee20788e3f326be78 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 1 Oct 2024 10:46:04 -0400 Subject: [PATCH 138/150] Reorder loc methods in API reference --- docs/_quarto.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 605c95e45..98be15cc1 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -148,16 +148,16 @@ quartodoc: [`tab_style()`](`great_tables.GT.tab_style`) method). The styling classes allow for the specification of the styling properties to be applied to the targeted locations. contents: - - loc.body + - loc.header - loc.title - loc.subtitle - - loc.header - - loc.column_labels - - loc.spanner_labels - - loc.column_header - loc.stubhead + - loc.column_header + - loc.spanner_labels + - loc.column_labels - loc.stub - loc.row_groups + - loc.body - loc.footer - loc.source_notes - style.fill From 236a6d2a4c505fc1ccf9bbe3667ec61a0eee3f4c Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 1 Oct 2024 11:05:34 -0400 Subject: [PATCH 139/150] Add note about future 'footnotes' loc in LocFooter --- great_tables/_locations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 05373254f..b9fb2472a 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -552,7 +552,8 @@ class LocSummary(Loc): class LocFooter(Loc): """A location specification for targeting the table footer. - With `loc.footer()` we can target the table's footer. This is useful when applying custom + With `loc.footer()` we can target the table's footer, which currently contains the source notes + (and may contain a 'footnotes' location in the future). This is useful when applying custom styling with the [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a `locations=` argument and this class should be used there to perform the targeting. The 'footer' location is generated by [`tab_source_note()`](`great_tables.GT.tab_source_note`). From 1de813d905991be27b7c2fee322e45e40ae1f738 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Tue, 1 Oct 2024 14:06:56 -0400 Subject: [PATCH 140/150] ci: update quartodoc commit used --- .github/workflows/ci-docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-docs.yaml b/.github/workflows/ci-docs.yaml index f4c9cf153..3dbb38bc1 100644 --- a/.github/workflows/ci-docs.yaml +++ b/.github/workflows/ci-docs.yaml @@ -20,7 +20,7 @@ jobs: - name: Install dependencies run: | python -m pip install ".[all]" - pip install git+https://github.com/machow/quartodoc.git@a98bf47004a76578e6893b4ba6d9447322137a3c + pip install git+https://github.com/machow/quartodoc.git@5ed1430d4dc0e75414d0b11532ecd2393965b8d2 - uses: quarto-dev/quarto-actions/setup@v2 - name: Build docs run: | From e9fc9e38be997d061ff88b2bb6837a15b5949176 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 1 Oct 2024 15:11:09 -0400 Subject: [PATCH 141/150] Tweak docstring titles for Loc* classes --- great_tables/_locations.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index b9fb2472a..11c52d40f 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -52,7 +52,7 @@ class Loc: @dataclass class LocHeader(Loc): - """A location specification for targeting the table header (title and subtitle). + """Target the table header (title and subtitle). With `loc.header()`, we can target the table header which contains the title and the subtitle. This is useful for applying custom styling with the @@ -93,7 +93,7 @@ class LocHeader(Loc): @dataclass class LocTitle(Loc): - """A location specification for targeting the table title. + """Target the table title. With `loc.title()`, we can target the part of table containing the title (within the table header). This is useful for applying custom styling with the @@ -134,7 +134,7 @@ class LocTitle(Loc): @dataclass class LocSubTitle(Loc): - """A location specification for targeting the table subtitle. + """Target the table subtitle. With `loc.subtitle()`, we can target the part of table containing the subtitle (within the table header). This is useful for applying custom styling with the @@ -175,7 +175,7 @@ class LocSubTitle(Loc): @dataclass class LocStubhead(Loc): - """A location specification for targeting the stubhead. + """Target the stubhead. With `loc.stubhead()`, we can target the part of table that resides both at the top of the stub and also beside the column header. This is useful for applying custom styling with the @@ -219,12 +219,12 @@ class LocStubhead(Loc): @dataclass class LocStubheadLabel(Loc): - """A location for targeting the stubhead label""" + """Target the stubhead label.""" @dataclass class LocColumnHeader(Loc): - """A location specification for column spanners and column labels. + """Target column spanners and column labels. With `loc.column_header()`, we can target the column header which contains all of the column labels and any spanner labels that are present. This is useful for applying custom styling with @@ -275,7 +275,7 @@ class LocColumnHeader(Loc): @dataclass class LocColumnLabels(Loc): - """A location specification for targeting column labels. + """Target column labels. With `loc.column_labels()`, we can target the cells containing the column labels. This is useful for applying custom styling with the [`tab_style()`](`great_tables.GT.tab_style`) method. That @@ -319,7 +319,7 @@ class LocColumnLabels(Loc): @dataclass class LocSpannerLabels(Loc): - """A location specification for targeting spanner labels. + """Target spanner labels. With `loc.spanner_labels()`, we can target the cells containing the spanner labels. This is useful for applying custom styling with the [`tab_style()`](`great_tables.GT.tab_style`) method. @@ -375,7 +375,7 @@ class LocSpannerLabels(Loc): @dataclass class LocStub(Loc): - """A location specification for targeting rows of the table stub. + """Target the table stub. With `loc.stub()` we can target the cells containing the row labels, which reside in the table stub. This is useful for applying custom styling with the @@ -428,7 +428,7 @@ class LocStub(Loc): @dataclass class LocRowGroups(Loc): - """A location specification for targeting row groups. + """Target row groups. With `loc.row_groups()` we can target the cells containing the row group labels, which span across the table body. This is useful for applying custom styling with the @@ -488,7 +488,7 @@ class LocSummaryLabel(Loc): @dataclass class LocBody(Loc): # TODO: these can be tidyselectors - """A location specification for targeting data cells in the table body. + """Target data cells in the table body. With `loc.body()`, we can target the data cells in the table body. This is useful for applying custom styling with the [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a @@ -550,7 +550,7 @@ class LocSummary(Loc): @dataclass class LocFooter(Loc): - """A location specification for targeting the table footer. + """Target the table footer. With `loc.footer()` we can target the table's footer, which currently contains the source notes (and may contain a 'footnotes' location in the future). This is useful when applying custom @@ -588,12 +588,12 @@ class LocFooter(Loc): @dataclass class LocFootnotes(Loc): - """A location for targeting footnotes.""" + """Target the footnotes.""" @dataclass class LocSourceNotes(Loc): - """A location specification for targeting the source notes. + """Target the source notes. With `loc.source_notes()`, we can target the source notes in the table. This is useful when applying custom with the [`tab_style()`](`great_tables.GT.tab_style`) method. That method has a From 2edeb98415047ab8677565b874a784c39e1957c1 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Wed, 2 Oct 2024 12:13:57 -0400 Subject: [PATCH 142/150] feat: allow passing a webdriver instance to save --- great_tables/_export.py | 37 ++++++++++++++++++++++++++++++++----- tests/test_export.py | 15 ++++++++++++++- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/great_tables/_export.py b/great_tables/_export.py index fb4832ea1..8f8ea5bd2 100644 --- a/great_tables/_export.py +++ b/great_tables/_export.py @@ -177,13 +177,31 @@ def as_raw_html( DebugDumpOptions: TypeAlias = Literal["zoom", "width_resize", "final_resize"] +class _NoOpDriverCtx: + """Context manager that no-ops entering a webdriver(options=...) instance.""" + + def __init__(self, driver: webdriver.Remote): + self.driver = driver + + def __call__(self, options): + # no-op what is otherwise instantiating webdriver with options, + # since a webdriver instance was already passed on init + return self + + def __enter__(self): + return self.driver + + def __exit__(self, *args): + pass + + def save( self: GT, file: str, selector: str = "table", scale: float = 1.0, expand: int = 5, - web_driver: WebDrivers = "chrome", + web_driver: WebDrivers | webdriver.Remote = "chrome", window_size: tuple[int, int] = (6000, 6000), debug_port: None | int = None, encoding: str = "utf-8", @@ -209,9 +227,12 @@ def save( (NOT IMPLEMENTED) The number of pixels to expand the screenshot by. This can be increased to capture more of the surrounding area, or decreased to capture less. web_driver - The webdriver to use when taking the screenshot. By default, uses Google Chrome. Supports - `"firefox"` (Mozilla Firefox), `"safari"` (Apple Safari), and `"edge"` (Microsoft Edge). - Specified browser must be installed. + The webdriver to use when taking the screenshot. Either a driver name, or webdriver + instance. By default, uses Google Chrome. Supports `"firefox"` (Mozilla Firefox), `"safari"` + (Apple Safari), and `"edge"` (Microsoft Edge). + + Specified browser must be installed. Note that if a webdriver instance is passed, options + that require setting up a webdriver, like debug_port, will not be used. window_size The size of the browser window to use when laying out the table. This shouldn't be necessary to capture a table, but may affect the tables appearance. @@ -267,7 +288,11 @@ def save( html_content = as_raw_html(self) # Set the webdriver and options based on the chosen browser (`web_driver=` argument) - if web_driver == "chrome": + if isinstance(web_driver, webdriver.Remote): + wdriver = _NoOpDriverCtx(web_driver) + wd_options = None + + elif web_driver == "chrome": wdriver = webdriver.Chrome wd_options = webdriver.ChromeOptions() elif web_driver == "safari": @@ -279,6 +304,8 @@ def save( elif web_driver == "edge": wdriver = webdriver.Edge wd_options = webdriver.EdgeOptions() + else: + raise ValueError(f"Unsupported web driver: {web_driver}") # specify headless flag ---- if web_driver in {"firefox", "edge"}: diff --git a/tests/test_export.py b/tests/test_export.py index 538a700fd..a382912a3 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -44,7 +44,7 @@ def test_save_image_file(gt_tbl: GT, tmp_path): f_path = tmp_path / "test_image.png" gt_tbl.save(file=str(f_path)) - time.sleep(0.1) + time.sleep(0.05) assert f_path.exists() @@ -53,6 +53,19 @@ def test_save_non_png(gt_tbl: GT, tmp_path): gt_tbl.save(file=str(f_path)) +def test_save_custom_webdriver(gt_tbl: GT, tmp_path): + from selenium import webdriver + + f_path = tmp_path / "test_image.png" + options = webdriver.ChromeOptions() + options.add_argument("--headless=new") + with webdriver.Chrome(options) as wd: + gt_tbl.save(file=str(f_path), web_driver=wd) + + time.sleep(0.05) + assert f_path.exists() + + @pytest.mark.parametrize( "src, dst", [ From b3191fc2c36a4da12d83c57fc9fdff45e10e9901 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Wed, 2 Oct 2024 12:32:10 -0400 Subject: [PATCH 143/150] feat: allow save file argument to be a Path --- great_tables/_export.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/great_tables/_export.py b/great_tables/_export.py index 8f8ea5bd2..0e033dd03 100644 --- a/great_tables/_export.py +++ b/great_tables/_export.py @@ -197,7 +197,7 @@ def __exit__(self, *args): def save( self: GT, - file: str, + file: Path | str, selector: str = "table", scale: float = 1.0, expand: int = 5, @@ -280,6 +280,9 @@ def save( if selector != "table": raise NotImplementedError("Currently, only selector='table' is supported.") + if isinstance(file, Path): + file = str(file) + # If there is no file extension, add the .png extension if not Path(file).suffix: file += ".png" From 7f9ed3773d69f00b5154e68ead144db6e77a2adb Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 4 Oct 2024 13:41:47 -0400 Subject: [PATCH 144/150] fix: handle position or name to specify a group name location --- great_tables/_locations.py | 5 +++-- great_tables/_utils_render_html.py | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 11c52d40f..f2caff4f7 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -751,6 +751,7 @@ def resolve_rows_i( data: GTData | list[str], expr: RowSelectExpr = None, null_means: Literal["everything", "nothing"] = "everything", + row_name_attr: Literal["rowname", "group_id"] = "rowname", ) -> list[tuple[str, int]]: """Return matching row numbers, based on expr @@ -766,7 +767,7 @@ def resolve_rows_i( expr: list[str | int] = [expr] if isinstance(data, GTData): - row_names = [row.rowname for row in data._stub] + row_names = [getattr(row, row_name_attr) for row in data._stub] else: row_names = data @@ -854,7 +855,7 @@ def _(loc: LocColumnLabels, data: GTData) -> list[CellPos]: def _(loc: LocRowGroups, data: GTData) -> set[int]: # TODO: what are the rules for matching row groups? # TODO: resolve_rows_i will match a list expr to row names (not group names) - group_pos = set(pos for _, pos in resolve_rows_i(data, loc.rows)) + group_pos = set(name for name, _ in resolve_rows_i(data, loc.rows, row_name_attr="group_id")) return list(group_pos) diff --git a/great_tables/_utils_render_html.py b/great_tables/_utils_render_html.py index 4abd55ec7..9bdc97fd3 100644 --- a/great_tables/_utils_render_html.py +++ b/great_tables/_utils_render_html.py @@ -479,7 +479,11 @@ def create_body_component_h(data: GTData) -> str: "gt_empty_group_heading" if group_label == "" else "gt_group_heading_row" ) - _styles = [style for style in styles_row_group_label if i in style.grpname] + _styles = [ + style + for style in styles_row_group_label + if group_info.group_id in style.grpname + ] group_styles = _flatten_styles(_styles, wrap=True) group_row = f""" {group_label} From f41f4908e18619c5631b4476ac4478b3a6af6a5d Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 4 Oct 2024 14:13:21 -0400 Subject: [PATCH 145/150] fix: regression when selecting all row groups --- great_tables/_locations.py | 4 ++-- tests/test_locations.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index f2caff4f7..787ab6e18 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -773,7 +773,7 @@ def resolve_rows_i( if expr is None: if null_means == "everything": - return [(row.rowname, ii) for ii, row in enumerate(data._stub)] + return [(name, ii) for ii, name in enumerate(row_names)] else: return [] @@ -856,7 +856,7 @@ def _(loc: LocRowGroups, data: GTData) -> set[int]: # TODO: what are the rules for matching row groups? # TODO: resolve_rows_i will match a list expr to row names (not group names) group_pos = set(name for name, _ in resolve_rows_i(data, loc.rows, row_name_attr="group_id")) - return list(group_pos) + return group_pos @resolve.register diff --git a/tests/test_locations.py b/tests/test_locations.py index cd10f7940..52364871d 100644 --- a/tests/test_locations.py +++ b/tests/test_locations.py @@ -7,7 +7,9 @@ from great_tables._locations import ( CellPos, LocBody, + LocRowGroups, LocSpannerLabels, + LocStub, LocTitle, resolve, resolve_cols_i, @@ -176,6 +178,45 @@ def test_resolve_loc_spanner_label_error_missing(): resolve(loc, spanners) +@pytest.mark.parametrize( + "rows, res", + [ + (2, {"b"}), + ([2], {"b"}), + ("b", {"b"}), + (["a", "c"], {"a", "c"}), + ([0, 1], {"a"}), + (None, {"a", "b", "c"}), + ], +) +def test_resolve_loc_row_groups(rows, res): + df = pl.DataFrame({"group": ["a", "a", "b", "c"]}) + loc = LocRowGroups(rows=rows) + new_loc = resolve(loc, GT(df, groupname_col="group")) + + assert isinstance(new_loc, set) + assert new_loc == res + + +@pytest.mark.parametrize( + "rows, res", + [ + (2, {2}), + ([2], {2}), + ("b", {2}), + (["a", "c"], {0, 1, 3}), + ([0, 1], {0, 1}), + ], +) +def test_resolve_loc_stub(rows, res): + df = pl.DataFrame({"row": ["a", "a", "b", "c"]}) + loc = LocStub(rows=rows) + new_loc = resolve(loc, GT(df, rowname_col="row")) + + assert isinstance(new_loc, set) + assert new_loc == res + + @pytest.mark.parametrize( "expr", [ From 8c2b1827e2a7f22490c23cb9c618fe7cf516ccab Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 4 Oct 2024 14:14:38 -0400 Subject: [PATCH 146/150] docs: restructure get-started into more sections --- docs/_quarto.yml | 13 +++++++++---- docs/get-started/basic-styling.qmd | 2 +- docs/get-started/table-theme-options.qmd | 3 +-- docs/get-started/targeted-styles.qmd | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 98be15cc1..344e9a837 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -34,19 +34,24 @@ website: - get-started/basic-header.qmd - get-started/basic-stub.qmd - get-started/basic-column-labels.qmd - - section: Format and Style + - section: Format contents: - get-started/basic-formatting.qmd + - get-started/nanoplots.qmd + - section: Style + contents: - get-started/basic-styling.qmd + - get-started/targeted-styles.qmd - get-started/colorizing-with-data.qmd + - section: Theming + contents: - get-started/table-theme-options.qmd - get-started/table-theme-premade.qmd - - section: Extra Topics + - section: Selecting table parts contents: - get-started/column-selection.qmd - get-started/row-selection.qmd - - get-started/nanoplots.qmd - - get-started/targeted-styles.qmd + - get-started/loc-selection.qmd format: html: diff --git a/docs/get-started/basic-styling.qmd b/docs/get-started/basic-styling.qmd index 1e1d9c713..2d0bbae32 100644 --- a/docs/get-started/basic-styling.qmd +++ b/docs/get-started/basic-styling.qmd @@ -1,5 +1,5 @@ --- -title: Stying the Table Body +title: Styling the Table Body jupyter: python3 html-table-processing: none --- diff --git a/docs/get-started/table-theme-options.qmd b/docs/get-started/table-theme-options.qmd index b8ac0f693..00811d5bf 100644 --- a/docs/get-started/table-theme-options.qmd +++ b/docs/get-started/table-theme-options.qmd @@ -5,7 +5,7 @@ jupyter: python3 Great Tables exposes options to customize the appearance of tables via two methods: -* [](`~great_tables.GT.tab_style`) - targeted styles (e.g. color a specific cell of data). +* [](`~great_tables.GT.tab_style`) - targeted styles (e.g. color a specific cell of data, or a specific group label). * [](`~great_tables.GT.tab_options`) - broad styles (e.g. color the header and source notes). Both methods target parts of the table, as shown in the diagram below. @@ -14,7 +14,6 @@ Both methods target parts of the table, as shown in the diagram below. This page covers how to style and theme your table using `GT.tab_options()`, which is meant to quickly set a broad range of styles. -In the future, even more granular options will become available via `GT.tab_style()`. We'll use the basic GT object below for most examples, since it marks some of the table parts. diff --git a/docs/get-started/targeted-styles.qmd b/docs/get-started/targeted-styles.qmd index a3394ec75..14ea10281 100644 --- a/docs/get-started/targeted-styles.qmd +++ b/docs/get-started/targeted-styles.qmd @@ -1,5 +1,5 @@ --- -title: Targeted styles +title: Styling the whole table jupyter: python3 --- From 9fa9c561888dc67cfd994a3cd91396dadabd22e3 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 4 Oct 2024 14:17:37 -0400 Subject: [PATCH 147/150] tests: row and group locations via polars expression --- tests/test_locations.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_locations.py b/tests/test_locations.py index 52364871d..41cef2195 100644 --- a/tests/test_locations.py +++ b/tests/test_locations.py @@ -187,6 +187,7 @@ def test_resolve_loc_spanner_label_error_missing(): (["a", "c"], {"a", "c"}), ([0, 1], {"a"}), (None, {"a", "b", "c"}), + (pl.col("group") == "b", {"b"}), ], ) def test_resolve_loc_row_groups(rows, res): @@ -206,6 +207,7 @@ def test_resolve_loc_row_groups(rows, res): ("b", {2}), (["a", "c"], {0, 1, 3}), ([0, 1], {0, 1}), + (pl.col("row") == "a", {0, 1}), ], ) def test_resolve_loc_stub(rows, res): From 977dcf9efd67ed04dbc26eb451f4efc3a8456da3 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 4 Oct 2024 14:41:40 -0400 Subject: [PATCH 148/150] fix: clean up and test column and spanner loc selection --- great_tables/_locations.py | 14 ++++++-------- tests/test_locations.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/great_tables/_locations.py b/great_tables/_locations.py index 787ab6e18..b21b4aa21 100644 --- a/great_tables/_locations.py +++ b/great_tables/_locations.py @@ -845,10 +845,9 @@ def _(loc: LocSpannerLabels, spanners: Spanners) -> LocSpannerLabels: @resolve.register -def _(loc: LocColumnLabels, data: GTData) -> list[CellPos]: - cols = resolve_cols_i(data=data, expr=loc.columns) - cell_pos = [CellPos(col[1], 0, colname=col[0]) for col in cols] - return cell_pos +def _(loc: LocColumnLabels, data: GTData) -> list[tuple[str, int]]: + name_pos = resolve_cols_i(data=data, expr=loc.columns) + return name_pos @resolve.register @@ -940,17 +939,16 @@ def _( @set_style.register def _(loc: LocColumnLabels, data: GTData, style: list[CellStyle]) -> GTData: - positions: list[CellPos] = resolve(loc, data) + selected = resolve(loc, data) # evaluate any column expressions in styles styles = [entry._evaluate_expressions(data._tbl_data) for entry in style] all_info: list[StyleInfo] = [] - for col_pos in positions: + for name, pos in selected: crnt_info = StyleInfo( locname=loc, - colname=col_pos.colname, - rownum=col_pos.row, + colname=name, styles=styles, ) all_info.append(crnt_info) diff --git a/tests/test_locations.py b/tests/test_locations.py index 41cef2195..3dbc75d5a 100644 --- a/tests/test_locations.py +++ b/tests/test_locations.py @@ -7,6 +7,8 @@ from great_tables._locations import ( CellPos, LocBody, + LocColumnLabels, + LocSpannerLabels, LocRowGroups, LocSpannerLabels, LocStub, @@ -219,6 +221,39 @@ def test_resolve_loc_stub(rows, res): assert new_loc == res +@pytest.mark.parametrize( + "cols, res", + [ + (["b"], [("b", 1)]), + ([0, 2], [("a", 0), ("c", 2)]), + (cs.by_name("a"), [("a", 0)]), + ], +) +def test_resolve_loc_column_labels(cols, res): + df = pl.DataFrame({"a": [0], "b": [1], "c": [2]}) + loc = LocColumnLabels(columns=cols) + + selected = resolve(loc, GT(df)) + assert selected == res + + +@pytest.mark.parametrize( + "ids, res", + [ + (["b"], ["b"]), + (["a", "b"], ["a", "b"]), + pytest.param(cs.by_name("a"), ["a"], marks=pytest.mark.xfail), + ], +) +def test_resolve_loc_spanner_labels(ids, res): + df = pl.DataFrame({"x": [0], "y": [1], "z": [2]}) + gt = GT(df).tab_spanner("a", ["x", "y"]).tab_spanner("b", ["z"]) + loc = LocSpannerLabels(ids=ids) + + new_loc = resolve(loc, gt._spanners) + assert new_loc.ids == res + + @pytest.mark.parametrize( "expr", [ From 25b6ad43ca3444c9009cac83f8e9628ed53e3d86 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 4 Oct 2024 14:45:24 -0400 Subject: [PATCH 149/150] docs: add loc selection to get started --- docs/get-started/loc-selection.qmd | 129 +++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 docs/get-started/loc-selection.qmd diff --git a/docs/get-started/loc-selection.qmd b/docs/get-started/loc-selection.qmd new file mode 100644 index 000000000..dca47e5be --- /dev/null +++ b/docs/get-started/loc-selection.qmd @@ -0,0 +1,129 @@ +--- +title: Location selection +jupyter: python3 +--- + +```{python} +# | echo: false +import polars as pl +from great_tables import GT + +data = [ + ["header", "loc.header()", "composite"], + ["", "loc.title()", ""], + ["", "loc.subtitle()", ""], + ["boxhead", "loc.column_header()", "composite"], + ["", "loc.spanner_labels()", "columns"], + ["", "loc.column_labels()", "columns"], + ["row stub", "loc.stub()", "rows"], + ["", "loc.row_groups()", "rows"], + ["table body", "loc.body()", "columns and rows"], + ["footer", "loc.footer()", "composite"], + ["", "loc.source_notes()", ""], +] + +df = pl.DataFrame(data, schema=["table part", "name", "selection"], orient="row") + +GT(df) +``` + + +```{python} +import polars as pl +import polars.selectors as cs + +from great_tables import GT, loc, style, exibble + +pl_exibble = pl.from_pandas(exibble)[[0, 1, 4], ["num", "char", "group"]] +``` + +## simple locations + +```{python} +( + GT(pl_exibble) + .tab_header("A title", "A subtitle") + .tab_style( + style.fill("yellow"), + loc.title(), + ) +) +``` + +## composite locations + +```{python} +( + GT(pl_exibble) + .tab_header("A title", "A subtitle") + .tab_style( + style.fill("yellow"), + loc.header(), + ) +) +``` + +## body columns and rows + +```{python} +( + GT(pl_exibble).tab_style( + style.fill("yellow"), + loc.body( + columns=cs.starts_with("cha"), + rows=pl.col("char").str.contains("a"), + ), + ) +) +``` + +## column labels + +```{python} +GT(pl_exibble).tab_style( + style.fill("yellow"), + loc.column_labels( + cs.starts_with("cha"), + ), +) +``` + +## row and group names + +* by name +* by index +* by expression + +```{python} +gt = GT(pl_exibble).tab_stub( + rowname_col="char", + groupname_col="group", +) + +gt.tab_style(style.fill("yellow"), loc.stub()) +``` + + +```{python} +gt.tab_style(style.fill("yellow"), loc.stub("banana")) +``` + +```{python} +gt.tab_style(style.fill("yellow"), loc.stub(["apricot", 2])) +``` + +### groups by name and position + +```{python} +gt.tab_style( + style.fill("yellow"), + loc.row_groups("grp_b"), +) +``` + +```{python} +gt.tab_style( + style.fill("yellow"), + loc.row_groups(1), +) +``` From fb5709f40357dcb0a10d031482fb048cbdb2705e Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 4 Oct 2024 15:33:22 -0400 Subject: [PATCH 150/150] docs: add more narrative --- docs/get-started/loc-selection.qmd | 68 ++++++++++++++++++++++++---- docs/get-started/targeted-styles.qmd | 2 +- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/docs/get-started/loc-selection.qmd b/docs/get-started/loc-selection.qmd index dca47e5be..31e2c03b3 100644 --- a/docs/get-started/loc-selection.qmd +++ b/docs/get-started/loc-selection.qmd @@ -3,6 +3,12 @@ title: Location selection jupyter: python3 --- +Great Tables uses the `loc` module to specify locations for styling in `tab_style()`. Some location specifiers also allow selecting specific columns and rows of data. + +For example, you might style a particular row name, group, column, or spanner label. + +The table below shows the different location specifiers, along with the types of column or row selection they allow. + ```{python} # | echo: false import polars as pl @@ -27,6 +33,11 @@ df = pl.DataFrame(data, schema=["table part", "name", "selection"], orient="row" GT(df) ``` +Note that composite specifiers are ones that target multiple locations. For example, `loc.header()` specifies both `loc.title()` and `loc.subtitle()`. + +## Setting up data + +The examples below will use this small dataset to show selecting different locations, as well as specific rows and columns within a location (where supported). ```{python} import polars as pl @@ -35,9 +46,15 @@ import polars.selectors as cs from great_tables import GT, loc, style, exibble pl_exibble = pl.from_pandas(exibble)[[0, 1, 4], ["num", "char", "group"]] + +pl_exibble ``` -## simple locations +## Simple locations + +Simple locations don't take any arguments. + +For example, styling the title uses `loc.title()`. ```{python} ( @@ -50,7 +67,11 @@ pl_exibble = pl.from_pandas(exibble)[[0, 1, 4], ["num", "char", "group"]] ) ``` -## composite locations +## Composite locations + +Composite locations target multiple simple locations. + +For example, `loc.header()` includes both `loc.title()` and `loc.subtitle()`. ```{python} ( @@ -63,7 +84,9 @@ pl_exibble = pl.from_pandas(exibble)[[0, 1, 4], ["num", "char", "group"]] ) ``` -## body columns and rows +## Body columns and rows + +Use `loc.body()` to style specific cells in the table body. ```{python} ( @@ -77,7 +100,13 @@ pl_exibble = pl.from_pandas(exibble)[[0, 1, 4], ["num", "char", "group"]] ) ``` -## column labels +This is discussed in detail in [Styling the Table Body](./basic-styling.qmd). + +## Column labels + +Locations like `loc.spanner_labels()` and `loc.column_labels()` can select specific column and spanner labels. + +You can use name strings, index position, or polars selectors. ```{python} GT(pl_exibble).tab_style( @@ -88,11 +117,15 @@ GT(pl_exibble).tab_style( ) ``` -## row and group names +However, note that `loc.spanner_labels()` currently only accepts list of string names. + +## Row and group names + +Row and group names in `loc.stub()` and `loc.row_groups()` may be specified three ways: * by name * by index -* by expression +* by polars expression ```{python} gt = GT(pl_exibble).tab_stub( @@ -112,18 +145,35 @@ gt.tab_style(style.fill("yellow"), loc.stub("banana")) gt.tab_style(style.fill("yellow"), loc.stub(["apricot", 2])) ``` -### groups by name and position +### Groups by name and position + +Note that for specifying row groups, the group corresponding to the group name or row number in the original data is used. + +For example, the code below styles the group corresponding to the row at index 1 (i.e. the second row) in the data. ```{python} gt.tab_style( style.fill("yellow"), - loc.row_groups("grp_b"), + loc.row_groups(1), ) ``` +Since the second row (starting with "banana") is in "grp_a", that is the group that gets styled. + +This means you can use a polars expression to select groups: + ```{python} gt.tab_style( style.fill("yellow"), - loc.row_groups(1), + loc.row_groups(pl.col("group") == "grp_b"), +) +``` + +You can also specify group names using a string (or list of strings). + +```{python} +gt.tab_style( + style.fill("yellow"), + loc.row_groups("grp_b"), ) ``` diff --git a/docs/get-started/targeted-styles.qmd b/docs/get-started/targeted-styles.qmd index 14ea10281..2b02e4aac 100644 --- a/docs/get-started/targeted-styles.qmd +++ b/docs/get-started/targeted-styles.qmd @@ -7,7 +7,7 @@ In [Styling the Table Body](./basic-styling), we discussed styling table data wi In this article we'll cover how the same method can be used to style many other parts of the table, like the header, specific spanner labels, the footer, and more. :::{.callout-warning} -This feature is currently a work in progress, and not yet released. Great Tables must be installed from github in order to try it. +This feature is new, and this page of documentation is still in development. :::