Skip to content

Commit 1af68a3

Browse files
juleswg23machowrich-iannone
authored
Fix: Modify the group labels when render_formats() is called (#769)
* modify the stub formats when render_formats() is called * Refactor rowgroup label update logic into helper fn * Add tests for row group label formatting * Move update_group_row_labels into Stub --------- Co-authored-by: Michael Chow <mc_al_github@fastmail.com> Co-authored-by: Richard Iannone <riannone@me.com>
1 parent 9a48651 commit 1af68a3

4 files changed

Lines changed: 138 additions & 2 deletions

File tree

great_tables/_gt_data.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
copy_data,
2525
create_empty_frame,
2626
get_column_names,
27+
is_na,
2728
n_rows,
2829
to_list,
2930
validate_frame,
@@ -626,6 +627,49 @@ def order_groups(self, group_order: RowGroups) -> Self:
626627
# TODO: validate
627628
return self.__class__(self.rows, self.group_rows.reorder(group_order))
628629

630+
def update_group_row_labels(self, body: Body, tbl_data: TblData, boxhead: Boxhead) -> Self:
631+
"""Update group row labels using formatted values from the rendered body.
632+
633+
For each group, the formatted cell value for the first row of the group is
634+
looked up in `body`. If the cell was not formatted (i.e., it is still NA),
635+
the original value from `tbl_data` is used instead.
636+
637+
If no row-group column exists in `boxhead`, the stub is returned unchanged.
638+
639+
Parameters
640+
----------
641+
body
642+
The rendered body whose cells may contain formatted values.
643+
tbl_data
644+
The original (unformatted) source data.
645+
boxhead
646+
The boxhead containing column metadata, used to identify the row-group column.
647+
648+
Returns
649+
-------
650+
Stub
651+
A new Stub with group labels replaced by formatted values, or the
652+
original Stub if no row-group column exists.
653+
"""
654+
rowgroup_var = boxhead._get_row_group_column()
655+
if rowgroup_var is None:
656+
return self
657+
658+
new_group_rows: list[Any] = []
659+
660+
for group_row in self.group_rows:
661+
first_index = group_row.indices[0]
662+
cell_content = _get_cell(body.body, first_index, rowgroup_var.var)
663+
664+
# When no formatter was applied, the cell is still NA — fall back to
665+
# the original data value.
666+
if is_na(tbl_data, cell_content):
667+
cell_content = _get_cell(tbl_data, first_index, rowgroup_var.var)
668+
669+
new_group_rows.append(group_row.with_group_label(cell_content))
670+
671+
return self.__class__(self.rows, GroupRows(new_group_rows))
672+
629673
def group_indices_map(self) -> list[tuple[int, GroupRowInfo | None]]:
630674
return self.group_rows.indices_map(len(self.rows))
631675

@@ -717,6 +761,10 @@ def defaulted_label(self) -> str:
717761
label = self.group_label if self.group_label is not None else self.group_id
718762
return label
719763

764+
def with_group_label(self, label: str | None) -> Self:
765+
"""Return a copy of the object with the specified group label."""
766+
return replace(self, group_label=label)
767+
720768

721769
class MISSING_GROUP:
722770
"""Represent a category of all missing group levels in data."""

great_tables/_row_groups.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
from __future__ import annotations

great_tables/gt.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,11 @@ def _render_formats(self, context: str) -> Self:
320320
# TODO: this body method performs a mutation. Should we make a copy of body?
321321
new_body.render_formats(self._tbl_data, self._formats, context)
322322
new_body.render_formats(self._tbl_data, self._substitutions, context)
323-
return self._replace(_body=new_body)
323+
324+
# Update group row labels with formatted values when a row_group column exists
325+
new_stub = self._stub.update_group_row_labels(new_body, self._tbl_data, self._boxhead)
326+
327+
return self._replace(_body=new_body, _stub=new_stub)
324328

325329
def _build_data(self, context: str) -> Self:
326330
# Build the body of the table by generating a dictionary

tests/test_row_groups.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import pandas as pd
2+
import pytest
3+
4+
from great_tables import GT
5+
from great_tables._gt_data import Body, GroupRowInfo, GroupRows, Stub, RowInfo
6+
7+
8+
@pytest.fixture
9+
def grouped_gt():
10+
"""A GT table with a groupname column and two groups."""
11+
df = pd.DataFrame(
12+
{
13+
"group": ["A", "A", "B", "B"],
14+
"value": [1, 2, 3, 4],
15+
}
16+
)
17+
return GT(df, groupname_col="group")
18+
19+
20+
@pytest.fixture
21+
def numeric_grouped_gt():
22+
"""A GT table with a numeric groupname column and two groups."""
23+
df = pd.DataFrame(
24+
{
25+
"group": [1000, 1000, 2000, 2000],
26+
"value": [1, 2, 3, 4],
27+
}
28+
)
29+
return GT(df, groupname_col="group")
30+
31+
32+
def test_update_group_row_labels_uses_formatted_value(numeric_grouped_gt: GT):
33+
"""When a formatter has been applied, group labels should reflect the formatted value."""
34+
gt_fmt = numeric_grouped_gt.fmt_number(columns="group", decimals=0)
35+
36+
built = gt_fmt._render_formats("html")
37+
labels = [gr.defaulted_label() for gr in built._stub.group_rows]
38+
39+
assert labels == ["1,000", "2,000"]
40+
41+
42+
def test_update_group_row_labels_falls_back_to_original(grouped_gt: GT):
43+
"""When no formatter is applied, group labels should use the original data value."""
44+
built = grouped_gt._render_formats("html")
45+
labels = [gr.defaulted_label() for gr in built._stub.group_rows]
46+
47+
assert labels == ["A", "B"]
48+
49+
50+
def test_update_group_row_labels_no_groups():
51+
"""When there is no groupname column, the stub is returned unchanged."""
52+
df = pd.DataFrame({"value": [1, 2, 3]})
53+
gt_tbl = GT(df)
54+
55+
built = gt_tbl._render_formats("html")
56+
57+
# No group rows should exist
58+
assert len(built._stub.group_rows) == 0
59+
60+
61+
def test_formatted_group_label_in_html(numeric_grouped_gt: GT):
62+
"""Formatted group labels should appear in the rendered HTML output."""
63+
html = numeric_grouped_gt.fmt_number(columns="group", decimals=0).as_raw_html()
64+
65+
assert ">1,000</th>" in html
66+
assert ">2,000</th>" in html
67+
68+
69+
def test_markdown_link_in_group_label_renders_as_anchor():
70+
"""Markdown links in group labels should render as <a> tags in HTML output."""
71+
df = pd.DataFrame(
72+
{
73+
"group": [
74+
"[Google](https://google.com)",
75+
"[Google](https://google.com)",
76+
"[GitHub](https://github.com)",
77+
"[GitHub](https://github.com)",
78+
],
79+
"value": [1, 2, 3, 4],
80+
}
81+
)
82+
html = GT(df, groupname_col="group").fmt_markdown(columns="group").as_raw_html()
83+
84+
assert '<a href="https://google.com">Google</a>' in html
85+
assert '<a href="https://github.com">GitHub</a>' in html

0 commit comments

Comments
 (0)