Skip to content

Instantly share code, notes, and snippets.

@boeric
Last active December 29, 2022 16:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save boeric/67aeb6e441b5569be0dbc7b8ed43b286 to your computer and use it in GitHub Desktop.
Save boeric/67aeb6e441b5569be0dbc7b8ed43b286 to your computer and use it in GitHub Desktop.
D3 Selections and Key Function Demo

D3 Selection and Key Function Demo

Demonstrates the D3 enter, update, and exit selections, and the use of a key function when mutating and binding data. An array of 20 items is repeatedly mutated as the user is clicking the buttons. For each step, the visualization shows the content of the three selections after data binding.

The effect of turning off the key function is clearly visible.

Also demonstrates simple use of D3 transitions and javascript generators

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>D3 Key Function</title>
<!-- Author: Bo Ericsson, bo@boe.net -->
<script src="https://d3js.org/d3.v5.js"></script>
<style>
body {
font-family: helvetica;
margin: 0px;
}
button {
background-color: #eee;
border-radius: 5px;
font-size: 14px;
font-weight: bold;
height: 25px;
width: 100px;
}
svg {
outline: 1px solid lightgray;
}
.container {}
.headerContainer {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.headerTopContainer {
display: flex;
flex-direction: row;
font-size: 18px;
font-weight: bold;
justify-content: space-between;
margin: 10px;
}
.headerBottomContainer {
display: flex;
flex-direction: column;
font-size: 15px;
font-weight: normal;
margin: 10px;
}
.titleContainer {
width: 500px;
}
.checkboxContainer {
font-size: 13px;
font-weight: normal;
display: flex;
flex-direction: row;
width: 150px;
}
.buttonContainer {
height: 40px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.svgContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.svgTitle {
font-weight: bold;
}
.info {
background-color: white;
border-top: 1px solid lightgray;
display: none;
height: 90px;
left: 660px;
position: absolute;
top: 370px;
width: 270px;
}
.infoHeader {
font-size: 14px;
font-weight: bold;
margin-top: 5px;
}
.infoDetails {
font-size: 14px;
font-weight: normal;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<div class="headerContainer">
<div class="headerTopContainer">
<div class="titleContainer">
D3 Selections, Data Binding and Key Function Demo
</div>
<div class="checkboxContainer">
<div>
<input type="checkbox" id="key-function" name="key-function" checked>
<label for="key-function">Use key function</label>
</div>
</div>
</div>
<div class="headerBottomContainer">
<div class="buttonContainer">
</div>
</div>
</div>
<div class="svgContainer">
</div>
</div>
<div class="info">
<div class="infoHeader">
Info
</div>
<div class="infoDetails">
Stuff
</div>
</div>
</body>
<script>
'use strict';
// Average bar width and generator of random bar widths
const avgWidth = 120;
const generateWidth = () => avgWidth + ((Math.random() - 0.5) * avgWidth * 0.7);
// Create data
const data = [];
const dataSpec = [
{ color: 'Red', count: 4 },
{ color: 'Green', count: 3 },
{ color: 'Blue', count: 4 },
{ color: 'Violet', count: 3 },
{ color: 'Gray', count: 3 },
{ color: 'Tan', count: 3 },
].forEach((d) => {
const { color, count } = d;
for (let i = 0; i < count; i++) {
data.push({
color,
idx: `${i}`,
name: `${color}-${i}`,
width: generateWidth(),
});
}
});
// Then sort the data in place (Firsher-Yates)
for (let i = data.length - 1; i > 0; i--) {
const j = ~~(Math.random() * i);
const iValue = data[i];
data[i] = data[j];
data[j] = iValue;
}
// Constants
const LOAD = 'Load';
const MUTATE1 = 'Mutate1';
const MUTATE2 = 'Mutate2';
const REMOVE = 'Remove';
const RESTORE = 'Restore';
const MUTATE3 = 'Mutate3';
const MUTATE4 = 'Mutate4';
const RESET = 'Reset';
// Dimensions
const width = 960;
const height = 500;
const padding = 10;
const containerWidth = width - padding * 2;
const containerHeight = height - padding * 2;
const headerHeight = 90;
const svgContainerHeight = containerHeight - headerHeight;
const tileWidth = (containerWidth - padding) / 3;
const svgMainWidth = tileWidth * 2;
const svgExitWidth = tileWidth;
// Boolean that determines whether the key function should be used
let useKeyFn = true;
// Buttons
const buttons = [
LOAD,
MUTATE1,
MUTATE2,
REMOVE,
RESTORE,
MUTATE3,
MUTATE4,
RESET
];
// Enabbles specific button
function enableButton(button) {
d3.select('.buttonContainer').selectAll('button')
.property('disabled', d => d !== button);
}
// Disables all buttons
function disableButtons() {
d3.select('.buttonContainer').selectAll('button')
.property('disabled', true);
}
// Button click handler, which will initiate mutation of the data
function onClick(button) {
switch(button) {
case LOAD:
refresh(data);
enableButton(MUTATE1);
break;
case MUTATE1:
data.forEach(d => d.width = generateWidth());
refresh(data);
enableButton(MUTATE2);
break;
case MUTATE2:
data.forEach(d => d.width = generateWidth());
refresh(data);
enableButton(REMOVE);
break;
case REMOVE:
// Remove some elements
const filtered = data.filter(d => d.color !== 'Blue');
refresh(filtered);
enableButton(RESTORE);
break;
case RESTORE:
refresh(data);
enableButton(MUTATE3);
break;
case MUTATE3:
data.forEach(d => d.width = generateWidth());
refresh(data);
enableButton(MUTATE4);
break;
case MUTATE4:
data.forEach(d => d.width = generateWidth());
refresh(data);
enableButton(RESET);
break;
case RESET:
updateStaticDom();
enableButton(LOAD);
break;
default:
throw new Error(`Illegal option: ${button}`);
}
}
// Handler that triggers when mouse is hovered over a button and causes display of the info panel
function onMouseover(button) {
const header = button;
let details;
switch(button) {
case LOAD:
details = 'Loads the initial data bars with random widths. All data is new, and therefore will be placed in the enter selection';
break;
case MUTATE1:
details = 'The data is now being mutated (bar widths are changing). Nothing is added or removed, therefore all data will be in the update selection';
break;
case MUTATE2:
details = 'The data is now being mutated again';
break;
case REMOVE:
details = 'The blue bars are now removed (by being filtered out), and therefore are removed from the update selection and placed inte exit selection. Here they are reconstructed in the right pane (a different svg)';
break;
case RESTORE:
details = 'The blue bars are now added back in (by no longer being filtered out), and therefore are placed in the enter selection';
break;
case MUTATE3:
details = 'The data is now being mutated again. The previously added blue bars are now part of the update selection';
break;
case MUTATE4:
details = 'Another data mutation';
break;
case RESET:
details = 'Clears the DOM and resets the visualization';
break;
default:
throw new Error(`Illegal option: ${button}`);
}
// Update the info panel
d3.select('.info').style('display', 'block');
d3.select('.infoHeader').text(header);
d3.select('.infoDetails').text(details);
}
// Handler that turns off the info panel
function onMouseout() {
d3.select('.info').style('display', 'none');
}
// Handler that controls the use of the key function
function onChange() {
useKeyFn = d3.select("#key-function").property('checked');
}
// Triggers after every button click
function refresh(data) {
const svgMain = d3.select('.svgMain');
const svgExit = d3.select('.svgExit');
// Set key function
const keyFn = useKeyFn ? (d) => d.name : null;
// Get the selections
const updateSelection = svgMain.selectAll('rect').data(data, keyFn);
const enterSelection = updateSelection.enter();
const exitSelection = updateSelection.exit();
// Get any data from the exit selection
const exitData = exitSelection.data();
// Bar layout variables
const barHeight = 14;
const pitch = 17;
const enterY = 35;
const enterX = 10;
const updateX = enterX + tileWidth;
// Define two generator functions that will be used to generate y (instead of the array index)
function* updateYPosGen() {
let index = 0;
while (true) yield pitch * index++;
}
function* enterYPosGen() {
let index = 0;
while (true) yield pitch * index++;
}
const updateYPos = updateYPosGen(pitch);
const enterYPos = enterYPosGen(pitch);
updateSelection
.transition()
.duration(500)
.attr('x', updateX)
.attr('y', d => updateYPos.next().value + enterY)
.attr('width', d => d.width)
.attr('height', barHeight)
.style('fill', d => d.color);
enterSelection
.append('rect')
.transition()
.duration(500)
.attr('x', enterX)
.attr('y', d => enterYPos.next().value + enterY)
.attr('height', barHeight)
.style('fill', d => d.color)
.attr('width', d => d.width);
exitSelection
.transition()
.duration(500)
.style('opacity', 0)
.remove();
svgExit.selectAll('rect').remove();
svgExit.selectAll('rect')
.data(exitData, d => d.name)
.enter()
.append('rect')
.transition()
.duration(500)
.attr('x', enterX)
.attr('y', (d, i) => i * pitch + enterY)
.attr('width', d => d.width)
.attr('height', barHeight)
.style('fill', d => d.color);
}
// First update the dom with dimensions and add buttons and svg elements
updateStaticDom();
// Enable Load button
enableButton('Load');
// Update the static DOM
function updateStaticDom() {
d3.select('.svgContainer').selectAll('svg').remove();
// Adjust dimensions of container divs
d3.select('.container')
.style('padding', `${padding}px`)
.style('max-width', `${containerWidth}px`)
.style('max-height', `${containerHeight}px`);
const headerContainer = d3.select('.headerContainer')
.style('height', `${headerHeight}px`)
.on('mouseout', onMouseout);
const svgContainer = d3.select('.svgContainer')
.style('max-height', svgContainerHeight);
d3.select('.info')
.style('width', `${svgExitWidth - padding * 2}px`)
.style('left', `${width - svgExitWidth}px`);
// Add the buttons and event handlers
d3.select('.buttonContainer').selectAll('button')
.data(buttons)
.enter()
.append('button')
.on('click', onClick)
.on('mouseover', onMouseover)
.on('mouseout', onMouseout)
.text(d => d);
// Add event handler to checkbox
d3.select('#key-function')
.on('change', onChange);
// Create main svg
const svgMain = svgContainer
.append('svg')
.attr('class', 'svg svgMain')
.attr('width', svgMainWidth)
.attr('height', svgContainerHeight);
// Create exit svg
const svgExit = svgContainer
.append('svg')
.attr('class', 'svg svgExit')
.attr('width', svgExitWidth)
.attr('height', svgContainerHeight);
// Set svg titles in the main svg
svgMain
.append('text')
.attr('x', padding)
.attr('y', 20)
.attr('class', 'svgTitle')
.text('Enter Selection');
svgMain
.append('text')
.attr('x', padding + tileWidth)
.attr('y', 20)
.attr('class', 'svgTitle')
.text('Update Selection');
// Set the title in exit svg
svgExit
.append('text')
.attr('x', padding)
.attr('y', 20)
.attr('class', 'svgTitle')
.text('Exit Selection');
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment