How to remove white spaces between stacked geom_col

Viewed 2656
library(tidyverse)
library(lubridate)

date <- seq(ymd('2018-08-01'), ymd('2018-08-31'), by = '1 day')
c <- 21.30
x1 <- runif(length(date), 0, 20)
x2 <- rnorm(length(date), 10, 3)
x3 <- abs(rnorm(length(date), 40, 10))
data <- data.frame(c, x1, x2, x3) %>% 
  t() %>% as.data.frame() %>% rownames_to_column('var')
data <- data %>%
  mutate(category1 = c('catA', 'catB', 'catB', 'catC') %>% as.factor(),
         category2 = c('catAA', 'catBA', 'catBB', 'catCA') %>% as.factor())
names(data) <- c('var', as.character(date), 'category1', 'category2')
data_long <- data %>% 
  gather(date, value, -var, -category1, -category2) %>% 
  mutate(date = ymd(date))

data_long %>%
  ggplot(aes(date, value, fill = category1)) +
  geom_col(position = 'stack') +
  scale_x_date(breaks = '1 week', date_labels = '%Y-%m-%d', expand = c(.01, .01)) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, vjust = .4)) +
  labs(fill = '')

With the example data and code above I generate the following plot: enter image description here

What I need to do is to remove white spaces between columns. I have found some similar topics, but they recommended use of position_dodge() while it can't be used in my case as I already have position = 'stack', which can't be replaced. How can I make the columns adjacent to each other then?

Edit

Setting width = 1, as proposed by @camille, seems to work ok with the raw data, but not with aggregated to weeks or months - please see the code below:

data_long %>%
  mutate(date = floor_date(date, unit = 'week', week_start = 1)) %>% 
  group_by(category1, date) %>% 
  summarise(value = sum(value, na.rm = TRUE)) %>% 
  ungroup() %>% 
  ggplot(aes(date, value, fill = category1, width = 1)) +
  geom_col(position = 'stack') +
  scale_x_date(breaks = '1 month', date_labels = '%Y-%m', expand = c(.01, .01)) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, vjust = .4)) +
  labs(fill = '')

enter image description here

Edit 2.

As pointed out by @Camille, width of 1 may refer to 1 day in case of date scale. However, the following doesn't produce expected output and returns warning message: position_stack requires non-overlapping x intervals

 data_long %>%
    mutate(date = floor_date(date, unit = 'month', week_start = 1)) %>% 
    group_by(category1, date) %>% 
    summarise(value = sum(value, na.rm = TRUE),
              n = n()) %>% 
    ungroup() %>% 
    ggplot(aes(date, value, fill = category1, width = n)) +
    geom_col(position = 'stack') +
    scale_x_date(breaks = '1 month', date_labels = '%Y-%m', expand = c(.01, .01)) +
    theme_minimal() +
    theme(axis.text.x = element_text(angle = 90, vjust = .4)) +
    labs(fill = '')

enter image description here

2 Answers

The docs for geom_col are more specific than what I put in my comment above. The more detailed meaning of the width parameter:

Bar width. By default, set to 90% of the resolution of the data.

In a general case, such as your first one, this probably just means the distance between one discrete case and another. But in the case of dates, which have a real resolution, this seems to refer to days. I'm not sure if there's a different way to set the resolution of the dates, such as for one unit to refer to one week, instead of one day.

I'm decreasing the alpha just to be able to see if bars overlap.

So without setting a width, this defaults to 90% of the distance between observations, i.e. 90% of one week.

library(tidyverse)
library(lubridate)
...

summarized <- data_long %>%
  mutate(date = floor_date(date, unit = 'week', week_start = 1)) %>% 
  group_by(category1, date) %>% 
  summarise(value = sum(value, na.rm = TRUE)) %>% 
  ungroup()

ggplot(summarized, aes(date, value, fill = category1)) +
  geom_col(alpha = 0.6) +
  scale_x_date(breaks = '1 week', expand = c(.01, .01))

Setting width to 1 means the width is 1 day. I feel like there's a discrepancy here that someone else might be able to explain, why this is read as 1 day rather than 100% of the resolution.

ggplot(summarized, aes(date, value, fill = category1)) +
  geom_col(alpha = 0.6, width = 1) +
  scale_x_date(breaks = '1 week', expand = c(.01, .01))

So to get a width of 1 week, aka 7 days, set width to 7. Again, I think there's a bit of explanation someone else could fill in here.

ggplot(summarized, aes(date, value, fill = category1)) +
  geom_col(alpha = 0.6, width = 7) +
  scale_x_date(breaks = '1 week', expand = c(.01, .01))

Edit: Based on the link in my comment, the best way might just be converting the dates to strings so you can just plot on a discrete x-scale as normal. Before you call as.character, you could do whatever formatting you might want.

summarized %>%
  mutate(date = as.character(date)) %>%
  ggplot(aes(x = date, y = value, fill = category1)) +
    geom_col(width = 1)

(BTW, it can be helpful to include set.seed() at the top so that we all come up with same data. I used set.seed(42) for these.)

One alternative that can bring some more flexibility would be to use geom_rect or geom_tile instead of geom_col. Then you can make each bar exactly as many days/weeks/months wide as you want. But it takes a little more prep work.

As an example, here I pre-calculate the cumulative y-coordinates for each bar, by grouping by date, sorting by category2, and getting the cumulative sum. I also determine the x range from the date and by grabbing the following date. (I do have one manual bit at the end where I assume the final column on the right of the chart should be one "day" wide. Adjust if using weeks/months. There might be a clever way to use padr::pad or something else to automatically intuit what that increment should be.)

data_long2 <- data_long %>%
  group_by(date) %>%
  arrange(desc(category2)) %>%
  mutate(top = cumsum(value),
         bottom = top - value) %>%
  ungroup() %>%
  group_by(category2) %>%
  mutate(next_date = lead(date, default = max(date) + 1)) %>%
  ungroup()

With this, you can use geom_rect or geom_tile to get your chart. They're interchangeable, but they use different coordinate systems, based on the corners or center, respectively.

Here's an example using geom_rect where each bar's left edge is aligned to date.

ggplot(data_long2) +
  geom_rect(aes(xmin = date, xmax = next_date,
                ymin = bottom, ymax = top,
                fill = category1)) +
  scale_x_date(breaks = '1 week', date_labels = '%Y-%m-%d', expand = c(.01, .01)) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, vjust = .4)) +
  labs(fill = '', y = "")

enter image description here

Or you might use geom_tile, and in this case I'm aligning with date in the middle of each bar.

ggplot(data_long2) +
  geom_tile(aes(x = date, width = as.numeric(next_date - date),
                y = (top + bottom)/2, height = (top - bottom),
                fill = category1)) +
  scale_x_date(breaks = '1 week', date_labels = '%Y-%m-%d', expand = c(.01, .01)) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, vjust = .4)) +
  labs(fill = '')

enter image description here

Related