Building a Choropleth Map with D3 and Svelte
In this article, we will create a data visualization that displays the ratio of Dunkin’ Donuts locations to Starbucks locations (by state) using D3 and Svelte. Which of America’s largest coffee chains keeps your state awake and ready for the 9-5 workday?Choropleth maps bring data to life. By projecting data onto a map, you can craft a captivating, visual narrative around your data that uncovers geographical patterns and insights. Choropleth maps color (or shade) geographical areas like countries and states based on numeric data values. The intensity of the color represents the magnitude of the data value in a specific geographical area. With a single glance, these colors allow us to easily identify regional hotspots, trends and disparities that are not immediately apparent from raw data. Think about the geographic distribution of registered Democrat and Republican voters across the United States. A state with an overwhelming majority of registered Democrat voters might be colored blue, whereas a state with an overwhelming majority of registered Republican voters might colored red. A state with a single-digit percentage difference between registered Democrat and Republican voters, such as Pennsylvania, would be colored blue purple. On the contrary, a state with a significantly larger ratio of registered Democrat voters to Republican voters, such as California, would be colored a more intense blue. Examining all of the states, you will recognize that registered Democrat voters primarily reside in states in the northeast region and along the western coastline of the United States. Choropleth maps let us answer geographic questions about our data and contextualize our data through the lens of our knowledge of the world. For example, looking at a choropleth map of registered Democrat and Republican voters in the United States on a state basis, it may make evident the differences in the laws and policies enacted across each state. Anyone who can read a map will have zero troubles navigating, understanding and deriving conclusions from choropleth maps. A common method for creating Choropleth maps for the web is D3, a popular JavaScript data visualization library. However, using just D3 to create choropleth maps comes with several downsides: And so, why not delegate the rendering logic to a declarative, UI framework like Svelte? Svelte surgically updates the DOM and produces highly optimized JavaScript code with zero runtime overhead. Additionally, Svelte components consist of three sections — script, styles and markup — to keep logic organized and consistent. By letting Svelte handle the rendering logic and D3 handle the data transformation logic (and difficult mathematical calculations), we can: Below, I'm going to show you how to build a choropleth map with D3 and Svelte. The choropleth map will display the ratio of Dunkin’ Donuts locations to Starbucks locations (by state). States with significantly more Starbucks locations than Dunkin’ Donuts locations will be colored green, and states with significantly more Dunkin’ Donuts locations than Starbucks locations will be colored orange. A legend will be added to map colors to magnitudes of location ratios. By the end of this tutorial, you will have built the following choropleth map: 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 choropleth map. Therefore, it’s more preferred to use a lighter template so that you don’t need to mess around with extra project files. To obtain the number of Dunkin’ Donuts and Starbucks locations in those states, visit the following websites: And record the states and their location counts in two CSV files: dunkin_donuts_locations_counts.csv and starbucks_locations_counts.csv . Each CSV’s header row includes titles for two columns: state and count . The delimiter should be a comma. Then, within the public directory, create a data directory and place both CSV datasets in this new directory. To obtain a TopoJSON file of the geometries that represent US states, visit the U.S. Atlas TopoJSON GitHub repository ( https://github.com/topojson/us-atlas ). Then, scroll through the contents of the repository’s [README.md](http://README.md) file and download the states-albers-10m.json file. The state boundaries are drawn based on the 2017 edition of the Census Bureau’s cartographic state boundaries. Unlike the states-10m.json file, the geometries within this file have been projected to fit a 975 x 610 viewport. Once downloaded, rename the file as us_topojson.json and place it within the public/data directory. To create geographic features in an SVG canvas, D3 consumes GeoJSON data. Therefore, why are we downloading a TopoJSON file? TopoJSON is an extension of GeoJSON that eliminates redundancy in geometries via arcs . It’s more compact than GeoJSON (typically 80% smaller than their GeoJSON equivalents), and it preserves and encodes topology. For the choropleth map, it will download a TopoJSON file, not a GeoJSON file, of US states so that the choropleth map does not have to wait long . Then, we will leverage a module, topojson-client , to convert TopoJSON features to GeoJSON features for D3 to work with. For the choropleth map, we will need to install four specific D3 modules and a related module that’s also from the creator of D3: 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 <ChoroplethMap /> component and declare two variables: ( src/App.svelte ) Within the style section, let’s add some minor styles to horizontally center the <ChoroplethMap /> 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 <ChoroplethMap /> component. Also, pass datasets to the datasets prop and colors to the colors prop of the ChoroplethMap /> component, like so: ( src/App.svelte ) 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: ChoroplethMap.svelte . Create this file inside of the src/components directory. Within the src/components/ChoroplethMap.svelte file, begin with an empty script section for the <ChoroplethMap /> component: ( src/components/ChoroplethMap.svelte ) At the top of the script section, import several methods from the installed D3 modules: ( src/components/ChoroplethMap.svelte ) Then, declare the datasets and colors props that the <ChoroplethMap /> component currently accepts. Set their default values to empty arrays when no value is passed to either prop. ( src/components/ChoroplethMap.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. For example, since numeric values in a CSV file will initially be represented as strings, they must be parsed as numbers. In our case, we want to parse count as a number. 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: For us to load both datasets, we can: Note : We’re flattening the returned data so that we can later group the data by state and calculate the location ratio on a per state basis. d3-fetch comes with a convenient method for fetching and parsing JSON files: json() . For the choropleth map, we will only want the method to accept, as an argument, a URL to the TopoJSON file with the geometry collection for US states. We will need to add this line of code to the onMount lifecycle method so that the TopoJSON data gets fetched alongside the CSV data, like so: To convert TopoJSON data to GeoJSON data, we will need to… Add these lines of code to the onMount lifecycle method, like so: Like with any D3 data visualization, you need to define its dimensions . Let’s define the choropleth map’s width, height and margins, like so: In the <ChoroplethMap /> 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 <path /> elements that will represent the states and the internal borders between them. Back in the script section of the <ChoroplethMap /> component, create a new geographic path generator via the geoPath() method, like so: path is a function that turns GeoJSON data into a string that defines the path to be drawn for a <path /> element. In other words, this function, when called with stateMesh or a feature object from statesFeatures , will return a string that we can set to the d attribute of a <path /> element to render the internal borders between states or a state respectively. Here, we’ll render the internal borders between states and use an each block to loop over the feature objects in statesFeatures and render the states inside of the <g /> element, like so: Since stateMesh and statesFeatures are declared within the onMount lifecycle method, we’ll have to move the declarations to the top-level of the script section to ensure that these values can be used in the markup section of the <ChoroplethMap /> component. When you run the project in development via npm run dev , you should see a choropleth map that looks like the following: To adjust the fill color of each state by location ratio, first locally declare two variables at the top-level of the script section: Note : <string, string> corresponds to <Range, Output> . The Range generic represents the type of the range data. The Output generic represents the type of the output data (what’s outputted when calling scale() ). Within the onMount lifecycle method, using the d3-array 's rollup() method, group the data by state name, and map each state name to a ratio of Dunkin’ Donuts locations in the state to Starbucks locations in the state. Then, get the maximum location ratio from ratios via D3’s extent() method. Since the method only accepts an array as an argument, you will need to first convert the map to an array via Array.from() . Then, set scale to a linear scale that maps the ratios to colors passed into the colors prop. The max value corresponds to the first color in the colors list, an orange color. Any state that’s colored orange will indicate a higher ratio of Dunkin’ Donuts locations to Starbucks locations. Additionally, any state that’s colored green will indicate a lower ratio of Dunkin’ Donuts locations to Starbucks locations. A 1:1 ratio ( 1 in the domain) denotes an equal number of Dunkin’ Donuts locations to Starbucks locations. Note : A quantized scale would be better suited. However, the domain of scaleQuantize() accepts only two arguments, a minimum and maximum value. This means you cannot define your own threshold values ( scaleQuantize() automatically creates its own threshold values from the provided minimum and maximum values). Within the markup section of the <ChoroplethMap /> component, replace the currently set fill of "green" to scale(ratios.get(feature.properties.name)) . Upon saving these changes, you should see the colors of the states update. Wow, it seems Dunkin’ Donuts keeps the northeast of the US awake! The colors chosen for this data visualization are based on the official branding colors of Dunkin’ Donuts and Starbucks. For folks who might not be familiar with Dunkin’ Donuts and Starbucks official branding colors, let’s create a simple legend for the choropleth map so they know which states have a higher concentration of Starbucks locations and which states have a higher concentration of Dunkin’ Donuts locations. First, let’s locally declare a variable categories that maps datasets to an array that contains only the labels of the datasets. Then, create a new file in the src/components directory: Legend.svelte . This <Legend /> component will accept three props: dimensions , colors and categories . Given that we only want two labels for the legend, one for the first color in colors and one for the last color in colors , we create the labels by setting the first item in labels to categories[0] (”Dunkin’ Donuts”) and the last item in labels to categories[1] (”Starbucks”). Then, we leave the middle three labels undefined. This way, we can render the colors and labels one-to-one in the markup section. ( src/components/Legend.svelte ) Back in the <ChoroplethMap /> component, we can import the <Legend /> component and render it within the <svg /> element like so: Upon saving these changes, you should see the legend appear in the bottom-right corner of the choropleth map. Try customizing the choropleth map with your own location counts 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.