Practicing D3 with Free Code Camp - MartijnKeesmaat/functional-programming GitHub Wiki

I used freecodecamp.org to practice the techniques used in D3.js. I came across these lessons as I was brushing up on my functional programming skills. The d3 course was a great introduction to d3. In these you go over the basic principles from selecting elements, adding data, styling to using scales and adding the axis. When you are done with these you know how to make basic graphs like the bar graph and scatter plot graph. Here you see my notes I made whilst learning those basics.

D3

D3.js, or D3, stands for Data-Driven Documents. D3 is a JavaScript library to create dynamic and interactive data visualizations in the browser. It's built to work with common web standards, namely HTML, CSS, and Scalable Vector Graphics (SVG).

Selecting

d3.select("ul") // queryselector
  .append("li") // just an append
  .text("Very important item"); // textcontent

Well this is pretty cool (you can select all the items and then chain another method)

d3.selectAll('li').text('list item');

Data

The first step is to make D3 aware of the data. The data() method is used on a selection of DOM elements to attach the data to those elements. The data set is passed as an argument to the method.

enter()

A common workflow pattern is to create a new element in the document for each piece of data in the set. D3 has the enter() method for this purpose.

When enter() is combined with the data() method, it looks at the selected elements from the page and compares them to the number of data items in the set. If there are fewer elements than data items, it creates the missing elements.

<body>
  <ul></ul>
  <script>
    const dataset = ["a", "b", "c"];
    d3.select("ul").selectAll("li")
      .data(dataset)
      .enter()
      .append("li")
      .text("New item");
  </script>
</body>

The enter() method sees there are no li elements on the page, but it needs 3 (one for each piece of data in dataset). New li elements are appended to the ul and have the text "New item".

Dynamic data

The D3 text() method can take a string or a callback function as an argument:

selection.text((d) => d)

selection.text((number) => ${number} USD)

In the example above, the parameter d refers to a single entry in the dataset that a selection is bound to.

Using the current example as context, the first h2 element is bound to 12, the second h2 element is bound to 31, the third h2 element is bound to 22, and so on

Style

D3 lets you add inline CSS styles on dynamic elements with the style() method.

The style() method takes a comma-separated key-value pair as an argument. Here's an example to set the selection's text color to blue:

selection.style("color","blue");

Dynamic styling

D3 is about visualization and presentation of data. It's likely you'll want to change the styling of elements based on the data. You can use a callback function in the style() method to change the styling for different elements.

For example, you may want to color a data point blue if has a value less than 20, and red otherwise. You can use a callback function in the style() method and include the conditional logic. The callback function uses the d parameter to represent the data point:

selection.style("color", d => {
	if(d < 20) return "red"
	else return "green"
})

The style() method is not limited to setting the color - it can be used with other CSS properties as well.

Adding attributes

Using a lot of inline styles on HTML elements gets hard to manage, even for smaller apps. It's easier to add a class to elements and style that class one time using CSS rules. D3 has the attr() method to add any HTML attribute to an element, including a class name.

The attr() method works the same way that style() does. It takes comma-separated values, and can use a callback function. Here's an example to add a class of "container" to a selection:

selection.attr("class", "container");

selection.attr("class", "bar") // add a class of bar

Simple bar chart

const dataset = [12, 31, 22, 17, 25, 18, 29, 14, 9];

d3.select("body").selectAll("div")
  .data(dataset)
  .enter()
  .append("div")
  .attr("class", "bar")
  // Add your code below this line
  .style("height", d => `${d*3}px`)

SVG

SVG is a best practice for working with data. You work with SVG by adding a svg to the DOM as container and then append rect elements or circles etc. To position each rect you can return a function which returns either the element or index like this:

.attr("x", (d, i) => i * 30)

<body>
  <script>
    const dataset = [12, 31, 22, 17, 25, 18, 29, 14, 9];

    const w = 500;
    const h = 100;

    const svg = d3.select("body")
                  .append("svg")
                  .attr("width", w)
                  .attr("height", h);

    svg.selectAll("rect")
       .data(dataset)
       .enter()
       .append("rect")
       .attr("x", (d, i) => i * 30)
       .attr("y", 0)
       .attr("width", 25)
       .attr("height", (d, i) => {
         // Add your code below this line
          return d * 3


         // Add your code above this line
       });
  </script>
</body>

Position elements at the bottom

You may have noticed the bar chart looked like it's upside-down, or inverted. This is because of how SVG uses (x, y) coordinates.

In SVG, the origin point for the coordinates is in the upper-left corner. An x coordinate of 0 places a shape on the left edge of the SVG area. A y coordinate of 0 places a shape on the top edge of the SVG area. Higher x values push the rectangle to the right. Higher y values push the rectangle down.

To make the bars right-side-up, you need to change the way the y coordinate is calculated. It needs to account for both the height of the bar and the total height of the SVG area.

The height of the SVG area is 100. If you have a data point of 0 in the set, you would want the bar to start at the bottom of the SVG area (not the top). To do this, the y coordinate needs a value of 100. If the data point value were 1, you would start with a y coordinate of 100 to set the bar at the bottom. Then you need to account for the height of the bar of 1, so the final y coordinate would be 99.

The y coordinate that is y = heightOfSVG - heightOfBar would place the bars right-side-up.

.attr("y", (d, i) => {
	// Add your code below this line
	return heightOfSVG - heightOfBar

In general, the relationship is y = h - m * d, where m is the constant that scales the data points.

Labels

D3 lets you label a graph element, such as a bar, using the SVG text element.

Like the rect element, a text element needs to have x and y attributes, to place it on the SVG canvas. It also needs to access the data to display those values.

D3 gives you a high level of control over how you label your bars.


The code in the editor already binds the data to each new text element. First, append text nodes to the svg. Next, add attributes for the x and y coordinates. They should be calculated the same way as the rect ones, except the y value for the text should make the label sit 3 units higher than the bar. Finally, use the D3 text() method to set the label equal to the data point value.

const dataset = [12, 31, 22, 17, 25, 18, 29, 14, 9];

const w = 500;
const h = 100;

const svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);

svg.selectAll("rect")
 .data(dataset)
 .enter()
 .append("rect")
 .attr("x", (d, i) => i * 30)
 .attr("y", (d, i) => h - 3 * d)
 .attr("width", 25)
 .attr("height", (d, i) => 3 * d)
 .attr("fill", "navy");

// the label part (uses the same x and y as the rect, y is positioned -3)
svg.selectAll("text")
 .data(dataset)
 .enter()
.append("text")
.attr("x", (d, i) => i * 30)
.attr("y", (d, i) => h - (3 * d) - 3)
.text(d => d)

Hover

You can add hover effect simply by adding a class to a rect

<style>
  .bar:hover {
    fill: brown;
  }
</style>

<script>
	selection.attr("class", "bar")
</script>

Tooltip

A tooltip shows more information about an item on a page when the user hovers over that item. There are several ways to add a tooltip to a visualization, this challenge uses the SVG title element.

title pairs with the text() method to dynamically add data to the bars.


Append a title element under each rect node. Then call the text() method with a callback function so the text displays the data value.

https://www.freecodecamp.org/learn/data-visualization/data-visualization-with-d3/add-a-tooltip-to-a-d3-element

Working with circles

A circle in SVG has three main attributes. The cx and cy attributes are the coordinates. They tell D3 where to position the center of the shape on the SVG canvas. The radius (r attribute) gives the size of the circle.

All three attributes can use a callback function to set their values dynamically. Remember that all methods chained after data(dataset) run once per item in dataset. The d parameter in the callback function refers to the current item in dataset, which is an array for each point. You use bracket notation, like d[0], to access the values in that array.

svg.selectAll("circle")
   .data(dataset)
   .enter()
   .append("circle")
   // Add your code below this line
    .attr("cx", (d, i) => d[0])
    .attr("cy", (d, i) => w - d[1])
    .attr("r", 5)

const dataset = [
  [ 34,    78 ],
  [ 109,   280 ],
  [ 310,   120 ],
  [ 79,    411 ],
  [ 420,   220 ],
  [ 233,   145 ],
  [ 333,   96 ],
  [ 222,   333 ],
  [ 78,    320 ],
  [ 21,    123 ]
];

Scaling data

The bar and scatter plot charts both plotted data directly onto the SVG canvas. However, if the height of a bar or one of the data points were larger than the SVG height or width values, it would go outside the SVG area.

In D3, there are scales to help plot data. Scales are functions that tell the program how to map a set of raw data points onto the pixels of the SVG canvas.

For example, say you have a 100x500-sized SVG canvas and you want to plot Gross Domestic Product (GDP) for a number of countries. The set of numbers would be in the billion or trillion-dollar range. You provide D3 a type of scale to tell it how to place the large GDP values into that 100x500-sized area.

It's unlikely you would plot raw data as-is. Before plotting it, you set the scale for your entire data set, so that the x and y values fit your canvas width and height.

D3 has several scale types. For a linear scale (usually used with quantitative data), there is the D3 method scaleLinear():

const scale = d3.scaleLinear()

By default, a scale uses the identity relationship. The value of the input is the same as the value of the output. A separate challenge covers how to change this.

const scale = d3.scaleLinear(); // Create the scale here
const output = scale(50); // Call the scale with an argument here

Domain and range

By default, scales use the identity relationship - the input value maps to the output value. But scales can be much more flexible and interesting.

Say a data set has values ranging from 50 to 480. This is the input information for a scale, and is also known as the domain.

You want to map those points along the x axis on the SVG canvas, between 10 units and 500 units. This is the output information, which is also known as the range.

The domain() and range() methods set these values for the scale. Both methods take an array of at least two elements as an argument. Here's an example:

const scale = d3.scaleLinear();
scale
	.domain([250, 500])
  .range([10, 150])

Min and max

These are useful for setting the scale

The D3 methods domain() and range() set that information for your scale based on the data. There are a couple methods to make that easier.

Often when you set the domain, you'll want to use the minimum and maximum values within the data set. Trying to find these values manually, especially in a large data set, may cause errors.

D3 has two methods - min() and max() to return this information. Here's an example:

const exampleData = [34, 234, 73, 90, 6, 52];
d3.min(exampleData) // Returns 6
d3.max(exampleData) // Returns 234

A dataset may have nested arrays, like the [x, y] coordinate pairs that were in the scatter plot example. In that case, you need to tell D3 how to calculate the maximum and minimum. Fortunately, both the min() and max() methods take a callback function. In this example, the callback function's argument d is for the current inner array. The callback needs to return the element from the inner array (the x or y value) over which you want to compute the maximum or minimum. Here's an example for how to find the min and max values with an array of arrays:

const locationData = [[1, 7],[6, 3],[8, 3]];
// Returns the smallest number out of the first elements
const minX = d3.min(locationData, (d) => d[0]);
// minX compared 1, 6, and 8 and is set to 1

Using the scale

const padding = 60;

const xScale = d3.scaleLinear()
	.domain([0, d3.max(dataset, (d) => d[0])])
	.range([padding, w - padding]);

selection.attr("cx", d => xScale(value))

Adding the axes

Another way to improve the scatter plot is to add an x-axis and a y-axis.

D3 has two methods axisLeft() and axisBottom() to render the y and x axes, respectively. (Axes is the plural form of axis). Here's an example to create the x-axis based on the xScale in the previous challenges:

const xAxis = d3.axisBottom(xScale);

The next step is to render the axis on the SVG canvas. To do so, you can use a general SVG component, the g element. The g stands for group.

Unlike rectcircle, and text, an axis is just a straight line when it's rendered. Because it is a simple shape, using g works.

The last step is to apply a transform attribute to position the axis on the SVG canvas in the right place. Otherwise, the line would render along the border of SVG canvas and wouldn't be visible.

SVG supports different types of transforms, but positioning an axis needs translate. When it's applied to the g element, it moves the whole group over and down by the given amounts. Here's an example:

const xAxis = d3.axisBottom(xScale);

svg.append("g")
   .attr("transform", "translate(0, " + (h - padding) + ")")
   .call(xAxis);

The above code places the x-axis at the bottom of the SVG canvas. Then it's passed as an argument to the call() method. The y-axis works is the same way, except the translate argument is in the form (x, 0). Because translate is a string in the attr() method above, you can use concatenation to include variable values for its arguments.

https://www.freecodecamp.org/learn/data-visualization/data-visualization-with-d3/add-axes-to-a-visualization

⚠️ **GitHub.com Fallback** ⚠️