Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
0ac0203
Modify FootnoteInfo class
rich-iannone Aug 11, 2025
b8037ed
Add the tab_footnote() method
rich-iannone Aug 11, 2025
75d1d6e
Implement set_footnote for various location types
rich-iannone Aug 11, 2025
44c86ae
Remove unused tab_footnote() function and imports
rich-iannone Aug 11, 2025
6610a8d
Add footnote mark rendering to HTML table components
rich-iannone Aug 11, 2025
14e0d35
Improve footnote numbering to align with display order
rich-iannone Aug 11, 2025
a07f78f
Enable the footnotes_marks option in Options class
rich-iannone Aug 11, 2025
75557b4
Enable the footnotes_marks parameter in tab_options()
rich-iannone Aug 11, 2025
7673067
Add support for custom footnote mark symbols
rich-iannone Aug 11, 2025
cf770e7
Add footnote marks to col labels in HTML rendering
rich-iannone Aug 12, 2025
b23cfc6
Add several tests
rich-iannone Aug 12, 2025
298e3f1
Hide footnotes for hidden columns in HTML rendering
rich-iannone Aug 12, 2025
d679a16
Add several tests
rich-iannone Aug 12, 2025
fea2271
Update test_footnotes.py
rich-iannone Aug 12, 2025
8eba278
Remove period from footnote marks in footer HTML
rich-iannone Aug 12, 2025
f0ac15d
Add tab_footnote() to the API reference docs
rich-iannone Aug 12, 2025
aca0cb1
Improve footnote handling and ordering in rendered tbl
rich-iannone Aug 12, 2025
71daaef
Refactor footnote mark HTML generation
rich-iannone Aug 12, 2025
76727f8
Align stub and data cell footnote ordering
rich-iannone Aug 12, 2025
80f2ab3
Add snapshot test for footnotes in stub and body
rich-iannone Aug 12, 2025
7d37e50
Support unit notation in HTML/Markdown footnotes
rich-iannone Aug 12, 2025
1f28a4a
Add unified HTML footer for source notes and footnotes
rich-iannone Aug 13, 2025
71107cb
Update tests and snapshots
rich-iannone Aug 13, 2025
d9c44cb
Modify docstring for tab_footnote()
rich-iannone Aug 13, 2025
7a8d788
Ensure that spanners and stubhead label can have footnotes
rich-iannone Aug 13, 2025
d8f44fb
Update tests and snapshots
rich-iannone Aug 13, 2025
da34031
Remove unneeded utility function
rich-iannone Aug 13, 2025
cec470c
Remove more unneeded code
rich-iannone Aug 13, 2025
832825b
Refactor footnote location hierarchy mapping
rich-iannone Aug 13, 2025
8c7fcd8
Incorporate footnote placement (auto/left/right)
rich-iannone Aug 13, 2025
4b0a424
Revise tests and snapshots
rich-iannone Aug 13, 2025
efb6186
Refine numberlike detection
rich-iannone Aug 13, 2025
f82b6ef
Update tests and snapshots
rich-iannone Aug 13, 2025
ed6d9ad
Add several tests for single-line source notes
rich-iannone Aug 13, 2025
d990b4f
Remove unneeded utility function
rich-iannone Aug 13, 2025
95a0579
Add tests for some edge cases
rich-iannone Aug 13, 2025
218b379
Add tests of _get_column_index()
rich-iannone Aug 13, 2025
84292b7
Add tests for _get_spanner_leftmost_column_index()
rich-iannone Aug 13, 2025
3cc0da6
Refactor tests for tab_footnote()
rich-iannone Aug 13, 2025
6b58300
Add opt_footnote_marks to API reference docs
rich-iannone Aug 13, 2025
21ca100
Refactor footnote location handling to use Loc objects
rich-iannone Sep 9, 2025
ee88154
Refactor footnote application logic in HTML rendering
rich-iannone Sep 9, 2025
5fa1cc0
Split long HTML string in two for better readability
rich-iannone Sep 9, 2025
d07ebf5
Merge branch 'main' into feat-tab-footnote
rich-iannone Sep 9, 2025
310c9bf
Refactor footnote handling to use FootnoteEntry
rich-iannone Sep 9, 2025
7a50091
Remove unneeded text processing for footnotes
rich-iannone Sep 9, 2025
fec1d13
Refactor footnote mark HTML creation
rich-iannone Sep 9, 2025
4a7797a
Fix footnote mark type handling
rich-iannone Sep 9, 2025
6a39b35
Update _locations.py
rich-iannone Sep 9, 2025
73a89a4
Throw error if writing a LaTeX tbl with footnotes
rich-iannone Sep 22, 2025
385002f
Update as_latex() docstring with footnotes limitation
rich-iannone Sep 22, 2025
d88211b
Add test to check for as_latex() raising w/ footnote
rich-iannone Sep 22, 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
2 changes: 2 additions & 0 deletions docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ quartodoc:
- GT.tab_spanner_delim
- GT.tab_stub
- GT.tab_stubhead
- GT.tab_footnote
- GT.tab_source_note
- GT.tab_style
- GT.tab_options
Expand Down Expand Up @@ -219,6 +220,7 @@ quartodoc:
- GT.opt_table_outline
- GT.opt_table_font
- GT.opt_stylize
- GT.opt_footnote_marks
- title: Export
desc: >
There may come a day when you need to export a table to some specific format. A great method
Expand Down
1 change: 1 addition & 0 deletions great_tables/_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ def as_latex(self: GT, use_longtable: bool = False, tbl_pos: str | None = None)
functionality that is supported in HTML output tables is not currently supported in LaTeX
output tables:

