【问题标题】:R Highcharter: dynamic drilldown in Shiny on the flyR Highcharter:Shiny on the fly 中的动态钻取
【发布时间】:2019-08-05 16:48:01
【问题描述】:

我正在尝试使用highchartershiny 中的动态数据创建一个多层钻取图。在 SO 社区(向@K. Rohde 大喊)的帮助下,能够通过遍历所有可能的钻取来解决这个问题。我实际的闪亮应用程序将有数百个可能的向下钻取,我不想将这些额外的时间添加到应用程序中,而是使用addSingleSeriesAsDrilldown 即时创建向下钻取。不确定如何在 R 中使用它。

下面是我的问题的工作示例,循环遍历所有向下钻取的可能性:

library (shinyjs)
library (tidyr)
library (data.table)
library (highcharter)
library (dplyr)
library (shinydashboard)
library (shiny)

x <- c("Farm","Farm","Farm","City","City","City","Ocean","Ocean")
y <- c("Sheep","Sheep","Cow","Car","Bus","Bus","Boat","Boat")
z <- c("Bill","Tracy","Sandy","Bob","Carl","Newt","Fig","Tony")
a <- c(1,1,1,1,1,1,1,1)

dat <- data.frame(x,y,z,a)

header <- dashboardHeader()
body <- dashboardBody(

  highchartOutput("Working"),
  verbatimTextOutput("trial")

)
sidebar <- dashboardSidebar()

ui <- dashboardPage(header, sidebar, body)

server <- function(input, output, session) {

  output$Working <- renderHighchart({
    #First Tier #Copied
    datSum <- dat %>%
      group_by(x) %>%
      summarize(Quantity = sum(a)
      )
    datSum <- arrange(datSum,desc(Quantity))
    Lvl1dfStatus <- tibble(name = datSum$x, y = datSum$Quantity, drilldown = tolower(name))

    #Second Tier # Generalized to not use one single input
    # Note: I am creating a list of Drilldown Definitions here.

    Level_2_Drilldowns <- lapply(unique(dat$x), function(x_level) {
      # x_level is what you called 'input' earlier.
      datSum2 <- dat[dat$x == x_level,]

      datSum2 <- datSum2 %>%
        group_by(y) %>%
        summarize(Quantity = sum(a)
        )
      datSum2 <- arrange(datSum2,desc(Quantity))

      # Note: The "drilldown" variable has to be unique, this is why we use level 1 plus level 2 names.
      Lvl2dfStatus <- tibble(name = datSum2$y,y = datSum2$Quantity, drilldown = tolower(paste(x_level, name, sep = "_")))

      list(id = tolower(x_level), type = "column", data = list_parse(Lvl2dfStatus))
    })


    #Third Tier # Generalized through all of level 2
    # Note: Again creating a list of Drilldown Definitions here.
    Level_3_Drilldowns <- lapply(unique(dat$x), function(x_level) {

      datSum2 <- dat[dat$x == x_level,]

      lapply(unique(datSum2$y), function(y_level) {

        datSum3 <- datSum2[datSum2$y == y_level,]

        datSum3 <- datSum3 %>%
          group_by(z) %>%
          summarize(Quantity = sum(a)
          )
        datSum3 <- arrange(datSum3,desc(Quantity))

        Lvl3dfStatus <- tibble(name = datSum3$z,y = datSum3$Quantity)

        # Note: The id must match the one we specified above as "drilldown"
        list(id = tolower(paste(x_level, y_level, sep = "_")), type = "column", data = list_parse2(Lvl3dfStatus))
      })
    }) %>% unlist(recursive = FALSE)

    highchart() %>%
      hc_xAxis(type = "category") %>%
      hc_add_series(Lvl1dfStatus, "column", hcaes(x = name, y = y), color = "#E4551F") %>%
      hc_plotOptions(column = list(stacking = "normal")) %>%
      hc_drilldown(
        allowPointDrilldown = TRUE,
        series = c(Level_2_Drilldowns, Level_3_Drilldowns)
      )
  })

  output$trial <- renderText({input$ClickedInput})

}


shinyApp(ui, server)

以下是使用addSingleSeriesAsDrilldown 的 R 代码示例,但我不确定如何应用它。我需要动态更改JS 字符串。

library(highcharter)
highchart() %>%
  hc_chart(
    events = list(
      drilldown = JS("function(e) {
        var chart = this,
        newSeries = [{
          color: 'red',
          type: 'column',
          stacking: 'normal',
          data: [1, 5, 3, 4]
        }, {
          type: 'column',
          stacking: 'normal',
          data: [3, 4, 5, 1]
        }]
        chart.addSingleSeriesAsDrilldown(e.point, newSeries[0]);
        chart.addSingleSeriesAsDrilldown(e.point, newSeries[1]);
        chart.applyDrilldown();
      }")
    )
  ) %>%
  hc_add_series(type = "pie", data= list(list(y = 3, drilldown = TRUE), list(y = 2, drilldown = TRUE))) %>%
  hc_drilldown(
    series = list()
  )

【问题讨论】:

    标签: r highcharts shiny r-highcharter


    【解决方案1】:

    这个问题你得到了双重答案。有两种基本方法可以实现您的愿望。一种是使用 Highcharts 提供的向下钻取,即使您必须从 R 后端收集子系列。另一种是简单地替换 Highcharts 向下钻取并实现 R 驱动的向下钻取,仅使用 Highcharts 进行渲染。

    由于它可能更容易消化,我会从后者开始。

    Shiny 的钻取功能

    忘记 Highcharts 可以进行向下钻取。您已经拥有了所需的一切,因为您知道如何添加一个事件广播器,它会在图表上的某个点被点击时告诉您。

    为此,您真正使用了renderHighcharts 的反应性,并使用代表当前向下钻取的不同数据集重新渲染图表。该过程如下:单击“农场”列,您现在使用“农场”子集呈现图表。单击下一列,您构建更深的嵌套子集并呈现它。 Highcharts 提供的唯一一项您必须自己做的事情就是添加一个“返回”按钮以再次向上钻取。

    下面的解决方案一开始可能会让人感到困惑,因为它由一些反应式表达式组成,这些表达式汇聚成一个反应式数据集,其中包含您当前的钻取状态。请注意,我们必须将当前的钻取状态存储在后端,以便能够向上钻取并钻取到更深的层次。

    library (shinyjs)
    library (tidyr)
    library (data.table)
    library (highcharter)
    library (dplyr)
    library (shinydashboard)
    library (shiny)
    
    x <- c("Farm","Farm","Farm","City","City","City","Ocean","Ocean")
    y <- c("Sheep","Sheep","Cow","Car","Bus","Bus","Boat","Boat")
    z <- c("Bill","Tracy","Sandy","Bob","Carl","Newt","Fig","Tony")
    a <- c(1,1,1,1,1,1,1,1)
    
    dat <- data.frame(x,y,z,a)
    
    header <- dashboardHeader()
    body <- dashboardBody(
      actionButton("Back", "Back"),
      highchartOutput("Working"),
      verbatimTextOutput("trial")
    
    )
    sidebar <- dashboardSidebar()
    
    ui <- dashboardPage(header, sidebar, body)
    
    server <- function(input, output, session) {
      # To hold the current drilldown status as list, i.e. list("Farm", "Sheep")
      state <- reactiveValues(drills = list())
    
      # Reactive reacting to the above drill list, giving out a normalized data.frame (category, amount)
      filtered <- reactive({
        if (length(state$drills) == 0) {
          # Case no drills are present.
          data.frame(category = dat$x, amount = dat$a)
    
        } else if (length(state$drills) == 1) {
          # Case only x_level drill is present.
          x_level = state$drills[[1]]
          sub <- dat[dat$x == x_level,]
          data.frame(category = sub$y, amount = sub$a)
    
        } else if (length(state$drills) == 2) {
          # Case x_level and y_level drills are present.
    
          x_level = state$drills[[1]]
          y_level = state$drills[[2]]
          sub <- dat[dat$x == x_level & dat$y == y_level,]
          data.frame(category = sub$z, amount = sub$a)
        }
      })
    
      # Since Drilldown from Highcharts is not used: Install own click handler that builds up the drill list.
      observeEvent(input$ClickedInput, {
        if (length(state$drills) < 2) {
          # Push drill name.
          state$drills <<- c(state$drills, input$ClickedInput)
        }
      })
    
      # Since Drilldown from Highcharts is not used: Back button is manually inserted.
      observeEvent(input$Back, {
        if (length(state$drills) > 0) {
          # Pop drill name.
          state$drills <<- state$drills[-length(state$drills)]
        }
      })
    
      output$Working <- renderHighchart({
    
        # Using normalized names from above.
        summarized <- filtered() %>%
          group_by(category) %>%
          summarize(Quantity = sum(amount))
    
        summarized <- arrange(summarized, desc(Quantity))
        tibbled <- tibble(name = summarized$category, y = summarized$Quantity)
    
        # This time, click handler is needed.
        pointClickFunction <- JS("function(event) {Shiny.onInputChange('ClickedInput', event.point.name);}")
    
        highchart() %>%
          hc_xAxis(type = "category") %>%
          hc_add_series(tibbled, "column", hcaes(x = name, y = y), color = "#E4551F") %>%
          hc_plotOptions(column = list(stacking = "normal", events = list(click = pointClickFunction)))
      })
    
      output$trial <- renderText({input$ClickedInput})
    }
    
    shinyApp(ui, server)
    

    Highcharts 的下钻功能

    这里的情况是,您需要将数据从后端发送到 JavaScript 以使用图表库中的 addSeriesAsDrilldown 方法。这以一种异步方式工作:Highcharts 提醒某些点被请求向下钻取(通过单击它)。然后后端必须计算相应的数据集,然后将数据集报告回 Highcharts 以便它可以被渲染。我们为此使用 CustomMessageHandler。

    我们不会在原始 Highcharts 中添加任何向下钻取系列,但我们会告诉 Highcharts 在请求向下钻取时它必须发送什么关键字(向下钻取事件)。请注意,这不是点击事件,而是更专业的事件(仅在下钻可用时)。

    我们发回的数据格式必须正确,所以在这里你需要了解一下 Highcharts 的 api(JS,不是 highcharter)。

    创建向下钻取数据的方法有很多,所以我在这里编写了另一个更通用的函数。然而,最重要的是您使用的级别 ID 可用于确定我们当前所处的过滤级别。代码中有一些cmets来指出这些情况。

    library (shinyjs)
    library (tidyr)
    library (data.table)
    library (highcharter)
    library (dplyr)
    library (shinydashboard)
    library (shiny)
    
    x <- c("Farm","Farm","Farm","City","City","City","Ocean","Ocean")
    y <- c("Sheep","Sheep","Cow","Car","Bus","Bus","Boat","Boat")
    z <- c("Bill","Tracy","Sandy","Bob","Carl","Newt","Fig","Tony")
    a <- c(1,1,1,1,1,1,1,1)
    
    dat <- data.frame(x,y,z,a)
    
    header <- dashboardHeader()
    body <- dashboardBody(
      highchartOutput("Working"),
      verbatimTextOutput("trial")
    
    )
    sidebar <- dashboardSidebar()
    
    ui <- dashboardPage(header, sidebar, body)
    
    server <- function(input, output, session) {
      output$Working <- renderHighchart({
        # Make the initial data.
        summarized <- dat %>%
          group_by(x) %>%
          summarize(Quantity = sum(a))
    
        summarized <- arrange(summarized, desc(Quantity))
        tibbled <- tibble(name = summarized$x, y = summarized$Quantity)
    
        # This time, click handler is needed.
        drilldownHandler <- JS("function(event) {Shiny.onInputChange('ClickedInput', event.point.drilldown);}")
    
        # Also a message receiver for later async drilldown data has to be set.
        # Note in the JS: message.point is going to be the point ID. Highcharts addSeriesAsDrilldown need a point to attach
        #   the drilldown series to. This is retrieved via chart.get which takes the ID of any Highcharts Element.
        #   This means: IDs are kind of important here, so keep track of what you assign.
        installDrilldownReceiver <- JS("function() {
          var chart = this;
          Shiny.addCustomMessageHandler('drilldown', function(message) {
            var point = chart.get(message.point)
            chart.addSeriesAsDrilldown(point, message.series);
          });
        }")
    
        highchart() %>%
          # Both events are on the chart layer, not by series. 
          hc_chart(events = list(load = installDrilldownReceiver, drilldown = drilldownHandler)) %>%
          hc_xAxis(type = "category") %>%
          # Note: We add a drilldown directive (= name) to tell Highcharts that this has a drilldown functionality.
          hc_add_series(tibbled, "column", hcaes(x = name, y = y, drilldown = name, id = name), color = "#E4551F") %>%
          hc_plotOptions(column = list(stacking = "normal")) %>%
          hc_drilldown(allowPointDrilldown = TRUE)
      })
    
      # Drilldown handler to calculate the correct drilldown
      observeEvent(input$ClickedInput, {
        # We will code the drill levels to be i.e. Farm_Car. By that we calculate the next Sub-Chart.
        levels <- strsplit(input$ClickedInput, "_", fixed = TRUE)[[1]]
        # This is just for generalizing this function to work in all the levels and even be expandable to further more levels.
        resemblences <- c("x", "y", "z")
    
        dataSubSet <- dat
    
        # We subsequently narrow down the original dataset by walking through the drilled levels
        for (i in 1:length(levels)) {
          dataSubSet <- dat[dat[[resemblences[i]]] == levels[i],]
        }
    
        # Create a common data.frame for all level names.
        normalized <- data.frame(category = dataSubSet[[resemblences[length(levels) + 1]]], amount = dataSubSet$a)
    
        summarized <- normalized %>%
          group_by(category) %>%
          summarize(Quantity = sum(amount))
    
        summarized <- arrange(summarized, desc(Quantity))
    
        tibbled <- tibble(name = summarized$category, y = summarized$Quantity)
    
        # Preparing the names and drilldown directives for the next level below.
        # If already in "Farm_Car", the name for column "Bob" will be "Farm_Car_Bob"
        nextLevelCodes = lapply(tibbled$name, function(fac) {
          paste(c(levels, as.character(fac)), collapse = "_")
        }) %>% unlist
    
        tibbled$id = nextLevelCodes
    
        # This is dynamic handling for when there is no further drilldown possible.
        # If no "drilldown" property is set in the data object, Highcharts will not let further drilldowns be triggered.
        if (length(levels) < length(resemblences) - 1) {
          tibbled$drilldown = nextLevelCodes
        }
    
        # Sending data to the installed Drilldown Data listener.
        session$sendCustomMessage("drilldown", list(
          series = list(
            type = "column",
            name = paste(levels, sep = "_"),
            data = list_parse(tibbled)
          ),
          # Here, point is, as mentioned above, the ID of the point that triggered the drilldown.
          point = input$ClickedInput
        ))
      })
    
      output$trial <- renderText({input$ClickedInput})
    }
    
    shinyApp(ui, server)
    

    【讨论】:

    • 哇,好棒的答案!!我将采用您的第一个解决方案,因为它更容易理解和复制。太感谢了!在过去的 2 天里,我一直在用头撞墙,试图弄清楚该怎么做。
    • @Kevin 我的荣幸。
    • 解决方案 1 的唯一缺点是您失去了一些在 highcharter 中向下钻取所带来的平滑过渡,但总体而言它很棒!
    • 真棒答案@K.Rohde :) 这也将帮助我做捷径
    • @K. Rohde 谢谢。如果没有您的解决方案,我将无法解决我的问题。请参阅 SO 问题 -> stackoverflow.com/questions/55393013/…
    猜你喜欢
    • 2019-08-04
    • 1970-01-01
    • 2021-06-10
    • 2023-03-06
    • 2011-10-22
    • 2019-06-22
    • 2010-12-18
    • 1970-01-01
    • 2016-02-06
    相关资源
    最近更新 更多