Visualize Data with a Scatterplot Graph
This is a guided walkthrough to produce a scatterplot diagram with some data about doping in bicycle racing, and use d3.js to render it.
Notes
1. Project Setup
Looking at what we need to create and talking through the structure of the task.
- Create skeleton page, set title
- Import D3
- Create and link script page
- Create and link stylesheet
- Create svg canvas with id
- Set body display and svg bg
1
2
3
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg id = 'canvas'></svg>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
*{
font-family: Roboto, sans-serif;
}
body{
background-image: radial-gradient( circle farthest-corner at 92.3% 71.5%, rgba(83,138,214,1) 0%, rgba(134,231,214,1) 90% );
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100%;
}
svg{
background-color: #1a746f;
border-radius: 5px;
box-shadow: 0px 3px 15px rgba(0,0,0,0.2);
border: 2px solid cyan;
padding: 10px;
}
2. Creating variables and functions
- url points to JSON File
- request is XMLHTTPRequest used for importing
- data will store array of data
- xScale will be a scale used to create the x-axis and also to place the dots horizontally
- yScale is used to create y-axis, as well as place the dots vertically
- canvasDimension shows svg area
- padding will be padding
- svg is a d3 selection of the svg area we created for quick access
- drawCanvas() draws the svg canvas with width and height attributes
- generateScales() generates the scales and assigns them to the variables
- drawGraph() will draw the points
- generateAxes() will draw the X and Y axis on the canvas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//Fetch data to API
let url = 'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/cyclist-data.json';
let method = 'GET';
let request = new XMLHttpRequest();
//API data Array
let data = [];
let years = [];
let time = [];
//Create Scale Variable
let xScale;
let yScale;
// let xAxisScale;
// let yAxisScale;
//data format variable
var yearFormat = d3.format('d');
var timeFormat = d3.timeFormat('%M:%S');
//Padding Variable
let padding = {
width: 80,
height: 60,
};
//Canvas Dimension
let canvasDimension = {
width: 800,
height: 550,
};
//Create SVG element on html
let svg = d3.select('svg');
3. Fetching JSON Data
- Open request
- Set onload, parse the responsetext and store as data
- Set values to .data field which contains the array
- Log this
- Send the request and see if we have the array
- Draw the canvas first
- Then generate the scales
- Then draw the bars
- Then generate the axes
Get JSON with the Javascript XMLHTTPRequest Method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//Fetch data to API
let url = 'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/cyclist-data.json';
let method = 'GET';
let request = new XMLHttpRequest();
//API data Array
let data = [];
let years = [];
let time = [];
//Create Scale Variable
let xScale;
let yScale;
// let xAxisScale;
// let yAxisScale;
//data format variable
let yearFormat = d3.format('d');
let timeFormat = d3.timeFormat('%M:%S');
//Padding Variable
let padding = {
width: 80,
height: 60,
};
//Canvas Dimension
let canvasDimension = {
width: 800,
height: 550,
};
//Create SVG element on html
let svg = d3.select('svg');
//Main
request.open(method, url, true);
request.onload = function(){
data = JSON.parse(request.responseText);
drawCanvas();
generateScale();
generateAxis();
drawGraph();
drawHint();
}
request.send();
4. Add a title with a corresponding id
- Add title tag, set id, and inner text
- Position title tag
1
2
3
<div id="title">Doping in Professional Bicycle Racing
<div id="title-lead">35 Fastest times up Alpe d'Huez</div>
</div>
5. Creating an x-axis that has a corresponding id
- Set xAxis to axisBottom with the xAxisScale
- Append svg with a g
- Call the xAxis
- Set it’s id as required
- Push this down by (height-padding) on y
1
2
3
4
5
6
7
8
9
10
11
12
13
14
xScale = d3.scaleLinear()
.domain([d3.min(data, (d) => {
return d.Year;
}) - 1 , d3.max(data, (d) => {
return d.Year;
}) + 1])
.range([padding.width, canvasDimension.width - padding.width]);
let xAxis = d3.axisBottom(xScale)
.tickFormat(yearFormat);
svg.append('g')
.attr('id', 'x-axis')
.call(xAxis)
.attr('transform', 'translate(0, ' + (canvasDimension.height - padding.height) + ')');
6. Create a y-axis that has a corresponding id
- Set xAxis to axisBottom with the xAxisScale
- Append svg with a g
- Call the xAxis
- Set it’s id as required
- Push this down by (height-padding) on y
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yScale = d3.scaleTime()
.domain([d3.min(data, (d) => {
return new Date(d.Seconds * 1000);
}), d3.max(data, (d) => {
return new Date(d.Seconds * 1000);
})])
.range([padding.height, canvasDimension.height - padding.height]);
let yAxis = d3.axisLeft(yScale)
.tickFormat(timeFormat);
//yAxis
svg.append('g')
.attr('id', 'y-axis')
.call(yAxis)
.attr('transform', 'translate(' + (padding.width) + ', 0)');
7. Creating dots that each have a class of dot representing the data being plotted
- Select all circle elements in svg
- Bind this to the data set values
- Call enter() to specify what to do for array items where there are no circles
- append() with a new circle element
- Give a class attribute of “dot” like specified
- Give an “r” attribute of 5 to give the circles a radius
1
2
3
4
5
6
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('class', 'dot')
.attr('r', '5');
8. Adding properties to each dot in range of the actual data and in the correct data format. Integers or date objects for x-values and minutes(date objects) for y-values
- Set attribute “data-xvalue” to return d.Year
- Set attribute “data-yvalue”to return a new Date with the seconds field (since it asks for a date). Multiply by 1000 to convert from seconds to ms, since date objects require milliseconds.
1
2
3
4
5
6
7
8
9
10
11
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('class', 'dot')
.attr('data-xvalue', (d) => {
return d.Year;
})
.attr('data-yvalue', (d) => {
return new Date(d.Seconds * 1000);
})
9. Aligning the x-values and its corresponding dot with the corresponding point on the x-axis
- Start off by creating the xScale
- Since we are working with years (a number), we cal just use scaleLinear()
- Domain will be the minimum Year field to the maximum Year field from the values array
- Range will be the lowest x value (padding) to the highest x value (width-padding) we want the dots to be at
- Set a cx attribute on the dot to return xScale, passing in the Year field from the item since this is our domain
- Adjust the domain by taking away a year from the min and adding a year to the max to make the dots fit in nicer
1
2
3
4
5
6
7
8
9
10
11
xScale = d3.scaleLinear()
.domain([d3.min(data, (d) => {
return d.Year;
}) - 1 , d3.max(data, (d) => {
return d.Year;
}) + 1])
.range([padding.width, canvasDimension.width - padding.width]);
.attr('cx', (d) => {
return xScale(d.Year);
})
10. Aligning the y-values and its corresponding dot with the corresponding point on the y-axis
- Start off by creating the yScale
- Since we are working with time, we should use scaleTime()
- scaleTime takes in dates so when building the domain use the minimum Seconds field from each item. Create a new date object with this, but multiply by 1000 before to convert from seconds to milliseconds
- Range will be the lowest y value (height-padding) and highest y value (padding), inverted since positive y is downwards in d3
- Set a cy attribute on the dot to return yScale, passing in a new date using the Seconds field from the item, multiplying it by 1000 to convert to ms first
1
2
3
4
5
6
7
8
9
10
11
yScale = d3.scaleTime()
.domain([d3.min(data, (d) => {
return new Date(d.Seconds * 1000);
}), d3.max(data, (d) => {
return new Date(d.Seconds * 1000);
})])
.range([padding.height, canvasDimension.height - padding.height]);
.attr('cy', (d) => {
return yScale(d.Seconds * 1000);
})
add attributes to circle elements
11. Adjusting the range of the y-values within the range of the actual y-axis data and adding ticks labels on the y-axis with %M:%S time format
- Pass the yScale we created to the y axis
- Call tickFormat() on the y axis
- Give a d3.timeFormat() with Ms and Ss to specify how the values should be formatted
1
2
yAxis = d3.axisLeft(yScale)
.tickFormat(d3.timeFormat('%M:%S'))
12. Adjusting the range of the x-values within the range of the actual x-axis data and adding ticks labels on the x-axis that show the year
- Pass the xScale we created to the x axis
- Call tickFormat() on the x axis
- Give it a d3.format() with d to represent decimals. This removes the percentages from the years
1
2
xAxis = d3.axisBottom(xScale)
.tickFormat(d3.format('d'))
13. Changing the dot color
- Add a fill attribute to the circles to return either orange or light green depending on whether URL exists. URL means there is a doping allegations
add link to changes styles based on Data
14. Creating a legend containing descriptive text
- Add div with the id of “legend” under the SVG area, explaining the axes and the color codes
1
2
3
4
5
6
7
8
9
10
11
12
<svg id = 'canvas'>
<div id="legend" class="legend">
<div class="text">
No doping allegations
<div id="noDop" class="box"></div>
</div>
<div class="text">
Riders with doping allegations
<div id="withDop" class="box"></div>
</div>
</div>
</svg>
15. Creating a tooltip and adding some properties to it
- Create a div with the id of “tooltip” under the svg area
- Set the variable tooltip to a d3 selection of tooltip
- Set the css of the tooltip div to visibility hidden, width and height auto
- Add a mouseover event to circles to make the tooltips visible
- If doping is not empty set the text of the tooltip to show the year, name, time, and allegation details
- If doping is empty set the text of the tooltp to show the year, name, time and that there are no allegations
- Add a mouseout event to the circles to make the tooltips hidden
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var tooltip = d3.select('body')
.append('div')
.attr('id', 'tooltip')
.attr('class', 'tooltip');
.on('mouseover', (d, i) => {
tooltip.transition()
.duration(200)
.attr('data-year', d.Year)
.style('opacity', 0.6);
//Description of the content
tooltip.html("" + "Year: " + d.Year + "<br/><br/>" +
"<strong>Time: </strong>" + d.Time + "<br/><br/>" +
"<strong>Name: </strong>" + d.Name + "<br/><br/>" +
"<strong>Nationanlity: </strong>" + d.Nationality + "<br/><br/>" +
"<br/>" + "<small>" + d.Doping + "</small>" + "<br/>");
tooltip.style("left", (d3.event.pageX+10) + 'px')
.style("top", (d3.event.pageY-30) + 'px');
// Alternative method to set an attribute with the query selector
// document.querySelector('#tooltip').setAttribute('data-year', d.Year);
})
.on('mouseout', (d, i) => {
tooltip.transition(100)
.style('opacity', 0);
});
16. Final touches and CSS styling
Source code
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Visualize data with a Scatter Plot Graph</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="title">Doping in Professional Bicycle Racing
<div id="title-lead">35 Fastest times up Alpe d'Huez</div>
</div>
<svg id = 'canvas'>
<div id="legend" class="legend">
<div class="text">
No doping allegations
<div id="noDop" class="box"></div>
</div>
<div class="text">
Riders with doping allegations
<div id="withDop" class="box"></div>
</div>
</div>
</svg>
</body>
<script defer src="./script.js"></script>
</html>
style.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap');
*{
font-family: Roboto, sans-serif;
}
body{
background-image: radial-gradient( circle farthest-corner at 92.3% 71.5%, rgba(83,138,214,1) 0%, rgba(134,231,214,1) 90% );
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100%;
}
#title{
font-size: 32px;
margin: 45px 10px 45px 10px;
/* color: #5AA364; */
}
#title-lead{
font-size: 16px;
text-align: center;
color: #5AA364;
}
svg{
/* background-color: #DEF2F1; */
background-color: #1a746f;
border-radius: 5px;
box-shadow: 0px 3px 15px rgba(0,0,0,0.2);
border: 2px solid cyan;
padding: 10px;
}
.tooltip {
position: absolute;
padding: .5rem;
text-align: center;
font: 'Roboto', sans-serif;
border-radius: .3rem;
border: solid 1px green;
opacity: 0;
background: #eaeaea;
box-shadow: 4px 4px 6px rgba(0, 0, 0, 0.4);
display: flex;
font-size: 14px;
align-items: center;
justify-content: center;
flex-direction: column;
}
.dot:hover {
fill: aqua;
}
.tooltip::before {
color:gray;
width: max-content;
max-width: 100%;
}
.legend {
position: absolute;
font-size: 12px;
text-align: right;
top: 50%;
right: 25%;
padding: 20px;
}
.legend .text{
margin: 15px;
padding-right: 25px;
min-width: 200px;
position: relative;
}
.legend .box {
position: absolute;
height: 15px;
width: 15px;
display: inline-block;
top: 0;
right: 0;
}
.legend #noDop {
background-color: orange;
}
.legend #withDop {
background-color: red;
}
.info{
font-size: 12px;
color: #5AA364;
}
script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
//Fetch data to API
let url = 'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/cyclist-data.json';
let method = 'GET';
let request = new XMLHttpRequest();
//API data Array
let data = [];
let years = [];
let time = [];
//Create Scale Variable
let xScale;
let yScale;
// let xAxisScale;
// let yAxisScale;
//data format variable
let yearFormat = d3.format('d');
let timeFormat = d3.timeFormat('%M:%S');
//Padding Variable
let padding = {
width: 80,
height: 60,
};
//Canvas Dimension
let canvasDimension = {
width: 800,
height: 550,
};
//Create SVG element on html
let svg = d3.select('svg');
//Drawing Canvas
function drawCanvas(){
svg.attr('width', canvasDimension.width);
svg.attr('height', canvasDimension.height);
}
//Generate Scale
function generateScale(){
//xScale for data
xScale = d3.scaleLinear()
.domain([d3.min(data, (d) => {
return d.Year;
}) - 1 , d3.max(data, (d) => {
return d.Year;
}) + 1])
.range([padding.width, canvasDimension.width - padding.width]);
//yScale for Data
yScale = d3.scaleTime()
.domain([d3.min(data, (d) => {
return new Date(d.Seconds * 1000);
}), d3.max(data, (d) => {
return new Date(d.Seconds * 1000);
})])
.range([padding.height, canvasDimension.height - padding.height]);
}
//Generate Axis with scale Data
function generateAxis(){
//Generate Axis
let xAxis = d3.axisBottom(xScale)
.tickFormat(yearFormat)
let yAxis = d3.axisLeft(yScale)
.tickFormat(timeFormat);
//Render Axis
//xAxis
svg.append('g')
.attr('id', 'x-axis')
.call(xAxis)
.attr('transform', 'translate(0, ' + (canvasDimension.height - padding.height) + ')');
//yAxis
svg.append('g')
.attr('id', 'y-axis')
.call(yAxis)
.attr('transform', 'translate(' + (padding.width) + ', 0)');
}
//Draw a legend for the graph
function drawHint(){
let info = d3.select('svg')
.append('text')
.attr('class', 'info')
.attr('x', 650)
.attr('y', 548)
.text('Made by Venom Cocytus')
let hint = d3.select('svg')
.append('text')
.attr('class', 'info')
.style('font-size', 17)
.attr('transform', 'rotate(-90)')
.attr('x', -325)
.attr('y', 20)
.text('Times in minutes')
}
//Draw Scatter-plot
function drawGraph(){
//Create a tooltip
let tooltip = d3.select('body')
.append('div')
.attr('id', 'tooltip')
.attr('class', 'tooltip');
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('class', 'dot')
.attr('data-xvalue', (d) => {
return d.Year;
})
.attr('data-yvalue', (d) => {
return new Date(d.Seconds * 1000);
})
.attr('fill', (d) => {
if(d.Doping != ''){
return 'red';
}else{
return 'orange';
}
})
.attr('cx', (d) => {
return xScale(d.Year);
})
.attr('cy', (d) => {
return yScale(d.Seconds * 1000);
})
.attr('r', '3')
.attr('stroke', 'black')
.on('mouseover', (d, i) => {
tooltip.transition()
.duration(200)
.attr('data-year', d.Year)
.style('opacity', 0.6);
//Description of the content
tooltip.html("" + "Year: " + d.Year + "<br/><br/>" +
"<strong>Time: </strong>" + d.Time + "<br/><br/>" +
"<strong>Name: </strong>" + d.Name + "<br/><br/>" +
"<strong>Nationanlity: </strong>" + d.Nationality + "<br/><br/>" +
"<br/>" + "<small>" + d.Doping + "</small>" + "<br/>");
tooltip.style("left", (d3.event.pageX+10) + 'px')
.style("top", (d3.event.pageY-30) + 'px');
// Alternative method to set an attribute with the query selector
// document.querySelector('#tooltip').setAttribute('data-year', d.Year);
})
.on('mouseout', (d, i) => {
tooltip.transition(100)
.style('opacity', 0);
})
}
//Main
request.open(method, url, true);
request.onload = function(){
data = JSON.parse(request.responseText);
//Calling Function
drawCanvas();
generateScale();
generateAxis();
drawGraph();
drawHint();
}
request.send();
Interactive Frame
Links
Getting The Code On CodePen
The entire codePen containing all the code mentioned in this post can be found here.
Getting The Code On Github
The entire folder containing all the code mentioned in this post can be found via this link.
Just bear in mind that you will need to install all the dependencies. If you find any issues with the code, feel free to either comment down below or raise an issue on Github.
Add me on LinkedIn
Don’t hesitate to follow me on linkedIn or o other social network to encourage me to do more posts on IT.
Comments powered by Venom Cocytus.