plotly / plotly.R

An interactive graphing library for R
https://plotly-r.com
Other
2.55k stars 623 forks source link

Update y-axis range when x-axis rangeslider is used #912

Closed wave-electron closed 7 years ago

wave-electron commented 7 years ago

This is what currently happens with Plotly when using the range slider. If a short interval x-axis range from the slider is selected, the displayed trace(s) could become quasi flat:

problem

This occurs because the y-axis range is not updating when using the range slider. IMHO this seems to defeat the point of having a range slider.

Others have already realised this and have implemented a solution in the JS version of Plotly.

Javascript solution

To achieve updating y axis range, it listens to plotly_relayout event, use it's eventdata (xaxis.range) to filter the data, and redraw the plot with filtered data.

The only remaining issue is that the range slider y axis changes and therefore does not display the traces in full.

Is this feature something that could be easily implemented in the R version?

cpsievert commented 7 years ago

I'm not sure I agree this is a better default behavior -- especially considering that optimal aspect ratios depends on the context, and it's much easier to zoom in on the y-axis rather than zoom out.

That isn't to say we couldn't make it easier to opt-in to this behavior, but it would be better asked/implemented as a feature request in plotly.js -- https://github.com/plotly/plotly.js

FWIW, you can already take advantage of that Javascript solution from R via htmlwidget::onRender()

wave-electron commented 7 years ago

Firstly, thank you for the tip about using htmlwidget::onRender() ... that help me get it working in my own app :)

However, it still probably worth at least visually demonstrating here with two charts how the default behaviour is falling short... particularly when it comes to charting financial stocks.

The first chart is the APPLE stock without Y-scaling implemented. Its very evident the trace becomes quasi-flat.

screen shot 2017-04-07 at 12 03 22 pm

With the Y-scaling implemented using the onRender method.... it maintains a much superior aspect ratio in the 2nd chart.

screen shot 2017-04-07 at 12 01 27 pm

cox-michael commented 7 years ago

I'm trying to solve this same issue with plotly range buttons on a Shiny app. Could you share your code you used to fix it in R? I read through the Javascript solution you posted, but unfortunately for me, my javascript skills are rusty. Since it is a Shiny web app, is it possible for me to put javascript in the head of the document to fix the y-axis scaling?

Here's an example of what I need to fix:

shinyApp(
  ui = fluidPage(
    tags$head(tags$script("//some javascript here")),
    plotlyOutput('myChart')
    ),
  server = function(input, output){
    x <- data.frame(Date = seq(Sys.Date()-365, Sys.Date(), by = 1), Value = 366:1)

    p <- plot_ly(x = x$Date, y = x$Value, type = 'scatter', mode = 'lines') %>%
      layout(
        xaxis = list(type = 'date', rangeselector = list(
          buttons = list(
            list(count = 30, label = "30 days", step = "day", stepmode = "backward")
            , list(step = "all", label = 'All')
          )
        ))
      )
    output$myChart = renderPlotly(p)
  }
)
cpsievert commented 7 years ago

@ldsmike88 just FYI, a support plan might be worthwhile in your case -- https://support.plot.ly/plans

wave-electron commented 7 years ago

you have to use the onRender() method and add it to the plotly object.

 p <- plot_ly(x = x$Date, y = x$Value, type = 'scatter', mode = 'lines') %>%
                layout(
                        xaxis = list(type = 'date', rangeselector = list(
                                buttons = list(
                                        list(count = 30, label = "30 days", step = "day", stepmode = "backward")
                                        , list(step = "all", label = 'All')
                                )
                        ))
                ) %>% onRender(javascript goes here......)

But to be honest... until y-scaling is built into plotly its not a desirable solution.

For this reason I'm now using highcharter which has built in scaling. You can solve your y-scaling problems doing this.

library("shiny")
library("highcharter")

ui <- fluidPage(
        h1("Highcharter Demo"),
        fluidRow(
                column(width = 8,
                       highchartOutput("hcontainer",height = "500px")
                )
        )
)

server = function(input, output) {

       x <- data.frame(Date = seq(Sys.Date()-365, Sys.Date(), by = 1), Value = 366:1)
       # create xts: Extensible Time Series object
       qxts <- xts(x[,-1], order.by=x[,1])

        output$hcontainer <- renderHighchart({

             hc <- highchart(type = "stock") %>% hc_add_series(qxts, type = "line") 

          hc      
        })

}

shinyApp(ui = ui, server = server)
cox-michael commented 7 years ago

Highcharter looks very good, but unfortunately, you have to pay for commercial use. I ended up using Dygraphs. Not as graphically pleasing as Highcharter or Plotly, but it got the job done. Here's my code:

  library(shiny)
  library(xts)
  library(dygraphs)
  shinyApp(
    ui = fluidPage(
      radioButtons('pPlotSelector', '', c('30 days', 'All'), inline = TRUE),
      dygraphOutput('myChart')
    ),
    server = function(input, output){
      observe({
        x <- data.frame(Date = seq(Sys.Date()-365, Sys.Date(), by = 1), Value = 366:1)
        rownames(x) <- x$Date
        x <- as.xts(x[, 2, drop = FALSE])

        window <- switch(input$pPlotSelector,
                         '30 days' = c(Sys.Date()-30, Sys.Date()),
                         'All' = NULL
        )

        output$myChart = renderDygraph(
          dygraph(x) %>%
            dyRangeSelector(dateWindow = window)
        )
      })
    }
  )

Thanks for all of your help!

cpsievert commented 7 years ago

1040 enables you to modify plotly graphs in shiny using the plotly.js API. I've made a demo which addresses this issue. Try it out with:

devtools::install_github("ropensci/plotly#1040")
library(plotly)
demo("proxy-relayout", package = "plotly")
cox-michael commented 7 years ago

It crashed for me as soon as I tried to change the slider range.

cpsievert commented 7 years ago

Interesting, mind reporting the output you see in the R console? Also, the output of devtools::session_info()?

cox-michael commented 7 years ago

I'm using R version 3.2.3 (2015-12-10). The error is:

Error in plotlyProxy: could not find function "startsWith"

cpsievert commented 7 years ago

Ah, good to know, thanks! Should be fixed now, try reinstalling.

cox-michael commented 7 years ago

I got it working with my example. Your demo seemed to be too much for my system to handle. It kept crashing RStudio.

shinyApp(
  ui = fluidPage(
    plotlyOutput('plot')
  ),
  server = function(input, output, session){
    x <- data.frame(Date = seq(Sys.Date()-365, Sys.Date(), by = 1), Value = 366:1)

    p <- plot_ly(x = x$Date, y = x$Value, type = 'scatter', mode = 'lines') %>%
      layout(
        xaxis = list(type = 'date', rangeselector = list(
          buttons = list(
            list(count = 30, label = "30 days", step = "day", stepmode = "backward")
            , list(step = "all", label = 'All')
          )
        )
        , rangeslider = list(type = "date")
        )
      )
    output$plot = renderPlotly(p)

    observeEvent(event_data("plotly_relayout"), {
      d <- event_data("plotly_relayout")
      xmin <- if (length(d[["xaxis.range[0]"]])) d[["xaxis.range[0]"]] else d[["xaxis.range"]][1]
      xmax <- if (length(d[["xaxis.range[1]"]])) d[["xaxis.range[1]"]] else d[["xaxis.range"]][2]
      if (is.null(xmin) || is.null(xmax)) return(NULL)

      # compute the y-range based on the new x-range
      idx <- with(x, xmin <= Date & Date <= xmax)
      yrng <- extendrange(x$Value[idx])

      plotlyProxy("plot", session) %>%
        plotlyProxyInvoke("relayout", list(yaxis = list(range = yrng)))
    })
  }
)

It works fine whenever you click "30 days" or use the range slider, but it doesn't relayout when you click "All." Not a big deal. I can work with this. Thanks for all your help! Do you anticipate plotlyProxy being included in an official release soon?

cpsievert commented 7 years ago

It kept crashing RStudio.

