D3 Visualization - NathanNeelis/frontend-data GitHub Wiki

Learning D3 and my progress

Index

Used d3 examples
bar chart anatomy
D3 scales
D3 axis
D3 bars
D3 update
My D3 process - Recommondation to read!

Examples I used for my bar chart

Bar chart anatomy

My bar chart is created by four different components

  • Scales
  • Axis
  • Bars
  • Updates

In my makeVisualization function it gathers the data and then used four function for each of the different components

makeVisualization()
async function makeVisualization() {
  getData(endpointNPR)
    .then(data => {

      let prData = cleaningData(data);
      let combine = combineDoubleCities(prData);

      prDataSet = cleaningData(data) // binding data to global data variable

      setupScales(prData) // import from visual
      setupAxis(prData) // import from visual
      drawBars(prData) // import from visual
      setupInput(prData) // not an import 
    });
}

D3 Scales

I created a function setupScales to set up my scales for my bar chart.
It creates a width and height for my bar chart according to the data input.
A scale works with a domain and a range.
Domain: The domain is the scale on which the data operates.
Range: The range is the scale of the visual on the screen.
To visualize this, there is an image in Currans tutorials that explains how this scaling works for a linear scale:

There are however a lot more scaling options.

  • Linear scale
  • Scale point
  • Bandscale
  • Scale time
  • scaleOrdinal

The code example below shows how I setup my scales

function setupScales(data){
  const yScale = scaleLinear()
  const xScale = scaleBand()

      yScale
        .domain([max(data, yValue), 0])
        .range([0, innerHeight])
        .nice();

      xScale
        .domain(data.map(xValue))
        .range([0, innerWidth])
        .padding(0.2); 
    }

D3 Axis

In my bar chart, there is an x- and y-axis. In these axis, I want to have the information about my parking facilities and the capacity. I also want to add labels, at least on the capacity side, to show what the numbers are for. I think it might be clear what the data is for the parking facilities following the title's name.

On creating my axis on the left I also increase the ticksize to create lines so the data becomes more readable.
On creating the axis I create group elements to keep everything together and later on, to update the information by targeting the classes.

In the code example below I create the axis, group them and label them. I also remove some extra lines, that have the class .domain to clear up my visual. The x-axis label is also created here but left empty in the end. So I can decide if I want to add a label or not, I will not have to write the whole code for it again.

export function setupAxis(data){
  const yAxis = axisLeft(yScale)
    .tickSize(-innerWidth)
  const xAxis = axisBottom(xScale)

  
  // y Axis GROUPI G
  const yAxisG = g.append('g').call(yAxis)
  
  yAxisG
  .selectAll('.domain') // removing Y axis line and ticks
    .remove()
  
  yAxisG.append('text') // Y LABEL
    .attr('class', 'axis-label')
    .attr('y', -40)
    .attr('x', -innerHeight / 2)
    .attr('fill', 'black')
    .attr('transform', `rotate(-90)`)  // ROTATING Y LABEL
    .attr('text-anchor', 'middle')
    .text(yAxisLabel)
  
  yAxisG
    .attr('class', 'axis-y')
  	
  
  // x Axis GROUPING
  const xAxisG = g.append('g').call(xAxis)
  
  xAxisG
  .attr('transform', `translate(0, ${innerHeight})`)
  .attr('class', 'axis-x')
    .selectAll("text")
      .attr("transform", `rotate(50)`)
      .attr('text-anchor', 'start')
      .attr('x', 10)
      .attr('y', 5)
    .selectAll('.domain, .tick line') // removing X axis line and ticks
      .remove();
  xAxisG.append('text') // X AS LABEL
      .attr('class', 'axis-label')
      .attr('x', innerWidth / 2)
      .attr('y', 60)
      .attr('fill', 'black')
      .text(xAxisLabel)
  }

D3 bars

Creating the bars is where it gets really interesting. This is where I start to make data joins.

data joins

A data joins actually exists out of 2 parts: Data and Elements. The data part is the data we get from our API, and then there are elements, these are the DOM elements that are going to be in the HTML. These elements will be the rectangles in my bar chart because I enter my data and creating rectangles using .data(data).enter().append('rect')

There are three different states for a data join:

  1. enter
  2. update
  3. exit

In the code below I am only going to be using the enter state. Later on, I will explain how I used update and exit as well. The enter state works as follows:
In the enter state, there is the case that we have more data than elements. That is why we are selecting all (.bar) elements and appending to the enter a ('rect') with an attribute of the class (.bar). So now we create a rectangle with the class bar for each row of our data. In my case, this is about 50 data rows. So I will get 50 bars of data.

export function drawBars(data){
  g.selectAll('.bar')
    .data(data)
    .enter()
       .append('rect')
         .attr('class', 'bar')
         .attr('x', d => xScale(xValue(d)))
         .attr('y', d => yScale(yValue(d)))
         .attr('width', xScale.bandwidth())
         .attr("y", function(d) {
    		return yScale(0);
    		})
        .attr("height", 0)
        .transition().duration(1000)
        .attr('y', d => yScale(yValue(d)))
        .attr('height', d => innerHeight - yScale(yValue(d)));
  }

D3 update

This is where the data join states update and exit come in.

Update

In the update, there are existing dom elements that correspond to the data elements I have. Here we just change the data that we put in there on the enter data join state. So if we have not enough dom elements the enter state makes them for us to fit our new updated data. And if there are too many elements, thats where the exit state comes in.

Exit

As described above, sometimes you have leftover dom elements. These need to exit our HTML, or just say, to be deleted or removed.

In my example below I change the data input when there is a click on the input.
My changeOutput function then decides if the input is true or false.
If the input is true then it will take my data and use the function combineDoubleCities on them, changing the data input. If the input is false however, it will send in the original data.

It first sets up the scales again, then after that it is going to create the bars. Notice how the data input is now dataSelection instead of the original data. It then creates the bars with the new or old data depending on the checkbox input data.

After changing the bars it moves on to the axis, the code looks for the classes axis-x and axis-y to change to call the functions again with the new input.

function setupInput() {
  // &CREDITS code example by Laurens
  const input = select('#filter')
    .on('click', changeOutput)
}

function changeOutput() {
  const filterOn = this ? this.checked : false;
  console.log('checkbox', this.checked)

  const dataSelection = filterOn ? combineDoubleCities(prDataSet) : prDataSet
  console.log('new data', dataSelection)

  //Update the domains
  yScale
    .domain([max(dataSelection, yValue), 0])
    .nice()

  xScale
    .domain(dataSelection.map(xValue))

  //Bars will store all bars created so far
  //$CREDITS ==  Code example by LAURENS 
  const bars = g.selectAll('.bar')
    .data(dataSelection)

  // update
  bars
    .attr('y', d => yScale(yValue(d)))
    .attr('x', d => xScale(xValue(d)))
    .attr('width', xScale.bandwidth())

    .attr("y", function (d) {
      return yScale(0);
    })
    .attr("height", 0)
    .transition().duration(1000)
    .attr('y', d => yScale(yValue(d)))
    .attr('height', d => innerHeight - yScale(yValue(d)))
  // console.log('data at update point', dataSelection)


  //Enter
  bars.enter()
    .append('rect')
    .attr('class', 'bar')
    .attr('x', d => xScale(xValue(d)))
    .attr('y', d => yScale(yValue(d)))
    .attr('width', xScale.bandwidth())

    .attr("y", function (d) {
      return yScale(0);
    })
    .attr("height", 0)
    .transition().duration(1000)
    .attr('y', d => yScale(yValue(d)))
    .attr("height", d => innerHeight - yScale(yValue(d)));

  // RESOURCE BARS FROM BOTTOM TO TOP:
  // credits for Harry Vane @ stackoverflow
  // RESOURCE: https://stackoverflow.com/questions/36126004/height-transitions-go-from-top-down-rather-than-from-bottom-up-in-d3

  //Exit
  bars.exit()
    .remove()

  //Update the ticks	
  svg.select('.axis-x')
    .call(axisBottom(xScale))
    .attr('transform', `translate(0, ${innerHeight})`)
    .selectAll("text")
    .attr("transform", `rotate(50)`)
    .attr('text-anchor', 'start')
    .attr('x', 10)
    .attr('y', 5)
  svg.select('.axis-y')
    .call(axisLeft(yScale).tickSize(-innerWidth))
    .selectAll('.domain') // removing Y axis line and ticks
    .remove()

}

My D3 process

Curran's tutorial

Starting my adventure in D3 I followed Currans tutorials.
Here I made a smiley face, a bar chart, different scatter plots, and a bowl of fruit.

Creating a barchart

For my visual I used this tutorial from Curran as a starting point.

Starting with my own data

I was struggling a lot with where to start, but decided to take the bar chart under the loop and see how I could transform my data.
In the example below you can see how I managed to get my own data in the bar chart.

Vertical instead of horizontal

I wanted my bar charts to have vertical bars instead of horizontal ones.
So I rewrote my bar chart following the tutorial once again but changing my x and y axis.

my scales code before:

  const xScale = scaleLinear()
  	.domain([0, max(data, xValue)]) // max of data capacity
  	.range([0, innerWidth]); // width of svg
  
  const yScale = scaleBand()
  	.domain(data.map(yValue)) 
  	.range([0, innerHeight])
  	.padding(0.2); 

my scales after changing it from horizontal to vertical

  const xScale = scaleBand()
  	.domain(data.map(xValue))
  	.range([0, innerWidth])
  	.padding(0.2);

  const yScale = scaleLinear()
  	.domain([0, max(data, xValue)])
  	.range([0, innerHeight]);

But this resulted in my y-axis starting at max and going to 0 on the top. Also, my bars were on top.

Fixing my bars

So I had to rotate the values from 0 to max, after struggling and asking for help Macro helped me to set my axis right.

old:  	.domain([0, max(data, xValue)])
new:  	.domain([max(data, yValue), 0])

adding:
.append('rect')
  .attr('height', d => innerHeight - yScale(yValue(d)));

So I had to rotate how my data was imported. This fixed all my bars where still on top.
I had to set the attribute for Height in my bars so they started at the height minus the yScale yvalue.

Now with my own data

Now that I had the horizontal bar chart set up, I could start by adding my own data into the bar chart.

Other visual

Because we have to make an interactive component, I decided I wanted to show all
the P+R parking areas with their capacity as a starting point. In this visual, I changed the data input to show just that.

Adding interaction

As you can see below, I got the bars moving but my axis didn't get replaced but just added on top of it.
BC_Version6-interaction

Fixing interaction

Instead of targeting the axis, I just added a new axis apparently.
This was done by the code below:

//Update the ticks
const yAxis = axisLeft(yScale)
  .tickSize(-innerWidth);
    
const xAxis = axisBottom(xScale)

  // y Axis GROUP G
  const yAxisG = g.append('g').call(yAxis)
  
  // x Axis GROUPING
  const xAxisG = g.append('g').call(xAxis)
  
  xAxisG
    .attr('transform', `translate(0, ${innerHeight})`)
    .selectAll("text")
        .attr("transform", `rotate(90)`)
        .attr('text-anchor', 'start')
        .attr('x', 10)
        .attr('y', -2)
 

But during a video call with my teacher Laurens, we found out that I should create classes on the groups (not on the individual ticks)
and get those classes to update the information. So I created classes on the right groups by inspecting the dom elements.

  yAxisG
    .attr('class', 'axis-y')

  xAxisG
    .attr('class', 'axis-x')

Now I could target these classes and update the axis information

svg.select('.axis-x')
  .call(axisBottom(xScale))
    .attr('transform', `translate(0, ${innerHeight})`)
  .selectAll("text")
    .attr("transform", `rotate(50)`)
    .attr('text-anchor', 'start')
    .attr('x', 10)
    .attr('y', 5)
svg.select('.axis-y')
  .call(axisLeft(yScale).tickSize(-innerWidth))
    .selectAll('.domain') 
      .remove()

BC_version8_itWorks

Adding transitions

To make my bar chart more pretty I decided to add some transitions.
It was a bit of a search to find out how I could transitions them from the bottom upwards,
but I had to create a base point of nothing and then letting them grow into position by using the following code:

.attr("y", function(d) {
       return yScale(0);
      })
.attr("height", 0)
.transition().duration(1000)
.attr('y', d => yScale(yValue(d)))
.attr("height", d => innerHeight - yScale(yValue(d)));

So this codes starts with creating a 0 basepoint for the yScale and the height of the bars.
Then with a transition with a duration of 1 second it creates the proper yScale and height the bars had originally.
BC_version9_Transitions