How to control the z-axis (depth) of bars in ggplot

I want to create a plot that looks similar to the figure below, which was created in PowerPoint.

Example person-time figure created in PowerPoint

Here is an example data frame containing data for the first two people in the figure.

people <- 1:2
months <- -1:12
pt_df <- expand_grid(person = people, month = months)
rows_per_person <- length(months)

p01 <- rep("Diseased", rows_per_person)
p02 <- c(rep("Pre-follow-up", 2), rep("At Risk", 2), rep("Diseased", 3), rep("Deceased", 7))
status <- c(p01, p02)

pt_df$status <- status

pt_df <- pt_df |> 
    person_f = factor(person),
    status_f = factor(status, levels = c("Pre-follow-up", "At Risk", "Diseased", "Deceased"))

Attempt 1

My first attempt was to simply create a stacked bar chart.

ggplot(pt_df, aes(person_f, month, fill = status_f)) +
  geom_col(position = "stack")

A stacked bar chart

However, position = "stack" doesn’t "slide" the bars on top of each other. Instead, as is appropriate for the name, it "stacks" the bars on top of each other, which has an additive effect on the y-axis. This is not what I want.

Attempt 2

So, I thought that maybe I should use position = "dodge" with zero horizontal travel instead.

ggplot(pt_df, aes(person_f, month, fill = status_f)) +
  geom_col(width = 0.5, position = position_dodge(0))

A chart with position dodge 1

This sort of works. As you can see in the figure below, the bars are "sliding" on top of each other as I wanted. I just need to control the depth so that the longest bar (i.e., "Deceased") is sent to the "back" of the stack, followed by "At Risk" and then "Diseased".

A chart with position dodge 2

Attempt 3

So, I thought I would just reverse the factor order.

pt_df <- pt_df |> 
  mutate(status_f_rev = forcats::fct_rev(status_f))

ggplot(pt_df, aes(person_f, month, fill = status_f_rev)) +
  geom_col(width = 0.5, position = position_dodge(0))

A chart with position dodge 3

Unfortunately, that doesn’t work either. Deceased is now listed at the top of the legend, but the bar for Deceased is still at the front of the stack. And the bars now appear to be "sliding" in reverse order (Deceased on the left when I use status_f_rev) as compared to before (Deceased on the right when I use status_f)

A chart with position dodge 4

So, it appears as though the factor order affects the ordering of the legend and x-axis, but not the depth (z-axis). Any help is greatly appreciated!

>Solution :

ggplot2 doesn’t really have a concept of a z-axis in terms of "depth." This is probably why it doesn’t follow e.g. factor order (which I agree is a reasonable guess). Instead, you can control the draw order in two ways. First, the layers are drawn in the order they are added to the plot. Second, within each layer, elements are drawn by the order they appear in the data.

In your case, reversing the data order works:

ggplot(pt_df[nrow(pt_df):1, ], aes(person_f, month, fill = status)) +
    geom_col(width = 0.5, position = 'identity')

Or you can arrange first, which is more robust:

pt_df |>
  arrange(desc(status_f)) |>
  ggplot(aes(person_f, month, fill = status_f)) +
  geom_col(width = 0.5, position = 'identity')

(Note that position = 'identity' is the standard way to set the positions to not stack.)

Leave a Reply