Better Inputs for Storytelling With Data

Author

Filip Reierson

Published

January 2, 2025

How this was made
show_code = "hidden"
To learn more about how the output in this article was created you can choose to show the code I used. Currently the code in this article is  
.

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: ""
})
cyl_categories = Array(4) [Object, Object, Object, Object]
trans_categories = Array(2) ["automatic", "manual"]
cyl_select = Object {name: "four", value: "4"}
trans_select = "automatic"

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
});
cyl_select_old = Object {name: "four", value: "4"}
trans_select_old = "automatic"
all_manufacturers = Array(15) ["audi", "chevrolet", "dodge", "ford", "honda", "hyundai", "jeep", "land rover", "lincoln", "mercury", "nissan", "pontiac", "subaru", "toyota", "volkswagen"]
manufacturer_select_old = Array(15) ["audi", "chevrolet", "dodge", "ford", "honda", "hyundai", "jeep", "land rover", "lincoln", "mercury", "nissan", "pontiac", "subaru", "toyota", "volkswagen"]

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>`
In the following plot we highlight cars that have  
cylinders,
transmission,
and were manufactured by  
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
});
mpg2 = Array(41) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
mpg3 = Array(41) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
mpg4 = Array(234) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
manufacturers = Array(9) ["audi", "chevrolet", "dodge", "honda", "hyundai", "nissan", "subaru", "toyota", "volkswagen"]
manufacturer_select = Array(9) ["audi", "chevrolet", "dodge", "honda", "hyundai", "nissan", "subaru", "toyota", "volkswagen"]
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'}
});
152025303540↑ highway miles per gallon234567engine displacement, in litres →
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();
}
mpg5 = Array(234) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
manufacturermodelyeardisplhwycyltranshighlight
audia41,9991.8294automatictrue
audia41,9991.8294manualfalse
audia42,0082314manualfalse
audia42,0082304automatictrue
audia41,9992.8266automaticfalse
audia41,9992.8266manualfalse
audia42,0083.1276automaticfalse
audia4 quattro1,9991.8264manualfalse
audia4 quattro1,9991.8254automatictrue
audia4 quattro2,0082284manualfalse
audia4 quattro2,0082274automatictrue
audia4 quattro1,9992.8256automaticfalse
audia4 quattro1,9992.8256manualfalse
audia4 quattro2,0083.1256automaticfalse
audia4 quattro2,0083.1256manualfalse
audia6 quattro1,9992.8246automaticfalse
audia6 quattro2,0083.1256automaticfalse
audia6 quattro2,0084.2238automaticfalse
chevroletc1500 suburban 2wd2,0085.3208automaticfalse
chevroletc1500 suburban 2wd2,0085.3158automaticfalse
chevroletc1500 suburban 2wd2,0085.3208automaticfalse
chevroletc1500 suburban 2wd1,9995.7178automaticfalse
chevroletc1500 suburban 2wd2,0086178automaticfalse
chevroletcorvette1,9995.7268manualfalse
chevroletcorvette1,9995.7238automaticfalse
chevroletcorvette2,0086.2268manualfalse
chevroletcorvette2,0086.2258automaticfalse
chevroletcorvette2,0087248manualfalse
chevroletk1500 tahoe 4wd2,0085.3198automaticfalse
chevroletk1500 tahoe 4wd2,0085.3148automaticfalse
chevroletk1500 tahoe 4wd1,9995.7158automaticfalse
chevroletk1500 tahoe 4wd1,9996.5178automaticfalse
chevroletmalibu1,9992.4274automatictrue
chevroletmalibu2,0082.4304automatictrue
chevroletmalibu1,9993.1266automaticfalse
chevroletmalibu2,0083.5296automaticfalse
chevroletmalibu2,0083.6266automaticfalse
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.