Interaktive Kontrollelemente für R-Diagramme ohne Shiny! plotly, crosstalk

Wusstest Du, dass Du keine Shiny App programmieren musst, um Diagramme mit Checkboxen, Drop-Down-Feldern und Schiebereglern zu versehen für bequeme visuelle Daten-Exploration? Shiny ist zweifellos ein großartiges Werkzeug – hat jedoch den Nachteil, dass R laufen muss, um die Shiny App zu bedienen – sei es auf einem Webserver oder auf einem lokalen Rechner.

plotly und crosstalk – ein starkes Duo

Mit der Kombination aus plotly und crosstalk (beide von Carson Sievert) ist man zwar nicht ganz so flexibel wie mit Shiny (das zu fordern wäre vermessen). Man kann jedoch sehr nützliche Interaktivität erreichen, die nur auf Javascript basiert. Das Endprodukt, ein HTML-Dokument, ist damit von R unabhängig – R wird lediglich ein Mal zur Erstellung benötigt. Jeder Browser kann das Dokument darstellen, man kann es auf einem R-freien Server hochladen oder sogar per Email verschicken.

Dashboard mit flexdashboard

Hier habe ich ein Dashboard mit dem flexdashboard-Paket erstellt, das etwas mehr Layout-Möglichkeiten bietet als ein Markdown-Bericht. Der Schritt von Markdown zu flexdashboard ist klein; Seitennavigation, Tabsets / Reiter, Zeilen- und Spaltenvorgaben etc. sind einfach umzusetzen. Das Dashboard mit dem eingebetteten R-Code gibt es hier. Seitennavigation in der blauen Titelleiste.

Markdown-Berichte beginnen mit einem sog. YAML-Header (Yet Another Markup Language), der einige Meta-Informationen enthält. Meiner für das Dashboard sieht so aus:

---
title: "Plotly: Kontroll-Elemente wie in Shiny Apps - ohne Shiny!"
output:
  flexdashboard::flex_dashboard:
    orientation: columns
    vertical_layout: fill
    theme: spacelab
    source_code: embed
---

Wichtig sind die Einrückungen (indentation). Ein tolles Feature ist, dass man den Quellcode in das fertige Dokument einbetten kann (source_code: embed), sodass Kollegen (oder man selbst) nicht separate Dateien suchen müssen (das Dashboard selbst, .html, und das Skript zur Erstellung, .Rmd).

Die Daten stammen von der Weltbank: World Development Indicators, bequem geladen mit dem R-Paket WDI. Heute geht es um Luftverschmutzung (air pollution), definiert als durchschnittliche jährliche Menge in Mikrogramm pro Kubikmeter, der Menschen ausgesetzt sind, sowie Lebenserwartung (life expectancy) in Jahren zum Geburtszeitpunkt.

library(knitr)
library(kableExtra)
library(flexdashboard)
library(WDI)
library(tidyverse)
library(plotly)
library(crosstalk)
library(ggthemes)

cache <- WDIcache()

data <- WDI(country = "all",
 indicator = c(survival_15_60 = "HD.HCI.AMRT",
 life_expectancy = "SP.DYN.LE00.IN",
 air_pollution = "EN.ATM.PM25.MC.M3"),
 extra = TRUE,
 cache = cache)

data <- data %>% 
   filter(region != "Aggregates") %>% 
   filter(!is.na(air_pollution) & !is.na(life_expectancy)) %>% 
   filter(year == 2017)

Erste Diagramme: Lebenserwartung und Luftverschmutzung 2017

Im ersten Versuch stelle ich die Daten in einem statischen ggplot-Diagramm dar. Bei sieben Regionen finde ich es nicht einfach zu lesen:

Luftverschmutzung und Lebenserwartung 2017 in sieben Regionen. ggplot2-Diagramm, theme_economist_white aus ggthemes.
p <- data %>% 
   ggplot(aes(x = air_pollution, y = life_expectancy, group = country, color = region)) +
   geom_point(size = 3, alpha = 0.8) +
   theme_economist_white(base_family = "Verdana") +
   scale_color_brewer(palette = "Dark2") +
   labs(title = "Life Expectancy vs. Air Pollution 2017",
        x = "Air Pollution
        Mean Annual Exposure in micrograms per cubic meter",
        y = "Life Expectancy\nat birth in years",
        caption = "Data Source: World Bank, World Development Indicators (WDI),
                   obtained via the WDI R package, version 2.7.1") +
   theme(legend.position = "right",
         legend.text = element_text(size = 14),
         legend.title = element_blank())
 p

Etwas hilfreicher ist es als plotly-Variante: Bei Mouse-Over werden detaillierte Informationen zum jeweiligen Datenpunkt eingeblendet, inklusive des Landes, das aus obigem Diagramm gar nicht ablesbar ist. Das geht dankenswerter Weise mit dem simplen Einzeiler

ggplotly(p)

Das Land haben wir oben bereits in der zuvor nicht genutzten Ästhetik group hinterlegt: group = country – plotly stellt diese Information mit dar.

Luftverschmutzung und Lebenserwartung 2017 in sieben Regionen. plotly-Diagramm mit Mouse-Over-Effekt. Hier als statisches Bild – die interaktive Variante kann im Dashboard ausprobiert werden.

Auch wenn diese Variante schon hilfreicher und informativer ist, ist sie noch nicht optimal. Es sind etwas zu viele Informationen, die nicht gut unterscheidbar sind. Könntest Du auf einen Blick sagen, wie Europa im Vergleich zu Ostasien / Pazifik abschneidet?

(Man kann in der plotly-Variante auf Legendeneinträge klicken, um Gruppen aus- oder abzuwählen. Die folgenden Steuerelemente bieten jedoch noch mehr.)

Visuelle Datenexploration mit plotly und crosstalk

In Verbindung mit crosstalk wird plotly noch leistungsfähiger:

Dashboard mit interaktiven Kontrollelementen dank crosstalk und plotly – ohne Shiny.
Animierte Grafik (gif) zum Anklicken

crosstalk implementiert einen Objekttyp für gemeinsam genutzte Daten (shared data object), das auf dem R6-System für objektorientiertes Programmieren basiert (wie Shiny). Herzstück ist die SharedData$new()-Funktion.

shared_data <- data %>%
   select(country, air_pollution, life_expectancy, region) %>%
   na.omit() %>%
   mutate(region = stringr::str_replace(region, "&", "and"),
           air_pollution = round(air_pollution, 1),
           life_expectancy = round(life_expectancy, 1)) %>%
   SharedData$new()

Das Diagramm habe ich diesmal mit plotly-Code erstellt, nicht mit der ggplotly()-Abkürzung. Hier benötigt man die Tilde ~, um Variablen den Ästhetiken zuzuordnen:

p <- shared_data %>%
   plot_ly(x = ~air_pollution, y = ~life_expectancy, color = ~region,
               hoverinfo = "text",
               text = ~paste("Country:", country,
                             "Region:", region,
                             "Air Pollution:", air_pollution,
                             "Life Expectancy:", life_expectancy)) %>%
   group_by(region) %>%
   add_markers(size = 3) %>%
   layout(xaxis = list(title = "Air PollutionMean Annual Exposure in micrograms per cubic meter"),
          yaxis = list(title = "Life Expectancyat birth in years"),
          legend = list(font = list(size = 16)))

Die Steuerelemente werden mittels der bscols()-Funktion angelegt und greifen ebenfalls auf das shared-data-Objekt zurück.

bscols(widths = c(3, 9),
        list(
             filter_checkbox(id = "region", label = "Region",
                     sharedData = shared_data, group = ~region),
             filter_select(id = "country", label = "Country",
                     sharedData = shared_data, group = ~country),
             filter_slider(id = "slider_ap", label = "Air Pollution",
                     sharedData = shared_data, column = ~air_pollution),
             filter_slider(id = "slider_le", label = "Life Expectancy",
                     sharedData = shared_data, column = ~life_expectancy)
       ),
        p)

Ich möchte wetten, dass viele R-Nutzer bei einem Blick auf das Dashboard auf Shiny tippen würden.

Siehe auch den vorigen Beitrag Zwei interaktive Diagramme in R verknüpfen ohne Shiny: plotly, crosstalk.

Gern führe ich Workshops durch zur Dashboard-Erstellung und für interaktive Datenvisualisierungen in R. Viel Erfolg mit Euren R-Projekten!