Racing Bar Chart (Using D3 JavaScript Library) - DevSeries

CCarter

Final Boss ®
Moderator
BuSo Pro
Boot Camp
Digital Strategist
Joined
Sep 15, 2014
Messages
4,341
Likes
8,855
Degree
8
Here is the code for those racing bar charts you see everywhere.

This code was coded 100% with ChatGPT from scratch. I didn't write a single line of code. Here is a live example using GDP of the top 10 countries with the highest GDP up to 2022: https://www.makoboard.com/demo/d3/

There are whole Faceless YouTube channels like this that simple uses these charts: @DataIsBeautifulOfficial

Here is a video of the code running:


Here is the HTML code:

Code:
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://d3js.org/d3.v6.min.js"></script>
  <style>
    .axis-label {
      font: 10px sans-serif;
      text-anchor: middle;
    }
    .gdp-label {
      font: 12px sans-serif;
      text-anchor: start;
    }
    .icon {
      width: 40px;
      height: 40px;
    }
    .year-text {
      font: 60px sans-serif;
      text-anchor: end;
      fill: lightgray;
    }
  </style>
</head>
<body>
  <center><svg width="1024" height="500"></svg></center>
  <script>
    var svg = d3.select("svg"),
        margin = {top: 20, right: 100, bottom: 30, left: 100},
        width = +svg.attr("width") - margin.left - margin.right,
        height = +svg.attr("height") - margin.top - margin.bottom;

    var barPadding = 5; // Adjust this value to change the padding/margin of the bars
    var intervalDuration = 300; // Default interval duration
    var startDelay = 100; // Delay before the graphic starts
    var xAxisLineColor = "#999999"; // Default x-axis line color
    var maxLoops = 1; // Maximum number of loops

    var g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    var x = d3.scaleLinear().range([0, width]),
        y = d3.scaleBand().range([0, height]).paddingInner(0.1).paddingOuter(0.1);

    var parseTime = d3.timeParse("%Y");

    // Define icons for each country (you can add more icons here as needed)
    var icons = {
      'USA': 'https://www.makoboard.com/img/d3/flags/com.png',
      'China': 'https://www.makoboard.com/img/d3/flags/cn.png',
      'India': 'https://www.makoboard.com/img/d3/flags/in.png',
      'Germany': 'https://www.makoboard.com/img/d3/flags/de.png',
      'UK': 'https://www.makoboard.com/img/d3/flags/co.uk.png',
      'Japan': 'https://www.makoboard.com/img/d3/flags/co.jp.png',
      'France': 'https://www.makoboard.com/img/d3/flags/fr.png',
      'Russia': 'https://www.makoboard.com/img/d3/flags/ru.png',
      'Canada': 'https://www.makoboard.com/img/d3/flags/ca.png',
      'Italy': 'https://www.makoboard.com/img/d3/flags/it.png'
    };

    // Define custom colors for each country (you can add more colors here as needed)
    var colors = {
      'USA': '#3C3B6E',
      'China': '#FFDE00',
      'India': '#138808',
      'Germany': '#DD0000',
      'UK': '#C8102E',
      'Japan': '#BC002D',
      'France': '#0055A4',
      'Russia': '#CA190C',
      'Canada': '#FF0000',
      'Italy': '#009246'
    };

    d3.csv("real-gdp.csv").then(function(data) {
      data.forEach(function(d) {
        d.date = parseTime(d.date);
        for (const country of Object.keys(d)) {
          if (country !== 'date') {
            d[country] = +d[country];
          }
        }
      });

      var yearData = {};
      data.forEach(function(d) {
        var year = d.date.getFullYear();
        if (!yearData[year]) {
          yearData[year] = [];
        }
        for (const country of Object.keys(d)) {
          if (country !== 'date' && d[country] > 0) {
            yearData[year].push({country: country, gdp: d[country]});
          }
        }
      });

      var years = Object.keys(yearData).map(d => +d).sort((a, b) => a - b);
      years.forEach(function(year) {
        yearData[year].sort((a, b) => b.gdp - a.gdp);
        yearData[year] = yearData[year].slice(0, 10); // Keep only the top 10 countries for each year
      });

      var firstYear = years[0];
      x.domain([0, d3.max(yearData[firstYear], d => d.gdp)]);
      updateYScale(yearData[firstYear]);

      g.append("g")
        .attr("class", "axis axis--x")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(x).ticks(width / 80))
        .selectAll(".tick line")
        .attr("y1", -height)
        .attr("stroke", xAxisLineColor);

      g.append("g")
        .attr("class", "axis axis--y")
        .call(d3.axisLeft(y).tickSize(0).tickPadding(6));

      var bars = g.selectAll(".bar")
        .data(yearData[firstYear], d => d.country)
        .enter().append("rect")
          .attr("class", "bar")
          .attr("x", x(0))
          .attr("y", d => y(d.country))
          .attr("width", d => x(d.gdp))
          .attr("height", y.bandwidth()) // Use y.bandwidth() for bar height
          .attr("fill", d => colors[d.country]);

      var iconsSelection = g.selectAll(".icon")
        .data(yearData[firstYear], d => d.country)
        .enter().append("image")
          .attr("class", "icon")
          .attr("x", d => x(d.gdp) - 60) // Adjust the icon position to the right within the bar
          .attr("y", d => y(d.country) + (y.bandwidth() - 40) / 2) // Center the icon vertically within the bar
          .attr("width", 40)
          .attr("height", 40)
          .attr("href", d => icons[d.country]);

      var gdpLabels = g.selectAll(".gdp-label")
        .data(yearData[firstYear], d => d.country)
        .enter().append("text")
          .attr("class", "gdp-label")
          .attr("x", d => x(d.gdp) + 5)
          .attr("y", d => y(d.country) + y.bandwidth() / 2)
          .attr("dy", ".35em")
          .text(d => `$${d3.format(",")(Math.round(d.gdp))}B GDP`); // Format GDP values with commas and add "B"

      var yearText = svg.append("text")
        .attr("class", "year-text")
        .attr("x", width + margin.left + margin.right - 10)
        .attr("y", height - 50)
        .attr("text-anchor", "end")
        .text(firstYear);

      function updateYScale(data) {
        y.domain(data.map(d => d.country))
         .range([0, height]);
      }

      function update(year) {
        var yearDataSlice = yearData[year];
        x.domain([0, d3.max(yearDataSlice, d => d.gdp)]);
        updateYScale(yearDataSlice);

        var xAxis = g.select(".axis--x").transition()
          .duration(intervalDuration)
          .call(d3.axisBottom(x).ticks(width / 80));

        xAxis.selectAll(".tick line")
          .attr("stroke", xAxisLineColor);

        g.select(".axis--y").transition()
          .duration(intervalDuration)
          .call(d3.axisLeft(y).tickSize(0).tickPadding(6));

        // Update bars
        bars = g.selectAll(".bar").data(yearDataSlice, d => d.country);
        bars.exit().transition().duration(intervalDuration).style("opacity", 0).remove();
        bars.enter().append("rect")
          .attr("class", "bar")
          .attr("x", x(0))
          .attr("y", d => y(d.country))
          .attr("width", d => x(d.gdp))
          .attr("height", y.bandwidth())
          .attr("fill", d => colors[d.country])
          .style("opacity", 0)
          .transition().duration(intervalDuration).style("opacity", 1);

        bars.transition()
          .duration(intervalDuration)
          .attr("y", d => y(d.country))
          .attr("width", d => x(d.gdp))
          .attr("height", y.bandwidth());

        // Update icons
        iconsSelection = g.selectAll(".icon").data(yearDataSlice, d => d.country);
        iconsSelection.exit().transition().duration(intervalDuration).style("opacity", 0).remove();
        iconsSelection.enter().append("image")
          .attr("class", "icon")
          .attr("x", d => x(d.gdp) - 60)
          .attr("y", d => y(d.country) + (y.bandwidth() - 40) / 2)
          .attr("width", 40)
          .attr("height", 40)
          .attr("href", d => icons[d.country])
          .style("opacity", 0)
          .transition().duration(intervalDuration).style("opacity", 1);

        iconsSelection.transition()
          .duration(intervalDuration)
          .attr("x", d => x(d.gdp) - 60)
          .attr("y", d => y(d.country) + (y.bandwidth() - 40) / 2);

        // Update GDP labels
        gdpLabels = g.selectAll(".gdp-label").data(yearDataSlice, d => d.country);
        gdpLabels.exit().transition().duration(intervalDuration).style("opacity", 0).remove();
        gdpLabels.enter().append("text")
          .attr("class", "gdp-label")
          .attr("x", d => x(d.gdp) + 5)
          .attr("y", d => y(d.country) + y.bandwidth() / 2)
          .attr("dy", ".35em")
          .text(d => `$${d3.format(",")(Math.round(d.gdp))}B GDP`)
          .style("opacity", 0)
          .transition().duration(intervalDuration).style("opacity", 1);

        gdpLabels.transition()
          .duration(intervalDuration)
          .attr("x", d => x(d.gdp) + 5)
          .attr("y", d => y(d.country) + y.bandwidth() / 2)
          .tween("text", function(d) {
            var i = d3.interpolateNumber(+this.textContent.replace(/[^\d]/g, ''), d.gdp);
            return function(t) {
              this.textContent = `$${d3.format(",")(Math.round(i(t)))}B GDP`;
            };
          });

        yearText.transition()
          .duration(intervalDuration)
          .text(year);
      }

      var currentIndex = 0;
      var loopCount = 0;
      setTimeout(function() {
        var interval = setInterval(() => {
          update(years[currentIndex]);
          currentIndex = (currentIndex < years.length - 1) ? currentIndex + 1 : 0;
          if (currentIndex === 0) {
            loopCount++;
            if (loopCount >= maxLoops) {
              clearInterval(interval);
            }
          }
        }, intervalDuration);
      }, startDelay);
    }).catch(function(error) {
      console.error("Error loading or parsing data:", error);
    });
  </script>
</body>
</html>

Here is the CSV that it calls (you'll need to save this as real-gdp.csv):

Code:
date,USA,China,India,Germany,UK,Japan,France,Russia,Canada,Italy
1960,543.30,59.72,37.03,0,73.23,44.31,62.23,0,40.46,40.39
1961,563.30,50.06,39.23,0,77.74,53.51,67.46,0,40.93,44.84
1962,605.10,47.21,42.16,0,81.25,60.72,75.61,0,42.23,50.38
1963,638.60,50.71,48.42,0,86.56,69.50,84.76,0,45.03,57.71
1964,685.80,59.71,56.48,0,94.41,81.75,94.01,0,49.38,63.18
1965,743.70,70.44,59.55,0,101.82,90.95,101.54,0,54.52,67.98
1966,815.00,76.72,45.87,0,108.57,105.63,110.05,0,61.09,73.65
1967,861.70,72.88,50.13,0,113.12,123.78,118.97,0,65.67,81.13
1968,942.50,70.85,53.09,0,107.76,146.60,129.79,0,71.83,87.94
1969,1019.90,79.71,58.45,0,116.46,172.20,141.90,0,79.15,97.09
1970,1073.30,92.60,62.42,215.84,130.67,217.22,148.46,0,88.19,113.40
1971,1164.85,99.80,67.35,249.99,148.11,245.36,165.97,0,99.61,124.67
1972,1279.11,113.69,71.46,299.80,169.97,324.93,203.49,0,113.46,145.26
1973,1425.38,138.54,85.52,398.37,192.54,441.46,264.43,0,131.76,175.49
1974,1545.24,144.19,99.53,445.30,206.13,490.04,285.55,0,160.95,199.56
1975,1684.90,163.43,98.47,490.64,241.76,532.86,360.83,0,174.42,227.70
1976,1873.41,153.94,102.72,519.75,232.61,598.88,372.32,0,207.27,224.72
1977,2081.83,174.94,121.49,600.50,263.07,737.07,410.28,0,212.33,257.60
1978,2351.60,218.50,137.30,740.47,335.88,1035.61,506.71,0,219.37,315.06
1979,2627.33,263.71,152.99,881.35,438.99,1077.91,613.95,0,243.89,393.68
1980,2857.31,306.17,186.33,950.29,564.95,1129.38,701.29,0,274.78,477.26
1981,3207.04,289.58,193.49,800.47,540.77,1245.22,615.55,0,307.25,430.70
1982,3343.79,283.93,200.72,776.58,515.05,1158.73,584.88,0,314.65,427.27
1983,3634.04,304.75,218.26,770.68,489.62,1270.86,559.87,0,341.87,443.04
1984,4037.61,313.73,212.16,725.11,461.49,1345.82,530.68,0,356.72,437.89
1985,4338.98,309.84,232.51,732.53,489.29,1427.02,553.14,0,366.19,452.22
1986,4579.63,300.51,248.99,1046.26,601.45,2120.08,771.47,0,379.01,640.39
1987,4855.22,327.09,279.03,1298.18,745.16,2580.75,934.17,0,433.13,805.71
1988,5236.44,407.84,296.59,1401.23,910.12,3125.72,1018.85,554.83,509.38,891.61
1989,5641.58,456.29,296.04,1398.97,926.88,3109.46,1025.21,506.63,567.21,928.66
1990,5963.14,394.57,320.98,1771.67,1093.17,3185.90,1269.18,517.01,596.08,1181.22
1991,6158.13,413.38,270.11,1868.95,1142.80,3648.07,1269.28,517.96,612.53,1246.22
1992,6520.33,493.14,288.21,2131.57,1179.66,3980.70,1401.47,460.29,594.39,1320.16
1993,6858.56,619.11,279.30,2071.32,1061.39,4536.94,1322.82,435.08,579.05,1064.96
1994,7287.24,564.32,327.28,2205.07,1140.49,4998.80,1393.98,395.08,579.94,1099.22
1995,7639.75,734.48,360.28,2585.79,1344.24,5545.56,1601.09,395.54,605.96,1174.66
1996,8073.12,863.75,392.90,2497.24,1419.65,4923.39,1605.68,391.72,630.61,1312.43
1997,8577.55,961.60,415.87,2211.99,1560.91,4492.45,1452.88,404.93,654.99,1241.88
1998,9062.82,1029.06,421.35,2238.99,1653.69,4098.36,1503.11,270.96,634.00,1270.05
1999,9631.17,1094.01,458.82,2194.95,1687.83,4635.98,1493.15,195.91,678.41,1252.45
2000,10250.95,1211.33,468.39,1947.98,1665.53,4968.36,1365.64,259.71,744.77,1146.68
2001,10581.93,1339.40,485.44,1945.79,1649.83,4374.71,1377.66,306.60,738.98,1168.02
2002,10929.11,1470.56,514.94,2078.48,1785.73,4182.85,1501.41,345.47,760.65,1276.77
2003,11456.44,1660.28,607.70,2501.64,2054.42,4519.56,1844.54,430.35,895.54,1577.62
2004,12217.19,1955.35,709.15,2814.35,2421.53,4893.12,2119.63,591.02,1026.69,1806.54
2005,13039.20,2285.96,820.38,2846.86,2543.18,4831.47,2196.95,764.02,1173.11,1858.22
2006,13815.59,2752.12,940.26,2994.70,2708.44,4601.66,2320.54,989.93,1319.26,1949.55
2007,14474.23,3550.33,1216.74,3425.58,3090.51,4579.75,2660.59,1299.70,1468.82,2213.10
2008,14769.86,4594.34,1198.90,3745.26,2929.41,5106.68,2930.30,1660.85,1552.99,2408.66
2009,14478.06,5101.69,1341.89,3411.26,2412.84,5289.49,2700.89,1222.65,1374.63,2199.93
2010,15048.96,6087.19,1675.62,3399.67,2485.48,5759.07,2645.19,1524.92,1617.34,2136.10
2011,15599.73,7551.55,1823.05,3749.31,2663.81,6233.15,2865.16,2045.92,1793.33,2294.99
2012,16253.97,8532.19,1827.64,3527.14,2707.09,6272.36,2683.67,2208.29,1828.37,2086.96
2013,16843.19,9570.47,1856.72,3733.80,2784.85,5212.33,2811.88,2292.47,1846.60,2141.92
2014,17550.68,10475.62,2039.13,3889.09,3064.71,4896.99,2855.96,2059.24,1805.75,2162.01
2015,18206.02,11061.57,2103.59,3357.59,2927.91,4444.93,2439.19,1363.48,1556.51,1836.64
2016,18695.11,11233.31,2294.80,3469.85,2689.11,5003.68,2472.96,1276.79,1527.99,1877.07
2017,19477.34,12310.49,2651.47,3690.85,2680.15,4930.84,2595.15,1574.20,1649.27,1961.80
2018,20533.06,13894.91,2702.93,3974.44,2871.34,5040.88,2790.96,1657.33,1725.33,2091.93
2019,21380.98,14279.97,2835.61,3889.18,2851.41,5117.99,2728.87,1693.12,1743.73,2011.30
2020,21060.47,14687.74,2671.60,3887.73,2697.81,5055.59,2647.42,1493.08,1655.68,1897.46
2021,23315.08,17820.46,3150.31,4278.50,3141.51,5034.62,2959.36,1836.89,2007.47,2155.36
2022,25439.70,17963.17,3416.65,4082.47,3089.07,4256.41,2779.09,2240.42,2161.48,2049.74

--

Github version of code: D3 Racing Bar Chart

--

You guys can adjust this code for whatever industry or topic you want and publish this on YouTube or as parts of your articles when creating compelling content.
 
Last edited:
Hell yeah. ChatGPT rules. I'm not sure if this is something I saw on BS or elsewhere but here is another example of a racing bar chart being used in an interesting way. It visualizes the most popular YouTube channels by number of subscribers from 2005 - 2023.

 
Back