D3.js: visualize your imagination🎨
By: Harrison Xia
Tags: Star Wars, Visualization, JavaScript, Big Data, D3

With that all out of the way, let’s start building a bar chart using the Star Wars API. The x-axis will be the characters’ names, and y-axis will be their heights. We will add a simple button such that every time a user clicks on the button, it will make an API call and get a new character’s information from the Star Wars API. It is also not a bad idea to have the chart load some initial data before any user interaction.

First, we will create a new HTML file, add D3 default bundle to our header, add an empty SVG tag and a button inside the body tag. We will stick to the KISS design principle today, aiming to build a minimal viable program.

<!DOCTYPE html> <html> <head> <script src=”https://d3js.org/d3.v5.min.js"></script> </head> <body> <svg/> <br/> <button id=”addData”>Add data</button> <script></script> </body> </html>

Let’s get into JavaScript! All of the following code should be within the <script> tag. Before diving into the D3 part, it’s generally a great idea to have our API calls in place first. Otherwise, we will not have data to draw, hence no way to debug.

We will create two variables here to count how many characters have we put on the charts and a list to keep track of all the data.

let curCount = 1 let curData = []

Add an EventListener to the button we created.

document.getElementById(‘addData’).addEventListener(‘click’, getData)

There are two ways to make API calls using Promises as we explained earlier. Both work the same.

const getData = () => { d3.json(‘https://swapi.co/api/people/' + curCount).then((res) => { curCount++ drawChart(res) }).catch(reason => { console.error(reason) }) } // Alternatively you can also do async function getData() { let res = await d3.json(‘https://swapi.co/api/people/' + curCount) curCount++ drawChart(res) } // Initial run getData()

Set the margin, width, and height. We all want D3 to select the SVG we just created earlier. Feel free to customize those values as you wish.

“g” element is used to group SVG shapes together.

const margin = 50 const width = 1000 – 2 * margin const height = 500 – 2 * margin const svg = d3.select(‘svg’).attr(‘width’, 1000) .attr(‘height’, 500) const chart = svg.append(‘g’) .attr(‘transform’, `translate(${margin}, ${margin})`)

You won’t see anything on your screen just yet except the button. For a basic bar chart, there are a few must-have elements: the bars, x-axis, and y-axis. We will start by creating x-axis and y-axis. Band scales are like ordinal scales except the output range is continuous and numeric. Learn more about d3-scale here.

let x = d3.scaleBand().range([0, width]).padding(0.3) let y = d3.scaleLinear().range([height, 0])

Now we create a function that draws the chart when called upon.

const drawChart = (data) => { curData.push(data) }

Note that we just haven’t initialized our axes with values, now would be a great time to do so inside of this function.

x.domain(curData.map(function (d) { return d.name }))

y-axis takes in a list of size 2, lower and upper range respectively. d3.max() gets the max value from a particular attribute of an object.

y.domain([0, d3.max(curData, function (d) { return parseInt(d.height) })])

Now we add two axes to the chart.

// x-axis chart.append(‘g’) .attr(‘class’, ‘x axis’) .attr(‘transform’, `translate(0, ${height})`) .call(d3.axisBottom(x)) // y-axis chart.append(‘g’) .attr(‘class’, ‘y axis’) .call(d3.axisLeft(y))

At this point, we can check if the two axes have appeared on the chart with values initialized correctly.

Both axes with values initialized.

It’s finally the time to plot some data on our chart. Note I choose to parse heights into int here, but for better practice, consider formatting your data the way you want when you first receive the response from API calls to avoid redundancy.

const barGroups = chart.selectAll() .data(curData) .enter() .append(‘g’) barGroups.append(‘rect’) .attr(‘class’, ‘bar’) .attr(‘x’, (g) => x(g.name)) .attr(‘y’, (g) => y(parseInt(g.height))) .attr(‘height’, (g) => height — y(parseInt(g.height))) .attr(‘width’, x.bandwidth())

Refresh the page, you should see Luke Skywalker, aka Darth Vader’s son, on the chart.

However, if we click the button several times, we will see bars getting overlapped on top of each other.

To solve this, add the following line at the beginning of drawChart() function to eliminate multiple drawing of the same data.

svg.selectAll(‘.bar’).remove().exit()

Learn more about D3’s exit() and enter() function here.

Another unexpected behavior you may have noticed is that x-axis and y-axis also have overlapping values. The solution is to only append two axes only once on the initial run and update two axes later on each time you update the chart.

// Initial run if (curData.length === 1) { // x-axis chart.append(‘g’) .attr(‘class’, ‘x axis’) .attr(‘transform’, `translate(0, ${height})`) .call(d3.axisBottom(x)) // y-axis chart.append(‘g’) .attr(‘class’, ‘y axis’) .call(d3.axisLeft(y)) } else { // x-axis chart.select(‘.x.axis’) .attr(‘transform’, `translate(0, ${height})`) .call(d3.axisBottom(x)) // y-axix chart.select(‘.y.axis’) .call(d3.axisLeft(y)) }

We now have a fully functioning bar chart using new features from D3.js v5! I made some extra styling changes along with dynamic grid and animation. Check my final full code here. Some of the stylings was inspired by Mr. Máté Huszárik’s article here (@matehuszarik).

Full code here: