From 7f9ed3773d69f00b5154e68ead144db6e77a2adb Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Fri, 4 Oct 2024 13:41:47 -0400 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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. :::