Building a Bar Chart Race with D3 and Svelte

In this article, we will create a data visualization that animates the changes in the stargazer counts of popular front-end library/framework GitHub repositories over the past 15 years. Which front-end libraries/frameworks currently dominate the web development landscape? Which front-end libraries/frameworks used to dominate web development landscape?Bar chart races make boring bar charts dynamic and fun. Unlike regular bar charts, bar chart races show the growth and decline (the fluctuations) in the relative values of categories over time. Each bar represents a category, and the bar grows or shrinks in length with respect to its corresponding value at a given time and an ever-changing scale. The bars reposition themselves, commonly, in descending order of values. Depending on the maximum number of bars that can be shown in the bar chart race, you may occasionally see a bar drop off at or re-emerge from the bottom of the visualization. Due to the animation aspect of bar chart races (the racing effect created by animated bars), they have become popular in recent years on social media platforms. They turn vast amounts of complex data into a captivating, easy-to-digest medium. Bar chart races reveal trends that emerged or fell off across intervals of time. For example, if you created a bar chart race of browser usage over the 1990s to the present day, then you may initially see the rise of Internet Explorer, followed by its gradual decline as browsers like Chrome and Firefox became dominate forces in the browser market. D3 is great at tracking elements in an animation and animating enter and exit transitions. However, its imperative .join() approach to data-binding and managing enter, update and exit animations separately is not as expressive as Svelte’s declarative approach via reactivity, dynamic attributes (via curly braces) and built-in animation and transition directives. Below, I'm going to show you how to build a bar chart race with D3 and Svelte. The bar chart race will show the rate of growth in each GitHub repository’s stargazer count from April 2009 to the present day. By the end of this tutorial, you will have build the following bar chart race: To set up a new Svelte project with Vite and TypeScript , run the command npm init vite . Note : You may generate a new Svelte application with SvelteKit, but this tutorial is only focused on building out a single Svelte component for the bar chart race. Therefore, it’s more preferred to use a lighter template so that you don’t need to mess around with extra project files. Currently, you cannot query GitHub’s GraphQL API for a GitHub repository’s stargazer counts history. However, there’s an open source project that maintains records of repositories’ stargazer counts through the years: Star History. To get a CSV of historical stargazer counts for a GitHub repository, enter the both the username of the GitHub repository’s author and the name of the GitHub repository, delimited by a / . For example, facebook/react for React.js. Once you’ve clicked on the “View star history” button and waited for the chart to be generated, click on the CSV button to download this data into a CSV file. You can add more GitHub repositories to the chart so that the CSV will contain data for all of these GitHub repositories. For the bar chart race, we will be visualizing the historical stargazer counts for the following repositories: Once downloaded, rename the file as frontend-libraries-frameworks.csv and place it within the public/data directory. Since the data is incomplete, we will be interpolating stargazer counts for unknown dates. Additionally, from the dates, omit the day of week, the time and the time zone from the values of the second column (e.g., Thu Feb 11 2016 12:06:18 GMT-0500 (Eastern Standard Time) → Feb 11 2016 ). At the top of the CSV, add a header row to label the columns: “name,date,value.” For the bar chart race, we will need to install five specific D3 modules: Run the following command to install these D3 modules and their type definitions in the Svelte project. First, delete the src/lib directory and src/app.css file. Then, in src/main.ts , omit the import './app.css' statement at the top of the file. In the src/App.svelte file, clear out the contents of the script, style and markup sections. Within the script section, let’s add the import statement for the <BarChartRace /> component and two variables: ( src/App.svelte ) Within the style section, let’s add some minor styles to horizontally center the <BarChartRace /> component in the <main /> element. ( src/App.svelte ) Note : Styles defined in the <App /> component won’t leak into other Svelte components. Within the <main /> element of the markup section, call the <BarChartRace /> component. Also, pass datasetUrl to the datasetUrl prop and maxBars to the maxBars prop of the <BarChartRace /> component, like so: ( src/App.svelte ) Then, create a types folder under the src directory. Within this folder, create an index.ts file and define and export two interfaces: Record and KeyframeRecord . ( types/index.ts ) We will annotate the records from the raw CSV dataset with Record , and we will annotate the records stored in a “keyframe” (we will cover this later in this tutorial) with KeyframeRecord . Within the src directory, create a new folder named components . This folder will contain any reusable components used in this Svelte application. In this case, there will only be one component in this directory: BarChartRace.svelte . Create this file inside of the src/components directory. Within the src/components/BarChartRace.svelte file, begin with an empty script section for the <BarChartRace /> component: ( src/components/BarChartRace.svelte ) At the top of the script section, import several methods from the installed D3 modules: ( src/components/BarChartRace.svelte ) Then, declare the datasetUrl and maxBars props that the <BarChartRace /> component currently accepts. Additionally, locally declare three variables: ( src/components/BarChartRace.svelte ) d3-fetch comes with a convenient method for fetching and parsing CSV files: csv() . This method accepts, as arguments, a URL to a CSV dataset and a callback function that maps each row’s values to actual data values. All values in the CSV dataset are represented as strings. For the bar chart race, we need to parse value as a number and date as a Date object. To parse date as a Date object, create a parser by calling the timeParse() method with the structure of the stringified date (so that the parser understands how to parse the date string). Since date is formatted as <abbreviated month name> <zero-padded day of the month> <year with century> (e.g., Feb 11 2016 ), we pass the specifier string of "%b %d %Y" to the timeParse() method. In a Svelte component, we will need to use the onMount lifecycle method to fetch data after the component gets rendered to the DOM for the first time, like so: The bar chart race’s animation iterates over a series of keyframes. Each keyframe represents exactly one moment of the bar chart race; it contains data of the GitHub repositories’ stargazer counts at a given date. Because the source dataset from Star History doesn’t contain stargazer counts at every single date from April 2009 (the month of the earliest known stargazer count) to the present day, we will need to interpolate between data points (estimate stargazer counts for unknown dates) to guarantee that there’s enough keyframes to make the animation run smoothly. Every x milliseconds, we can update the animation with the data from the next keyframe until we run out of keyframes, at which point, the animation will stop at the current day stargazer counts for the GitHub repositories. To create these keyframes, we need to: Like with any D3 data visualization, you need to define its dimensions . Let’s define the bar chart race’s width, height and margins, like so: In the <BarChartRace /> component’s markup section, add an <svg /> element and set its width , height and viewBox using the values from dimensions . Within this <svg /> element, add a <g /> element that will group the <rect /> elements that will represent the bars. The x-scale maps a domain of stargazer counts to the horizontal dimensions of the bar chart race. You’re probably wondering why the maximum value of the domain is 1 despite our source dataset shows that the maximum stargazer count is 210,325. This domain serves as a placeholder for the x-scale’s domain. When we’re animating the bar chart race by iterating over the keyframes, we will adjust the x-scale’s domain based on the current keyframe’s data. This way, during the animation, the maximum stargazer count will always span the entire width ( dimensions.width - dimensions.margin.right ) of the bar chart race. On the other hand, the y-scale maps a domain of visible bar indices to the vertical dimension of the bar chart race. The domain specifies 1 more than the maximum number of visible bars since we want to be able to transition between the bottom-most visible bar and the hidden bar beneath it smoothly. Note : <number> corresponds to <Range> . This generic represents the data type of the domain values. Then, define a color scheme. Initialize it as a function that returns “#FFFFFF.” This function will serve as a placeholder function until we actually fetch the CSV dataset, at which point, we can reassign the color scheme to map each GitHub repository to a specific color. Note : _d ensures that the function signature matches the function signature of the function that will override this placeholder function. In the onMount lifecycle method, after fetching the CSV dataset and creating a set of GitHub repository names from the source data, assign a new color scheme that assigns each GitHub repository name to a specific color, like so: To animate the bar chart race, first locally declare a variable keyframeItems at the top-level of the script section: keyframeItems will hold a keyframe’s list of the GitHub repositories and their stargazer counts and ranks. By reassigning this variable for each keyframe, Svelte’s reactivity will automatically update the bars’ widths and positions. Additionally, at the top-level of the script section, call the timeFormat() method with a string that describes how to format the date based on an input Date object. This way, the formatter knows what to output when given an input Date (e.g., “Jul 2023”). In the onMount lifecycle method, once the keyframes have been created, set up a setInterval() function that… Note : In a future tutorial, I will show you how to re-implement this with requestAnimationFrame . Within the <g /> element in the markup section of the <BarChartRace /> component, use an each block to loop over keyframeItems and render a <rect /> for each visible bar. The items are keyed by the GitHub repositories’ names so that Svelte knows not to recreate the bars anytime keyframeItems gets updated and to just continue modifying properties of the existing bars. The in and out directives allow us to control the enter and exit animations of the bars. For example, in corresponds to an enter animation, and out corresponds to an exit animation. To keeps things simple, we’ll have the bar fade out when it exits and fade in when it enters. Finally, add an axis line to show the bars left-aligned and the ticker to the <svg /> element. When you run the project in development via npm run dev , you should see a bar chart race that looks like the following: Try customizing the bar chart race for your own historical count data. If you find yourself stuck at any point while working through this tutorial, then feel free to check out the project's GitHub repository or a live demo of this project in the following CodeSandbox: If you want to learn more about building visualizations with D3 and Svelte, then check out the Better Data Visualizations with Svelte course by Connor Rothschild, a partner and data visualization engineer at Moksha Data Studio.

Thumbnail Image of Tutorial Building a Bar Chart Race with D3 and Svelte