Tables with htmlTable and some alternatives

Max Gordon

2018-05-26

Introduction

Tables are an essential part of publishing, well… anything. I therefore want to explore the options available for generating these in markdown. It is important to remember that there are two ways of generating tables in markdown:

  1. Markdown tables
  2. HTML tables

As the htmlTable-package is all about HTML tables we will start with these.

HTML tables

Tables are possibly the most tested HTML-element out there. In early web design this was the only feature that browsers handled uniformly, and therefore became the standard way of doing layout for a long period. HTML-tables are thereby an excellent template for generating advanced tables in statistics. There are currently a few different implementations that I’ve encountered, the xtable, ztable, the format.tables, and my own htmlTable function. The format.tables is unfortunately not yet on CRAN and will not be part of this vignette due to CRAN rules. If you are interested you can find it here.

The htmlTable-package

I developed the htmlTable in order to get tables matching those available in top medical journals. After finding no HTML-alternative to the Hmisc::latex function on Stack Overflow I wrote a basic function allowing column spanners and row groups. Below is a basic example on these two:

output <- 
  matrix(paste("Content", LETTERS[1:16]), 
         ncol=4, byrow = TRUE)

library(htmlTable)
htmlTable(output,
          header =  paste(c("1st", "2nd",
                            "3rd", "4th"), "header"),
          rnames = paste(c("1st", "2nd",
                           "3rd", "4th"), "row"),
          rgroup = c("Group A",
                     "Group B"),
          n.rgroup = c(2,2),
          cgroup = c("Cgroup 1", "Cgroup 2&dagger;"),
          n.cgroup = c(2,2), 
          caption="Basic table with both column spanners (groups) and row groups",
          tfoot="&dagger; A table footer commment")
Basic table with both column spanners (groups) and row groups
Cgroup 1   Cgroup 2†
1st header 2nd header   3rd header 4th header
Group A
  1st row Content A Content B   Content C Content D
  2nd row Content E Content F   Content G Content H
Group B
  3rd row Content I Content J   Content K Content L
  4th row Content M Content N   Content O Content P
† A table footer commment

Example based upon Swedish statistics

In order to make a more interesting example we will try to look at how the average age changes between Swedish counties the last 15 years. Goal: visualize migration patterns.

The dataset has been downloaded from Statistics Sweden and is attached to the htmlTable-package. We will start by reshaping our tidy dataset into a more table adapted format.

data(SCB)

# The SCB has three other coulmns and one value column
library(reshape)
SCB$region <- relevel(SCB$region, "Sweden")
SCB <- cast(SCB, year ~ region + sex, value = "values")

# Set rownames to be year
rownames(SCB) <- SCB$year
SCB$year <- NULL

# The dataset now has the rows
names(SCB)
## [1] "Sweden_men"              "Sweden_women"           
## [3] "Norrbotten county_men"   "Norrbotten county_women"
## [5] "Stockholm county_men"    "Stockholm county_women" 
## [7] "Uppsala county_men"      "Uppsala county_women"
# and the dimensions
dim(SCB)
## [1] 15  8

The next step is to calculate two new columns:

  • Δint = The change within each group since the start of the observation.
  • Δstd = The change in relation to the overall age change in Sweden.

To convey all these layers of information will create a table with multiple levels of column spanners:

County
Men   Women
Age Δint. Δext.   Age Δint. Δext.
mx <- NULL
for (n in names(SCB)){
  tmp <- paste0("Sweden_", strsplit(n, "_")[[1]][2])
  mx <- cbind(mx,
              cbind(SCB[[n]], 
                    SCB[[n]] - SCB[[n]][1],
                    SCB[[n]] - SCB[[tmp]]))
}
rownames(mx) <- rownames(SCB)
colnames(mx) <- rep(c("Age", 
                      "&Delta;<sub>int</sub>",
                      "&Delta;<sub>std</sub>"), 
                    times = ncol(SCB))
mx <- mx[,c(-3, -6)]

# This automated generation of cgroup elements is 
# somewhat of an overkill
cgroup <- 
  unique(sapply(names(SCB), 
                function(x) strsplit(x, "_")[[1]][1], 
                USE.NAMES = FALSE))
n.cgroup <- 
  sapply(cgroup, 
         function(x) sum(grepl(paste0("^", x), names(SCB))), 
         USE.NAMES = FALSE)*3
n.cgroup[cgroup == "Sweden"] <-
  n.cgroup[cgroup == "Sweden"] - 2

cgroup <- 
  rbind(c(cgroup, rep(NA, ncol(SCB) - length(cgroup))),
        Hmisc::capitalize(
          sapply(names(SCB), 
                 function(x) strsplit(x, "_")[[1]][2],
                 USE.NAMES = FALSE)))
n.cgroup <- 
  rbind(c(n.cgroup, rep(NA, ncol(SCB) - length(n.cgroup))),
        c(2,2, rep(3, ncol(cgroup) - 2)))

print(cgroup)
##      [,1]     [,2]                [,3]               [,4]            
## [1,] "Sweden" "Norrbotten county" "Stockholm county" "Uppsala county"
## [2,] "Men"    "Women"             "Men"              "Women"         
##      [,5]  [,6]    [,7]  [,8]   
## [1,] NA    NA      NA    NA     
## [2,] "Men" "Women" "Men" "Women"
print(n.cgroup)
##      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
## [1,]    4    6    6    6   NA   NA   NA   NA
## [2,]    2    2    3    3    3    3    3    3

Next step is to output the table after rounding to the correct number of decimals. The txtRound function helps with this, as it uses the sprintf function instead of the round the resulting strings have the correct number of decimals, i.e. 1.02 will by round become 1 while we want it to retain the last decimal, i.e. be shown as 1.0.

htmlTable(txtRound(mx, 1), 
          cgroup = cgroup,
          n.cgroup = n.cgroup,
          rgroup = c("First period", 
                     "Second period",
                     "Third period"),
          n.rgroup = rep(5, 3),
          tfoot = txtMergeLines("&Delta;<sub>int</sub> correspnds to the change since start",
                                "&Delta;<sub>std</sub> corresponds to the change compared to national average"))
Sweden   Norrbotten county   Stockholm county   Uppsala county
Men   Women   Men   Women   Men   Women   Men   Women
Age Δint   Age Δint   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd
First period
  1999 38.9 0.0   41.5 0.0   39.7 0.0 0.8   41.9 0.0 0.4   37.3 0.0 -1.6   40.1 0.0 -1.4   37.2 0.0 -1.7   39.3 0.0 -2.2
  2000 39.0 0.1   41.6 0.1   40.0 0.3 1.0   42.2 0.3 0.6   37.4 0.1 -1.6   40.1 0.0 -1.5   37.5 0.3 -1.5   39.4 0.1 -2.2
  2001 39.2 0.3   41.7 0.2   40.2 0.5 1.0   42.5 0.6 0.8   37.5 0.2 -1.7   40.1 0.0 -1.6   37.6 0.4 -1.6   39.6 0.3 -2.1
  2002 39.3 0.4   41.8 0.3   40.5 0.8 1.2   42.8 0.9 1.0   37.6 0.3 -1.7   40.2 0.1 -1.6   37.8 0.6 -1.5   39.7 0.4 -2.1
  2003 39.4 0.5   41.9 0.4   40.7 1.0 1.3   43.0 1.1 1.1   37.7 0.4 -1.7   40.2 0.1 -1.7   38.0 0.8 -1.4   39.8 0.5 -2.1
Second period
  2004 39.6 0.7   42.0 0.5   40.9 1.2 1.3   43.1 1.2 1.1   37.8 0.5 -1.8   40.3 0.2 -1.7   38.1 0.9 -1.5   40.0 0.7 -2.0
  2005 39.7 0.8   42.0 0.5   41.1 1.4 1.4   43.4 1.5 1.4   37.9 0.6 -1.8   40.3 0.2 -1.7   38.3 1.1 -1.4   40.1 0.8 -1.9
  2006 39.8 0.9   42.1 0.6   41.3 1.6 1.5   43.5 1.6 1.4   37.9 0.6 -1.9   40.2 0.1 -1.9   38.5 1.3 -1.3   40.4 1.1 -1.7
  2007 39.8 0.9   42.1 0.6   41.5 1.8 1.7   43.8 1.9 1.7   37.8 0.5 -2.0   40.1 0.0 -2.0   38.6 1.4 -1.2   40.5 1.2 -1.6
  2008 39.9 1.0   42.1 0.6   41.7 2.0 1.8   44.0 2.1 1.9   37.8 0.5 -2.1   40.1 0.0 -2.0   38.7 1.5 -1.2   40.5 1.2 -1.6
Third period
  2009 39.9 1.0   42.1 0.6   41.9 2.2 2.0   44.2 2.3 2.1   37.8 0.5 -2.1   40.0 -0.1 -2.1   38.8 1.6 -1.1   40.6 1.3 -1.5
  2010 40.0 1.1   42.1 0.6   42.1 2.4 2.1   44.4 2.5 2.3   37.8 0.5 -2.2   40.0 -0.1 -2.1   38.9 1.7 -1.1   40.6 1.3 -1.5
  2011 40.1 1.2   42.2 0.7   42.3 2.6 2.2   44.5 2.6 2.3   37.9 0.6 -2.2   39.9 -0.2 -2.3   39.0 1.8 -1.1   40.7 1.4 -1.5
  2012 40.2 1.3   42.2 0.7   42.4 2.7 2.2   44.6 2.7 2.4   37.9 0.6 -2.3   39.9 -0.2 -2.3   39.1 1.9 -1.1   40.8 1.5 -1.4
  2013 40.2 1.3   42.2 0.7   42.4 2.7 2.2   44.7 2.8 2.5   38.0 0.7 -2.2   39.9 -0.2 -2.3   39.2 2.0 -1.0   40.9 1.6 -1.3
Δint correspnds to the change since start

Δstd corresponds to the change compared to national average

In order to increase the readability we may want to separate the Sweden columns from the county columns, one way is to use the align option with a |. Note that in 1.0 the function continues with the same alignment until the end, i.e. you no longer need count to have the exact right number of columns in your alignment argument.

htmlTable(txtRound(mx, 1), 
          align="rrrr|r",
          cgroup = cgroup,
          n.cgroup = n.cgroup,
          rgroup = c("First period", 
                     "Second period",
                     "Third period"),
          n.rgroup = rep(5, 3),
          tfoot = txtMergeLines("&Delta;<sub>int</sub> correspnds to the change since start",
                                "&Delta;<sub>std</sub> corresponds to the change compared to national average"))
Sweden   Norrbotten county   Stockholm county   Uppsala county
Men   Women   Men   Women   Men   Women   Men   Women
Age Δint   Age Δint   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd
First period
  1999 38.9 0.0   41.5 0.0   39.7 0.0 0.8   41.9 0.0 0.4   37.3 0.0 -1.6   40.1 0.0 -1.4   37.2 0.0 -1.7   39.3 0.0 -2.2
  2000 39.0 0.1   41.6 0.1   40.0 0.3 1.0   42.2 0.3 0.6   37.4 0.1 -1.6   40.1 0.0 -1.5   37.5 0.3 -1.5   39.4 0.1 -2.2
  2001 39.2 0.3   41.7 0.2   40.2 0.5 1.0   42.5 0.6 0.8   37.5 0.2 -1.7   40.1 0.0 -1.6   37.6 0.4 -1.6   39.6 0.3 -2.1
  2002 39.3 0.4   41.8 0.3   40.5 0.8 1.2   42.8 0.9 1.0   37.6 0.3 -1.7   40.2 0.1 -1.6   37.8 0.6 -1.5   39.7 0.4 -2.1
  2003 39.4 0.5   41.9 0.4   40.7 1.0 1.3   43.0 1.1 1.1   37.7 0.4 -1.7   40.2 0.1 -1.7   38.0 0.8 -1.4   39.8 0.5 -2.1
Second period
  2004 39.6 0.7   42.0 0.5   40.9 1.2 1.3   43.1 1.2 1.1   37.8 0.5 -1.8   40.3 0.2 -1.7   38.1 0.9 -1.5   40.0 0.7 -2.0
  2005 39.7 0.8   42.0 0.5   41.1 1.4 1.4   43.4 1.5 1.4   37.9 0.6 -1.8   40.3 0.2 -1.7   38.3 1.1 -1.4   40.1 0.8 -1.9
  2006 39.8 0.9   42.1 0.6   41.3 1.6 1.5   43.5 1.6 1.4   37.9 0.6 -1.9   40.2 0.1 -1.9   38.5 1.3 -1.3   40.4 1.1 -1.7
  2007 39.8 0.9   42.1 0.6   41.5 1.8 1.7   43.8 1.9 1.7   37.8 0.5 -2.0   40.1 0.0 -2.0   38.6 1.4 -1.2   40.5 1.2 -1.6
  2008 39.9 1.0   42.1 0.6   41.7 2.0 1.8   44.0 2.1 1.9   37.8 0.5 -2.1   40.1 0.0 -2.0   38.7 1.5 -1.2   40.5 1.2 -1.6
Third period
  2009 39.9 1.0   42.1 0.6   41.9 2.2 2.0   44.2 2.3 2.1   37.8 0.5 -2.1   40.0 -0.1 -2.1   38.8 1.6 -1.1   40.6 1.3 -1.5
  2010 40.0 1.1   42.1 0.6   42.1 2.4 2.1   44.4 2.5 2.3   37.8 0.5 -2.2   40.0 -0.1 -2.1   38.9 1.7 -1.1   40.6 1.3 -1.5
  2011 40.1 1.2   42.2 0.7   42.3 2.6 2.2   44.5 2.6 2.3   37.9 0.6 -2.2   39.9 -0.2 -2.3   39.0 1.8 -1.1   40.7 1.4 -1.5
  2012 40.2 1.3   42.2 0.7   42.4 2.7 2.2   44.6 2.7 2.4   37.9 0.6 -2.3   39.9 -0.2 -2.3   39.1 1.9 -1.1   40.8 1.5 -1.4
  2013 40.2 1.3   42.2 0.7   42.4 2.7 2.2   44.7 2.8 2.5   38.0 0.7 -2.2   39.9 -0.2 -2.3   39.2 2.0 -1.0   40.9 1.6 -1.3
Δint correspnds to the change since start

Δstd corresponds to the change compared to national average

If we still feel that we want more separation it is always possible to add colors.

htmlTable(txtRound(mx, 1), 
          col.columns = c(rep("#E6E6F0", 4),
                          rep("none", ncol(mx) - 4)),
          align="rrrr|r",
          cgroup = cgroup,
          n.cgroup = n.cgroup,
          rgroup = c("First period", 
                     "Second period",
                     "Third period"),
          n.rgroup = rep(5, 3),
                    tfoot = txtMergeLines("&Delta;<sub>int</sub> correspnds to the change since start",
                                "&Delta;<sub>std</sub> corresponds to the change compared to national average"))
Sweden   Norrbotten county   Stockholm county   Uppsala county
Men   Women   Men   Women   Men   Women   Men   Women
Age Δint   Age Δint   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd
First period
  1999 38.9 0.0   41.5 0.0   39.7 0.0 0.8   41.9 0.0 0.4   37.3 0.0 -1.6   40.1 0.0 -1.4   37.2 0.0 -1.7   39.3 0.0 -2.2
  2000 39.0 0.1   41.6 0.1   40.0 0.3 1.0   42.2 0.3 0.6   37.4 0.1 -1.6   40.1 0.0 -1.5   37.5 0.3 -1.5   39.4 0.1 -2.2
  2001 39.2 0.3   41.7 0.2   40.2 0.5 1.0   42.5 0.6 0.8   37.5 0.2 -1.7   40.1 0.0 -1.6   37.6 0.4 -1.6   39.6 0.3 -2.1
  2002 39.3 0.4   41.8 0.3   40.5 0.8 1.2   42.8 0.9 1.0   37.6 0.3 -1.7   40.2 0.1 -1.6   37.8 0.6 -1.5   39.7 0.4 -2.1
  2003 39.4 0.5   41.9 0.4   40.7 1.0 1.3   43.0 1.1 1.1   37.7 0.4 -1.7   40.2 0.1 -1.7   38.0 0.8 -1.4   39.8 0.5 -2.1
Second period
  2004 39.6 0.7   42.0 0.5   40.9 1.2 1.3   43.1 1.2 1.1   37.8 0.5 -1.8   40.3 0.2 -1.7   38.1 0.9 -1.5   40.0 0.7 -2.0
  2005 39.7 0.8   42.0 0.5   41.1 1.4 1.4   43.4 1.5 1.4   37.9 0.6 -1.8   40.3 0.2 -1.7   38.3 1.1 -1.4   40.1 0.8 -1.9
  2006 39.8 0.9   42.1 0.6   41.3 1.6 1.5   43.5 1.6 1.4   37.9 0.6 -1.9   40.2 0.1 -1.9   38.5 1.3 -1.3   40.4 1.1 -1.7
  2007 39.8 0.9   42.1 0.6   41.5 1.8 1.7   43.8 1.9 1.7   37.8 0.5 -2.0   40.1 0.0 -2.0   38.6 1.4 -1.2   40.5 1.2 -1.6
  2008 39.9 1.0   42.1 0.6   41.7 2.0 1.8   44.0 2.1 1.9   37.8 0.5 -2.1   40.1 0.0 -2.0   38.7 1.5 -1.2   40.5 1.2 -1.6
Third period
  2009 39.9 1.0   42.1 0.6   41.9 2.2 2.0   44.2 2.3 2.1   37.8 0.5 -2.1   40.0 -0.1 -2.1   38.8 1.6 -1.1   40.6 1.3 -1.5
  2010 40.0 1.1   42.1 0.6   42.1 2.4 2.1   44.4 2.5 2.3   37.8 0.5 -2.2   40.0 -0.1 -2.1   38.9 1.7 -1.1   40.6 1.3 -1.5
  2011 40.1 1.2   42.2 0.7   42.3 2.6 2.2   44.5 2.6 2.3   37.9 0.6 -2.2   39.9 -0.2 -2.3   39.0 1.8 -1.1   40.7 1.4 -1.5
  2012 40.2 1.3   42.2 0.7   42.4 2.7 2.2   44.6 2.7 2.4   37.9 0.6 -2.3   39.9 -0.2 -2.3   39.1 1.9 -1.1   40.8 1.5 -1.4
  2013 40.2 1.3   42.2 0.7   42.4 2.7 2.2   44.7 2.8 2.5   38.0 0.7 -2.2   39.9 -0.2 -2.3   39.2 2.0 -1.0   40.9 1.6 -1.3
