Search code examples
rggplot2plotlyggplotly

Format ggplotly tick labels when class is POSIXct and update axis when zooming


I'm trying to get a sensible X-axis in a ggplotly with class POSIXct, and maintain sensibility when zooming.

Calling ggplotly directly on the ggplot object results in a plot in which the x-axis does NOT rescale on zoom, so after two zooms, you've zoomed in past the tick marks which makes the plot worthless.

Over here I found that we could autoscale the x-axis and it becomes sensible with each zoom... for numerics.

How do I make a sensible x-axis when it is class POSIXct? Or conversely, format it in a way that is sensible? And of course zooming should switch the date format to date-time format at the appropriate zoom level.

I've tried multiple combinations of tickmode, type, and tickformat after piping to layout. I get bizarre results in all combinations. E.g., the combo below results in a plot in which the hover dates are correct over the 30-year span, but the x-axis shows only about 12 days of scale (see screenshot below).

enter image description here

Here is an MRE to create the data and plot it.

library(ggplot2)
library(plotly)

# create dataframe with random POSIXct x-axis and sort
df <- data.frame(mydate = sort(as.POSIXct(round(runif(1000, max = 10e8), 0))),
                 value = sort(sample(1:1000)))

gg <- ggplot(df, aes(mydate, value)) +
      geom_point()

ggplotly(gg) %>% 
layout(xaxis = list(tickmode = "auto",
                    type = "date"          
                    #tickformat = "%Y-%m"
                    ),
       yaxis = list(tickmode = "auto"))

Solution

  • Primarily 4 things have to happen, 2 of which you already addressed. The type , tickmode and autorange for the axis and the data used for the axis.

    It gets a bit trickier with dates, depending on how you build your plot before using ggplotly, sometimes the data is converted to character text... (why, oh why Plotly???!!) That didn't happen here. However, the values' class is now numeric, not dates or POSIXct.

    (BTW -- Plotly would recognize millisecond-formatted numeric dates - if designated in the layout, but R primarily uses seconds-based dates.)

    You could multiply the values of x by 103 or you can just change the formatting back to dates after plotting.

    Here's how you can do that:

    fixer <- function(plt) {         # make them dates again
      plt <- plotly_build(plt)       # build data
      plt$x$data[[1]]$x <- as.POSIXct(plt$x$data[[1]]$x) # make them dates
      plt                            # return plot
    }
    
    
    ggplotly(gg) %>% 
      layout(xaxis = list(tickmode = 'auto', type = "date", autorange = T),
      yaxis = list(tickmode = "auto")) %>% fixer()
    
    

    fixed plot

    If you had more than one trace or split traces (typically due to colors' assignment) you could use lapply to go through all of the x-axes' data, where the fixer() has plt$x$data[[1]]$x, the second trace would be plt$x$data[[2]]... and so on.

    Here's an example of what that might look like. There are a lot of different ways to plot and combine different data, so this method may require adjustments for different plots.

    set.seed(234)
    df <- data.frame(mydate = sort(as.POSIXct(round(runif(1000, max = 10e8), 0))),
                     value = sort(sample(1:1000)))
    set.seed(394)
    df <- mutate(df, value2 = sort(sample(1:1000), decreasing = T))
    
    fixer2 <- function(plt) {      # this could be used for 1 or more traces
      plt <- plotly_build(plt)     # prepare plot
      lapply(1:length(plt$x$data), \(k) {    # for each trace
        plt$x$data[[k]]$x <<- as.POSIXct(plt$x$data[[k]]$x) # change the x back to date format
      })
      plt
    }
    
    ggplot(df, aes(x = mydate)) + geom_point(aes(y = value, color = "value")) + 
      geom_point(aes(y = value2, color = "value2"))
    
    ggplotly(ggplot2::last_plot()) %>% 
      layout(xaxis = list(tickmode = 'auto', type = 'date', autorange = T)) %>% 
      fixer2()
    

    more than 1 trace