Search code examples
rplotlyyaxisx-axis

How do I optimize the position of Y-axis titles and data representation?


How can I in the plotly plot: a) position the axis titles horizontally above their respective axes? b) reduce the space between y=0 and the X-axis? c) draw the green data points for Power before the increase and after the decrease (at y=0) as thick as the green data points where y>0?

I would greatly appreciate your support.

library(plotly)

# Sample data
d <- data.frame(
  time = seq(1, 600, by = 1),
  VO2 = c(rnorm(120, 250, 10), rnorm(120, 300, 15), seq(300, 2000, length.out = 240), rnorm(120, 300, 15)),
  VCO2 = c(rnorm(120, 200, 10), rnorm(120, 250, 15), seq(250, 1800, length.out = 240), rnorm(120, 250, 15)),
  power = c(rep(0, 120), rep(0, 120), seq(0, 160, length.out = 240), rep(0, 120))
)

# Create the plot
fig <- plot_ly(data = d) %>% 
  add_lines(x = ~time, y = ~VO2, 
            line = list(color = 'blue'), name = "VO2") %>% 
  add_lines(x = ~time, y = ~VCO2, yaxis = "y2", 
            line = list(color = 'red'), name = "VCO2") %>%
  add_lines(x = ~time, y = ~power, yaxis = "y3", 
            line = list(color = 'green'), name = "Power") %>%
  layout(
    xaxis = list(
      title = list(text = 'Time [sec]', standoff = 10),
      color = 'black', 
      showline = TRUE, 
      tickcolor = 'black', 
      tickwidth = 2, 
      linewidth = 2,
      ticks = "outside"
    ),
    yaxis = list(
      title = list(text = 'VO2', standoff = 20), 
      showline = TRUE, 
      side = "left", 
      color = 'blue', 
      tickcolor = 'blue', 
      tickwidth = 2, 
      linewidth = 2,
      ticks = "outside",
      range = c(0, max(c(d$VO2, d$VCO2)))
    ),
    yaxis2 = list(
      title = list(text = 'VCO2', standoff = 20), 
      showline = TRUE, 
      overlaying = "y", 
      anchor = "free", 
      side = "left", 
      color = 'red', 
      tickcolor = 'red', 
      tickwidth = 2, 
      linewidth = 2,
      ticks = "outside",
      position = -0.1,
      range = c(0, max(c(d$VO2, d$VCO2)))
    ),
    yaxis3 = list(
      title = list(text = 'Power', standoff = 20), 
      showline = TRUE, 
      overlaying = "y", 
      side = "right", 
      color = 'green', 
      tickcolor = 'green', 
      tickwidth = 2, 
      linewidth = 2,
      ticks = "outside",
      range = c(0, max(d$power) * 1.25)
    ),
    showlegend = FALSE, # Removes legend
    margin = list(pad = 50, b = 0, l = 100, r = 100)
  ) 

fig

