
Using gtscales
gtscales.Rmd
library(gt)
library(gtscales)
library(scales)
big_number_labels <- label_number(scale_cut = cut_short_scale())
date_labels <- label_date('%b %d')gtscales adds matched legends to color-encoded
gt tables. The package is built around two common
needs:
- Color a table column and attach a matching legend in one step.
- Reuse a scale definition across multiple tables, placements, or output formats.
The package supports continuous, diverging, binned, quantile, and
discrete scales. It is designed to work naturally with
scales helpers such as label functions, break functions,
transform specifications, and palette functions.
One-step helpers
For most work, the gtscale_data_color_*() helpers are
the fastest path. They call gt::data_color() and then
attach a matching legend.
exibble |>
gt() |>
gtscale_data_color_continuous(
column = num,
palette = c('#A0442C', 'white', '#0063B1'),
labels = big_number_labels,
width = '220px',
title = 'Numeric scale'
)| num | char | fctr | date | time | datetime | currency | row | group |
|---|---|---|---|---|---|---|---|---|
| 1.111e-01 | apricot | one | 2015-01-15 | 13:35 | 2018-01-01 02:22 | 49.950 | row_1 | grp_a |
| 2.222e+00 | banana | two | 2015-02-15 | 14:40 | 2018-02-02 14:33 | 17.950 | row_2 | grp_a |
| 3.333e+01 | coconut | three | 2015-03-15 | 15:45 | 2018-03-03 03:44 | 1.390 | row_3 | grp_a |
| 4.444e+02 | durian | four | 2015-04-15 | 16:50 | 2018-04-04 15:55 | 65100.000 | row_4 | grp_a |
| 5.550e+03 | NA | five | 2015-05-15 | 17:55 | 2018-05-05 04:00 | 1325.810 | row_5 | grp_b |
| NA | fig | six | 2015-06-15 | NA | 2018-06-06 16:11 | 13.255 | row_6 | grp_b |
| 7.770e+05 | grapefruit | seven | NA | 19:10 | 2018-07-07 05:22 | NA | row_7 | grp_b |
| 8.880e+06 | honeydew | eight | 2015-08-15 | 20:20 | NA | 0.440 | row_8 | grp_b |
Numeric scale
2M4M6M8M
|
||||||||
Binned scales are useful when you want fixed intervals rather than a smooth gradient.
exibble |>
gt() |>
gtscale_data_color_bins(
column = currency,
palette = c('#f7fbff', '#08306b'),
bins = c(0, 10, 100, 1000, 10000, 70000),
title = 'Currency bins'
)| num | char | fctr | date | time | datetime | currency | row | group |
|---|---|---|---|---|---|---|---|---|
| 1.111e-01 | apricot | one | 2015-01-15 | 13:35 | 2018-01-01 02:22 | 49.950 | row_1 | grp_a |
| 2.222e+00 | banana | two | 2015-02-15 | 14:40 | 2018-02-02 14:33 | 17.950 | row_2 | grp_a |
| 3.333e+01 | coconut | three | 2015-03-15 | 15:45 | 2018-03-03 03:44 | 1.390 | row_3 | grp_a |
| 4.444e+02 | durian | four | 2015-04-15 | 16:50 | 2018-04-04 15:55 | 65100.000 | row_4 | grp_a |
| 5.550e+03 | NA | five | 2015-05-15 | 17:55 | 2018-05-05 04:00 | 1325.810 | row_5 | grp_b |
| NA | fig | six | 2015-06-15 | NA | 2018-06-06 16:11 | 13.255 | row_6 | grp_b |
| 7.770e+05 | grapefruit | seven | NA | 19:10 | 2018-07-07 05:22 | NA | row_7 | grp_b |
| 8.880e+06 | honeydew | eight | 2015-08-15 | 20:20 | NA | 0.440 | row_8 | grp_b |
Currency bins
0.4 - 1010.0 - 100100.0 - 1,0001,000.0 - 10,00010,000.0 - 65,100
|
||||||||
Quantile scales instead split the data into equally sized groups.
exibble |>
gt() |>
gtscale_data_color_quantiles(
column = num,
palette = c('#fdd49e', '#fdbb84', '#ef6548', '#990000'),
quantiles = 4,
labels = big_number_labels,
width = '220px',
title = 'Quartiles'
)| num | char | fctr | date | time | datetime | currency | row | group |
|---|---|---|---|---|---|---|---|---|
| 1.111e-01 | apricot | one | 2015-01-15 | 13:35 | 2018-01-01 02:22 | 49.950 | row_1 | grp_a |
| 2.222e+00 | banana | two | 2015-02-15 | 14:40 | 2018-02-02 14:33 | 17.950 | row_2 | grp_a |
| 3.333e+01 | coconut | three | 2015-03-15 | 15:45 | 2018-03-03 03:44 | 1.390 | row_3 | grp_a |
| 4.444e+02 | durian | four | 2015-04-15 | 16:50 | 2018-04-04 15:55 | 65100.000 | row_4 | grp_a |
| 5.550e+03 | NA | five | 2015-05-15 | 17:55 | 2018-05-05 04:00 | 1325.810 | row_5 | grp_b |
| NA | fig | six | 2015-06-15 | NA | 2018-06-06 16:11 | 13.255 | row_6 | grp_b |
| 7.770e+05 | grapefruit | seven | NA | 19:10 | 2018-07-07 05:22 | NA | row_7 | grp_b |
| 8.880e+06 | honeydew | eight | 2015-08-15 | 20:20 | NA | 0.440 | row_8 | grp_b |
Quartiles
0 - 1818 - 444444 - 391K391K - 9M
|
||||||||
Discrete legends are useful when colors encode categories rather than ordered values.
data.frame(
district = c('A', 'B', 'C', 'D'),
status = c('Safe D', 'Toss-up', 'Lean R', 'Safe R'),
margin = c(18, 2, -6, -21)
) |>
gt() |>
gtscale_data_color_discrete(
column = status,
values = c('#2166ac', '#f7f7f7', '#f4a3b4', '#b2182b'),
labels = c('Safe D', 'Toss-up', 'Lean R', 'Safe R'),
title = 'Race rating'
)| district | status | margin |
|---|---|---|
| A | Safe D | 18 |
| B | Toss-up | 2 |
| C | Lean R | -6 |
| D | Safe R | -21 |
Race rating
Safe DToss-upLean RSafe R
|
||
Legend-only helpers
Sometimes the table is already colored, or the color mapping is
handled elsewhere. In that case, the gtscale_color_*()
helpers attach only the legend.
exibble |>
gt() |>
gt::data_color(
columns = num,
method = 'numeric',
palette = c('#A0442C', 'white', '#0063B1')
) |>
gtscale_color_continuous(
column = num,
palette = c('#A0442C', 'white', '#0063B1'),
title = 'Numeric scale'
)| num | char | fctr | date | time | datetime | currency | row | group |
|---|---|---|---|---|---|---|---|---|
| 1.111e-01 | apricot | one | 2015-01-15 | 13:35 | 2018-01-01 02:22 | 49.950 | row_1 | grp_a |
| 2.222e+00 | banana | two | 2015-02-15 | 14:40 | 2018-02-02 14:33 | 17.950 | row_2 | grp_a |
| 3.333e+01 | coconut | three | 2015-03-15 | 15:45 | 2018-03-03 03:44 | 1.390 | row_3 | grp_a |
| 4.444e+02 | durian | four | 2015-04-15 | 16:50 | 2018-04-04 15:55 | 65100.000 | row_4 | grp_a |
| 5.550e+03 | NA | five | 2015-05-15 | 17:55 | 2018-05-05 04:00 | 1325.810 | row_5 | grp_b |
| NA | fig | six | 2015-06-15 | NA | 2018-06-06 16:11 | 13.255 | row_6 | grp_b |
| 7.770e+05 | grapefruit | seven | NA | 19:10 | 2018-07-07 05:22 | NA | row_7 | grp_b |
| 8.880e+06 | honeydew | eight | 2015-08-15 | 20:20 | NA | 0.440 | row_8 | grp_b |
Numeric scale
2,000,0004,000,0006,000,0008,000,000
|
||||||||
Scale specifications
For more control, use a gtscale_spec. Specs separate
scale definition from application and legend placement.
spec <- gtscale_spec_continuous(
num,
palette = c('#A0442C', 'white', '#0063B1'),
labels = big_number_labels,
width = '220px',
title = 'Numeric scale'
) |>
gtscale_spec_set_application(apply_to = 'fill') |>
gtscale_spec_set_legend(placement = 'subtitle')
exibble |>
gt() |>
gtscale_apply_legend(spec)Numeric scale
2M4M6M8M
|
||||||||
| num | char | fctr | date | time | datetime | currency | row | group |
|---|---|---|---|---|---|---|---|---|
| 1.111e-01 | apricot | one | 2015-01-15 | 13:35 | 2018-01-01 02:22 | 49.950 | row_1 | grp_a |
| 2.222e+00 | banana | two | 2015-02-15 | 14:40 | 2018-02-02 14:33 | 17.950 | row_2 | grp_a |
| 3.333e+01 | coconut | three | 2015-03-15 | 15:45 | 2018-03-03 03:44 | 1.390 | row_3 | grp_a |
| 4.444e+02 | durian | four | 2015-04-15 | 16:50 | 2018-04-04 15:55 | 65100.000 | row_4 | grp_a |
| 5.550e+03 | NA | five | 2015-05-15 | 17:55 | 2018-05-05 04:00 | 1325.810 | row_5 | grp_b |
| NA | fig | six | 2015-06-15 | NA | 2018-06-06 16:11 | 13.255 | row_6 | grp_b |
| 7.770e+05 | grapefruit | seven | NA | 19:10 | 2018-07-07 05:22 | NA | row_7 | grp_b |
| 8.880e+06 | honeydew | eight | 2015-08-15 | 20:20 | NA | 0.440 | row_8 | grp_b |
This becomes more useful when the same scale needs to be reused or when you want to separate coloring from legend placement.
Working with scales
gtscales is designed to accept the same kinds of helpers
you would already use in plots.
Labels and breaks
You can pass label functions and break functions directly.
data.frame(share = c(0.12, 0.33, 0.57, 0.91)) |>
gt() |>
gtscale_data_color_bins(
column = share,
palette = c('#f7fbff', '#08306b'),
bins = c(0, 0.25, 0.5, 0.75, 1),
labels = label_percent(),
title = 'Share bins'
)| share |
|---|
| 0.12 |
| 0.33 |
| 0.57 |
| 0.91 |
Share bins
12% - 25%25% - 50%50% - 75%75% - 91%
|
Palette functions
Palette functions from scales can be supplied
directly.
data.frame(value = c(1, 10, 100, 1000)) |>
gt() |>
gtscale_data_color_continuous(
column = value,
palette = pal_viridis(),
transform = 'log10',
breaks = breaks_log(),
labels = label_number(),
title = 'Log scale'
)| value |
|---|
| 1 |
| 10 |
| 100 |
| 1000 |
Log scale
1101001 000
|
Date and time data
Date-like columns work through the continuous and binned workflows.
In many cases, gtscales can infer the appropriate transform
from the column class.
data.frame(
when = as.Date(c('2024-01-01', '2024-01-20', '2024-02-10', '2024-03-05')),
value = c(10, 18, 35, 52)
) |>
gt() |>
gtscale_data_color_bins(
column = when,
palette = pal_viridis(),
bins = breaks_width('1 month'),
labels = date_labels,
width = '220px',
title = 'Monthly bins'
)| when | value |
|---|---|
| 2024-01-01 | 10 |
| 2024-01-20 | 18 |
| 2024-02-10 | 35 |
| 2024-03-05 | 52 |
Monthly bins
Jan 01 - Feb 01Feb 01 - Mar 01Mar 01 - Mar 05
|
|
Legend placement
Legends can be attached as source notes, subtitles, or titles.
Source notes are the default and are the most portable across outputs.
exibble |>
gt() |>
gtscale_legend(
gtscale_spec_continuous(
num,
palette = c('#A0442C', 'white', '#0063B1'),
labels = big_number_labels,
width = '220px',
title = 'Numeric scale'
) |>
gtscale_spec_set_legend(placement = 'source_note')
)| num | char | fctr | date | time | datetime | currency | row | group |
|---|---|---|---|---|---|---|---|---|
| 1.111e-01 | apricot | one | 2015-01-15 | 13:35 | 2018-01-01 02:22 | 49.950 | row_1 | grp_a |
| 2.222e+00 | banana | two | 2015-02-15 | 14:40 | 2018-02-02 14:33 | 17.950 | row_2 | grp_a |
| 3.333e+01 | coconut | three | 2015-03-15 | 15:45 | 2018-03-03 03:44 | 1.390 | row_3 | grp_a |
| 4.444e+02 | durian | four | 2015-04-15 | 16:50 | 2018-04-04 15:55 | 65100.000 | row_4 | grp_a |
| 5.550e+03 | NA | five | 2015-05-15 | 17:55 | 2018-05-05 04:00 | 1325.810 | row_5 | grp_b |
| NA | fig | six | 2015-06-15 | NA | 2018-06-06 16:11 | 13.255 | row_6 | grp_b |
| 7.770e+05 | grapefruit | seven | NA | 19:10 | 2018-07-07 05:22 | NA | row_7 | grp_b |
| 8.880e+06 | honeydew | eight | 2015-08-15 | 20:20 | NA | 0.440 | row_8 | grp_b |
Numeric scale
2M4M6M8M
|
||||||||
When you want the legend closer to the heading, use
subtitle or title.
exibble |>
gt() |>
gtscale_legend(
gtscale_spec_quantiles(
num,
palette = c('#fdd49e', '#fdbb84', '#ef6548', '#990000'),
quantiles = 4,
labels = big_number_labels,
width = '220px',
title = 'Quartiles'
) |>
gtscale_spec_set_legend(placement = 'subtitle')
)Quartiles
0 - 1818 - 444444 - 391K391K - 9M
|
||||||||
| num | char | fctr | date | time | datetime | currency | row | group |
|---|---|---|---|---|---|---|---|---|
| 1.111e-01 | apricot | one | 2015-01-15 | 13:35 | 2018-01-01 02:22 | 49.950 | row_1 | grp_a |
| 2.222e+00 | banana | two | 2015-02-15 | 14:40 | 2018-02-02 14:33 | 17.950 | row_2 | grp_a |
| 3.333e+01 | coconut | three | 2015-03-15 | 15:45 | 2018-03-03 03:44 | 1.390 | row_3 | grp_a |
| 4.444e+02 | durian | four | 2015-04-15 | 16:50 | 2018-04-04 15:55 | 65100.000 | row_4 | grp_a |
| 5.550e+03 | NA | five | 2015-05-15 | 17:55 | 2018-05-05 04:00 | 1325.810 | row_5 | grp_b |
| NA | fig | six | 2015-06-15 | NA | 2018-06-06 16:11 | 13.255 | row_6 | grp_b |
| 7.770e+05 | grapefruit | seven | NA | 19:10 | 2018-07-07 05:22 | NA | row_7 | grp_b |
| 8.880e+06 | honeydew | eight | 2015-08-15 | 20:20 | NA | 0.440 | row_8 | grp_b |
exibble |>
gt() |>
gtscale_legend(
gtscale_spec_continuous(
num,
palette = c('#A0442C', 'white', '#0063B1'),
labels = big_number_labels,
width = '220px',
title = 'Numeric scale'
) |>
gtscale_spec_set_legend(placement = 'title')
)Numeric scale
2M4M6M8M
|
||||||||
| num | char | fctr | date | time | datetime | currency | row | group |
|---|---|---|---|---|---|---|---|---|
| 1.111e-01 | apricot | one | 2015-01-15 | 13:35 | 2018-01-01 02:22 | 49.950 | row_1 | grp_a |
| 2.222e+00 | banana | two | 2015-02-15 | 14:40 | 2018-02-02 14:33 | 17.950 | row_2 | grp_a |
| 3.333e+01 | coconut | three | 2015-03-15 | 15:45 | 2018-03-03 03:44 | 1.390 | row_3 | grp_a |
| 4.444e+02 | durian | four | 2015-04-15 | 16:50 | 2018-04-04 15:55 | 65100.000 | row_4 | grp_a |
| 5.550e+03 | NA | five | 2015-05-15 | 17:55 | 2018-05-05 04:00 | 1325.810 | row_5 | grp_b |
| NA | fig | six | 2015-06-15 | NA | 2018-06-06 16:11 | 13.255 | row_6 | grp_b |
| 7.770e+05 | grapefruit | seven | NA | 19:10 | 2018-07-07 05:22 | NA | row_7 | grp_b |
| 8.880e+06 | honeydew | eight | 2015-08-15 | 20:20 | NA | 0.440 | row_8 | grp_b |
If you attach multiple legends to the same heading area, use
layout = "inline" to place them side by side.
data.frame(a = 1:3, b = 4:6) |>
gt() |>
gtscale_legend(
gtscale_spec_continuous(
a,
palette = c('#f7fbff', '#08306b'),
title = 'A'
) |>
gtscale_spec_set_legend(placement = 'subtitle', layout = 'inline')
) |>
gtscale_legend(
gtscale_spec_continuous(
b,
palette = c('#fff5eb', '#7f2704'),
title = 'B'
) |>
gtscale_spec_set_legend(placement = 'subtitle', layout = 'inline')
)|
A
1.01.52.02.53.0
B
4.04.55.05.56.0
|
|
| a | b |
|---|---|
| 1 | 4 |
| 2 | 5 |
| 3 | 6 |
Output support
Validated example workflows live in inst/examples
for:
- HTML
- PDF/LaTeX
- Typst
- DOCX
- RTF
The gt integration is strongest when legends are
attached as source notes, since that path now works across HTML, LaTeX,
DOCX, and RTF. Heading placement is still useful, but it is best
reserved for workflows where you control the output path more
tightly.