0

Trying to have a pie (or preferably, donut) chart that displays totals per category and that allows a drilldown to display specifics per category when clicked. Make sense?

I think I might not have every system setting correct, since also copy/paste standard examples render an empty page. Unless that's somehow outdated or something. My system: Ubuntu 20.04, R 4.0.5, packageVersion("shiny") 1.6.0, shiny-server --version 1.5.16.958 Blank examples: https://plotly-r.com/linking-views-with-shiny.html#drill-down and Creating drill down report in R Shiny (amongst others)

My current attempt (not reactive yet because I can't for the life of me figure it out):

library(shiny)
library(DBI)
library(ggplot2)
library(dplyr)
library(ggiraph)

ui<-fluidPage(
  titlePanel("Budget visuals"),

  sidebarLayout(
    sidebarPanel(
      selectInput("fase", "Choose a budget phase:", choices = c("Budget" = "OWB", "Report" = "JV")),
      selectInput("jaar", "Choose a year:", choices = c(2021, 2020, 2019, 2018, 2017, 2016, 2015)),
      selectInput("vuo", "V/U/O:", choices = c("Verplichtingen" = "V", "Uitgaven" = "U", "Ontvangsten" = "O")),
      submitButton("Submit")
    ),

    mainPanel(
      h4(textOutput("header")),
      girafeOutput("donut"),
      tableOutput("view")
    )
  )
)

server<-function(input, output, session) {
  output$header <- renderText({paste0("Visual: ", input$fase, " (", input$vuo, ") ", input$jaar)})

  output$donut <- renderGirafe({
    conn <- dbConnect(
      drv = RMySQL::MySQL(),
      dbname = "btabellen",
      host = "localhost",
      username = "dbuser",
      password = "***")
    on.exit(dbDisconnect(conn), add = TRUE)
    dbGetQuery(conn, 'set character set "utf8"')
    data <- dbGetQuery(conn, paste0(
      "SELECT naam_begroting as begroting, sum(bedrag_t) as bedrag FROM ", input$fase, " WHERE jaar = ", input$jaar, " AND VUO = '", input$vuo, "' GROUP BY jaar, naam_begroting ORDER BY bedrag DESC;"))
    data$fraction <- data$bedrag / sum(data$bedrag)
    data$fraclbl <- paste0(round(100 * data$fraction, 1), "%")
    data$ymax <- cumsum(data$fraction)
    data$ymin <- c(0, head(data$ymax, n=-1))
    data$label <- paste0(data$begroting, ": € ", format(data$bedrag, big.mark=".", decimal.mark=","), " (k)")

    donut_plot <- ggplot(data, aes(y = bedrag, fill = begroting, data_id = begroting)) +
    geom_bar_interactive(
      aes(x = 1, tooltip = label),
      width = 0.1,
      stat = "identity",
      show.legend = FALSE
      ) +
    coord_polar(theta = "y") +
    theme_void() +
    theme(legend.position = "bottom")

    girafe(ggobj = donut_plot, opts_selection(type = "single"))
  })

  output$view <- renderTable({
    conn <- dbConnect(
      drv = RMySQL::MySQL(),
      dbname = "btabellen",
      host = "localhost",
      username = "dbuser",
      password = "***")
    on.exit(dbDisconnect(conn), add = TRUE)
    dbGetQuery(conn, 'set character set "utf8"')
    data <- dbGetQuery(conn, paste0(
      "SELECT naam_begroting as Begroting, SUM(bedrag_t) as Bedrag FROM ", input$fase, " WHERE jaar = ", input$jaar, " AND VUO = '", input$vuo, "' GROUP BY jaar, naam_begroting ORDER BY bedrag DESC;"))
  }, digits=0)
}

shinyApp(ui=ui, server=server)

So basically, what I would like to achieve is to open the page up with a donut plot of the budget showing all of the totals for the categories. When clicking a category, the donut should update itself to showing the totals per subcategory for the category that was just clicked. Effectively, a click should change the SQL query to "SELECT artikelnaam, sum(bedrag_t) FROM OWB WHERE jaar=2018 AND VUO='U' AND naam_begroting='Financiën'" given the user's selection of these parameters. The renderTable should ideally then show a nested table listing the subcategories, but that's for a different question.

Any thoughts what I might be doing wrong?

Aurèle
  • 12,545
  • 1
  • 31
  • 49
  • Making the example reproducible by replacing the external MySQL connection with a dummy SQLite database (so that the code can be copy-pasted and run as is) would make it easier to help – Aurèle May 10 '21 at 09:33
  • Note on security: as is, the app is vulnerable to [SQL injection](https://db.rstudio.com/best-practices/run-queries-safely/) – Aurèle May 10 '21 at 09:35
  • 1
    And thanks, will definitely parametrize neatly once it's working – Christian C. Schouten May 10 '21 at 10:02
  • 1
    Unsure exactly how to do that (only started learning R about a week ago), but is it equally useful that the (open data) dataset I used is: https://gitlab-minfin.nl/datasets/OWB.csv – Christian C. Schouten May 10 '21 at 10:10

1 Answers1

0

The key is to use input$donut_selected, the automatically generated _selected-suffixed input, see https://davidgohel.github.io/ggiraph/articles/offcran/shiny.html#access-the-selected-values.
Like so:

dbGetQuery(conn, paste0(
      "SELECT .... ",
      "FROM ", input$fase, " WHERE .... ",

      if (!is.null(input$donut_selected)) paste0(" AND naam_begroting = '", input$donut_selected, "' "),

      " GROUP BY ....;"))

(As discussed in the comments, not parameterising the query is bad practice, but the above is provided as is to address the main question (how to drill down)).

Note that due to the use of a submitButton, the entire app, including the drill-down functionality, will not be fully reactive, and drilling down will only happen upon clicking "Submit" (see ?submitButton)


Making the example reproducible/runnable (but not minimal):

Writing to disk to a dummy SQLite database:

library(DBI)

if (!dir.exists("data")) dir.create("data")
if (!file.exists(csv_file <- "data/OWB.csv")) {
  download.file("https://www.gitlab-minfin.nl/datasets/OWB.csv",
                destfile = csv_file)
}
if (!file.exists(db_file <- "data/owb.sqlite")) {
  df <- read.csv(csv_file, fileEncoding = "UTF-8")
  con <- dbConnect(RSQLite::SQLite(), db_file)
  dbWriteTable(con, "owb", df)
}

Example adapted to use SQLite database:

library(shiny)
library(ggplot2)
library(dplyr)
library(ggiraph)

ui<-fluidPage(
  titlePanel("Budget visuals"),
  
  sidebarLayout(
    sidebarPanel(
      selectInput("fase", "Choose a budget phase:", choices = c(
        "Budget" = "OWB", "Report" = "JV")),
      selectInput("jaar", "Choose a year:", choices = c(
        2021, 2020, 2019, 2018, 2017, 2016, 2015)),
      selectInput("vuo", "V/U/O:", choices = c(
        "Verplichtingen" = "V", "Uitgaven" = "U", "Ontvangsten" = "O")),
      submitButton("Submit")
    ),
    
    mainPanel(
      h4(textOutput("header")),
      girafeOutput("donut"),
      tableOutput("view")
    )
  )
)

server<-function(input, output, session) {
  output$header <- renderText({
    paste0("Visual: ", input$fase, " (", input$vuo, ") ", input$jaar)
  })
  
  output$donut <- renderGirafe({
    conn <- dbConnect(drv = RSQLite::SQLite(), db_file)
    on.exit(dbDisconnect(conn), add = TRUE)
    data <- dbGetQuery(conn, paste0(
      "SELECT naam_begroting as begroting, sum(bedrag_t) as bedrag ",
      "FROM ", input$fase, " WHERE jaar = ", input$jaar, " AND VUO = '", input$vuo, 
      "' GROUP BY jaar, naam_begroting ORDER BY bedrag DESC;"))
    data$fraction <- data$bedrag / sum(data$bedrag)
    data$fraclbl <- paste0(round(100 * data$fraction, 1), "%")
    data$ymax <- cumsum(data$fraction)
    data$ymin <- c(0, head(data$ymax, n=-1))
    data$label <- paste0(
      data$begroting, ": € ", 
      format(data$bedrag, big.mark=".", decimal.mark=","), " (k)")
    
    donut_plot <- ggplot(data, aes(y = bedrag, fill = begroting, data_id = begroting)) +
      geom_bar_interactive(
        aes(x = 1, tooltip = label),
        width = 0.1,
        stat = "identity",
        show.legend = FALSE
      ) +
      coord_polar(theta = "y") +
      theme_void() +
      theme(legend.position = "bottom")
    
      girafe(ggobj = donut_plot, options = list(opts_selection(type = "single")))
  })
  
  output$view <- renderTable({
    conn <- dbConnect(drv = RSQLite::SQLite(), db_file)
    on.exit(dbDisconnect(conn), add = TRUE)
    data <- dbGetQuery(conn, paste0(
      "SELECT naam_begroting as Begroting, SUM(bedrag_t) as Bedrag ",
      "FROM ", input$fase, " WHERE jaar = ", input$jaar, " AND VUO = '", input$vuo, "' ",
      
      if (!is.null(input$donut_selected)) paste0(" AND naam_begroting = '", input$donut_selected, "' "),
      
      " GROUP BY jaar, naam_begroting ORDER BY bedrag DESC;"))
  }, digits=0)
}

shinyApp(ui=ui, server=server)

Other changes:

  • girafe options passing
  • removing set instructions that may not be compatible with SQLite, and are not relevant to the question
Aurèle
  • 12,545
  • 1
  • 31
  • 49
  • Thanks a lot, will try this tonight. Following your comment on the submitButton, a quick five minute tinker just showed me that indeed clicking submit got the donut_selected working. Never thought of that. And in fact, simply removing the submit button already also made a lot of intermediate attempts working. Thanks for that. Guess I'm in my mind still stuck on ancient ways... – Christian C. Schouten May 10 '21 at 17:59
  • 1
    Worked like a charm, thanks again! Now onto the next challenges of parametrizing SQL input, adjusting the legend and table and making everything look pretty :-) – Christian C. Schouten May 11 '21 at 08:45