Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wave-electron opened this issue Mar 28, 2017 · 21 comments
Closed

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

wave-electron opened this issue Mar 28, 2017 · 21 comments

Comments

@wave-electron
Copy link

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
Copy link
Collaborator

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
Copy link
Author

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
Copy link

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
Copy link
Collaborator

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

@wave-electron
Copy link
Author

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
Copy link

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
Copy link
Collaborator

#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
Copy link

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

@cpsievert
Copy link
Collaborator

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

@cox-michael
Copy link

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

Error in plotlyProxy: could not find function "startsWith"

@cpsievert
Copy link
Collaborator

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

@cox-michael
Copy link

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
Copy link
Collaborator

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
Copy link
Author

@cpsievert Awesome work!

@brianalexander
Copy link

brianalexander commented Jan 20, 2018

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
Copy link

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
Copy link
Collaborator

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

@ghost
Copy link

ghost commented Feb 28, 2018

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
Copy link

gersteing commented Oct 5, 2018

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
Copy link

chrisoutwright commented Oct 25, 2018

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
Copy link

j03m commented Nov 1, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants