使用 ggplotly rangeslider 实现交互式相对性能(股票 returns)

Using ggplotly rangeslider for interactive relative performance (stock returns)

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

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

使用 ggplot2 和 plotly/ggplotly,rangeslider() 允许交互 select x 轴范围。现在我希望性能从任何起始范围 selected.

开始

我怎样才能将 dplyr 计算移到绘图中,或者在范围改变时有一个反馈循环来重新计算?

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

我尝试了几个 options for rangeslider. Also I tried with ggplot stat_function but could not achieve the desired result. Also I found 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 = "%"))

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

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)

定义 rangeslider 的 start/end 仅适用于 plot_ly,不适用于由 ggplotly 转换的 ggplot 对象。我不确定这是否是一个错误,因此打开了一个 issue on Github.

如果您不想使用 shiny,您可以在 dygraphs 中使用 dyRebase 选项,或者您必须在 plotly。在这两个示例中,我都将基数改为 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)