Skip to content

Instantly share code, notes, and snippets.

@w3tweaks
Last active May 14, 2020 02:01
Show Gist options
  • Select an option

  • Save w3tweaks/cc2ce8c2c1d7400053f617af4929f914 to your computer and use it in GitHub Desktop.

Select an option

Save w3tweaks/cc2ce8c2c1d7400053f617af4929f914 to your computer and use it in GitHub Desktop.
Demo 3
<!-- markup structure
.app, wrapping container
header, introducing the visualization
.input, showing the array behind the visualization
.visualization, showing one circle for each data point
.controls, allowing to add or remove data points in the array
.selection, highlighting the selections after each operation
-->
<div class="app">
<header>
<h1>D3 Data Binding</h1>
</header>
<div class="input">
<p>
<strong>Input array:</strong>
<!-- include here the array describing the data points -->
<span></span>
</p>
</div>
<!-- include here one sphere for each data point -->
<main class="visualization"></main>
<div class="controls">
<button class="controls--add">Add data point</button>
<button class="controls--remove">Remove data point</button>
</div>
<!-- include in each paragraph the data points described in the enter/update/exit selection -->
<div class="selection">
<div class="selection--enter">
<h3>Enter Selection</h3>
<p></p>
</div>
<div class="selection--update">
<h3>Update Selection</h3>
<p></p>
</div>
<div class="selection--exit">
<h3>Exit Selection</h3>
<p></p>
</div>
</div>
</div>
// utility functions to generate a random data point
const randomInt = (max = 10) => Math.floor(Math.random() * max);
const randomLetter = () => String.fromCharCode(Math.floor(Math.random() * ('Z'.charCodeAt() - 'A'.charCodeAt()) + 'A'.charCodeAt()));
// specify a unique identifier to differentiate the data points
const randomDataPoint = () => ({
value: `${randomInt()}${randomLetter()}`,
id: Math.random(),
});
const dataPoints = 10;
// array describing the data
let data = Array(dataPoints).fill('').map(() => randomDataPoint());
// function visualizing the data points
function visualizeData(inputData) {
const visualization = d3
.select('.visualization');
// update selection, existing nodes
const updateSelection = visualization
.selectAll('div')
.data(inputData, d => d.id); // object constancy, bind each element according to the id of the connected data point
// enter selection, to be added
const enterSelection = updateSelection
.enter();
// exit selection, to be removed
const exitSelection = updateSelection
.exit();
// style the update selection with the matching class
updateSelection
.attr('class', 'selection--update');
// add a div for each new data point
enterSelection
.append('div')
.attr('class', 'selection--enter')
.text(d => d.value)
// transition the div from 0 to 1
.style('transform', 'scale(0)')
.transition()
.delay((d, i) => 200 + 50 * i)
.duration(500)
.style('transform', 'scale(1)');
// remove the div for each no-longer-existing data point
exitSelection
.attr('class', 'selection--exit')
// remove after the transition
// ! if two elements are removed in succession and before the transition is complete the exit selection will consider both
.transition()
.delay(200)
.duration(1000)
.style('transform', 'scale(0)')
.remove();
// complete the visualization updating the UI of the application
// include the data points in the prescribed span
d3
.select('.input p span')
.html(inputData.map(({ value }) => value));
// add the selections to their respective container
d3
.select('.selection .selection--enter p')
.text(enterSelection._groups[0].map(group => group.__data__.value));
d3
.select('.selection .selection--update p')
.text(updateSelection._groups[0].map(group => group.__data__.value));
d3
.select('.selection .selection--exit p')
.text(exitSelection._groups[0].map(group => group.__data__.value));
}
// immediately call the function to visualize the existing data
visualizeData(data);
// call the function following a click event on the .controls buttons, afte adding / removing a data point from the array
// ! add / remove a data point at a random index of the array
function addDataPoint() {
const randomIndex = randomInt(data.length);
data = [...data.slice(0, randomIndex), randomDataPoint(), ...data.slice(randomIndex)];
visualizeData(data);
}
// function removing a data point
function removeDataPoint() {
const randomIndex = randomInt(data.length);
data = [...data.slice(0, randomIndex), ...data.slice(randomIndex + 1)];
visualizeData(data);
}
// bind the function
d3
.select('.controls--add')
.on('click', addDataPoint);
d3
.select('.controls--remove')
.on('click', removeDataPoint);
<script src="https://unpkg.com/d3@5.9.7/dist/d3.min.js"></script>
@import url("https://fonts.googleapis.com/css?family=Lato:400,900&display=swap");
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
min-height: 100vh;
background: repeating-linear-gradient(
-45deg,
hsl(0, 0%, 94%) 0px,
hsl(0, 0%, 94%) 5px,
transparent 0px,
transparent 40px
),
repeating-linear-gradient(
-45deg,
hsl(0, 0%, 94%) 0px,
hsl(0, 0%, 94%) 10px,
transparent 0px,
transparent 100px
),
hsl(0, 0%, 97%);
font-family: "Lato", sans-serif;
}
.app {
background: #fff;
padding: 1rem 2rem;
max-width: 900px;
margin: 2rem auto;
min-height: 80vh;
box-shadow: 0 1px 10px hsla(0, 0%, 0%, 0.1);
/* display the containers in a column */
display: flex;
flex-direction: column;
}
/* separate the direct children vertically */
.app > * {
margin: 1rem 0;
}
/* stretch the visualization to cover the available space, if any */
.visualization {
flex-grow: 1;
/* display the circle in a wrapping row */
display: flex;
flex-wrap: wrap;
}
/* style the div included for the data points as circles */
.visualization div {
width: 48px;
height: 48px;
margin: 0.5rem;
line-height: 48px;
border-radius: 50%;
text-align: center;
font-weight: 900;
}
/* center the controls */
.controls {
display: flex;
justify-content: center;
}
.controls button {
margin: 0 1rem;
padding: 0.5rem 1rem;
background: hsl(0, 0%, 20%);
color: hsl(0, 0%, 94%);
border: none;
border-radius: 5px;
box-shadow: 0 1px 5px hsla(0, 0%, 0%, 0.1);
font-family: inherit;
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.1rem;
}
.controls button:active {
transform: translateY(2px);
}
/* display the selections in a wrapping row */
.selection {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.selection div {
margin: 1rem;
border-radius: 20px;
padding: 1rem;
}
/* style the classes describing the different selection with a different background and border-style */
.selection--update {
background: hsl(240, 75%, 90%);
border: 2px solid currentColor;
}
.selection--enter {
background: hsl(120, 75%, 90%);
border: 2px dashed currentColor;
}
.selection--exit {
background: hsl(0, 75%, 90%);
border: 2px dotted currentColor;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment