How I adapted the example code and made it my own - MartijnKeesmaat/functional-programming GitHub Wiki

The code I use in its entirety is referenced in the code. For example with this wrap function below. These types of functions are easily referenced. Libraries I use like D3 or the examples I've used as a base are referenced as an acknowledgment in the readme. Below this code block, I show how I adapted the examples and made them my own.

// https://bl.ocks.org/guypursey/f47d8cd11a8ff24854305505dbbd8c07
export function wrap(text, width) {
  text.each(function () {
    let text = d3.select(this),
      words = text.text().split(/\s+/).reverse(),
      word,
      line = [],
      lineNumber = 0,
      lineHeight = 1.1, // ems
      y = text.attr("y"),
      dy = parseFloat(text.attr("dy")),
      tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em")
    while (word = words.pop()) {
      line.push(word)
      tspan.text(line.join(" "))
      if (tspan.node().getComputedTextLength() > width) {
        line.pop()
        tspan.text(line.join(" "))
        line = [word]
        tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", `${++lineNumber * lineHeight + dy}em`).text(word)
      }
    }
  })
}

Bar chart

I learned how the build the most basic chart from FCC. Below you see the screenshot of how this chart looks.

Just 8 bars created with a rect elements that have a navy blue fill. Above those, you see the value of each bar displayed.

    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")

This is the code that creates these bars. Since I followed the FCC course I gained the basic knowledge of how to use D3. Then I used that basic knowledge, created a custom design and coded the graph.

How I adopted the bar graph

image Here you see the result I created from the example.

  • Changed the orientation of the bars to horizontal.
  • Added labels to the side and wrapped it with a max-width
  • Added a scale
  • Added the x-axis
  • Added gridlines
  • Styled the graph according to the design

Change the orientation

This can be done by changing the x and y values. Here I also adjust the values to match the design. This is the code above which I adjusted to fit my needs.

const addBarsToBarChart = (xScale, svg, categories, barheight, barSpacing) => {
  svg
    .selectAll("rect")
    .data(categories)
    .enter()
    .append("rect")
    .attr("x", (d, i) => 100)
    .attr("y", (d, i) => i * barSpacing)
    .attr("width", d => xScale(d.value))
    .attr("height", barheight)
    .attr("class", "bar")
}

Add labels to the side

I wrote this code myself. Since I followed the FCC tutorial I knew how to dynamically add text elements. Using the same principles as the bar chart, I could now add the text. I wrote a capitalize function, which capitalizes each first letter of a string. Then I used a wrap function that I used from someone else. This is linked as a comment in the code.

const addLabelsToBarChart = (svg, categories, labelWidth, barSpacing) => {
  svg
    .selectAll("text")
    .data(categories)
    .enter()
    .append("text")
    .text(d => capitalize(d.name))
    .attr("x", (d, i) => 0)
    .attr("y", (d, i) => (i * barSpacing) + 10)
    .attr("class", "label")
    .attr("dy", 0)
    .attr("text-anchor", "end")
    .attr("transform", "translate(90," + 0 + ")")
    .call(wrap, labelWidth);
}

xScale

I used the scale from a later chapter in the tutorial. The example was from a scatter plot that had implemented a scale. What I did with this code is put it into a function that I later save in a variable. This makes the code more functional and reusable.

const addXScaleBarChart = (width, barSpacing, categories) => {
  return d3
    .scaleLinear()
    .domain([0, d3.max(categories, d => d.value)])
    .range([barSpacing, width - barSpacing]);
}

Gridlines

I wrote this code based on the D3 docs and then adapted it to the design.

const addGridlinesToBarChart = (svg, width, height, xScale) => {
  const x = xScale;

  svg.append("g")
    .attr("class", "grid")
    .attr("transform", "translate(50," + (height - 45) + ")")
    .attr("stroke", "#E9EBF1")
    .call(makeXGridlines(x)
      .tickSize(-height)
      .tickFormat("")
    )
}

Donut chart

Here you can see the example I used for the donut chart. I used an example for this chart since I didn't know how to create the arcs of the donut shape.

image

Here you see the main code that the donut chart uses to create the donut-like shape. This code is just one blob. There is no structure to it and lacks comments. It reminds me of the spaghetti code I used to write with jQuery. This code became the base of my code. I then added structure to it and adapted it to my design.

var radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal(d3.schemeCategory10);

var svg = d3.select("#chart")
.append('svg')
.attr('class', 'pie')
.attr('width', width)
.attr('height', height);

var g = svg.append('g')
.attr('transform', 'translate(' + (width/2) + ',' + (height/2) + ')');

var arc = d3.arc()
.innerRadius(radius - thickness)
.outerRadius(radius);

var pie = d3.pie()
.value(function(d) { return d.value; })
.sort(null);

How I adopted it

e244807514e2c71ecf712a7877a37faa

Instead of writing all the code in the global scope, I went for a more functional approach. Here you see my main function which contains a function for each step like storing the arcs in a variable and adding a legend.

export default function renderDonutChart(categories, size, thickness) {

  // Setup
  const width = size, height = size;
  const radius = Math.min(width, height) / 2;
  const colorPalette = addColorPalette();

  // Create donut
  const svg = addGlobalSvg(width + 180, height + 30)
  const arc = addArc(thickness, radius);
  const g = rotateArc(svg, width, height);
  const pie = addPieRadius();
  let path = createArcPaths(g, pie, categories);
  addLegend(categories, colorPalette);

  // Interactions
  showCategoryText(path);
  resetDonutText(path, categories);
  path = addFillToDonut(path, arc, colorPalette);
  addArcHover(path, colorPalette);
  addDefaultText(categories, width, height);
}

Then I adopted this donut chart to my design and added functionality.

Adopting the style

I added a custom color palette with D3, added the correct data and changed the dimensions.

const addColorPalette = () => {
  const colorArr = ['#B83B5E', '#995A3A', '#F08A5D', '#F9D769', '#6A2C70'];
  return d3.scaleOrdinal(colorArr);
}

Adding functionality

Default text

This function adds the text to the center of the donut. This text is later updated with a mouseover to show the value of the category.

function addDefaultText(categories, width, height) {
  const defaultText = d3
    .select('.pie')
    .append("g")
    .attr('class', 'default-text');

  defaultText.append("text")
    .attr("class", "donut-title")
    .text(truncator(categories[1].name, 1))
    .attr('text-anchor', 'middle')
    .attr('dx', width / 2 + 180)
    .attr('dy', height / 2)

  defaultText.append("text")
    .attr("class", "donut-sub-title")
    .text('Categorie')
    .attr('text-anchor', 'middle')
    .attr('dx', width / 2 + 180)
    .attr('dy', height / 2 + 20)
}

Adding a legend

I also created a legend that shows a text and a circle that corresponds with the donut-chart. For this, I could use the same principles I learned from the FCC tutorial.

const addLegend = (categories, colorPalette) => {
  const legend = d3
    .select('.pie')
    .append("g")
    .attr('class', 'legend');

  legend
    .selectAll("text")
    .data(categories[1].materials)
    .enter()
    .append("text")
    .text(d => capitalize(d.name))
    .attr("x", (d, i) => 14)
    .attr("y", (d, i) => 140 + (50 * (i / 1.7)))
    .attr("class", "legend-label")

  legend
    .selectAll("circle")
    .data(categories[1].materials)
    .enter()
    .append("circle")
    .attr("r", 4)
    .attr("cx", (d, i) => 4)
    .attr("cy", (d, i) => 140 + (50 * (i / 1.7)) - 4)
    .attr("class", "legend-color")
    .attr('fill', (d, i) => colorPalette(i))
}

Donut part hover

The original donut already had a hover effect. I removed this code and used my own. The code was removed since it simply had too much code for what it did.

const showCategoryText = el => {
  el.on("mouseover", function (d) {
    d3.select('.donut-title').text(truncator(d.data.name, 1));
    d3.select('.donut-sub-title').text(`${d.data.value} objecten`);
  })
}

This text is then reset by replacing the hover text with the default text. This is done on the mouseout event.

const resetDonutText = (el, categories) => {
  el.on("mouseout", function () {
    d3.select('.donut-title').text(truncator(categories[1].name, 1));
    d3.select('.donut-sub-title').text('Categorie');
  })
}

Darken donut color

The color of the donut is darkened with this mouseover function. I select the this element which updates the style element and uses this darken function. The link is placed in the javascript helpers.js file.

.on("mouseover", function (d) {
      d3.select(this)
        .style("cursor", "pointer")
        .style("fill", shadeColor(colorPalette(this._current), -20));
    })