Tagged with visualization

Table as an Image in R

Usually, it's best to keep tables as text, but if you're making a lot of graphics, it can be helpful to be able to create images of tables.

PNG table

Creating the Table

After loading the data, let's first use this trick to put line breaks between the levels of the effect variable. Depending on your data, you may or may not need or want to do this.

library(OIdata)
data(birds)
library(gridExtra)

# line breaks between words for levels of birds$effect:
levels(birds$effect) <- gsub(" ", "\n", levels(birds$effect))

Next let's make our table:

xyTable <- table(birds$sky, birds$effect)

Now we can create an empty plot, center our table in it, and use the grid.table function from the gridExtra package to display the table and choose a font size.

plot.new()
grid.table(xyTable,
    # change font sizes:
    gpar.coltext = gpar(cex = 1.2),
    gpar.rowtext = gpar(cex = 1.2))

Now you can view and save the image just like any other plot.

The code is available in a gist.

Citations and Further Reading

Tagged , , , , , ,

Line Breaks Between Words in Axis Labels in ggplot in R

Sometimes when plotting factor variables in R, the graphics can look pretty messy thanks to long factor levels. If the level attributes have multiple words, there is an easy fix to this that often makes the axis labels look much cleaner.

Without Line Breaks

Here's the messy looking example:

No line breaks in axis labels

And here's the code for the messy looking example:

library(OIdata)
data(birds)
library(ggplot2)

ggplot(birds,
    aes(x = effect,
        y = speed)) +
geom_boxplot()

With Line Breaks

We can use regular expressions to add line breaks to the factor levels by substituting any spaces with line breaks:

library(OIdata)
data(birds)
library(ggplot2)

levels(birds$effect) <- gsub(" ", "\n", levels(birds$effect))
ggplot(birds,
    aes(x = effect,
        y = speed)) +
geom_boxplot()

Line breaks in axis labels

Just one line made the plot look much better, and it will carry over to other plots you make as well. For example, you could create a table with the same variable.

Horizontal Boxes

Here we can see the difference in a box plot with horizontal boxes. It's up to you to decide which style looks better:

No line breaks in axis labels

Line breaks in axis labels

library(OIdata)
data(birds)
library(ggplot2)

levels(birds$effect) <- gsub(" ", "\n", levels(birds$effect))
ggplot(birds,
    aes(x = effect,
        y = speed)) +
geom_boxplot() + 
coord_flip()

Just a note: if you're not using ggplot, the multi-line axis labels might overflow into the graph.

The code is available in a gist.

Citations and Further Reading

In a comment, Jason Bryer mentioned that you can also break the lines by using a set character width instead of breaking at every space. Here's the code he suggested: :::r sapply(strwrap(as.character(value), width=25, simplify=FALSE), paste, collapse="\n")

Tagged , , , , , , ,

Custom Legend in R

This particular custom legend was designed with three purposes:

  • To effectively bin values based on a theoretical minimum and maximum value for that variable (e.g. -1 and 1 or 0 and 100)
  • To use a different interval notation than the default
  • To handle NA values

Even though this particular legend was designed with those needs, it should be simple to extrapolate from that to build legends based on other criteria.

Standard Legend

For this post, I'll be assuming you've looked through the Oregon map tutorial or have other experience making legends in R. If not, you'll probably want to check that link out. It's an awesome tutorial.

Let's start by creating a map with a standard legend, and then we move on to customization later.

First, we'll load the packages we need and the data from OIdata:

library(OIdata)
library(RColorBrewer)
library(classInt)

# load state data from OIdata package:
data(state)

Next we want to set some constants. This will save us a bunch of typing and will make the code easier to read, especially once we start creating a custom legend. Also, it will allow us to easily change the values if we want a different number of bins or a different min and max.

In this example, we're assuming we have a theoretical minimum and maximum and want to determine our choropleth bins based on that.

nclr <- 8 # number of bins
min <- 0 # theoretical minimum
max <- 100 # theoretical maximum
breaks <- (max - min) / nclr

Next, we'll set up our choropleth colors (this should look familiar from the Oregon tutorial):

# set up colors:
plotclr <- brewer.pal(nclr, "Oranges")
plotvar <- state$coal
class <- classIntervals(plotvar,
    nclr,
    style = "fixed",
    fixedBreaks = seq(min, max, breaks))
colcode <- findColours(class, 
    plotclr)

And now let's map the data:

# map data:
map("state", # base
    col = "gray80",
    fill = TRUE,
    lty = 0)
map("state", # data
    col = colcode,
    fill = TRUE,
    lty = 0,
    add = TRUE)
map("state", # border
    col = "gray",
    lwd = 1.4,
    lty = 1,
    add = TRUE)

And finally let's add our default legend:

legend("bottomleft", # position
    legend = names(attr(colcode, "table")), 
    title = "Percent",
    fill = attr(colcode, "palette"),
    cex = 0.56,
    bty = "n") # border

Here's the output of this code (see map-standard-legend.R in the gist):

Percent of power coming from coal sources (standard legend)

Custom Legend

Next we want to add a few lines here and there to enhance the legend.

For starters, let's deal with NA values. We don't have any in this particular dataset, but if we did, we would have seen they were left as the base color of the map and not included in the legend.

After our former code setting up the colors, we should add the color for NAs. It's important that these lines go after all the other set up code, or the wrong colors will be mapped.

# set up colors:
plotclr <- brewer.pal(nclr, "Oranges")
plotvar <- state$coal
class <- classIntervals(plotvar,
    nclr,
    style = "fixed",
    fixedBreaks = seq(min, max, breaks))
colcode <- findColours(class, 
    plotclr)
NAColor <- "gray80"
plotclr <- c(plotclr, NAColor)

We also want to let the map know to have our NA color as the default color, so the map will use that instead of having those areas be transparent:

# map data:
map("state", # base
    col = NAColor,
    fill = TRUE,
    lty = 0)
map("state", # data
    col = colcode,
    fill = TRUE,
    lty = 0,
    add = TRUE)
map("state", # border
    col = "gray",
    lwd = 1.4,
    lty = 1,
    add = TRUE)

Next, we want to set up the legend text. For all but the last interval, we want it to say i ≤ n < (i + breaks). The last interval should be i ≤ n ≤ (i + breaks). This can be accomplished by

# set legend text:
legendText <- c()
for(i in seq(min, max - (max - min) / nclr, (max - min) / nclr)) {
    if (i == max(seq(min, max - (max - min) / nclr, (max - min) / nclr))) {
        legendText <- c(legendText, paste(round(i,3), "\u2264 n \u2264", round(i + (max - min) / nclr,3)))
    } else
        legendText <- c(legendText, paste(round(i,3), "\u2264 n <", round(i + (max - min) / nclr,3))) 
}

But we also want to include NAs in the legend, so we need to add a line:

# set legend text:
legendText <- c()
for(i in seq(min, max - (max - min) / nclr, (max - min) / nclr)) {
    if (i == max(seq(min, max - (max - min) / nclr, (max - min) / nclr))) {
        legendText <- c(legendText, paste(round(i,3), "\u2264 n \u2264", round(i + (max - min) / nclr,3)))
        if (!is.na(NAColor)) legendText <- c(legendText, "NA")
    } else
        legendText <- c(legendText, paste(round(i,3), "\u2264 n <", round(i + (max - min) / nclr,3))) 
}

And finally we need to add the legend to the map:

legend("bottomleft", # position
    legend = legendText, 
    title = "Percent",
    fill = plotclr,
    cex = 0.56,
    bty = "n") # border

The new map (see map-new-legend.R) meets all the criteria we started with that the original legend didn't have.

Percent of power coming from coal sources (custom legend)

Code is available in a gist.

Citations and Further Reading

Tagged , , , , , ,

Plot Weekly or Monthly Totals in R

When plotting time series data, you might want to bin the values so that each data point corresponds to the sum for a given month or week. This post will show an easy way to use cut and ggplot2's stat_summary to plot month totals in R without needing to reorganize the data into a second data frame.

Let's start with a simple sample data set with a series of dates and quantities:

library(ggplot2)
library(scales)

# load data:
log <- data.frame(Date = c("2013/05/25","2013/05/28","2013/05/31","2013/06/01","2013/06/02","2013/06/05","2013/06/07"), 
  Quantity = c(9,1,15,4,5,17,18))
log
str(log)


:::r
> log
        Date Quantity
1 2013/05/25        9
2 2013/05/28        1
3 2013/05/31       15
4 2013/06/01        4
5 2013/06/02        5
6 2013/06/05       17
7 2013/06/07       18