Interesting...does it work in a modern browser (e.g., firefox/chrome)?

It works fine whenever you click "30 days" or use the range slider, but it doesn't relayout when you click "All."

Yep, same for me, feel free to improve on it ;)

Do you anticipate plotlyProxy being included in an official release soon?

Hopefully next week or sooner

wave-electron commented 7 years ago

@cpsievert Awesome work!

brianalexander commented 6 years ago

Is this going to be added as standard any time soon? I'm starting a new project using financial charts and was considering using plotly dash, but this functionality doesn't seem available in python.

i'm looking for exactly this

https://www.anychart.com/products/anystock/gallery/Stock_Event_Markers/Stock_Chart_with_Event_Markers.php

JuanRedondoHernan commented 6 years ago

Did you find out how to do it in Python? I'm a bit stuck on the same issue, and I wanted to keep all my work in Python.

cpsievert commented 6 years ago

Please post an issue here if you want it python https://github.com/plotly/plotly.py

ghost commented 6 years ago

cpsievert, thanks for all the awesome work you're doing on plotly for R users. I have a simple question: Is there a way to update a plotly plot when the dataframe it is plotting is changed, as an equivalent of this code from above: plotlyProxy("plot", session) %>% plotlyProxyInvoke("relayout", list(yaxis = list(range = yrng))) but then update the entire data set?

lets say I have a data frame with x, y (and z possibly) data I plot in 2D or 3D with n groups to color / group by, and then the user either loads a new file that should be shown in the same plotly plot, or rbinds a new piece of data, adding an extra group to the dataframe.

gersteing commented 6 years ago

I struggled with getting this to work so I developed a code example to share. Example provides automatic updates to the yaxis range when and x range slider is updated. Y axis is set to show only price range of bars that are in the chart with a two tick padding. This worked well for me and I can potentially add any number of trace overlays that I want on top of the chart. Plus, I can run it locally without having to use plotly services. See gitub repo here:

https://github.com/gersteing/DashCandlestickCharting/blob/master/README.md

For whom ever wants to try it out. Runs locally using Flask. Enjoy

chrisoutwright commented 5 years ago

I have used the python library to create html file with candlesticks (challege was to integrate it with talib for pattern recognition), where the js file was seperated from the data.

In the original js plotly file, why does one need to add a plotly_relayout event, when the corresponding function group to be possibly edited is at 741: [function(t, e, r) {}]?

For example, I have DAX OHLC data with extreme values (11436,11220). The section in the debugger for the first function at 741 starts with the following:

        741: [function(t, e, r) {
            "use strict";
            var n = t("fast-isnumeric")
              , i = t("../../lib")
              , a = t("../../constants/numerical").FP_SAFE;
            function o(t, e) {
                var r, n, a = [], o = s(e), c = l(t, e), u = c.min, f = c.max;
                if (0 === u.length || 0 === f.length)
                    return i.simpleMap(e.range, e.r2l);
                var h = u[0].val
                  , p = f[0].val;
                for (r = 1; r < u.length && h === p; r++)
                    h = Math.min(h, u[r].val);
                for (r = 1; r < f.length && h === p; r++)
                    p = Math.max(p, f[r].val);
                var d = !1;
                if (e.range) {
                    var g = i.simpleMap(e.range, e.r2l);
                    d = g[1] < g[0]
                }
                "reversed" === //etc.}

How is this related to the function of group 729 r.relayout = function t(e, r, n), considering the group 741 has also the functions doAutoRange: function(t, e) and findExtremes: function(t, e, r) I made a debugger screenshot. Especially in doAutoRange (not shown), I think the those lines seem interesting:

                     e.autorange && (e.range = o(t, e),
                    e._r = e.range.slice(),
                    e._rl = i.simpleMap(e._r, e.r2l),

Any ideas?

unbenannt

I am not a js person, but shouldn't one adjust the autorange part of these functions instead of calling another eventhandler? Or am I wrong? As this is related to the JS script used within plotly, I don't think the python people can help me out.

j03m commented 5 years ago

This should 100% be the default behavior of the range slider.