DEV Community

Pierre Le Bras
Pierre Le Bras

Posted on • Edited on

Want to learn D3? Let's make a bar chart!

D3 is one of the most widely used JavaScript chart library out there. It is free, open-source, and while it may be daunting at first, it provides unlimited customisation for your interactive data visualisations.

I have taught it for many years now. We usually have to accommodate for a variety of experiences from students and teach using examples they have to complete, but some of the more hands-on learners sometimes need to do things by themselves from start to finish.

While I was not too sure what to tell them at first, I realise over time that a great way to play with D3 for beginners is to make bar charts.

It may seem trivial at first (and compared to other charts, it is), but making a bar chart in D3 actually lets you explore quite the number of key concepts for you to progress further. So let's get started.

What we want to achieve

Normally we would match the chart to the type of data we are given, not the other way around. But this is a tutorial about bar charts so we will have to work in reverse just for now.

Bar charts typically show elements with two attributes: a category, or key, and a value used to compare categories (check this post from the Data Visualisation Catalogue).

Bar chart example

So let's imagine you are given this data:



const data1 = [{key: 'A', value: 30},{key: 'B', value: 20},
               {key: 'E', value: 50},{key: 'F', value: 80},
               {key: 'G', value: 30},{key: 'H', value: 70},
               {key: 'J', value: 60},{key: 'L', value: 40}];


Enter fullscreen mode Exit fullscreen mode

Our goal is to map it onto a set of rectangles, spread across vertically, with their width scaling to the value attribute.

Setup

We will start by making a simple HTML page, where we load D3's library and add one title and a div:



<!DOCTYPE html>
<html>
<head>
    <title>D3 Bar Chart</title>
    <script type="text/javascript" src="https://d3js.org/d3.v6.min.js"></script>

    <style type="text/css">
    /* our custom styles */
    </style>
</head>
<body>
    <h1>D3 Bar Chart Example</h1>
    <div id="barContainer"></div>

    <script type="text/javascript">

        const data1 = [{key: 'A', value: 30},{key: 'B', value: 20},
                       {key: 'C', value: 60},{key: 'D', value: 40},
                       {key: 'E', value: 50},{key: 'F', value: 80},
                       {key: 'G', value: 30},{key: 'H', value: 70}];

        const width = 600, height = 400, margin = {t:10,b:30,l:30,r:10};
    </script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

Note: We are working with version 6 of D3.

We have also added our data and a set of values for our chart's dimensions. We will reuse these values multiple times, so we better save them in constants.

We are all set up here, and we know what we want to do. Let's create our bar chart with D3 now.

Selections

To understand what D3 is, it is always useful to remind ourselves of what it stands for:
Data Driven Documents.

The title says it all, it is a library that lets us manipulate the Document Object Model (DOM) of HTML pages using data. And the D3 way to do that is with Selections. To make it simple, selections are like wrappers for DOM elements, giving us an API to program these elements (there is more to it, but we will get there in a bit).

Single selection

Say we want to add an SVG element to our div using D3. The way to do this is to select the parent (the div element) and append an svg element to it. And because the append method returns the newly created element selection, we can use it to set our chart's dimensions and save that selection into a variable.



const svg = d3.select('div#barContainer') // use the same css selectors to target your selections
    .append('svg')                        // our selection now maps to a newly created svg
    .attr('width', width)                 // the .attr() method lets you set the attribute value of your element
    .attr('height', height)
    .style('border', 'solid 1px #222');   // the .style() method lets you set the style of you element


Enter fullscreen mode Exit fullscreen mode

The code above does just that, select our container adds an SVG to it, and saves the new selection in a variable svg. It does two additional things. With the .attr() method, we set the SVG's width and height using the values we defined previously and with the .style() method, we give a style to the SVG's border.

In fact, if we run our code in a browser, it displays the following:
Page svg
And inspecting our DOM, here is what you should get:
Dom svg

I recommend using a local server to that, for example, python's or a node server

Logically, this new SVG is the root element of our bar chart and saving its selection in a variable means we can access it across our code easily. For example, let's define a chart area, where we will later draw our bars:



const chart = svg.append('g') // add a group to the svg
    .classed('chart', true)   // give our svg group a class name
    .attr('transform', `translate(${margin.l},${margin.t})`); // set the transform attribute to translate the chart area and set margins


Enter fullscreen mode Exit fullscreen mode

Here we use a new method, .classed() as a way to set a class name for this SVG group. You use the same method to remove a class from an element, by putting false as your second parameter. You could technically use .attr('class', ...), but beware of this, since it will replace the whole value for the attribute class. The method .classed() remains the safest way to add/remove classes from elements.

We have also translated this group, using the margin values. Now, any element appended to this group will be drawn from a new reference point.
Chart area


Quick Summary of selection methods
There are many methods you can use with selections, and I encourage you to take a look at the API for more details. But for now, here is a quick summary of what we have seen so far.

Method Description
d3.select(selector) Creates a selection with the first element matching the selector's criteria
selection.select(selector) Creates a new selection with the first child element matching the selector's criteria
selection.append(element) Adds a child element and returns it as a new selection
selection.attr(name, value) Sets the attribute name for the elements mapped in the selection
selection.style(name, value) Sets the style name for the elements mapped in the selection
selection.classed(name, bool) Adds or remove a class name to the elements mapped in the selection

Bind, Enter, Exit, Update: the General Update Pattern

So far, what we have seen about selections is pretty basic, and you might be thinking that it is probably not worth using a whole library for that.

But we have only just scratch the surface. Remember that D3 stands for Data Driven Documents.

Binding data

Where D3's selections become truly useful is with data binding. In essence, this makes the selection a bridge between your data and the DOM.

Data binding

We do so by calling the .data() selection method:


 javascript
let bars = chart.selectAll('rect.bar'); // from chart, select all rect element with class bar in 
bars = bars.data(data1, d=>d.key);      // bind data to the bars and save the selection


Enter fullscreen mode Exit fullscreen mode

The .selectAll() method is similar to the .select() we have seen before. But instead of selecting the first DOM element matched by the selector, .selectAll() selects all the elements that matched. In this instance, it is all the SVG rectangles, with the class bar, children of our chart SVG group.

Then, the .data() method binds our data to the selection. The method's second parameter is what we call the key function, it is used to identify the data entry and create a unique link with the selection entry.

At this stage however, you might be wondering: where are all this SVG rectangles?. And you would be right, we have not created them yet. But we will use D3 to build exactly what we need.

Updating the DOM to match the dataset

When you bind data to a selection, the .data() method returns a new version of the selection, where its entries are separate in three categories: the new, the old, and the obsolete.

The new

The new are data entries the selection has no DOM element to match with (according to the key function). This is referred to as the enter selection and is accessed with the .enter() method.



// the new, create the element from scratch
bars.enter().append('rect')
    .classed('bar', true)
    .attr('x', 0)
    .attr('y', (d,i)=>i*35)
    .attr('height', 30)
    .attr('width', d=>d.value*6);


Enter fullscreen mode Exit fullscreen mode

Because these rectangles are new, we have to create them (.append()) and set all of their attributes/style.

For some of these attributes, you will notice we did not use a fixed value as we did before. Because we bound our data to them, we can customise their look to fit with the data. That is where we can drive our document from the data and create awesome charts! Essentially you can now use functions to decide on the value of your attributes (or style). These functions have three parameters: the element's datum d, the element's index i, and the group the element is part of nodes.

Here we set the rectangles' positions to align them on the left (x = 0) and distribute them vertically using the elements' indices (y(d,i) = i*35). We also set the rectangles' sizes to a fixed height (height = 30) and a width function of the data value (width(d) = d.value*6).

And like that, we have bars, straight from the data we were "given" earlier.

page_1

The old

But let's finish touring our sub-selection. While we have not faced such case yet, it could be that the chart's elements you are currently drawing already exist, and used an older version of the data.

The second sub-selection, the old, are data-DOM links the selection used to have and which are still there (again, according to the key function), but with possibly new values. This is sometimes referred to as the update selection. You do not need a specific method to access it, just the selection variable.



// the old, just update the bar position and length
bars.attr('y', (d,i)=>i*35)
    .attr('width', d=>d.value*6);


Enter fullscreen mode Exit fullscreen mode

Here, we just change what is dependent on the data: the vertical position of the bar and its length.

The obsolete

Finally, the obsolete are DOM elements the selection has no data to attach to anymore (you guessed it, according to the key function). This is referred to as the exit selection and is accessed with the .exit() method.



bars.exit().remove();


Enter fullscreen mode Exit fullscreen mode

Here, we simply use the .remove() method to delete the rectangles which are not needed anymore.

The General Update Pattern

What we have just seen constitutes D3's General Update Pattern. It is a process typically followed when updating your charts:

  1. Bind the data
  2. Create the enter selection
  3. Remove the exit selection
  4. Update the selection's old entries

General Update Pattern

It is often a good idea to wrap it in a function, where you just need to give a dataset, and your script will draw the new or updated chart:



function updateData(dataset){
    // make our selection
    let bars = chart.selectAll('rect.bar');
    // bind data
    bars = bars.data(dataset, d=>d.key);
    // create the new    
    bars.enter().append('rect')
        .classed('bar new', true)
        .attr('x', 0)
        .attr('y', (d,i)=>i*35)
        .attr('height', 30)
        .attr('width', d=>d.value*6);
    // remove the obsolete
    bars.exit()
        .classed('obs', true)
        .remove();
    // update the old
    bars.classed('new', false)
        .attr('y', (d,i)=>i*35)
        .attr('width', d=>d.value*6);
}


Enter fullscreen mode Exit fullscreen mode

Notice how I added a class new to the new elements, obs to the obsolete elements, and removed the new class for old ones. We can use it to see which rectangles are new when the chart updates:



svg > g.chart > rect.bar{
    fill: steelblue;
    stroke-width: 1px;
    stroke: #444;
}
svg > g.chart > rect.bar.new{
    fill: seagreen;
}
svg > g.chart > rect.bar.obs{
    fill: tomato;
}


Enter fullscreen mode Exit fullscreen mode

Now, we are repeating ourselves with the enter and update selections, and from a programming point of view, this is not quite right. Since they will be the same for both selections, we should be setting the rectangles' position and width all at once, which is possible thanks to the .merge() method:



function updateData(dataset){
    // make our selection
    let bars = chart.selectAll('rect.bar');
    // bind data
    bars = bars.data(dataset, d=>d.key);
    // create the new and save it
    let barsEnter = bars.enter().append('rect')
        .classed('bar new', true)
        .attr('x', 0)
        .attr('height', 30);
    // remove the obsolete
    bars.exit()
        .classed('obs', true)
        .remove();
    // update old alone
    bars.classed('new', false);
    // merge old and new and update together
    bars.merge(barsEnter)
        .attr('y', (d,i)=>i*35)
        .attr('width', d=>d.value*6);
}


Enter fullscreen mode Exit fullscreen mode

Setting attributes for the enter and update selection is actually the 5th optional step of the General Update Pattern. We can now use this update function to render and update our bar chart:



// assume a second set of data, updating data1
const data2 = [{key: 'A', value: 40},{key: 'C', value: 20},
               {key: 'D', value: 10},{key: 'F', value: 50},
               {key: 'G', value: 60},{key: 'H', value: 90},
               {key: 'I', value: 10},{key: 'J', value: 30},
               {key: 'K', value: 50},{key: 'L', value: 80}];

// calling our update function
setTimeout(()=>{updateData(data1)}, 1000);
setTimeout(()=>{updateData(data2)}, 5000);


Enter fullscreen mode Exit fullscreen mode

GUP_1

It's alive!! However, the update is not really salient. But do not worry, we can use transitions for this.


Quick Summary of selection methods
Again, here is a recap of the methods we have seen in this section.

Method Description
d3.selectAll(selector) Creates a new selection with all the elements matching the selector's criteria
selection.selectAll(selector) Creates a new selection with all the children elements matching the selector's criteria
selection.data(dataset, keyFunction) Binds data to the selection
selection.enter() Accesses the enter selection
selection.exit() Accesses the exit selection
selection.remove() Removes elements of the selection from the DOM
selection.merge(selection2) Merges selections together

Animating your chart

You would have guessed it, D3 also provides us with means to add animations to our chart. They are particularly useful to transition between your charts' updates to check what is exactly happening. As such, D3 conveniently named this concept Transitions.

Now, back to our update function. We will need three different transitions in the following order:

  1. removing the exit selection;
  2. positioning the enter and update selections;
  3. adjusting the length of the enter and update selections.


const tRemove = d3.transition();
const tPosition = d3.transition();
const tSize = d3.transition();


Enter fullscreen mode Exit fullscreen mode

The API of transitions is quite similar to the selections one. One difference however, is that it provides methods for timing the animations. The most important ones being .duration() to set the animation span, and .delay() to postpone the animation start. Using these methods, we can customise our transitions:



const d = 500;                    // our base time in milliseconds
const tRemove = d3.transition()
    .duration(d);                 // 500ms duration for this animation
const tPosition = d3.transition()
    .duration(d)
    .delay(d);                    // 500ms wait time before this animation starts
const tSize = d3.transition()
    .duration(d)
    .delay(d*2);                  // 1000ms wait time before this animation starts


Enter fullscreen mode Exit fullscreen mode

In the code above we are essentially creating 3 transitions that will animate our selections for 500ms, but should be launched one after the other. Note that the default value for durations is 250ms and 0ms for delays.

Next, we need to add these transition in our update pattern:



// ...
// remove the obsolete
bars.exit()
    .classed('obs', true)
    .transition(tRemove)          // remove transition
    .attr('width', 0)             // animate the length to bars to 0
    .remove();                    // delete the rectangles when finished
// ...
// merge old and new and update together
bars.merge(barsEnter)
    .transition(tPosition)        // position transtition
    .attr('y', (d,i)=>i*35)       // align all rectangles to their vertical position
    .transition(tSize)            // size transition
    .attr('width', d=>d.value*6); // set the rectanble sizes


Enter fullscreen mode Exit fullscreen mode

As you can see we use the .transition() method to apply the predefined transitions to our selections. Note that once a transition applied, the chained methods (.attr() for example) are transition methods. As such, they may behave differently: .remove(), for example, only deletes elements when the transition ends.

For the same reason, transitions do not work with the .classed() method. And since we are using classes to style your chart (which I strongly recommend for global styles), it is best to add the appropriate CSS transitions:



svg > g.chart > rect.bar{
    fill: steelblue;
    stroke-width: 1px;
    stroke: #444;
    transition: fill 300ms;
}


Enter fullscreen mode Exit fullscreen mode

And then call the .classed() method outside of transitions, using a timeout. Adding the following at the end of our function will return the bars to their default style once the update is complete:



setTimeout(()=>{bars.merge(barsEnter).classed('new', false)}, d*4)


Enter fullscreen mode Exit fullscreen mode

And just like that, we have got a complete update transition, which makes it easier to follow what is happening.

GUP_2

Next, we will see how to better manage our chart area.


Quick Summary of transition methods
Here are the transition methods we have seen in this section, and what are probably the most common ones.

Method Description
d3.transition() Creates a new transition
transition.duration(value) Sets the duration (in milliseconds) of the transition
transition.delay(value) Sets the delay (in milliseconds) before the transition can start
selection.transition(t) Applies transition t to your selection

Scaling our charts to the view

So far, we have been setting our bar height with an arbitrary value (30), from which we had to infer the space between the bars (35 = 30 bar height + 5 spacing). Similarly, we have arbitrarily decided that the bars' length will be a product of 6. All of that worked okay so far, but as we have seen, any data update could suddenly change the number of entries or the maximum value, which makes our arbitrary decisions impractical.

We could be all fancy and come up with ways to automatically compute, with every new dataset, what value we should use. Or we could use D3's Scales.

These scales have one simple task, mapping a domain to a range, but come with a lot of perks. Typically, you would use them to map from your data domain to your view range, which is what we will do now. They are many scales available, but we will look at two in particular: the continuous-linear scale, and the ordinal-band scale.

Getting the correct length of bars

The first scale we will look at is the continuous linear scale. This is the most forward scale, as the name suggests, it simply maps, linearly, a continuous domain to a continuous range.

scale_linear

It is the perfect tool to ensure that our bars are always contained within our chart view while keeping the ratio between bar lengths correct, after all, that is the point of bar charts.

To use it, we will simply create an instance of linear scale, and set the boundaries of its domain and range:



const xScale = d3.scaleLinear()
    .domain([0, d3.max(dataset, d=>d.value)])
    .range([0, width-margin.l-margin.r]);


Enter fullscreen mode Exit fullscreen mode

With this scale, we keep the same origin 0, however, we match the maximum value from our dataset with the maximum length possible (the width minus horizontal margins). To get the maximum dataset value, I have used one D3's Array methods, .max(), by providing it with the appropriate accessor function.

We can now use this scale to scale our bars so that they always fit in length:



// ...
// create the new and save it
let barsEnter = bars.enter().append('rect')
    .classed('bar new', true)
    .attr('x', xScale(0))               // in case we change our origin later
    .attr('height', 30); 
// ...
// merge old and new and update together
bars.merge(barsEnter)
    .transition(tPosition)
    .attr('y', (d,i)=>i*35)
    .transition(tSize)
    .attr('width', d=>xScale(d.value)); // scaling the bar length
}


Enter fullscreen mode Exit fullscreen mode

Spreading the bars evenly

The second scale we will look at is an ordinal band scale: our domain is categorical (no longer continuous) but our range remains continuous. Essentially it divides our range into even bands and map them to the categories in our domain.

scale_band

It will allow us to always position the bars vertically and given the appropriate height, no matter the number of entries in the data.

Like linear scales, we just need to create an instance of it and define its range boundaries. Unlike linear scales, we have to provide the whole domain:



const yScale = d3.scaleBand()
    .domain(dataset.map(d=>d.key))
    .range([0, height-margin.t-margin.b])
    .padding(0.2);


Enter fullscreen mode Exit fullscreen mode

This scale's range goes from 0 to the height of the chart minus vertical margins. The .padding() method lets us define the space (in proportion) between the bands.

Next, we can add it to our update process:



// ...
// create the new and save it
let barsEnter = bars.enter().append('rect')
    .classed('bar new', true)
    .attr('x', xScale(0));              // in case we change our origin later
// ...
// merge old and new and update together
bars.merge(barsEnter)
    .transition(tPosition)
    .attr('y', d=>yScale(d.key))        // scaling the bar position
    .attr('height', yScale.bandwidth()) // using the computed band height
    .transition(tSize)
    .attr('width', d=>xScale(d.value)); // scaling the bar length


Enter fullscreen mode Exit fullscreen mode

Note that we have moved the height definition to the position animation and used the .bandwidth() method to get the computed height from the scale.

And that is all there is to it. Just a few lines of code and we have got bars that are perfectly fitted within their chart.

GUP_3

There are two important components missing to finish our bar chart: axes! But since we have used D3's scales, you will see that axes are going to be a piece of cake.


Quick Summary of scale methods
I have recap below the scale methods we saw in this section. But I encourage you to have a look at D3's API and see how much you can do with scales.