Δint correspnds to the change since start

Δstd corresponds to the change compared to national average

If we add a color to the row group and restrict the rgroup spanner we may even have a more visual aid.

htmlTable(txtRound(mx, 1),
          col.rgroup = c("none", "#FFFFCC"),
          col.columns = c(rep("#EFEFF0", 4),
                          rep("none", ncol(mx) - 4)),
          align="rrrr|r",
          cgroup = cgroup,
          n.cgroup = n.cgroup,
          # I use the &nbsp; - the no breaking space as I don't want to have a
          # row break in the row group. This adds a little space in the table
          # when used together with the cspan.rgroup=1.
          rgroup = c("1st&nbsp;period", 
                     "2nd&nbsp;period",
                     "3rd&nbsp;period"),
          n.rgroup = rep(5, 3),
          tfoot = txtMergeLines("&Delta;<sub>int</sub> correspnds to the change since start",
                                "&Delta;<sub>std</sub> corresponds to the change compared to national average"),
          cspan.rgroup = 1)
Sweden   Norrbotten county   Stockholm county   Uppsala county
Men   Women   Men   Women   Men   Women   Men   Women
Age Δint   Age Δint   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd
1st period              
  1999 38.9 0.0   41.5 0.0   39.7 0.0 0.8   41.9 0.0 0.4   37.3 0.0 -1.6   40.1 0.0 -1.4   37.2 0.0 -1.7   39.3 0.0 -2.2
  2000 39.0 0.1   41.6 0.1   40.0 0.3 1.0   42.2 0.3 0.6   37.4 0.1 -1.6   40.1 0.0 -1.5   37.5 0.3 -1.5   39.4 0.1 -2.2
  2001 39.2 0.3   41.7 0.2   40.2 0.5 1.0   42.5 0.6 0.8   37.5 0.2 -1.7   40.1 0.0 -1.6   37.6 0.4 -1.6   39.6 0.3 -2.1
  2002 39.3 0.4   41.8 0.3   40.5 0.8 1.2   42.8 0.9 1.0   37.6 0.3 -1.7   40.2 0.1 -1.6   37.8 0.6 -1.5   39.7 0.4 -2.1
  2003 39.4 0.5   41.9 0.4   40.7 1.0 1.3   43.0 1.1 1.1   37.7 0.4 -1.7   40.2 0.1 -1.7   38.0 0.8 -1.4   39.8 0.5 -2.1
2nd period              
  2004 39.6 0.7   42.0 0.5   40.9 1.2 1.3   43.1 1.2 1.1   37.8 0.5 -1.8   40.3 0.2 -1.7   38.1 0.9 -1.5   40.0 0.7 -2.0
  2005 39.7 0.8   42.0 0.5   41.1 1.4 1.4   43.4 1.5 1.4   37.9 0.6 -1.8   40.3 0.2 -1.7   38.3 1.1 -1.4   40.1 0.8 -1.9
  2006 39.8 0.9   42.1 0.6   41.3 1.6 1.5   43.5 1.6 1.4   37.9 0.6 -1.9   40.2 0.1 -1.9   38.5 1.3 -1.3   40.4 1.1 -1.7
  2007 39.8 0.9   42.1 0.6   41.5 1.8 1.7   43.8 1.9 1.7   37.8 0.5 -2.0   40.1 0.0 -2.0   38.6 1.4 -1.2   40.5 1.2 -1.6
  2008 39.9 1.0   42.1 0.6   41.7 2.0 1.8   44.0 2.1 1.9   37.8 0.5 -2.1   40.1 0.0 -2.0   38.7 1.5 -1.2   40.5 1.2 -1.6
3rd period              
  2009 39.9 1.0   42.1 0.6   41.9 2.2 2.0   44.2 2.3 2.1   37.8 0.5 -2.1   40.0 -0.1 -2.1   38.8 1.6 -1.1   40.6 1.3 -1.5
  2010 40.0 1.1   42.1 0.6   42.1 2.4 2.1   44.4 2.5 2.3   37.8 0.5 -2.2   40.0 -0.1 -2.1   38.9 1.7 -1.1   40.6 1.3 -1.5
  2011 40.1 1.2   42.2 0.7   42.3 2.6 2.2   44.5 2.6 2.3   37.9 0.6 -2.2   39.9 -0.2 -2.3   39.0 1.8 -1.1   40.7 1.4 -1.5
  2012 40.2 1.3   42.2 0.7   42.4 2.7 2.2   44.6 2.7 2.4   37.9 0.6 -2.3   39.9 -0.2 -2.3   39.1 1.9 -1.1   40.8 1.5 -1.4
  2013 40.2 1.3   42.2 0.7   42.4 2.7 2.2   44.7 2.8 2.5   38.0 0.7 -2.2   39.9 -0.2 -2.3   39.2 2.0 -1.0   40.9 1.6 -1.3
