Interactivity

Slides

 Open slides in new window  Download PDF of slides

Exercises

{plotly} and {ggiraph}

Together we’ll make some plots with plotly::ggplotly() and ggiraph::girafe() (see this for a general ggplotly tutorial)

I’ll post all the final code on the course website when we’re done.

Dashboards

Together we’ll make an interactive dashboard about the Palmer Penguins.

I’ll post all the final code on the course website when we’re done.

webR

Together we’ll do this:

  1. Create a {webr} chunk that helps teach something and provides feedback
  2. Recreate the Shiny k-means example
  3. Bonus: Make a live ggplot plot!

I’ll post all the final code on the course website when we’re done.

Code examples

{plotly} and {ggiraph}

Plotly:

Code
library(tidyverse)
library(plotly)

p1 <- ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = drv))

ggplotly(p1)

{ggiraph}:

Code
library(ggiraph)

mpg_fancy <- mpg |>
  mutate(
    carname = glue::glue(
      "{str_to_title(manufacturer)} {str_to_title(model)}"
    )
  )

p2 <- ggplot(mpg_fancy, aes(x = displ, y = hwy, color = drv)) +
  geom_point_interactive(aes(tooltip = carname, data_id = carname), size = 2) +
  theme_minimal()

girafe(ggobj = p2)

Dashboards

See here for the code for this dashboard (and click here to open the dashboard in a new tab):

Quarto Live

Live rnorm() (see code here):

 

Original Shiny k-means example

Recreation with webR and OJS inputs (see code here):

Replicated Shiny example!

Original faithful example

With OJS

Warning

This required hours of fighting with Claude and it is so janky and awful

This is R code that sends the faithful data to OJS:

Code
ojs_define(faithful = faithful)

This is OJS code for making input options:

Code
viewof nbins = Inputs.select(
  [10, 20, 35, 50],
  {label: "Number of bins"}
)

viewof individual_obs = Inputs.checkbox(["Show individual observations"], {
  value: []
})

viewof density = Inputs.checkbox(["Show density estimate"], {
  value: []
})

viewof bw_adjust = Inputs.range([0.2, 2], {
  label: "Bandwidth adjustment:",
  step: 0.2,
  value: 1
})

This is OJS code for plotting the data:

Code
faithful_js = transpose(faithful)

Plot.plot({
  height: 300,
  y: { label: "Density" },
  x: { label: "Duration (minutes)" },
  marks: [
    // Probability-based histogram
    Plot.rectY(
      faithful_js,
      Plot.binX(
        { y: (bin, { x1, x2 }) => bin.length / faithful_js.length / (x2 - x1) }, // Normalize to probability density
        { x: "eruptions", thresholds: nbins } // Use nbins for threshold count
      )
    ),

    // Zero line
    Plot.ruleY([0]),

    // Individual observations
    // Individual observations (separate layer for ticks)
    individual_obs.length > 0
      ? Plot.dot(faithful_js, {
          x: "eruptions",
          y: (d) => jitter(d.eruptions, 0.05), // Deterministic jitter based on data
          // y: () => Math.random() * 0.05, // Jitter points randomly along the y-axis
          stroke: "white",
          fill: "red",
        })
      : null,

    // Density line
    density.length > 0
      ? Plot.line(
          densityEstimate(
            faithful_js.map((d) => d.eruptions),
            bw_adjust // Bandwidth adjustment
          ),
          { x: "x", y: "density", stroke: "blue" }
        )
      : null,
  ].filter((d) => d !== null),
});
Code
function densityEstimate(values, bwAdjust) {
  const kde = kernelDensityEstimator(
    kernelEpanechnikov(0.2 * bwAdjust), // Kernel function
    d3.scaleLinear().domain(d3.extent(values)).ticks(200) // Evaluate KDE at 200 equally spaced points
  );
  return kde(values);
}

// Kernel density estimator function
function kernelDensityEstimator(kernel, X) {
  return function (sample) {
    return X.map((x) => ({
      x: x,
      density: d3.mean(sample, (v) => kernel(x - v)), // Adjusted scaling
    }));
  };
}

// Epanechnikov kernel function
function kernelEpanechnikov(bandwidth) {
  return function (u) {
    return Math.abs(u /= bandwidth) <= 1 ? (0.75 * (1 - u * u)) / bandwidth : 0;
  };
}

// Deterministic jitter function because Math.random() doesn't support seeds
function jitter(value, range) {
  const hash = Math.sin(value) * 10000; // Generate pseudo-random hash based on value
  return (hash - Math.floor(hash)) * range; // Scale hash to the desired range
}

With webR

TipMAGIC

This required literally 8 minutes of reading the documentation.

Here’s OJS code for creating the interactive inputs:

Code
viewof nbins_r = Inputs.select(
  [10, 20, 35, 50],
  {label: "Number of bins"}
)

viewof individual_obs_r = Inputs.toggle({
  label: "Show individual observations",
  value: false
})

viewof density_r = Inputs.toggle({
  label: "Show density estimate",
  value: false
})

viewof bw_adjust_r = density_r 
  ? Inputs.range([0.2, 2], {
      label: "Bandwidth adjustment:",
      step: 0.2,
      value: 1
    })
  : html`<input type="range" value="1" style="display:none">`

This is the R code for connecting to those OJS inputs and using them live:

Code
```{webr}
#| autorun: true
#| echo: false
#| input:
#|   - nbins_r
#|   - individual_obs_r
#|   - density_r
#|   - bw_adjust_r

hist(
  faithful$eruptions,
  probability = TRUE,
  breaks = as.numeric(nbins_r),
  xlab = "Duration (minutes)",
  main = "Geyser eruption duration"
)

if (individual_obs_r) {
  rug(faithful$eruptions)
}

if (density_r) {
  dens <- density(faithful$eruptions, adjust = bw_adjust_r)
  lines(dens, col = "blue")
}
```

This works with ggplot too!

Here’s OJS code for creating the interactive inputs:

Code
numeric_vars = ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"]
categorical_vars = ["species", "island", "sex"]

viewof species_filter = Inputs.checkbox(
  ["Adelie", "Chinstrap", "Gentoo"],
  {label: "Species to include", value: ["Adelie", "Chinstrap", "Gentoo"]}
)

viewof x_var = Inputs.select(numeric_vars, {
  label: "X Variable",
  value: "bill_length_mm"
})

viewof y_var = Inputs.select(numeric_vars, {
  label: "Y Variable",
  value: "bill_depth_mm"
})

viewof color_var = Inputs.select(categorical_vars, {
  label: "Color by",
  value: "species"
})

viewof show_trend_species = Inputs.toggle({
  label: "Show species trends",
  value: false
})

viewof show_trend_overall = Inputs.toggle({
  label: "Show overall trend",
  value: false
})