Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for granular section restyling via convenience api #341

Merged
merged 33 commits into from
Sep 27, 2024

Conversation

timkpaine
Copy link
Contributor

@timkpaine timkpaine commented May 14, 2024

Summary

This is a work-in-progress PR for allowing more granular styling of the various components via the API currently used for cell styling (e.g. loc.body()). The motivation is that it's often desired to style e.g. all column labels at once. It looks like this was already started in part (there are many TODOs such as these).

This PR is nowhere near done yet, so please don't approve CI running (it will just make noise).

Early Example:

from great_tables import GT, style, loc
from superstore import superstore

(
    GT(superstore(10))
    .tab_style(style=style.text(color="red"), locations=loc.column_labels())
)
Screenshot 2024-05-14 at 13 42 14

Checklist

@timkpaine timkpaine force-pushed the tkp/sectionstyle branch 6 times, most recently from d1e303d to ee9d265 Compare May 14, 2024 17:28
@timkpaine
Copy link
Contributor Author

Screenshot 2024-05-14 at 13 53 17

@timkpaine
Copy link
Contributor Author

Screenshot 2024-05-14 at 14 09 07

great_tables/_locations.py Outdated Show resolved Hide resolved
@machow
Copy link
Collaborator

machow commented May 14, 2024

Hey, thanks for this incredibly comprehensive PR! At first I was nervous about the idea of a PR implementing all locations at once, but after seeing your changes, it seems very reasonable! Happy to kick the tires, help with anything :)

@machow
Copy link
Collaborator

machow commented May 14, 2024

Here's my quick attempt at mapping the Loc names here, to those used in the R library gt:

code
from great_tables import GT
import polars as pl

# I made this in a spreadsheet, then used pl.DataFrame.to_init_repr() :)
df = pl.DataFrame(
    [
        pl.Series("Location category", ['Header', 'Stubhead', 'Column labels', None, 'Stub elements', None, None, 'Body elements', None, 'Footer elements', None, None, None], dtype=pl.String),
        pl.Series("gt R package", ['cells_title', 'cells_stubhead', 'cells_column_labels', 'cells_column_spanners', 'cells_stub', 'cells_row_groups', 'cells_stub_summary', 'cells_body', 'cells_summary', 'cells_footnotes', 'cells_source_notes', 'cells_grand_summary', 'cells_stub_grand_summary'], dtype=pl.String),
        pl.Series("Great Tables", ['LocHeader', 'LocStubhead', 'LocColumnLabels*', 'LocSpannerLabel', 'LocStub', 'LocRowGroupLabel', 'LocSummaryLabel', 'LocBody', 'LocSummary', 'LocFootnotes', 'LocSourceNotes', None, None], dtype=pl.String),
        pl.Series("extra1", ['LocTitle', 'LocStubheadLabel', 'LocColumnLabel', None, 'LocRowLabel', None, None, None, None, 'LocFooter', None, None, None], dtype=pl.String),
        pl.Series("extra2", ['LocSubtitle', None, None, None, None, None, None, None, None, None, None, None, None], dtype=pl.String),
    ]
)
GT(df).sub_missing(missing_text="")
image

It seems like you may have surfaced two important things:

  1. The R program sometimes maps to sub-locations. E.g. cells_title(groups=...) -> title or subtitle.
  2. If you just represent the sub-locations directly (e.g. your use of LocTitle and LocSubtitle), then they can be used directly. This seems like a nice simplification :).

Potentially replacing StyleInfo(loc="some_string")

It seems like defining all Loc sub-locations might also let us replace things like StyleInfo(loc="footnotes") with StyleInfo(loc=loc), where loc is the LocFootnotes object itself. Replacing all the string checks with isinstance would make it very easy to find references to Loc classes in VS Code, etc..!

In any event, this is looking really great! Thanks for this huge effort---I'm happy to check anything, do anything to support!

@machow
Copy link
Collaborator

machow commented Sep 16, 2024

I'm going to try and complete this PR this week, if that's okay! It's looking really good, and I think will be nice to have for WIP on rendering GT objects interactively (in reactable-py) 😁

Copy link

codecov bot commented Sep 17, 2024

Codecov Report

Attention: Patch coverage is 97.43590% with 4 lines in your changes missing coverage. Please review.

Project coverage is 87.54%. Comparing base (3bafd96) to head (febbd70).
Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
great_tables/_locations.py 97.64% 2 Missing ⚠️
great_tables/_gt_data.py 85.71% 1 Missing ⚠️
great_tables/_utils_render_html.py 98.18% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #341      +/-   ##
==========================================
+ Coverage   87.14%   87.54%   +0.39%     
==========================================
  Files          42       42              
  Lines        4768     4840      +72     
==========================================
+ Hits         4155     4237      +82     
+ Misses        613      603      -10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@timkpaine
Copy link
Contributor Author

🚀

@machow
Copy link
Collaborator

machow commented Sep 27, 2024

Alright, I think this is ready for review.

Completed:

  • feat: loc.*() specifiers implemented / renamed based on feedback from Rich (see diagram below)
  • feat: added style.css(), which enables adding raw css strings
  • refactor: StyleInfo.loc now just holds Loc instances, .locnum is removed

TODO in new PRs next week:

  • styles should always be applied in the order they were specified in. This mostly is the case now, except currently "composite locations", like loc.header() are always applied before ones like loc.title().
  • test and harden any selectors happening inside locs, e.g. loc.stub(rows = ...)
  • docstrings inside loc classes, added to API

Loc specification

Here is the final loc specification we settled on for this PR. It specifies 11 location classes. 3 of those are composite locations: header(), footer(), and column_header(). 5 allow selecting specific parts based on data values.

image

Note these important details:

  • composite locations are ones that select multiple locations. For example, loc.header() selects both loc.title() and loc.subtitle()
  • composite locations always apply styles in the same place as the locations they select. In other words, they never try to set styles on a containing element around locations they select.

@machow machow requested a review from rich-iannone September 27, 2024 19:27
@machow machow marked this pull request as ready for review September 27, 2024 19:27
@@ -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]]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

now returning GroupRowInfo directly, since we need the group ids, not just their defaulted labels to match group name locs

@@ -869,8 +870,7 @@ class FootnoteInfo:

@dataclass(frozen=True)
class StyleInfo:
locname: str
locnum: int
locname: Loc
Copy link
Collaborator

Choose a reason for hiding this comment

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

locname with a Loc class now fully identifies a style info, so no need for locnum

Copy link
Member

Choose a reason for hiding this comment

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

Great!

@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)
Copy link
Collaborator

@machow machow Sep 27, 2024

Choose a reason for hiding this comment

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

Since we removed .locnum and just use the Loc isntances directly, we were able to consolidate most simple versions of set_style()

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))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note that this currently can't match row groups by their name (which is also their id). It can only match by index number or match every group.

Copy link
Member

Choose a reason for hiding this comment

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

Totally okay since we don't currently make it possible for the user to set nice IDs anyhow.

Copy link
Member

@rich-iannone rich-iannone left a comment

Choose a reason for hiding this comment

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

LGTM!

@machow
Copy link
Collaborator

machow commented Sep 27, 2024

Shoot, @timkpaine I accidentally did a squash merge, but this doesn't let me see which lines I changed in the blame (so I can scold myself for any dumb changes I made). I reverted the merge, but apparently you can't reopen a merged PR :/.

Any chance you might be able to open a new PR against this repo again(can do it from tkp/sectionstyle)? I can do it from a fork of yours, but I think gh would make it look like I contributed the PR 😞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants