Skip to content

Instantly share code, notes, and snippets.

@haydenwagner
Last active March 5, 2017 22:29
Show Gist options
  • Save haydenwagner/a85fbb54b4ff60259678d9a1d171d030 to your computer and use it in GitHub Desktop.
Save haydenwagner/a85fbb54b4ff60259678d9a1d171d030 to your computer and use it in GitHub Desktop.
Visualization: D3 data-binding and selections
license: mit

This visual to shows the concept of D3 data-binding and enter/update/exit selections. It allows users to manually add (enter selection), change (update selection), and delete (exit selection) data points and watch the effect on the data-bound visual elements.

The data is an array of x/y coordinates that is visible to the user, and the visual elements are circles that correspond to the coordinate data.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name=viewport content="width=device-width, initial-scale=1">
<title>Visualization: D3 data-binding and selections</title>
<style type="text/css">html {
height: 100%;
width: 100%;
position: relative;
display: block; }
body {
margin: 0; }
.hw {
width: 100%;
max-width: 750px;
margin: 0 auto;
height: 300px;
position: relative;
display: block; }
.hw div.hw-data,
.hw div.hw-vis {
width: 50%;
height: 100%;
float: left; }
.hw circle {
stroke-width: .5px;
stroke: #cecece; }
.hw .hw-buttons {
text-align: center; }
.hw .hw-buttons .btn {
display: inline-block;
padding: 6px 12px;
margin-top: 5px;
margin-bottom: 0;
font-size: 14px;
font-weight: 400;
line-height: 1.42857143;
text-align: center;
white-space: nowrap;
vertical-align: middle;
-ms-touch-action: manipulation;
touch-action: manipulation;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-image: none;
border: 1px solid transparent;
border-radius: 4px; }
.hw .hw-buttons .btn-enter {
color: #fff;
background-color: #5cb85c;
border-color: #4cae4c; }
.hw .hw-buttons .btn-enter:hover {
color: #fff;
background-color: #449d44;
border-color: #398439; }
.hw .hw-buttons .btn-exit {
color: #fff;
background-color: #d9534f;
border-color: #d43f3a; }
.hw .hw-buttons .btn-exit:hover {
color: #fff;
background-color: #c9302c;
border-color: #ac2925; }
.hw .hw-buttons .btn-update {
color: #fff;
background-color: #5bc0de;
border-color: #46b8da; }
.hw .hw-buttons .btn-update:hover {
color: #fff;
background-color: #31b0d5;
border-color: #269abc; }
.hw .hw-data {
background: #c5c5c5;
overflow: auto; }
.hw .hw-data p {
font-family: "Lucida Console", Monaco, monospace;
font-size: 12px;
margin: 10px 0 0 10px;
float: left; }
@media screen and (max-width: 750px) {
.hw div {
width: 100%;
float: none; } }
</style>
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.js"></script>
</head>
<body>
<div class="hw">
<div class="hw-data"></div>
<div class="hw-vis"></div>
<div class="hw-buttons">
<button class="btn btn-enter" id="enter">Enter</button>
<button class="btn btn-update" id="update">Update</button>
<button class="btn btn-exit" id="exit">Exit</button>
</div>
</div>
</body>
<script>var containerWidth = d3.select(".hw-vis").style('width');
var containerHeight = d3.select(".hw-vis").style('height');
var pointPadding = 20;
var mainData = [];
var dataDiv = d3.select('.hw-data');
var svg = d3.select('.hw-vis').append('svg')
.attr('width', containerWidth)
.attr('height', containerHeight)
.style('background', 'gray');
var g = svg.append('g')
.attr('class','vis');
///////////
//add button listeners
d3.select('#enter').on('click', enterData);
d3.select('#update').on('click', updateData);
d3.select('#exit').on('click', exitData);
//make two random points and add to data
mainData.push(getRandomPoint(), getRandomPoint());
//update vis to reflect 2 newly added points
updateOrEnter();
////////////
function enterData(){
mainData.push(getRandomPoint());
updateOrEnter();
}
function updateData(){
updateOrEnter(getRandomDataPoint(true));
}
function exitData(){
exit( getRandomDataPoint() );
}
////d3 selection functions
/**
* Used when data enters or is updated within the main data array, and handles
* the d3 enter and update selections for the text and circle elements in the visualization
* @param updatedIndex {number} Index of an updated data point, used to selectively
* apply styles and transitions to only visual elements whose data has been updated.
*/
function updateOrEnter(updatedIndex){
var data = mainData;
var textSelection = dataDiv.selectAll('p:not(.exiting)').data(data),
circleSelection = g.selectAll('circle:not(.exiting)').data(data);
//text update
textSelection
.text(function(d){return handleText(d);})
.filter(function(d,i){
return i === updatedIndex;
})
.interrupt()
.style('color', '#31b0d5')
.transition()
.delay(500)
.duration(250)
.style('color', 'black');
//text enter
textSelection.enter()
.append('p')
.text(function(d){return handleText(d);})
.style('color', '#449d44')
.transition()
.delay(500)
.duration(250)
.style('color', 'black');
//circle update
circleSelection
.filter(function(d,i){
return i === updatedIndex;
})
.interrupt()
.attr('fill', '#5bc0de')
.transition()
.duration(500)
.attr('cx', function(d){return d[0];})
.attr('cy', function(d){ return d[1];})
.on('end', function () {
d3.select(this)
.transition()
.duration(250)
.attr('fill','black');
});
//circle enter
circleSelection.enter()
.append('circle')
.attr('cx', -10)
.attr('cy', function(d){ return d[1];})
.attr('r', 5)
.attr('fill', '#5cb85c')
.transition()
.duration(500)
.attr('cx', function(d){return d[0];})
.on('end', function(){
d3.select(this).transition()
.duration(250)
.attr('fill', 'black');
});
}
/**
* Used when data is deleted to exit the text and circle elements. Handles
* the d3 exit selection. Selections have additional class applied to them
* when they start their exiting process so they are not selected on subsequent
* calls of this function.
* @param removedIndex {number}
*/
function exit(removedIndex){
//make selections before deleting element
var data = mainData;
var textSelection = dataDiv.selectAll('p:not(.exiting)').data(data),
circleSelection = g.selectAll('circle:not(.exiting)').data(data);
//delete the removed object from the data
mainData.splice(removedIndex, 1);
//text exit
textSelection.filter(function(d,i){
return i === removedIndex;
})
.interrupt()
.classed("exiting", true)
.style('color', '#c9302c')
.transition()
.duration(350)
.on('end', function () {
d3.select(this).remove();
});
//circle exit
circleSelection.filter(function(d,i){
return i === removedIndex;
})
.interrupt()
.classed("exiting", true)
.attr('fill', '#d9534f')
.transition()
.duration(500)
.attr('cx', parseFloat(containerWidth) + 30)
.on('end', function () {
d3.select(this).remove();
});
}
//////////util functions
/**
* Returns a random number between min (inclusive) and max (exclusive)
* @param min {number} minimum for random number including this number
* @param max {number} maximum for random number excluding this number
* @returns {number} random num between the passed min and max
*/
function getRand(min, max) {
return Math.random() * (max - min) + min;
}
/**
* Makes and returns a set of random coordinates inside the container
* @returns {object} array of random coordinates
*/
function getRandomPoint(){
var p = pointPadding;
var w = parseFloat(containerWidth) - p;
var h = parseFloat(containerHeight) - p;
return [getRand(p,w), getRand(p,h)];
}
/**
* Turns the d3 data into a string so the user can view the data that
* is driving the visual elements
* @param d {string} Data point that we need get the coordinates from to show user
* @returns {string} String of the coordinates for this data point
*/
function handleText(d){
var wP = Math.floor(d[0]).toString();
var hP = Math.floor(d[1]).toString();
if(wP.length < 3){
wP = (' ' + wP);
}
if(hP.length < 3){
hP = (' ' + hP);
}
return ( '[' + wP + ',' + hP + ']' );
}
/**
* Randomly chooses a random data point and returns its index in the main data array,
* also an option to change the value of that data point before returning the index.
* @param updateData {boolean} Whether to change the value of the data point
* @returns {number} Index of randomly chosen data point
*/
function getRandomDataPoint(updateData){
if(mainData.length === 0){
console.log('no points to update or exit');
return;
}
var dataPointIndex = Math.floor( getRand(0, mainData.length) );
//for updating the randomly chosen data point
if(updateData){
mainData[dataPointIndex] = getRandomPoint();
}
return dataPointIndex;
}
//only if you want to get wild
// for(var i = 0; i < 500; i++){
// enterData();
// }
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment