commenting scatter plot code

master
Matt Huntington 7 years ago
parent 5832f13ccd
commit a0ac6c696e

@ -1,26 +1,32 @@
circle { circle {
r:5; r:5;
fill: black; fill: black;
/* transitions to make growing and changing color look nice */
transition: r 0.5s linear, fill 0.5s linear; transition: r 0.5s linear, fill 0.5s linear;
} }
#container { #container {
overflow: visible; overflow: visible; /* make sure axes are visible */
margin-bottom: 50px; margin-bottom: 50px; /* space below graph so table doesn't overlap */
} }
body { body {
/* move graph away from edge of screen */
margin: 20px 40px; margin: 20px 40px;
} }
table, th, td { table, th, td {
/* make table cell borders visible */
border: 1px solid black; border: 1px solid black;
} }
th, td { th, td {
/* make table cels look better */
padding:10px; padding:10px;
text-align: center; text-align: center;
} }
/* hover state for circles */
circle:hover { circle:hover {
r:10; r:10;
fill: blue; fill: blue;
} }
/* styles for circles when you click on them */
circle:active { circle:active {
fill: red; fill: red;
} }

@ -1,6 +1,8 @@
// dimensions for svg
var WIDTH = 800; var WIDTH = 800;
var HEIGHT = 600; var HEIGHT = 600;
// run data
var runs = [ var runs = [
{ {
id: 1, id: 1,
@ -19,66 +21,90 @@ var runs = [
} }
]; ];
// set dimensions of outer SVG
d3.select('#container') d3.select('#container')
.style('width', WIDTH) .style('width', WIDTH)
.style('height', HEIGHT); .style('height', HEIGHT);
var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p"); var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p"); //use this to convert strings to dates
var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p"); var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p"); //use this to convert dates to strings
var xScale = d3.scaleTime(); var xScale = d3.scaleTime(); //create the scale used to convert dates to x position values
xScale.range([0,WIDTH]); xScale.range([0,WIDTH]); //set visual range of xScale to be 0 -> 800
var xDomain = d3.extent(runs, function(datum, index){ var xDomain = d3.extent(runs, function(datum, index){ //create array containing min/max date values for run data
return parseTime(datum.date); return parseTime(datum.date); //use parseTime to convert string data value to data object
}); });
xScale.domain(xDomain); xScale.domain(xDomain);//set domain of xScale to min/max values created by d3.extent in last step
var yScale = d3.scaleLinear(); //create the scale var yScale = d3.scaleLinear(); //create the scale used to convert distances run to y position values
yScale.range([HEIGHT, 0]); //set the visual range (e.g. 600 to 0) // set the visual range to 600 -> 0
// remember 600 will map to a low run distance value and 0 will map to a high run distance value
// we do this because y starts at 0 at the top of the SVG and increases in value as we move down the SVG
yScale.range([HEIGHT, 0]);
//create array containing min/max distance values for run data
var yDomain = d3.extent(runs, function(datum, index){ var yDomain = d3.extent(runs, function(datum, index){
return datum.distance; //compare distance properties of each item in the data array return datum.distance; //compare distance properties of each item in the data array
}) })
yScale.domain(yDomain); yScale.domain(yDomain); //set domain of yScale to min/max values created by d3.extent in the last step
//render function which creates the circles and attaches event handlers to them
var render = function(){ var render = function(){
var circles = d3.select('#points') //circles var for holding our circle selction
.selectAll('circle') var circles = d3.select('#points') //first select #points so we have somewhere to append circles later
.selectAll('circle') //now select all circles even if none exist
//attach data to the circles and set up each circle's id according to the following function
.data(runs, function(datum){ .data(runs, function(datum){
return datum.id return datum.id //use each datum's id property as the id for the circle it's being attached to
}); });
//find all data elements not attached to circles and create a circle for each one
circles.enter().append('circle'); circles.enter().append('circle');
//remove all extra circles that are not attached to data
circles.exit().remove(); circles.exit().remove();
d3.selectAll('circle') d3.selectAll('circle') //select all circles
.attr('cy', function(datum, index){ .attr('cy', function(datum, index){ //loop through each circle and set its cy according to the follwing function
//find the distance property of the circle's associated datum
//convert that distance value from a numeric value to a visual point on the SVG using yScale
return yScale(datum.distance); return yScale(datum.distance);
}); });
d3.selectAll('circle') d3.selectAll('circle') //select all circles
.attr('cx', function(datum, index){ .attr('cx', function(datum, index){ //loop through each circle and set its cx according to the follwing function
return xScale(parseTime(datum.date)); //use parseTime to convert the date string property on the datum object to a Date object, which xScale then converts to a visual value //find the date property of the circle's associated datum
//convert that date value from a string value to a date using parseTime
//convert that date object to a visual point on the SVG using xScale
return xScale(parseTime(datum.date));
}); });
//put this at the bottom of the render function, so that click handlers are attached when the circle is created //put this at the bottom of the render function, so that click handlers are attached after the circles is created
//if you put it outside the render function, there will be no circles to attach click handlers to
d3.selectAll('circle').on('click', function(datum, index){ d3.selectAll('circle').on('click', function(datum, index){
d3.event.stopPropagation(); //stop click event from propagating to the SVG element and creating a run d3.event.stopPropagation(); //stop click event from propagating to the SVG element and creating a run accidentally
runs = runs.filter(function(run, index){ //create a new array that has removed the run with the correct id. Set it to the runs var //create a new array that has removed the run with the correct id. Set it to the runs var
return run.id != datum.id; runs = runs.filter(function(run, index){
return run.id != datum.id; //keep all elements in the 'runs' array that do not have the id of the cirlce that was clicked
}); });
render(); //re-render dots render(); //re-render dots
createTable(); //re-render table createTable(); //re-render table
}); });
//function to be called once dragging a circle is complette
var dragEnd = function(datum){ var dragEnd = function(datum){
//find x/y of where the click happened
var x = d3.event.x; var x = d3.event.x;
var y = d3.event.y; var y = d3.event.y;
var date = xScale.invert(x); //convert those x/y values to date/distance values
var date = xScale.invert(x); //note date is Date object. We'll convert it to a properly formatted string later
var distance = yScale.invert(y); var distance = yScale.invert(y);
datum.date = formatTime(date); //adjust the date/distance values on the datum associated with the circle that was dragged
datum.date = formatTime(date); //use formatTime to turn the date object into a string
datum.distance = distance; datum.distance = distance;
createTable(); createTable();//re-render the table
} }
// function to be called while the user drags a circle
var drag = function(datum){ var drag = function(datum){
var x = d3.event.x; //get current x position of the cursor var x = d3.event.x; //get current x position of the cursor
var y = d3.event.y; //get current y position of the cursor var y = d3.event.y; //get current y position of the cursor
@ -86,46 +112,67 @@ var render = function(){
d3.select(this).attr('cy', y); //change the dragged element's cy attribute to whatever the y position of the cursor is d3.select(this).attr('cy', y); //change the dragged element's cy attribute to whatever the y position of the cursor is
} }
var dragBehavior = d3.drag() //create a drag behavior var dragBehavior = d3.drag() //create a drag behavior
.on('drag', drag) //call the "drag" function (the 2nd param) each time the user moves the cursor before releasing the mouse button. The "drag" function is defined above //call the "drag" function (the 2nd param) each time the user moves the cursor before releasing the mouse button.
.on('end', dragEnd); //dragEnd is a reference to a function we haven't created yet //The "drag" function is defined above
.on('drag', drag)
//call the "dragEnd" function (the 2nd param) once the user releases the "mouse button"
.on('end', dragEnd);
d3.selectAll('circle').call(dragBehavior); //attach the dragBehavior behavior to all <circle> elements d3.selectAll('circle').call(dragBehavior); //attach the dragBehavior behavior to all <circle> elements
} }
//render circles on page load
render(); render();
//create a bottomAxis generator. Pass the xScale so it knows how to label the axis
var bottomAxis = d3.axisBottom(xScale); var bottomAxis = d3.axisBottom(xScale);
d3.select('#container') d3.select('#container') //select the outer SVG
.append('g') .append('g') //append a <g> to it
.attr('id', 'x-axis') //add an id .attr('id', 'x-axis') //add an id
.call(bottomAxis) .call(bottomAxis) //call the axis generator on that <g> so that an axis is generated within it
.attr('transform', 'translate(0,'+HEIGHT+')'); .attr('transform', 'translate(0,'+HEIGHT+')'); //move the axis to the bottom of the SVG
//create a leftAxis generator. Pass the yScale so it knows how to label the axis
var leftAxis = d3.axisLeft(yScale); var leftAxis = d3.axisLeft(yScale);
d3.select('#container') d3.select('#container') //select the outer SVG
.append('g') .append('g') //append a <g> to it
.attr('id', 'y-axis') //add an id .attr('id', 'y-axis') //add an id
.call(leftAxis); .call(leftAxis); //call the axis generator on that <g> so that an axis is generated within it
//define the createTable function here
var createTable = function(){ var createTable = function(){
d3.select('tbody').html(''); //clear out all rows from the table d3.select('tbody').html(''); //clear out all rows from the table
//loop through each element in the runs array
for (var i = 0; i < runs.length; i++) { for (var i = 0; i < runs.length; i++) {
var row = d3.select('tbody').append('tr'); var row = d3.select('tbody').append('tr');//append a tr to the tbody element
//create cells for id, date, and distance and add the values appropriately
row.append('td').html(runs[i].id); row.append('td').html(runs[i].id);
row.append('td').html(runs[i].date); row.append('td').html(runs[i].date);
row.append('td').html(runs[i].distance); row.append('td').html(runs[i].distance);
} }
} }
//render the table on page load
createTable(); createTable();
//create a click handler on the main SVG, allowing the user to create new runs by click anywhere in it
d3.select('#container').on('click', function(){ d3.select('#container').on('click', function(){
var x = lastTransform.invertX(d3.event.offsetX);
var y = lastTransform.invertY(d3.event.offsetY); //create x and y vars to the x/y values of the point that the user clicked
var x = d3.event.offsetX;
var y = d3.event.offsetY;
//if a transform occurred (zoom/pan), adjust the x/y vars to take this into account
if(lastTransform !== null){
x = lastTransform.invertX(d3.event.offsetX);
y = lastTransform.invertY(d3.event.offsetY);
}
var date = xScale.invert(x) //get a date value from the visual point that we clicked on var date = xScale.invert(x) //get a date value from the visual point that we clicked on
var distance = yScale.invert(y); //get a numeric distance value from the visual point that we clicked on var distance = yScale.invert(y); //get a numeric distance value from the visual point that we clicked on
var newRun = { //create a new "run" object var newRun = { //create a new "run" object
id: ( runs.length > 0 ) ? runs[runs.length-1].id+1 : 1, //generate a new id by adding 1 to the last run's id //generate a new id by adding 1 to the last run's id
//if no runs exist, set the id to 1
id: ( runs.length > 0 ) ? runs[runs.length-1].id+1 : 1,
date: formatTime(date), //format the date object created above to a string date: formatTime(date), //format the date object created above to a string
distance: distance //add the distance distance: distance //add the distance
} }
@ -134,16 +181,20 @@ d3.select('#container').on('click', function(){
render(); //add this line render(); //add this line
}); });
//create a var to hold any zoom/pan transformations that occur
var lastTransform = null; var lastTransform = null;
//function to be called each to a user zooms/pans
var zoomCallback = function(){ var zoomCallback = function(){
lastTransform = d3.event.transform; //add this lastTransform = d3.event.transform; //save the transformation that just happened
d3.select('#points').attr("transform", d3.event.transform); d3.select('#points').attr("transform", d3.event.transform); //transform the <g id="point"> appropriately
d3.select('#x-axis') d3.select('#x-axis') //adjust the values in the x axis
.call(bottomAxis.scale(d3.event.transform.rescaleX(xScale))); .call(bottomAxis.scale(d3.event.transform.rescaleX(xScale)));
d3.select('#y-axis') d3.select('#y-axis') //adjust the values in the y axis
.call(leftAxis.scale(d3.event.transform.rescaleY(yScale))); .call(leftAxis.scale(d3.event.transform.rescaleY(yScale)));
} }
//set up the zoom behavior generator
var zoom = d3.zoom() var zoom = d3.zoom()
.on('zoom', zoomCallback); .on('zoom', zoomCallback);
d3.select('#container').call(zoom); d3.select('#container').call(zoom); //attach the zoom behavior to the #container SVG

@ -6,11 +6,15 @@
<link rel="stylesheet" href="app.css"> <link rel="stylesheet" href="app.css">
</head> </head>
<body> <body>
<!-- container for axes-->
<svg id="container"> <svg id="container">
<!-- inner svg for clipping circles-->
<svg> <svg>
<!-- group to contain circles for zooming/panning -->
<g id="points"></g> <g id="points"></g>
</svg> </svg>
</svg> </svg>
<!-- table for displaying data -->
<table> <table>
<thead> <thead>
<tr> <tr>
@ -22,6 +26,7 @@
<tbody> <tbody>
</tbody> </tbody>
</table> </table>
<!-- put scripts at bottom to ensure DOM loads before scripts begin processing it -->
<script src="https://d3js.org/d3.v5.min.js"></script> <script src="https://d3js.org/d3.v5.min.js"></script>
<script src="app.js" charset="utf-8"></script> <script src="app.js" charset="utf-8"></script>
</body> </body>

Loading…
Cancel
Save