Better Inputs for Storytelling With Data

Data-visualisation
Observable
Quarto
Author

Filip Reierson

Published

January 2, 2025

How this was made

In this article I share stylised versions of the select and check box inputs that can be used for more elegant storytelling with data. Sometimes I would like to make inputs feel like they are part of a paragraph. The idea is that this doesn’t take the reader out of the story and can make the inputs more intuitive. For my inputs I use observable with css styling. To illustrate the inputs I use the mpg dataset included in ggplot2.

Code
# R
library(stringr)
library(tidyverse)
data('mpg', package='ggplot2')
mpg <- mpg |>
  mutate(trans = case_when(
    str_detect(trans,'auto')~'automatic',
    str_detect(trans,'manual')~'manual',
    T ~ NA_character_
  ))
ojs_define(mpg = mpg)
Code
cyl_categories = [
  {name: "four", value: "4"},
  {name: "five", value: "5"},
  {name: "six", value: "6"},
  {name: "eight", value: "8"}
]

trans_categories = [
  "automatic",
  "manual"
]

viewof cyl_select = Inputs.select(cyl_categories, {
  label: "",
  format: (t) => t.name
})

viewof trans_select = Inputs.select(trans_categories, {
  label: ""
})

Default inputs

Code
viewof cyl_select_old = Inputs.select(cyl_categories, {
  label: "Cylinders",
  format: (t) => t.name
})

viewof trans_select_old = Inputs.select(trans_categories, {
  label: "Transmission"
})

all_manufacturers = [...new Set(transpose(mpg).map(x=>x.manufacturer))];
viewof manufacturer_select_old = Inputs.checkbox(all_manufacturers, {
  label: "Manufacturer",
  value: all_manufacturers
});

The default Observable inputs are functional and often appropriate. However, sometimes I would like the inputs to flow with the text.

My inputs

Notice how the following paragraph makes it intuitive how the filtering works, is arguably more elegant, and still has the same functionality.

Code
html`<span style="line-height: 1.5rem;">In the following plot we highlight cars that have &nbsp;<span style="white-space: nowrap;"><span class="dropdown-container" style="width: 5rem;">${viewof cyl_select}</span> cylinders, </span> <span style="white-space: nowrap;"><span class="dropdown-container" style="width: 7rem;">${viewof trans_select}</span> transmission, </span> and were manufactured by &nbsp;<span class="checkbox-styling">${viewof manufacturer_select}</span></span>`
Code
mpg2 = transpose(mpg).filter(x=>x.cyl==cyl_select.value&&x.trans==trans_select);
mpg3 = mpg2.filter(x=>manufacturer_select.includes(x.manufacturer));
mpg4 = transpose(mpg).map(x => ({
  ...x,
  highlight: x.cyl==cyl_select.value&&x.trans==trans_select&&manufacturer_select.includes(x.manufacturer)
}));
manufacturers = [...new Set(mpg2.map(x=>x.manufacturer))];
viewof manufacturer_select = Inputs.checkbox(manufacturers, {
  label: "",
  format: (t) => manufacturers.length==1? 
  html`<span class="checkbox-option"><span class="checkbox-background">${t}</span>.</span>`
  : t==manufacturers[manufacturers.length - 1]?html`<span class="checkbox-option">or <span class="checkbox-background">${t}</span>.</span>`:
  html`<span class="checkbox-option"><span class="checkbox-background">${t}</span>${manufacturers.length==2 ? " " : ", "}</span>`,
  value: manufacturers
});
Code
Plot.plot({
  marks: [
    Plot.dot(mpg4.filter(x=>!x.highlight), {x: "displ", y: "hwy", stroke: 'lightgrey', strokeOpacity: 0.4}),
    Plot.dot(mpg4.filter(x=>x.highlight), {x: "displ", y: "hwy", stroke: 'red', strokeOpacity: 0.4})
  ],
  x: {label: 'engine displacement, in litres'},
  y: {label: 'highway miles per gallon'}
});
Code
html`
<style>
tr:has(td span.highlight-this-row) {
  background-color: yellow;
}
</style>
`
Code
mpg5 = mpg4.map(({manufacturer, model, year, displ, hwy, cyl, trans, highlight}) => ({manufacturer, model, year, displ, hwy, cyl, trans, highlight}));
table = {
  let div = d3.create("div").attr("id", "mpg_table");
  div.append(() => Inputs.table(mpg5, {rows: 18.5,
    format: {highlight: x=>x?html`<span class='highlight-this-row'>true</span>`:"false"}}
  ));
  return div.node();
}
Code
html`
<style>
/* The drop downs */
.dropdown-container {
  display: inline-block;
  position: relative;
  line-height: 1;
}

.dropdown-container select {
  border: none;
  border-bottom: 2px dotted grey;
  background: none;
  font-size: 1em;
  padding: 0 5px;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  line-height: 1;
  vertical-align: baseline;
  text-align: center;
}

.dropdown-container select:focus {
  outline: none;
}

.dropdown-container::after {
  content: '▼';
  font-size: 0.7em;
  position: absolute;
  right: 5px;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
}

.dropdown-container select {
  padding-right: 20px;
}

/* The multi-select */
.checkbox-styling form {
  display: inline; 
  white-space: normal;
}

.checkbox-styling form > div {
  display: inline; 
}

.checkbox-styling input[type="checkbox"] {
  display: none;
}

.checkbox-styling > form > div > label {
  margin-right: 10px;
}

.checkbox-styling > form > div > label:last-child {
  margin-right: 0px;
}

.checkbox-styling .checkbox-option .checkbox-background {
  display: inline-block;
  height: 1.5rem;
  line-height: 1.5rem;
  padding: 0px 2px;
  margin: .25rem 0px;
  margin-right: 0px;
  cursor: pointer;
  border: none;
  border-radius: 3px;
  box-shadow: none;
  transition: border 0.3s, box-shadow 0.3s, opacity 0.3s;
}

.checkbox-styling input[type="checkbox"]:not(:checked) + .checkbox-option .checkbox-background {
  opacity: 0.5;
  text-decoration: line-through;
}

.checkbox-background {
  background: lightgrey;
  color: black;
}

/* Hide checkboxes from table */
div#mpg_table input {
  display: none
}
</style>
`

I hope that this article has made you think about how inputs can be more engaging and better integrated into storytelling with data.