Join the R Community at ShinyConf 2023

How to Create a Drill-Down Bar Chart in Shiny with echarts4r


Data visualization is important for communicating results. Selecting the right graphical representation of your data can enhance your data storytelling. In this article, I’ll show you how to implement drill-down using the echarts4r package, set and observe custom inputs, and use e_on() observers. We’ll do this in a Shiny app, as a way to demonstrate the functionality of interactive data.

Why drill-down? Well, sometimes, data can be grouped into different levels. For example, you might have recorded sales data at a city level, which can be grouped at a country level, which can then be grouped into a larger level (such as a continent).

This kind of data can be represented in different ways depending on what you want to communicate:

  • If you are creating a static document, you could have one chart per level, showing all elements of a level at the same time.
  • If you are working on a Shiny Application, you could allow the user to select data using different inputs (for example, you can define one `selectInput` per level) and chart selected data.

Sometimes, providing a lot of inputs doesn’t make for a really good user experience. What if we could start with a chart at the top level (for example, sales by continent) and then allow the user to drill down to the level of their choice?

Important note: This drill-down approach shouldn’t be applied if you want to compare inner-level data across large-level data (in other words, if you want to compare countries between continents).

TOC:


What Are Drill-Down Charts?

Drill-down functionality allows users to navigate through hierarchical data, starting with the highest level of data (in this case, continents) and then successively exploring lower levels of data (countries, and cities).

In this article I will show you how to generate the following drill-down chart:

For example, in our sales data scenario, the user initially has access to sales by continent. Then, the user can select a continent to explore the total sales data for its countries. Finally, by clicking on a specific country within the continent, the user can view sales data at the city level. This allows the user to get a deeper understanding of the sales data by exploring it at different levels of granularity.

Steps to Building a Drill-Down Chart in Shiny with echarts4R

Step 0: Install and Load the Required Libraries

Before we begin, be sure to load the following libraries:


```{r, message = FALSE, warning = FALSE}
# Load the required libraries. All these libraries are on CRAN.
library(dplyr)
library(echarts4r)
library(glue)
library(shiny)
```

Step 1: Prepare Sample Data

To showcase the drill-down functionality I generated mock data using tibble::tribble().


```{r}
# Create mock data
mock_data <- tibble::tribble(
  ~level1  ,   ~level2     , ~level3           , ~sales,
  "Asia"   ,    "China"    ,   "Shanghai"      ,  32L,
  "Asia"   ,    "China"    ,   "Beijing"       ,  86L,
  "Asia"   ,    "China"    ,   "Chongqing"     ,  30L,
  "Asia"   ,    "India"    ,   "Mumbai"        ,  92L,
  "Asia"   ,    "India"    ,   "Kolkata"       ,  75L,
  "Asia"   ,    "India"    ,   "Chennai"       ,  99L,
  "America",    "USA"      ,   "New York"      ,   6L,
  "America",    "USA"      ,   "Chicago"       ,  12L,
  "America",    "Argentina",   "Buenos Aires"  ,  54L,
  "America",    "Argentina",   "Rosario"       ,  36L,
  "America",    "Brasil"   ,   "Sao Paulo"     ,   2L,
  "America",    "Brasil"   ,   "Rio de Janeiro",  64L,
  "Europe" ,    "Spain"    ,   "Madrid"        ,  54L,
  "Europe" ,    "Spain"    ,   "Barcelona"     ,  46L,
  "Europe" ,    "Spain"    ,   "Sevilla"       ,  67L,
  "Europe" ,    "Italy"    ,   "Rome"          ,  22L,
  "Europe" ,    "France"   ,   "Paris"         ,  42L,
  "Europe" ,    "France"   ,   "Marseille"     ,  91L
)
```

The mock data included sales information for cities that belong to countries, which in turn belong to continents. This nested data structure is excellent to show how the aggregated data at one level can be decomposed into more detailed information at the next level using the drill-down.

Want to visualize spatial data in R? Read our in-depth intro to R ggmap.

The column names were defined as level1, level2 and level3 instead of continent, country and city. This decision was made to ensure that the example was as general and flexible as possible. By using more abstract column names, the example can be applied to a wider range of data and use cases without becoming specific to this particular scenario.

This is how the first rows of our mock data look like:


```{r}
mock_data |>
  head() |> 
  knitr::kable()
```

Looking to generate Word docs in Shiny? See how with {officer}, {flextable}, and {shinyglide}.

Step 2: Create Bar Chart with echarts4r

The following code uses `echarts4r` to create a simple bar chart that summarizes sales data by continent:


```{r}
# Prepare data for chart
chart_data <- mock_data |>
  group_by(level = level1) |>
  summarise(total = sum(sales))
# Create chart
chart_data |> 
  e_chart(x = level) |>
  e_bar(total, name = "Sales by Continent", color = "#5470C6")
```

Step 3: Move Chart Creation Logic to a Function

In this Section the process of generating the chart was extracted into a function of its own named plot_sales_data().


```{r, eval = FALSE}
plot_sales_data <- function(chart_data, chart_title, chart_color) { chart_data |> 
    e_chart(x = level) |>
    e_bar(total, name = chart_title, color = chart_color)
}
# Prepare data for chart
chart_data <- mock_data |>
  group_by(level = level1) |>
  summarise(total = sum(sales))
# Create chart
plot_sales_data(
  chart_data = chart_data,
  chart_title = "Sales by Continent",
  chart_color = "#5470C6"
)
```

Step 4: Plot the Drill Down with plot_sales_data

In this section we use plot_sales_data() to create a chart that summarizes sales by countries, but only for countries in a particular continent: America. This shows that we can use our function to create charts for different data levels by modifying the function’s parameters.


```{r}
plot_sales_data <- function(chart_data, chart_title, chart_color) { chart_data |> 
    e_chart(x = level) |>
    e_bar(total, name = chart_title, color = chart_color)
}
# Prepare data for chart
chart_data <- mock_data |>
  filter(level1 == "America") |>
  group_by(level = level2) |>
  summarise(total = sum(sales))
# Create chart
plot_sales_data(
  chart_data = chart_data,
  chart_title = "Sales by Country (Filtered for America)",
  chart_color = "#91CC75"
)
```

Step 5: Moving an eChart into a Shiny App

To demonstrate the drill-down functionality, it is necessary to incorporate interactivity within an observable environment.

Shiny is a great provider of such context and in combination with JavaScript we will be able to showcase the drill-down capabilities.

Worried about s-l-o-w dashboards? Make R Shiny faster with updateInput, CSS, and JavaScript.

The following code moves our first chart into a Shiny application. In future sections, we will add the drill functionality.

CAVEAT: Before running the code, it is necessary to load the libraries and create the mock data (Step 0 and Step 1, respectively). This code was not included in the code chunk below because it occupies a lot of space.


```{r, eval = FALSE}
plot_sales_data <- function(chart_data, chart_title, chart_color) { chart_data |> 
    e_chart(x = level) |>
    e_bar(total, name = chart_title, color = chart_color)
}
ui <- fluidPage(
  h1("Drill Down in Shiny"),
  echarts4rOutput("chart")
)
# Define server
server <- function(input, output) {
  output$chart <- renderEcharts4r({
    # Prepare data for chart
    chart_data <- mock_data |>
      group_by(level = level1) |>
      summarise(total = sum(sales))
    
    # Create chart
    plot_sales_data(
      chart_data = chart_data,
      chart_title = "Sales by Continent",
      chart_color = "#5470C6"
    )
  })
}
shinyApp(ui, server)
```

When you launch the Shiny App, you should see the following screen:

Step 6: Add Bar Click Observers

In this section, we’ll add bar click observers to our chart. To do so, we will modify plot_sales_data().

echarts4r::e_on() is used to add an event listener to the chart. The event listened to in this case is a click (defined by the event argument) on a bar of the chart (defined by the query argument). When a bar is clicked, the handler function (defined in handler argument) is executed in response.

The handler contains the JavaScript code to execute when the click event is triggered. In this case, the Shiny.setInputValue function is called to set the value of a Shiny input named custom_bar_click. The input value is set to an object that contains information about the clicked bar, such as the clicked_level and drilled_place. The value is converted from JSON data in Javascript to a list in Shiny.

Confused about Reactive Observers? Get started with two hands-on examples and the Shiny basics.

The params in the handler function is an object that contains information about the event that triggered the function (in this case, the click on the bar). We use the name property of this object, which is the name of the bar that was clicked.

IMPORTANT: This Section is just to observe the click and print the custom value in our R console. You will see no changes in the chart.

Step 7: Modify the Bar Chart on Bar Click

In this section, we add the drill-down functionality from continent-to-country in our application.

Country-to-city drill-down will be added in the next section.


```{r, eval = FALSE}
plot_sales_data <- function(chart_data, chart_title, chart_color) { chart_data |> 
    e_chart(x = level) |>
    e_bar(total, name = chart_title, color = chart_color) |>
    e_on(
      query = "series.bar",
      # Set input values
      handler = "function(params){
         Shiny.setInputValue(
          'custom_bar_click',
          {clicked_level: 'level2', drilled_place: params.name}, {priority: 'event'}
         );
       }",
      event = "click"
    )
}
ui <- fluidPage(
  h1("Drill Down in Shiny"),
  echarts4rOutput("chart")
)
# Define server
server <- function(input, output) {
  output$chart <- renderEcharts4r({
    # Our custom input value that we send from the bar click
    print(input$custom_bar_click)
    if (is.null(input$custom_bar_click)) {
      # Prepare data for chart
      chart_data <- mock_data |>
        group_by(level = level1) |>
        summarise(total = sum(sales))
      
      # Create chart
      plot_sales_data(
        chart_data = chart_data,
        chart_title = "Sales by Continent",
        chart_color = "#5470C6"
      )      
    } else if(input$custom_bar_click$clicked_level == "level2") {
      # Prepare data for chart
      chart_data <- mock_data |>
        filter(level1 == input$custom_bar_click$drilled_place) |>
        group_by(level = level2) |>
        summarise(total = sum(sales))
      
      # Create chart
      plot_sales_data(
        chart_data = chart_data,
        chart_title = glue::glue(
          "Sales by Country (Filtered for {input$custom_bar_click$drilled_place})"
        ),
        chart_color = "#91CC75"
      )
    }
  })
}
shinyApp(ui, server)
```

This is the resulting application behavior in this step:

Step 8: Dynamically Modify the Bar Chart’s level on Bar Click

In this section, a new parameter chart_drill_to is included in plot_sales_data(). This allows us to dynamically modify the bar chart’s level on bar click.


```{r, eval = FALSE}
plot_sales_data <- function(chart_data, chart_title, chart_color, chart_drill_to) {
  sales_chart <- chart_data |> 
    e_chart(x = level) |>
    e_bar(total, name = chart_title, color = chart_color)
  # Adding the click observer only when drill_to is passed
  if (!is.null(chart_drill_to)) {
    sales_chart <- sales_chart |>
      e_on(
        query = "series.bar",
        # Set input values
        handler = glue::glue(
          "function(params){
             Shiny.setInputValue(
              'custom_bar_click',
              {clicked_level: '<>', drilled_place: params.name}, {priority: 'event'}
             );
           }",
          .open = "<<", .close = ">>"
        ),
        event = "click"
      )
  }
  return(sales_chart)
}
ui <- fluidPage(
  h1("Drill Down in Shiny"),
  echarts4rOutput("chart")
)
# Define server
server <- function(input, output) {
  output$chart <- renderEcharts4r({
    # Our custom input value that we send from the bar click
    print(input$custom_bar_click)
    if (is.null(input$custom_bar_click)) {
      # Prepare data for chart
      chart_data <- mock_data |>
        group_by(level = level1) |>
        summarise(total = sum(sales))
      
      # Create chart
      plot_sales_data(
        chart_data = chart_data,
        chart_title = "Sales by Continent",
        chart_color = "#5470C6",
        chart_drill_to = "level2"
      )      
    } else if(input$custom_bar_click$clicked_level == "level2") {
      # Prepare data for chart
      chart_data <- mock_data |>
        filter(level1 == input$custom_bar_click$drilled_place) |>
        group_by(level = level2) |>
        summarise(total = sum(sales))
      
      # Create chart
      plot_sales_data(
        chart_data = chart_data,
        chart_title = glue::glue(
          "Sales by Country (Filtered for {input$custom_bar_click$drilled_place})"
        ),
        chart_color = "#91CC75",
        chart_drill_to = "level3"
      )
    } else if(input$custom_bar_click$clicked_level == "level3") {
      # Prepare data for chart
      chart_data <- mock_data |>
        filter(level2 == input$custom_bar_click$drilled_place) |>
        group_by(level = level3) |>
        summarise(total = sum(sales))
      
      # Create chart
      plot_sales_data(
        chart_data = chart_data,
        chart_title = glue::glue(
          "Sales by City (Filtered for {input$custom_bar_click$drilled_place})"
        ),
        chart_color = "#FAC858",
        chart_drill_to = NULL
      )
    }
  })
}
shinyApp(ui, server)
```

Now we can drill down to the city level:

Step 9: Add an Additional Observer to Go Back from the Drill

Finally, to complete the drill-down functionality we actually need to be able to drill-up in order to explore how the different parent categories are composed.

The trick to accomplishing this is to add a title called Back and observe it in the same way.


```{r, eval = FALSE}
plot_sales_data <- function(
    chart_data, chart_title, chart_color,
    chart_drill_to, chart_back_drill_to, filtered_place) {
  sales_chart <- chart_data |> 
    e_chart(x = level) |>
    e_bar(total, name = chart_title, color = chart_color)
  # Adding the click observer only when drill_to is passed
  if (!is.null(chart_drill_to)) {
    sales_chart <- sales_chart |>
      e_on(
        query = "series.bar",
        # Set input values
        handler = glue::glue(
          "function(params){
             Shiny.setInputValue(
              'custom_bar_click',
              {clicked_level: '<>', drilled_place: params.name}, {priority: 'event'}
             );
           }",
          .open = "<<", .close = ">>"
        ),
        event = "click"
      )
  }
  if (!is.null(chart_back_drill_to)) {
    if (is.null(filtered_place)) {
      observe_handler = glue::glue(
      "function(params){
        Shiny.setInputValue(
          'custom_bar_click',
          {clicked_level: '<>'},
          {priority: 'event'}
        );
      }",
      .open = "<<", .close = ">>"
      )
    } else {
      observe_handler = glue::glue(
        "function(params){
          Shiny.setInputValue(
            'custom_bar_click',
            {clicked_level: '<>', drilled_place: '<>'},
            {priority: 'event'}
          );
        }",
        .open = "<<", .close = ">>"
      )
    }
    sales_chart <- sales_chart |>
      e_title("Back", triggerEvent = TRUE) |>
      e_on(
        query = "title",
        # Set input values
        handler = observe_handler,
        event = "click"
      )
  }
  return(sales_chart)
}
ui <- fluidPage(
  h1("Drill Down in Shiny"),
  echarts4rOutput("chart")
)
# Define server
server <- function(input, output) {
  observeEvent(input$custom_bar_click1, {
    print(input$custom_bar_click1)
  })
  output$chart <- renderEcharts4r({
    # Our custom input value that we send from the bar click
    print(input$custom_bar_click)
    if (is.null(input$custom_bar_click$clicked_level) || input$custom_bar_click$clicked_level == "level1") {
      # Prepare data for chart
      chart_data <- mock_data |>
        group_by(level = level1) |>
        summarise(total = sum(sales))
      
      # Create chart
      plot_sales_data(
        chart_data = chart_data,
        chart_title = "Sales by Continent",
        chart_color = "#5470C6",
        chart_drill_to = "level2",
        chart_back_drill_to = NULL,
        filtered_place = NULL
      )      
    } else if (input$custom_bar_click$clicked_level == "level2") {
      # Prepare data for chart
      chart_data <- mock_data |>
        filter(level1 == input$custom_bar_click$drilled_place) |>
        group_by(level = level2) |>
        summarise(total = sum(sales))
      
      # Create chart
      plot_sales_data(
        chart_data = chart_data,
        chart_title = glue::glue(
          "Sales by Country (Filtered for {input$custom_bar_click$drilled_place})"
        ),
        chart_color = "#91CC75",
        chart_drill_to = "level3",
        chart_back_drill_to = "level1",
        filtered_place = NULL
      )
    } else if (input$custom_bar_click$clicked_level == "level3") {
      # Prepare data for chart
      chart_data <- mock_data |>
        filter(level2 == input$custom_bar_click$drilled_place) |>
        group_by(level = level3) |>
        summarise(total = sum(sales))
      previous_place <- mock_data |>
        filter(level2 == input$custom_bar_click$drilled_place) |>
        pull(level1) |> unique()
      
      # Create chart
      plot_sales_data(
        chart_data = chart_data,
        chart_title = glue::glue(
          "Sales by City (Filtered for {input$custom_bar_click$drilled_place})"
        ),
        chart_color = "#FAC858",
        chart_drill_to = NULL,
        chart_back_drill_to = "level2",
        filtered_place = previous_place
      )
    }
  })
}
shinyApp(ui, server)
```

If you run the code above, you should see the following working example of a drill-down in Shiny using echarts4r:

Summing Up echarts4r for Building a Drill-Down Chart in a Shiny App

And with that, you now have an understanding of how the functionality of a drill-down chart works in the context of a Shiny application. Thanks to John Coene’s echarts4r package Shiny developers like yourself can easily create charts and enhance user interaction with your data by leveraging the Echarts Javascript library.

Additionally, you might explore echarts4r.assets and echarts4r.maps.

Are you working with genomics data? Try shiny.gosling to produce interactive genomics charts in R Shiny.