Contents

How to Add an Update Function in D3.js? [Part 1]

Contents

In this post, I will show you how to move from a static visualization where all the code is only running once to a dynamic visualization where a portion of the code is constantly executing one function.

To do this, We will define an update function called inside the interval callback that we wrote in a previous article (see: https://www.datackling.com/post/how-to-loop-with-intervals-in-d3-js ).

This require us to find which part of the code should run every time our data changes and which parts should run once.

We will need to change our scales and axes so that they update based on the data. If the chart changes to show more data point, our visualization will scale accordingly.

We also need to update the label on the Y axis. We want also to change the size and the position of the rectangles according to the new data.

In this article we will focus on make the scales and the axes dynamic before update the rectangles in a next article.

First we define an update function at the end of the main.js file:

function update(data){ }

This function take the data load above in the code (see: https://www.datackling.com/post/how-to-load-external-data-in-d3-js ) Then we call the update function in the interval callback:

1
2
3
d3.interval( () => {
	update(data)
}, 1000)

Then we move all the code corresponding to the axis and the scales into the update function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function update(data){
// Scaling the y axis
    const y = d3.scaleLinear()
       .domain([0, d3.max(data, d => d.revenue)])    // input
       .range([340, 0])  // output

    // Scaling the x axis
    const x = d3.scaleBand()
        .domain(data.map(d => d.year))    // input
        .range([0, 480])
        .paddingInner(0.5)
        .paddingOuter(0.2)// ouput

    const xAxisCall = d3.axisBottom(x)
    g.append("g")
        .attr("class", "x axis")
        .attr("transform", `translate(0, 340)`)
        .call(xAxisCall)
        .selectAll("text")
        .attr("y", "10")
        .attr("x", "-5")
        .attr("text-anchor", "end")
        .attr("transform", "rotate(-40)")

    const yAxisCall = d3.axisLeft(y)
    .ticks(3)
    .tickFormat(d => "$"+d)
    g.append("g")
      .attr("class", "y axis")
      .call(yAxisCall)

}

See this article to know what is going on for the axis and the scales. We will focus on the rectangle of the chart in a next post so they do not appear here.

What do we need to update here? For the scales, we only need to update the domain function because it is the only part that depend on the data.

We move the scales outside the update function keeping the domain definition into it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function update(data){
    y .domain([0, d3.max(data, d => d.revenue)])    // input
    x.domain(data.map(d => d.year))    // input

    const xAxisCall = d3.axisBottom(x)
    g.append("g")
        .attr("class", "x axis")
        .attr("transform", `translate(0, 340)`)
        .call(xAxisCall)
        .selectAll("text")
        .attr("y", "10")
        .attr("x", "-5")
        .attr("text-anchor", "end")
        .attr("transform", "rotate(-40)")

    const yAxisCall = d3.axisLeft(y)
    .ticks(3)
    .tickFormat(d => "$"+d)
    g.append("g")
      .attr("class", "y axis")
      .call(yAxisCall)

}

Now, we will append the axis group “g” just once at the top the file. So remove them from the update function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const xAxisGroup = g.append("g").attr("class", "x axis").attr("transform", `translate(0, 340)`)

const yAxisGroup = g.append("g").attr("class", "y axis")

function update(data){
    y .domain([0, d3.max(data, d => d.revenue)])    // input
    x.domain(data.map(d => d.year))    // input

    const xAxisCall = d3.axisBottom(x)
    xAxisGroup.call(xAxisCall).selectAll("text")
        .attr("y", "10")
        .attr("x", "-5")
        .attr("text-anchor", "end")
        .attr("transform", "rotate(-40)")

    const yAxisCall = d3.axisLeft(y)
    .ticks(3)
    .tickFormat(d => "$"+d)
    yAxisGroup.call(yAxisCall)
}

So now, the axis group will be add just one time and the labels will be updated automatically with the data.

When you reload the page, you can see a delay when we do not see the axis. The first run of the loop waits for the delay to start. This can be easily fix by adding a call to the update function in the interval callback.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
d3.csv("mydata.csv").then(data => {
	data.forEach(d => {
		d.revenue = Number(d.revenue)
	})
d3.interval( () => {
	update(data)
}, 1000)
update(data)

})

Now, when you will add data, scales and axis will update automatically. I hope you enjoyed this article. I know that it is more complex than other so if you have questions, feel free to ask me in DM.

See you!