-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathquarto_gt.qmd
380 lines (321 loc) · 10.7 KB
/
quarto_gt.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# Quarto and {gt} {#sec-quarto}
Quarto is great!
It really is.
It makes creating a variety of documents so much easier.
For example, [my blog](https://albert-rapp.de/) runs on Quarto.
So does this book.
I think one reason why Quarto works so smoothly is because it comes with so many useful default settings.
That way, you can change aspects of your document's appearance but you don't have to.
Unfortunately, as useful as these defaults are, they get a little bit annoying when using `{gt}` with Quarto.
Take a look at how our penguin table from [@sec-getting-started] renders in Quarto.
```{r}
#| code-fold: true
library(tidyverse)
library(gt)
penguins <- palmerpenguins::penguins |> filter(!is.na(sex))
penguin_counts <- penguins |>
mutate(year = as.character(year)) |>
group_by(species, island, sex, year) |>
summarise(n = n(), .groups = 'drop')
penguin_counts_wider <- penguin_counts |>
pivot_wider(
names_from = c(species, sex),
values_from = n
) |>
# Make missing numbers (NAs) into zero
mutate(across(.cols = -(1:2), .fns = ~replace_na(., replace = 0))) |>
arrange(island, year)
actual_colnames <- colnames(penguin_counts_wider)
desired_colnames <- actual_colnames |>
str_remove('(Adelie|Gentoo|Chinstrap)_') |>
str_to_title()
names(desired_colnames) <- actual_colnames
spanners_and_header <- function(gt_tbl) {
gt_tbl |>
tab_spanner(
label = md('**Adelie**'),
columns = 3:4
) |>
tab_spanner(
label = md('**Chinstrap**'),
columns = c('Chinstrap_female', 'Chinstrap_male')
) |>
tab_spanner(
label = md('**Gentoo**'),
columns = contains('Gentoo')
) |>
tab_header(
title = 'Penguins in the Palmer Archipelago',
subtitle = 'Data is courtesy of the {palmerpenguins} R package'
)
}
penguin_table <- penguin_counts_wider |>
mutate(across(.cols = -(1:2), ~if_else(. == 0, NA_integer_, .))) |>
mutate(
island = as.character(island),
year = as.numeric(year),
island = paste0('Island: ', island)
) |>
gt(groupname_col = 'island', rowname_col = 'year') |>
cols_label(.list = desired_colnames) |>
spanners_and_header() |>
sub_missing(missing_text = '-') |>
summary_rows(
groups = TRUE,
fns = list(
'Maximum' = ~max(.),
'Total' = ~sum(.)
),
formatter = fmt_number,
decimals = 0,
missing_text = '-'
) |>
tab_options(
data_row.padding = px(2),
summary_row.padding = px(3), # A bit more padding for summaries
row_group.padding = px(4) # And even more for our groups
) |>
opt_stylize(style = 6, color = 'gray')
```
::: panel-tabset
### Regular output (screenshot)
```{r}
#| echo: false
penguin_table |>
gt::gtsave('penguins_screenshot.png')
knitr::include_graphics('penguins_screenshot.png')
```
### Quarto output
```{r}
#| echo: false
penguin_table
```
:::
But there is a workaround, right?
Otherwise, how did I manage to write this Quarto book.
Yes, there is a way.
Let me teach you the two secret ingredients to save your `{gt}` tables from Quarto.
## Convert table to HTML {#sec-convert-table-to-html}
As you have seen in [@sec-case-studies] you can transform any `{gt}` table to HTML code with `as_raw_html()`.
Let's have a look how this compares to the regular output.
::: panel-tabset
### Quarto output
```{r}
penguin_table |> as_raw_html()
```
### Regular output
::: {style="all:initial;"}
```{r}
penguin_table |> as_raw_html()
```
:::
:::
As you can see in the "Quarto Output" panel, `as_raw_html()` fixes most of the problems already.
But notice that the regular table uses narrower line heights.
So, `as_raw_html()` may not be enough.
Behind the scenes, I applied the second secret ingredient to the "Regular output" panel.
Let me tell you what I did.
## Reset CSS styles.
The CSS code to reset any styles is `style="all:initial;"`. T
hus, you can wrap your code chunk into an HTML `div` with that style.
So, what I wrote in my Quarto document looked something like
```` markdown
::: {style="all:initial;"}
```{.r}
penguin_table |> as_raw_html()
```
:::
````
In the actual document, I would use `{r}` instead of `{.r}`.
Also, you don't need the indentation in front of the code chunk.
This was just added here so that the code is displayed properly.
## Apply style isolation to all `{gt}` outputs automatically
Obviously, you do not want to write `as_raw_html()` all the time.
And that's not what I did in this book.
Thus, here's a third bonus ingredient for you.
What you'll need to do is the following:
- Write a function `knit_print.gt(x, ...)` that
1. transforms a `{gt}` table into HTML,
2. wraps the HTML code into a `<div>` with reseted style and
3. applies `knitr::asis_output()` which ensures proper HTML output.
- Overwrite the default `{gt}` output function with `registerS3method()`.
```{r}
#| echo: fenced
library(knitr)
knit_print.gt <- function(x, ...) {
stringr::str_c(
"<div style='all:initial';>\n",
gt::as_raw_html(x),
"\n</div>"
) |>
knitr::asis_output()
}
registerS3method(
"knit_print", 'gt_tbl', knit_print.gt,
envir = asNamespace("gt")
# important to overwrite {gt}s knit_print
)
```
Once this code chunk is run, you don't need to call `as_raw_html()` anymore.
But if you do, then `style="all:initial;"` is **not** applied to the output.
That's because our change only affects those outputs that are `{gt}` tables and not HTML code (that may correspond to a `{gt}` table).
::: panel-tabset
### `as_raw_html()`
```{r}
penguin_table |> as_raw_html()
```
### Regular output
```{r}
penguin_table
```
:::
Also, there is one more advantage of overwriting `knit_print.gt()`.
This way, only the style of the output is reseted.
But if you wrap your whole code chunk into `::: {style="all:initial;}` the display of the code chunk is also affected.
This is what happened earlier.
In case you haven't notice, go back to [@sec-convert-table-to-html] and compare the code chunks of the panels.
The second one uses a smaller font.
## A fallback plan
What happens if our strategy fails?
Most of the time you can just add your own custom CSS code via `opt_css()`.
This should overwrite Quarto's defaults most of the time.
But there has been one case in this book where this did not work.
Remember this table from the end of [@sec-styling]?
```{r}
#| code-fold: true
penguins_styled_tabspanner <- penguin_counts_wider |>
mutate(across(.cols = -(1:2), ~if_else(. == 0, NA_integer_, .))) |>
mutate(
island = as.character(island),
year = as.numeric(year),
island = paste0('Island: ', island)
) |>
gt(
groupname_col = 'island',
rowname_col = 'year',
id = 'fixed-penguins'
) |>
cols_label(.list = desired_colnames) |>
tab_spanner(
label = md('**Adelie**'),
columns = 3:4
) |>
tab_spanner(
label = md('**Chinstrap**'),
columns = c('Chinstrap_female', 'Chinstrap_male'),
id = 'chinstrap'
) |>
tab_spanner(
label = md('**Gentoo**'),
columns = contains('Gentoo')
) |>
tab_header(
title = 'Penguins in the Palmer Archipelago',
subtitle = 'Data is courtesy of the {palmerpenguins} R package'
) |>
sub_missing(missing_text = '-') |>
summary_rows(
groups = TRUE,
fns = list(
'Maximum' = ~max(.),
'Total' = ~sum(.)
),
formatter = fmt_number,
decimals = 0,
missing_text = '-'
) |>
tab_options(
data_row.padding = px(2),
summary_row.padding = px(3), # A bit more padding for summaries
row_group.padding = px(4) # And even more for our groups
) |>
opt_stylize(style = 6, color = 'gray') |>
tab_style(
locations = cells_column_spanners(spanners = 'chinstrap'),
style = cell_fill(color = 'dodgerblue')
) |>
opt_css(
"#fixed-penguins th[id='<strong>Chinstrap</strong>'] > span {
border-bottom-style: none;
}
"
)
penguins_styled_tabspanner
```
Notice that there is a grey border in the blue cell.
This border should not be there as we have already included the CSS code to fix that.
I'm not sure what's going on there but here's a fix.
I've written a (rudimentary) function `make_tbl_quarto_robust()` that
- converts a `{gt}` table to HTML,
- splits out the CSS part from that using text manipulation and
- replaces all `.gt_*` classes with some other name so that Quarto can't target it.
```{r}
#| code-fold: true
make_tbl_quarto_robust <- function(tbl) {
# Get tbl html code (without the inline stuff)
tbl_html <- tbl |>
as_raw_html(inline_css = FALSE)
# Find table id
tbl_id <- str_match(tbl_html, 'id="(.*)"\\s')[,2]
# Split html so that we only replace strings in the css part at first
# That's important for performance
split_html <- tbl_html |>
str_split_1('<table class="gt_table".{0,}>')
css_part <- split_html[1] |>
str_split_1('<style>')
# Create regex to add table id
my_regex <- str_c('(', tbl_id, ' )?(.* \\{)')
replaced_css <- css_part[2] |>
# Make global html changes more specific
str_replace_all('html \\{', str_c(tbl_id, ' .gt_table {')) |>
# Make changes to everything specific to the table id
str_replace_all(my_regex, str_c('\\#', tbl_id, ' \\2')) |>
# Replace duplicate names
str_replace_all(
str_c('\\#', tbl_id, ' \\#', tbl_id),
str_c('\\#', tbl_id)
)
# Put split html back together
str_c(
css_part[1], '<style>',
replaced_css, '<table class="gt_table">',
split_html[2]
) |>
# Rename all gt_* classes to new_gt_*
str_replace_all('(\\.|class="| )gt', '\\1new_gt') |>
# Reformat as html
html()
}
```
With this function we could do the same trick as before.
This will give us the output we desire.
```{r}
library(knitr)
knit_print.gt <- function(x, ...) {
stringr::str_c(
"<div style='all:initial';>\n",
make_tbl_quarto_robust(x),
"\n</div>"
) |>
knitr::asis_output()
}
registerS3method(
"knit_print", 'gt_tbl', knit_print.gt,
envir = asNamespace("gt")
# important to overwrite {gt}s knit_print
)
penguins_styled_tabspanner
```
But I really do not recommend this approach generally.
It is a brute-force solution to a slightly annoying problem that will likely be fixed in the future anyway.
Also, compared to `as_raw_html()` my function will likely not work for nested tables.
Thus, I use my own function only when `as_raw_html()` and `opt_css()` fail me (which is rare).
## Summary
Quarto and `{gt}` are great projects.
But as it is right now, they do not always play well together.
That's no problem, though.
As we have seen in this chapter, we can force them to play nicely like we want them to.
My guess is that less force will be necessary in the future.
Both projects improve all the time.
So, it is only a matter of time until this chapter becomes obsolete.
Until then, I hope that you could find the solutions you were looking for in this chapter.