Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
04f2f8e
a very very rough draft, does not work for polars yet
juleswg23 Aug 12, 2025
ff29302
set stub and boxhead to include all rows in final table
juleswg23 Aug 13, 2025
f3a26a1
add create_no_row_frame and refactor insert_row
juleswg23 Aug 13, 2025
c810a6a
add SummaryRows to GTData object
juleswg23 Aug 13, 2025
21996ad
refactor modify rows
juleswg23 Aug 13, 2025
aaa887c
add merge_summary_rows to append to body
juleswg23 Aug 13, 2025
6019d7e
replace len with n_rows
juleswg23 Aug 13, 2025
0e85363
update build step
juleswg23 Aug 13, 2025
059686f
add comments mapping out rendering plan
juleswg23 Aug 13, 2025
cd6299e
new locations for grand summary (and summary)
juleswg23 Aug 14, 2025
7e1985a
condensing locations
juleswg23 Aug 14, 2025
07d3574
add getter for summary rows
juleswg23 Aug 14, 2025
1acbabd
begin design on rendering top grand summary rows
juleswg23 Aug 14, 2025
afae957
refactor _create_row_component_h(), outside of create_body_component_…
juleswg23 Aug 14, 2025
dcbf84c
remove merge_summary_rows
juleswg23 Aug 14, 2025
f310f7e
remove commented out code
juleswg23 Aug 14, 2025
b832652
Change SummaryRowInfo values from list to dict
juleswg23 Aug 14, 2025
6e9af0a
reorder functions
juleswg23 Aug 14, 2025
986d8a6
remove dead code
juleswg23 Aug 14, 2025
aab38b9
support grand summary rows at bottom of table
juleswg23 Aug 14, 2025
bf372cf
location mask class var added
juleswg23 Aug 14, 2025
3765a48
support sum
juleswg23 Aug 14, 2025
ae9caf1
support opt_stylize and other tab_options
juleswg23 Aug 14, 2025
52741b5
Apply special classes to get double border
juleswg23 Aug 14, 2025
7e201df
clean up add summary and set up styling
juleswg23 Aug 15, 2025
801bd99
rename for clairity
juleswg23 Aug 15, 2025
e618668
experimenting with styles on grandSummaryStub
juleswg23 Aug 15, 2025
cf0d165
Merge branch 'main' into feat-grand-summary-rows
juleswg23 Aug 19, 2025
52e125a
remove prints
juleswg23 Aug 19, 2025
2c64419
Add stub column when none exists for summary rows (this approach does…
juleswg23 Aug 19, 2025
d6a5ecb
target location for LocGrandSummaryStub
juleswg23 Aug 19, 2025
d906e67
style LocGrandSummaryStub and LocGrandSummary
juleswg23 Aug 19, 2025
6672e4a
Handle special cases for summary rows with group stub columns
juleswg23 Aug 19, 2025
e4d038c
locations docstrings
juleswg23 Aug 19, 2025
3302cc4
adding new locs to quarto
juleswg23 Aug 19, 2025
5a88e85
more documentaiton
juleswg23 Aug 19, 2025
a06cd13
accept summaryFn and label
juleswg23 Aug 20, 2025
d8249ec
possible approach to fmt in grand Summary Rows, WIP
juleswg23 Aug 20, 2025
64b1cb0
refactor summary rows to mapping, and add summary rows grand attribut…
juleswg23 Aug 22, 2025
16cb29e
use eval_aggregate for summary rows
juleswg23 Aug 22, 2025
e532148
remove dead code
juleswg23 Aug 22, 2025
bcd89d4
docstring
juleswg23 Aug 22, 2025
0b4159b
testing docstring in site preview
juleswg23 Aug 22, 2025
3aab95d
fix build
juleswg23 Aug 25, 2025
4eecef0
refactor to rely on class attribute to determine if grand summary
juleswg23 Aug 25, 2025
1820fa7
Merge branch 'main' into feat-grand-summary-rows
juleswg23 Aug 25, 2025
b472da4
test eval_aggregate
juleswg23 Aug 26, 2025
4b8ed63
grand summary rows tests added
juleswg23 Aug 26, 2025
e6d0fff
fix kitchen sink example
juleswg23 Aug 26, 2025
954ad1f
ensure example compiles
juleswg23 Aug 26, 2025
e08069b
locations tests
juleswg23 Aug 26, 2025
f590355
snapshot updates, cover case in utils_render_html
juleswg23 Aug 26, 2025
f16d80a
main docstring
juleswg23 Aug 26, 2025
085aef0
get started documentation
juleswg23 Aug 26, 2025
86a4d9f
docs nitpicks
juleswg23 Aug 26, 2025
179a68f
remove unused attribute
juleswg23 Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ quartodoc:
- GT.cols_move_to_end
- GT.cols_hide
- GT.cols_unhide
- title: Adding rows
desc: >
The [`grand_summary_rows()`](`great_tables.GT.grand_summary_rows`) function will add rows to
summarize data in your table, such as totals or averages.
contents:
- GT.grand_summary_rows

- title: Location Targeting and Styling Classes
desc: >
Location targeting is a powerful feature of **Great Tables**. It allows for the precise
Expand All @@ -179,8 +186,10 @@ quartodoc:
- loc.column_header
- loc.spanner_labels
- loc.column_labels
- loc.grand_summary_stub
- loc.stub
- loc.row_groups
- loc.grand_summary
- loc.body
- loc.footer
- loc.source_notes
Expand Down
4 changes: 4 additions & 0 deletions docs/get-started/loc-selection.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ data = [
["", "loc.column_labels()", "columns"],
["row stub", "loc.stub()", "rows"],
["", "loc.row_groups()", "rows"],
# ["", "loc.summary_stub()", "rows"],
["", "loc.grand_summary_stub()", "rows"],
["table body", "loc.body()", "columns and rows"],
# ["", "loc.summary_rows()", "columns and rows"],
["", "loc.grand_summary_rows()", "columns and rows"],
["footer", "loc.footer()", "composite"],
["", "loc.source_notes()", ""],
]
Expand Down
2 changes: 2 additions & 0 deletions docs/get-started/table-theme-options.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ gt_ex = (
.tab_header("THE HEADING", "(a subtitle)")
.tab_stubhead("THE STUBHEAD")
.tab_source_note("THE SOURCE NOTE")
.grand_summary_rows({"GRAND SUMMARY ROW": lambda df: df.sum(numeric_only=True)})
)

gt_ex
Expand All @@ -48,6 +49,7 @@ The code below illustrates the table parts `~~GT.tab_options()` can target, by s
row_group_background_color="lightyellow",
stub_background_color="lightgreen",
source_notes_background_color="#f1e2af",
grand_summary_row_background_color="lightpink",
)
)
```
Expand Down
21 changes: 20 additions & 1 deletion docs/get-started/targeted-styles.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ 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
# https://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12 and grey
brewer_colors = [
"#a6cee3",
"#1f78b4",
Expand All @@ -32,6 +32,7 @@ brewer_colors = [
"#6a3d9a",
"#ffff99",
"#b15928",
"#808080",
]

c = iter(brewer_colors)
Expand All @@ -43,6 +44,7 @@ gt = (
.tab_source_note("yo")
.tab_spanner("spanner", ["char", "fctr"])
.tab_stubhead("stubhead")
.grand_summary_rows(fns={"Total": lambda x: x.sum(numeric_only=True)})
)

(
Expand All @@ -64,6 +66,9 @@ gt = (
.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())
# Summary Rows --------------
.tab_style(style.fill(next(c)), loc.grand_summary())
.tab_style(style.fill(next(c)), loc.grand_summary_stub())
)
```

Expand Down Expand Up @@ -129,3 +134,17 @@ gt.tab_style(style.fill("yellow"), loc.body())
```{python}
gt.tab_style(style.fill("yellow"), loc.stubhead())
```

## Grand Summary Rows

```{python}
(
gt.tab_style(
style.fill("yellow"),
loc.grand_summary_stub(),
).tab_style(
style.fill("lightblue"),
loc.grand_summary(),
)
)
```
203 changes: 171 additions & 32 deletions great_tables/_gt_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import copy
import re
from collections.abc import Sequence
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field, replace
from enum import Enum, auto
from itertools import chain, product
Expand Down Expand Up @@ -75,6 +75,8 @@ class GTData:
_spanners: Spanners
_heading: Heading
_stubhead: Stubhead
_summary_rows: SummaryRows
_summary_rows_grand: SummaryRows
_source_notes: SourceNotes
_footnotes: Footnotes
_styles: Styles
Expand Down Expand Up @@ -122,6 +124,8 @@ def from_data(
_spanners=Spanners([]),
_heading=Heading(),
_stubhead=None,
_summary_rows=SummaryRows(),
_summary_rows_grand=SummaryRows(_is_grand_summary=True),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like _is_grand_summary is used so that for methods like __getitem__ and add_summary_rows() some arguments that are required for regular summary rows can be optional for the grand one.

_source_notes=[],
_footnotes=[],
_styles=[],
Expand Down Expand Up @@ -510,10 +514,12 @@ def _get_number_of_visible_data_columns(self) -> int:

# Obtain the number of visible columns in the built table; this should
# account for the size of the stub in the final, built table
def _get_effective_number_of_columns(self, stub: Stub, options: Options) -> int:
def _get_effective_number_of_columns(
self, stub: Stub, has_summary_rows: bool, options: Options
) -> int:
n_data_cols = self._get_number_of_visible_data_columns()

stub_layout = stub._get_stub_layout(options=options)
stub_layout = stub._get_stub_layout(has_summary_rows=has_summary_rows, options=options)
# Once the stub is defined in the package, we need to account
# for the width of the stub at build time to fully obtain the number
# of visible columns in the built table
Expand Down Expand Up @@ -674,7 +680,7 @@ def _stub_group_names_has_column(self, options: Options) -> bool:

return row_group_as_column

def _get_stub_layout(self, options: Options) -> list[str]:
def _get_stub_layout(self, has_summary_rows: bool, options: Options) -> list[str]:
# Determine which stub components are potentially present as columns
stub_rownames_is_column = "row_id" in self._get_stub_components()
stub_groupnames_is_column = self._stub_group_names_has_column(options=options)
Expand All @@ -684,14 +690,12 @@ def _get_stub_layout(self, options: Options) -> list[str]:

# Resolve the layout of the stub (i.e., the roles of columns if present)
if n_stub_cols == 0:
# TODO: If summary rows are present, we will use the `rowname` column
# # for the summary row labels
# if _summary_exists(data=data):
# stub_layout = ["rowname"]
# else:
# stub_layout = []

stub_layout = []
# If summary rows are present, we will use the `rowname` column
# for the summary row labels
if has_summary_rows:
stub_layout = ["rowname"]
else:
stub_layout = []

else:
stub_layout = [
Expand Down Expand Up @@ -719,7 +723,7 @@ class GroupRowInfo:
indices: list[int] = field(default_factory=list)
# row_start: int | None = None
# row_end: int | None = None
has_summary_rows: bool = False
# has_summary_rows: bool = False # TODO: remove
summary_row_side: str | None = None

def defaulted_label(self) -> str:
Expand Down Expand Up @@ -972,6 +976,141 @@ def __init__(self, func: FormatFns, cols: list[str], rows: list[int]):
Formats = list


# Summary Rows ---

# This can't conflict with actual group ids since we have a
# seperate data structure for grand summary row infos


@dataclass(frozen=True)
class SummaryRowInfo:
"""Information about a single summary row"""

id: str
label: str # For now, label and id are identical
# The motivation for values as a dict is to ensure cols_* functions don't have to consider
# the implications on existing SummaryRowInfo objects
values: dict[str, Any] # TODO: consider datatype, series?
side: Literal["top", "bottom"] # TODO: consider enum


class SummaryRows(Mapping[str, list[SummaryRowInfo]]):
"""A sequence of summary rows

The following strctures should always be true about summary rows:
- The id is also the label (often the same as the function name)
- There is at most 1 row for each group and id pairing
- If a summary row is added and no row exists for that group and id, add it
- If a summary row is added and a row exists for that group and id pairing,
then replace all cells (in values) that are numeric in the new version
"""

_d: dict[str, list[SummaryRowInfo]]
_is_grand_summary: bool

GRAND_SUMMARY_KEY = "grand"

def __init__(self, _is_grand_summary: bool = False):
self._d = {}
self._is_grand_summary = _is_grand_summary

def __bool__(self) -> bool:
"""Return True if there are any summary rows, False otherwise."""
return len(self._d) > 0

def __getitem__(self, key: str | None) -> list[SummaryRowInfo]:
if self._is_grand_summary:
key = SummaryRows.GRAND_SUMMARY_KEY

if not key:
raise KeyError("Summary row group key must not be None for group summary rows.")

if key not in self._d:
raise KeyError(f"Group '{key}' not found in summary rows.")

return self._d[key]

def add_summary_row(self, summary_row: SummaryRowInfo, group_id: str | None = None) -> None:
"""Add a summary row following the merging rules in the class docstring."""

if self._is_grand_summary:
group_id = SummaryRows.GRAND_SUMMARY_KEY

existing_group = self.get(group_id)

if not existing_group:
self._d[group_id] = [summary_row]
return

else:
existing_index = None
for i, existing_row in enumerate(existing_group):
if existing_row.id == summary_row.id:
existing_index = i
break

new_rows = existing_group

if existing_index is None:
# No existing row for this group and id, add it
new_rows.append(summary_row)
else:
# Replace existing row, but merge numeric values from new version
existing_row = new_rows[existing_index]

# Start with existing values
merged_values = existing_row.values.copy()

# Replace with numeric values from new row
for key, new_value in summary_row.values.items():
if isinstance(new_value, (int, float)):
merged_values[key] = new_value
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can tell this is not the ideal way, since we can't guarantee that summary values have to be numeric.


# Create merged row with new row's properties but merged values
merged_row = SummaryRowInfo(
id=summary_row.id,
label=summary_row.label,
values=merged_values,
# Setting this to existing row instead of summary_row means original
# side is fixed by whatever side is first assigned to this row
side=existing_row.side,
)

new_rows[existing_index] = merged_row

self._d[group_id] = new_rows

return

def get_summary_rows(
self, group_id: str | None = None, side: str | None = None
) -> list[SummaryRowInfo]:
"""Get list of summary rows for that group. If side is None, do not filter by side.
Sorts result with 'top' side first, then 'bottom'."""

result: list[SummaryRowInfo] = []

if self._is_grand_summary:
group_id = SummaryRows.GRAND_SUMMARY_KEY

summary_row_group = self.get(group_id)

if summary_row_group:
for summary_row in summary_row_group:
if side is None or summary_row.side == side:
result.append(summary_row)

# Sort: 'top' first, then 'bottom'
result.sort(key=lambda r: 0 if r.side == "top" else 1) # TODO: modify if enum for side
return result

def __iter__(self):
raise NotImplementedError

def __len__(self):
raise NotImplementedError


# Options ----

default_fonts_list = [
Expand Down Expand Up @@ -1130,25 +1269,25 @@ class Options:
# summary_row_border_style: OptionsInfo = OptionsInfo(True, "summary_row", "value", "solid")
# summary_row_border_width: OptionsInfo = OptionsInfo(True, "summary_row", "px", "2px")
# summary_row_border_color: OptionsInfo = OptionsInfo(True, "summary_row", "value", "#D3D3D3")
# grand_summary_row_padding: OptionsInfo = OptionsInfo(True, "grand_summary_row", "px", "8px")
# grand_summary_row_padding_horizontal: OptionsInfo = OptionsInfo(
# True, "grand_summary_row", "px", "5px"
# )
# grand_summary_row_background_color: OptionsInfo = OptionsInfo(
# True, "grand_summary_row", "value", None
# )
# grand_summary_row_text_transform: OptionsInfo = OptionsInfo(
# True, "grand_summary_row", "value", "inherit"
# )
# grand_summary_row_border_style: OptionsInfo = OptionsInfo(
# True, "grand_summary_row", "value", "double"
# )
# grand_summary_row_border_width: OptionsInfo = OptionsInfo(
# True, "grand_summary_row", "px", "6px"
# )
# grand_summary_row_border_color: OptionsInfo = OptionsInfo(
# True, "grand_summary_row", "value", "#D3D3D3"
# )
grand_summary_row_padding: OptionsInfo = OptionsInfo(True, "grand_summary_row", "px", "8px")
grand_summary_row_padding_horizontal: OptionsInfo = OptionsInfo(
True, "grand_summary_row", "px", "5px"
)
grand_summary_row_background_color: OptionsInfo = OptionsInfo(
True, "grand_summary_row", "value", None
)
grand_summary_row_text_transform: OptionsInfo = OptionsInfo(
True, "grand_summary_row", "value", "inherit"
)
grand_summary_row_border_style: OptionsInfo = OptionsInfo(
True, "grand_summary_row", "value", "double"
)
grand_summary_row_border_width: OptionsInfo = OptionsInfo(
True, "grand_summary_row", "px", "6px"
)
grand_summary_row_border_color: OptionsInfo = OptionsInfo(
True, "grand_summary_row", "value", "#D3D3D3"
)
# footnotes_font_size: OptionsInfo = OptionsInfo(True, "footnotes", "px", "90%")
# footnotes_padding: OptionsInfo = OptionsInfo(True, "footnotes", "px", "4px")
# footnotes_padding_horizontal: OptionsInfo = OptionsInfo(True, "footnotes", "px", "5px")
Expand Down
Loading
Loading