Δint correspnds to the change since start

Δstd corresponds to the change compared to national average

If you want to further add to the visual hints you can use specific HTML-code and insert it into the cells. Here we will color the Δstd according to color. By default htmlTable does not escape HTML characters.

cols_2_clr <- grep("&Delta;<sub>std</sub>", colnames(mx))
# We need a copy as the formatting causes the matrix to loos
# its numerical property
out_mx <- txtRound(mx, 1)

min_delta <- min(mx[,cols_2_clr])
span_delta <- max(mx[,cols_2_clr]) - min(mx[,cols_2_clr]) 
for (col in cols_2_clr){
  out_mx[, col] <- mapply(function(val, strength)
    paste0("<span style='font-weight: 900; color: ", 
           colorRampPalette(c("#009900", "#000000", "#990033"))(101)[strength],
           "'>",
           val, "</span>"), 
    val = out_mx[,col], 
    strength = round((mx[,col] - min_delta)/span_delta*100 + 1),
    USE.NAMES = FALSE)
}

htmlTable(out_mx,
          caption = "Average age in Sweden counties over a period of
                     15 years. The Norbotten county is typically known
                     for having a negative migration pattern compared to
                     Stockholm, while Uppsala has a proportionally large 
                     population of students.",
          pos.rowlabel = "bottom",
          rowlabel="Year", 
          col.rgroup = c("none", "#FFFFCC"),
          col.columns = c(rep("#EFEFF0", 4),
                          rep("none", ncol(mx) - 4)),
          align="rrrr|r",
          cgroup = cgroup,
          n.cgroup = n.cgroup,
          rgroup = c("1st&nbsp;period", 
                     "2nd&nbsp;period",
                     "3rd&nbsp;period"),
          n.rgroup = rep(5, 3),
          tfoot = txtMergeLines("&Delta;<sub>int</sub> corresponds to the change since start",
                                "&Delta;<sub>std</sub> corresponds to the change compared to national average"),
          cspan.rgroup = 1)
Average age in Sweden counties over a period of 15 years. The Norbotten county is typically known for having a negative migration pattern compared to Stockholm, while Uppsala has a proportionally large population of students.
Sweden   Norrbotten county   Stockholm county   Uppsala county
Men   Women   Men   Women   Men   Women   Men   Women
Year Age Δint   Age Δint   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd   Age Δint Δstd
1st period              
  1999 38.9 0.0   41.5 0.0   39.7 0.0 0.8   41.9 0.0 0.4   37.3 0.0 -1.6   40.1 0.0 -1.4   37.2 0.0 -1.7   39.3 0.0 -2.2
  2000 39.0 0.1   41.6 0.1   40.0 0.3 1.0   42.2 0.3 0.6   37.4 0.1 -1.6   40.1 0.0 -1.5   37.5 0.3 -1.5   39.4 0.1 -2.2
  2001 39.2 0.3   41.7 0.2   40.2 0.5 1.0   42.5 0.6 0.8   37.5 0.2 -1.7   40.1 0.0 -1.6   37.6 0.4 -1.6   39.6 0.3 -2.1
  2002 39.3 0.4   41.8 0.3   40.5 0.8 1.2   42.8 0.9 1.0   37.6 0.3 -1.7   40.2 0.1 -1.6   37.8 0.6 -1.5   39.7 0.4 -2.1
  2003 39.4 0.5   41.9 0.4   40.7 1.0 1.3   43.0 1.1 1.1   37.7 0.4 -1.7   40.2 0.1 -1.7   38.0 0.8 -1.4   39.8 0.5 -2.1
2nd period              
  2004 39.6 0.7   42.0 0.5   40.9 1.2 1.3   43.1 1.2 1.1   37.8 0.5 -1.8   40.3 0.2 -1.7   38.1 0.9 -1.5   40.0 0.7 -2.0
  2005 39.7 0.8   42.0 0.5   41.1 1.4 1.4   43.4 1.5 1.4   37.9 0.6 -1.8   40.3 0.2 -1.7   38.3 1.1 -1.4   40.1 0.8 -1.9
  2006 39.8 0.9   42.1 0.6   41.3 1.6 1.5   43.5 1.6 1.4   37.9 0.6 -1.9   40.2 0.1 -1.9   38.5 1.3 -1.3   40.4 1.1 -1.7
  2007 39.8 0.9   42.1 0.6   41.5 1.8 1.7   43.8 1.9 1.7   37.8 0.5 -2.0   40.1 0.0 -2.0   38.6 1.4 -1.2   40.5 1.2 -1.6
  2008 39.9 1.0   42.1 0.6   41.7 2.0 1.8   44.0 2.1 1.9   37.8 0.5 -2.1   40.1 0.0 -2.0   38.7 1.5 -1.2   40.5 1.2 -1.6
3rd period              
  2009 39.9 1.0   42.1 0.6   41.9 2.2 2.0   44.2 2.3 2.1   37.8 0.5 -2.1   40.0 -0.1 -2.1   38.8 1.6 -1.1   40.6 1.3 -1.5
  2010 40.0 1.1   42.1 0.6   42.1 2.4 2.1   44.4 2.5 2.3   37.8 0.5 -2.2   40.0 -0.1 -2.1   38.9 1.7 -1.1   40.6 1.3 -1.5
  2011 40.1 1.2   42.2 0.7   42.3 2.6 2.2   44.5 2.6 2.3   37.9 0.6 -2.2   39.9 -0.2 -2.3   39.0 1.8 -1.1   40.7 1.4 -1.5
  2012 40.2 1.3   42.2 0.7   42.4 2.7 2.2   44.6 2.7 2.4   37.9 0.6 -2.3   39.9 -0.2 -2.3   39.1 1.9 -1.1   40.8 1.5 -1.4
  2013 40.2 1.3   42.2 0.7   42.4 2.7 2.2   44.7 2.8 2.5   38.0 0.7 -2.2   39.9 -0.2 -2.3   39.2 2.0 -1.0   40.9 1.6 -1.3
Δint corresponds to the change since start

Δstd corresponds to the change compared to national average

Although a graph most likely does the visualization task better, tables are good at conveying detailed information. It is in my mind without doubt easier in the latest version to find the pattern in the data.

Lastly I would like to thank Stephen Few, ThinkUI, ACAPS, and LabWrite for inspiration.

Other alternatives

The ztable-package

A promising and interesting alternative package is the ztable package. The package can also export to LaTeX and if you need this functionality it may be a good choice. The grouping for columns is currently (version 0.1.5) not working entirely as expected and the html-code does not fully validate, but the package is under active development and will hopefully soon be a fully functional alternative.

library(ztable)
options(ztable.type="html")
zt <- ztable(out_mx, 
             caption = "Average age in Sweden counties over a period of
             15 years. The Norbotten county is typically known
             for having a negative migration pattern compared to
             Stockholm, while Uppsala has a proportionally large 
             population of students.",
             zebra.type = 1,
             zebra = "peach",
             align=paste(rep("r", ncol(out_mx) + 1), collapse = ""))
# zt <- addcgroup(zt,
#                 cgroup = cgroup,
#                 n.cgroup = n.cgroup)
# Causes an error:
# Error in if (result <= length(vlines)) { : 
zt <- addrgroup(zt, 
                rgroup = c("1st&nbsp;period", 
                           "2nd&nbsp;period",
                           "3rd&nbsp;period"),
                n.rgroup = rep(5, 3))

print(zt)
Average age in Sweden counties over a period of 15 years. The Norbotten county is typically known for having a negative migration pattern compared to Stockholm, while Uppsala has a proportionally large population of students.
  Age Δint Age Δint Age Δint Δstd Age Δint Δstd Age Δint Δstd Age Δint Δstd Age Δint Δstd Age Δint Δstd