> str(log)
'data.frame': 7 obs. of  2 variables:
 $ Date    : Factor w/ 7 levels "2013/05/25","2013/05/28",..: 1 2 3 4 5 6 7
 $ Quantity: num  9 1 15 4 5 17 18

Next, if the date data is not already in a date format, we'll need to convert it to date format:

# convert date variable from factor to date format:
log$Date <- as.Date(log$Date,
    "%Y/%m/%d") # tabulate all the options here
str(log)


:::r
> str(log)
'data.frame': 7 obs. of  2 variables:
 $ Date    : Date, format: "2013-05-25" "2013-05-28" ...
 $ Quantity: num  9 1 15 4 5 17 18

Next we need to create variables stating the week and month of each observation. For week, cut has an option that allows you to break weeks as you'd like, beginning weeks on either Sunday or Monday.

# create variables of the week and month of each observation:
log$Month <- as.Date(cut(log$Date,
  breaks = "month"))
log$Week <- as.Date(cut(log$Date,
  breaks = "week",
  start.on.monday = FALSE)) # changes weekly break point to Sunday
log

> log
        Date Quantity      Month       Week
1 2013-05-25        9 2013-05-01 2013-05-19
2 2013-05-28        1 2013-05-01 2013-05-26
3 2013-05-31       15 2013-05-01 2013-05-26
4 2013-06-01        4 2013-06-01 2013-05-26
5 2013-06-02        5 2013-06-01 2013-06-02
6 2013-06-05       17 2013-06-01 2013-06-02
7 2013-06-07       18 2013-06-01 2013-06-02

Finally, we can create either a line or bar plot of the data by month and by week, using stat_summary to sum up the values associated with each week or month:

# graph by month:
ggplot(data = log,
    aes(Month, Quantity)) +
    stat_summary(fun.y = sum, # adds up all observations for the month
        geom = "bar") + # or "line"
    scale_x_date(
        labels = date_format("%Y-%m"),
        breaks = "1 month") # custom x-axis labels

Time series plot, binned by month

# graph by week:
ggplot(data = log,
    aes(Week, Quantity)) +
    stat_summary(fun.y = sum, # adds up all observations for the week
        geom = "bar") + # or "line"
    scale_x_date(
        labels = date_format("%Y-%m-%d"),
        breaks = "1 week") # custom x-axis labels

Time series plot, totaled by week

The full code is available in a gist.

In a comment, Achim Zeileis pointed out that the aggregation part can be more easily handled using time series packages like zoo or xts.

References

Tagged , , , , , , ,

geom_point Legend with Custom Colors in ggplot

Formerly, I showed how to make line segments using ggplot.

Working from that previous example, there are only a few things we need to change to add custom colors to our plot and legend in ggplot.

First, we'll add the colors of our choice. I'll do this using RColorBrewer, but you can choose whatever method you'd like.

library(RColorBrewer)
colors = brewer.pal(8, "Dark2")

The next section will be exactly the same as the previous example, except for removing the scale_color_discrete line to make way for the scale_color_manual we'll be adding later.

library(ggplot2)

data <- as.data.frame(USPersonalExpenditure) # data from package datasets
data$Category <- as.character(rownames(USPersonalExpenditure)) # this makes things simpler later

ggplot(data,
    aes(x = Expenditure,
        y = Category)) +
labs(x = "Expenditure",
    y = "Category") +
geom_segment(aes(x = data$"1940",
        y = Category,
        xend = data$"1960",
        yend = Category),
    size = 1) +
geom_point(aes(x = data$"1940",
        color = "1940"), # these can be any string, they just need to be unique identifiers
    size = 4,
    shape = 15) +
geom_point(aes(x = data$"1960",
        color = "1960"),
    size = 4,
    shape = 15) +
theme(legend.position = "bottom") +

And finally, we'll add a scale_color_manual line to our plot. We need to define the name, labels, and colors of the plot.

scale_color_manual(name = "Year", # or name = element_blank()
    labels = c(1940, 1960),
    values = colors)

And here's our final plot, complete with whatever custom colors we've chosen in both the plot and legend:

geom_point in ggplot with custom colors in the graph and legend

I've updated the gist from the previous post to also include a file that has custom colors.

Tagged , , , , , ,

Shapefiles in R

