Skip to contents

Une version française de ce document est disponible : Introduction à tabxplor.

The first time only, install the package :

install.packages("tabxplor", dependencies = TRUE)

At the start of each new R session, load it :

tabxplor helps you explore data with cross-tables, coloring the cells so you can read a table at a glance. Over-represented cells use shades of blue, under-represented ones turn red/orange, so patterns jump out without you squinting at every number.

Everything is a tibble, so the result works with the usual dplyr verbs, and tables export to Excel, HTML and Markdown with their color helpers. Underlying heavy computations run on data.table

No R needed, if you prefer menus. Everything below is also available point-and-click, through a module for jamovi — a free, open-source statistical software. Install it, open the modules menu (the + at the top right), choose jamovi library, and install tabxplor: it adds a Crosstables analysis and a Regressions analysis, with the same coloured, exportable tables. The options carry the same names as the arguments taught here, so this vignette reads as its manual.

Throughout this vignette we use gss_simple, a cleaned-up version of the US General Social Survey (forcats::gss_cat) with factors levels merged and reordered.

gss_simple <- gss_cat_data_formatting()

Your first cross-tables

tab() needs a data frame, a row variable and a column variable. By default it shows counts:

tab(gss_simple, marital, race)
race
marital White Black Other Total
<n> <n>
Married 8 316 869 932 10 117
Separated 437 196 110 743
Divorced 2 676 495 212 3 383
Widowed 1 475 262 70 1 807
Never married 3 478 1 305 633 5 416
NA 13 2 2 17
Total 16 395 3 129 1 959 21 483

This is tabxplor’s html table — what you get in the RStudio or Positron Viewer pane with the recommended session option, used throughout this vignette:

options(tabxplor.print = "html")

Without the option, the same table prints in the console, as a colored tibble — same information, lighter display:

tab(gss_simple, marital, race)
#> # A tabxplor tab: 7 × 5
#>   marital        White Black Other  Total
#>                    <n>   <n>   <n>    <n>
#> 1 Married        8 316   869   932 10 117
#> 2 Separated        437   196   110    743
#> 3 Divorced       2 676   495   212  3 383
#> 4 Widowed        1 475   262    70  1 807
#> 5 Never married  3 478 1 305   633  5 416
#> 6 NA                13     2     2     17
#> 7 Total         16 395 3 129 1 959 21 483

Add pct = "row" for row percentages (or "col" for column percentages). A Total row/column and a count column (n) are added automatically:

tab(gss_simple, marital, race, pct = "row")
race
marital White Black Other Total
<row%> <row% (n)>
Married 82% 9% 9% 100% (10 117)
Separated 59% 26% 15% 100% (   743)
Divorced 79% 15% 6% 100% ( 3 383)
Widowed 82% 14% 4% 100% ( 1 807)
Never married 64% 24% 12% 100% ( 5 416)
NA 76% 12% 12% 100% (    17)
Total 76% 15% 9% 100% (21 483)

When the column variable is numeric, tab() shows its mean in each row instead of percentages:

tab(gss_simple, marital, age)
age
marital mean
<mean (cv)>
Married 49 (cv 31%)
Separated 45 (cv 30%)
Divorced 51 (cv 26%)
Widowed 72 (cv 18%)
Never married 34 (cv 40%)
NA 52 (cv 32%)
Total 47 (cv 37%)

You can pass several row and column variables at once.

tab(gss_simple, c(race, relig), c(party3, tvhours), na = "drop_all", pct = "row")
party3 tvhours
levels 1-Democrat 2-Independent,
other
3-Republican Total mean
<row%> <row% (n)> <mean (cv)>
race White 40% 20% 40% 100% ( 8 544) 2.8 (cv 84%)
Black 76% 16% 8% 100% ( 1 676) 4.2 (cv 83%)
Other 52% 30% 18% 100% ( 1 014) 2.7 (cv 85%)
Total 46% 20% 33% 100% (11 234) 3.0 (cv 86%)
relig 1-Protestant 44% 16% 40% 100% ( 5 629) 3.1 (cv 86%)
2-Catholic 47% 21% 32% 100% ( 2 681) 3.0 (cv 80%)
3-Other christian 42% 24% 35% 100% (   424) 2.8 (cv 90%)
4-Jewish 67% 14% 20% 100% (   189) 2.5 (cv 82%)
5-Buddhist/Hinduist 60% 22% 17% 100% (   116) 2.2 (cv 85%)
6-Muslim 63% 24% 13% 100% (    63) 2.4 (cv 89%)
7-Other 50% 29% 21% 100% (   203) 2.7 (cv 91%)
8-None 51% 31% 18% 100% ( 1 929) 2.7 (cv 95%)
Total 46% 20% 33% 100% (11 234) 3.0 (cv 86%)

A few other everyday arguments: na = "drop" to drop missing values from the base, digits = for the number of decimals, and cleannames = TRUE to strip prefixes like "1-" from level names. See ?tab for the full list.

If your numbers are already counted — a published table, a table(), a count() — start from tab_counts() instead. It builds the same object, with the same percentages, colours and tests:

counts <- dplyr::count(gss_simple, marital, race) # or a published table
tab_counts(counts, marital, race, counts = n, pct = "row", color = "difference")
race
marital White Black Other Total
<row%> <row% (n)>
Married 82% 9% 9% 100% (10 117)
Separated 59% 26% 15% 100% (   743)
Divorced 79% 15% 6% 100% ( 3 383)
Widowed 82% 14% 4% 100% ( 1 807)
Never married 64% 24% 12% 100% ( 5 416)
NA 76% 12% 12% 100% (    17)
Total 76% 15% 9% 100% (21 483)
Percentage points (risk) difference: cell ≥ the Total row +5; +10; +20; +30 points; cell ≤ the Total row -5; -10; -20; -30 points.

Colours: reading a table at a glance

One of the main purposes of tabxplor is its palette of colour helpers for data exploration. color = "difference" colors each cell by how far it sits from its reference — by default the Total of its row or column. Cells clearly above the average turn blue, cells clearly below turn red/orange — the further a cell sits from its reference, the stronger the shade — and a color legend is printed underneath.

tab(gss_simple, race, party3, pct = "row", color = "difference")
party3
race Democrat Independent,
other
Republican NA Total
<row%> <row% (n)>
White 39% 21% 40% 1% 100% (16 395)
Black 75% 16% 8% 1% 100% ( 3 129)
Other 48% 32% 18% 1% 100% ( 1 959)
Total 45% 21% 33% 1% 100% (21 483)
Percentage points (risk) difference: cell ≥ the Total row +5; +10; +20; +30 points; cell ≤ the Total row -5; -10; -20; -30 points.

color = "auto" picks a sensible scheme automatically for each column type (difference as text colour and ratio as background for percentages, ratio alone for means, …); the legend says which one:

tab(gss_simple, rincome, c(party3, marital), pct = "row", color = "auto")
party3 marital
rincome Democrat Independent,
other
Republican NA Married Separated Divorced Widowed Never married NA Total
<row%> <row%> <row% (n)>
Lt $10000 44% 25% 30% 1% 37% 5% 11% 4% 43% 0% 100% ( 2 153)
$10000 to 14999 46% 26% 27% 1% 41% 5% 16% 5% 33% 0% 100% ( 1 168)
$15000 to 24999 45% 25% 29% 0% 43% 4% 18% 3% 31% 0% 100% ( 2 331)
$25000 or more 45% 16% 38% 0% 55% 3% 18% 2% 22% 0% 100% ( 7 363)
NA 45% 22% 32% 1% 45% 3% 14% 17% 21% 0% 100% ( 8 468)
Total 45% 21% 33% 1% 47% 3% 16% 8% 25% 0% 100% (21 483)
Percentage points (risk) difference: cell ≥ the Total row +5; +10; +20; +30 points; cell ≤ the Total row -5; -10; -20; -30 points. Background colour, relative risk (ratio): cell ≥ the Total row ×1.5; ×2; cell ≤ the Total row ÷2; ÷4.

Numeric columns are colored the same way, on their means (here, hours of TV per day by income):

tab(gss_simple, rincome, tvhours, color = "difference")
tvhours
rincome mean
<mean (cv)>
Lt $10000 3.1 (cv 91%)
$10000 to 14999 3.0 (cv 78%)
$15000 to 24999 2.8 (cv 75%)
$25000 or more 2.2 (cv 74%)
NA 3.6 (cv 86%)
Total 3.0 (cv 87%)
Standardized mean difference: cell ≥ the Total row +0.1; +0.2; +0.4; +0.8 SD; cell ≤ the Total row -0.1; -0.2; -0.4; -0.8 SD.

Which cell is the reference for comparison ? By default each cell is compared to the relevant Total (Total row for row percentages and Total column for column percentages) to highlight over-representations and under-representations. Two useful alternatives:

  • ref = 1 compares each level to the first one — perfect for reading an evolution over time, or an ordinal factor. Here the deviation from the year 2000 is read as a ratio (color = "ratio").
  • with sub-tables, comp = "all" compares against the overall Total instead of each sub-table’s own Total.
tab(gss_simple, relig, year, pct = "col", color = "ratio", ref = 1)
year
relig 2000 2002 2004 2006 2008 2010 2012 2014 Total
<col%> <col%>
Protestant 54% 53% 53% 52% 51% 48% 46% 44% 50%
Catholic 24% 24% 23% 25% 23% 24% 22% 24% 24%
Other christian 2% 3% 3% 3% 4% 5% 6% 6% 4%
Jewish 2% 2% 2% 2% 2% 2% 1% 2% 2%
Buddhist/Hinduist 1% 1% 1% 1% 1% 1% 1% 2% 1%
Muslim 0% 0% 1% 0% 1% 1% 1% 0% 0%
Other 2% 2% 3% 1% 1% 2% 2% 1% 2%
None 14% 14% 14% 16% 16% 18% 20% 21% 16%
NA 0% 1% 0% 1% 0% 1% 0% 1% 1%
Total 100% 100% 100% 100% 100% 100% 100% 100% 100%
n 2 817 2 765 2 812 4 510 2 023 2 044 1 974 2 538 21 483
Relative risk (ratio): cell ≥ the reference category (in bold) ×1.1; ×1.2; ×1.5; ×2; cell ≤ ref ÷1.1; ÷1.25; ÷2; ÷4.
tab(gss_simple, rincome, party3, race, na = "drop", pct = "row", 
    color = "auto", comp="all")
party3
rincome Democrat Independent,
other
Republican Total
<row%> <row% (n)>
Lt $10000 38% 26% 36% 100% ( 1 501)
$10000 to 14999 40% 27% 33% 100% (   828)
$15000 to 24999 38% 26% 36% 100% ( 1 682)
$25000 or more 39% 17% 45% 100% ( 5 843)
Total White 39% 20% 41% 100% ( 9 854)
Lt $10000 67% 22% 11% 100% (   374)
$10000 to 14999 76% 14% 10% 100% (   207)
$15000 to 24999 79% 15% 6% 100% (   400)
$25000 or more 81% 12% 7% 100% (   881)
Total Black 77% 15% 8% 100% ( 1 862)
Lt $10000 49% 31% 20% 100% (   264)
$10000 to 14999 43% 44% 13% 100% (   125)
$15000 to 24999 45% 39% 16% 100% (   243)
$25000 or more 56% 22% 22% 100% (   618)
Total Other 51% 30% 19% 100% ( 1 250)
Total Ensemble 45% 20% 34% 100% (12 966)
Percentage points (risk) difference: cell ≥ the Total Ensemble row +5; +10; +20; +30 points; cell ≤ the Total Ensemble row -5; -10; -20; -30 points. Background colour, relative risk (ratio): cell ≥ the Total Ensemble row ×1.5; ×2; cell ≤ the Total Ensemble row ÷2; ÷4.

A different reference for each variable. ref is reinterpreted by pct. Under row percentages (or means) it picks a reference row, so a named vector gives each row variable its own — here race is read against its first row, relig against its Total:

tab(gss_simple, c(race, relig), party3, pct = "row", color = "difference",
    ref = c(race = 1, relig = "tot"), na = "drop")
party3
levels Democrat Independent,
other
Republican Total
<row%> <row% (n)>
race White 39% 21% 40% 100% (16 301)
Black 76% 17% 8% 100% ( 3 093)
Other 49% 33% 18% 100% ( 1 934)
Total 45% 21% 33% 100% (21 328)
relig Protestant 43% 17% 40% 100% (10 794)
Catholic 46% 22% 32% 100% ( 5 090)
Other christian 42% 24% 34% 100% (   778)
Jewish 68% 12% 20% 100% (   388)
Buddhist/Hinduist 57% 29% 14% 100% (   212)
Muslim 66% 22% 12% 100% (    99)
Other 48% 29% 24% 100% (   384)
None 50% 31% 19% 100% ( 3 509)
Total 45% 21% 34% 100% (21 254)
Percentage points (risk) difference: cell ≥ the reference category (in bold) +5; +10; +20; +30 points; cell ≤ ref -5; -10; -20; -30 points.

Under column percentages ref picks a reference column instead, vectorised over the column variables — either named (ref = c(party3 = "first", marital = "tot")) or positional, one value per column variable:

tab(gss_simple, race, c(party3, marital), pct = "col", color = "difference",
    ref = c("first", "tot"), na = "drop")
party3 marital
race Democrat Independent,
other
Republican Married Separated Divorced Widowed Never married Total
<col%> <col%> <col%>
White 66% 75% 92% 82% 59% 79% 82% 64% 76%
Black 24% 11% 3% 9% 26% 15% 14% 24% 15%
Other 10% 14% 5% 9% 15% 6% 4% 12% 9%
Total 100% 100% 100% 100% 100% 100% 100% 100% 100%
n 9 679 4 512 7 137 10 117 743 3 383 1 807 5 416 21 466
party3 — Percentage points (risk) difference: cell ≥ the reference category (in bold) +5; +10; +20; +30 points; cell ≤ ref -5; -10; -20; -30 points.
marital — Percentage points (risk) difference: cell ≥ the Total column +5; +10; +20; +30 points; cell ≤ the Total column -5; -10; -20; -30 points.

Color thresholds and the palette can be customised : set them once for the whole session with set_color_breaks() and set_color_palette().

Colours that respect significance

The colors above show the size of a deviation, but not whether it is statistically reliable. On small samples a big-looking difference can be pure noise. The color_signif argument brings significance into the coloring:

  • "ignore" (default): color every deviation by its observed size. Grey out small differences below a certain threshold.
  • "grey_non_signif": color by size of the effect, grey out small effects below a certain threshold, but also grey out cells with important effects that are not significant. Every colored cell is then guaranteed to be significantly different from its reference, without being bothered by very small significant differences.
  • "guaranteed_effect": color only by the part of the effect you can be confident about (its confidence bound), with dimmer, conservative colors. Use it on small samples to highlight all the differences you have the right to interpret. Everything colored is significant ; nothing grey is.
tab(gss_simple, race, party3, pct = "row", color = "difference", 
    color_signif = "grey_non_signif")
party3
race Democrat Independent,
other
Republican NA Total
<row%> <row% (n)>
White 39% 21% 40% 1% 100% (16 395)
Black 75% 16% 8% 1% 100% ( 3 129)
Other 48% 32% 18% 1% 100% ( 1 959)
Total 45% 21% 33% 1% 100% (21 483)
Percentage points (risk) difference: cell ≥ the Total row +5; +10; +20; +30 points; cell ≤ the Total row -5; -10; -20; -30 points. Uncoloured: not significantly different from the Total row (Newcombe score interval, 95% confidence) or under the first colour threshold (±5 points).
gss_simple |>
  dplyr::filter(year == "2012") |> # n=1 974
  tab(race, party3, pct = "row", color = "difference", color_signif = "guaranteed_effect")
party3
race Democrat Independent,
other
Republican NA Total
<row%> <row% (n)>
White 40% 22% 37% 1% 100% (1 477)
Black 81% 14% 5% 0% 100% (  301)
Other 47% 32% 19% 2% 100% (  196)
Total 47% 22% 30% 1% 100% (1 974)
95%-guaranteed percentage points (risk) difference (Newcombe score interval floor) from the Total row +0; +5; +10; +20; -0; -5; -10; -20 points. Uncoloured: not significantly different from the Total row.

On small samples a big-looking percentage can rest on a handful of respondents. n_min = is a purely visual filter, applied last: it hides cells whose (unweighted) base is below the threshold and drops a row entirely when its largest base falls short. Here the rarest religions drop out:

tab(gss_simple, relig, race, pct = "row", n_min = 400)
race
relig White Black Other Total
<row%> <row% (n)>
Protestant 75% 21% 4% 100% (10 846)
Catholic 78% 4% 18% 100% ( 5 124)
Other christian 72% 18% 10% 100% (   784)
None 80% 11% 9% 100% ( 3 523)
Total 76% 15% 9% 100% (21 483)

An alternative is to keep the small rows and cols but group them all in a “Other” level :

tab(gss_simple, relig, race, pct = "row",  other_if_less_than = 400)
race
relig White Black Other Total
<row%> <row% (n)>
Protestant 75% 21% 4% 100% (10 846)
Catholic 78% 4% 18% 100% ( 5 124)
Other christian 72% 18% 10% 100% (   784)
None 80% 11% 9% 100% ( 3 523)
Others 68% 10% 22% 100% ( 1 098)
NA 67% 18% 16% 100% (   108)
Total 76% 15% 9% 100% (21 483)

Confidence intervals, tests and stars

Print confidence intervals for the percentage or mean of each cell with ci = "cell" :

tab(gss_simple, race, party3, pct = "row", ci = "cell") # by default, conf_level = 0.95
party3
race Democrat Independent,
other
Republican NA Total
<row%-ci> <row% (n)>
White [38;40]% [20;21]% [39;41]% [0;1]% 100% (16 395)
Black [73;76]% [15;18]% [7;9]% [1;2]% 100% ( 3 129)
Other [46;50]% [30;34]% [16;20]% [1;2]% 100% ( 1 959)
Total [44;46]% [20;22]% [33;34]% [1;1]% 100% (21 483)

Print the confidence intervals of the difference with a reference, used to calculate significance (if 0 belongs to the confidence interval, the cell is not significantly different from the reference, here the Total row) :

gss_simple |>
  dplyr::filter(year == "2012") |> # n=1 974
  tab(race, party3, pct = "row", 
      color = "difference", ref = 1, color_signif = "guaranteed_effect",
      display = "base_ci" # "{base} {ci}"
  )
party3
race Democrat Independent,
other
Republican NA Total
<row% ci> <row% (n)>
White 40%            22%           37%            1%          100% (1 477)
Black 81% [+35;+45]% 14% [-12;-3]%  5% [-35;-28]% 0% [-1;+1]% 100% (  301)
Other 47%  [-0;+14]% 32% [+3;+17]% 19% [-24;-12]% 2% [+0;+5]% 100% (  196)
Total 47%  [+3;+10]% 22%  [-3;+3]% 30%  [-10;-4]% 1% [-1;+1]% 100% (1 974)
95%-guaranteed percentage points (risk) difference (Newcombe score interval floor) from the reference category (in bold) +0; +5; +10; +20; -0; -5; -10; -20 points. Uncoloured: not significantly different from the reference category.

display = "base_ci" is the named layout for this. {base} means “the level this column shows” — a percentage on a factor column, a mean on a numeric one — so one call works for a mix of factors and numbers, and each column answers with its own quantity. The next section covers the rest of the layouts.

display = "ci" prints that interval on its own. Add significance stars with stars = TRUE: they tell the same story as the interval of the deviation from the reference, but at fixed confidence levels (99 %, 95 %, 90 %) :

gss_simple |>
  dplyr::filter(year == "2012") |> # n=1 974
  tab(rincome, party3, pct = "row", ref = 1, display = "ci", stars = TRUE)
party3
rincome Democrat Independent,
other
Republican NA Total
<mixed> <n>
Lt $10000 40%   33%    27%   0% (  200)
$10000 to 14999 [-6;+17]%   [-16;+5]%    [-11;+10]%   [-1;+6]% (  103)
$15000 to 24999 [+1;+20]%** [-17;+1]%*   [-11;+6]%   [-3;+1]% (  194)
$25000 or more [+2;+18]%** [-25;-10]%*** [+0;+14]%** [-2;+1]% (  649)
NA [+0;+15]%** [-18;-4]%*** [-4;+9]%   [-2;+2]% (  828)
Total [+1;+15]%** [-18;-5]%*** [-3;+9]%   [-2;+1]% (1 974)
***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant.

test = TRUE adds a statistical test of independence per (sub-)table — Chi-squared for factor columns, Welch’s F ANOVA for numeric variables (options(tabxplor.anova = "classic") switches to the pooled F):

tab(gss_simple, race, c(party3, tvhours), pct = "row", test = TRUE)
party3 tvhours
race Democrat Independent,
other
Republican NA Total mean
<row%> <row% (n_range)> <mean (cv)>
White 39% 21% 40% 1% 100% ( 8 610-16 395) 2.8 (cv 84%)
Black 75% 16% 8% 1% 100% ( 1 700- 3 129) 4.2 (cv 84%)
Other 48% 32% 18% 1% 100% ( 1 027- 1 959) 2.8 (cv 87%)
Total 45% 21% 33% 1% 100% (11 337-21 483) 3.0 (cv 87%)
pvalue (Chi2, Welch F) <0.01% <0.01%
Cramér’s V, eta2 0.21 0.04

Batteries of yes/no items, and a score

Multiple-answer questions — “which of these apply to you?” — reach the data as a battery of yes/no factors, one per item. levels = "first" keeps only the first level of each column factor, so a whole battery fits in one compact table, one column per item.

facto_tea, which ships with tabxplor (300 tea drinkers, from the FactoMineR package of François Husson, Julie Josse, Sébastien Lê and Jérémy Mazet — with thanks; see ?facto_tea), has such a battery: when do you drink tea?, asked as six yes/no items. Everything below depends on one thing — the “yes” answer has to be each factor’s first level, which is how the shipped copy stores it:

tea_when_vars <- c("breakfast", "lunch", "tea.time", "evening", "dinner", "always")
# levels(facto_tea$breakfast)   # always check: the "yes" answer must come first

score_from_lv1() reduces that same battery to a single summed score: for each person it counts the factors sitting at their first level — here, at how many of the six moments they drink tea (missing values never count). The score is an ordinary numeric variable, so it takes its place in the same table, as a mean:

tea <- facto_tea |> score_from_lv1("tea_when", vars_list = tea_when_vars) # score variable
tab(tea, SPC, all_of(c(tea_when_vars, "tea_when")), pct = "row", 
    levels = "first", na = "drop", color = "difference")
breakfast lunch tea.time evening dinner always tea_when
SPC n breakfast_lv lunch_lv tea time evening_lv dinner_lv always_lv mean
<n> <row%> <row%> <row%> <row%> <row%> <row%> <mean (cv)>
employee 59 49% 7% 53% 44% 14% 34% 2.0 (cv 54%)
middle 40 60% 5% 48% 30% 0% 28% 1.7 (cv 52%)
non-worker 64 44% 20% 59% 20% 3% 23% 1.7 (cv 56%)
other worker 20 40% 0% 60% 40% 10% 35% 1.9 (cv 50%)
senior 35 63% 26% 57% 31% 3% 34% 2.1 (cv 50%)
student 70 43% 21% 61% 44% 7% 50% 2.3 (cv 50%)
workman 12 25% 8% 50% 17% 25% 25% 1.5 (cv 53%)
Total 300 48% 15% 56% 34% 7% 34% 1.9 (cv 54%)
breakfast, lunch, tea.time, evening, dinner, always — Percentage points (risk) difference: cell ≥ the Total row +5; +10; +20; +30 points; cell ≤ the Total row -5; -10; -20; -30 points.
tea_when — Standardized mean difference: cell ≥ the Total row +0.1; +0.2; +0.4; +0.8 SD; cell ≤ the Total row -0.1; -0.2; -0.4; -0.8 SD.

Each percentage reads down its own column: 63 % of senior managers drink tea at breakfast, against 25 % of manual workers. There is no Total column, and rightly so — the six items do not add up to 100 %, since one person can tick several. The score plays that role instead: 2.1 moments a day on average for senior managers, 1.5 for manual workers.

levels = "auto" makes that choice per variable: it keeps the first level only of two-level factors, and every level of the others. It is what you want when a battery of yes/no items sits beside an ordinary factor — here two tea moments and the social category, in one table:

tab(tea, sex, c(breakfast, evening, SPC), pct = "row", 
    levels = "auto", na = "drop", tot = "row")
breakfast evening SPC
sex n breakfast_lv evening_lv employee middle non-worker other worker senior student workman
<n> <row%> <row%> <row%>
F 178 47% 33% 21% 12% 24% 7% 5% 29% 2%
M 122 50% 37% 17% 16% 17% 7% 21% 16% 7%
Total 300 48% 34% 20% 13% 21% 7% 12% 23% 4%

A battery is also what spread_vars was made for — see the most compact table below.

A summed score is also exactly what a grouped-binomial regression models — see trials in vignette("tabxplor-reg").

Sub-tables, and several row variables

Give tab() a third variable as tab_vars and it builds one sub-table per group (here, one per income group). The result is grouped: dplyr operations then run within each sub-table.

tab(gss_simple, race, party3, rincome, na = "drop", pct = "row")
party3
race Democrat Independent,
other
Republican Total
<row%> <row% (n)>
White 38% 26% 36% 100% ( 1 501)
Black 67% 22% 11% 100% (   374)
Other 49% 31% 20% 100% (   264)
Total Lt $10000 45% 26% 30% 100% ( 2 139)
White 40% 27% 33% 100% (   828)
Black 76% 14% 10% 100% (   207)
Other 43% 44% 13% 100% (   125)
Total $10000 to 14999 47% 26% 27% 100% ( 1 160)
White 38% 26% 36% 100% ( 1 682)
Black 79% 15% 6% 100% (   400)
Other 45% 39% 16% 100% (   243)
Total $15000 to 24999 46% 25% 29% 100% ( 2 325)
White 39% 17% 45% 100% ( 5 843)
Black 81% 12% 7% 100% (   881)
Other 56% 22% 22% 100% (   618)
Total $25000 or more 45% 16% 38% 100% ( 7 342)
Total Ensemble 45% 20% 34% 100% (12 966)

When you pass several row variables without tab_vars, tab() merges the mirror tables into a single table by default. output_list = TRUE returns instead a list with one table per row variable (with tab_vars, the result is always a list):

tab(gss_simple, c(married, income25k), race, pct = "row", output_list = TRUE)

The most compact table: one block of columns per group

spread_vars shows a sub-table variable across the page instead of down it: each of its levels becomes a block of columns. Put it together with levels = "first" (one column per variable, see above) and you get the most condensed table the package can draw — several variables, several groups, one screen:

tab(gss_simple, rincome, c(married, tvhours), tab_vars = race, spread_vars = race,
    pct = "row", na = "drop", levels = "first", comp = "all",
    color = "auto", color_signif = "grey_non_signif")
married tvhours
rincome White Black Other Ensemble White Black Other Ensemble White Black Other Ensemble
<row%> <row%> <row%> <row%> <mean> <mean> <mean> <mean> <n_range> <n_range> <n_range> <n_range>
Lt $10000 41% 20% 34% 37% 2.8 4.7 2.7 3.1   800-1 506   206-  377 151-  270 1 157- 2 153
$10000 to 14999 45% 24% 44% 41% 2.7 4.4 2.9 3.0   465-  832   124-  210  76-  126   665- 1 168
$15000 to 24999 47% 28% 46% 43% 2.6 3.6 3.0 2.8   825-1 685   221-  400 113-  246 1 159- 2 331
$25000 or more 57% 38% 59% 55% 2.1 3.1 2.0 2.2 3 055-5 856   463-  886 329-  621 3 847- 7 363
Total 52% 31% 49% 49% 2.4 3.7 2.4 2.6 5 145-9 879 1 014-1 873 669-1 263 6 828-13 015
White married, Black married, Other married, Ensemble married — Percentage points (risk) difference: cell ≥ the Total Ensemble row +5; +10; +20; +30 points; cell ≤ the Total Ensemble row -5; -10; -20; -30 points. Background colour, relative risk (ratio): cell ≥ the Total Ensemble row ×1.5; ×2; cell ≤ the Total Ensemble row ÷2; ÷4. Uncoloured: not significantly different from the Total Ensemble row (Newcombe score interval, 95% confidence) or under the first colour threshold (±5 points).
White tvhours, Black tvhours, Other tvhours, Ensemble tvhours — Ratio of means: cell ≥ the Total Ensemble row ×1.1; ×1.2; ×1.5; ×2; cell ≤ the Total Ensemble row ÷1.1; ÷1.2; ÷1.5; ÷2. Uncoloured: not significantly different from the Total Ensemble row (Welch t interval, 95% confidence; Welch closed form) or under the first colour threshold (×1.1).

The layout follows from the shape, and the table says so:

  • there is one Total row for the whole table; each block answers in its own columns.
  • the base count takes one n column per block, gathered at the right, so the counts can be read against each other. A Total column per block would only repeat 100%.
  • comp = "all" compares every cell against the overall total rather than its own group’s — here the Ensemble block’s Total cell, the one number every shade in the table is measured from. The color legend names it.
  • a total line cannot become a block of columns, so totaltab = "line" (the default) is promoted to "table", and the message says so. Say totaltab = "no" if you want no overall block at all.

A variable named in spread_vars alone is added to tab_vars for you, so tab(gss_simple, rincome, party3, spread_vars = race, pct = "row") is a complete call.

Weights

Give wt = a weight column and every percentage and mean becomes an estimate of the population rather than of the people you happened to interview:

gss_w <- dplyr::mutate(gss_simple, w = ifelse(marital %in% "Never married", 2.5, 0.8))
tab(gss_w, race, party3, wt = w, pct = "row", na = "drop")
party3
race Democrat Independent,
other
Republican Total
<row%> <row% (n)>
White 41% 22% 37% 100% (16 301)
Black 74% 18% 8% 100% ( 3 093)
Other 51% 33% 17% 100% ( 1 934)
Total 48% 22% 30% 100% (21 328)
Weighted by w; confidence intervals and tests use the unweighted sample size.

That is the easy half. The margins of error around those percentages have three levels, and a table’s footer always says which one it is on:

  1. weighted percentages, plain margins of error — the default, and the convention almost every textbook uses. Under unequal weights it runs a little too narrow.
  2. design_effect = TRUE — every interval, star, colour threshold and test accounts for the unequal weighting, exactly.
  3. a survey design passed as data — everything follows the full design: strata, clusters, calibration.

Which one you need, what each costs you, and whether it matters for your table: vignette("tabxplor-weights").

Exporting tables

A finished table exports with its colors to Excel, HTML or Markdown:

tabs <- tab(gss_simple, race, party3, pct = "row", color = "difference")
tab_export(tabs) # default : html table (RStudio Viewer, .Rmd/.qmd, etc.)
tab_export(tabs, format = "xl", path = "table") # Excel export 
tab_export(tabs, format = "md", path = "table") # flat markdown file

Functions tab_html(), tab_xl(), tab_md() do the same thing.

Getting a table into Word goes through Excel, and that is the recommended route rather than a workaround: the workbook holds the real numbers, not rounded strings, so you can still fix a decimal or a label there. Then copy the cells and paste them into Word — from the desktop app, not the browser version, which drops the formatting. Colours, bold and borders all survive.

Two options are worth knowing:

  • theme = "auto" lets an HTML or Markdown export follow the reader’s light/dark mode (it flips live). For the console, set_color_palette(theme = "auto") detects the editor (RStudio, Positron, etc.) and picks the matching palette — it is applied automatically when the package loads.
tab_export(tabs, theme = "auto") # HTML that follows the reader's light/dark modes
  • Since numeric variables can only be passed in columns, some complex layout with numeric variables in rows need to transpose the table during export using transpose = TRUE :
tab(gss_simple, party3, c(race, tvhours), pct = "row",
    color = "ratio", display = "base", n = "min") |>
  tab_html(transpose = TRUE)
party3
Democrat Independent, other Republican NA Total
race White 66% 75% 92% 61% 76%
Black 24% 11% 3% 23% 15%
Other 10% 14% 5% 16% 9%
Total 100% 100% 100% 100% 100%
n 5 220 2 319 3 734 64 11 337
tvhours mean 3.2 3.1 2.7 3.2 3.0
race — Relative risk (ratio): cell ≥ the Total row ×1.1; ×1.2; ×1.5; ×2; cell ≤ the Total row ÷1.1; ÷1.25; ÷2; ÷4.
tvhours — Ratio of means: cell ≥ the Total row ×1.1; ×1.2; ×1.5; ×2; cell ≤ the Total row ÷1.1; ÷1.2; ÷1.5; ÷2.
  • One stylesheet for a whole document. In an .Rmd/.qmd report, tab_css() writes the colour CSS once and every later table emits only classes, so a single theme — including "auto", which follows the reader’s light/dark mode — styles every table at once. This very vignette does exactly that (with theme = "light"):
options(tabxplor.tab_kable_css = FALSE)
tab_css(theme = "auto")   # emit once, near the top of the document

Nothing is written inline on a cell, so any look is overridable with plain CSS afterwards (column widths, fonts…); see ?tab_css for the role classes (.tx-rv, .tx-tot, .tx-num).

Black and white, for publication

Colors are for exploring. For a journal, theme = "print_ready" renders the same reading in black and white. It is not one palette but a choice of one, made from what the table is, so a cross-table and a regression table each get the treatment that suits them. A cross-table gets "print_marks", where every cell states its own direction and size in its own characters — one superscript or per colour threshold it crosses, and an underline from the third. Those marks replace the significance stars rather than sitting beside them, and the legend names the typography instead of the hues. ("print_minimalistic", the bold/italic/underline palette, is still there by name.)

tab(gss_simple, race, party3, pct = "row", color = "difference") |>
  tab_html(theme = "print_ready")
party3
race Democrat Independent,
other
Republican NA Total
<row%> <row% (n)>
White 39%   21%   40%   1% 100% (16 395)
Black 75%⁺⁺⁺ 16%   8%⁻⁻⁻ 1% 100% ( 3 129)
Other 48%    32%⁺⁺ 18%⁻⁻ 1% 100% ( 1 959)
Total 45%    21%   33%    1% 100% (21 483)
Percentage points (risk) difference: cell ≥ the Total row +5⁺; +10⁺⁺; +20⁺⁺⁺; +30⁺⁺⁺⁺ points; cell ≤ the Total row -5⁻; -10⁻⁻; -20⁻⁻⁻; -30⁻⁻⁻⁻ points.

This is not merely a taste: converted to lightness, the two directions of the color palette come out as the same shades of grey, so a grayscale print loses over-vs-under entirely. It works in every export — tab_html(), tab_md() and tab_xl() (real bold/italic/underline in the spreadsheet) — and the typography is written as <b>/<i>/<u> markup as well as CSS, so it also survives a paste into Word. The marks are cell text, so a plain-text copy keeps them and a screen reader reads them aloud.

One option sets that typographic palette for every export at once:

options(tabxplor.theme = "print_ready")

You rarely need to ask for it. Every stylesheet already carries this palette in an @media print block, so a colored html table prints — or saves to PDF from your browser — publication-ready on its own. Set options(tabxplor.print_rules = FALSE) if your printer is a color one and the colors are the point.

What the cell shows: the display grammar

Every cell already holds far more than the one number it prints — its count, its percentage, its difference from the reference, its confidence interval. display chooses which of them you see. Nothing is recomputed: a display is picked after the table is built, so changing it never changes a number.

The quickest route is a named layout. The everyday ones:

display = the cell shows
"base" the level alone — the percentage, the mean or the count (the default)
"base_ci" the level with its confidence interval, 48.6 [45.1; 52.1]
"base_moe" the level with its margin of error, 48.6 ± 3.5
"base_diff" the level and, in brackets, its difference from the reference
"base_ratio" the level and the same comparison as a ratio
"mean_sd" a mean and its standard deviation (numeric columns)
"mean_cv" a mean and its coefficient of variation — the spread as a percentage of the level, so two columns measured in different units become comparable (the default on numeric columns)
tab(gss_simple, race, c(party3, tvhours), pct = "row", display = "base_moe")
party3 tvhours
race Democrat Independent,
other
Republican NA Total mean
<row%> <row% (n_range)> <mean>
White 39% 21% 40% 1% 100% ( 8 610-16 395) 2.8
Black 75% 16% 8% 1% 100% ( 1 700- 3 129) 4.2
Other 48% 32% 18% 1% 100% ( 1 027- 1 959) 2.8
Total 45% 21% 33% 1% 100% (11 337-21 483) 3.0

Or write your own, with a {} template naming the fields you want. display = "{pct} ({diff})" prints each percentage followed by its difference from the reference; "{pct} (n={n})" follows it with the count:

tab(gss_simple, race, party3, pct = "row", color = "difference", display = "{pct} ({diff})")
party3
race Democrat Independent,
other
Republican NA Total
<row% (diff)> <row% (n)>
White 39% ( -6%) 21% ( +0%) 40% ( +7%) 1% (+0%) 100% (16 395)
Black 75% (+30%) 16% ( -5%)  8% (-26%) 1% (+0%) 100% ( 3 129)
Other 48% ( +3%) 32% (+11%) 18% (-15%) 1% (+1%) 100% ( 1 959)
Total 45% (  0%) 21% (  0%) 33% (  0%) 1% ( 0%) 100% (21 483)
Percentage points (risk) difference: cell ≥ the Total row +5; +10; +20; +30 points; cell ≤ the Total row -5; -10; -20; -30 points.