Method Description
d3.scaleLinear() Creates a new linear scale
linearScale.domain([min, max]) Sets the domain boundaries of a linear scale
linearScale.range([min, max]) Sets the range boundaries of a linear scale
d3.scaleBand() Creates a new band scale
bandScale.domain(array) Sets the domain of a band scale
bandScale.range([min, max]) Sets the range boundaries of a band scale
bandScale.padding(value) Sets the padding between bands for a band scale
bandScale.bandwidth() Returns the computed band size of a band scale
d3.max(data,accessor) Returns the maximum value of a dataset according to the accessor function

Don't forget the axes!

Axes and labels are ones of the most crucial elements of data visualisations. Without them, your visualisation loses all its context, making it essentially useless. That is why D3 has an integrated an Axis module which works seamlessly with scales.

To include these, we first need to define a space for them, adding two groups to our svg:



const xAxis = svg.append('g')
    .classed('axis', true)
    .attr('transform', `translate(${margin.l},${height-margin.b})`);
const yAxis = svg.append('g')
    .classed('axis', true)
    .attr('transform', `translate(${margin.l},${margin.t})`);


Enter fullscreen mode Exit fullscreen mode

Next, in our update process, we need to change these group selections to render an updated axis:



d3.axisBottom(xScale)(xAxis.transition(tSize));
d3.axisLeft(yScale)(yAxis.transition(tPosition));


Enter fullscreen mode Exit fullscreen mode

And that is it. D3 axes were made to render D3 scales, and that is what the code above does. To break it down, d3.axisBottom(xScale) creates a new axis, based on xScale, to be rendered with its ticks downwards. We then directly call this axis on the xAxis selection defined before. And the same goes with d3.axisLeft(yScale) (the ticks are directed towards the left). Note that we also applied our transitions to sync the axis change with the bar change.

GUP_4


Quick Summary of axes methods
Like scales, there is a lot more in D3's API, but here are the methods we have used in this section.

Method Description
d3.axisBottom(scale) Creates a new bottom axis based on scale
d3.axisLeft(scale) Creates a new left axis based on scale
axis(selection) Renders the axis within the provided selection

Bonus: Adding interactivity

Interactivity is one of the greatest advantages of browser-based data visualisations. Mousing over the element of one chart can highlight the corresponding element(s) in a second coordinated chart or display a tooltip with more information for context, you can also use clicks on one view to filter data in another view, etc.

It is no surprise then, that D3 added event listeners to its selections. Let's imagine we want to apply a highlight class to our bars when you mouse over it.



svg > g.chart > rect.bar.highlight{
    fill: gold;
    stroke-width: 4px;
}


Enter fullscreen mode Exit fullscreen mode

We can do so with the .on() selection method, which takes two parameters: the event name to listen for, and the callback function to apply. We just need to apply these listeners to our enter selection (they will remain after an update).



//...
let barsEnter = bars.enter().append('rect')
    .classed('bar new', true)
    .attr('x', xScale(0))
    .on('mouseover', function(e,d){
        d3.select(this).classed('highlight', true);
    })
    .on('mouseout', function(e,d){
        d3.select(this).classed('highlight', false);
    });
//...


Enter fullscreen mode Exit fullscreen mode

There are two things to note here. First, we have not used an arrow function like other callbacks, that is because we want to have access to the caller's scope (the element moused over) and use its this to select only the element and apply our class change. Second, the callback does not have the typical parameters (data and index), instead, it uses event and data.

We have added listeners to two events: mousover for the cursor enters the element and mouseout for when it exits.

mouseover

Conclusion

That is it for this tutorial. From just the simple goal of creating a bar chart, we have explored many concepts core to using D3:

  • Selections
  • the General Update Pattern
  • Transitions
  • Scales and Axes
  • Events

There is of course a lot more to D3 than that: data manipulation, layout generators (pies, Voronoi, chords, etc.), geographical maps, colour scales, time and number formatting, complex interactions (brushing, zooming, dragging, forces, etc.), complex transitions. But, hopefully, this tutorial has given you the desire to go further.

Here is the complete code I have used.



<!DOCTYPE html>
<html>
<head>
    <title>D3 Bar Chart</title>
    <script type="text/javascript" src="https://d3js.org/d3.v6.min.js"></script>
    <style type="text/css">
        svg{
            border: solid 1px #222;
        }
        svg > g.chart > rect.bar{
            fill: steelblue;
            stroke-width: 1px;
            stroke: #444;
            transition: fill 300ms;
        }
        svg > g.chart > rect.bar.new{
            fill: seagreen;
        }
        svg > g.chart > rect.bar.obs{
            fill: tomato;
        }
        svg > g.chart > rect.bar.highlight{
            fill: gold;
            stroke-width: 4px;
        }
    </style>
</head>
<body>
    <h1>D3 Bar Chart Example</h1>
    <div id="barContainer"></div>
    <script type="text/javascript">
        // datasets
        let data1 = [{key: 'A', value: 30},{key: 'B', value: 20},
                     {key: 'E', value: 50},{key: 'F', value: 80},
                     {key: 'G', value: 30},{key: 'H', value: 70},
                     {key: 'J', value: 60},{key: 'L', value: 40}];
        let data2 = [{key: 'A', value: 40},{key: 'C', value: 20},
                     {key: 'D', value: 10},{key: 'F', value: 50},
                     {key: 'G', value: 60},{key: 'H', value: 90},
                     {key: 'I', value: 10},{key: 'J', value: 30},
                     {key: 'K', value: 50},{key: 'L', value: 80}];
        // chart dimensions 
        let width = 600, height = 400, margin = {t:10,b:30,l:30,r:10};
        // svg element
        let svg = d3.select('div#barContainer')
            .append('svg')
            .attr('width', width)
            .attr('height', height)
            .style('border', 'solid 1px #222');
        // chart area
        let chart = svg.append('g')
            .classed('chart', true)
            .attr('transform', `translate(${margin.l},${margin.t})`);
        // axes areas
        let xAxis = svg.append('g')
            .classed('axis', true)
            .attr('transform', `translate(${margin.l},${height-margin.b})`);
        let yAxis = svg.append('g')
            .classed('axis', true)
            .attr('transform', `translate(${margin.l},${margin.t})`);
        // update function
        function updateData(dataset){
            // transitions
            let d = 500;
            let tRemove = d3.transition()
                .duration(d);
            let tPosition = d3.transition()
                .duration(d)
                .delay(d);
            let tSize = d3.transition()
                .duration(d)
                .delay(d*2);
            // scales
            let xScale = d3.scaleLinear()
                .domain([0, d3.max(dataset, d=>d.value)])
                .range([0, width-margin.l-margin.r]);
            let yScale = d3.scaleBand()
                .domain(dataset.map(d=>d.key))
                .range([0, height-margin.t-margin.b])
                .padding(0.2);
            // axes
            d3.axisBottom(xScale)(xAxis.transition(tSize));
            d3.axisLeft(yScale)(yAxis.transition(tPosition));
            // update pattern
            // initial selection
            bars = chart.selectAll('rect.bar');
            // data binding
            bars = bars.data(dataset, d=>d.key);
            // exit selection
            bars.exit()
                .classed('obs', true)
                .transition(tRemove)
                .attr('width', 0)
                .remove();
            // enter selection
            let barsEnter = bars.enter().append('rect')
                .classed('bar new', true)
                .attr('x', xScale(0))
                .on('mouseover', function(e,d){
                    d3.select(this).classed('highlight', true);
                })
                .on('mouseout', function(e,d){
                    d3.select(this).classed('highlight', false);
                });
            // update selection
            bars.classed('new', false);
            // enter + update selection
            bars.merge(barsEnter)
                .transition(tPosition)
                .attr('y', d=>yScale(d.key))
                .attr('height', yScale.bandwidth())
                .transition(tSize)
                .attr('width', d=>xScale(d.value));
            // class reset
            setTimeout(()=>{bars.merge(barsEnter).classed('new', false)}, d*4)
        }

        setTimeout(()=>{updateData(data1)}, 2000)
        setTimeout(()=>{updateData(data2)}, 6000)
    </script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

Top comments (0)