0

I am stuck implementing the idea to combine the barplot containing fact, target and prognosis values with a line representing a fitted model based on fact and leading to prognosis values.

I am trying the following:

library(dplyr)
library(ggplot2)

bardf <- data.frame(vals = c(12,12.5, 11, 14,14.5, 15.2,14.5),
           groups = c("fact", "target", "fact", "fact", "target", "target","prognosis") %>% factor,
           xaxs = c("Jan","Jan", "Feb", "Mar","Mar", "Apr","Apr") %>% 
             factor(ordered = T, levels = c("Jan", "Feb", "Mar", "Apr"))) 
p <- bardf %>% 
  ggplot(aes(x = xaxs, y = vals, group = groups, fill = groups))+
  geom_bar(stat= "identity", position = position_dodge(0.9))

model_fits<- data.frame(fittedvals = c(12.1, 11.5, 14.1, 14.5),
                      groups = c("fact", "fact", "fact", "prognosis") %>% factor,
                      xaxs = c("Jan", "Feb", "Mar", "Apr") %>% 
                        factor(ordered = T, levels = c("Jan", "Feb", "Mar", "Apr"))) 

p +
  geom_line(aes(x = xaxs, y = fittedvals, group = groups),
            data = model_fits, stat= "identity",position = position_dodge(0.9))

This is returning the following plot:

enter image description here

I would like to place the nodes of the line at the x axis middle of fact or prognosis bars like this:

enter image description here

Note: there will be no situation when the prognosis and fact bars will be plotted for the same month simultaneously but I need to consider different fact sources like this:

data.frame(vals = c(12,12.5,13, 11, 14,14.5, 15.2,14.5),
           groups = c("fact1","target","fact2", "fact1", "fact1", "target", "target","prognosis") %>% factor,
           xaxs = c("Jan","Jan","Jan", "Feb", "Mar","Mar", "Apr","Apr") %>% 
             factor(ordered = T, levels = c("Jan", "Feb", "Mar", "Apr"))) %>% 
  ggplot(aes(x = xaxs, y = vals, group = groups, fill = groups))+
  geom_bar(stat= "identity", position = position_dodge(0.9))

For groups where two or more fact sources are available I would like to skip any x adjustments of the plotted line and plot it at exactly month x position:

enter image description here

In other words: how do I manually specify adjustment of each node of the line plotted over barplotor or set that the x adjustment is not required for the specific node?

asd-tm
  • 3,381
  • 2
  • 24
  • 41

1 Answers1

2

Rather than using position_dodge for the line, you could use position_nudge to specify a vector of adjustments for the x co-ordinate:

p +
  geom_line(aes(x = xaxs, y = fittedvals, group = 1),
            data = model_fits, stat = "identity",
            position = position_nudge(x = c(-0.25, 0, -0.25, -0.25)))

enter image description here

Extended answer with calculation of nudge offsetts:

DODGE_WIDTH <- 0.9

bardf$usedInModel<-c(1,0,1,1,1,0,0,1) # Mark fact columns 

# used for modelling and result in prognosis columns
model_fits <-  bardf %>% 
  mutate(
    groups = groups %>% factor(ordered = T),
    usedInModelNotNA = ifelse(usedInModel&!is.na(vals), 1, 0), # skip is.na that will not be plotted
         barID = as.integer(groups)) %>% # for ordering of bars in a group
  group_by(xaxs ) %>% 
  mutate(nBars = n(), # We require number of bars in each group, and ids of bars where the line node is placed
         usedInModelNotNA = sum(usedInModelNotNA),
         barID =  barID %>% rank(),
         barID = ifelse(usedInModel, barID, NA),
         plottedBar = ifelse(nBars %in% c(0,1), 0,
                             ifelse(usedInModelNotNA > 1, 0, # if > 1 bars (values) used - place node in the middle
                                    min(barID, na.rm = T))
         )
  ) %>% 
  summarise(nBars = min(nBars),
            plottedBar = min(plottedBar)) %>% 
  ungroup() %>% 
  transmute(xaxs , 
            nudgeOffset = ifelse(plottedBar == 0, # calculate offset
                                 0, 
                                 ((plottedBar * 2 - 1) / (nBars * 2) - 0.5) * DODGE_WIDTH)
  )   %>% 
  right_join(model_fits, by = "xaxs") %>% 
  filter(!is.na(fittedvals)) 

p <- bardf %>% 
  ggplot(aes(x = xaxs, y = vals, group = groups, fill = groups))+
  geom_bar(stat= "identity", position = position_dodge(DODGE_WIDTH))


p +
  geom_line(aes(x = xaxs, y = fittedvals, group = 1),
            data = model_fits, stat = "identity",
            position = position_nudge(model_fits$nudgeOffset), 
            ) +
  geom_point(aes(xaxs, fittedvals, color = NULL, fill = NULL), 
             model_fits, 
             position = position_nudge(model_fits$nudgeOffset), 
             show.legend = FALSE,
             size = 2
  )
 

will return:

enter image description here

asd-tm
  • 3,381
  • 2
  • 24
  • 41
Allan Cameron
  • 147,086
  • 7
  • 49
  • 87
  • I there a way to rapidly calculate the `x` offset for `position_nudge` or I need to find all the offsets manually? – asd-tm Feb 03 '23 at 13:35
  • You _could_ calculate it, but it would require a few lines of code. Essentially you need to work out what order ggplot is going to dodge the bars, and which bars are going to be present at each xaxs value. You also need to know how wide the bars' position_dodge is. It would be best to wrap all this in a function, or even better a ggproto object. However, the function would need to know the bars' data frame, and the line's data frame. – Allan Cameron Feb 03 '23 at 13:43
  • I've appended the answer, replaced manual setting of nudge offset with calculation of these values. Probably it will be useful for the community in the future. Please kindly make any edits you consider useful. Now I can accept and upvote your answer. Should you decide to present a more beautiful code (ggproto object or other) it would be cool. Thank you. – asd-tm Feb 09 '23 at 09:09
  • @asd-tm good work! You seem to have added in an unused level of the fill, but this would be easy to fix. – Allan Cameron Feb 09 '23 at 09:18
  • I would be grateful if you apply any corrections. I also wonder if `DODGE_WIDTH` is in place... I rebult the script from my real system that is far more complicated so some traces of unused aes might be there – asd-tm Feb 09 '23 at 09:21