How to make a map of US election results
This tutorial demonstrates how to make a map of US election results. You can also:
- Read about election mapping with MapTiler
- See a full screen Demo
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
-
Install the npm package.
npm install --save @maptiler/sdk -
Include the CSS file.
If you have a bundler that can handle CSS, you can
importthe CSSimport "@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' /> -
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 }); -
Replace
YOUR_MAPTILER_API_KEY_HEREwith your own API key. Make sure to protect the key before you publish it. -
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 thestarting zoom) to match your users’ needs. Additionally, you can change the map’s look (by updating thesource 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. -
Add an event handler for the map
readyevent. You will add code to create your map in this handler.map.on('ready', async () => { }); -
Add the MapTiler countries data source.
map.on('ready', async () => { map.addSource('countries', { type: 'vector', url: `https://api.maptiler.com/tiles/countries/tiles.json` }); }); -
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; } -
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 ); }); -
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; } -
Add the election result data to the states layer features. We will use the
sourcedataevent 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 ]; } -
Change the US state’s layer
fill-colorto 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 ); -
Reload the page to see the map with the states colored according to the winner.
-
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
moveandidleevents 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(); }); -
The last thing we will do is use the map’s
clickevent 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); } }); -
To display the information we will use a popup.
const popup = new maptilersdk.Popup({closeOnClick: false}).setLngLat([0, 0]); -
We will create the popup content with the
showInfofunction. 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); } -
To create the graph we need to load the d3.js library.
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script> -
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}); } -
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: '%'; } -
Reload the map and click on a state to see the election results.
-
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.
-
Add the US counties layer
map.addLayer( { 'id': 'county', 'source': 'countries', 'source-layer': 'administrative', 'type': 'fill', 'minzoom': 5, 'filter': [ 'all', ['==', 'level', 2], ['==', 'level_0', 'US'] ], 'paint': { 'fill-opacity': 1, 'fill-outline-color': '#262626', 'fill-color': getStyleColorByParty(), } }, firstSymbolId ); -
Load the US election results data by county
`...`let resultsCounty;
`...`resultsCounty = await getCountiesResults();async function getCountiesResults() { const response = await fetch("https://docs.maptiler.com/sdk-js/examples/election-map-usa/2020-county-president.json"); const data = await response.json(); return data; } -
Add the election result data to the county layer features.
async function updatePolygonFeatures() { const features = map.queryRenderedFeatures({layers:['states', 'county']}); const filteredFeatutes = filterFeaturesNoFeatureState(features, map); filteredFeatutes.forEach(item => { const resultsData = (item.layer.id === 'states') ? resultsStates : resultsCounty; const source = { source: item.source, sourceLayer: item.sourceLayer } addResultToFeature(item, resultsData, source, map); }); } -
Show the election results corresponding to the county where the user clicks.
map.on('click', (e) => { const features = map.queryRenderedFeatures(e.point, {layers: ['states', 'county']}); if (features.length){ showInfo(e, features[0], map, popup); } }); -
Animate the transition between the state and county visualization.
`...`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(), 'fill-opacity-transition': { duration: 300 } } }, firstSymbolId );map.on('zoom', (e) => { if (map.getZoom() >= 6) { if (map.getPaintProperty('states', 'fill-opacity') === 1) { map.setPaintProperty('states', 'fill-opacity', 0); setTimeout(() => { map.setLayoutProperty('states', 'visibility', 'none'); },400); } } else { if (map.getPaintProperty('states', 'fill-opacity') === 0) { map.setLayoutProperty('states', 'visibility', 'visible'); map.setPaintProperty('states', 'fill-opacity', 1); } } }); - Congratulations, you have made a map that shows the US election results by state and county.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>How to create a interactive US election map</title>
<script src="https://cdn.maptiler.com/maptiler-sdk-js/v4.1.0/maptiler-sdk.umd.min.js"></script>
<link href="https://cdn.maptiler.com/maptiler-sdk-js/v4.1.0/maptiler-sdk.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
.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: '%';
}
</style>
</head>
<body>
<div id="map"></div>
<script>
let resultsStates;
let resultsCounty;
const colors = {
republicans: '#D61822',
democrats: '#0A3B7E',
nodata: '#DEDEDE'
}
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
});
const popup = new maptilersdk.Popup({ closeOnClick: false }).setLngLat([0, 0]);
map.on('load', async () => {
map.addSource('countries', {
type: 'vector',
url: `https://api.maptiler.com/tiles/countries/tiles.json`
});
const firstSymbolId = findFirstSymbolLayer(map);
map.addLayer(
{
'id': 'county',
'source': 'countries',
'source-layer': 'administrative',
'type': 'fill',
'minzoom': 5,
'filter': [
'all',
['==', 'level', 2],
['==', 'level_0', 'US']
],
'paint': {
'fill-opacity': 1,
'fill-outline-color': '#262626',
'fill-color': getStyleColorByParty(),
}
},
firstSymbolId
);
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(),
'fill-opacity-transition': { duration: 300 }
}
},
firstSymbolId
);
resultsStates = await getStatesResults();
resultsCounty = await getCountiesResults();
map.on('sourcedata', getSourceData);
map.on('move', (e) => {
updatePolygonFeatures();
});
// just before the map enters an "idle" state.
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);
}
});
map.on('zoom', (e) => {
if (map.getZoom() >= 6) {
if (map.getPaintProperty('states', 'fill-opacity') === 1) {
map.setPaintProperty('states', 'fill-opacity', 0);
setTimeout(() => {
map.setLayoutProperty('states', 'visibility', 'none');
}, 400);
}
} else {
if (map.getPaintProperty('states', 'fill-opacity') === 0) {
map.setLayoutProperty('states', 'visibility', 'visible');
map.setPaintProperty('states', 'fill-opacity', 1);
}
}
});
});
function findFirstSymbolLayer() {
const layers = map.getStyle().layers;
const firstSymbolId = layers.find(layer => layer.type === 'symbol').id;
return firstSymbolId;
}
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;
}
async function getCountiesResults() {
const response = await fetch("https://docs.maptiler.com/sdk-js/examples/election-map-usa/2020-county-president.json");
const data = await response.json();
return data;
}
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', 'county'] });
const filteredFeatutes = filterFeaturesNoFeatureState(features, map);
filteredFeatutes.forEach(item => {
const resultsData = (item.layer.id === 'states') ? resultsStates : resultsCounty;
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
];
}
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);
}
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 });
}
</script>
</body>
</html>
Related examples
Interactive choropleth map
ExamplesUse events and feature states to create a interactive choropleth map.