How to make a map of US election results

This tutorial demonstrates how to make a map of US election results. You can also:

In this tutorial, we will create a map to show the results of the 2020 elections in the US. We have obtained the data from Hardvard Dataverse. To use this data in our application we have transformed the downloaded CSV files into JSON. Here you can download the transformed JSON files 2020 president by state y 2020 president by county

Click on the map to get the election results. Zoom in to view results by county

NPM module setup

  1. Install the npm package.

    
     npm install --save @maptiler/sdk
    
  2. Include the CSS file.

    If you have a bundler that can handle CSS, you can import the CSS

    
     import "@maptiler/sdk/dist/maptiler-sdk.css";
    

    or include it with a <link /> in the head of the document via the CDN

    
     <link href='https://cdn.maptiler.com/maptiler-sdk-js/v4.1.0/maptiler-sdk.css' rel='stylesheet' />
    
  3. Include the following code in your JavaScript file (Example: app.js).

    
     import * as maptilersdk from '@maptiler/sdk';
    
     maptilersdk.config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
     const map = new maptilersdk.Map({
       container: 'map', // container's id or the HTML element to render the map
       style: maptilersdk.MapStyle.DATAVIZ.LIGHT,
          
       center: [-96.05, 36.79], // starting position [lng, lat]
       zoom: 4, // starting zoom
          
     });
    
  4. Replace YOUR_MAPTILER_API_KEY_HERE with your own API key. Make sure to protect the key before you publish it.

  5. The next is up to you. You can center your map wherever you desire (modifying the starting position) and set an appropriate zoom level (modifying the starting zoom) to match your users’ needs. Additionally, you can change the map’s look (by updating the source URL); choose from a range of visually appealing map styles from our extensive MapTiler standard maps, or create your own to truly differentiate your application.

  6. Add an event handler for the map ready event. You will add code to create your map in this handler.

    
     map.on('ready', async () => {
        
     });
    
  7. Add the MapTiler countries data source.

    
     map.on('ready', async () => {
       map.addSource('countries', {
         type: 'vector',
         url: `https://api.maptiler.com/tiles/countries/tiles.json`
       });
     });
    
  8. Find the id of the first symbol (text) layer in the map style. This is useful for adding polygon or line layers below the map labels.

    
     map.on('ready', async () => {
       map.addSource('countries', {
         type: 'vector',
         url: `https://api.maptiler.com/tiles/countries/tiles.json`
       });
    
       const firstSymbolId = findFirstSymbolLayer(map);
    
     });
        
     function findFirstSymbolLayer() {
       const layers = map.getStyle().layers;
       const firstSymbolId = layers.find(layer => layer.type === 'symbol').id;
       return firstSymbolId;
     }
    
  9. Add the US state’s layer

    
     map.on('ready', async () => {
       map.addSource('countries', {
         type: 'vector',
         url: `https://api.maptiler.com/tiles/countries/tiles.json`
       });
    
       const firstSymbolId = findFirstSymbolLayer(map);
    
       map.addLayer(
         {
           'id': 'states',
           'source': 'countries',
           'source-layer': 'administrative',
           'type': 'fill',
           'maxzoom': 7,
           'filter': [
             'all',
             ['==', 'level', 1],
             ['==', 'level_0', 'US']
           ],
           'paint': {
             'fill-opacity': 1,
             'fill-outline-color': '#fff',
             'fill-color': '#DEDEDE',
           }
         },
         firstSymbolId
       );
    
     });
    
  10. Load the US election results data by states

    
     let resultsStates;
     maptilersdk.config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
    

    ...

    
     resultsStates = await getStatesResults();
    

    ...

    
     async function getStatesResults() {
       const response = await fetch("https://docs.maptiler.com/sdk-js/examples/election-map-usa/2020-president.json");
       const data = await response.json();
       return data;
     }
    
  11. Add the election result data to the states layer features. We will use the sourcedata event to know when the data loading into the map is finished.

    
     let resultsStates;
     const colors = {
       republicans: '#D61822',
       democrats: '#0A3B7E',
       nodata: '#DEDEDE' 
     }
    

    ...

    
     resultsStates = await getStatesResults();
    
     map.on('sourcedata', getSourceData);
    

    ...

    
     async function getSourceData(e) {
       if (e.isSourceLoaded && e.dataType === 'source') {
         const {sourceId} = e;
         // Do something when the source has finished loading
         if (sourceId === 'countries') {
           map.off('sourcedata', getSourceData);
           await updatePolygonFeatures();
           map.redraw();
           //map.panBy([0,0]);
         }
       }
     }
    
     async function updatePolygonFeatures() {
       const features = map.queryRenderedFeatures({layers:['states']});
       const filteredFeatutes = filterFeaturesNoFeatureState(features, map);
       filteredFeatutes.forEach(item => {
         const resultsData = resultsStates;
         const source = {
           source: item.source,
           sourceLayer: item.sourceLayer
         }
         addResultToFeature(item, resultsData, source, map);
       });
     }
    
     function filterFeaturesNoFeatureState(features, map) {
       const noStateFeatures = features.filter(item => {
         const fState = map.getFeatureState({
           source: item.source,
           sourceLayer: item.sourceLayer,
           id: item.id,
         });
         return !Object.keys(fState).length
       });
       return noStateFeatures
     }
    
     function addResultToFeature(feature, resultsData, source, map){
       const code = getCode(feature);
       const result = resultsData[code];
       if (!result) return;
       const {winner, winner_percentage, loser_percentage} = getWinner(result);
       if (source) {
         const fState = map.getFeatureState({
           ...source,
           id: feature.id,
         });
         if (!fState?.winner) {
           map.setFeatureState({
             ...source,
             id: feature.id,
           }, {
             totalvotes: result.totalvotes,
             trump: result.trump,
             biden: result.biden,
             winner: winner,
             winner_percentage: winner_percentage,
             loser_percentage: loser_percentage
           });
         }
       } else {
         const {properties} = feature;
         feature.properties = {...properties, 
           totalvotes: result.totalvotes,
           trump: result.trump,
           biden: result.biden,
           winner: winner,
           winner_percentage: winner_percentage,
           loser_percentage: loser_percentage
         }
       }
     }
    
     function getCode(feature) {
       if (feature?.properties?.ste_stusps_code || feature?.properties?.coty_code) {
         return feature?.properties?.ste_stusps_code || feature?.properties?.coty_code.replace(/^\(1:|\)$/g, "").replace(/^0+/, "");
       } else {
         return feature.properties.code.replace('US-','').replace(/^0+/, "");
       }
     }
    
     function getWinner(result){
       if (!result) return {winner: '', winner_percentage: 0};
       if (parseInt(result.trump) > parseInt(result.biden)) {
         return {winner: 'trump', 
           winner_percentage: percentage(result.trump, result.totalvotes), 
           loser_percentage: percentage(result.biden, result.totalvotes)}
       } else {
         return {winner: 'biden', 
           winner_percentage: percentage(result.biden, result.totalvotes),
           loser_percentage: percentage(result.trump, result.totalvotes)
         }
       }
     }
    
     function percentage(partialValue, totalValue) {
       return Math.round((100 * partialValue) / totalValue);
     }
    
     function getStyleColorByParty() {
       return ['case',
         ['==', ['feature-state', 'winner'], 'biden'],
         colors.democrats,
         ['==', ['feature-state', 'winner'], 'trump'],
         colors.republicans,
         colors.nodata
       ];
     }
    
  12. Change the US state’s layer fill-color to the color of the winning party.

    
     map.addLayer(
       {
         'id': 'states',
         'source': 'countries',
         'source-layer': 'administrative',
         'type': 'fill',
         'maxzoom': 7,
         'filter': [
           'all',
           ['==', 'level', 1],
           ['==', 'level_0', 'US']
         ],
         'paint': {
           'fill-opacity': 1,
           'fill-outline-color': '#fff',
           'fill-color': getStyleColorByParty(),
         }
       },
       firstSymbolId
     );
    
  13. Reload the page to see the map with the states colored according to the winner.

  14. In the previous step, we have assigned the election results to the states. We’ve only done this for the states that are visible on the map. If we move the map towards Alaska we will see that the state remains in gray (it does not have any electoral results assigned). To avoid this we are going to use the move and idle events to assign the results to the rest of the states as we move around the map.

    
     map.on('sourcedata', getSourceData);
    
     map.on('move', (e) => {
       updatePolygonFeatures();
     });
    
     // just before the map enters an "idle" state.
     map.on('idle', function() {
       updatePolygonFeatures();
     });
    
  15. The last thing we will do is use the map’s click event to show the election results corresponding to the state where the user clicks.

    
     map.on('idle', function() {
       updatePolygonFeatures();
     });
    
     map.on('click', (e) => {
       const features = map.queryRenderedFeatures(e.point, {layers: ['states']});
       if (features.length){
         showInfo(e, features[0], map, popup);
       }
     });
    
  16. To display the information we will use a popup.

    
     const popup = new maptilersdk.Popup({closeOnClick: false}).setLngLat([0, 0]);
    
  17. We will create the popup content with the showInfo function. The information displayed is the name of the state, a table showing the results according to the candidates and a graph showing the percentages of votes obtained and the winner’s margin of victory.

    
     function showInfo(e, feature, map, popup) {
       const {lng, lat} = e.lngLat;
       const info = [];
       if (feature?.properties?.name) {
         info.push(`<h2 class="pop-header">${feature.properties.name}</h2>`);
       }
       const {state} = feature;
       if (state.winner === 'trump') {
         state.loser_percentage = percentage(state.biden, state.totalvotes);
       } else  {
         state.loser_percentage = percentage(state.trump, state.totalvotes);
       }
       state.margin = state.winner_percentage - state.loser_percentage;
       const data = {};
       if (state.winner === 'trump') {
         data.winner = {
           name: 'Donald J. Trump',
           votes: state.trump,
           pct: state.winner_percentage
         }
         data.loser = {
           name: 'Joseph R. Biden',
           votes: state.biden,
           pct: state.loser_percentage
         }
       } else {
         data.winner = {
           name: 'Joseph R. Biden',
           votes: state.biden,
           pct: state.winner_percentage
         }
         data.loser = {
           name: 'Donald J. Trump',
           votes: state.trump,
           pct: state.loser_percentage
         }
       }
       data.margin = state.margin;
    
       info.push(`<div class="popup-results">
         <div class="table">
           <table>
               <thead>
                 <tr>
                   <td>Candidate</td>
                   <td>Votes</td>
                   <td>%.</td>
                 </tr>
               </thead>
               <tbody>
               <tr>
                 <td class="name">
                   ${data.winner.name}
                 </td>
                 <td class="votes">
                   ${data.winner.votes}
                 </td>
                 <td class="pct">
                   ${data.winner.pct}
                 </td>
               </tr>
               <tr>
                 <td class="name">
                   ${data.loser.name}
                 </td>
                 <td class="votes">
                   ${data.loser.votes}
                 </td>
                 <td class="pct">
                   ${data.loser.pct}
                 </td>
               </tr>
               </tbody>
           </table>
         </div>
       </div>`);
    
       info.push(`<div class="chart">
         <div class="margin ${state.winner === 'trump'? 'rep' : 'dem'}">
           <div>${data.margin}</div>  
           <div>Margin</div>
         </div>  
       </div>`);
    
       const html = info.join("");
    
       popup.setLngLat([lng, lat])
         .setHTML(html)
         .addTo(map);
    
       const svg = createChart(data, state.winner);
    
       const charContainer = document.querySelector(".chart");
       charContainer.appendChild(svg);
    
     }
    
  18. To create the graph we need to load the d3.js library.

    
     <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
    
  19. Create the graph.

    
     function createChart(results, winner) {
       const data = [
         {votes: results.winner.pct, init: 0},
         {votes: results.loser.pct, init: 0},
         {votes: 0, init: 100},
         ["votes", "init"]
       ]
       const width = 700;
       const height = Math.min(width, width / 2);
       const outerRadius = height / 2 - 10;
       const innerRadius = outerRadius * 0.75;
       const tau = 2 * Math.PI;
       const color = function(i) {
         if (i === 0) {
           if (winner === 'trump') {
             return colors.republicans
           } else {
             return colors.democrats
           }
         } else if (i === 1) {
           if (winner === 'trump') {
             return colors.democrats
           } else {
             return colors.republicans
           }
         } else {
           return colors.nodata
         }
       };
    
       const svg = d3.create("svg")
           .attr("viewBox", [-width/2, -height/2, width, height]);
    
       const arc = d3.arc()
             .innerRadius(innerRadius)
             .outerRadius(outerRadius);
    
       const pie = d3.pie().sort(null).value((d) => {
         return d["init"]
       });
    
       const path = svg.datum(data).selectAll("path")
           .data(pie)
         .join("path")
           .attr("fill", (d, i) => {
             return color(i)
           })
           .attr("d", arc)
           .each(function(d) { this._current = d; }); // store the initial angles
    
       function change(value) {
         pie.value((d) => d[value]); // change the value function
         path.data(pie); // compute the new angles
         path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
       }
    
       // Store the displayed angles in _current.
       // Then, interpolate from _current to the new angles.
       // During the transition, _current is updated in-place by d3.interpolate.
       function arcTween(a) {
         const i = d3.interpolate(this._current, a);
         this._current = i(0);
         return (t) => arc(i(t));
       }
    
       setTimeout(() => {
         change('votes');
       }, 300);
    
       // Return the svg node to be displayed.
       return Object.assign(svg.node(), {change});
     }
    
  20. Finally, style the information displayed in the popup so that it is more attractive and easier to understand.

    
     .pop-header {
       font-family: "Noto Sans", "Ubuntu", sans-serif;
       font-size: 24px;
       font-weight: 500;
     }
     .popup-results {
       display: flex;
     }
     .maplibregl-popup {
       max-width: unset !important;
     }
     .maplibregl-popup-close-button {
       font-size: 24px;
       padding: 26px;
     }
     .maplibregl-popup-content {
       padding: 26px;
       border-radius: 8px;
     }
     .chart {
       position: relative;
       text-align: center;
     }
     .margin {
       display: flex;
       flex-direction: column;
       justify-items: center;
       align-content: center;
       font-size: 20px;
       position: absolute;
       top: 50%;
       left: 50%;
       transform: translate(-50%, -50%);
       font-family: "Noto Sans", "Ubuntu", sans-serif;
     }
     .margin div:nth-child(2) {
       color: #aeb6c7;
       text-transform: uppercase;
       margin: auto;
       font-size: 0.7rem;
     }
     .margin div:nth-child(1) {
       margin: auto;
       font-weight: 600;
       font-size: 1rem;
     }
     .margin.rep div:nth-child(1) {
       color: #b51800;
     }
     .margin.dem div:nth-child(1) {
       color: #005689;
     }
     .margin div:nth-child(1)::after {
       content: '%';
     }
     .popup-results table {
       width: 200px;
       font-family: "Noto Sans", "Ubuntu", sans-serif;
     }
     .popup-results table thead tr {
       color: #aeb6c7;
       text-transform: uppercase;
       border-color: #dfe1e6;
     }
     .popup-results table td:nth-child(2) {
       text-align: right;
       padding-right: 10px;
     }
     .popup-results table td:nth-child(3) {
       text-align: right;
       padding-right: 5px;
     }
     .popup-results table tbody tr {
       border-color: #dfe1e6;
     }
     .popup-results table tbody td:first-child {
       padding-right: 10px;
       width: 75px;
     }
     .popup-results table tbody td:nth-child(3) {
       font-weight: 600;
       text-align: right;
     }
     .popup-results table tbody td:nth-child(3)::after {
       content: '%';
     }
    
  21. Reload the map and click on a state to see the election results.

  22. Congratulations, you have made a map that shows the US election results by state.

Optional: add the county layer

Below we will explain the steps to add the results by county to the map.

How to create a choropleth Map from GeoJSON

Choropleth GeoJSON

Tutorials

Create a styled choropleth map using a GeoJSON overlay.

Interactive choropleth map

Interactive choropleth map

Examples

Use events and feature states to create a interactive choropleth map.

Show polygon information on click

Show polygon information on click

Examples

Display a popup when a user clicks a polygon.

Display a popup on hover

Display a popup on hover

Examples

Show a popup when hovering over a custom marker.

Was this helpful?