D3 Beginnings
Reference
January 31, 2018
Today we started learning D3. Lots more new info to process! I will compile this post as a catch-all reference guide over the next few days of instruction.
Quick Reference:
Installing
I don’t know why I assumed it would require a big installation to work with D3. Not so! It’s just a matter of including the JS library in your HTML:
<script src="https://d3js.org/d3.v4.js"></script> |
Basic Selections
d3.select() // select one element |
The basic selector methods select in the same way CSS and jQuery select, by .class
, #id
, or element
. These methods return a selection object containing a _groups
array and a _parents
array.
If you want to access the elements themselves, use the node
method (or nodes
for multiple elements):
d3.selectAll("li").nodes(); // returns an array of li elements |
Selections can be manipulated with several methods:
.style(property [, newValue])
allows you to add CSS.attr(attribute [, newValue])
allows you to change attributes.text([newValue])
allows you to add/remove text.html([newValue])
allows you to add/remove HTML.append(tagName)
allows you to add HTML elements & return a new D3 selection
For each of these methods, you can also place a callback function in place of newValue
. This callback has a specific structure which is defined below.
If you don’t pass in any value, these methods will act as getters:
// Manipulate elements |
Instead of getting classes with the attr
method, it’s preferred to use the classed
method instead. The first parameter of the method is a list of classes and the second is true
if you want to add the list of classes to the selection, or false
if the classes should be removed from the selection:
selection.classed("space separated list of classes", boolean); |
Finally, the remove
method also works as a selector and removes elements at the same time.
Event Listeners
selection.on("eventType", callback) |
Note that only one event listener can be attached to each element; if you attach more than one, it will only run the last one.
You can also remove an event listener with null
passed in as the 2nd parameter:
selection.on("eventType", null) |
For the callback function, the d3.event
property must be used inside of the event handler to gain access to normal event handling object properties. Here is an example of an form submission event handler callback function in action:
// On submitting form, add the value of the input to a new list item |
Passing Data With D3
Here is a first look at passing data into the DOM for display. This work with an empty ul
with an id of #quotes
, and an array or objects var quotes
which contains (you guessed it) movie titles and quotes.
d3.select("#quotes") |
A few things are going on here:
- Select the unordered list and style it
- Select all
li
s in the list…but there are none to start! D3 creates a selection object with empty nodes for theseli
s. - Use the
data
method to attach thequotes
array data to placeholder__data__
nodes. - Use the
enter
method to create a D3 selection from the placeholder nodes. - Append the data to the
li
DOM elements (note: append must be after the parent element has been selected, otherwise the element in question will be appended to thehtml
element) - And finally set the text to return the desired property from the data object with a callback function.
Also worth noting: once the elements have been added to the DOM, they can be selected and manipulated using normal D3 selectors, and they remain bound to whatever data they were created with. In the above example, we could select the li
s to change the text to the film title for example:
d3.selectAll("li") |
D3 Callback Structure
Callback functions in D3 take two parameters: the first is the data that’s getting passed into the DOM, and the second is the index it’s being passed in at (not needed/shown above). This is the default structure any time a callback is passed into a D3 method.
Refactoring
The operation above could be refactored and expanded on to make a more visually compelling display:
d3.select("#quotes") |
Removing Data
Like enter()
, there is an exit()
method on D3 objects to remove data. By default data is bound by index, so it’s necessary to bind data to elements to remove items correctly.
For example, if there are 5 values and you only want to display three of them (lets say odd integers from 1-5), by default D3 will recognize that there are three elements to keep, but it will only keep indices 0, 1, and 2. Not what we want!
Instead we can bind the data to DOM elements by adding a key function as the second parameter to the data()
method during the selection. In the refactored code above, we add all quotes to the DOM and style them. Now let’s select only certain quotes, bind the data to each DOM element, and delete the ones we don’t want:
var nonRQuotes = quotes.filter(function(movie) { |
Merging Data / Update Pattern
When items are added to or removed from the DOM, they are stored separately from items that were already in the DOM. This refers to the selection types:
- Enter selection: data with no DOM elements attached
- Exit selection: DOM elements with no data attached
- Update selection: items with both data and DOM elements attached
To treat all of the items on a page as one, these separate storage areas need to be merged:
selection.merge(otherSelection) |
This will create a new single selection with everything in it. All together, this makes up the general update pattern that is standard in D3:
- Grab the update selection, make any changes unique to that selection, and then store the selection in a variable.
- Grab the exit selection and remove any unnecessary elements.
- Grab the enter selection and make any changes necessary to that selection.
- Merge the enter and update selections, and make any changes you want to be shared across both selections.
Putting It All Together
To put it all of this (so far) together we coded a simple form which would display all of the unique characters in a string as a bar graph, where the height of the bar represents the number of times the character appears. It also stores the count from a previous string, but exits those items when a third string comes into the mix. This is the code I came up with (partly on my own):
const form = d3.select("form"); |
The main part I had trouble with was handling the new vs. old string. In my first attempts I tried to store these values for comparison, but merging them was very convoluted and the walk-through showed a much better way (above).
I also tried a few different approaches for the getFrequencies()
function, but ultimately created the currentObj
in a way that didn’t work well with joining the data in D3: it was necessary to create an array of objects so that each object could be treated as a data entry. Creating a single object from the array ({h: 1, e: 1, l: 2, o: 1}
etc.) made it much harder to join, trying to iterate through the keys. Actually, I couldn’t do it at all! So good to have the walk-through :)