Let's learn how to use Shapefiles in R. This will allow us to map data for complicated areas or jurisdictions like zipcodes or school districts. For the United States, many shapefiles are available from the [Census Bureau](http://www.census.gov/geo/www/tiger/tgrshp2010/tgrshp2010.html. Our example will map U.S. national parks.

First, download the U.S. Parks and Protected Lands shape files from Natural Earth. We'll be using the ne_10m_parks_and_protected_lands_area.shp file.

Next, start working in R. First, we'll load the shapefile and maptools:

# load up area shape file:
library(maptools)
area <- readShapePoly("ne_10m_parks_and_protected_lands_area.shp")

# # or file.choose:
# area <- readShapePoly(file.choose())

Next we can set the colors we want to use. And then we can set up our basemap.

library(RColorBrewer)
colors <- brewer.pal(9, "BuGn")

library(ggmap)
mapImage <- get_map(location = c(lon = -118, lat = 37.5),
    color = "color",
    source = "osm",
    # maptype = "terrain",
    zoom = 6)

Next, we can use the fortify function from the ggplot2 package. This converts the crazy shape file with all its nested attributes into a data frame that ggmap will know what to do with.

area.points <- fortify(area)

Finally, we can map our shape files!

ggmap(mapImage) +
    geom_polygon(aes(x = long,
            y = lat,
            group = group),
        data = area.points,
        color = colors[9],
        fill = colors[6],
        alpha = 0.5) +
labs(x = "Longitude",
    y = "Latitude")

National Parks and Protected Lands in California and Nevada

Same figure, with a Stamen terrain basemap with ColorBrewer palette "RdPu"

Citations and Further Reading

Tagged , , , , , , , , ,

Elevation Profiles in R

First, let's load up our data. The data are available in a gist. You can convert your own GPS data to .csv by following the instructions here, using gpsbabel.

gps <- read.csv("callan.csv",
    header = TRUE)

Next, we can use the function SMA from the package TTR to calculate a moving average of the altitude or elevation data, if we want to smooth out the curve. We can define a constant for the number of data points we want to average to create each moving average value.

If you don't want to convert meters to feet, a metric version of the code is available in the gist (callanMetric.R).

library(TTR)
movingN <- 5 # define the n for the moving average calculations
gps$Altitude <- gps$Altitude * 3.281 # convert m to ft
gps$SMA <- SMA(gps$Altitude,
    n = movingN)
gps <- gps[movingN:length(gps$SMA), ] # remove first n-1 points

Next, we want to calculate the distance of each point. You can skip this step if your dataset already includes distances.

library(sp)
Dist <- 0
for(i in 2:length(gps$Longitude)) {
    Dist[i] = spDistsN1(as.matrix(gps[i,c("Longitude", "Latitude")]),
    c(gps$Longitude[i-1], gps$Latitude[i-1]),
    longlat = TRUE) / 1.609 # longlat so distances will be in km, then divide to convert to miles
}
gps$Dist <- Dist

DistTotal <- 0
for(i in 2:length(gps$Longitude)) {
    DistTotal[i] = Dist[i] + DistTotal[i-1]
}
gps$DistTotal <- DistTotal

And finally, we can plot our elevation data using geom_ribbons and ggplot:

library(ggplot2)
ggplot(gps, aes(x = DistTotal)) +
geom_ribbon(aes(ymin = 600, # change this to match your min below
        ymax = SMA),
    fill = "#1B9E77") + # put your altitude variable here if not using moving averages
labs(x = "Miles",
    y = "Elevation") +
scale_y_continuous(limits = c(600,1200)) # change this to limits appropriate for your region

Elevation profile in ggplot2

Code and data available in a gist.

Tagged , , , , , , , ,

GPS Basemaps in R Using get_map

There are many different maps you can use for a background map for your gps or other latitude/longitude data (i.e. any time you're using geom_path, geom_segment, or geom_point.)

get_map

Helpfully, there's just one function that will allow you to query Google Maps, OpenStreetMap, Stamen maps, or CloudMade maps: get_map in the ggmap package. You could also use either get_googlemap, get_openstreetmap, get_stamenmap, or get_cloudmademap, but instead you can just use get_map for the same functionality as all of those combined. This makes it easy to try out different basemaps for your data.

You need to supply get_map with your location data and the color, source, maptype, and zoom of the base map.

Let's go ahead and map the trails in Elwyn John Wildlife Sanctuary here in Atlanta. The csv data and R file are available in a gist.

gps <- read.csv("elwyn.csv",
    header = TRUE)

library(ggmap)
mapImageData <- get_map(location = c(lon = mean(gps$Longitude),
    lat = 33.824),
    color = "color", # or bw
    source = "google",
    maptype = "satellite",
    # api_key = "your_api_key", # only needed for source = "cloudmade"
    zoom = 17)

pathcolor <- "#F8971F"

ggmap(mapImageData,
    extent = "device", # "panel" keeps in axes, etc.
    ylab = "Latitude",
    xlab = "Longitude",
    legend = "right") +
    geom_path(aes(x = Longitude, # path outline
    y = Latitude),
    data = gps,
    colour = "black",
    size = 2) +
    geom_path(aes(x = Longitude, # path
    y = Latitude),
    colour = pathcolor,
    data = gps,
    size = 1.4) # +
# labs(x = "Longitude",
#   y = "Latitude") # if you do extent = "panel"

We'll be changing the four lines marked above in orange to change what basemap is used.

source = "google"

get_map option source = "google" (or using get_googlemap) downloads a map from the Google Maps API. The basemaps are © Google. Google Maps have four different maptype options: terrain, satellite, roadmap, and hybrid.

source = "google", maptype = "terrain"

source = "google", maptype = "terrain", zoom = 14

Max zoom: 14

source = "google", maptype = "satellite"

source = "google", maptype = "satellite", zoom = 17

Max zoom: 20

source = "google", maptype = "roadmap"

source = "google", maptype = "roadmap", zoom = 17

source = "google", maptype = "hybrid"

Hybrid combines roadmap and satellite. source = "google", maptype = "hybrid", zoom = 17

Max zoom: 14

source = "osm"

get_map option source = "osm" (or using get_openstreetmap) downloads a map from OpenStreetMap. These maps are Creative Commons licensed, specifically Attribution-ShareAlike 2.0 (CC-BY-SA). This means you are free to use the maps for commercial purposes, as long as you release your final product under the same Creative Commons license. OpenStreetMap has no maptype options.

source = "osm" (no maptype needed)

source = "osm", zoom = 17

Max zoom: 20

source = "stamen"

get_map option source = "stamen" (or using get_stamenmap) downloads a map from Stamen Maps. The map tiles are by Stamen Design, licensed under CC BY 3.0. The data for Stamen Maps is by OpenStreetMap, licensed under CC BY SA. Stamen has three different maptype options: terrain, watercolor, and toner.

source = "stamen", maptype = "terrain"

source = "stamen", maptype = "terrain", zoom = 17

Max zoom: 18

source = "stamen", maptype = "watercolor"

source = "stamen", maptype = "watercolor", zoom = 17

Max zoom: 18

source = "stamen", maptype = "toner"

source = "stamen", maptype = "toner", zoom = 17

Max zoom: 18

source = "cloudmade"

N.B. As of March 2014, CloudMade no longer provides this API service.

CloudMade styles build on top of OpenStreetMap data. Thousands of CloudMade styles are available. You can browse them on the CloudMade site. You can also make your own styles.

To use CloudMade map styles in R, you will first need to get an API key to insert into your R code so it can access the maps. You can get an API key from the CloudMade site.

Here are just a couple examples of CloudMade basemaps:

source = "cloudmade", maptype = "1", api_key="your_api_key_here, zoom = 17

source = "cloudmade", maptype = "67367", api_key="your_api_key_here, zoom = 17

Max zoom: 18

The code and data are available in a gist.

Tagged , , , , , , , , , , , , ,

Using Line Segments to Compare Values in R

Sometimes you want to create a graph that will allow the viewer to see in one glance:

  • The original value of a variable
  • The new value of the variable
  • The change between old and new

One method I like to use to do this is using geom_segment and geom_point in the ggplot2 package.

First, let's load ggplot2 and our data:

library(ggplot2)

data <- as.data.frame(USPersonalExpenditure) # data from package datasets
data$Category <- as.character(rownames(USPersonalExpenditure)) # this makes things simpler later

Next, we'll set up our plot and axes:

ggplot(data,
    aes(y = Category)) +
labs(x = "Expenditure",
    y = "Category") +

For geom_segment, we need to provide four variables. (Sometimes two of the four will be the same, like in this case.) x and y provide the start points, and xend and yend provide the endpoints.

In this case, we want to show the change between 1940 and 1960 for each category. Therefore our variables are the following:

  • x: "1940"
  • y: Category
  • xend: "1960"
  • yend: Category
geom_segment(aes(x = data$"1940",
  y = Category,
  xend = data$"1960",
  yend = Category),
 size = 1) +

Next, we want to plot points for the 1940 and 1960 values. We could do the same for the 1945, 1950, and 1955 values, if we wanted to.

geom_point(aes(x = data$"1940",
    color = "1940"),
    size = 4, shape = 15) +
geom_point(aes(x = data$"1960",
    color = "1960"),
    size = 4, shape = 15) +

Finally, we'll finish up by touching up the legend for the plot:

scale_color_discrete(name = "Year") +
theme(legend.position = "bottom")

geom_segment, then geom_point

The order of geom_segment and the geom_points matters. The first geom line in the code will get plotted first. Therefore, if you want the points displayed over the segments, put the segments first in the code. Likewise, if you want the segments displayed over the points, put the points first in the code.

For example, we could change the middle section of the code to:

geom_point(aes(x = data$"1940",
  color = "1940"),
  size = 4, shape = 15) +
geom_point(aes(x = data$"1960",
  color = "1960"),
  size = 4, shape = 15) +

geom_segment(aes(x = data$"1940",
    y = Category,
    xend = data$"1960",
    yend = Category),
  size = 1) +

And the output would look like:

geom_point, then geom_segment

Similarly, if you have points that will be overlapping, make sure you think about which of the point lines you want R to plot first.

The code is available in a gist.

Tagged , , , , , ,

Stacked Bar Charts in R

Reshape Wide to Long

Let's use the Loblolly dataset from the datasets package. These data track the growth of some loblolly pine trees.

> Loblolly[1:10,]
   height age Seed
1    4.51   3  301
15  10.89   5  301
29  28.72  10  301
43  41.74  15  301
57  52.70  20  301
71  60.92  25  301
2    4.55   3  303
16  10.92   5  303
30  29.07  10  303
44  42.83  15  303

First, we need to convert the data to wide form, so each age (i.e. 3, 5, 10, 15, 20, 25) will have its own variable.

wide <- reshape(Loblolly,
    v.names = "height",
    timevar = "age",
    idvar = "Seed",
    direction = "wide")

> wide[1:5,]
  Seed height.3 height.5 height.10 height.15 height.20 height.25
1  301     4.51    10.89     28.72     41.74     52.70     60.92
2  303     4.55    10.92     29.07     42.83     53.88     63.39
3  305     4.79    11.37     30.21     44.40     55.82     64.10
4  307     3.91     9.48     25.66     39.07     50.78     59.07
5  309     4.81    11.20     28.66     41.66     53.31     63.05

Create Variables

Then we want to create new columns showing how much each tree has grown between data points. For example, instead of knowing a tree's height at age 10, we want to know how much it's grown between age 5 and age 10, so that can be a bar in our graph.

wide$h0.3 <- wide$height.3
wide$h3.5 <- wide$height.5 - wide$height.3
wide$h5.10 <- wide$height.10 - wide$height.5
wide$h10.15 <- wide$height.15 - wide$height.10
wide$h15.20 <- wide$height.20 - wide$height.15
wide$h20.25 <- wide$height.25 - wide$height.20

Plot Stacked Bar Chart

Finally, we want to plot all the new data points:

library(RColorBrewer)
sequential <- brewer.pal(6, "BuGn")
barplot(t(wide[,8:13]),
    names.arg = wide$Seed, # x-axis labels
    cex.names = 0.7, # makes x-axis labels small enough to show all
    col = sequential, # colors
    xlab = "Seed Source",
    ylab = "Height, Feet",
    xlim = c(0,20), # these two lines allow space for the legend
    width = 1) # these two lines allow space for the legend
legend("bottomright", 
    legend = c("20-25", "15-20", "10-15", "5-10", "3-5", "0-3"), #in order from top to bottom
    fill = sequential[6:1], # 6:1 reorders so legend order matches graph
    title = "Years")

Stacked bar chart

If you decide you'd rather have clustered bars instead of stacked bars, you can just add the option beside = TRUE to the barplot.

The full code is available in a gist.

Citations and Further Reading

Tagged , , , , ,