Two rules are worth knowing. The first field outside brackets is the primary one: it carries the significance stars, it is what an Excel export keeps as a real number, and it is the only part the colours paint. And a field can carry its own precision, {pct:1} ({n:0}), which beats the table’s digits — useful when an aside would otherwise be printed to as many decimals as the estimate.

On a table you have already built, set_display() does the same thing:

tabs <- tab(gss_simple, race, party3, pct = "row")
set_display(tabs, "{pct} (n={n})")
party3
race Democrat Independent,
other
Republican NA Total
<row% (n)> <row% (n)>
White 39% (n=6 390) 21% (n=3 365) 40% (n=6 546) 1% (n= 94) 100% (16 395)
Black 75% (n=2 344) 16% (n=  513)  8% (n=  236) 1% (n= 36) 100% ( 3 129)
Other 48% (n=  945) 32% (n=  634) 18% (n=  355) 1% (n= 25) 100% ( 1 959)
Total 45% (n=9 679) 21% (n=4 512) 33% (n=7 137) 1% (n=155) 100% (21 483)

?tab lists every field a template can name; vignette("tabxplor-programming") explains the record behind them.

Which cells make the table interesting? (color = "contrib")

Every colour you have seen so far compares a cell to a reference you chose — the Total row, the first row. color = "contrib" asks a different question, and needs no reference at all: which cells depart from what we would expect if the two variables were unrelated? It is the natural way to colour a table of raw counts (a count has no reference), and it is what color = "auto" picks for one.

There are two useful ways to answer that question, and color_signif chooses between them.

1. “Which cells build this association?”

tab(gss_simple, race, party3, color = "contrib")
party3
race Democrat Independent,
other
Republican NA Total
<n> <n>
White 6 390 3 365 6 546 94 16 395
Black 2 344 513 236 36 3 129
Other 945 634 355 25 1 959
Total 9 679 4 512 7 137 155 21 483
pvalue (Chi2) <0.01%
Cramér’s V 0.21
Contribution to Chi2: cell over-represented vs independence, by ×1; ×2; ×5; ×10 the mean contribution; cell under-represented, by ×1; ×2; ×5; ×10 the mean contribution.
# tab(gss_simple, race, party3, pct = "all", color = "contrib")  # works with pct too

Each cell is coloured by its share of the table’s Chi-squared, written as a multiple of the average cell: ×1 means “this cell carries an average share”, ×5 means “five times the average”. The strong cells are, literally, the ones that make the table’s association what it is.

This scale is relative to this table: it says how the association is distributed inside it, not how big it is. That is exactly what a correspondence analysis reads, and it is why this is the default reading — but it also means you cannot compare a ×2 here with a ×2 in another table.

If you have met log-linear models for contingency tables (Goodman’s tradition, familiar from social mobility research), this colouring is their descriptive core: the chi-squared is the log-linear model of independence, and each cell’s contribution is its departure from it — so a color = "contrib" table is a heatmap of the association pattern, read at a glance. For the specialist models built on top of that — quasi-independence, RC association models, UNIDIFF for comparing fluidity across groups or cohorts — use the logmult package, which also supports complex survey designs. (Note that “log-linear” in this sense means models of cell counts in a table; it is unrelated to tab_reg(family = "poisson"), which models an individual outcome.)

The share itself can be printed as well as coloured. set_display("ctr") gives each cell’s percentage of the table’s chi-squared, and the Total row gives the average cell’s share — which is the ×1 the colours are measured against:

tab(gss_simple, race, party3, color = "contrib") |> set_display("ctr")
party3
race Democrat Independent,
other
Republican NA Total
<n-ctr> <n-ctr>
White -7% 0% 12% 0% 0%
Black 32% -2% -33% 0% 0%
Other 0% 6% -7% 0% 0%
Total mean:8% mean:8% mean:8% mean:8% 8%
pvalue (Chi2) <0.01%
Cramér’s V 0.21
Contribution to Chi2: cell over-represented vs independence, by ×1; ×2; ×5; ×10 the mean contribution; cell under-represented, by ×1; ×2; ×5; ×10 the mean contribution.

2. “Which cells are notably off, on a scale I can compare?”

Add color_signif = "guaranteed_effect" and the colour switches to the standardized residual — how many standard errors each cell sits away from what independence predicts:

tab(gss_simple, race, party3, color = "contrib", color_signif = "guaranteed_effect") |>
  set_display("resid")
party3
race Democrat Independent,
other
Republican NA Total
<n-resid> <n-resid>
White -32.1 -3.1 +37.5 -4.6
Black +36.3 -6.8 -33.0 +3.1
Other +3.0 +12.9 -14.9 +3.0
Total
pvalue (Chi2) <0.01%
Cramér’s V 0.21
Standardized residual: cell over-represented vs independence, by +1.96; +2.58; +3.89; +6; cell under-represented, by -1.96; -2.58; -3.89; -6. Uncoloured: below the significance threshold (95% confidence). The thresholds above are comparable between tables.

Read it with the rule you may know from SPSS: beyond ±2 a cell is notable, beyond ±3 strongly so. Positive means over-represented, negative under-represented. Unlike the share above, ±3 means the same thing in every table, so you can compare two tables — and any cell that is not significant stays grey.

The middle option, color_signif = "grey_non_signif", keeps the contribution scale of (1) but greys out cells that are not significant.

In one line: use the default to see where the association lives, guaranteed_effect to see which cells are individually notable.

Either number reads beside the percentages, with a display template:

tab(gss_simple, race, party3, pct = "row", color = "contrib",
    display = "{pct} ({resid})")
party3
race Democrat Independent,
other
Republican NA Total
<row% (resid)> <row% (n)>
White 39% (-32.1) 21% ( -3.1) 40% (+37.5) 1% (-4.6) 100% (16 395)
Black 75% (+36.3) 16% ( -6.8)  8% (-33.0) 1% (+3.1) 100% ( 3 129)
Other 48% ( +3.0) 32% (+12.9) 18% (-14.9) 1% (+3.0) 100% ( 1 959)
Total 45%         21%         33%         1%        100% (21 483)
pvalue (Chi2) <0.01%
Cramér’s V 0.21
Contribution to Chi2: cell over-represented vs independence, by ×1; ×2; ×5; ×10 the mean contribution; cell under-represented, by ×1; ×2; ×5; ×10 the mean contribution.

