From eaefe9b452215fc80d1a9c4bd99b12f43f29367e Mon Sep 17 00:00:00 2001 From: Matt Huntington Date: Mon, 10 Sep 2018 14:41:04 -0400 Subject: [PATCH] removing notes --- BAR.md | 513 --------------- D3.md | 319 ---------- FORCE_DIRECTED_GRAPH.md | 285 --------- INTRO.md | 54 -- Lab.md | 146 ----- MAPS.md | 220 ------- PIE.md | 440 ------------- README.md | 25 - SCATTER_PLOT.md | 1306 --------------------------------------- SUMMARY.md | 25 - SVG.md | 480 -------------- 11 files changed, 3813 deletions(-) delete mode 100644 BAR.md delete mode 100644 D3.md delete mode 100644 FORCE_DIRECTED_GRAPH.md delete mode 100644 INTRO.md delete mode 100644 Lab.md delete mode 100644 MAPS.md delete mode 100644 PIE.md delete mode 100644 README.md delete mode 100644 SCATTER_PLOT.md delete mode 100644 SUMMARY.md delete mode 100644 SVG.md diff --git a/BAR.md b/BAR.md deleted file mode 100644 index 479bd0e..0000000 --- a/BAR.md +++ /dev/null @@ -1,513 +0,0 @@ -# Bar Graph - -In this section, we'll use AJAX to build a bar graph. By the end, you should be able to: - -1. Use AJAX to make an asynchronous call to an external data file -1. Create a Bar graph - -## Set up - -Let's create our standard setup in `index.html`: - -```html - - - - - - - - - - - -``` - -`app.js`: - -```javascript -var WIDTH = 800; -var HEIGHT = 600; - -d3.select('svg') - .style('width', WIDTH) - .style('height', HEIGHT); -``` - -`app.css`: - -```css -svg { - border:1px solid black; -} -``` - -This is what we should have: - -![](https://i.imgur.com/unogWXl.png) - -## Create an external file to hold our data - -Let's create a `data.json` file, which will hold fake data regarding how often job posts require certain skills. This should be the contents of the file: - -```json -[ - { - "name": "HTML", - "count": 21 - }, - { - "name": "CSS", - "count": 17 - }, - { - "name": "Responsive Web Design", - "count": 17 - }, - { - "name": "JavaScript", - "count": 17 - }, - { - "name": "Git", - "count": 16 - }, - { - "name": "Angular.js", - "count": 9 - }, - { - "name": "Node.js", - "count": 9 - }, - { - "name": "PostgreSQL", - "count": 8 - }, - { - "name": "Agile Project Management", - "count": 8 - }, - { - "name": "MongoDB", - "count": 7 - }, - { - "name": "Trello", - "count": 7 - }, - { - "name": "Testing / TDD", - "count": 7 - }, - { - "name": "jQuery", - "count": 7 - }, - { - "name": "User Testing", - "count": 6 - }, - { - "name": "MySQL", - "count": 6 - }, - { - "name": "PHP", - "count": 6 - }, - { - "name": "React.js", - "count": 6 - }, - { - "name": "AJAX", - "count": 6 - }, - { - "name": "Express.js", - "count": 5 - }, - { - "name": "Heroku", - "count": 5 - }, - { - "name": "Wireframing", - "count": 5 - }, - { - "name": "Sass/SCSS", - "count": 5 - }, - { - "name": "Mobile Web", - "count": 4 - }, - { - "name": "Rails", - "count": 4 - }, - { - "name": "WordPress", - "count": 4 - }, - { - "name": "Drupal", - "count": 3 - }, - { - "name": "Ruby", - "count": 3 - }, - { - "name": "Ember.js", - "count": 3 - }, - { - "name": "Python", - "count": 3 - }, - { - "name": "Amazon EC2", - "count": 2 - }, - { - "name": "Computer Science degree", - "count": 1 - }, - { - "name": "Backbone.js", - "count": 1 - }, - { - "name": "Less", - "count": 1 - }, - { - "name": "Prototyping", - "count": 1 - }, - { - "name": "Redis", - "count": 1 - } -] -``` - -## Make an AJAX Request - -### Write the basic code: - -D3 has lots of different methods for making AJAX requests to files of different data types: - -```javascript -d3.json('path').then(function(data){ - //do something with the json data here -}); -d3.csv('path').then(function(data){ - //do something with the csv data here -}); -d3.tsv('path').then(function(data){ - //do something with the tsv data here -}); -d3.xml('path').then(function(data){ - //do something with the xml data here -}); -d3.html('path').then(function(data){ - //do something with the html data here -}); -d3.text('path').then(function(data){ - //do something with the text data here -}); -``` - -Since our data is in JSON format, we'll use the first kind of call: - -```javascript -d3.json('data.json').then(function(data){ - console.log(data); -}); -``` - -### Handle file access - -If you opened the `index.html` file in Chrome directly, instead of serving it on a web server, you'll notice we've encountered an error. Check your developer console: - -![](https://i.imgur.com/OyNP4o0.png) - -The issue here is that web browsers are not supposed to make AJAX requests to files on your computer. If it could, this would be a huge security flaw because any website could access files on your computer. Let's create a basic file server. To do this, you'll need to install Node.js (https://nodejs.org/en/). Once that's done, open up your computer's terminal - -- Mac: `command + space` then type `terminal` and hit enter -- Windows: click Start, type `cmd` and hit enter - -Next type the following into your terminal: - -``` -npm install -g http-server -``` - -If you get error messages try - -``` -sudo npm install -g http-server -``` - -This installs a basic http server that was built using Node.js. To run it, use the terminal to navigate to the directory where you code is (type `cd` to change folders in the terminal) and run the following: - -``` -http-server . -``` - -You should see something like this: - -![](https://i.imgur.com/Wja8NNL.png) - -Now go to http://localhost:8080/ in your browser. You should now see that your AJAX call is succeeding (if you have issues, hold down shift and hit the refresh button to force the browser to reload all files that may have been cached): - -![](https://i.imgur.com/9QOUqSK.png) - -## Use AJAX data to create SVG elements - -Now that our AJAX calls are succeeding, let's start building our app. From here on out, it's all basic JavaScript and D3. Note that everything we'll write for the rest of this lesson is done within the success callback of our AJAX request. In production we might want to move this code elsewhere, but for now this is easier for learning. Let's create some rectangles for our bar graph. The bottom of `app.js` (the callback to the AJAX request) should now look like this: - -```javascript -d3.json('data.json').then(function(data){ - d3.select('svg').selectAll('rect') - .data(data) - .enter() - .append('rect'); -}); -``` - -Our Elements tab in our dev tools should look something like this: - -![](https://i.imgur.com/39BKG1V.png) - -## Adjust the height/width of the bars - -Let's create a scale that maps the `count` property of each element in `data` to a visual height for the corresponding bar. We'll use a linear scale. Remember to map the `HEIGHT` of the graph to a very low data point and the top of the graph (0 in the range) map to a very high data value. Add this code at the bottom of the AJAX callback: - -```javascript -var yScale = d3.scaleLinear(); -yScale.range([HEIGHT, 0]); -var yMin = d3.min(data, function(datum, index){ - return datum.count; -}) -var yMax = d3.max(data, function(datum, index){ - return datum.count; -}) -yScale.domain([yMin, yMax]); -``` - -We could use `d3.extent`, but we're going to need the individual min/max values later on. Immediately after the above code, let's tell D3 to adjust the height of the rectangles using the `yScale`. Remember that the Y axis is flipped. A low data value produces a high range value. But a even though the range is high, the bar itself should be small. We'll need to re-flip the values just for height so that a low data value produces a small bar and a high data value produces a large bar. To do this, let's subtract whatever the range point is from the `HEIGHT` of the graph. This way, if `yScale(datum.count)` produces, say, 500, the height of the bar will be 100. We can use `yScale(datum.count)` normally when adjusting the position of the bars later. Add this code at the bottom of the AJAX callback: - -```javascript -d3.selectAll('rect') - .attr('height', function(datum, index){ - return HEIGHT-yScale(datum.count); - }); -``` - -Now our rectangles have height, but no width: - -![](https://i.imgur.com/HKSnXzl.png) - -At the bottom of `app.css` let's give all our bars the same width: - -```css -rect { - width: 15px; -} -``` - -Here's what we should see in Chrome now: - -![](https://i.imgur.com/W2yoUyC.png) - -## Adjust the horizontal/vertical placement of the bars - -Our bars all overlap each other at the moment. Let's space them out by mapping x position to index in the data array. Add the following to the bottom of the AJAX callback: - -```javascript -var xScale = d3.scaleLinear(); -xScale.range([0, WIDTH]); -xScale.domain([0, data.length]); -d3.selectAll('rect') - .attr('x', function(datum, index){ - return xScale(index); - }); -``` - -This maps indices in the in the array to horizontal range points. Chrome should look like this: - -![](https://i.imgur.com/3d7ddVy.png) - -Now let's move the bars so they grow from the bottom, not the hang from the top. Add the following to the end of the AJAX callback: - -```javascript -d3.selectAll('rect') - .attr('y', function(datum, index){ - return yScale(datum.count); - }); -``` - -Using our `yScale` function, a high data value produces a low range value which doesn't push a large bar down much. A low data point produces a high range value which pushes a small bar down a lot. - -Our last few bars don't have any height, because we've mapped the minimum `count` property of our data to a visual range value of 0 in our `yScale`. Let's adjust the last line of this code: - -```javascript -var yScale = d3.scaleLinear(); -yScale.range([HEIGHT, 0]); -var yMin = d3.min(data, function(datum, index){ - return datum.count; -}) -var yMax = d3.max(data, function(datum, index){ - return datum.count; -}) -yScale.domain([yMin, yMax]); -``` - -to be this code: - -```javascript -var yScale = d3.scaleLinear(); -yScale.range([HEIGHT, 0]); -var yMin = d3.min(data, function(datum, index){ - return datum.count; -}) -var yMax = d3.max(data, function(datum, index){ - return datum.count; -}) -yScale.domain([yMin-1, yMax]); //adjust this line -``` - -Now the domain min is 1 less than what's actually in our data set. Domains with the original min are treated as higher values than what's expected for the min of the graph. We get this: - -![](https://i.imgur.com/PnIvwux.png) - -## Make width of bars dynamic - -Currently, our bars have a fixed width. No matter how many elements we have, they have 15px width. If we had more data elements, the bars could overlap. Let's change this. Since each `rect` will be the same width, no matter what the data is, we can just assign `width` a computed value. Add the following to the end of the AJAX callback: - -```javascript -d3.selectAll('rect') - .attr('width', WIDTH/data.length); -``` - -Now let's adjust our `rect` css so our bars are more visible: - -```css -rect { - /* remove the width rule that was here */ - stroke:white; - stroke-width:1px; -} -``` - -![](https://i.imgur.com/7FZyBqu.png) - -## Change the color of the bar based on data - -Right now the bars are black. A linear scale will interpolate between colors just like a regular number. Add the following to the end of the AJAX callback: - -```javascript -var yDomain = d3.extent(data, function(datum, index){ - return datum.count; -}) -var colorScale = d3.scaleLinear(); -colorScale.domain(yDomain) -colorScale.range(['#00cc00', 'blue']) -d3.selectAll('rect') - .attr('fill', function(datum, index){ - return colorScale(datum.count) - }) -``` - -Notice that we calculate the yDomain using `d3.extent` so that the real min of the data set is used to map #00cc00: - -![](https://i.imgur.com/zCrKZtB.png) - -## Add axes - -The left axis is just like in the scatter plot chapter. Add this code to the bottom of the AJAX callback: - -```javascript -var leftAxis = d3.axisLeft(yScale); -d3.select('svg') - .append('g').attr('id', 'left-axis') - .call(leftAxis); -``` - -To create the bottom axis, we need to be able to map strings to points on a domain. We'll use a band scale for this, which just divides up the range into equal parts and maps it to an array of discrete values (values that can't be interpolated. e.g. strings): - -```javascript -var skillScale = d3.scaleBand(); -var skillDomain = data.map(function(skill){ - return skill.name -}); -skillScale.range([0, WIDTH]); -skillScale.domain(skillDomain); -``` - -Notice we use `data.map()`. This is regular javascript which simply loops through an array and modifies each element based on the given function. It then returns the resulting array, leaving the original array in tact. In the above example, `skillDomain` will be an array containing the various name properties of each of the data elements. - -Once we have an array of each of the skills, we use this as the domain and map each skill to a point within the range. Remember the point in the range is created by dividing up the full range equally based on the number of elements in the domain. - -Now that we have a scale which maps each skill text to a point in the x range, we can create the bottom axis as before. Add this code to the bottom of the AJAX callback: - -```javascript -var bottomAxis = d3.axisBottom(skillScale); -d3.select('svg') - .append('g').attr('id', 'bottom-axis') - .call(bottomAxis) - .attr('transform', 'translate(0,'+HEIGHT+')'); -``` - -We still need to stop the `` element from clipping the axes. Change the css for `svg` in `app.css`: - -```css -svg { - overflow: visible; -} -``` - -Our result: - -![](https://i.imgur.com/DroVw9c.png) - -The bottom axis text is all cluttered, though. Let's add some CSS to bottom of `app.css` to fix this: - -```css -#bottom-axis text { - transform:rotate(45deg); -} -``` - -![](https://i.imgur.com/y8Na794.png) - -It's rotated, but it's rotated around the center of the element. Let's change add a line to what we just wrote, so it rotates around the start of the text: - -```css -#bottom-axis text { - transform:rotate(45deg); - text-anchor: start; /* add this line */ -} -``` - -![](https://i.imgur.com/d6dkyDf.png) - -Let's move the graph to the right, so we can see the values for the left axis. Adjust our `svg` css code so it looks like this: - -```css -svg { - overflow: visible; - margin-left: 20px; /* add this line */ -} -``` - -![](https://i.imgur.com/USIPF0A.png) - -## Conclusion - -In this chapter we learned how to use AJAX to make an asynchronous request that will populate a bar graph. In the next chapter we'll create a pie chart that animates when you remove sections from it. diff --git a/D3.md b/D3.md deleted file mode 100644 index 4e4d50f..0000000 --- a/D3.md +++ /dev/null @@ -1,319 +0,0 @@ -# D3.js - -## Basics - -### Selection - -```javascript -d3.select('#some-id') //like document.querySelector() -d3.selectAll('.some-class') //like document.querySelectorAll() -d3.select('main').selectAll('span'); //can chain to select ancestors -``` - -### .style() - -```javascript -d3.select('div').style('color', 'orange'); //sets the style for an element -d3.select('div').style('color', 'orange').style('font-size': '20px'); //will return the selection for chaining -``` - -### .attr() - -```javascript -d3.select('div').attr('anExampleAttribute', 'someValue'); //adds/changes an attribute on an selection -``` - -### .classed() - -```javascript -d3.selectAll('.house').classed('house'); // returns true if all elements in selection contain the chosen class -d3.selectAll('div').classed('frog', true); //adds the class and returns the selection -d3.selectAll('div').classed('frog', false); //removes the class and returns the selection -``` - -### .append() - -```javascript -d3.selectAll('div').append('span'); //append html to a selection and return appended element -``` - -### .remove() - -```javascript -d3.selectAll('div').remove(); //remvoe selection -``` - -### .html() - -```javascript -d3.selectAll('div').html('hi'); //change the inner html of an element -``` - -### .text() - -```javascript -d3.selectAll('div').text('hi'); //set the content of the selection to the exact text (escapes html) -``` - -## AJAX - -Named based off of what kind of data they accept - -```javascript -d3.json('path').then(function(data){}); -d3.csv('path').then(function(data){}); -d3.tsv('path').then(function(data){}); -d3.xml('path').then(function(data){}); -d3.html('path').then(function(data){}); -d3.text('path').then(function(data){}); - -//make a post -d3.json('/runs', { - method:'POST', - headers:{ - 'Content-type': 'application/json; charset=UTF-8' - }, - body:JSON.stringify(runObject) -}).then(function(data){}); - -//send delete -d3.json('/runs/'+d.id, { - method:'DELETE', - headers:{ - 'Content-type': 'application/json; charset=UTF-8' - } -}).then(function(data){}); - -//send update -d3.json('/runs/'+d.id, { - method:'PUT', - headers:{ - 'Content-type': 'application/json; charset=UTF-8' - }, - body:JSON.stringify(runObject) -}).then(function(data){}); -``` - -## Data binding - -```javascript -d3.select('svg').selectAll('circle')//make a "ghost call" to all circles, even if there are none already. Make sure to select the svg, or appended circles will attach to html element - .data(dataArray) //joins each element in dataArray to an element in the selection - .enter() //returns the sub section of dataArray that has not been matched with DOM elements - .append('circle'); //creates a DOM element for each of the remaining dataArray elements -``` - -once data has been bound to elements, you can call something like: - -```javascript -d3.selectAll('circle').attr('r', function(d,i){ //d is data for the current element, i is the index of that element in the array - //callback will be executed for each DOM element - //return value is how each value will be set - return d.value * 2 //takes value property of d (data), multiplies it by two and sets the radius to that -}) -``` - -Can remove elements: - -```javascript -d3.selectAll('circle')//make a "ghost call" to all circles, even if there are none already - .data(dataArray) //joins each element in dataArray to an element in the selection - .exit() //returns the sub section of DOM elements that has not been matched with dataArray elements - .remove(); //removes those elements -``` - -To bind data to elements by something other than index: - -```javascript -.data(data, function(d){ - //match data based on d.id, not index - return d.id -}); -``` - -## Linear Scale - -A scale will map a data value to a visual value. - -1. Create a scale. There are many types. Here we'll use a linear scale - - ```javascript - var yScale = d3.scaleLinear(); - ``` - -1. Set up a visual range - - ```javascript - yScale.range([height,0]); - ``` - -1. Add the domain - - ```javascript - yScale.domain(yDomain); - ``` - -1. Can check range and domain after initialization - - ```javascript - yScale.range(); - yScale.domain(); - ``` - -1. Can now pass a data value into the scale to get a visual value - - ```javascript - yScale(361); //returns the visual value that maps to this data value - ``` - -1. Can go the opposite way - - ```javascript - yScale.invert(800); //returns the data value that maps to this visual value - ``` - -1. If data min/max of a data set (called the "domain") are not found, you can find them: - - ```javascript - var yMax = d3.max(data, function(element){ - return parseInt(element.TMAX); - }) - var yMin = d3.min(data, function(element){ - return parseInt(element.TMAX); - }) - - var yDomain = [yMin, yMax]; - ``` - - - Can combine this into one call if max/min come from same element: - - ```javascript - var yDomain = d3.extent(data, function(element){ - return parseInt(element.TMAX); - }); - ``` - -## Time Scale - -1. Create the scale - - ```javascript - var xScale = d3.scaleTime(); - ``` - -1. Set up the visual range - - ```javascript - xScale.range([0, width]); - ``` - -1. Set up the time range - - ```javascript - xScale.domain([new Date('2016-1-1'), new Date('2017-1-1')]); - ``` - -### Dealing with alternate date formats - -Date formatting options: https://github.com/d3/d3-time-format#locale_format - -To parse an alternate format into a date object - -```javascript -var parseTime = d3.timeParse("%Y%m%d"); -parseTime('20160101') //returns a date object -``` - -To create an alternately formated string from a date object - -```javascript -var formatTime = d3.timeFormat("%Y%m%d"); -formatTime(new Date()); //returns a string in the above format -``` - -## Axes - -```javascript -var leftAxis = d3.axisLeft(yScale); //create a left axis based on the yScale -d3.select('svg') - .append('g') //append a group element - .call(leftAxis); //apply the axis to it -``` - -Different types of axes: https://github.com/d3/d3-axis#axisTop - -## Events - -```javascript -select.on('mouseenter', function(data, index){ - d3.select(this); //select just element that was hovered - console.log(d3.event); //the event object -}) -``` - -click, mouseenter and mouseleave are common - -use `d3.event.stopPropagation();` when events conflict - -## Behaviors - -### Dragging - -```javascript -//create the behavior -var drag = d3.drag() - .on('start', dragStart) - .on('drag', drag) - .on('end', dragEnd); -//... -//apply it to a selection -d3.selectAll('circle').call(drag); -//.... -//define callbacks -function dragStart(d){ //d is the data for the dragged object - d3.select(this); //the visual object - d3.event.x; //x position of cursor - d3.event.y; //y position of cursor -} -``` - -You can use the xScale.invert and yScale.invert to get data from d3.event.x and d3.event.y - -### Zooming - -```javascript -//previously defined: var xAxis = d3.axisBottom(xScale); -//previously defined: var yAxis = d3.axisLeft(yScale); -//previously defined: d3.select('svg').append('g').attr('id', 'x-axis').attr('transform', 'translate(0,' + HEIGHT + ')').call(xAxis); -//previously defined: d3.select('svg').append('g').attr('id', 'y-axis').call(yAxis); //y axis is good as it is -var zoomCallback = function(){ - lastTransform = d3.event.transform; //save the transform for later inversion with clicks - d3.select('#points').attr("transform", d3.event.transform); //apply transform to g element containing circles - //recalculate the axes - d3.select('#x-axis').call(xAxis.scale(d3.event.transform.rescaleX(xScale))); - d3.select('#y-axis').call(yAxis.scale(d3.event.transform.rescaleY(yScale))); -} -var zoom = d3.zoom().on('zoom', zoomCallback); -d3.select('svg').call(zoom); -``` - -If you need to recalculate new mouse position after transform, use the last saved event transform's invert methods - -```javascript -var lastTransform = null; -d3.select('svg').on('click', function(d){ - - //d3.event contains data for click event - var x = d3.event.offsetX; //use offset to get point within svg container - var y = d3.event.offsetY; - - if(lastTransform !== null){ - x = lastTransform.invertX(d3.event.offsetX); //use offset to get point within svg container - y = lastTransform.invertY(d3.event.offsetY); - } - //... -``` - -## Basic Layouts -- https://github.com/d3/d3/wiki/Plugins -- http://c3js.org/ diff --git a/FORCE_DIRECTED_GRAPH.md b/FORCE_DIRECTED_GRAPH.md deleted file mode 100644 index 42c74d9..0000000 --- a/FORCE_DIRECTED_GRAPH.md +++ /dev/null @@ -1,285 +0,0 @@ -# Force Directed Graphs - -This lesson covers how to make a force directed graph which will visualize relationships between various nodes. In it we will learn about the following: - -- Creating a physics based force that will center nodes -- Creating a physics based force that make the nodes repel each other -- Creating a physics based force that will link the nodes to show their relationship - -## Describe a Force Directed Graph - -A force directed graph is a graph that is affected by various forces (e.g. gravity, repulsion, etc). It can be extremely useful when setting up graphs of relationships - -## Describe how to set up a graph of relationships - -### Display - -- We're going to have a list of nodes representing people and display them as circles -- We're going to have a list of links representing connections between people and display them as lines - -### Physics - -- We're going to have a gravitational force at the center of the `svg` that draws all nodes towards it -- We're going to have repulsive forces on each node so that they don't get too close each other -- We're going to have link forces that connect each of the nodes so that they don't repel each other too much - -## Set up the HTML - -Pretty standard index.html file, but we'll need two `` elements: - -- One to contain the nodes (people - circles) -- One to contain the links (relationships - lines) - -```html - - - - - - - - - - - - - - - -``` - -## Set up styling for nodes and links - -Create an `app.css` file for our circles (nodes/people) and lines (links/relationships) - -```css -circle { - fill: red; - r: 5; -} - -line { - stroke: grey; - stroke-width: 1; -} -``` - -Don't forget to link to it in your index.html file! - -```html - - - - -``` - -## Set up svg - -At the top of our `app.js` file, add the following: - -```javascript -var WIDTH = 300; -var HEIGHT = 200; - -d3.select("svg") - .attr("width", WIDTH) - .attr("height", HEIGHT); -``` - -If we open up `index.html` in Chrome and look at Elements in the dev tools, we should see this: - -![](https://i.imgur.com/s6worhU.png) - -## Add data for people - -Let's create an array of people objects at the bottom of `app.js`: - -```javascript -var nodesData = [ - {"name": "Charlie"}, - {"name": "Mac"}, - {"name": "Dennis"}, - {"name": "Dee"}, - {"name": "Frank"}, - {"name": "Cricket"} -]; -``` - -## Add data for relationships - -Now let's create the relationships by adding the following array to the bottom of `app.js`. **NOTE** that the attributes must be `source` and `target` in order for D3 to do its magic - -```javascript -var linksData = [ - {"source": "Charlie", "target": "Mac"}, - {"source": "Dennis", "target": "Mac"}, - {"source": "Dennis", "target": "Dee"}, - {"source": "Dee", "target": "Mac"}, - {"source": "Dee", "target": "Frank"}, - {"source": "Cricket", "target": "Dee"} -]; -``` - -## Add circles to the svg - -Add the following to the bottom of `app.js`: - -```javascript -var nodes = d3.select("#nodes") - .selectAll("circle") - .data(nodesData) - .enter() - .append("circle"); -``` - -This will create circles for each element in our `nodesData` array. Our dev tools should look like this: - -![](https://i.imgur.com/TO2ogs5.png) - -## Add lines to the svg - -Add the following to the bottom of `app.js`: - -```javascript -var links = d3.select("#links") - .selectAll("line") - .data(linksData) - .enter() - .append("line"); -``` - -This will create lines for each element in our `linksData` array. Our dev tools should look like this: - -![](https://i.imgur.com/MpIl6Z4.png) - -## Create simulation - -Now we'll generate a simulation by adding the following to the bottom of `app.js`: - -```javascript -d3.forceSimulation() -``` - -Note that this simply creates a simulation, but doesn't specify how the simulation should run. Let's tell it what data to act on by modifying the previous line of code: - -```javascript -d3.forceSimulation() - .nodes(nodesData) // add this line -``` - -## Specify how the simulation affects the visual elements - -At this point, our visualization still looks the same as before. - -![](https://i.imgur.com/MpIl6Z4.png) - -Let's have our simulation affect the circles/lines that we created - -- The simulation runs "ticks" which run very quickly -- Each time a new "tick" occurs, you can update the visual elements. This allows our simulation to animate -- D3 will calculate and tack on positional data to our regular data so that we can make use of it - -Add the following to the bottom of `app.js`: - -```javascript -d3.forceSimulation() - .nodes(nodesData) - .on("tick", function(){ - nodes.attr("cx", function(datum) { return datum.x; }) - .attr("cy", function(datum) { return datum.y; }); - - links.attr("x1", function(datum) { return datum.source.x; }) - .attr("y1", function(datum) { return datum.source.y; }) - .attr("x2", function(datum) { return datum.target.x; }) - .attr("y2", function(datum) { return datum.target.y; }); - }); -``` - -Now our circles distance themselves from each other a little bit, but this is just a side effect of not having any forces attached to them. We'll add forces next. - -![](https://i.imgur.com/jwfpTp9.png) - -## Create forces - -Let's create a centering force at the center of the screen that pulls all elements towards it. Adjust the code we added in the previous step so it looks like below. **NOTE**, we only add `.force("center_force", d3.forceCenter(WIDTH / 2, HEIGHT / 2))` to the previous code: - -```javascript -d3.forceSimulation() - .nodes(nodesData) - .force("center_force", d3.forceCenter(WIDTH / 2, HEIGHT / 2)) // add this line - .on("tick", function(){ - nodes.attr("cx", function(datum) { return datum.x; }) - .attr("cy", function(datum) { return datum.y; }); - - links.attr("x1", function(datum) { return datum.source.x; }) - .attr("y1", function(datum) { return datum.source.y; }) - .attr("x2", function(datum) { return datum.target.x; }) - .attr("y2", function(datum) { return datum.target.y; }); - }); -``` - -Now our circles are pulled towards the center of the SVG element: - -![](https://i.imgur.com/ggGNctB.png) - -Create a force on each of the nodes so that they repel each other. Just like in the last step, we only add `.force("charge_force", d3.forceManyBody())` to the previous code: - -```javascript -d3.forceSimulation() - .nodes(nodesData) - .force("center_force", d3.forceCenter(WIDTH / 2, HEIGHT / 2)) - .force("charge_force", d3.forceManyBody()) //add this line - .on("tick", function(){ - nodes.attr("cx", function(datum) { return datum.x; }) - .attr("cy", function(datum) { return datum.y; }); - - links.attr("x1", function(datum) { return datum.source.x; }) - .attr("y1", function(datum) { return datum.source.y; }) - .attr("x2", function(datum) { return datum.target.x; }) - .attr("y2", function(datum) { return datum.target.y; }); - }); -``` - -You'll notice that the cx/cy values for the circles change rapidly initially before finally stopping. This is because D3 is running a simulation. The center_force is trying to reach a state of equilibrium with the charge_force. You'll even notice when you first load the page that the circles move outward from the center. This is due to the same reason. - -![](https://i.imgur.com/C37zzPO.png) - -Lastly, we'll create the links between the nodes so that they don't repel each other too much. Just like in the last step, we only add the following code to what we previously had: - -```javascript -.force("links", d3.forceLink(linksData).id(function(datum){ - return datum.name -}).distance(160)) -``` - -Our last chunk of code should now look like this: - -```javascript -d3.forceSimulation() - .nodes(nodesData) - .force("center_force", d3.forceCenter(WIDTH / 2, HEIGHT / 2)) - .force("charge_force", d3.forceManyBody()) - .force("links", d3.forceLink(linksData).id(function(datum){ //add this - return datum.name //add this - }).distance(160)) //add this - .on("tick", function(){ - nodes.attr("cx", function(datum) { return datum.x; }) - .attr("cy", function(datum) { return datum.y; }); - - links.attr("x1", function(datum) { return datum.source.x; }) - .attr("y1", function(datum) { return datum.source.y; }) - .attr("x2", function(datum) { return datum.target.x; }) - .attr("y2", function(datum) { return datum.target.y; }); - }); -``` - -- The `d3.forceLink` function takes the array of links. It then uses the `source` and `target` attributes of each link data object to connect the nodes via their `.name` properties (as specified in the return value of the function we just wrote) -- You can tack on `.distance()` to specify how long the links are visually between each node - -Finally, our graph looks like this: - -![](https://i.imgur.com/1w8Po1b.png) - -## Conclusion - -In this chapter we used D3 to create a graph that visualizes relationships between various nodes of data. This can be very useful in situations like graphing a friend network, showing parent/child company relationships, or displaying a company's staff hierarchy. In the next chapter we'll cover how to create a map from GeoJSON data. diff --git a/INTRO.md b/INTRO.md deleted file mode 100644 index 4de491d..0000000 --- a/INTRO.md +++ /dev/null @@ -1,54 +0,0 @@ -# Introduction - -The era of big data is upon us! Advances in hardware have made it possible for computers to store, analyze, and transmit massive amounts of information in a way that was previously impossible. Data Science has become one of the most in-demand fields in the United States, companies are constantly coming up with new techniques to analyze customer information, and it seems like every day there are new ways to visualize all this data. D3 has become the most popular library used to create dynamic, interactive, data-driven visualizations on the web. Unlike many technologies previously used in data-viz, D3 leverages the power of combining SVG images with web browsers and JavaScript. In this chapter we'll discuss: - -1. What is SVG? -1. What Makes D3 So Special? -1. This Book's Approach to Learning - -## What is SVG? - -One of the best ways to present your data is via an interactive graphic on the web. The advantage of this approach is that its interactivity allows creators to pack more information into a single visualization, while the ubiquity of the web allows anyone to instantly access it. Gone are the days of power point presentations, or worse yet, printing static images onto paper for handout. There are many ways to create a web-based interactive data visualization, but none is more popular than a JavaScript library called D3.js. - -To understand why D3.js works so well, it's important to understand what SVG is and how it relates to D3. SVG stands for Scalable Vector Graphics, and it's a way to display shapes using mathematical directions/commands. Traditionally, the information for an image is stored in a grid, also called a `raster`. Each square (called a pixel) of the image has a specific color: - -![](https://upload.wikimedia.org/wikipedia/commons/5/54/Raster_graphic_fish_20x23squares_sdtv-example.jpg) - -But with SVG, a set of succinct drawing directions are stored. For example, the drawing command for a circle is: - -```html - -``` - -This code produces a much smaller file size, and because it's a set of drawing directions, the image can enlarged without any pixelation. A raster image becomes blurry and pixelated as it's enlarged. The advantage of raster graphics over vector graphics is that they're great for storing complex images like photographs. In a situation like a photograph, where each pixel probably has a different color, it's better to use a raster image. Imagine writing SVG drawing commands for a photograph: you would end up creating a new element for each pixel, and the file size would be too large. - -Once an SVG drawing command is written, a program needs to interpret command and display the image. Up until somewhat recently, only designated drawing applications like Adobe Illustrator could view and manipulate these images. But by 2011 all major modern browsers supported SVG tags, allowing for developers to embed SVG directly on a web page. Since the SVG image was directly embedded in the code of a web page, JavaScript -- which normally is used for manipulating HTML -- could be used to manipulate the shape, size, and colors of the image in response to user events. To make the circle in the SVG example above grow to twice its original size, all that JavaScript had to do was change the `r` attribute: - -```html - -``` - -This was the massive breakthrough that allowed complex interactive data visualizations to be hosted on the web. - -## What Makes D3 So Special? - -D3.js came in at this point because writing the code to make complex Data Driven Documents (how D3 got its name) that linked SVG images with the big data that had become available on the internet was a difficult task. It rose to prominence during the Obama/Romney presidential debates as the New York times publishes a series of amazing visualizations. Check out some examples here: - -- https://archive.nytimes.com/www.nytimes.com/interactive/2012/11/07/us/politics/obamas-diverse-base-of-support.html -- http://archive.nytimes.com/www.nytimes.com/interactive/2012/11/02/us/politics/paths-to-the-white-house.html -- https://archive.nytimes.com/www.nytimes.com/interactive/2012/10/15/us/politics/swing-history.html -- https://www.nytimes.com/elections/2012/electoral-map.html -- https://archive.nytimes.com/www.nytimes.com/interactive/2012/09/06/us/politics/convention-word-counts.html -- https://archive.nytimes.com/www.nytimes.com/interactive/2012/03/07/us/politics/how-candidates-fared-with-different-demographic-groups.html - -D3 simplifies some of the most common, as well as some of the most complex tasks that a developer can run into when creating browser-based visualizations. At it's core, D3 easily maps SVG image properties to data values. As the data values change, due to user interactions, so do the images. - -## This Book's Approach to Learning - -D3 is a massive library, full of millions of options, but its core concepts are easy to learn. One does not need to know every detail of the library in order to become a functional D3 developer. Instead, this book attempts to teach the most fundamental aspects of D3, so that the reader can get job-ready quickly. It does so by stepping the user through a series of the most common graphs that a developer will be asked to make: a scatter plot, a bar graph, a pie chart, a force directed graph, and a map. The goal is not only to teach the basics but also give the reader a final set of builds that are fun to work towards as well as useful to draw from as their career continues. - -Please note, the code demonstrated here was created to be easy to understand from an educational standpoint. It is not meant to be code that is ready for production. Nor does it employ ES6 or ES7 syntax. Often times demonstrating a concept in code that is production-ready or written in ES6/ES7 can hinder the educational experience. It is assumed that the reader is comfortable enough with the core concepts of programming that they can refine the code on their own, once they are comfortable with the fundamentals of D3. - -## Conclusion - -In this chapter you've received a high-level overview of what makes D3 so interesting. In the next section, we'll dive into create SVG elements diff --git a/Lab.md b/Lab.md deleted file mode 100644 index 0e18271..0000000 --- a/Lab.md +++ /dev/null @@ -1,146 +0,0 @@ -Create a bar graph using the following data: - -```javascript -[ - { - "name": "HTML", - "count": 21 - }, - { - "name": "CSS", - "count": 17 - }, - { - "name": "Responsive Web Design", - "count": 17 - }, - { - "name": "JavaScript", - "count": 17 - }, - { - "name": "Git", - "count": 16 - }, - { - "name": "Angular.js", - "count": 9 - }, - { - "name": "Node.js", - "count": 9 - }, - { - "name": "PostgreSQL", - "count": 8 - }, - { - "name": "Agile Project Management", - "count": 8 - }, - { - "name": "MongoDB", - "count": 7 - }, - { - "name": "Trello", - "count": 7 - }, - { - "name": "Testing / TDD", - "count": 7 - }, - { - "name": "jQuery", - "count": 7 - }, - { - "name": "User Testing", - "count": 6 - }, - { - "name": "MySQL", - "count": 6 - }, - { - "name": "PHP", - "count": 6 - }, - { - "name": "React.js", - "count": 6 - }, - { - "name": "AJAX", - "count": 6 - }, - { - "name": "Express.js", - "count": 5 - }, - { - "name": "Heroku", - "count": 5 - }, - { - "name": "Wireframing", - "count": 5 - }, - { - "name": "Sass/SCSS", - "count": 5 - }, - { - "name": "Mobile Web", - "count": 4 - }, - { - "name": "Rails", - "count": 4 - }, - { - "name": "WordPress", - "count": 4 - }, - { - "name": "Drupal", - "count": 3 - }, - { - "name": "Ruby", - "count": 3 - }, - { - "name": "Ember.js", - "count": 3 - }, - { - "name": "Python", - "count": 3 - }, - { - "name": "Amazon EC2", - "count": 2 - }, - { - "name": "Computer Science degree", - "count": 1 - }, - { - "name": "Backbone.js", - "count": 1 - }, - { - "name": "Less", - "count": 1 - }, - { - "name": "Prototyping", - "count": 1 - }, - { - "name": "Redis", - "count": 1 - } -] -```` diff --git a/MAPS.md b/MAPS.md deleted file mode 100644 index cafc5cc..0000000 --- a/MAPS.md +++ /dev/null @@ -1,220 +0,0 @@ -# Creating a map - -The topics that we will cover in this chapter include: - -1. Creating a map -1. Define GeoJSON -1. Use a projection -1. Generate a `` using a projection and the GeoJSON data - -In this section we'll generate `` elements from GeoJSON data that will draw a map of the world - -## Define GeoJSON - -GeoJSON is just JSON data that has specific properties that are assigned specific data types. Here's an example: - -```javascript -{ - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [125.6, 10.1] - }, - "properties": { - "name": "Dinagat Islands" - } -} -``` - -In this example, we have one `Feature` who's geometry is a `Point` with the coordinates `[125.6, 10.1]`. It has "Dinagat Islands" as its name. Each `Feature` follows this general structure: - -```javascript -{ - "type": STRING, - "geometry": { - "type": STRING, - "coordinates": ARRAY - }, - "properties": OBJECT -} -``` - -We can also have a `Feature Collection` which is many `Features` grouped together in a `features` array: - -```javascript -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [102.0, 0.5] - }, - "properties": { - "prop0": "value0" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0] - ] - }, - "properties": { - "prop0": "value0", - "prop1": 0.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], - [100.0, 1.0], [100.0, 0.0] - ] - ] - }, - "properties": { - "prop0": "value0", - "prop1": { "this": "that" } - } - } - ] -} -``` - -This basically follows the form: - -```javascript -{ - "type": "FeatureCollection", - "features": ARRAY -} -``` - -The `features` property is an array of `feature` objects which we've defined previously. - -### Set up the HTML - -Let's set up a basic D3 page: - -```html - - - - - - - - - - - - - -``` - -The only thing different from the setup that we've used in previous chapters is this line: - -```html - -``` - -This just loads an external javascript file which sets our GeoJSON data to a variable. Here's what the beginning of it looks like: - -```javascript -var map_json = { - type: "FeatureCollection", - features: [ - { - type: "Feature", - id: "AFG", - properties: { - name: "Afghanistan" - }, - geometry: { - type: "Polygon", - coordinates: [ - //lots of coordinates - ] - } - } - // lots of other countries - ] -} -``` - -Note that the `map_json` variable is just a JavaScript object that adheres to the GeoJSON structure (it adds an `id` property which is optional). This is very important. If the object didn't adhere to the GeoJSON structure, D3 would not work as it should. - -In production, you would probably make an AJAX call to get this data, or at the very least, create your own geoJSON file similar to the one being hosted on rawgit.com. The setup above was created to make learning easier by decreasing the complexity associated with AJAX. - -## Use a projection - -Now let's start our `app.js` file: - -```javascript -var width = 960; -var height = 490; - -d3.select('svg') - .attr('width', width) - .attr('height', height); -``` - -At the bottom of `app.js` let's add: - -```javascript -var worldProjection = d3.geoEquirectangular(); -``` - -This generates a projection, which governs how we're going to display a round world on a flat screen. There's lots of different types of projections we can use: https://github.com/d3/d3-geo/blob/master/README.md#azimuthal-projections - -The line above tells D3 to create an equirectangular projection (https://github.com/d3/d3-geo/blob/master/README.md#geoEquirectangular) - -## Generate a `` using a projection and the GeoJSON data - -Now that we have our projection, we're going to generate `` elements for each data element in the `map_json.features` array. Then we set the fill of each element to `#099`. Add this at the end of app.js: - -```javascript -d3.select('svg').selectAll('path') - .data(map_json.features) - .enter() - .append('path') - .attr('fill', '#099'); -``` - -Here's what it should look like at the moment if we open index.html in Chrome and view the elements tab in the developer tools: - -![](https://i.imgur.com/ljSlk4s.png) - -We created the `path` elements, but they each need a `d` attribute which will determine how they're going to drawn (i.e. their shape). - -We want something like: - -```javascript -d3.selectAll('path').attr('d', function(datum, index){ - //somehow use datum to generate the value for the 'd' attributes -}); -``` - -Writing the kind of code described in the comment above would be very difficult. Luckily, D3 can generate that entire function for us. All we need to do is specify the projection that we created earlier. At the bottom of `app.js` add the following: - -```javascript -var dAttributeFunction = d3.geoPath() - .projection(worldProjection); - -d3.selectAll('path').attr('d', dAttributeFunction); -``` - -`geoPath()` generates the function that we'll use for the `d` attribute, and `projection(worldProjection)` tells it to use the `worldProjection` var created earlier so that the `path` elements appear as an equirectangular projection like this (This is helpful because we can use different projects to view a round world on a flat screen in different ways): - -![](https://i.imgur.com/hX7hOoB.png) - -## Conclusion - -In this section we've covered how to use D3 to create a projection and render GeoJSON data as a map, and we've learned about using different projects to visualize the world. This can be helpful when displaying populations or perhaps average rainfall of various regions. Congratulations! You've made it to the end of this book. Now go off and create amazing visualizations. diff --git a/PIE.md b/PIE.md deleted file mode 100644 index b184d5a..0000000 --- a/PIE.md +++ /dev/null @@ -1,440 +0,0 @@ -# Creating a Pie Chart - -In this section we'll be using animations to make our graphs move. This can give your visualizations a more polished and professional feel. By the end of this section, you'll be able to: - -1. Create an ordinal scale -1. Create a color scale -1. Add paths for each pie segment -1. Generate an arc creating function -1. Format the data for the arc -1. Adjust the position of the pie -1. Make a donut graph -1. Remove parts of the pie - -## Set Up - -As always, we'll need an `index.html` file: - -```html - - - - - - - - - - - - - - -``` - -## Set Config Vars - -At the bottom of the `` tag, we're referencing an `app.js` file. Let's create that file, and add the following to it: - -```javascript -var WIDTH = 360; -var HEIGHT = 360; -var radius = Math.min(WIDTH, HEIGHT) / 2; - -var dataset = [ - { label: 'Bob', count: 10 }, - { label: 'Sally', count: 20 }, - { label: 'Matt', count: 30 }, - { label: 'Jane', count: 40 } -]; -console.log(dataset); -``` - -To be sure it's working and linked up properly, we've added the `console.log(dataset)` at the bottom. Let's open up `index.html` and view the developer console to make sure everything is hooked up the way it should be: - -![](https://i.imgur.com/Oy0fiTl.png) - -Once we're sure, it's working, remove the `console.log(dataset);`: - -```javascript -var WIDTH = 360; -var HEIGHT = 360; -var radius = Math.min(WIDTH, HEIGHT) / 2; - -var dataset = [ - { label: 'Bob', count: 10 }, - { label: 'Sally', count: 20 }, - { label: 'Matt', count: 30 }, - { label: 'Jane', count: 40 } -]; -``` - -## Create an Ordinal Scale - -An ordinal scale maps a discrete value to some other value. A discrete value is something can't be divided. In the past, we've used values like numbers that can be divided up and interpolated. Interpolated just means that for any two numbers, we can find other numbers in between them. For instance, given 10 and 5, we can find values between them (e.g. 6, 8.2, 7, 9.9, etc). Now we want to map values that can't be interpolated, the `label` properties in our dataset (`Bob`, `Sally`, `Matt`, `Jane`). What values lie between `Bob` and `Sally`? How about between `Bob` and `Matt`? There are none. These are just strings, not numerical values that can be divided up and interpolated. - -What we want to do, is map these discrete values to other values. Here's an example of how to do this with an ordinal scale. Add the following at the bottom of `app.js`: - -```javascript -var mapper = d3.scaleOrdinal(); -mapper.range([45, 63, 400]); //list each value for ordinal scales, not just min/max -mapper.domain(['Bob', 'Sally', 'Zagthor']); //list each value for ordinal scales, not just min/max - -console.log(mapper('Bob')); -console.log(mapper('Sally')); -console.log(mapper('Zagthor')); -``` - -The previous code should produce the following: - -![](https://i.imgur.com/0WHlYsx.png) - -**NOTE** When working with ordinal scales, you'll need to list all values for both domain and range. Even if one set is numerical (in the previous case, the range), you'll still need to list each value. If we had just listed the min/max for the range, omitting `63`, D3 would have no idea what value to map `Sally` to. After all, how close is `Sally` to `Bob` as a value? How close is `Sally` to `Zagthor` as a value? There's no way to calculate that distance, since they're all strings of text, not numbers. - -One thing that's surprising, is that you can't invert ordinal scales. Remove the previous three `console.log()` statements and temporarily add the following to the bottom of app.js: - -```javascript -console.log(mapper.invert(45)); -``` - -![](https://i.imgur.com/Bvugomc.png) - -D3 can only go in one direction: from domain to range. You can now remove that `console.log()` statement. - -## Create the color scale to map labels to colors - -Now we want to map the `label` properties of our data set to colors, instead of random numbers like in the previous section. We can come up with our own color scheme, or choose one of D3's sets of colors: - -- https://github.com/d3/d3-scale-chromatic#categorical - -If we want to, we can see that these color schemes are just arrays: - -```javascript -console.log(d3.schemeCategory10) -``` - -![](https://i.imgur.com/SstV7Wl.png) - -Consequently, we can use them when setting a range. Replace the previous `console.log()` statement with the following: - -```javascript -var colorScale = d3.scaleOrdinal(); -colorScale.range(d3.schemeCategory10); -``` - -We can generate an array of labels for the domain using JavaScript's native map function. Add the following to the bottom of `app.js`: - -```javascript -colorScale.domain(dataset.map(function(element){ - return element.label; -})); -``` - -Here's our code so far: - -```javascript -var WIDTH = 360; -var HEIGHT = 360; -var radius = Math.min(WIDTH, HEIGHT) / 2; - -var dataset = [ - { label: 'Bob', count: 10 }, - { label: 'Sally', count: 20 }, - { label: 'Matt', count: 30 }, - { label: 'Jane', count: 40 } -]; - -var colorScale = d3.scaleOrdinal(); -colorScale.range(d3.schemeCategory10); -colorScale.domain(dataset.map(function(element){ - return element.label; -})); -``` - -## Set up the SVG - -This is pretty standard. Add it to the bottom of `app.js`: - -```javascript -d3.select('svg') - .attr('width', WIDTH) - .attr('height', HEIGHT); -``` - -## Add paths for each pie segment - -Let's add `path` elements for each element in our dataset. Add the following to the bottom of `app.js`: - -```javascript -var path = d3.select('g').selectAll('path') - .data(dataset) - .enter() - .append('path') - .attr('fill', function(d) { - return colorScale(d.label); - }); -``` - -If we examine our elements in the developer tools, we'll see the paths were added, and each path has a fill value, as determined by `colorScale(d.label)`, which is mapping the label of each data object to a color: - -![](https://i.imgur.com/K9SSrf5.png) - -## Generate an arc creating function - -The paths have fill colors, but no shape. If you'll recall, `` elements take a `d=` attribute which determines how they're drawn. We want to set something up like this which will somehow map datum to a `d=` string (you don't have to add the next code snippet, it's only there for reference): - -```javascript -.attr('d', function(datum){ - //return path string here -}) -``` - -Fortunately, D3 can generate the anonymous function that we need for the second parameter of `.attr()` in the previous code snippet. Add the following to `app.js` just above our previous code for `var path = d3.select('g').selectAll('path')...`: - -```javascript -var arc = d3.arc() - .innerRadius(0) //to make this a donut graph, adjust this value - .outerRadius(radius); -``` - -Let's plug this function into its correct place in our previous `var path = d3.select('g').selectAll('path')...` code (it won't work yet, though): - -```javascript -var path = d3.select('g').selectAll('path') - .data(dataset) - .enter() - .append('path') - .attr('d', arc) //add this - .attr('fill', function(d) { - return colorScale(d.label); - }); -``` - -## Format the data for the arc - -The reason that our `arc()` function won't work is that the data isn't formatted properly for the function. The arc function we generated expects the data object to have things like start angle, end angle, etc. Fortunately, D3 can reformat our data so that it will work with our generated `arc()` function. To do this, we'll generate a `pie` function which will take a data set and add the necessary attributes to it for start angle, end angle, etc. Add the following just above our code for `var path = d3.select('g').selectAll('path')...` : - -```javascript -var pie = d3.pie() - .value(function(d) { return d.count; }) //use the 'count' property each value in the original array to determine how big the piece of pie should be - .sort(null); //don't sort the values -``` - -our `pie` variable is a function that takes an array of values as a parameter and returns an array of objects that are formatted for our `arc` function. Temporarily add the following code to the bottom of `app.js` and take a look at the console in Chrome's dev tools: - -```javascript -console.log(pie(dataset)); -``` - -![](https://i.imgur.com/eLkzxCA.png) - -You can remove the `console.log(pie(dataset))` call now. We can use this `pie()` function when attaching data to our paths. Adjust our previous `var path = d3.select('g').selectAll('path')` code: - -```javascript -var path = d3.select('g').selectAll('path') - .data(pie(dataset)) //adjust this line to reformat data for arc - .enter() - .append('path') - .attr('d', arc) - .attr('fill', function(d) { - return colorScale(d.label); - }); -``` - -Unfortunately, now each object from the data array that's been attached to our path elements doesn't have a `.label` property, so our code for `.attr('fill', function(d) {})` is broken. Fortunately, our data does have a `.data` attribute that mirrors what the data looked like before we passed it to the `pie()` function. Let's adjust our `var path = d3.select('g').selectAll('path')` code to use that instead: - -```javascript -var path = d3.select('g').selectAll('path') - .data(pie(dataset)) - .enter() - .append('path') - .attr('d', arc) - .attr('fill', function(d) { - return colorScale(d.data.label); //use .data property to access original data - }); -``` - -Our code so far: - -```javascript -var WIDTH = 360; -var HEIGHT = 360; -var radius = Math.min(WIDTH, HEIGHT) / 2; - -var dataset = [ - { label: 'Bob', count: 10 }, - { label: 'Sally', count: 20 }, - { label: 'Matt', count: 30 }, - { label: 'Jane', count: 40 } -]; - -var mapper = d3.scaleOrdinal(); -var colorScale = d3.scaleOrdinal(); -colorScale.range(d3.schemeCategory10); -colorScale.domain(dataset.map(function(element){ - return element.label; -})); - -d3.select('svg') - .attr('width', WIDTH) - .attr('height', HEIGHT); - -var arc = d3.arc() - .innerRadius(0) //to make this a donut graph, adjust this value - .outerRadius(radius); - -var pie = d3.pie() - .value(function(d) { return d.count; }) //use the 'count' property each value in the original array to determine how big the piece of pie should be - .sort(null); //don't sort the values - -var path = d3.select('g').selectAll('path') - .data(pie(dataset)) - .enter() - .append('path') - .attr('d', arc) - .attr('fill', function(d) { - return colorScale(d.data.label); //use .data property to access original data - }); -``` - -Produces this: - -![](https://i.imgur.com/lNGj6Hg.png) - -## Adjust the position of the pie - -Currently, we only see the lower right quarter of the pie graph. This is because the pie starts at (0,0), but we can move the `group` element containing the pie by adjusting our `d3.select('svg')` code: - -```javascript -d3.select('svg') - .attr('width', WIDTH) - .attr('height', HEIGHT); -var container = d3.select('g') //add this line and the next: - .attr('transform', 'translate(' + (WIDTH / 2) + ',' + (HEIGHT / 2) + ')'); //add this line -``` - -Now it looks like this: - -![](https://i.imgur.com/kxm6VRA.png) - -## Make a donut graph - -If you want the pie to have a hole at the center, just adjust the inner radius of the `arc()` function: - -```javascript -var arc = d3.arc() - .innerRadius(100) //to make this a donut graph, adjust this value - .outerRadius(radius); -``` - -Now we get this: - -![](https://i.imgur.com/f5eIwY0.png) - -## Remove parts of the pie - -We want to make it possible to click on a section of the pie, and it will be removed. First let's add ids to our data to make removing easie. Adjust the `var dataset` code at the top of `app.js`: - -```javascript -var dataset = [ - { id: 1, label: 'Bob', count: 10 }, //add id property - { id: 2, label: 'Sally', count: 20 }, //add id property - { id: 3, label: 'Matt', count: 30 }, //add id property - { id: 4, label: 'Jane', count: 40 } //add id property -]; -``` - -Now let's use those ids when we map data to paths. Adjust the `.data()` portion of our `var path = d3.select('g').selectAll('path')` code at the bottom of `app.js`: - -```javascript -var path = d3.select('g').selectAll('path') - .data(pie(dataset), function(datum){ //attach datum.data.id to each element - return datum.data.id - }) -``` - -Let's save a record of what the current data is for each element by adding a `_current` property to each element (we'll use this later). Add `.each(function(d) { this._current = d; });` to the end of our `var path = d3.select('g')` code at the bottom of `app.js` - -```javascript -var path = d3.select('g').selectAll('path') - .data(pie(dataset), function(datum){ - return datum.data.id - }) - .enter() - .append('path') - .attr('d', arc) - .attr('fill', function(d) { - return colorScale(d.data.label); - })//watch out! remove the semicolon here - .each(function(d) { this._current = d; }); //add this -``` - -Create the click handler by adding the following code to the bottom of `app.js`: - -```javascript -path.on('click', function(clickedDatum, clickedIndex){ -}); -``` - -Remove the selected data from the dataset array, using JavaScript's native filter function. Adjust the code we just added: - - -```javascript -path.on('click', function(clickedDatum, clickedIndex){ - dataset = dataset.filter(function(currentDatum, currentIndex){ //new - return clickedDatum.data.id !== currentDatum.id //new - }); //new -}); -``` - -Remove the `path` elements from the svg by adding the following to our click handler function: - -```javascript -path.on('click', function(clickedDatum, clickedIndex){ - dataset = dataset.filter(function(currentDatum, currentIndex){ - return clickedDatum.data.id !== currentDatum.id - }); - path //new - .data(pie(dataset), function(datum){ //new - return datum.data.id //new - }) //new - .exit().remove(); //new -}); -``` - -Now, if we click on the orange segment, we should get this: - -![](https://i.imgur.com/iuLEraU.png) - -Let's close the donut and add a transition. Add the following at the bottom of our click handler. Check out the comments in the code below to see what each line does: - -```javascript -path.on('click', function(clickedDatum, clickedIndex){ - dataset = dataset.filter(function(currentDatum, currentIndex){ - return clickedDatum.data.id !== currentDatum.id - }); - path - .data(pie(dataset), function(datum){ - return datum.data.id - }) - .exit().remove(); - - path.transition() //create the transition - .duration(750) //add how long the transition takes - .attrTween('d', function(d) { //tween the d attribute - var interpolate = d3.interpolate(this._current, d); //interpolate from what the d attribute was and what it is now - this._current = interpolate(0); //save new value of data - return function(t) { //re-run the arc function: - return arc(interpolate(t)); - }; - }); -}); -``` - -Now, when we click the orange segment, the donut closes smoothly: - -![](https://i.imgur.com/gh8lnEN.png) - -## Conclusion - -In this chapter we created a pie chart that animates when you remove sections from it. We've learned how to generate paths from data so that we get different parts of the pie without having to specify the drawing commands directly in the path elements. In the next chapter we will use D3 to create a graph that visualizes relationships between various nodes of data. diff --git a/README.md b/README.md deleted file mode 100644 index dc2ae41..0000000 --- a/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# D3 Notes - -## Lessons - -1. [Intro](/INTRO.md) -1. [SVG](/SVG.md) -1. [D3](/D3.md) -1. [Scatter Plot](/SCATTER_PLOT.md) -1. [Bar Graph](/BAR.md) -1. [Pie Chart](/PIE.md) -1. [Force Directed Graphs](/FORCE_DIRECTED_GRAPH.md) -1. [Mapping](/MAPS.md) - -## Labs - -1. [Lab](/Lab.md) - -## Completed Code - -1. [SVG](/examples/svg) -1. [Scatter Plot](/examples/scatter_plot) -1. [Bar Graph](/examples/bar) -1. [Pie Chart](/examples/pie) -1. [Force Directed Graphs](/examples/force_directed_graph) -1. [Mapping](/examples/mapping) diff --git a/SCATTER_PLOT.md b/SCATTER_PLOT.md deleted file mode 100644 index df8c1c4..0000000 --- a/SCATTER_PLOT.md +++ /dev/null @@ -1,1306 +0,0 @@ -# Creating a Scatter Plot - -Let's pretend we've started jogging, and we want to visualize the data regarding our progress as a runner with a scatter plot. We're going to have an array of objects each with date and distance properties. For each object in the array, we're going to create a circle in our SVG. If the `distance` property of an object is relatively high, its associated circle will be higher up on the graph. If the `date` property of an object is relatively high (a later date), its associated circle be farther right. - -By the end of this lesson, you should be able to: - -1. Add link to d3 library -1. Add an `` tag and size it with D3 -1. Create some fake data for our app -1. Add SVG circles and style them -1. Create a linear scale -1. Attach data to visual elements -1. Use data attached to a visual element to affect its appearance -1. Create a time scale -1. Parse and format times -1. Set dynamic domains -1. Dynamically generate svg elements -1. Create axes -1. Display data in a table -1. Create click handler -1. Remove data -1. Drag an element -1. Update data after a drag -1. Create a zoom behavior that scales elements -1. Update axes when zooming -1. Update click points after a transform -1. Avoid redrawing entire screen during render -1. Hide elements beyond axis -1. Use AJAX - -## Add link to d3 library - -The first thing we want to do is create basic `index.html` file: - -```html - - - - - - - - - -``` - -Now add a link to D3 at the bottom of your `` tag in `index.html`. We'll put it at the bottom so that the script loads after all your other HTML elements have loaded into the browser: - -```html - - - -``` - -Now create `app.js` in the same folder as your `index.html`. In it, we will store all of our JS code. For now just put this code in it to see if it works: - -```javascript -console.log('this works'); -console.log(d3); -``` - -and link to it in `index.html` at the bottom of the `` tag. Make sure it comes after the D3 script tag so that D3 loads before your `app.js` script: - -```html - - - - -``` - -Open `index.html` in Chrome just like we did in the SVG chapter (File->Open File) and check your dev tools (View->Developer->Developer Tools) to see if your javascript files are linked correctly: - -![](https://i.imgur.com/NOOdIyf.png) - -## Add an `` tag and size it with D3 - -In `index.html`, at the top of the ``, before your `script` tags, add an `` tag: - -```html - - - - - -``` - -If we examine the Elements tab of our dev tools, we'll see the `svg` element has been placed. In Chrome, it has a default width/height of 300px/150px - -![](https://i.imgur.com/pREbm8a.png) - -In `app.js`, remove your previous `console.log` statements and create variables to hold the width and height of the `` tag: - -```javascript -var WIDTH = 800; -var HEIGHT = 600; -``` - -Next, we can use `d3.select()` to select a single element, in this case the `` element: - -```javascript -var WIDTH = 800; -var HEIGHT = 600; - -d3.select('svg'); -``` - -The return value of `d3.select('svg')` is a D3 version of the `svg` element (just like in jQuery), so we can "chain" commands onto this. Let's add some styling to adjust the height/width of the element: - -```javascript -d3.select('svg') - .style('width', WIDTH) - .style('height', HEIGHT); -``` - -Now when we check the dev tools, we'll see the `` element has been resized: - -![](https://i.imgur.com/qsrPJkf.png) - -## Create some fake data for our app - -In `app.js` let's create an array of "run" objects (**NOTE:** I'm storing the date as a string on purpose. Also, it's important that this be an array of objects, in order to work with D3). Here's what your `app.js` code should look like so far: - -```javascript -var WIDTH = 800; -var HEIGHT = 600; - -var runs = [ - { - id: 1, - date: 'October 1, 2017 at 4:00PM', - distance: 5.2 - }, - { - id: 2, - date: 'October 2, 2017 at 5:00PM', - distance: 7.0725 - }, - { - id: 3, - date: 'October 3, 2017 at 6:00PM', - distance: 8.7 - } -]; - -d3.select('svg') - .style('width', WIDTH) - .style('height', HEIGHT); -``` - -## Add SVG circles and style them - -In `index.html`, add three circles to your `` element (each one will represent a run): - -```html - - - - - -``` - -Create `app.css` in the same folder as `index.html` with some styling for the circles and our `svg` element: - -```css -circle { - r:5; - fill: black; -} -svg { - border: 1px solid black; -} -``` - -and link to it in the head of `index.html`: - -```html - - - - - -``` - -Our page should now look like this: - -![](https://i.imgur.com/CIjpYWs.png) - -Note that all three circles are in the upper left corder of the screen. This is because all three are positioned at `(0,0)` so they overlap each other. It appears as if there is just one circle, but in reality all three are present - -## Create a linear scale - -We currently have three circles in our SVG and three objects in our `runs` array. One of the best things D3 does is provide the ability to link SVG elements with data so that as the data changes, so do the SVG elements. In this chapter, we're going to link each circle to an object in the `runs` array. If the `distance` property of an object is relatively high, its associated circle will be higher up on the graph. If the `date` property of an object is relatively high (a later date), its associated circle be farther right. - -First, let's position the circles vertically, based on the `distance` property of the objects in our `runs` array. One of the most important things that D3 does is provide the ability to convert (or "map") data values to visual points and vice versa. It does so using a `scale`. There are lots of different kinds of scales that handle lots of different data types, but for now we're just going to use a `linear scale` which will map numeric data values to numeric visual points and vice versa. - -At the bottom of `app.js`, add the following: - -```javascript -var yScale = d3.scaleLinear(); //create the scale -``` - -Whenever we create a scale, we need to tell it what are the minimum and maximum possible values that can exist in our data (this is called the "domain"). To do so for our `yScale`, add the following to the bottom of `app.js`: - -```javascript -yScale.domain([0, 10]); //minimum data value is 0, max is 10 -``` - -We also need to tell the scale what visual values correspond to those min/max values in the data (this is called the "range"). To do so, add the following to the bottom of `app.js`: - -```javascript -//HEIGHT corresponds to min data value -//0 corresponds to max data value -yScale.range([HEIGHT, 0]); -``` - -Your last three lines of code in app.js should look like this now: - -```javascript -var yScale = d3.scaleLinear(); //create the scale -yScale.domain([0, 10]); //minimum data value is 0, max is 10 -//HEIGHT corresponds to min data value -//0 corresponds to max data value -yScale.range([HEIGHT, 0]); -``` - -In the previous snippet, the first (starting) value for the range is `HEIGHT` (600) and the second (ending) value is 0. The minimum for the data values is 0 and the max is 10. By doing this, we're saying that a data point (distance run) of 0 should map to a visual height value of `HEIGHT` (600): - -![](https://i.imgur.com/VispBfN.png) - -This is because the lower the distance run (data value), the more we want to move the visual point down the Y axis. Remember that the Y axis starts with 0 at the top and increases in value as we move down vertically on the screen. - -We also say that a data point (distance run) of 10 should map to a visual height of 0: - -![](https://i.imgur.com/DsqDCzD.png) - -Again, this is because as the distance run increases, we want to get back a visual value that is lower and lower so that our circles are closer to the top of the screen. - -If you ever need to remind yourself what the domain/range are, you can do so by logging `yScale.domain()` or `yScale.range()`. Temporarily add the following at the bottom `app.js`: - -```javascript -console.log(yScale.domain()); //you can get the domain whenever you want like this -console.log(yScale.range()); //you can get the range whenever you want like this -``` - -Our Chrome console should look like this: - -![](https://i.imgur.com/H6l8HkQ.png) - -When declaring range/domain of a linear scale, we only need to specify start/end values for each. Values in between the start/end will be calculated by D3. For instance, to find out what visual value corresponds to the distance value of 5, use `yScale()`. Remove the previous two `console.log()` statements and add the following to the bottom of `app.js`: - -```javascript -console.log(yScale(5)); //get a visual point from a data value -``` - -Here's what our dev console should look like in Chrome: - -![](https://i.imgur.com/ggSwAv2.png) - -It makes sense that this logs `300` because the data value of `5` is half way between the minimum data value of `0` and the maximum data value of `10`. The range starts at `HEIGHT` (600) and goes to `0`, so half way between those values is 300. - -So whenever you want to convert a data point to a visual point, call `yScale()`. We can go the other way and convert a visual point to a data value by calling `yScale.invert()`. To find out what data point corresponds to a visual value of 450, remove the previous `console.log()` statement and add the following to the bottom of `app.js`: - -```javascript -console.log(yScale.invert(450)); //get a data values from a visual point -``` - -Here's what Chrome's console looks like: - -![](https://i.imgur.com/7BdxFkm.png) - -It makes sense that this logs 2.5 because the visual value of 450 is 25% of the way from the starting visual value of 600 (`HEIGHT`) to the ending visual value of 0. You can now delete that last `console.log()` line. - -## Attach data to visual elements - -Now let's attach each of the javascript objects in our "runs" array to a circle in our SVG. Once we do this, each circle can access the data of its associated "run" object in order to determine its position. Add the following to the bottom of `app.js`: - -```javascript -d3.selectAll('circle').data(runs); //selectAll is like select, but selects all elements that match the query string -``` - -If there were more objects in our "runs" array than there are circles, the extra objects are ignored. If there are more circles than objects, then javascript objects are attached to circles in the order in which they appear in the DOM until there are no more objects to attach. - -## Use data attached to a visual element to affect its appearance - -We can change attributes for a selection of DOM elements by passing static values, and all selected elements will have that attribute set to that one specific value. Add the following temporarily to the end of `app.js`: - -```javascript -d3.selectAll('circle').attr('cy', 300); -``` - -![](https://i.imgur.com/Nn6CrEX.png) - -But now that each circle has one of our "runs" javascript data objects attached to it, we can set attributes on each circle using that data. We do that by passing the `.attr()` method a callback function instead of a static value for its second parameter. Remove `d3.selectAll('circle').attr('cy', 300);` and adjust the last line of `app.js` from `d3.selectAll('circle').data(runs);` to the following: - -```javascript -d3.selectAll('circle').data(runs) - .attr('cy', function(datum, index){ - return yScale(datum.distance); - }); -``` - -If we refresh the browser, this is what we should see: - -![](https://i.imgur.com/qAcjQyt.png) - -Let's examine what we just wrote. The callback function passed as the second parameter to `.attr()` runs on each of the visual elements selected (each of the `circle` elements in this case). During each execution of the callback, the return value of that callback function is then assigned to whatever aspect of the current element is being set (in this case the `cy` attribute). - -The callback function takes two params: - -- the individual `datum` object from the `runs` array that was attached to that particular visual element when we called `.data(runs)` -- the `index` of that `datum` in the `runs` array - -In summary what this does is loop through each `circle` in the SVG. For each `circle`, it looks at the "run" object attached to that `circle` and finds its `distance` property. It then feeds that data value into `yScale()` which then converts it into its corresponding visual point. That visual point is then assigned to that circle's `cy` attribute. Since each data object has a different `distance` value, each `circle` is placed differently, vertically. - -## Create a time scale - -Let's position the circles horizontally, based on the date that their associated run happened. First, create a time scale. This is like a linear scale, but instead of mapping numeric values to visual points, it maps Dates to visual points. Add the following to the bottom of `app.js`: - -```javascript -var xScale = d3.scaleTime(); //scaleTime maps date values with numeric visual points -xScale.range([0,WIDTH]); -xScale.domain([new Date('2017-10-1'), new Date('2017-10-31')]); - -console.log(xScale(new Date('2017-10-28'))); -console.log(xScale.invert(400)); -``` - -Here's what our console should look like: - -![](https://i.imgur.com/zL7WQ3P.png) - -You can now remove the two `console.log()` statements. - -## Parse and format times - -Note that the `date` properties of the objects in our `runs` array are strings and not Date objects. This is a problem because `xScale`, as with all time scales, expects its data values to be Date objects. Fortunately, D3 provides us an easy way to convert strings to dates and vice versa. We'll use a specially formatted string, based on the documentation (https://github.com/d3/d3-time-format#locale_format), to tell D3 how to parse the `date` String properties of the objects in our `runs` array into actual JavaScript Date objects. Add the following at the end of `app.js`: - -```javascript -var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p"); //this format matches our data in the runs array -console.log(parseTime('October 3, 2017 at 6:00PM')); - -var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p"); //this format matches our data in the runs array -console.log(formatTime(new Date())); -``` - -![](https://i.imgur.com/vGH75ve.png) - -Let's use this when calculating `cx` attributes for our circles. Remove the last two `console.log()` statements and add the following at the bottom of `app.js`: - -```javascript -d3.selectAll('circle') - .attr('cx', function(datum, index){ - 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 - }); -``` - -Here's what Chrome should look like: - -![](https://i.imgur.com/nD9CW7V.png) - -In summary, this selects all of the `circle` elements. It then sets the `cx` attribute of each `circle` to the result of a callback function. That callback function runs for each `circle` and takes the "run" data object associated with that `circle` and finds its `date` property (remember it's a string, e.g. `'October 3, 2017 at 6:00PM'`). It passes that string value to `parseTime()` which then turns the string into an actual JavaScript Date object. That Date object is then passed to `xScale()` which converts the date into a visual value. That visual value is then used for the `cx` attribute of whichever `circle` the callback function has just run on. Since each `date` property of the objects in the `runs` array is different, the `circles` have different horizontal locations. - -## Set dynamic domains - -At the moment, we're setting arbitrary min/max values for the domains of both distance and date. D3 can find the min/max of a data set, so that our graph displays just the data ranges we need. All we need to do is pass the min/max methods a callback which gets called for each item of data in the `runs` array. D3 uses the callback to determine which properties of the datum object to compare for min/max - -Go to this part of the code: - -```javascript -var yScale = d3.scaleLinear(); //create the scale -yScale.range([HEIGHT, 0]); //set the visual range (e.g. 600 to 0) -yScale.domain([0, 10]); //set the data domain (e.g. 0 to 10) -``` - -and change it to this: - -```javascript -var yScale = d3.scaleLinear(); //create the scale -yScale.range([HEIGHT, 0]); //set the visual range (e.g. 600 to 0) -var yMin = d3.min(runs, function(datum, index){ - return datum.distance; //compare distance properties of each item in the data array -}) -var yMax = d3.max(runs, function(datum, index){ - return datum.distance; //compare distance properties of each item in the data array -}) -yScale.domain([yMin, yMax]); //now that we have the min/max of the data set for distance, we can use those values for the yScale domain -console.log(yScale.domain()); -``` - -Chrome should look like this: - -![](https://i.imgur.com/7JDfzD9.png) - -Let's examine what we just wrote. The following code finds the minimum distance: - -```javascript -var yMin = d3.min(runs, function(datum, index){ - return datum.distance; //compare distance properties of each item in the data array -}) -``` - -D3 loops through the `runs` array (the first parameter) and calls the callback function (the second parameter) on each element of the array. The return value of that function is compared the return values of the callback function as it runs on the other elements. The lowest value is assigned to `yMin`. The same thing happens for `d3.max()` but with the highest value. - -We can combine both the min/max functions into one `extent` function that returns an array that has the exact same structure as `[yMin, yMax]`. Change the code we just wrote: - -```javascript -var yScale = d3.scaleLinear(); //create the scale -yScale.range([HEIGHT, 0]); //set the visual range (e.g. 600 to 0) -var yMin = d3.min(runs, function(datum, index){ - return datum.distance; //compare distance properties of each item in the data array -}) -var yMax = d3.max(runs, function(datum, index){ - return datum.distance; //compare distance properties of each item in the data array -}) -yScale.domain([yMin, yMax]); //now that we have the min/max of the data set for distance, we can use those values for the yScale domain -``` - -to this: - -```javascript -var yScale = d3.scaleLinear(); //create the scale -yScale.range([HEIGHT, 0]); //set the visual range (e.g. 600 to 0) -var yDomain = d3.extent(runs, function(datum, index){ - return datum.distance; //compare distance properties of each item in the data array -}) -yScale.domain(yDomain); -``` - -Much shorter, right? Let's do the same for the xScale's domain. Go to this part of the code: - -```javascript -var xScale = d3.scaleTime(); //scaleTime maps date values with numeric visual points -xScale.range([0,WIDTH]); -xScale.domain([new Date('2017-10-1'), new Date('2017-10-31')]); - -var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p"); //this format matches our data in the runs array -var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p"); //this format matches our data in the runs array -``` - -and change it to: - -```javascript -var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p"); -var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p"); -var xScale = d3.scaleTime(); -xScale.range([0,WIDTH]); -var xDomain = d3.extent(runs, function(datum, index){ - return parseTime(datum.date); -}); -xScale.domain(xDomain); -``` - -Notice we moved `parseTime` and `formatTime` up so they could be used within the `.extent()`. Here's what Chrome should look like: - -![](https://i.imgur.com/gSA05gP.png) - -## Dynamically generate SVG elements - -Currently, we have just enough `` elements to fit our data. What if we don't want to count how many elements are in the array? D3 can create elements as needed. First, remove all `` elements from `index.html`. Your `` tag should look like this now: - -```html - -``` - -In `app.js`, go to this part of the code: - -```javascript -d3.selectAll('circle').data(runs) - .attr('cy', function(datum, index){ - return yScale(datum.distance); - }); -``` - -modify the code to create the circles: - -```javascript -d3.select('svg').selectAll('circle') //since no circles exist, we need to select('svg') so that d3 knows where to append the new circles - .data(runs) //attach the data as before - .enter() //find the data objects that have not yet been attached to visual elements - .append('circle'); //for each data object that hasn't been attached, append a to the - -d3.selectAll('circle') - .attr('cy', function(datum, index){ - return yScale(datum.distance); - }); -``` - -It should look exactly the same as before, but now circles are being created for each object in the `runs` array: - -![](https://i.imgur.com/r59oUuJ.png) - -Here's a more in depth explanation of what we just wrote. Take a look at the first line of new code from above: - -```javascript -d3.select('svg').selectAll('circle') -``` - -This might seem unnecessary. Why not just do `d3.selectAll('circle')`? Well, at the moment, there are no `circle` elements. We're going to be appending `circle` elements dynamically, so `d3.select('svg')` tells D3 where to append them. We still need `.selectAll('circle')` though, so that when we call `.data(runs)` on the next line, D3 knows what elements to bind the various objects in the `runs` array to. But there aren't any `circle` elements to bind data to. That's okay: `.enter()` finds the "run" objects that haven't been bound to any `circle` elements yet (in this case all of them). We then use `.append('circle')` to append a circle for each unbound "run" object that `.enter()` found. - -## Create axes - -D3 can automatically generate axes for you. Add the following to the bottom of `app.js`: - -```javascript -var bottomAxis = d3.axisBottom(xScale); //pass the appropriate scale in as a parameter -``` - -This creates a bottom axis generator that can be used to insert an axis into any element you choose. Add the following code at the bottom of `app.js` to append a `` element inside our SVG element and then insert a bottom axis inside of it: - -```javascript -d3.select('svg') - .append('g') //put everything inside a group - .call(bottomAxis); //generate the axis within the group -``` - -Here's what Chrome should look like: - -![](https://i.imgur.com/nLwIVBI.png) - -We want the axis to be at the bottom of the SVG, though. Modify the code we just wrote so it looks like this (**NOTE:** we removed a `;` after `.call(bottomAxis)` and added `.attr('transform', 'translate(0,'+HEIGHT+')');`): - -```javascript -var bottomAxis = d3.axisBottom(xScale); //pass the appropriate scale in as a parameter -d3.select('svg') - .append('g') //put everything inside a group - .call(bottomAxis) //generate the axis within the group - .attr('transform', 'translate(0,'+HEIGHT+')'); //move it to the bottom -``` - -Currently, our SVG clips the axis: - -[](https://i.imgur.com/byJXkLO.png) - -Let's alter our `svg` CSS so it doesn't clip any elements that extend beyond its bounds: - -```css -svg { - overflow: visible; -} -``` - -Now it looks good: - -![](https://i.imgur.com/kd0AiMt.png) - -The left axis is pretty similar. Add the following to the bottom of `app.js`: - -```javascript -var leftAxis = d3.axisLeft(yScale); -d3.select('svg') - .append('g') - .call(leftAxis); //no need to transform, since it's placed correctly initially -``` - -Note we don't need to set a `transform` attribute since it starts out in the correct place initially: - -![](https://i.imgur.com/aP4hTVq.png) - -It's a little tough to see, so let's add the following at the bottom of `app.css`: - -```css -body { - margin: 20px 40px; -} -``` - -Now our axes are complete: - -![](https://i.imgur.com/FFgC68e.png) - -## Display data in a table - -Just for debugging purposes, let's create a table which will show all of our data. Make your `` tag in `index.html` look like this: - -```html - - - - - - - - - - - - -
iddatedistance
- - - -``` - -D3 can also be used to manipulate the DOM, just like jQuery. Let's populate the `` in that style. Add the following to the bottom of `app.js`: - -```javascript -var createTable = function(){ - for (var i = 0; i < runs.length; i++) { - var row = d3.select('tbody').append('tr'); - row.append('td').html(runs[i].id); - row.append('td').html(runs[i].date); - row.append('td').html(runs[i].distance); - } -} - -createTable(); -``` - -Add some styling for the table at the bottom of `app.css`: - -```css -table, th, td { - border: 1px solid black; -} -th, td { - padding:10px; - text-align: center; -} -``` - -Adjust the CSS for `svg` to add a bottom margin. This will create some space between the graph and the table: - -```css -svg { - overflow: visible; - margin-bottom: 50px; -} -``` - -Now the browser should look like this: - -![](https://i.imgur.com/luGiAys.png) - -## Create click handler - -Let's say that we want it so that when the user clicks on the `` element, it creates a new run. Add the following to the bottom of `app.js`: - -```javascript -d3.select('svg').on('click', function(){ - var x = d3.event.offsetX; //gets the x position of the mouse relative to the svg element - var y = d3.event.offsetY; //gets the y position of the mouse relative to the svg element - - 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 newRun = { //create a new "run" object - id: runs[runs.length-1].id+1, //generate a new id by adding 1 to the last run's id - date: formatTime(date), //format the date object created above to a string - distance: distance //add the distance - } - runs.push(newRun); //push the new run onto the runs array - createTable(); //render the table -}); -``` - -Let's examine what we just wrote. `d3.select('svg').on('click', function(){` Sets up a click handler on the `svg` element. The anonymous function that gets passed in as the second parameter to `.on()` gets called each time the user clicks on the SVG. Once inside that callback function, we use `d3.event.offsetX` to get the x position of the mouse inside the SVG and `d3.event.offsetY` to get the y position. We then use `xScale.invert()` and `yScale.invert()` to turn the x/y visual points into data values (date and distance, respectively). We then use those data values to create a new run object. We create an id for the new run by getting the id of the last element in the `runs` array and adding 1 to it. Lastly, we push the new run onto the `runs` array and call `createTable()`. - -Click on the SVG to create a new run. You might notice that `createTable()` just adds on all the run rows again - -![](https://i.imgur.com/Vu2CwCI.png) - -Let's alter the `createTable()` function so that when it runs, it clears out any rows previously created and re-renders everything. Add `d3.select('tbody').html('')` to the top of the `createTable` function in `app.js`: - -```javascript -var createTable = function(){ - d3.select('tbody').html(''); //clear out all rows from the table - for (var i = 0; i < runs.length; i++) { - var row = d3.select('tbody').append('tr'); - row.append('td').html(runs[i].id); - row.append('td').html(runs[i].date); - row.append('td').html(runs[i].distance); - } -} -``` - -Now refresh the page, and click on the SVG to create a new run. The table should look like this now: - -![](https://i.imgur.com/YcoPxK7.png) - -The only issue now is that circles aren't being created when you click on the SVG. To fix this, let's wrap the code for creating `` elements in a render function, and call `render()` immediately after it's defined: - -```javascript -var render = function(){ - - var yScale = d3.scaleLinear(); - yScale.range([HEIGHT, 0]); - yDomain = d3.extent(runs, function(datum, index){ - return datum.distance; - }) - yScale.domain(yDomain); - - d3.select('svg').selectAll('circle') - .data(runs) - .enter() - .append('circle'); - - d3.selectAll('circle') - .attr('cy', function(datum, index){ - return yScale(datum.distance); - }); - - var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p"); - var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p"); - var xScale = d3.scaleTime(); - xScale.range([0,WIDTH]); - xDomain = d3.extent(runs, function(datum, index){ - return parseTime(datum.date); - }); - xScale.domain(xDomain); - - d3.selectAll('circle') - .attr('cx', function(datum, index){ - return xScale(parseTime(datum.date)); - }); - -} -render(); -``` - -If you refresh the browser, you'll see an error in the console. This is because the `bottomAxis` and `leftAxis` use `xScale` and `yScale` which are now scoped to exist only inside the `render()` function. For future use, let's move the `xScale` and `yScale` out of the render function along with the code for creating the domains/ranges: - -```javascript -var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p"); -var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p"); -var xScale = d3.scaleTime(); -xScale.range([0,WIDTH]); -xDomain = d3.extent(runs, function(datum, index){ - return parseTime(datum.date); -}); -xScale.domain(xDomain); - -var yScale = d3.scaleLinear(); -yScale.range([HEIGHT, 0]); -yDomain = d3.extent(runs, function(datum, index){ - return datum.distance; -}) -yScale.domain(yDomain); -var render = function(){ - - d3.select('svg').selectAll('circle') //since no circles exist, we need to select('svg') so that d3 knows where to append the new circles - .data(runs) //attach the data as before - .enter() //find the data objects that have not yet been attached to visual elements - .append('circle'); //for each data object that hasn't been attached, append a to the - - d3.selectAll('circle') - .attr('cy', function(datum, index){ - return yScale(datum.distance); - }); - - d3.selectAll('circle') - .attr('cx', function(datum, index){ - 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 - }); - -} -render(); -``` - -Now go to the bottom of `app.js` and add a line to call `render()` inside our `` click handler: - -```javascript -var newRun = { //create a new "run" object - id: runs[runs.length-1].id+1, //generate a new id by adding 1 to the last run's id - date: formatTime(date), //format the date object created above to a string - distance: distance //add the distance -} -runs.push(newRun); -createTable(); -render(); //add this line -``` - -Now when you click the SVG, a circle will appear: - -![](https://i.imgur.com/5KjqmNp.png) - -## Remove data - -Let's set up a click handler on all `` elements so that when the user clicks on a `` D3 will remove that circle and its associated data element from the array. Add the following code at the bottom of the `render` function declaration we wrote in the last section. We do this so that the click handlers are attached **AFTER** the circles are created: - -```javascript -//put this at the bottom of the render function, so that click handlers are attached when the circle is created -d3.selectAll('circle').on('click', function(datum, index){ - d3.event.stopPropagation(); //stop click event from propagating to the SVG element and creating a run - 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 - return run.id != datum.id; - }); - render(); //re-render dots - createTable(); //re-render table -}); -``` - -Let's examine the above code. The first line selects all `` elements and creates a click handler on each of them. `d3.event.stopPropagation();` prevents the our click from bubbling up the DOM to the SVG. If we don't add it, the click handler on the SVG will fire in addition, when we click on a circle. This would create an additional run every time we try to remove a run. Next we call: - -```javascript -runs = runs.filter(function(run, index){ - return run.id != datum.id; -}); -``` - -This loops through the `runs` array and filters out any objects that have an `id` property that matches that of the `id` property of the `datum` that is associated with the `` that was clicked. Notice that the callback function in `.on('click', function(datum, index){` takes two parameters: `datum`, the "run" object associated with that `` and the `index` of the that "run" object in the `runs` array. - -Once we've filtered out the correct "run" object from the `runs` array, we call `render()` and `createdTable()` to re-render the the graph and the table. - -But, if we click on the middle circle and examine the Elements tab of the dev tools, we'll see the `` element hasn't been removed: - -![](https://i.imgur.com/JoZyC1j.png) - -In the image above, it appears as though there are only two circles, but really the middle one has had its `cx` set to 800 and its `cy` set to 0. It's overlapping the other circle in the same position. This is because we've removed the 2nd element in the `runs` array. When we re-render the graph, the `runs` array only has two objects. The 2nd "run" object used to be the third "run" object before we removed the the middle run. Now that it's the 2nd "run" object, the second `` is assigned its data. The third circle still has its old data assigned to it, so both the second and the third circle have the same data and are therefore placed in the same location. - -Let's put the circles in a `` so that it's easy to clear out all the circles and re-render them when we remove a run. This way we won't have any extra `` elements laying around when we try to remove them. This approach is similar to what we do when re-rendering the table. Adjust your `` element in `index.html` so it looks like this: - -```html - - - -``` - -Now we can clear out the `` elements each time `render()` is called. This is a little crude, but it'll work for now. Later on, we'll do things in a more elegant fashion. At the top of the `render()` function declaration, add `d3.select('#points').html('');` and adjust the next line from `d3.select('svg').selectAll('circle')` to `d3.select('#points').selectAll('circle')`: - -```javascript -//adjust the code at the top of your render function -d3.select('#points').html(''); //clear out all circles when rendering -d3.select('#points').selectAll('circle') //add circles to #points group, not svg - .data(runs) - .enter() - .append('circle'); -``` - -Now if we click on the middle circle, the element is removed from the DOM: - -![](https://i.imgur.com/h8TFFdN.png) - -If you try to delete all the circles and then add a new one, you'll get an error: - -![](https://i.imgur.com/FprJXNN.png) - -This is because, our code for creating a `newRun` in the SVG click handler needs some work: - -```javascript -var newRun = { //create a new "run" object - id: runs[runs.length-1].id+1, //generate a new id by adding 1 to the last run's id - date: formatTime(date), //format the date object created above to a string - distance: distance //add the distance -} -``` - -This is because when there are no run elements in the `runs` array,`runs[runs.length-1]` tries to access an element at index -1 in the array. Inside the `` click handler, let's put in a little code to handle when the user has deleted all runs and tries to add a new one: - -```javascript -//inside svg click handler -var newRun = { - id: ( runs.length > 0 ) ? runs[runs.length-1].id+1 : 1, //add this line - date: formatTime(date), - distance: distance -} -``` - -Here's what Chrome should look like now if you delete all the runs and then try to add a new one: - -![](https://i.imgur.com/N1taq91.png) - -Lastly, let's put in some css, so we know we're clicking on a circle. First, add `transition: r 0.5s linear, fill 0.5s linear;` to the CSS code you've already written for `circle`: - -```css -circle { - r: 5; - fill: black; - transition: r 0.5s linear, fill 0.5s linear; /* add this transition to original code */ -} -``` - -then add this to the bottom of `app.css`: - -```css -/* add this css for the hover state */ -circle:hover { - r:10; - fill: blue; -} -``` - -Here's what a circle should look like when you hover over it: - -![](https://i.imgur.com/umPJiTD.png) - -## Drag an element - -We want to be able to update the data for a run by dragging the associated circle. To do this, we'll use a "behavior," which you can think of as a combination of multiple event handlers. For a drag behavior, there are three callbacks: - -- when the user starts to drag -- each time the user moves the cursor before releasing the "mouse" button -- when the user releases the "mouse" button - -There are two steps whenever we create a behavior: - -- create the behavior -- attach the behavior to one or more elements - -Put the following code at the bottom of the `render()` function declaration: - -```javascript -var drag = function(datum){ - var x = d3.event.x; - var y = d3.event.y; - d3.select(this).attr('cx', x); - d3.select(this).attr('cy', y); -} -var dragBehavior = d3.drag() - .on('drag', drag); -d3.selectAll('circle').call(dragBehavior); -``` - -You can now drag the circles around, but the data doesn't update: - -![](https://i.imgur.com/4pLzqt7.png) - -Let's examine how this code works: - -```javascript -var drag = function(datum){ - var x = d3.event.x; - var y = d3.event.y; - d3.select(this).attr('cx', x); - d3.select(this).attr('cy', y); -} -``` - -This `drag` function will be used as a callback anytime the user moves the cursor before releasing the "mouse" button. It gets the x and y coordinates of the mouse and sets the `cx` and `cy` values of the element being dragged (`d3.select(this)`) to those coordinates. - -Next we generate a drag behavior that will, at the appropriate time, call the `drag` function that was just explained: - -```javascript -var dragBehavior = d3.drag() - .on('drag', drag); -``` - -Lastly, we attach that behavior to all `` elements: - -```javascript -d3.selectAll('circle').call(dragBehavior); -``` - -## Update data after a drag - -Now we're going to add functionality so that when the user releases the "mouse" button, the data for the "run" object associated with the circle being dragged gets updated. - -First lets create the callback function that will get called when the user releases the "mouse" button. Towards the bottom of the `render()` function declaration, add the following code just above `var drag = function(datum){`: - -```javascript -var dragEnd = function(datum){ - var x = d3.event.x; - var y = d3.event.y; - - var date = xScale.invert(x); - var distance = yScale.invert(y); - - datum.date = formatTime(date); - datum.distance = distance; - createTable(); -} -``` - -Now attach that function to the `dragBehavior` so that it is called when the user stops dragging a circle. Change the following code: - -```javascript -var dragBehavior = d3.drag() - .on('drag', drag); -``` - -to this: - -```javascript -var dragBehavior = d3.drag() - .on('drag', drag) - .on('end', dragEnd); -``` - -Now, once you stop dragging a circle around, you should see the data in the table change. - -![](https://i.imgur.com/sIQrOMC.png) - -Let's change the color of a circle while it's being dragged too. Add this at the bottom of `app.css`: - -```css -circle:active { - fill: red; -} -``` - -When you drag a circle, it should turn red - -## Create a zoom behavior that scales elements - -Another behavior we can create is the zooming/panning ability. Once this functionality is complete you will be able to zoom in and out on different parts of the graph by doing one of the following: - -- two finger drag on a trackpad -- rotate your mouse wheel -- pinch/spread on a trackpad - -You will also be able to pan left/right/up/down on the graph by clicking and dragging on the SVG element - -Put the following code at the bottom of `app.js`: - -```javascript -var zoomCallback = function(){ - d3.select('#points').attr("transform", d3.event.transform); -} -``` - -This is the callback function that will be called when user attempts to zoom/pan. All it does is take the zoom/pan action and turn it into a `transform` attribute that gets applied to the `` element that contains the circles. Now add the following code at the bottom of `app.js` to create the `zoom` behavior and attach it to the `svg` element: - -```javascript -var zoom = d3.zoom() - .on('zoom', zoomCallback); -d3.select('svg').call(zoom); -``` - -Now if we zoom out, the graph should look something like this: - -![](https://i.imgur.com/Qvnu7yZ.png) - -## Update axes when zooming/panning - -Now when we zoom, the points move in/out. When we pan, they move vertically/horizontally. Unfortunately, the axes don't update accordingly. Let's first add ids to the `` elements that contain them. Find the following code: - -```javascript -var bottomAxis = d3.axisBottom(xScale); -d3.select('svg') - .append('g') - .call(bottomAxis) - .attr('transform', 'translate(0,'+HEIGHT+')'); - -var leftAxis = d3.axisLeft(yScale); -d3.select('svg') - .append('g') - .call(leftAxis); -``` - -Add `.attr('id', 'x-axis')` after the first `.append('g')` and `.attr('id', 'y-axis')` after the second `.append('g')`: - -```javascript -d3.select('svg') - .append('g') - .attr('id', 'x-axis') //add an id - .call(bottomAxis) - .attr('transform', 'translate(0,'+HEIGHT+')'); - -var leftAxis = d3.axisLeft(yScale); -d3.select('svg') - .append('g') - .attr('id', 'y-axis') //add an id - .call(leftAxis); -``` - -Now let's use those ids to adjust the axes when we zoom. Find this code: - -```javascript -var zoomCallback = function(){ - d3.select('#points').attr("transform", d3.event.transform); -} -``` - -Add the following at the end of the function declaration: - -```javascript -d3.select('#x-axis') - .call(bottomAxis.scale(d3.event.transform.rescaleX(xScale))); -d3.select('#y-axis') - .call(leftAxis.scale(d3.event.transform.rescaleY(yScale))); -``` - -Your zoomCallback should now look like this: - -```javascript -var zoomCallback = function(){ - d3.select('#points').attr("transform", d3.event.transform); - d3.select('#x-axis') - .call(bottomAxis.scale(d3.event.transform.rescaleX(xScale))); - d3.select('#y-axis') - .call(leftAxis.scale(d3.event.transform.rescaleY(yScale))); -} -``` - -Two things to note about the code above: - -- `bottomAxis.scale()` tells the axis to redraw itself -- `d3.event.transform.rescaleX(xScale)` returns a value indicating how the bottom axis should rescale - -Now when you zoom out, the axes should redraw themselves: - -![](https://i.imgur.com/AI3KoG9.png) - -## Update click points after a transform - -Try zoom/panning and the clicking on the SVG to create a new run. You'll notice it's in the wrong place. That's because the SVG click handler has no idea that a zoom/pan has happened. Currently, if you click on the visual point, no matter how much you may have zoomed/panned, the click handler still converts it as if you had never zoomed/panned. - -When we zoom, we need to save the transformation information to a variable so that we can use it later to figure out how to properly create circles and runs. Find the `zoomCallback` declaration and add `var lastTransform = null` right before it. Then add `lastTransform = d3.event.transform;` at the beginning of the function declaration. It should look like this: - -```javascript -var lastTransform = null; //add this -var zoomCallback = function(){ - lastTransform = d3.event.transform; //add this - d3.select('#points').attr("transform", d3.event.transform); - d3.select('#x-axis') - .call(bottomAxis.scale(d3.event.transform.rescaleX(xScale))); - d3.select('#y-axis') - .call(leftAxis.scale(d3.event.transform.rescaleY(yScale))); -} -``` - -Now, whenever the user zooms/pans the transform that was used to adjust the SVG and axes is saved in the `lastTransform` variable. Use that variable when clicking on the SVG. - -Find these two lines at the beginning of the SVG click handler: - -```javascript -var x = d3.event.offsetX; -var y = d3.event.offsetY; -``` - -change them to: - -```javascript -var x = lastTransform.invertX(d3.event.offsetX); -var y = lastTransform.invertY(d3.event.offsetY); -``` - -Your click handler should look like this now: - -```javascript -d3.select('svg').on('click', function(){ - var x = lastTransform.invertX(d3.event.offsetX); //adjust this - var y = lastTransform.invertY(d3.event.offsetY); //adjust this - - var date = xScale.invert(x) - var distance = yScale.invert(y); - - var newRun = { - id: ( runs.length > 0 ) ? runs[runs.length-1].id+1 : 1, - date: formatTime(date), - distance: distance - } - runs.push(newRun); - createTable(); - render(); -}); -``` - -But now clicking before any zoom is broken, since `lastTransform` will be null: - -![](https://i.imgur.com/2ozj3Nf.png) - -Find the code that we just wrote for the SVG click handler: - -```javascript -var x = lastTransform.invertX(d3.event.offsetX); -var y = lastTransform.invertY(d3.event.offsetY); -``` - -And adjust it so it looks like this: - -```javascript -var x = d3.event.offsetX; -var y = d3.event.offsetY; - -if(lastTransform !== null){ - x = lastTransform.invertX(d3.event.offsetX); - y = lastTransform.invertY(d3.event.offsetY); -} -``` - -Now initially, `x` and `y` are set to `d3.event.offsetX` and `d3.event.offsetY`, respectively. If a zoom/pan occurs, `lastTransform` will not be null, so we overwrite `x` and `y` with the transformed values. - -Add a new run initially: - -![](https://i.imgur.com/5DjmFlg.png) - -now pan right and add a new point: - -![](https://i.imgur.com/3gZAtmZ.png) - -## Avoid redrawing entire screen during render - -At the moment, every time we call `render()`, we wipe all `` elements in the ``. This is inefficient. Let's just remove the ones we don't want - -At the top of the `render()` function, assign the `d3.select('#points').selectAll('circle').data(runs)` to a variable, so we can use it later. This helps preserve how DOM elements are assigned to data elements in the next sections. Find this at the top of the `render()` function declaration: - -```javascript -d3.select('#points').html(''); -d3.select('#points').selectAll('circle') - .data(runs) - .enter() - .append('circle'); -``` - -change it to this: - -```javascript -d3.select('#points').html(''); -var circles = d3.select('#points') - .selectAll('circle') - .data(runs); -circles.enter().append('circle'); -``` - -Next remove the `d3.select('#points').html('');` line. We'll use `.exit()` to find the selection of circles that haven't been matched with data, then we'll use `.remove()` to remove those circles. Add the following after the last line we just wrote (`circles.enter().append('circle');`): - -```javascript -circles.exit().remove(); -``` - -Reload the page, click on the center (2nd) circle. You'll notice it looks like the circle disappears and the circle in the upper right briefly gains a hover state and then shrinks back down. That's not really what's happening. - -If we click on the middle circle (2nd), it deletes the 2nd "run" object in the `runs` array, and the third "run" object moves down to replace it in 2nd place. We now only have an array of two "run" objects: the first and what used to be the third (but is now second). When `render()` gets called again, what was the middle (2nd) circle gets assigned to what used to be the third "run" object in the `runs` array (but is now the second). This "run" object used to be assigned to the third circle, which was in the upper right. But now since there are only two runs, that third (upper right) circle gets deleted when we call `circles.exit().remove();`. The second circle's data has changed now, and it jumps to the upper right corner to match that data. It used to have a hover state, but all of a sudden it's moved out from under the cursor, so it shrinks back down to normal size and becomes black. - -To avoid these affects, we need to make sure that each circle stays with the data it used to be assigned to when we call `render()`. To do this, we can tell D3 to map `` to datum by id, rather than index in the array. At the top of the `render()` function, find this code: - -```javascript -var circles = d3.select('#points') - .selectAll('circle') - .data(runs); -``` - -Change it to this: - -```javascript -var circles = d3.select('#points') - .selectAll('circle') - .data(runs, function(datum){ - return datum.id - }); -``` - -This tells D3 to use the `id` property of each "run" object when determining which `` element to assign the data object to. It basically assigns that `id` property of the "run" object to the `` element initially. That way, when the 2nd "run" object is deleted, `circles.exit().remove();` will find the circle that had the corresponding id (the middle circle) and remove it. - -Now clicking on the middle circle should work correctly. - -## Hide elements beyond axis - -If you pan or zoom extensively, you'll notice that the circles are visible beyond the bounds of the axes: - -![](https://i.imgur.com/s2oXYxS.png) - -To hide elements once they get beyond an axis, we can just add an outer SVG with `id="container"` around our current `` element in `index.html`: - -```html - - - - - -``` - -Now replace all `d3.select('svg')` code with `d3.select('#container')`. You can do a find replace. There should be five instances to change: - -```javascript -d3.select('#container') - .style('width', WIDTH) - .style('height', HEIGHT); -// -// lots of code omitted here, including render() declaration... -// -var bottomAxis = d3.axisBottom(xScale); -d3.select('#container') - .append('g') - .attr('id', 'x-axis') - .call(bottomAxis) - .attr('transform', 'translate(0,'+HEIGHT+')'); - -var leftAxis = d3.axisLeft(yScale); -d3.select('#container') - .append('g') - .attr('id', 'y-axis') - .call(leftAxis); -// -// code for create table omitted here... -// -d3.select('#container').on('click', function(){ -// -// click handler functionality omitted -// -}); -// -// zoomCallback code omitted here -// -var zoom = d3.zoom() - .on('zoom', zoomCallback); -d3.select('#container').call(zoom); -``` - -And lastly, adjust css to replace `svg {` with `#container {`: - -```css -#container { - overflow: visible; - margin-bottom: 50px; -} -``` - -Now circles should be hidden once they move beyond the bounds of the inner `` element: - -![](https://i.imgur.com/t6BKuiz.png) - - -## Conclusion - -In this chapter we've learned the basics of D3 and created a fully interactive scatter plot. In the next chapter we'll learn how to use AJAX to make an asynchronous request that will populate a bar graph. diff --git a/SUMMARY.md b/SUMMARY.md deleted file mode 100644 index f285277..0000000 --- a/SUMMARY.md +++ /dev/null @@ -1,25 +0,0 @@ -# Summary - -## Lessons - -* [Intro](/INTRO.md) -* [SVG](/SVG.md) -* [D3](/D3.md) -* [Scatter Plot](/SCATTER_PLOT.md) -* [Bar Graph](/BAR.md) -* [Pie Chart](/PIE.md) -* [Force Directed Graphs](/FORCE_DIRECTED_GRAPH.md) -* [Mapping](/MAPS.md) - -## Labs - -* [Lab](/Lab.md) - -## Completed Code - -* [SVG](/examples/svg) -* [Scatter Plot](/examples/scatter_plot) -* [Bar Graph](/examples/bar) -* [Pie Chart](/examples/pie) -* [Force Directed Graphs](/examples/force_directed_graph) -* [Mapping](/examples/mapping) diff --git a/SVG.md b/SVG.md deleted file mode 100644 index 501d94f..0000000 --- a/SVG.md +++ /dev/null @@ -1,480 +0,0 @@ -# SVG - -This lesson covers how to create various SVG elements, the foundation of D3.js. In it we will cover the following topics - -1. Base tags -1. Basic Elements -1. Positioning -1. Styling -1. Important SVG elements - -## Base tag - -When viewing SVG graphics in a browser, it's important to embed and `` tag inside a basic HTML page. Let's create an `index.html` file and add the following to it: - -```html - - - - - - - - -``` - -Now start a web browser and open that file (usually File->Open File). For this book, it is recommended the reader use Google Chrome, but in development and production any browser will do. If we inspect our HTML in the Elements tab of Chrome's dev tools (View->Developer->Developer Tools), we'll see the following: - -![](https://i.imgur.com/Z4vlaZA.png) - -## Basic elements - -We can draw elements in our `` element by adding a variety of predefined tags as child elements of the ``. This is just like in HTML where we add `
`, ``, and `` tags inside the `` tag. There are many tags like ``, ``, and `` that we'll explore in a bit. Here's just one example: - -```html - - - - - - - - - - -``` - -Note that we can't see the circle because it doesn't have a radius, as shown in the below screenshot: - -![](https://i.imgur.com/yUXwBPK.png) - -We'll talk about this more later, but for now, if we want to see the circle, we can add a special attribute that all `` elements take: - -```html - -``` - -This tells the browser to give the circle a radius of 50px: - -![](https://i.imgur.com/vG8iQID.png) - -At the moment though, we only see the bottom right quarter of the ``. This is because the center of the `` is being drawn at the very upper left corner of the ``, and the rest of it is being clipped outside the ``. We can change this by changing the position of the circle, which we'll do next. - -## Positioning an Element - -The `` tag is an inline element, like an image (as opposed to a block element like a `
`). Elements within the `` are positioned similar to photoshop, with a set of coordinates which follow the form `(x,y)`. An example of this could be `(10,15)` which translates to `x=10` and `y=15`. This is different from HTML, where elements are laid out relative to each other. Some important things to keep in mind: - - - the point `(0,0)` is the top left of the `` element - - as y values increase, the point moves vertically down the `` element - - don't confuse this with a typical coordinate system that has `(0,0)` at the **bottom** left with a point moving up as y increases in value - - ![](http://res.cloudinary.com/dno0vkynk/image/upload/v1475392871/SVGCoordinateSystem.png) - - - we can use negative x/y values - - -x moves left - - -y moves up - -Let's adjust the position of our circle in our previous section by adjusting `cx` and `cy` values (the x and y values for the center of the element): - -```html - - - - - - - - - - -``` - -Now we see the full circle: - -![](https://i.imgur.com/JWOr8xH.png) - -## Styling Elements - -The appearance of any tag inside an `` can be styled with the following attributes (below are the attributes with example values): - -- `fill=red` or `fill=#ff0000` will alter the color of the shape -- `stroke=red` or `stroke=#ff0000` will alter stroke color. Stroke is a line that surrounds each element -- `stroke-width=4` will adjust the width of the stroke -- `fill-opacity=0.5` will adjust the transparency of the fill color -- `stroke-opacity=0.5` will adjust the transparency of the stroke color -- `transform = "translate(2,3)"` will translate the element by the given x,y values -- `transform = "scale(2.1)"` will scale the size of the element by the given proportion (e.g. 2.1 times a s big) -- `transform = "rotate(45)"` will rotate the element by the given number of degrees - -Let's style the circle we positioned previously: - -```html - -``` - -Now we get this: - -![](https://i.imgur.com/FPpHp1v.png) - -Note that the stroke in the image above is getting clipped. That's because the stroke is create outside the element. If we wanted to see the full stroke, we can resize the circle: - -```html - -``` - -Now we get: - -![](https://i.imgur.com/GO7E8v9.png) - - -Styling can also be done with CSS. The following steps will tell you how to style your `` element with CSS: - -1. Create an external `app.css` file in the same folder as your `index.html` file with the following contents: - - ```css - circle { - fill:red; - stroke:blue; - stroke-width:3; - fill-opacity:0.5; - stroke-opacity:0.1; - transform:rotate(45deg) scale(0.4) translate(155px, 1px); - r:50px; - } - ``` - -1. Link the file in the `` tag of `index.html`: - - ```html - - - - ``` - -1. Lastly, remove our previous inline styling that we had on our `` tag: - - ```html - - ``` - -Now we get this: - -![](https://i.imgur.com/E1unbtu.png) - -Note that I've hovered over the element in the dev tools to show that the element has been rotated 45 degrees. That's what the blue box is. - -## Important SVG elements - -To demo each element, we'll use the following code as a starting point and then add each element inside the `` tag: - -```html - - - - - - - - - -``` - -Let us now move on to each element. Note that you can write each tag in the form `` as we did with `` previously, or the self-closing form ``, which you will see next with ``. - -### Circle - -Circles have the following attributes: - -- `r` radius -- `cx` x position -- `cy` y position - -```xml - -``` - -![](https://i.imgur.com/yQAjbJg.png) - -### Line - -Lines have the following attributes: - -- `x1` starting x position -- `y1` starting y position -- `x2` ending x position -- `y2` ending y position - -```xml - - -``` - -![](https://i.imgur.com/GuxSy1n.png) - -### Rectangle - -Rectangles have the following attributes: - -- `x` x position of top left -- `y` y position of top left -- `width` width -- `height` height - -```xml - -``` - -![](https://i.imgur.com/vVmIbOP.png) - -### Ellipse - -An ellipse has the following attributes: - -- `cx` x position -- `cy` y position -- `rx` x radius -- `ry` y radius - -```xml - -``` - -![](https://i.imgur.com/Y1YdlWE.png) - -### Polygon - -Polygons have the following attributes: - - - `points` set of coordinate pairs - - each pair is of the form `x,y` - -```xml - -``` - -![](https://i.imgur.com/0KqlNvR.png) - -### Polyline - -Polyline is a series of connected lines. It can have a fill like a polygon, but won't automatically rejoin itself - -```xml - -``` - -![](https://i.imgur.com/A1Xv1ex.png) - -### Text - -The content of the tag is the text to be displayed. It has the following attributes: - -- `x` x position of top left corner of the element -- `y` y position of top left corner of the element - -```xml -I love SVG! -``` - -You can use font-family and font-size CSS styling on this element - -### Group - -- This element has no special attributes, so use transform on it. -- You can put multiple elements inside it and all of its positioning and styling will apply to its children -- It's good for moving many elements together as one - -```xml - -``` - -### Bezier Curves - -What if we want to draw complex organic shapes? To do this, we'll need to use paths. First, though, to understand paths, you have to first understand bezier curves. - -#### Cubic Bezier Curves - -There are two types of Bezier curves: - -- [Bezier curves](http://blogs.sitepointstatic.com/examples/tech/svg-curves/cubic-curve.html) -- [Quadratic Bezier curves](http://math.hws.edu/eck/cs424/notes2013/canvas/bezier.html) - -Each curve is made up of four points: - -- start point -- end point -- starting control point -- ending control point - -The start/end point are where the curve starts and ends. The control points define the shape of the curve. It's easiest to conceptualize if with the following diagram: - -![](https://i.imgur.com/QAxI2AD.png) - -As we manipulate the control points, we can see how the shape of the curve is affected: - -![](https://i.imgur.com/M9S1sW7.png - -Here's another example: - -![](https://i.imgur.com/Dw1z4hl.png) - -You can even join multiple bezier curves together: - -![](https://i.imgur.com/A67aIgW.png) - -#### Smooth Cubic Bezier Curves - -Smooth Cubic Bezier curves are just a way to simplify some cubic bezier curves when they're joined together. Take a look at the two control points in the red square below: - -![](https://i.imgur.com/fqBRIDg.png) - -The point in the lower left of the square is the end control point of the first curve. The point in the upper right of the square is start control point of the second curve. - -Note that the two points are reflections of each other around the central black dot which is the end point of the first curve and the start point of the second curve. The two points are exactly 180 degrees in opposite directions, and they have the same distance from that central point. - -In scenarios like this, where the start control point of one curve is a reflection of the end control point of the previous curve, we can skip stating the start control point of the second curve. Instead, we let the browser calculate it, based on the end control point of the first curve. - -![](https://i.imgur.com/8RVJONC.png) - -We can also omit the start point since the browser knows it will be the same as the end point of the previous curve. In summary, to define that second curve, we only need two points: - -- the end point -- the end control point - -#### Quadratic Bezier Curve - -Another situation where we can simplify defining a bezier curve is where the start control point and end control point are the same - -![](https://i.imgur.com/xlByyu2.png) - -Here we can define the curve with just three points: - -- the start point -- the end point -- one single control point which acts as both start control point and end control point - -#### Smooth Quadratic Bezier Curve - -The final situation where we can simplify defining a bezier curve is where we have a quadratic bezier curve (one single control point) that is a reflection of the end control point of a previous curve: - -![](https://i.imgur.com/Uw9zVrs.png) - -In this situation, the browser knows the start point of the curve (the end point of the previous curve), and it can calculate the single control point needed (since it is a quadratic bezier curve) based on the end control point of the previous curve. This is a smooth quadratic bezier curve, and you only need one point to define it: - -- the end point - -### Drawing a path - -Now that we understand bezier curves, we can use them in our SVGs with `` elements - -[Documentation](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths) - -These tags take a `d` attribute which stands for a set of drawing commands. The value of this attribute is any combination of the below: - -- M = moveto: move the drawing point to the given coordinates - - M x y -- L = lineto: draw a line from the previous point in the `d` command to the point given - - L x y -- C = curveto: draw a curve from the previous point in the `d` command to the point given with the given control points - - C x1 y1, x2 y2, x y - - first pair is first control point - - second pair is second control point - - last pair is final ending point of curve -- S = smooth curveto - - S x2 y2, x y - - follows another curve - - uses reflection of x2 y2 of previous S or C command for x1 y1 -- Q = quadratic Bézier curve - - Q x1 y1, x y - - uses one control point for start and end controls (x1, y1) -- T = smooth quadratic Bézier curveto - - T x y - - follows another curve - - uses reflection of previous quadratic curve's control point as its control point -- Z = closepath: draw a line from the previous point in the `d` command to the first point in the `d` command - -**Note:** All of the commands above can also be expressed with lower case letters. Capital letters means absolutely positioned, lower case letters mean the all points are expressed relative to the previous point in the `d` command. - -```xml - -``` - -![](https://i.imgur.com/u4zFUUh.png) - -```xml - -``` - -![](https://i.imgur.com/6Q07cJN.png) - -```xml - -``` - -![](https://i.imgur.com/1WAjDUD.png) - -#### Arcs - -An arc is a command that you can add to a path that will draw part of an ellipse. To do this, we first begin with only two points: - -![two point](http://blog.arcbees.com/wp-content/uploads/svg-arc2.png) - -For any two points, there are only two ellipses with the same width/height and rotation that contain both points. In the image above, try to imagine moving the ellipses around without rotating or scaling them. As soon as you do, they loose contact with at least one of the two given points. One point might be on the ellipse, but the other won't be. - -We can use this information to draw any of the four colored arcs shown in the image above. - -Make the following code part of the `d` attribute's value on a `` element. - -``` -A rx ry x-axis-rotation large-arc-flag sweep-flag x y -``` - -- `A` - create an arc draw command -- `rx` - x radius of both ellipses (in px) -- `ry` - y radius of both ellipses (in px) -- `x-axis-rotation` - rotate both ellipses a certain number of degrees -- `large-arc-flag` - whether or not to travel along the arc that contains more than 180 degrees (1 to do so, 0 to not do so) -- `sweep-flag` - whether or not to move along the arc that goes clock-wise (1 to do so, 0 to not do so) -- `x` - destination x value (in px) -- `y` - destination y value (in px) - -The `large-arc-flag` determines whether to make an arc that is greater than 180 degrees. Here's an example without it (note, the red shows the arc drawn, while the green arcs are other possible arcs that could be drawn using a combination of `large-arc-flag` and `sweep-flag`): - -![](https://i.imgur.com/fiX7Hmj.png) - -Note, it chose one of the two smaller arcs. Here's an example with the `large-arc-flag` set: - -![](https://i.imgur.com/frBj6zL.png) - -Note, it chose on of the two larger arcs. - -In the previous example, for both situations where the `large-arc-flag` was set or not set, there was one other arc that could have been taken. To determine which of those two arcs to take, we use the `sweep-flag`, which determines whether to travel clock-wise or not from starting point to ending point. Here's an example with the `large-arc-flag` set, but without the `sweep-flag` set: - -![](https://i.imgur.com/AmNPzYp.png) - -Note we move in a counter clock-wise motion from start to end (left to right). If we set the `sweep-flag`, we travel in a clock-wise motion: - -![](https://i.imgur.com/0Z8RTVE.png) - -Here are all the possible combinations for `sweep-flag` and `large-arc-flag`: - -![](https://www.w3.org/TR/SVG/images/paths/arcs02.svg) - -Here's example code for a `path` that uses an arc in its `d` attribute: - -```html - -``` - -Here's what it looks like: - -![](https://i.imgur.com/tpEXk2Z.png) - -Play with the different kinds of arc values here: http://codepen.io/lingtalfi/pen/yaLWJG - -### Documentation - -https://developer.mozilla.org/en-US/docs/Web/SVG/Element - -## Conclusion - -Now that we've covered the basics of SVG, we're ready to continue on to learn how D3 can be used to modify these elements.