parent
efaecf519e
commit
eaefe9b452
@ -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
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg></svg>
|
||||
<script src="https://d3js.org/d3.v5.min.js"></script>
|
||||
<script src="app.js" charset="utf-8"></script>
|
||||
</body>
|
||||
</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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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):
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
## 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;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
## 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 `<svg>` element from clipping the axes. Change the css for `svg` in `app.css`:
|
||||
|
||||
```css
|
||||
svg {
|
||||
overflow: visible;
|
||||
}
|
||||
```
|
||||
|
||||
Our result:
|
||||
|
||||

|
||||
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
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 */
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
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 */
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@ -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('<span>hi</span>'); //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/
|
||||
@ -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 `<g>` elements:
|
||||
|
||||
- One to contain the nodes (people - circles)
|
||||
- One to contain the links (relationships - lines)
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<script src="https://d3js.org/d3.v5.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<svg>
|
||||
<g id="nodes"></g>
|
||||
<g id="links"></g>
|
||||
</svg>
|
||||
<script src="app.js" charset="utf-8"></script>
|
||||
</body>
|
||||
</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
|
||||
<head>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
<script src="https://d3js.org/d3.v5.min.js"></script>
|
||||
</head>
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@ -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:
|
||||
|
||||

|
||||
|
||||
But with SVG, a set of succinct drawing directions are stored. For example, the drawing command for a circle is:
|
||||
|
||||
```html
|
||||
<circle r=50><circle>
|
||||
```
|
||||
|
||||
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
|
||||
<circle r=100><circle>
|
||||
```
|
||||
|
||||
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
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
````
|
||||
@ -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 `<path>` using a projection and the GeoJSON data
|
||||
|
||||
In this section we'll generate `<path>` 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
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
|
||||
<script src="https://cdn.rawgit.com/mahuntington/mapping-demo/master/map_data3.js" charset="utf-8"></script>
|
||||
</head>
|
||||
<body>
|
||||
<svg></svg>
|
||||
<script src="app.js" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The only thing different from the setup that we've used in previous chapters is this line:
|
||||
|
||||
```html
|
||||
<script src="https://cdn.rawgit.com/mahuntington/mapping-demo/master/map_data3.js" charset="utf-8"></script>
|
||||
```
|
||||
|
||||
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 `<path>` using a projection and the GeoJSON data
|
||||
|
||||
Now that we have our projection, we're going to generate `<path>` 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:
|
||||
|
||||

|
||||
|
||||
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):
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@ -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
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<script src="https://d3js.org/d3.v5.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<svg>
|
||||
<g></g>
|
||||
</svg>
|
||||
<script src="app.js" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Set Config Vars
|
||||
|
||||
At the bottom of the `<body>` 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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
**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));
|
||||
```
|
||||
|
||||

|
||||
|
||||
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)
|
||||
```
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
## Generate an arc creating function
|
||||
|
||||
The paths have fill colors, but no shape. If you'll recall, `<path>` 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));
|
||||
```
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
@ -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 `<svg>` tag inside a basic HTML page. Let's create an `index.html` file and add the following to it:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<svg></svg>
|
||||
</body>
|
||||
</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:
|
||||
|
||||

|
||||
|
||||
## Basic elements
|
||||
|
||||
We can draw elements in our `<svg>` element by adding a variety of predefined tags as child elements of the `<svg>`. This is just like in HTML where we add `<div>`, `<a>`, and `<img>` tags inside the `<body>` tag. There are many tags like `<circle>`, `<rect>`, and `<line>` that we'll explore in a bit. Here's just one example:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<svg>
|
||||
<circle></circle>
|
||||
</svg>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Note that we can't see the circle because it doesn't have a radius, as shown in the below screenshot:
|
||||
|
||||

|
||||
|
||||
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 `<circle>` elements take:
|
||||
|
||||
```html
|
||||
<circle r=50></circle>
|
||||
```
|
||||
|
||||
This tells the browser to give the circle a radius of 50px:
|
||||
|
||||

|
||||
|
||||
At the moment though, we only see the bottom right quarter of the `<circle>`. This is because the center of the `<circle>` is being drawn at the very upper left corner of the `<svg>`, and the rest of it is being clipped outside the `<svg>`. We can change this by changing the position of the circle, which we'll do next.
|
||||
|
||||
## Positioning an Element
|
||||
|
||||
The `<svg>` tag is an inline element, like an image (as opposed to a block element like a `<div>`). Elements within the `<svg>` 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 `<svg>` element
|
||||
- as y values increase, the point moves vertically down the `<svg>` 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<svg>
|
||||
<circle r=50 cx=50 cy=50></circle>
|
||||
</svg>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Now we see the full circle:
|
||||
|
||||

|
||||
|
||||
## Styling Elements
|
||||
|
||||
The appearance of any tag inside an `<svg>` 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
|
||||
<circle r=50 cx=50 cy=50 fill=red stroke=blue stroke-width=5></circle>
|
||||
```
|
||||
|
||||
Now we get this:
|
||||
|
||||

|
||||
|
||||
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
|
||||
<circle r=45 cx=50 cy=50 fill=red stroke=blue stroke-width=5></circle>
|
||||
```
|
||||
|
||||
Now we get:
|
||||
|
||||

|
||||
|
||||
|
||||
Styling can also be done with CSS. The following steps will tell you how to style your `<svg>` 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 `<head>` tag of `index.html`:
|
||||
|
||||
```html
|
||||
<head>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
```
|
||||
|
||||
1. Lastly, remove our previous inline styling that we had on our `<circle>` tag:
|
||||
|
||||
```html
|
||||
<circle></circle>
|
||||
```
|
||||
|
||||
Now we get this:
|
||||
|
||||

|
||||
|
||||
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 `<svg>` tag:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<svg width=800 height=600>
|
||||
</svg>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Let us now move on to each element. Note that you can write each tag in the form `<element></element>` as we did with `<circle></circle>` previously, or the self-closing form `<element/>`, which you will see next with `<circle/>`.
|
||||
|
||||
### Circle
|
||||
|
||||
Circles have the following attributes:
|
||||
|
||||
- `r` radius
|
||||
- `cx` x position
|
||||
- `cy` y position
|
||||
|
||||
```xml
|
||||
<circle r="50" cx="200" cy="300"/>
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Line
|
||||
|
||||
Lines have the following attributes:
|
||||
|
||||
- `x1` starting x position
|
||||
- `y1` starting y position
|
||||
- `x2` ending x position
|
||||
- `y2` ending y position
|
||||
|
||||
```xml
|
||||
<line x1="0" y1="0" x2="100" y2="100"/> <!-- this element won't be visible because it doesn't have a stroke -->
|
||||
<line x1="0" y1="0" x2="100" y2="100" stroke="purple"/>
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Rectangle
|
||||
|
||||
Rectangles have the following attributes:
|
||||
|
||||
- `x` x position of top left
|
||||
- `y` y position of top left
|
||||
- `width` width
|
||||
- `height` height
|
||||
|
||||
```xml
|
||||
<rect x="50" y="20" width="150" height="150"/>
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Ellipse
|
||||
|
||||
An ellipse has the following attributes:
|
||||
|
||||
- `cx` x position
|
||||
- `cy` y position
|
||||
- `rx` x radius
|
||||
- `ry` y radius
|
||||
|
||||
```xml
|
||||
<ellipse cx="200" cy="80" rx="100" ry="50"/>
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Polygon
|
||||
|
||||
Polygons have the following attributes:
|
||||
|
||||
- `points` set of coordinate pairs
|
||||
- each pair is of the form `x,y`
|
||||
|
||||
```xml
|
||||
<polygon points="200,10 250,190 160,210" />
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Polyline
|
||||
|
||||
Polyline is a series of connected lines. It can have a fill like a polygon, but won't automatically rejoin itself
|
||||
|
||||
```xml
|
||||
<polyline points="20,20 40,25 60,40 80,120 120,140 200,180" stroke="blue" fill="none"/>
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
<text x="0" y="15">I love SVG!</text>
|
||||
```
|
||||
|
||||
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
|
||||
<g transform = "translate(20,30) rotate(45) scale(0.5)"></g>
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||

|
||||
|
||||
As we manipulate the control points, we can see how the shape of the curve is affected:
|
||||
|
||||

|
||||
|
||||
You can even join multiple bezier curves together:
|
||||
|
||||

|
||||
|
||||
#### 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:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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 `<path>` 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
|
||||
<path d="M150 0 L75 200 L225 200 Z" stroke="black" fill="transparent"/>
|
||||
```
|
||||
|
||||

|
||||
|
||||
```xml
|
||||
<path d="M0 70 C 0 120, 50 120, 50 70 S 100 20, 100 70" stroke="black" fill="transparent"/>
|
||||
```
|
||||
|
||||

|
||||
|
||||
```xml
|
||||
<path d="M0 100 Q 50 50, 100 100 T 200 100 Z" stroke="black" fill="transparent"/>
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### 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:
|
||||
|
||||

|
||||
|
||||
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 `<path>` 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`):
|
||||
|
||||

|
||||
|
||||
Note, it chose one of the two smaller arcs. Here's an example with the `large-arc-flag` set:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
Here are all the possible combinations for `sweep-flag` and `large-arc-flag`:
|
||||
|
||||

|
||||
|
||||
Here's example code for a `path` that uses an arc in its `d` attribute:
|
||||
|
||||
```html
|
||||
<path d="M10 10 A 50 50 0 0 0 50 10" stroke="black" fill="transparent"/>
|
||||
```
|
||||
|
||||
Here's what it looks like:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
Loading…
Reference in new issue