1st period
1999 38.9 0.0 41.5 0.0 39.7 0.0 0.8 41.9 0.0 0.4 37.3 0.0 -1.6 40.1 0.0 -1.4 37.2 0.0 -1.7 39.3 0.0 -2.2
2000 39.0 0.1 41.6 0.1 40.0 0.3 1.0 42.2 0.3 0.6 37.4 0.1 -1.6 40.1 0.0 -1.5 37.5 0.3 -1.5 39.4 0.1 -2.2
2001 39.2 0.3 41.7 0.2 40.2 0.5 1.0 42.5 0.6 0.8 37.5 0.2 -1.7 40.1 0.0 -1.6 37.6 0.4 -1.6 39.6 0.3 -2.1
2002 39.3 0.4 41.8 0.3 40.5 0.8 1.2 42.8 0.9 1.0 37.6 0.3 -1.7 40.2 0.1 -1.6 37.8 0.6 -1.5 39.7 0.4 -2.1
2003 39.4 0.5 41.9 0.4 40.7 1.0 1.3 43.0 1.1 1.1 37.7 0.4 -1.7 40.2 0.1 -1.7 38.0 0.8 -1.4 39.8 0.5 -2.1
2nd period
2004 39.6 0.7 42.0 0.5 40.9 1.2 1.3 43.1 1.2 1.1 37.8 0.5 -1.8 40.3 0.2 -1.7 38.1 0.9 -1.5 40.0 0.7 -2.0
2005 39.7 0.8 42.0 0.5 41.1 1.4 1.4 43.4 1.5 1.4 37.9 0.6 -1.8 40.3 0.2 -1.7 38.3 1.1 -1.4 40.1 0.8 -1.9
2006 39.8 0.9 42.1 0.6 41.3 1.6 1.5 43.5 1.6 1.4 37.9 0.6 -1.9 40.2 0.1 -1.9 38.5 1.3 -1.3 40.4 1.1 -1.7
2007 39.8 0.9 42.1 0.6 41.5 1.8 1.7 43.8 1.9 1.7 37.8 0.5 -2.0 40.1 0.0 -2.0 38.6 1.4 -1.2 40.5 1.2 -1.6
2008 39.9 1.0 42.1 0.6 41.7 2.0 1.8 44.0 2.1 1.9 37.8 0.5 -2.1 40.1 0.0 -2.0 38.7 1.5 -1.2 40.5 1.2 -1.6
3rd period
2009 39.9 1.0 42.1 0.6 41.9 2.2 2.0 44.2 2.3 2.1 37.8 0.5 -2.1 40.0 -0.1 -2.1 38.8 1.6 -1.1 40.6 1.3 -1.5
2010 40.0 1.1 42.1 0.6 42.1 2.4 2.1 44.4 2.5 2.3 37.8 0.5 -2.2 40.0 -0.1 -2.1 38.9 1.7 -1.1 40.6 1.3 -1.5
2011 40.1 1.2 42.2 0.7 42.3 2.6 2.2 44.5 2.6 2.3 37.9 0.6 -2.2 39.9 -0.2 -2.3 39.0 1.8 -1.1 40.7 1.4 -1.5
2012 40.2 1.3 42.2 0.7 42.4 2.7 2.2 44.6 2.7 2.4 37.9 0.6 -2.3 39.9 -0.2 -2.3 39.1 1.9 -1.1 40.8 1.5 -1.4
2013 40.2 1.3 42.2 0.7 42.4 2.7 2.2 44.7 2.8 2.5 38.0 0.7 -2.2 39.9 -0.2 -2.3 39.2 2.0 -1.0 40.9 1.6 -1.3

The xtable-package

The xtable is a solution that delivers both HTML and LaTeX. The syntax is very similar to kable:

output <- 
  matrix(sprintf("Content %s", LETTERS[1:4]),
         ncol=2, byrow=TRUE)
colnames(output) <- 
  c("1st header", "2nd header")
rownames(output) <- 
  c("1st row", "2nd row")

library(xtable)
print(xtable(output, 
             caption="A test table", 
             align = c("l", "c", "r")), 
      type="html")
A test table
1st header 2nd header
1st row Content A Content B
2nd row Content C Content D

The downside with the function is that you need to change output depending on your target and there is not that much advantage compared to kable.

Markdown tables

Raw tables

A markdown table is fairly straight forward and are simple to manually create. Just write the plain text below:

1st Header  | 2nd Header
----------- | -------------
Content A   | Content B
Content C   | Content D

And you will end up with this beauty:

1st Header 2nd Header
Content A Content B
Content C Content D

The knitr::kable function

Now this is not the R way, we want to use a function that does this. The knitr comes with a table function well suited for this, kable:

library(knitr)
kable(output, 
      caption="A test table", 
      align = c("c", "r"))
A test table
1st header 2nd header
1st row Content A Content B
2nd row Content C Content D

The advantage with the kable function is that it outputs true markdown tables and these can through the pandoc system be converted to any document format. Some of the downsides are:

The pander::pandoc.table function

Another option is to use the pander function that can help with text-formatting inside a markdown-compatible table (Thanks Gergely Daróczi for the tip). Here’s a simple example:

library(pander)
pandoc.table(output, emphasize.rows = 1, emphasize.strong.cols = 2)
  1st header 2nd header
1st row Content A Content B
2nd row Content C Content D

More raw markdown tables

There are a few more text alternatives available when designing tables. I included these from the manual for completeness.

| Right | Left | Default | Center |
|------:|:-----|---------|:------:|
|   12  |  12  |    12   |    12  |
|  123  |  123 |   123   |   123  |
|    1  |    1 |     1   |     1  |

: Demonstration of pipe table syntax.
Demonstration of pipe table syntax.
Right Left Default Center
12 12 12 12
123 123 123 123
1 1 1 1
: Sample grid table.

+---------------+---------------+--------------------+
| Fruit         | Price         | Advantages         |
+===============+===============+====================+
| Bananas       | $1.34         | - built-in wrapper |
|               |               | - bright color     |
+---------------+---------------+--------------------+
| Oranges       | $2.10         | - cures scurvy     |
|               |               | - tasty            |
+---------------+---------------+--------------------+
Sample grid table.
Fruit Price Advantages

Bananas

$1.34

  • built-in wrapper
  • bright color

Oranges

$2.10

  • cures scurvy
  • tasty