Hovering an html table shows both numbers (ctr: and resid:) in the tooltip, whatever you display: every tooltip line is named after the field it shows, so the hover uses the same words as the column headers and as $.

If you need to cite it: this is Haberman’s adjusted standardized residual — SPSS’s “adjusted residual”, R’s chisq.test()$stdres — not the (o − e)/√e that several tools also call “standardized”, which sits below a ±2 scale and under-states. As everywhere in tabxplor the test is per cell, so in a 30-cell table expect one or two false positives at 5 % (±3 is roughly a Bonferroni correction at that size). See ?tab.

Hover tooltips (html tables)

Every html table carries per-cell hover tooltips with the numbers behind the cell: the unweighted count, the difference from the reference, the ratio, the confidence interval… They are on by default in the Viewer and in reports — this vignette only switched them off document-wide with options(tabxplor.tab_kable_tooltips = FALSE), to keep the page light. So you write no argument at all — hover the cells of the table below, where they are switched back on:

tab(gss_simple, race, party3, pct = "row", color = "difference")
party3
race Democrat Independent,
other
Republican NA Total
<row%> <row% (n)>
White 39% 21% 40% 1% 100% (16 395)
Black 75% 16% 8% 1% 100% ( 3 129)
Other 48% 32% 18% 1% 100% ( 1 959)
Total 45% 21% 33% 1% 100% (21 483)
Percentage points (risk) difference: cell ≥ the Total row +5; +10; +20; +30 points; cell ≤ the Total row -5; -10; -20; -30 points.

A note on weights. With a weight (wt =), every proportion or mean is weighted, but by default the sample size behind the confidence intervals and tests stays the real, unweighted number of observations — the footer says so. Under unequal weights that carries no design effect, so it runs a little too narrow : options(tabxplor.design_effect = TRUE) (see Weights) widens every interval by exactly the weighting’s own design effect and switches the whole-table Chi2 / F tests to their design-based counterparts.

Deviations and their confidence intervals, charted: forest_plot()

A colored table already reads at a glance. When the pattern is what matters, forest_plot() draws the same numbers: one whisker per cell, in the cell’s own colour, with the percentage or mean printed just above it.

tab(tea, SPC, c(breakfast, lunch, evening, dinner), pct = "row",
    levels = "first", na = "drop",
    color = "ratio", color_signif = "guaranteed_effect", ref = 1) |>
  forest_plot()

The axis is centred on the reference level, here employee. What a point’s position shows is the deviation — the comparison your color = grades: a difference from the reference in percentage points, a ratio or an odds ratio on a log axis. The level it sits on is the number above the whisker, so the two say different things. what = "level" swaps them.

The whisker is the confidence interval, so a level whose whisker crosses the reference line is not significantly different from it.

Two more things are worth knowing:

  • The gridlines are your colour breaks — one dashed rule per threshold, in that threshold’s own colour, continued beyond the last one (each step twice the one before) as far as your data goes. Change them with set_color_breaks() and the axis moves with the table.
  • It reads the table, and computes nothing. It returns a plain ggplot, so + ggplot2::labs(...), + ggplot2::theme(...) and ggsave() work; theme = "print_ready" gives the greyscale version, guide = "bands" shades the panel behind the whiskers with the colour scale itself (a good way to show a class what the colours mean).

forest_plot() draws regression tables too — see vignette("tabxplor-reg").

Working with the result

tab() returns a tibble (of class tabxplor_tab), so dplyr verbs just work. Use the helper is_totrow() to keep the Total row in place when you re-order (it flags total rows, so sorting on it first sends them to the bottom):

library(dplyr)
tab(gss_simple, race, marital, pct = "row") |>
  arrange(desc(Married))
marital
race Married Separated Divorced Widowed Never married NA Total
<row%> <row% (n)>
White 51% 3% 16% 9% 21% 0% 100% (16 395)
Other 48% 6% 11% 4% 32% 0% 100% ( 1 959)
Black 28% 6% 16% 8% 42% 0% 100% ( 3 129)
Total 47% 3% 16% 8% 25% 0% 100% (21 483)

Titling and annotating. subtext = prints one or more legend lines under a table (a data source, a note). set_caption() gives a table a title that survives a dplyr pipeline, and every exporter uses it as the table title:

tab(gss_simple, race, marital, pct = "row",
    subtext = c("Population: ", "Source: GSS, 2000-2014")) |>
  set_caption("Custom title")
Custom title
marital
race Married Separated Divorced Widowed Never married NA Total
<row%> <row% (n)>
White 51% 3% 16% 9% 21% 0% 100% (16 395)
Black 28% 6% 16% 8% 42% 0% 100% ( 3 129)
Other 48% 6% 11% 4% 32% 0% 100% ( 1 959)
Total 47% 3% 16% 8% 25% 0% 100% (21 483)
Population:
Source: GSS, 2000-2014

Global R options

A handful of options() set your preferred defaults once for the whole session — put them at the top of a script, or in your .Rprofile. Each one has a per-call argument too; the option just changes the default. The everyday ones:

  • options(tabxplor.print = "html") — print tables not in console, but as html in RStudio or Positron Viewer Pane by default (recommended)
  • options(tabxplor.cleannames = TRUE) — strip "1-"-style prefixes from level names everywhere.
  • options(tabxplor.parallel = 8) — parallelise tables with multiples variables on different CPU cores by default (needs mirai)
  • options(tabxplor.var_labels = TRUE) — in exports, show a variable’s label (from haven/labelled data) instead of its bare name.
  • options(tabxplor.theme = "auto") — the export theme ("light"/"dark"/"auto"); set_color_palette(theme = "auto") does the same for the console.
  • options(tabxplor.stars = TRUE) — show significance stars in every table (like stars = TRUE).
  • options(tabxplor.conf_level = 0.9) — the confidence level for intervals and tests (default 0.95).
  • options(tabxplor.design_effect = TRUE) — on weighted data, make every interval, star, colour threshold and test account for the unequal weighting (see vignette("tabxplor-weights")).
  • options(tabxplor.lang = "fr") — the language of the colour legends and footers ("auto"/"en"/"fr").

Colour thresholds and palettes have their own helpers, set_color_breaks() and set_color_palette(). ?tabxplor-options documents every option, and vignette("tabxplor-programming") covers the more advanced ones (export fonts, parallel builds…).

Where to go next