Skip to contents
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:

  1. Color a table column and attach a matching legend in one step.
  2. 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.