Welcome to our tutorial on how to make a Choropleth Map with JavaScript. In this guide, we’ll walk you through the process of building a choropleth map step by step, using the Geoapify Boundaries API to retrieve administrative boundaries and visualizing them with color-coded regions based on your data.
Choropleth maps shade or color geographic areas, such as countries or districts, according to data values, making it easier to identify trends and patterns across different locations.
As we proceed through this tutorial, we’ll be using a JSFiddle code sample to demonstrate the implementation. For more background on choropleth maps, check out our earlier article: "Definition of a Choropleth Map".
Building a Choropleth Map with JS Code Examples
Let’s guide you through the process of creating your own custom choropleth map. You’ll explore how to use JavaScript libraries and techniques to effectively visualize geographic data with clarity and impact. Additionally, we’ll provide JS code samples to help you get started quickly, ensuring you can easily adapt the code to match your specific data and design requirements.
Get Boundary Data
In order to create a custom choropleth map, the first step is obtaining boundary data, which defines the regions you want to display on the map. Boundary data typically consists of geographic polygons representing different areas, such as countries, cities, or districts. You can source boundary data from a variety of places, including:
- Public open data repositories such as OpenStreetMap or GeoJSON files from government or research institutions.
- Additionally, API services like Geoapify offer a Boundaries API that supplies data for various administrative areas or custom shapes.
How to Use the Geoapify Boundaries API for Choropleth Maps with JavaScript
The Geoapify Boundaries API provides a convenient way to retrieve detailed administrative boundary data, supporting various geographic regions such as countries, states, cities, districts, and custom shapes. This data is particularly useful for creating choropleth maps, spatial analysis, or other location-based visualizations.
Parameter | Description |
---|---|
apiKey | Your Geoapify API key required for authentication. |
id | The place ID returned by the Geocoding API or Places API, used to specify the location of interest. |
boundary | The type of boundary to retrieve. Valid options include: administrative , postal_code , political , and low_emission_zone . The default value is administrative . |
sublevel | Defines the level of subdivisions. Setting this parameter allows you to get smaller subdivisions, such as neighborhoods instead of districts. |
geometry | Specifies the accuracy of the boundary geometry. Valid options are: point , geometry_1000 , geometry_5000 , and geometry_10000 . The default value is point . |
lang | The language of the result. Supported languages are specified by 2-character ISO 639-1 language codes (e.g., en for English, fr for French). |
This flexibility allows you to tailor boundary data requests according to the specific needs of your application or visualization, like choropleth maps or boundary-specific analyses.
Example from the JSFiddle: Using Place ID and Boundaries API
Since the API requires a place ID as a parameter, we first use the Geocoding API to find the desired location (country, state, or city) and obtain its corresponding place ID:
// Fetch the place data (including place ID) using Geoapify's Geocoding API
const placeData = await fetch(`https://api.geoapify.com/v1/geocode/search?text=${encodeURIComponent(place)}&limit=1&apiKey=${myAPIKey}`).then(result => result.json());
...
Once we have the place ID, we can then use the Boundaries API (consist-of
endpoint) to fetch the administrative subdivisions for that place.
if (placeData && placeData.features.length) {
// Get the place ID from the API response
const placeId = placeData.features[0].properties.place_id;
// Fetch the administrative subdivisions using Geoapify's Boundaries API
const divisionsData = await fetch(`https://api.geoapify.com/v1/boundaries/consists-of?id=${placeId}&sublevel=${sublevel}&geometry=geometry_1000&apiKey=${myAPIKey}`)
.then(result => result.json());
...
}
In this tutorial, we'll use the GeoJSON format, which is widely supported by mapping libraries like Leaflet, MapLibre GL and others. Here is an example of a boundary object received from the API:
{
"type": "Feature",
"properties": {
"country": "France",
"country_code": "fr",
"region": "Metropolitan France",
"state": "Brittany",
"county": "Côtes-d'Armor",
"municipality": "Saint-Brieuc",
"lon": -2.62742363947322,
"lat": 48.35423535,
"state_code": "BRE",
"state_COG": "53",
"formatted": "Saint-Brieuc, Côtes-d'Armor, France",
"address_line1": "Saint-Brieuc",
"address_line2": "Côtes-d'Armor, France",
"name": "Saint-Brieuc",
"categories": [
"administrative",
"administrative.city_level"
],
"datasource": {
"sourcename": "openstreetmap",
"attribution": "© OpenStreetMap contributors",
"license": "Open Database License",
"url": "https://www.openstreetmap.org/copyright",
...
},
"place_id": "51c0acace5f10405c059e2d9e5d65a2d4840f00101f901ccb2690000000000c0020892030c5361696e742d427269657563"
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [...]
}
}
When you have the boundary geography, you can also add corresponding values to each object, for example, by matching the feature name or ID to your dataset. The following code sample will show how to add some random values to the boundaries for demonstration purposes:
// Generate random data for each boundary feature
divisionsData.features.forEach(feature => {
// Assign a random value (between 0 and 1) to the 'value' property of each feature
feature.properties.value = Math.random();
// Map the random value to a color from the palette (assuming palette is an array of colors)
feature.properties.valueColor = palette[Math.floor(feature.properties.value * 10)];
// Create a display string showing the address (if available) and the random value, formatted to two decimal places
feature.properties.displayValue = `<b>${feature.properties.address_line1}</b>: ${feature.properties.value.toFixed(2)}`;
});
Once you've retrieved the boundary data, the next step is to load it onto the map.
Visualize the Data
Once you have the boundary data, you can begin visualizing it by rendering the data on the map. A choropleth map is particularly effective for this, as it allows you to represent regions with different colors based on various properties like population density, pollution levels, or economic indicators.
Adding an Interactive Map with Leaflet
Although you can add a base map layer using map tiles from the Geoapify Map Tiles API for a customizable background, in this tutorial, we will focus on creating a map that displays only the boundary data, without a base map.
The following code sample demonstrates how to create a map using the Leaflet library and integrate the boundary data:
// Create a Leaflet map and set the initial view with default coordinates [0, 0] and zoom level 1
const map = L.map('my-map').setView([0, 0], 1);
Additionally, you can obtain the map view box from the placeData response to adjust the map's view to fit the bounding box of the selected place. Optionally, you can also set the maximum bounds for the map to restrict the user's navigation:
// Get the bounding box (bbox) from the placeData response
const bbox = placeData.features[0].bbox;
document.getElementById("header").textContent = placeData.features[0].properties.formatted; // Display the formatted place name in the header
// Adjust the map view to fit the bounding box of the place, ensuring all boundaries are visible
map.fitBounds([
[bbox[1], bbox[0]], // Southwest corner of the bounding box
[bbox[3], bbox[2]] // Northeast corner of the bounding box
]);
// OPTIONAL: Set maximum map bounds to restrict the user's ability to pan beyond the region's bounding box
map.setMaxBounds([
[bbox[1], bbox[0]], // Southwest corner of the bounding box
[bbox[3], bbox[2]] // Northeast corner of the bounding box
]);
Adding Boundary Data as a Map Layer
The following part demonstrates how to add the boundaries data as map layers using Leaflet. Each boundary feature is styled according to its associated properties, such as the randomly generated value and corresponding color. Additionally, popups are bound to each feature, displaying the formatted value when clicked:
const divisionsLayer = L.geoJSON(divisionsData, {
// Bind a popup to each feature displaying the formatted value
onEachFeature: (feature, layer) => {
layer.bindPopup(feature.properties.displayValue);
},
// Apply custom styling to each boundary feature
style: (feature) => {
return {
stroke: true, // Enable boundary strokes
fill: true, // Enable fill for the region
fillColor: feature.properties.valueColor, // Set the fill color based on the feature's value
fillOpacity: 0.5, // Set fill opacity
color: '#3e3e3e', // Set the border color
weight: 2, // Set border thickness
opacity: 0.7, // Set border opacity
smoothFactor: 0.5 // Smoothing factor for rendering the feature
};
}
}).addTo(map); // Add the layer to the map
This code snippet creates a map layer using Leaflet's geoJSON
method, which takes the divisionsData
(boundary data in GeoJSON format) and adds it to the map. Each boundary feature is customized with a popup that displays a formatted value (e.g., the feature's address and a corresponding value). The style
function is used to define the visual appearance of the boundaries, including stroke, fill, color, and opacity settings, which are dynamically adjusted based on the feature's properties. Finally, the divisionsLayer
is added to the map to render the boundaries with the applied styles.
Add Legend
A key component of any choropleth map is the legend, which explains the color scheme used to represent different data values. By adding a legend, you help users understand what the different colors on the map signify, such as population density ranges in the example above.
Here's how to add a legend to your Leaflet map using Leaflet Control, as demonstrated in the provided JSFiddle code. This legend will display color-coded intervals, helping users interpret the data values on the choropleth map:
// Define a palette array with colors representing different ranges
const palette = [
"#B7EFC5", // Lightest color for the lowest value range
"#92E6A7",
"#6EDE8A",
"#4AD66D",
"#2DC653",
"#25A244",
"#208B3A",
"#1A7431",
"#155D27",
"#10451D" // Darkest color for the highest value range
];
// Extend the Leaflet Control to create a custom legend
L.Control.Legend = L.Control.extend({
options: {
position: "topright", // Position the legend at the top right corner of the map
},
// Function to create and add the legend to the map
onAdd: (map) => {
const paletteElement = L.DomUtil.create("div", "Legend") // Create a div element with a "Legend" class
// Loop through the colors in the palette and create a legend item for each
palette.forEach((color, index) => {
const paletteItemElement = document.createElement("div") // Create a container for each legend item
paletteItemElement.classList.add("palette-item")
const colorElement = document.createElement("div") // Create a div to display the color
colorElement.classList.add("palette-item-color")
colorElement.style.backgroundColor = color // Set the background color based on the palette
paletteItemElement.appendChild(colorElement)
const textElement = document.createElement("div") // Create a text element to display the range of values
const textValue = `${(0.1 * index).toFixed(2)} to ${(0.1 * (index + 1) - 0.01).toFixed(2)}` // Set the range of values for this color
textElement.classList.add("palette-item-text")
textElement.textContent = textValue // Add the value range as text
paletteItemElement.appendChild(textElement)
paletteElement.appendChild(paletteItemElement) // Add the legend item to the main legend container
})
return paletteElement; // Return the complete legend element to be added to the map
},
// Optional function to remove the legend from the map (if needed)
onRemove: function (map) {}
});
// Create an instance of the custom legend control and add it to the map
const control = new L.Control.Legend();
control.addTo(map);
This code defines a custom legend control for a Leaflet map using the L.Control.Legend
extension. It generates a legend positioned at the top right of the map, where each entry corresponds to a color from a predefined palette
array and displays a range of data values. For each color, a div
is created to represent the color, and a label is added to show the corresponding value range. The legend is then added to the map using control.addTo(map)
, providing a visual reference for interpreting the data on the map.
JS Code Sample Explanation
In this section, we'll break down the key parts of the code sample from the JSFiddle example to help you understand how it all works. This explanation will cover how the different components — including the data, visualization, and customization — come together to create a choropleth map.
This code showcases how to build a choropleth map using Leaflet and the Geoapify API to fetch administrative boundary data, display it on the map, and visualize data with color-coding and legends.
Key Sections of the Code:
-
API Key Setup:
const myAPIKey = "YOUR_API_KEY";
This line stores your Geoapify API key, which is required for making API requests. Replace
"YOUR_API_KEY"
with your actual API key obtained from Geoapify's project dashboard. -
Map Initialization:
const map = L.map("my-map").setView([0, 0], 1);
Here, a Leaflet map is created and centered at coordinates
[0, 0]
with an initial zoom level of1
. This sets up a global map view. -
Optional Base Map Layer: The commented-out section allows you to add a base map layer from Geoapify:
const baseUrl = "https://maps.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}.png?apiKey={apiKey}"; const retinaUrl = "https://maps.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}@2x.png?apiKey={apiKey}";
If needed, you can include this section to load tiles for background maps. The URLs for both standard and retina displays are provided, and the
L.tileLayer()
function would be used to add these tiles. -
Place Selection and Sublevel:
const place = "Brittany, France"; const sublevel = 2;
This defines the place (in this case, "Brittany, France") whose administrative boundaries will be fetched. The
sublevel
parameter indicates that the request should retrieve second-level administrative subdivisions (e.g., regions or districts within Brittany). -
Fetching Administrative Division with Geoapify API:
async function getAdministrativeDivisions(place) { const placeData = await fetch(`https://api.geoapify.com/v1/geocode/search?text=${encodeURIComponent(place)}&limit=1&apiKey=${myAPIKey}`) .then((result) => result.json());
This function uses the Geoapify Geocoding API to search for the specified
place
. It fetchesplaceData
that includes the place’splace_id
and bounding box (bbox
). The bounding box is then used to adjust the map's view to the region.- The map zooms in on the selected region using:
map.fitBounds([ [bbox[1], bbox[0]], [bbox[3], bbox[2]], ]);
- The
setMaxBounds()
function optionally sets the map’s boundaries to restrict panning beyond the fetched area.
- The map zooms in on the selected region using:
-
Fetching Administrative Subdivisions:
const divisionsData = await fetch( `https://api.geoapify.com/v1/boundaries/consists-of?id=${placeId}&sublevel=${sublevel}&geometry=geometry_1000&apiKey=${myAPIKey}` ).then((result) => result.json());
Using the
placeId
from the Geocoding API, this request to the Geoapify Boundaries API retrieves administrative subdivisions of the specifiedsublevel
. Thegeometry_1000
parameter ensures that boundary details are fetched at high resolution. -
Displaying Boundaries as a GeoJSON Layer:
const divisionsLayer = L.geoJSON(divisionsData, { onEachFeature: (feature, layer) => { layer.bindPopup(feature.properties.displayValue); }, style: (feature) => { return { stroke: true, fill: true, fillColor: feature.properties.valueColor, fillOpacity: 0.5, color: "#3e3e3e", weight: 2, opacity: 0.7, smoothFactor: 0.5, }; }, }).addTo(map);
This section uses Leaflet's
L.geoJSON()
function to display the administrative divisions on the map. Each feature (boundary) is styled with:- Popup Binding: A popup is bound to each feature, displaying a value when clicked (e.g., formatted address and randomly generated data).
- Custom Style: Boundaries are styled with a
fillColor
,stroke
, and other map layer properties (e.g., border color, opacity).
-
Generating Choropleth Data:
function addChoroplethMapData(divisionsData) { divisionsData.features.forEach((feature) => { feature.properties.value = Math.random(); feature.properties.valueColor = palette[Math.floor(feature.properties.value * 10)]; feature.properties.displayValue = `<b>${feature.properties.address_line1}</b>: ${feature.properties.value.toFixed(2)}`; }); }
The function
addChoroplethMapData()
randomly generates avalue
between 0 and 1 for each boundary feature. Based on this value, a color from thepalette
array is assigned to the feature (valueColor
). This simulates a choropleth map where regions are shaded according to varying values. Each feature’s address and value are displayed usingdisplayValue
. -
Color Palette for the Choropleth Map:
const palette = [ "#B7EFC5", "#92E6A7", "#6EDE8A", "#4AD66D", "#2DC653", "#25A244", "#208B3A", "#1A7431", "#155D27", "#10451D" ];
This array defines the colors used to shade the map regions. Each color corresponds to a different range of values, creating the choropleth effect.
-
Custom Legend:
function buildLegend() { L.Control.Legend = L.Control.extend({ onAdd: (map) => { const paletteElement = L.DomUtil.create("div", "Legend"); palette.forEach((color, index) => { const paletteItemElement = document.createElement("div"); paletteItemElement.classList.add("palette-item"); const colorElement = document.createElement("div"); colorElement.style.backgroundColor = color; paletteItemElement.appendChild(colorElement); const textValue = `${(0.1 * index).toFixed(2)} to ${(0.1 * (index + 1) - 0.01).toFixed(2)}`; const textElement = document.createElement("div"); textElement.textContent = textValue; paletteItemElement.appendChild(textElement); paletteElement.appendChild(paletteItemElement); }); return paletteElement; }, }); const control = new L.Control.Legend(); control.addTo(map); }
The
buildLegend()
function defines a custom Leaflet control to create a map legend that visually represents the color-coded intervals. Each color in the legend corresponds to a range of values (e.g.,0.00 to 0.09
), helping users understand the meaning of different colors on the choropleth map. The legend is added to the map usingcontrol.addTo(map)
.
Conclusion
In this tutorial, we have walked through the process of creating a choropleth map using JavaScript, with a focus on understanding how to load boundary data, visualize it, and add a legend for context. We also broke down the core components of a sample JSFiddle implementation using the Leaflet library and GeoJSON data. Choropleth maps are a powerful tool for visualizing data distributions across geographic regions, whether you're analyzing population density, sales performance, or other metrics.
By customizing the color scheme and adding interactivity (such as legends and hover effects), you can create a map that is not only informative but also highly engaging for users.
Recap of Steps:
-
Get Boundary Data:
- Obtain boundary data (e.g., GeoJSON) from Geoapify.
- Get and prepare the data for use in your map.
-
Visualize the Data:
- Initialize the map using Leaflet.
- Load and display the boundary data with
L.geoJson()
and style the regions based on data properties.
-
Add a Legend:
- Create a legend that explains the meaning of each color, giving users the ability to interpret the map accurately.
By following these steps, you now have the foundation to build your own custom choropleth maps, tailored to your specific data and visualization needs.
Now that you’ve got the basics down, don’t stop here! Try using your own data and exploring different customizations. Experiment with color schemes, zoom levels, or add new interactive features like click events and tooltips.
Mapping is a great way to tell data-driven stories, and the more you customize, the more insights you'll discover. Have fun experimenting with your map and see how you can make it even more impactful!