Skip to content

Instantly share code, notes, and snippets.

@rpruim
Last active October 8, 2022 08:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rpruim/fd50d23933c63f3113a2bb8576b5b34a to your computer and use it in GitHub Desktop.
Save rpruim/fd50d23933c63f3113a2bb8576b5b34a to your computer and use it in GitHub Desktop.
D3 Interaction: Drop down menus
license: Creative Commons Attribution-ShareAlike (CC BY-SA)
height: 700
scrolling: yes
border: yes

Basic interaction with drop down menus

The example demonstrates how to use drop down menus to allow users to interact with your D3 visualizations.

Steps:

  1. Add <select> elements for each drop down. This could be done in HTML (as it is here) or in D3.

  2. Populate the menu items (<option>). If these are computed from your data (as they are here), you will want to do this in D3.

  3. Use .on("change", change_function) to call change_function() each time the seletion is changed. change_function() should be a function with no inputs. Make sure this is applied to the D3 selection for the <select> element and not for the <option> elements.

Additional notes for this example.

  1. This uses .join(enter => {}, update => {}, exit => {}) to add, modify, and remove elements in the DOM as selections change.
body {
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.axis line,
.axis path {
stroke: black;
opacity: 0.2;
}
.axis text {
fill: black;
opacity: 0.8;
}
// how long transitions last (msec)
let transitionTime = 2000
// explanations of the variables that make good plot labels.
let longVars = {
mpg: 'Miles per US gallon',
cyl: 'Number of cylinders',
disp: 'Displacement (cu.in.)',
hp: 'Gross horsepower',
drat: 'Rear axle ratio',
wt: 'Weight (1000 lbs)',
qsec: '1 / 4 mile time (sec)',
vs: 'Engine(0 = V - shaped, 1 = straight)',
am: 'Transmission (0 = automatic, 1 = manual)',
gear: 'Number of forward gears',
carb: 'Number of carburetors',
}
// use Margin Convention to layout the SVG with an inner plotting region
// and an outer region for axes, labels, etc.
let outerWidth = 600
let outerHeight = 400
let margins = { top: 30, bottom: 50, left: 50, right: 30 }
let innerWidth = outerWidth - margins.left - margins.right
let innerHeight = outerHeight - margins.top - margins.bottom
let scatterOuter = d3
.select('svg#cars-scatter')
.attr('width', outerWidth)
.attr('height', outerHeight)
let scatterInner = scatterOuter
.append('g')
.attr('id', 'inner-cars')
.attr('width', innerWidth)
.attr('height', innerHeight)
.attr('transform', 'translate(' + margins.left + ',' + margins.right + ')')
// load data
let url =
'https://raw.githubusercontent.com/rpruim/D3-examples/master/ReadingFiles/cars-data.csv'
d3.csv(url, cleanup_data)
.then(d => {
setup(d)
update(d)
})
.catch(error => console.log(error))
// d3.csv() reads in everything as a string. convert all but `model` to number.
function cleanup_data(d) {
for (k of Object.keys(d)) {
// NB: of, no in
if (k != 'model') d[k] = +d[k]
}
return d
}
// declare some variables that will assigned in functions below
let cars_global = []
let xScale, yScale, colorScale, sizeScalele
let xAxis, yAxis
// setup() is for setting up plot elements once at the beginning
// update() will be used for the initial draw and for redrawing upon change
function setup(cars) {
cars_global = cars
// model is not quantitative, so it shouldn't be selected
let variables = Object.keys(cars[0]).filter(d => d != 'model')
// border around plotting region
scatterInner
.append('rect')
.attr('width', innerWidth)
.attr('height', innerHeight)
.attr('fill', 'transparent')
.attr('stroke', 'black')
// populate selectors
d3.select('select.xvar')
.on('change', () => update(cars_global))
.selectAll('option')
.data(variables)
.enter()
.append('option')
.attr('value', d => d)
.text(d => longVars[d])
d3.select('select.yvar')
.on('change', () => update(cars_global)) // make sure .on() is above .selectAll()
.selectAll('option')
.data(variables)
.enter()
.append('option')
.attr('value', d => d)
.text(d => longVars[d])
d3.select('select.sizevar')
.on('change', () => update(cars_global))
.selectAll('option')
.data(variables)
.enter()
.append('option')
.attr('value', d => d)
.text(d => longVars[d])
// initialize values of select elements. NB: property, not attr!
d3.select('select.xvar').property('value', 'disp')
d3.select('select.yvar').property('value', 'mpg')
d3.select('select.sizevar').property('value', 'hp')
// read current selections
let xvar = d3.select('select.xvar').property('value')
let yvar = d3.select('select.yvar').property('value')
let sizevar = d3.select('select.sizevar').property('value')
// create scales based on selections
// the domain will be modified when selections change
xScale = d3
.scaleLinear()
.domain(d3.extent(cars.map(d => d[xvar]))) // get x variable from <select>
.range([20, innerWidth - 20])
xAxis = d3.axisBottom(xScale).tickSize(-innerHeight)
yScale = d3
.scaleLinear()
.domain(d3.extent(cars.map(d => d[yvar]))) // get y variable from <select>
.range([20, innerHeight - 20].reverse())
yAxis = d3.axisLeft(yScale).tickSize(-innerWidth)
sizeScale = d3
.scaleSqrt()
.domain([0, d3.max(cars.map(d => d[sizevar]))]) // get size variable from <select>
.range([5, 15])
// this scale won't change
colorScale = d3
.scaleOrdinal()
.domain(Object.keys(cars.map(d => d.cyl))) // 4, 6, and 8
.range(['red', 'green', 'blue'])
// create axes
scatterInner
.append('g')
.attr('transform', 'translate(' + 0 + ', ' + innerHeight + ')')
.attr('class', 'x axis') // note: two classes; handy!
.call(xAxis)
scatterInner
.append('g')
.attr('class', 'y axis')
.call(yAxis)
scatterOuter
.append('text')
.attr('class', 'x axis')
.attr('x', margins.left + innerWidth / 2)
.attr('y', outerHeight - margins.bottom / 2)
.attr('text-anchor', 'middle')
.text(longVars[xvar])
scatterOuter
.append('text')
.attr('class', 'y axis')
.attr('x', margins.left / 2)
.attr('y', margins.bottom + innerHeight / 2)
.attr('text-anchor', 'middle')
.attr(
'transform',
`rotate(-90 ${margins.left / 2} ${margins.bottom + innerHeight / 2})`
)
.text(longVars[yvar])
}
// update elements that get modified when selections change
function update(cars) {
let xvar = d3.select('select.xvar').property('value')
let yvar = d3.select('select.yvar').property('value')
let sizevar = d3.select('select.sizevar').property('value')
// update scales
xScale.domain([0, d3.max(cars.map(d => d[xvar]))])
yScale.domain([0, d3.max(cars.map(d => d[yvar]))])
sizeScale.domain([0, d3.max(cars.map(d => d[sizevar]))])
// update axes
scatterInner
.select('.x.axis')
.transition()
.duration(transitionTime)
.call(xAxis)
scatterInner
.select('.y.axis')
.transition()
.duration(transitionTime)
.call(yAxis)
// main plot
scatterInner
.selectAll('circle')
.data(cars)
.join(
enter =>
enter
.append('circle')
.attr('cx', d => xScale(d[xvar]))
.attr('cy', d => yScale(d[yvar]))
.style('fill', 'transparent')
.style('stroke-width', 5)
.attr('r', d => sizeScale(d[sizevar]))
.style('stroke', d => colorScale(d.cyl))
.style('opacity', 0.6),
update =>
update
.transition()
.duration(transitionTime)
.attr('cx', d => xScale(d[xvar]))
.attr('cy', d => yScale(d[yvar]))
.attr('r', d => sizeScale(d[sizevar])),
exit =>
exit
.transition()
.duration(transitionTime)
.remove()
)
// axis labels
scatterOuter
.selectAll('text.y.axis') // select text elements with two both classes
.transition()
.duration(transitionTime)
.text(longVars[yvar])
scatterOuter
.selectAll('text.x.axis')
.transition()
.duration(transitionTime)
.text(longVars[xvar])
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="stylesheet" href="cars-explorer.css" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<title>MT Cars</title>
</head>
<body>
<h1>Motor Trend Cars Data (1974)</h1>
<div id="controls">
<p>Select your variables and watch the plot change.</p>
<ul>
<li>
<span class="xvar">
x variable:
<select class="xvar" name="xvar"></select>
</span>
<br />
</li>
<li>
<span class="yvar">
y variable:
<select class="yvar" name="yvar"></select>
</span>
<br />
</li>
<li>
<span class="sizevar">
size variable:
<select class="sizevar" name="yvar"></select>
</span>
</li>
</ul>
</div>
<svg id="cars-scatter"></svg>
<p>
<strong>Data Source:</strong> mtcars data set in the datasets R package.
</p>
<script src="cars-explorer.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment