Graph for relationship between two ordinal variables What is an appropriate graph to illustrate the relationship between two ordinal variables?
A few options I can think of:


*

*Scatter plot with added random jitter to stop points hiding each other. Apparently a standard graphic - Minitab calls this an "individual values plot". In my opinion it may be misleading as it visually encourages a kind of linear interpolation between ordinal levels, as if the data were from an interval scale.

*Scatter plot adapted so that size (area) of point represents frequency of that combination of levels, rather than drawing one point for each sampling unit. I have occasionally seen such plots in practice. They can be hard to read, but the points lie on a regularly-spaced lattice which somewhat overcomes the criticism of the jittered scatter plot that it visually "intervalises" the data.

*Particularly if one of the variables is treated as dependent, a box plot grouped by the levels of the independent variable. Likely to look terrible if the number of levels of the dependent variable is not sufficiently high (very "flat" with missing whiskers or even worse collapsed quartiles which makes visual identification of median impossible), but at least draws attention to median and quartiles which are relevant descriptive statistics for an ordinal variable.

*Table of values or blank grid of cells with heat map to indicate frequency. Visually different but conceptually similar to the scatter plot with point area showing frequency.


Are there other ideas, or thoughts on which plots are preferable? Are there any fields of research in which certain ordinal-vs-ordinal plots are regarded as standard? (I seem to recall frequency heatmap being widespread in genomics but suspect that is more often for nominal-vs-nominal.) Suggestions for a good standard reference would also be very welcome, I am guessing something from Agresti.
If anyone wants to illustrate with a plot, R code for bogus sample data follows.
"How important is exercise to you?" 1 = not at all important, 2 = somewhat unimportant, 3 = neither important nor unimportant, 4 = somewhat important, 5 = very important.
"How regularly do you take a run of 10 minutes or longer?" 1 = never, 2 = less than once per fortnight, 3 = once every one or two weeks, 4 = two or three times per week, 5 = four or more times per week.
If it  would be natural to treat "often" as a dependent variable and "importance" as an independent variable, if a plot distinguishes between the two.
importance <- rep(1:5, times = c(30, 42, 75, 93, 60))
often <- c(rep(1:5, times = c(15, 07, 04, 03, 01)), #n=30, importance 1
           rep(1:5, times = c(10, 14, 12, 03, 03)), #n=42, importance 2
           rep(1:5, times = c(12, 23, 20, 13, 07)), #n=75, importance 3
           rep(1:5, times = c(16, 14, 20, 30, 13)), #n=93, importance 4
           rep(1:5, times = c(12, 06, 11, 17, 14))) #n=60, importance 5
running.df <- data.frame(importance, often)
cor.test(often, importance, method = "kendall") #positive concordance
plot(running.df) #currently useless

A related question for continuous variables I found helpful, maybe a useful starting point: What are alternatives to scatterplots when studying the relationship between two numeric variables?
 A: Using the R package riverplot:
  data$importance <- factor(data$importance, 
                            labels = c("not at all important",
                                       "somewhat unimportant",
                                       "neither important nor unimportant",
                                       "somewhat important",
                                       "very important"))
  data$often <- factor(data$often, 
                       labels = c("never",
                                  "less than once per fortnight",
                                  "once every one or two weeks",
                                  "two or three times per week",
                                  "four or more times per week"))

  makeRivPlot <- function(data, var1, var2, ...) {

    require(plyr)
    require(riverplot)
    require(RColorBrewer)

    names1 <- levels(data[, var1])
    names2 <- levels(data[, var2])

    var1 <- as.numeric(data[, var1])
    var2 <- as.numeric(data[, var2])

    edges <- data.frame(var1, var2 + max(var1, na.rm = T))
    edges <- count(edges)

    colnames(edges) <- c("N1", "N2", "Value")

    nodes <- data.frame(ID     = c(1:(max(var1, na.rm = T) +
                                      max(var2, na.rm = T))),
                        x      = c(rep(1, times = max(var1, na.rm = T)),
                                   rep(2, times = max(var2, na.rm = T))),
                        labels = c(names1, names2) ,
                        col    = c(brewer.pal(max(var1, na.rm = T), "Set1"),
                                   brewer.pal(max(var2, na.rm = T), "Set1")),
                        stringsAsFactors = FALSE)

    nodes$col <- paste(nodes$col, 95, sep = "")

    return(makeRiver(nodes, edges))

  }

