Skip to contents

Charts for comparing values across categories, from the everyday bar chart to before/after comparisons, rankings over time and bridges that explain how a total was built. Most of them expect data that is already aggregated, one row per category; dplyr::summarise() is a convenient way to get there.

Chart Use it when
bar_chart() You compare a value across a handful of categories
horz_bar_chart() You have many categories or long labels
lollipop_chart() You want a lighter alternative to a bar chart
horz_lollipop() A lollipop chart with many categories or long labels
dumbbell_chart() You show the gap between two values for each category
slope_chart() You compare two points in time and want to show who rose and who fell
bump_chart() You follow how rankings change over several periods
waterfall_chart() You explain how a starting value becomes a final value
radar_chart() You compare a few items across several variables
radial_bar_chart() You show progress of a few items towards a goal
bar_chart_race() You animate a ranking through time for a presentation

bar_chart()

The bar_chart() function allows you to create bar charts however you need to make the aggregation beforehand. In the following example, we will plot the average cty for each manufacturer using the dplyr package.

library(dplyr)

mpg %>% group_by(manufacturer) %>%
  summarise(mean_cty = mean(cty)) %>%
  bar_chart(
    x = "manufacturer",
    y = "mean_cty",
    xFontSize = 10,
    yFontSize = 10,
    fill = "orange",
    strokeWidth = 2,
    ytitle = "average cty value",
    title = "Average City Miles per Gallon by manufacturer"
  )

The bars can be easily sorted in ascending or descending order using the sort parameter:

mpg %>% group_by(manufacturer) %>%
  summarise(mean_cty = mean(cty)) %>%
  bar_chart(
    x = "manufacturer",
    y = "mean_cty",
    sort = "ascending",
    xFontSize = 10,
    yFontSize = 10,
    fill = "orange",
    strokeWidth = 1,
    ytitle = "average cty value",
    title = "Average City Miles per Gallon by manufacturer",
    titleFontSize = 16
  )

horz_bar_chart()

If you’ve many categories, it might be a good idea to go for a horizontal bar chart. It has the same parameters as the bar_chart() function except that the x-axis parameter is named value and the y-axis parameter named label, this naming convention aims to mitigate some confusion that can arise.

If we want to replicate the above graphic in a horizontal way, we can do:

mpg %>% group_by(manufacturer) %>%
  summarise(mean_cty = mean(cty)) %>%
  horz_bar_chart(
    label = "manufacturer",
    value = "mean_cty",
    sort = "ascending",
    labelFontSize  = 10,
    valueFontSize = 10,
    fill = "orange",
    stroke = "crimson",
    strokeWidth = 1,
    valueTitle  = "average cty value",
    title = "Average City Miles per Gallon by manufacturer",
    titleFontSize = 16
  )

As in bar_chart(), we can also sort in descending order:

mpg %>% group_by(manufacturer) %>%
  summarise(mean_cty = mean(cty)) %>%
  horz_bar_chart(
    label = "manufacturer",
    value = "mean_cty",
    sort = "descending",
    labelFontSize  = 10,
    valueFontSize = 10,
    bgcol = "black",
    axisCol = "white",
    fill = "white",
    stroke = "white",
    strokeWidth = 1,
    valueTitle  = "average cty value",
    labelTitle = "Manufacturers",
    title = "Average City Miles per Gallon by manufacturer",
    titleFontSize = 16
  )

lollipop_chart()

lollipop chart follows the same behavior as bar charts but instead of bars you get lollipops, hence the name. Below an example of a lollipop chart with ddplot:

mpg %>% group_by(drv) %>%
  summarise(median_cty = median(cty)) %>%
  lollipop_chart(
    x = "drv",
    y = "median_cty",
    sort = "ascending",
    xtitle = "drv variable",
    ytitle = "median cty",
    title = "Median cty per drv",
    xFontSize = 20
  )

It’s possible to grasp the distribution of some variable according to a specific categorical variable using the same function:


mpg %>% filter(year == 2008) %>%
lollipop_chart(
    x = "manufacturer",
    y = "hwy",
    circleFill = 'red',
    circleStroke = 'orange',
    circleRadius = 5,
    sort = "none",
    xFontSize = 10
  )

From above, it’s quite easy to notice that although Toyota has two cars with high highway miles per gallon (hwy), it also produces many other vehicles with poor hwy.

horz_lollipop()

Same with bar charts, if you have a variable that has many categorical values, you can work with the reversed version of lollipop_chart() which is horz_lollipop():

mpg %>% group_by(manufacturer) %>%
  summarise(median_cty = median(cty)) %>%
  horz_lollipop(
    label = "manufacturer",
    value = "median_cty",
    sort = "descending")

You can also do:

mpg %>% filter(year == 2008) %>%
horz_lollipop(
    label = "manufacturer",
    value = "hwy",
    circleFill = 'red',
    circleStroke = 'orange',
    circleRadius = 5,
    sort = "none"
  )

dumbbell_chart()

A dumbbell chart (also called a connected dot plot) shows two values per category as dots joined by a horizontal line. It is the clearest way to communicate the gap or change between two groups, time points, or conditions across many categories , a grouped bar chart would require much more visual scanning to reach the same conclusion. The New York Times and The Economist use this format regularly.

life_exp <- data.frame(
  country   = c("Brazil", "China", "Egypt", "India",
                "Japan", "Mexico", "Nigeria", "Turkey"),
  year_1952 = c(50.9, 44.0, 41.9, 37.4, 63.0, 50.8, 36.3, 43.6),
  year_2007 = c(72.4, 72.9, 71.3, 64.7, 82.6, 76.2, 46.9, 71.8)
)

dumbbell_chart(
  data     = life_exp,
  x1       = "year_1952",
  x2       = "year_2007",
  y        = "country",
  x1Label  = "1952",
  x2Label  = "2007",
  title    = "Life expectancy: 1952 vs 2007",
  xtitle   = "Life expectancy (years)",
  sort     = "ascending"
)

The chart works equally well for comparing two conditions within the same time period. Here we compare average city and highway fuel economy across vehicle classes using the mpg dataset:

library(dplyr)

mpg_summary <- mpg |>
  group_by(class) |>
  summarise(city = mean(cty), highway = mean(hwy))

dumbbell_chart(
  data    = mpg_summary,
  x1      = "city",
  x2      = "highway",
  y       = "class",
  x1Label = "City",
  x2Label = "Highway",
  col1    = "steelblue",
  col2    = "darkorange",
  title   = "City vs highway fuel economy by class",
  xtitle  = "Miles per gallon",
  sort    = "ascending"
)

slope_chart()

A slope chart (or slopegraph) places two values per category on two vertical axes, typically “before” and “after” or two different years, and connects them with a straight line. The steepness and direction of each line tell you at a glance who rose, who fell, and how the ranking changed. Edward Tufte popularised the form in The Visual Display of Quantitative Information (1983) with his chart of government receipts across rich countries, and the New York Times graphics desk has used it regularly ever since. Use it when you have a small-to-moderate number of categories measured at exactly two points; with many categories or many time points, a line chart is the better choice.

Each line is labelled with its category name and value at both ends, and labels that would overlap are nudged apart vertically. By default lines are colored by direction (increase or decrease). Hover over a line to emphasise it and fade the others.

# Current receipts of government as a percentage of GDP, 1970 vs 1979
# (adapted from Edward Tufte's classic slopegraph)
receipts <- data.frame(
  country = c("Sweden", "Netherlands", "Norway", "Britain", "France",
              "Germany", "Belgium", "Canada", "Finland", "Italy",
              "United States", "Greece", "Switzerland", "Spain", "Japan"),
  y1970 = c(46.9, 44.0, 43.5, 40.7, 39.0, 37.5, 35.2, 35.2, 34.9, 30.4,
            30.3, 26.8, 26.5, 22.5, 20.7),
  y1979 = c(57.4, 55.8, 52.2, 39.0, 43.4, 42.9, 43.2, 35.8, 38.2, 35.7,
            32.5, 30.6, 33.2, 27.1, 26.6)
)

slope_chart(
  data       = receipts,
  start      = "y1970",
  end        = "y1979",
  group      = "country",
  startLabel = "1970",
  endLabel   = "1979",
  digits     = 1,
  title      = "Government receipts as % of GDP",
  height     = 520
)

Use highlight to tell a story about a few categories: the rest are drawn in a muted grey so they give context without competing for attention. Here we compare life expectancy in 1952 and 2007 using the gapminder dataset:

library(gapminder)
library(dplyr)
library(tidyr)

countries <- c("China", "India", "Japan", "Brazil", "Nigeria",
               "Egypt", "Germany", "United States", "South Africa")

life <- filter(gapminder, year %in% c(1952, 2007), country %in% countries)
life <- select(life, country, year, lifeExp)
life <- pivot_wider(life, names_from = year, values_from = lifeExp,
                    names_prefix = "y")

slope_chart(
  data       = life,
  start      = "y1952",
  end        = "y2007",
  group      = "country",
  startLabel = "1952",
  endLabel   = "2007",
  highlight  = c("China", "South Africa"),
  digits     = 1,
  title      = "Life expectancy: 1952 vs 2007"
)

bump_chart()

A bump chart shows how the rank of several entities changes from one period to the next. Each entity is a smooth line that climbs or falls as it overtakes, or is overtaken by, the others, and a circle at every period displays its rank. The Financial Times, The Economist and FiveThirtyEight use this format for league tables, polls and country rankings: it is the right choice when the order matters more than the exact values.

The data must be in long format, with one row per entity and period. By default bump_chart() computes the ranks for each period from the numeric y column (rank 1 = highest value, use rankOrder = "asc" to reverse). If your y column already holds ranks, set ranked = TRUE. Hover over a line to make it stand out.

league <- data.frame(
  season = rep(c("2023", "2024", "2025", "2026"), each = 5),
  team   = rep(c("Lions", "Tigers", "Bears", "Wolves", "Eagles"), 4),
  points = c(80, 75, 60, 55, 50,
             70, 82, 58, 66, 49,
             65, 78, 72, 69, 52,
             60, 74, 81, 70, 77)
)

bump_chart(
  data  = league,
  x     = "season",
  y     = "points",
  group = "team",
  title = "League ranking by season"
)

Use highlight to bring a few entities forward and grey out the rest. Here are the GDP per capita ranks of eight Asian economies from the gapminder dataset:

library(gapminder)

countries <- c("Japan", "Singapore", "Hong Kong, China", "Taiwan",
               "Korea, Rep.", "Malaysia", "Thailand", "China")

gdp <- gapminder[gapminder$country %in% countries & gapminder$year >= 1967, ]
gdp$country <- as.character(gdp$country)

bump_chart(
  data      = gdp,
  x         = "year",
  y         = "gdpPercap",
  group     = "country",
  highlight = c("Singapore", "Korea, Rep."),
  title     = "GDP per capita rank, 1967-2007"
)

waterfall_chart()

A waterfall chart shows how an initial value is built up or eroded by a sequence of positive and negative contributions. Each bar starts exactly where the previous one ended. It is the standard chart for P&L decomposition, budget variance analysis, and any “what changed and why” narrative.

The optional measure column lets you mark certain bars as "total", they span from zero to the running cumulative sum, making subtotals and final totals visually distinct. Hovering over any bar shows a tooltip with the label, the delta, and the running total.

pnl <- data.frame(
  label   = c("Revenue", "COGS", "Gross Profit",
              "R&D", "S&M", "G&A", "Operating Income"),
  value   = c(1200, -450, 750, -120, -90, -60, 480),
  measure = c("relative", "relative", "total",
              "relative", "relative", "relative", "total")
)

waterfall_chart(
  data   = pnl,
  x      = "label",
  y      = "value",
  measure = "measure",
  title  = "P&L bridge",
  ytitle = "USD thousands"
)

Without a measure column every bar is a simple delta — useful for tracking a running balance over time:

cashflow <- data.frame(
  month = c("Jan", "Feb", "Mar", "Apr", "May", "Jun"),
  delta = c(120, -30, 80, -60, 95, -20)
)

waterfall_chart(
  data          = cashflow,
  x             = "month",
  y             = "delta",
  title         = "Monthly cash flow",
  ytitle        = "USD thousands",
  positiveColor = "steelblue",
  negativeColor = "tomato"
)

radar_chart()

radar_chart() draws one translucent polygon per series over a polygonal grid, with one spoke per variable. Its look and feel is inspired by the radar chart of Chart.js. Polygons grow out from the center, hovering over a point shows its value, and clicking a legend entry toggles that series.

players <- data.frame(
  player    = c("Ana", "Ben", "Chloe"),
  speed     = c(88, 72, 65),
  power     = c(60, 91, 70),
  passing   = c(79, 58, 93),
  dribbling = c(92, 64, 81),
  defending = c(45, 83, 74),
  stamina   = c(70, 77, 88)
)

radar_chart(
  data     = players,
  group    = "player",
  axes     = c("speed", "power", "passing", "dribbling", "defending", "stamina"),
  maxValue = 100,
  title    = "Player skill ratings"
)

When the variables are measured in different units, scaleEachAxis = TRUE gives each axis its own scale:

cyl_means <- aggregate(cbind(mpg, hp, wt, qsec, disp) ~ cyl, data = mtcars, FUN = mean)
cyl_means$cyl <- paste(cyl_means$cyl, "cylinders")

radar_chart(
  data          = cyl_means,
  group         = "cyl",
  axisLabels    = c("Miles/gallon", "Horsepower", "Weight (1000 lbs)",
                    "1/4 mile time (s)", "Displacement (cu.in.)"),
  scaleEachAxis = TRUE,
  levels        = 4,
  gridShape     = "circle",
  valueFormat   = ".1f",
  title         = "mtcars averages by number of cylinders"
)

radial_bar_chart()

radial_bar_chart() draws each category as a concentric circular progress track, inspired by the radialBar chart of ApexCharts. Each ring has a faint background track and a rounded arc that sweeps in on load, a label sits at the start of each track, and the average (or total) is shown in the center. Hover over a ring to put its name and value in the center.

platforms <- data.frame(
  platform = c("Vimeo", "Messenger", "Facebook", "LinkedIn"),
  share    = c(76, 67, 61, 90)
)

radial_bar_chart(
  data  = platforms,
  label = "platform",
  value = "share",
  title = "Weekly active users (share of total, illustrative)"
)

With max you can set the value that fills a whole ring, and with endAngle = 360 the tracks make a full circle:

sales <- data.frame(
  region = c("North", "South", "East", "West", "Central"),
  units  = c(420, 310, 275, 190, 120)
)

radial_bar_chart(
  data        = sales,
  label       = "region",
  value       = "units",
  max         = 500,
  endAngle    = 360,
  centerValue = "total",
  centerLabel = "Units sold",
  title       = "Units sold by region (target: 500)"
)

bar_chart_race()

This function allows you to create an animated bar chart race. bar_chart_race() is similar to bar_chart() but takes a third variable mapped to the time dimension, with options for styling transitions.

Let’s make a bar chart race of population growth among various countries using a subset of the gapminder dataset from the {gapminder} package:

gapminder_subset <- gapminder::gapminder %>%
  select(country, year, pop) %>% 
  filter(country %in% c("Japan", "Mexico", "Germany", "Brazil", "Philippines", "Vietnam")) %>%
  mutate(pop = pop/1e6)

gapminder_subset %>%
  slice_sample(n = 10)

#>    year       pop     country
#> 1  2007  91.07729 Philippines
#> 2  1997  76.04900     Vietnam
#> 3  1972 107.18827       Japan
#> 4  1967  39.46391     Vietnam
#> 5  1952  30.14432      Mexico
#> 6  1987 142.93808      Brazil
#> 7  1997 168.54672      Brazil
#> 8  1962  41.12148      Mexico
#> 9  1952  69.14595     Germany
#> 10 1957  91.56301       Japan

In this example, we simply pass call bar_chart_race() like bar_chart(), but with an additional variable mapped to the time dimension specified with time = year:

gapminder_subset %>%
  bar_chart_race(
    x = "pop",
    y = "country",
    time = "year",
    ytitle = "Country",
    xtitle = "Population (in millions)",
    title = "Bar chart race of country populations"
  )

You can also stylize transitions with the frameDur, transitionDur, and ease arguments. For example, setting the time spent pausing on each frame to zero with frameDur = 0 will create a smooth animation:

gapminder_subset %>%
  bar_chart_race(
    x = "pop",
    y = "country",
    time = "year",
    transitionDur = 1000,
    frameDur = 0,
    ytitle = "Country",
    xtitle = "Population (in millions)",
    title = "Bar chart race of country populations"
  )

As you might have noticed, the value of the column passed to the time argument is automatically labelled at the bottom-right corner of the plot panel. We can stylize this with a list of options passed to the timeLabelOpts argument (or turn it off with timeLabel = FALSE). We also give the bars a little bounce here with ease = "BackInOut" for fun.

gapminder_subset %>%
  bar_chart_race(
    x = "pop",
    y = "country",
    time = "year",
    ease = "BackInOut",
    ytitle = "Country",
    xtitle = "Population (in millions)",
    title = "Bar chart race of country populations",
    timeLabelOpts = list(
      size = 40,
      prefix = "Year: ",
      xOffset = 0.2
    )
  )