使用 ggplotly rangeslider 进行交互式相对性能(股票收益)

Posted

技术标签:

【中文标题】使用 ggplotly rangeslider 进行交互式相对性能(股票收益)【英文标题】:Using ggplotly rangeslider for interactive relative performance (stock returns) 【发布时间】:2019-10-26 08:37:37 【问题描述】:

我正在尝试从 R 制作交互式股票表现图。它是比较几只股票的相对表现。每只股票的表现线应从 0% 开始。

对于静态图,我会使用 dplyr group_bymutate 来计算性能(请参阅我的代码)。

使用 ggplot2 和 plotly/ggplotly,rangeslider() 允许以交互方式选择 x 轴范围。现在我希望性能从选择的任何开始范围从 0 开始。

如何将 dplyr 计算移到绘图中,或者在更改范围时使用反馈循环重新计算?

理想情况下,它应该可以在静态 RMarkdown html 中使用。或者,我也会切换到 Shiny。

我尝试了几个options for rangeslider。我也尝试使用 ggplot stat_function 但无法达到预期的结果。我还找到了dygraphs,其中有dyRangeSelector。但在这里我也面临同样的问题。

这是我的代码:

library(plotly)
library(tidyquant)

stocks <- tq_get(c("AAPL", "MSFT"), from = "2019-01-01")

range_from <- as.Date("2019-02-01")

stocks_range <- stocks %>% 
  filter(date >= range_from) %>% 
  group_by(symbol) %>% 
  mutate(performance = adjusted/first(adjusted)-1)

p <- stocks_range %>% 
  ggplot(aes(x = date, y = performance, color = symbol)) +
  geom_line()

ggplotly(p, dynamicTicks = T) %>%
  rangeslider(borderwidth = 1) %>%
  layout(hovermode = "x", yaxis = list(tickformat = "%"))

【问题讨论】:

【参考方案1】:

我找到了plotly_relayout 的解决方案,它可以读出可见的 x 轴范围。这用于重新计算性能。它作为一个闪亮的应用程序工作。这是我的代码:

library(shiny)
library(plotly)
library(tidyquant)
library(lubridate)

stocks <- tq_get(c("AAPL", "MSFT"), from = "2019-01-01")

ui <- fluidPage(
    titlePanel("Rangesliding performance"),
        mainPanel(
           plotlyOutput("plot")
        )
)

server <- function(input, output) 

  d <- reactive( e <- event_data("plotly_relayout")
                  if (is.null(e)) 
                    e$xaxis.range <- c(min(stocks$date), max(stocks$date))
                  
                  e )

  stocks_range_dyn <- reactive(
    s <- stocks %>%
      group_by(symbol) %>%
      mutate(performance = adjusted/first(adjusted)-1)

    if (!is.null(d())) 
      s <- s %>%
        mutate(performance = adjusted/nth(adjusted, which.min(abs(date - date(d()$xaxis.range[[1]]))))-1)
    

    s
  )

    output$plot <- renderPlotly(

      plot_ly(stocks_range_dyn(), x = ~date, y = ~performance, color = ~symbol) %>% 
        add_lines() %>%
        rangeslider(start =  d()$xaxis.range[[1]], end =  d()$xaxis.range[[2]], borderwidth = 1)

      )


shinyApp(ui = ui, server = server)

定义范围滑块的开始/结束仅适用于plot_ly,不适用于ggplotly 转换的ggplot 对象。我不确定这是否是一个错误,因此打开了issue on Github。

【讨论】:

【参考方案2】:

如果您不想使用shiny,您可以使用dygraphs 中的dyRebase 选项,或者您必须在plotly 中插入自定义javascript 代码。在这两个例子中,我都变基为 1,而不是 0。

选项 1:使用dygraphs

library(dygraphs)
library(tidyquant)
library(timetk)
library(tidyr)

stocks <- tq_get(c("AAPL", "MSFT"), from = "2019-01-01")

stocks %>% 
  dplyr::select(symbol, date, adjusted) %>% 
  tidyr::spread(key = symbol, value = adjusted) %>% 
  timetk::tk_xts() %>% 
  dygraph() %>%
  dyRebase(value = 1) %>% 
  dyRangeSelector()

请注意,`dyRebase(value = 0) 不起作用。

选项 2:plotly 使用 event handlers。我尽量避免ggplotly,因此我的plot_ly 解决方案。这里的时间选择只是通过缩放,但我认为它也可以通过范围选择器来完成。 onRenderRebaseTxt 中的 javascript 代码将每个跟踪重新定位到第一个可见数据点(注意可能的缺失值)。它仅在 relayout 事件中调用,因此必须在绘图之前完成第一次变基。

library(tidyquant)
library(plotly)
library(htmlwidgets)
library(dplyr)

stocks <- tq_get(c("AAPL", "MSFT"), from = "2019-01-01")

pltly <- 
  stocks %>% 
  dplyr::group_by(symbol) %>% 
  dplyr::mutate(adjusted = adjusted / adjusted[1L]) %>% 
  plotly::plot_ly(x = ~date, y = ~adjusted, color = ~symbol,
                  type = "scatter", mode = "lines") %>% 
  plotly::layout(dragmode = "zoom", 
                 datarevision = 0)

onRenderRebaseTxt <- "
  function(el, x) 
el.on('plotly_relayout', function(rlyt) 
        var nrTrcs = el.data.length;
        // array of x index to rebase to; defaults to zero when all x are shown, needs to be one per trace
        baseX = Array.from(length: nrTrcs, (v, i) => 0);
        // if x zoomed, increase baseX until first x point larger than x-range start
        if (el.layout.xaxis.autorange == false) 
            for (var trc = 0; trc < nrTrcs; trc++) 
                while (el.data[[trc]].x[baseX[trc]] < el.layout.xaxis.range[0]) baseX[trc]++;
               
        
        // rebase each trace
        for (var trc = 0; trc < nrTrcs; trc++) 
            el.data[trc].y = el.data[[trc]].y.map(x => x / el.data[[trc]].y[baseX[trc]]);
        
        el.layout.yaxis.autorange = true; // to show all traces if y was zoomed as well
        el.layout.datarevision++; // needs to change for react method to show data changes
        Plotly.react(el, el.data, el.layout);

);
  
"
htmlwidgets::onRender(pltly, onRenderRebaseTxt)

【讨论】:

感谢您的回复。选项 1 有效,但选项 2 无效。可能缺少 pltly 定义的某些部分,因为还有一个似乎不应该存在的括号“)”? 感谢您指出pltly 绘图分配中的错误(。它是我原始版本的残余,其中变基是由自定义按钮触发的。没有括号,代码对我来说运行良好(RStudio Viewer)。我上传了完成的HTML Version 在添加实际范围滑块(在 pltly 定义末尾%&gt;% rangeslider())后,它运行顺利,非常好 我很高兴它对你有用。你介意接受这个解决方案吗?顺便说一句,与dygraphs 解决方案相比,plotly 范围滑块中的数据也被重新设置。我不知道如何防止这种情况。

以上是关于使用 ggplotly rangeslider 进行交互式相对性能(股票收益)的主要内容,如果未能解决你的问题,请参考以下文章

RangeSlider 的 JavaFX ControlsFX CSS

无法让 rangeslider.js 工作

Rangeslider.js |未捕获的类型错误:无法读取未定义的属性“0”

在 Shiny 中使用带有 ion.RangeSlider 滑块输入的日期

RangeSlider onFirstChange 不存在的属性

新的 ion.RangeSlider 给 Shiny 带来了哪些优势?