Horizontal Bar Chart

# D3

This horizontal bar chart—built with D3—shows the total points scored by each English Premier League team in the 2022/2023 season.

How I built the chart

It all begins with the data.

Manchester City won the league with 89 points while Southampton’s 25 points meant they finished the season as bottom of the rung.

TeamPoints
1Man City89
2Arsenal84
3Man United75
4Newcastle71
5Liverpool67
6Brighton62
7Aston Villa61
8Tottenham60
9Brentford59
10Fulham52
11Crystal Palace45
12Chelsea44
13Wolves41
14West Ham40
15Bournemouth39
16Nottingham Forest38
17Everton36
18Leicester City34
19Leeds United31
20Southampton25

Data

The data is structured as an array of each team’s name and total points. I didn’t have to do any cleanup here because I have total control over the data’s structure.

const eplStandings2022_23 = [
  { team: "Man City", pts: 89 },
  { team: "Arsenal", pts: 84 },
  { team: "Man United", pts: 75 },
  { team: "Newcastle", pts: 71 },
  { team: "Liverpool", pts: 67 },
  { team: "Brighton", pts: 62 },
  { team: "Aston Villa", pts: 61 },
  { team: "Tottenham", pts: 60 },
  { team: "Brentford", pts: 59 },
  { team: "Fulham", pts: 52 },
  { team: "Crystal Palace", pts: 45 },
  { team: "Chelsea", pts: 44 },
  { team: "Wolves", pts: 41 },
  { team: "West Ham", pts: 40 },
  { team: "Bournemouth", pts: 39 },
  { team: "Nottm Forest", pts: 38 },
  { team: "Everton", pts: 36 },
  { team: "Leicester City", pts: 34 },
  { team: "Leeds United", pts: 31 },
  { team: "Southampton", pts: 25 },
];

Dimensions

The width of the canvas/svg will be the same as the width of its parent container, in this case, chartContainer. The other dimensions are arbitrary. For example, marginBottom and marginRight are set to 0 since no spacing is needed on those axes, that is, axisBottom and axisRight.

The height of the chart is calculated based on the barHeight, the vertical margins, and the total number of teams.

const chartContainer = document.getElementById("chart-container");

const marginTop = 25;
const marginRight = 0;
const marginBottom = 0;
const marginLeft = 90;
const width = chartContainer.getBoundingClientRect().width;

const barHeight = 40;
const height =
  Math.ceil((eplStandings2022_23.length + 0.1) * barHeight) +
  marginTop +
  marginBottom;

Drawing the canvas

const canvas = drawCanvas();

The canvas is the svg. It is created (with its viewBox ) and appended to the chartContainer

function drawCanvas() {
  const canvas = d3
    .create("svg")
    .attr("width", width)
    .attr("height", height)
    .attr("viewBox", [0, 0, width, height]);

  // Always be rendering
  chartContainer.append(canvas.node());

  return canvas;
}

With the canvas drawn, I created the other parts of the charts and placed them on the canvas.

Horizontal Axis: left to right, axisTop / axisBottom

Before drawing the horizontal axis with:

horizontalAxis.draw({ svg: canvas });

is the logic to do so. The draw function relies on the getScale function for its subsistence.

A linear scale, d3.scaleLinear, is used since the data, that is, each team’s points are quantitative (can be counted) and continuous (can be measured) data.

const horizontalAxis = {
  getScale(data) {
    const xDomain = [0, d3.max(d3.map(data, (d) => d.pts))];
    const xRange = [marginLeft, width - marginRight];
    const xScale = d3.scaleLinear(xDomain, xRange);
    return { xDomain, xRange, xScale };
  },
  draw({ data = eplStandings2022_23, svg }) {
    const { xScale } = this.getScale(data);
    const xAxis = d3.axisTop(xScale);

    svg
      .append("g")
      .classed("h-axis", true)
      .append("g")
      .classed("top-axis", true)
      .attr("transform", `translate(0,${marginTop})`)
      .call(xAxis)
      .call((g) => g.select(".domain").remove());
  },
};

Vertical Axis: top to bottom, axisLeft / axisRight

The idea here is similar to that of the horizontal axis.

verticalAxis.draw({ svg: canvas });

The draw and getScale functions also go hand in hand. A band scale, d3.scaleBand, is used since the teams’ names are categorical.

const verticalAxis = {
  getScale(data) {
    const yDomain = d3.groupSort(
      data,
      ([d]) => d.pts,
      (d) => d.team
    );
    const yRange = [height - marginBottom, marginTop];
    const yScale = d3.scaleBand(yDomain, yRange).padding(0.1);
    return { yDomain, yRange, yScale };
  },
  draw({ data = eplStandings2022_23, svg }) {
    const { yScale } = this.getScale(data);
    const yAxis = d3
      .axisLeft(yScale)
      .tickPadding(10)
      .tickSizeInner(0)
      .tickSizeOuter(0);

    svg
      .append("g")
      .attr("transform", `translate(${marginLeft}, 0)`)
      .call(yAxis)
      .call((g) => g.select(".domain").attr("stroke", "var(--green-accent)"));
  },
};

Content

The content of the chart are the bars—and the labels.

With the canvas and axes set, the bars can be drawn. The xScale and yScale are also needed here.

function drawContent({ data = eplStandings2022_23, svg }) {
  const { xScale } = horizontalAxis.getScale(data);
  const { yScale } = verticalAxis.getScale(data);

  // Draw the bars
  svg
    .append("g")
    .attr("fill", "var(--green-fade)")
    .classed("bars", true)
    .selectAll()
    .data(data)
    .join("rect")
    .attr("height", yScale.bandwidth())
    .attr("width", (d) => xScale(d.pts) - xScale(0))
    .attr("x", marginLeft)
    .attr("y", (d) => yScale(d.team));

  // Draw the labels
  svg
    .append("g")
    .classed("labels", true)
    .attr("fill", "currentColor")
    .attr("text-anchor", "end")
    .selectAll()
    .data(eplStandings2022_23)
    .join("text")
    .attr("x", (d) => xScale(d.pts))
    .attr("y", (d) => yScale(d.team) + yScale.bandwidth() / 2)
    .attr("dy", "0.35em")
    .attr("dx", -4)
    .text((d) => `${d.pts} pts`);
}
drawContent({ svg: canvas });