a <- makeRivPlot(data, "importance", "often")

riverplot(a, srt = 45)


A: A faceted bar chart in R. It shows the distribution of "often" at each level of "importance" very clearly. But it wouldn't have worked so well if the maximum count had varied more between levels of "importance"; it's easy enough to set scales="free_y" in ggplot (see here) to avoid lots of empty space, but the shape of the distribution would be hard to discern at low-frequency levels of "importance" since the bars would be so small. Perhaps in those situations it is better to use relative frequency (conditional probability) on the vertical axis instead.

It isn't so "clean" as the tabplot in Stata that Nick Cox linked to, but conveys similar information.
R code:
library(ggplot)
running2.df <- data.frame(often = factor(often, labels = c("never", "less than once per fortnight", "once every one or two weeks", "two or three times per week", "four or more times per week")), importance = factor(importance, labels = c("not at all important", "somewhat unimportant", "neither important nor unimportant", "somewhat important", "very important")))
ggplot(running2.df, aes(often)) + geom_bar() +
  facet_wrap(~ importance, ncol = 1) +
  theme(axis.text.x=element_text(angle = -45, hjust = 0)) +
  theme(axis.title.x = element_blank())

A: A different idea that I didn't think of originally was a sieve plot.

Size of each tile is proportional to expected frequency; the little squares inside the rectangles represent actual frequencies. Hence greater density of the squares indicates higher than expected frequency (and is shaded blue); lower density of squares (red) is for lower than expected frequency.
I think I'd prefer it if the color represented the size, not just sign, of the residual. This is particularly true for edge cases where expected and observed frequencies are similar and the residual is close to zero; a dichotomous red/blue scheme seems to overemphasise small deviations.
Implementation in R:
library(vcd)
runningcounts.df <- as.data.frame(table(importance, often))
sieve(Freq ~ often + importance, data=runningcounts.df, shade= TRUE)

A: Here is a quick attempt at a heat map, I have used black cell borders to break up the cells, but perhaps the tiles should be separated more as in Glen_b's answer.

library(ggplot2)
runningcounts.df <- as.data.frame(table(importance, often))
ggplot(runningcounts.df, aes(importance, often)) +
   geom_tile(aes(fill = Freq), colour = "black") +
   scale_fill_gradient(low = "white", high = "steelblue")


Here is a fluctuation plot based on an earlier comment by Andy W. As he describes them "they are basically just binned scatterplots for categorical data, and the size of a point is mapped to the number of observations that fall within that bin." For a reference see

Wickham, Hadley and Heike Hofmann. 2011. Product
  plots. IEEE Transactions on
  Visualization and Computer Graphics (Proc. Infovis `11). Pre-print
  PDF


theme_nogrid <- function (base_size = 12, base_family = "") {
  theme_bw(base_size = base_size, base_family = base_family) %+replace% 
    theme(panel.grid = element_blank())   
}

ggplot(runningcounts.df, aes(importance, often)) +
  geom_point(aes(size = Freq, color = Freq, stat = "identity", position = "identity"), shape = 15) +
  scale_size_continuous(range = c(3,15)) + 
  scale_color_gradient(low = "white", high = "black") +
  theme_nogrid()

A: Here's an example of what a spineplot of the data would look like. I did this in Stata pretty quickly, but there's an R implementation. I think in R it should be just: 
spineplot(factor(often)~factor(importance))
The spineplot actually seems to be the default if you give R categorical variables:
plot(factor(often)~factor(importance))
The fractional breakdown of the categories of often is shown for each category of importance. Stacked bars are drawn with vertical dimension showing fraction of often given the importance category. The horizontal dimension shows the fraction in each importance category. Thus the areas of tiles formed represent the frequencies, or more generally totals, for each cross-combination of importance and often.

A: A spineplot (mosaic plot) works well for the example data here, but can be difficult to read or interpret if some combinations of categories are rare or don't exist. Naturally it's reasonable, and expected, that a low frequency is represented by a small tile, and zero by no tile at all, but the psychological difficulty can remain. It's also natural that people fond of spineplots choose examples which work well for their papers or presentations, but I've often produced examples that were too messy to use in public. Conversely, a spineplot does use the available space well. 
Some implementations presuppose interactive graphics, so that the user can interrogate each tile to learn more about it. 
An alternative which can also work quite well is a two-way bar chart (many other names exist). 
See for example tabplot within http://www.surveydesign.com.au/tipsusergraphs.html
For these data, one possible plot (produced using tabplot in Stata, but should be easy in any decent software) is 

The format means it is easy to relate individual bars to row and column identifiers and that you can annotate with frequencies, proportions or percents (don't do that if you think the result is too busy, naturally). 
Some possibilities: 


*

*If one variable can be thought of a response to another as predictor, then it is worth thinking of plotting it on the vertical axis as usual. Here I think of "importance" as measuring an attitude, the question then being whether it affects behaviour ("often"). The causal issue is often more complicated even for these imaginary data, but the point remains. 

*Suggestion #1 is always to be trumped if the reverse works better, meaning, is easier to think about and interpret. 

*Percent or probability breakdowns often make sense. A plot of raw frequencies can be useful too. (Naturally, this plot lacks the virtue of mosaic plots of showing both kinds of information at once.) 

*You can of course try the (much more common) alternatives of grouped bar charts or stacked bar charts (or the still fairly uncommon grouped dot charts in the sense of W.S. Cleveland). In this case, I don't think they work as well, but sometimes they work better. 

*Some might want to colour different response categories differently. I've no objection, and if you want that you wouldn't take objections seriously any way. 
The strategy of hybridising graph and table can be useful more generally, or indeed not what you want at all. An often repeated argument is that the separation of Figures and Tables was just a side-effect of the invention of printing and the division of labour it produced; it's once more unnecessary, just as it was to manuscript writers putting illustrations exactly how and where they liked. 
A: The way I've done this is a bit of a fudge, but it could be fixed up easily enough.
This is a modified version of the jittering approach.
Removing the axes reduces the temptation to interpret the scale as continuous; drawing boxes around the jittered combinations emphasizes there's something like a "scale break" - that the intervals aren't necessarily equal
Ideally, the 1..5 labels should be replaced with the category names, but I'll leave that for the imagination for now; I think it conveys the sense of it.
 plot(jitter(often)~jitter(importance),data=running.df,bty="n",
    ylim=c(0.5,5.5),xlim=c(0.5,5.5),cex=0.5,pty="s",xaxt="n",yaxt="n") 
 axis(1,tick=TRUE,col=0)
 axis(2,tick=TRUE,col=0)
 rect(rep(seq(0.75,4.75,1),5),rep(seq(0.75,4.75,1),each=5),
       rep(seq(1.25,5.25,1),5),rep(seq(1.25,5.25,1),each=5),
       border=8)



Possible refinements:
i) making the breaks smaller (I prefer larger breaks than this, personally), and 
ii) attempting to use a quasirandom sequence to reduce the incidence of apparent pattern within the boxes. While my attempt helped somewhat, you can see that in the cells with smaller numbers of points there are still subsequences with a more or less correlated look (e.g. the box in the top row, 2nd column). To avoid that, the quasi-random sequence might have to be initialized for each sub-box. (An alternative might be Latin Hypercube sampling.) Once that was sorted out, this could be inserted into a function that works exactly like jitter.

library("fOptions")

 hjit <- runif.halton(dim(running.df)[1],2) 
 xjit <- (hjit[,1]-.5)*0.8
 yjit <- (hjit[,2]-.5)*0.8  

 plot(I(often+yjit)~I(importance+xjit),data=running.df,bty="n",
    ylim=c(0.5,5.5),xlim=c(0.5,5.5),cex=0.5,pty="s",xaxt="n",yaxt="n") 
 axis(1,tick=TRUE,col=0)
 axis(2,tick=TRUE,col=0)
 rect(rep(seq(0.55,4.55,1),5),rep(seq(0.55,4.55,1),each=5),
       rep(seq(1.45,5.45,1),5),rep(seq(1.45,5.45,1),each=5),
       border=8)

