D3 attr(), style() and classed()

Tutorial

We've discussed how to add and remove elements based on data. When we create an element, often we need to do more than just create it. If we create a circle we need to specify it's radius and also it's position these are attributes that need to be set separately. The same goes for if we want to specify styles or classes. In this lab we'll discuss how to set attributes, styles, and classes based on data.

Specifying attributes

D3 provides the ability to set attributes of a selected element using the attr() function. This function takes two parameters:

  1. Attribute Name - For example, "r" to set an SVG circle's radius.
  2. Value or An accessor function - For example "10" to set the radius to 10 or an accessor that sets the value per data point based on the data.

I've create a scatter plot below using circles. I use attr() to specify the circle's attributes. First is the plot followed by the code to create it.

var scatterPlotData = [ 
  {x: 5, y: 44},
  {x: 25, y: 80},
  {x: 85, y: 30},
  {x: 55, y: 40},
  {x: 150, y: 60}
];

d3.select("#example1")
  .select("svg")
  .selectAll("circle")
  .data(scatterPlotData)
  .enter()
  .append("circle")
  .attr("cx", function(d) { return d.x; } )       // accessor sets cx to the data point's x
  .attr("cy", function(d) { return 100 - d.y; } ) // accessor sets cy to 100 - the data point's y
  .attr("r", 5);

This code creates the graphical output you saw above. Remember it is just creating SVG code. Compare the code above to writing SVG by hand (pictured below). With just five points it may not seem like a big win. But consider if you wanted to plot 100 points and also remember that the code above works for any listing of data points.

<circle cx="5" cy="56" r="5"></circle>
<circle cx="25" cy="20" r="5"></circle>
<circle cx="85" cy="70" r="5"></circle>
<circle cx="55" cy="60" r="5"></circle>
<circle cx="150" cy="40" r="5"></circle>

In this example, a circle is created for each data point. The circle's attributes are set based on the data. Notice that we can use attr() to set the constant value 5 for the radius for every inserted element. We can also use attr() with an accessor to have the value change based on the data. We do this with the "cx" and "cy" attributes. Notice how the example used method chaining to keep setting more attributes on the circle selection. The accessor for "cy" has 100 - d.y because in this case the SVG area is 100 pixels high and as you'll recall y=0 is the top while y=100 is the bottom, which is the opposite of how a scatter plot would normally be drawn..

(d,i) accessor

Sometimes the value that we want to set is not based on the value of the data point but rather than it's location within the dataset. Perhaps we want to highlight the third data point. Alternatively we can specify the x location of a bar in a bar-chart based on the location in the dataset. Consider the bar chart below.

It is created from the following dataset:

var barData = [10,20,60,40];

We want each of the bars from these datapoints to be evenly spaced horizontally. Say each bar is 10 pixels wide and we want 15 pixels between bars (As a quick aside, a ratio of the spacing between bars to the bar width of 1.5 to 2 works well visually [1,2]). We can create a function that specifies the location of each bar that depends on the position of the datapoint.

// d is the data point
// i is the location (or index) of the data point in the dataset
function(d, i) { 
  return 10+(i*15); // start at 10 and then add 15 for each data point
}

D3 can use the function above as an accessor. Notice how this function doesn't use d at all. It is still necessary as a parameter to disambiguate accessors that use d and i from those that just use d. Below is the complete listing to create the bar chart.

var barData = [10,20,60,40];

d3.select("#example2")
  .select("svg")
  .selectAll("rect")
  .data(barData)
  .enter()
  .append("rect")
  .attr("y", function(d) { return 90-d; } )
  .attr("height", function(d) { return d; } )
  .attr("width", 10)
  .attr("x", function(d, i) { return 10+(i*15); } );

This code creates the following SVG:

<rect y="80" height="10" width="10" x="10"></rect>
<rect y="70" height="20" width="10" x="25"></rect>
<rect y="30" height="60" width="10" x="40"></rect>
<rect y="50" height="40" width="10" x="55"></rect>

Specifying styles

D3 provides the ability to set styles based on data using the style() function. Say that you were creating a map and colored it based on the political party with the majority in each geography. You would need a way to set colors of the various regions based on the majority political party. This style() function takes two arguments:

  1. The style name - for example "fill"
  2. The value to set it to - for example "red"

By adding one more call to the method chain we used to create the bar chart above, we can use style to highlight all bars that represent data over 50.

.style("fill", function(d) 
               { 
                 if (d > 50) 
                 {
                   return "red";
                 } 
                 return "black";
               });

style() is great when you want to just set one style. However, I prefer to avoid style() when setting several pieces of style at once. This is often the case as you work to make the visualization look just so. In the example above, it might be easier to think in terms of "highlighed bar", which can be styled as a CSS class instead of several style calls for each style.

Specifying classes

Fortunatley, D3 provides the ability to set the classes of an element using the classed() function. This allows us to create a .highlightedBar CSS class and apply any styles we want to it. A given element can be part of several classes, so to use classed() you specify which class you are setting and then true or false.

// turn on highlighting
.classed("highlightedBar", true)

// turn off highlighting
.classed("highlightedBar", false)

Most times you will use an accessor function to determine true or false. classed() also allows you to set many classes at once. In this case, it takes just one parameter, an object of classnames set to true or false such as { class1: true, class2: false }.

.classed("highlightedBar", function(d) 
                           {
                             if (d > 50) { 
                               return true;
                             }
                             return false;
                           });

Using a class allows us to set several properties at once. Of course you have to specify these properties in CSS within a style element or in separate CSS file. In this case, the highlightedBar has the three CSS properties below.

.highlightedBar {  
  fill: red;
  stroke-width: 1px;
  stroke: black;
}

Citations

1. Wong, Dona (2013). The Wall Street Journal Guide to Information Graphics. W. W. Norton & Company.

2. Few, Stephen (2012). Show Me the Numbers. Analytics Press.

Quiz

function(a,b) { return b; }
This accessor function returns the index of the element in the data set. True/False.

Match the method to it's description
attr()    - Set's an attribute on an element
classed() - Set's the classes of an element
style()   - Set's the style of an element

Things to do

  1. Use classed() to set the class of any rectangle representing >80% to ".highlightedBar"
  2. Use attr() to move the "#chartarea" below the "#chartTitle" and also to the right so that the labels are visible.
  3. Use attr() to move to center the "#chartTitle" over the "#chartArea".
  4. Use attr() to move to center the "#chartTitle".
  5. Use style() to change the stroke-width of the gride lines to ".5px"

Extra Credit

Change the code on the left. Once you've made a change, the page will render on the right.