Solution

  • You said you were looking for three things:

    • the axis titles horizontal, above each axes
    • reduce padding (while maintaining space between y axes)
    • add points based on criteria

    Axis titles as Annotations

    There are three main steps to add the axis titles on top:

    • remove titles from current layout & add shift to "y2" axis
    • create annotations
    • call for an updated version of plotly.js (the dependency that runs in the background)

    New layout example removing titles (this code will be provided in full in a bit, this is just a bit to show you what's happening.

    yaxis = list(
          # title = list(text = 'VO2', standoff = 20),  # remove title
          title = '',                                   
          showline = TRUE, 
    

    For the shift, I did a bit of guess and check, since the plot wanted to overlap the axes when I removed padding, I moved y2 left of y1 axis. This is with the shift in yaxis2 = ... set to 70.

    enter image description here

    The other aspects of the layout remain as you coded them in your question.

    When I created the annotations, I used x positions like I used shift, a bit of guess and check for visual aesthetics. I used the colors for titles as you've assigned them. Lastly, I set the font size to 14. That's the plotly default axis title size.

    Most of the content needed to make an annotation is identical here, so I used lapply to build the list.

    This is the code used to create the annotations:

    xs <- c(-.095, -.2, 1.095)          # x-axis positions
    clr <- c("blue", "red", "green")    # title colors
    anno <- lapply(1:3, \(k) {          # build new axis titles as annotations
      if(names(d)[k + 1] != toupper(names(d)[k + 1])) {         # take titles from dataframe
        tlt = paste0(toupper(substring(names(d)[k + 1], 1, 1)), # capitalize if necessary
                     substring(names(d)[k + 1], 2, 1000))
      } else {
        tlt = names(d)[k + 1] 
      }           # using paper space, on top of y axes, at variable x positions
      list(xref = "paper", yref = "paper", x = xs[k], y = 1.1, showarrow = F,
           font = list(color = clr[k], size = 14), text = tlt)
    })
    

    The content in anno can now be assigned to annotations in the plot layout.

    Lastly, shift will not work without an update to the backend Plotly.js. To update this dependency, I used the following function.

    fixer <- function(plt) {
      # changes to dependency so that all code works
      plt$dependencies[[5]]$src$file = NULL
      plt$dependencies[[5]]$src$href = "https://cdn.plot.ly"
      plt$dependencies[[5]]$script = "plotly-2.33.0.min.js"
      plt$dependencies[[5]]$local = FALSE
      plt$dependencies[[5]]$package = NULL
      plt
    }
    

    Reduce padding

    Now that shift works, all you need to do to reduce the padding is lower the number you called for in layout(margin = list.

    Add markers for data based on criteria

    I'm not exactly sure what you want here, but I've interpreted it as markers for all points in which d$power == 0. That being said, with six hundred observations, even when the points are huge, it's just a blob that looks like a thick line until you zoom in. I'm guessing that this is just data for this question, not the data you're actually using, so perhaps this information will be more useful in the actual context you're using it in.

    To add the markers, you can add another trace. For this trace, you need to identify that it is not using the same data as the other traces.

    Here is the markers trace code I used:

    add_markers(inherit = F, data = d %>% filter(power == 0), # criteria for markers
                x = ~time, y = ~power, yaxis = "y3", 
                marker = list(color = "green", size = 10), name = "")
    

    This is what the plot looks like with these changes (note the blob for the zero line of power).

    graph

    Here's all the code used to put this together (yours and mine).

    library(plotly)
    
    # Sample data
    d <- data.frame(
      time = seq(1, 600, by = 1),
      VO2 = c(rnorm(120, 250, 10), rnorm(120, 300, 15), seq(300, 2000, length.out = 240), rnorm(120, 300, 15)),
      VCO2 = c(rnorm(120, 200, 10), rnorm(120, 250, 15), seq(250, 1800, length.out = 240), rnorm(120, 250, 15)),
      power = c(rep(0, 120), rep(0, 120), seq(0, 160, length.out = 240), rep(0, 120))
    )
    
    
    fixer <- function(plt) { 
                        # changes to dependency so that all code works
      plt$dependencies[[5]]$src$file = NULL
      plt$dependencies[[5]]$src$href = "https://cdn.plot.ly"
      plt$dependencies[[5]]$script = "plotly-2.33.0.min.js"
      plt$dependencies[[5]]$local = FALSE
      plt$dependencies[[5]]$package = NULL
      plt
    }
    
    # Create the plot
    fig <- plot_ly(data = d) %>% 
      add_lines(x = ~time, y = ~VO2, 
                line = list(color = 'blue'), name = "VO2") %>% 
      add_lines(x = ~time, y = ~VCO2, yaxis = "y2", 
                line = list(color = 'red'), name = "VCO2") %>%
      add_lines(x = ~time, y = ~power, yaxis = "y3", 
                line = list(color = 'green'), name = "Power") %>%
      add_markers(inherit = F, data = d %>% filter(power == 0), # criteria for markers
                  x = ~time, y = ~power, yaxis = "y3", 
                  marker = list(color = "green", size = 10), name = "") %>% 
      layout(
        xaxis = list(
          title = list(text = 'Time [sec]', standoff = 10), domain = c(0, 1),
          color = 'black', 
          showline = TRUE, 
          tickcolor = 'black', 
          tickwidth = 2, 
          linewidth = 2,
          ticks = "outside", zeroline = F
        ),
        yaxis = list(
          # title = list(text = 'VO2', standoff = 20),
          title = '',
          showline = TRUE, 
          side = "left", 
          color = 'blue', 
          tickcolor = 'blue', 
          tickwidth = 2, 
          linewidth = 2,
          ticks = "outside",
          range = c(0, max(c(d$VO2, d$VCO2)))
        ),
        yaxis2 = list(
          # title = list(text = 'VCO2', standoff = 20), 
          showline = TRUE, 
          overlaying = "y",
          anchor = "free", 
          side = "left", 
          color = 'red', 
          tickcolor = 'red', 
          tickwidth = 2, 
          linewidth = 2,
          ticks = "outside",
          shift = -70,
          # position = -0.1,
          range = c(0, max(c(d$VO2, d$VCO2)))
        ),
        yaxis3 = list(
          # title = list(text = 'Power', standoff = 20), 
          showline = TRUE, 
          overlaying = "y", 
          side = "right", 
          color = 'green', 
          tickcolor = 'green', 
          tickwidth = 2, 
          linewidth = 2,
          ticks = "outside",
          range = c(0, max(d$power) * 1.25)
        ),
        showlegend = FALSE, # Removes legend
        # margin = list(pad = 50, b = 0, l = 100, r = 100)
        margin = list(pad = 20, b = 0, l = 150, r = 100, t = 50)
      ) 
    
    xs <- c(-.095, -.2, 1.095)          # x-axis positions
    clr <- c("blue", "red", "green")    # title colors
    anno <- lapply(1:3, \(k) {          # build new axis titles as annotations
      if(names(d)[k + 1] != toupper(names(d)[k + 1])) {         # take titles from dataframe
        tlt = paste0(toupper(substring(names(d)[k + 1], 1, 1)), # capitalize if necessary
                     substring(names(d)[k + 1], 2, 1000))
      } else {
        tlt = names(d)[k + 1] 
      }           # using paper space, on top of y axes, at variable x positions
      list(xref = "paper", yref = "paper", x = xs[k], y = 1.1, showarrow = F,
           font = list(color = clr[k], size = 14), text = tlt)
    })
    
    fig %>% layout(annotations = anno) %>% fixer()        # create the plot!