- footnotes (via the `tab_footnote()` method)
- the rendering of the stub and row group labels (via the `=rowname_col` and `=groupname_col`
args in the `GT()` class)
- the use of the `md()` helper function to signal conversion of Markdown text
Expand Down
157 changes: 156 additions & 1 deletion great_tables/_footnotes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,159 @@
from __future__ import annotations

from typing import TYPE_CHECKING

# TODO: create the `tab_footnote()` function
from ._gt_data import FootnoteInfo, FootnotePlacement
from ._locations import FootnoteEntry, Loc, PlacementOptions, set_style
from ._text import Text, _process_text

if TYPE_CHECKING:
from ._types import GTSelf


def tab_footnote(
self: GTSelf,
footnote: str | Text,
locations: Loc | None | list[Loc | None] = None,
placement: PlacementOptions = "auto",
) -> GTSelf:
"""
Add a table footnote.

`tab_footnote()` can make it a painless process to add a footnote to a table. There are commonly
two components to a footnote: (1) a footnote mark that is attached to the targeted cell content,
and (2) the footnote text itself that is placed in the table's footer area. Each unit of
footnote text in the footer is linked to an element of text or otherwise through the footnote
mark.

The footnote system in **Great Tables** presents footnotes in a way that matches the usual
expectations, where:

1. footnote marks have a sequence, whether they are symbols, numbers, or letters
2. multiple footnotes can be applied to the same content (and marks are always presented in an
ordered fashion)
3. footnote text in the footer is never exactly repeated, **Great Tables** reuses footnote marks
where needed throughout the table
4. footnote marks are ordered across the table in a consistent manner (left to right, top to
bottom)

Each call of `tab_footnote()` will either add a different footnote to the footer or reuse
existing footnote text therein. One or more cells outside of the footer are targeted using
location classes from the `loc` module (e.g., `loc.body()`, `loc.column_labels()`, etc.). You
can choose to *not* attach a footnote mark by simply not specifying anything in the `locations`
argument.

By default, **Great Tables** will choose which side of the text to place the footnote mark via
the `placement="auto"` option. You are, however, always free to choose the placement of the
footnote mark (either to the `"left"` or `"right"` of the targeted cell content).

Parameters
----------
footnote
The text to be used in the footnote. We can optionally use [`md()`](`great_tables.md`) or
[`html()`](`great_tables.html`) to style the text as Markdown or to retain HTML elements in
the footnote text.
locations
The cell or set of cells to be associated with the footnote. Supplying any of the location
classes from the `loc` module is a useful way to target the location cells that are
associated with the footnote text. These location classes are: `loc.title`, `loc.stubhead`,
`loc.spanner_labels`, `loc.column_labels`, `loc.row_groups`, `loc.stub`, `loc.body`, etc.
Additionally, we can enclose several location calls within a `list()` if we wish to link the
footnote text to different types of locations (e.g., body cells, row group labels, the table
title, etc.).
placement
Where to affix footnote marks to the table content. Two options for this are `"left"` or
`"right"`, where the placement is either to the absolute left or right of the cell content.
By default, however, this option is set to `"auto"` whereby **Great Tables** will choose a
preferred left-or-right placement depending on the alignment of the cell content.

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
--------
This example table will be based on the `towny` dataset. We have a header part, with a title and
a subtitle. We can choose which of these could be associated with a footnote and in this case it
is the `"subtitle"`. This table has a stub with row labels and some of those labels are
associated with a footnote. So long as row labels are unique, they can be easily used as row
identifiers in `loc.stub()`. The third footnote is placed on the `"Density"` column label. Here,
changing the order of the `tab_footnote()` calls has no effect on the final table rendering.

```{python}
import polars as pl
from great_tables import GT, loc, md
from great_tables.data import towny

towny_mini = (
pl.from_pandas(towny)
.filter(pl.col("csd_type") == "city")
.select(["name", "density_2021", "population_2021"])
.top_k(10, by="population_2021")
.sort("population_2021", descending=True)
)

(
GT(towny_mini, rowname_col="name")
.tab_header(
title=md("The 10 Largest Municipalities in `towny`"),
subtitle="Population values taken from the 2021 census."
)
.fmt_integer()
.cols_label(
density_2021="Density",
population_2021="Population"
)
.tab_footnote(
footnote="Part of the Greater Toronto Area.",
locations=loc.stub(rows=[
"Toronto", "Mississauga", "Brampton", "Markham", "Vaughan"
])
)
.tab_footnote(
footnote=md("Density is in terms of persons per {{km^2}}."),
locations=loc.column_labels(columns="density_2021")
)
.tab_footnote(
footnote="Census results made public on February 9, 2022.",
locations=loc.subtitle()
)
.tab_source_note(
source_note=md("Data taken from the `towny` dataset.")
)
.opt_footnote_marks(marks="letters")
)
```
"""

# Store footnote as-is to preserve Text objects for later processing
footnote_str = footnote

# Handle None locations (footnote without mark)
if locations is None:
# For None location, directly add to footnotes
place = FootnotePlacement[placement]
processed_footnote = _process_text(footnote_str)
info = FootnoteInfo(locname=None, footnotes=[processed_footnote], placement=place)
return self._replace(_footnotes=self._footnotes + [info]) # type: ignore

# Ensure locations is a list
if not isinstance(locations, list):
locations = [locations]

# Apply footnote to each location
result = self
for loc in locations:
if loc is None:
# Handle None in the list
place = FootnotePlacement[placement]
processed_footnote = _process_text(footnote_str)
info = FootnoteInfo(locname=None, footnotes=[processed_footnote], placement=place)
result = result._replace(_footnotes=result._footnotes + [info]) # type: ignore
else:
# Use the new consolidated approach - FootnoteEntry will handle Text conversion internally
footnote_entry = FootnoteEntry(footnote=footnote_str, placement=placement)
result = set_style(loc, result, [footnote_entry]) # type: ignore

return result # type: ignore
4 changes: 2 additions & 2 deletions great_tables/_gt_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,7 @@ class FootnoteInfo:
locname: Loc | None = None
grpname: str | None = None
colname: str | None = None
locnum: int | None = None
locnum: int | float | None = None
rownum: int | None = None
colnum: int | None = None
footnotes: list[str] | None = None
Expand Down Expand Up @@ -1160,7 +1160,7 @@ class Options:
# footnotes_border_lr_style: OptionsInfo = OptionsInfo(True, "footnotes", "value", "none")
# footnotes_border_lr_width: OptionsInfo = OptionsInfo(True, "footnotes", "px", "2px")
# footnotes_border_lr_color: OptionsInfo = OptionsInfo(True, "footnotes", "value", "#D3D3D3")
# footnotes_marks: OptionsInfo = OptionsInfo(False, "footnotes", "values", "numbers")
footnotes_marks: OptionsInfo = OptionsInfo(False, "footnotes", "values", "numbers")
# footnotes_multiline: OptionsInfo = OptionsInfo(False, "footnotes", "boolean", True)
# footnotes_sep: OptionsInfo = OptionsInfo(False, "footnotes", "value", " ")
source_notes_padding: OptionsInfo = OptionsInfo(True, "source_notes", "px", "4px")
Expand Down
Loading
Loading