by Josue Reyes and Lihan (Neil) Zhu
This tutorial will walk through the steps of building an interactive network visualization of different types of beer and their relationships to each other.
We used Jose Christian's code to build our visualization. It's a great starting point if you're interested in better understanding how a very simple network is created in D3.
Take a look at the final project and explore its functionality. The beer visualization illustrates how most beers descend two common parents: Ales and Lagers. The nodes represent different beer families. You can click on a node to see which glasses that beer should be served in. You can also hover over each glass to see which beers should be served in that particular glass.
We'll start out with some code and work our way towards the final project.
You don't need to be an expert in web development to get started with a network visualization in D3 however, we do make some assumptions about our audience.
We assume that:
Inside the folder, we have 3 files and 3 folders:
Typically, you'd be able to right click on the index.html file and open it with a web browser to render the webpage. If you try this, you'll likely notice that D3 doesn't render. This is because we need a server to run D3.
cd Desktop/Beer-Project
python -m SimpleHTTPServer 8000
Now you're ready to render the HTML files. Click on local host to render the initial file. Open up the index.html file in your preferred text editor. We'll be adding functions, starting at line 235, to this file throughout this tutorial.
We provide you all the code for each of the four functions. Feel free to copy and paste the code directly into your index.html file or try to write it yourself to better understand what’s going on. We provide detailed comments along the way to help you understand exactly what’s happening at each step. Keep in mind that JavaScript does not require you to indent, but indenting will make your code more readable (so indent!).
With so many beers, it can be hard to read each label. Everytime we hover over a node on the network, we'll want to highlight it in green and enlarge the font to make the beer name more visible.
In our mouseoverNodes() function we use the D3 selector to select the circle our cursor hovers on.
// Step1: mouseover to highlight the node itself
// Step1.1: highlight the circle
d3.select(this).select("circle")
.transition()
.duration(50) //the amount of milliseconds the element transitions last
.style("fill","#99fd17") //makes the circles of the nodes glow green on mouseover
.attr("r", function(d) { //creates a function to return the radius (r) of the circle on mouseover
//groups represent the hierarchy of the different beer types
if (d.group == 1){ //group 1 represents the Ale or Lager, the earliest ancestor node
return 52; // since group 1 is the our earliest ancestor, the radius should be the largest
} else if (d.group == 2){
return 15;
} else if (d.group == 3){
return 10;
} else if (d.group == 4){
return 8;
} else if (d.group == 5){
return 5;
} else {
return 1;
}
});
//Step1.2: highlight the text
d3.select(this).select("text") //we can add effects to the text on mouseover too
.style("font-size","100%") //the '100%' changes the text to the default font-size. All labels are the same size.
Now reload your code to explore the new interactivity you’ve just added.
You might have noticed that each node you hover over, stays highlighted when you mouse out. We want to return the nodes back to their original appearance each time we mouse away from them. To do this, we'll create the first version of the mouseout function.
// Step2: return nodes (circles and texts) to original size and color
// Step2.1: return circles to normal
d3.select(".networkgraph").selectAll("circle") //selects all of the circles in the networkgraph class
.style("fill","white") //makes all the circles go back to white
.attr("r", function(d) { //makes the circles go back to their original size, depending on their group number (order in hierarchy)
if (d.group == 1){ //group 1 represents the Ale or Lager, the earliest ancestor node
return 50;
} else if (d.group == 2){
return 10;
} else if (d.group == 3){
return 6;
} else if (d.group == 4){
return 4;
} else if (d.group == 5){
return 2;
} else if (d.group == 6){
return 1;
} else {
return 4.5;
}
})
// Step2.2: return text to original size
d3.select(".networkgraph").selectAll(".nodeText") //selects all of text in the networkgraph class
.style("font-size","10px") //returns them to their original size, which is 10px
We're going to be adding a mouse over effect to each beer glass icon so that they appear to fill up and so that they can highlight which beers in the network should be served in that particular glass. The mouseoverGlass function will also add the effect of filling the glass with beer each time we hover over it.
//Step3: mouseover glass to highlight associated nodes
var glass = d3.select(this).attr("id") //creates a variable 'glass' to store the id of the mouseovered glass (each glass has a unique id)
d3.selectAll("g.node[glassTypes*="+ glass +" i]").select("circle") //selects all the circles from the nodes with the attribute "glassTypes" containing the id of the mouseovered glass (COPY THIS LINE EXACTLY AS YOU SEE IT HERE). FYI, you can check line 80 to see how nodes are structured
.style("fill","#99fd17") //makes the circles of the nodes glow green
.transition()
.duration(50)
.attr("r", 10) //changes the size of the circles
//Step4: mouseover to fill the glass with beer
d3.select("#" + glass + "whiterect") //selects the white rectangle (each has a unique id that corresponds to the glass and related beer color, which it covers)
.transition()
.duration(900)
.attr("height", "5px") //shrinks the height of the white rectangle, essentially hiding it from view. This gives the illusion that the glass fills up.
Similar to our first issue with the nodes, the glasses remain filled when we mouse away. Let's add functionality to our original mouseout function to also return the glasses to their pre-hovered state.
// Step5: return glasses to empty state
d3.selectAll(".transformableRect") //selects the white rectangle (class is named "transformableRect") that conceals the beer color of each glass
.transition()
.duration(900)
.attr("height", "95px") //brings back the height of the white rectangle to the original size. This gives the illusion that the glass empties out
Now that we have a good amount of interactivity in our visualization, let's finish by adding a click function to the nodes that will both highlight ancestral nodes for easier visibility, and fill up the corresponding glasses. Remember, each node represents a green circle with a beer name.
//Step6: Click to fill the related glass type(s) with beer
var glassesString = d3.select(this).attr("glassTypes"), //retrieves the attribute "glassTypes", which contains a string of concatenated glasses that correspond to that node
beerGlassList = glassesString.split('|'); //creates an array from the glassesString using the pipe symbol ('|') as a delimiter (example of string: "Mug|Tulip|Pint Glass")
for (beerIndex in beerGlassList){ //loops through the beerGlassList. In JS: for(i in array){}, i will be the index of the array; for(i in dict){}, i will be the key of the dictionary
d3.select("#"+ beerGlassList[beerIndex].split(' ')[0].toLowerCase() + "whiterect") //for each beer, find the respective white rectangle
.transition()
.duration(900)
.attr("height", "5px")
}
//Step7: Click to highlight all ancestral nodes
//Step7.1: get information about the clicked node
var clickedId = d3.select(this)[0][0].__data__.index, //gets the ID of the clicked node
clickedGroup = d3.select(this)[0][0].__data__.group; //gets the group number of the clicked nodes (groups represent the hierarchy of the different beer types)
for(i = clickedGroup; i > 1; i –){ //loops through the group number (represents hierarchy) from the clicked node to its highest-level parent, whose group is 1 glowId = json.links[glowId - 2].source_id; //based on how parental nodes are connected to the children nodes in the dataset, we get the ID of the parental node accordingly //console.log(json.links[glowId - 2]); //uncomment this if you’re interested in better understanding how child nodes are connected to parent nodes glowIdList.push(glowId); //adds the ID of each parental node into the glowIdList }
- Step 7.3: Activate (effects color and size) every node (circles and texts) in the array
```javascript
//Step7.3: activate (effects color and size) every node (circles and texts) in the array
for(var i = 0; i < glowIdList.length; i++){ //loops through the IDs in the glowIdList we built in step 7.2
var selector = "[sourceID='" + glowIdList[i] + "']"; //creates a variable to store the ID of the node
d3.select(selector).select("circle") //selects the node with the ID of the node stored in the selector variable
.transition()
.duration(50)
.style("fill","#99fd17")
.attr("r", function(d) {
if (d.group == 1){
return 52;
} else if (d.group == 2){
return 15;
} else if (d.group == 3){
return 10;
} else if (d.group == 4){
return 8;
} else if (d.group == 5){
return 5;
} else {
return 1;
}
});
d3.select(selector).select("text")
.transition()
.duration(50)
.style("font-size","100%")
}
Reload your code and explore the new functionality you’ve added.