How To Make a Choropleth Map With JavaScript

Developing a choropleth map is made easy with JavaScript and API integration, as they provide all the required tools and data to create interactive and visually compelling maps
Developing a choropleth map is made easy with JavaScript and API integration, as they provide all the required tools and data to create interactive and visually compelling maps

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.

ParameterDescription
apiKeyYour Geoapify API key required for authentication.
idThe place ID returned by the Geocoding API or Places API, used to specify the location of interest.
boundaryThe type of boundary to retrieve. Valid options include: administrative, postal_code, political, and low_emission_zone. The default value is administrative.
sublevelDefines the level of subdivisions. Setting this parameter allows you to get smaller subdivisions, such as neighborhoods instead of districts.
geometrySpecifies the accuracy of the boundary geometry. Valid options are: point, geometry_1000, geometry_5000, and geometry_10000. The default value is point.
langThe 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:

  1. 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.

  2. 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 of 1. This sets up a global map view.

  3. 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.

  4. 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).

  5. 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 fetches placeData that includes the place’s place_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.
  6. 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 specified sublevel. The geometry_1000 parameter ensures that boundary details are fetched at high resolution.

  7. 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).
  8. 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 a value between 0 and 1 for each boundary feature. Based on this value, a color from the palette 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 using displayValue.

  9. 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.

  10. 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 using control.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:

  1. Get Boundary Data:

    • Obtain boundary data (e.g., GeoJSON) from Geoapify.
    • Get and prepare the data for use in your map.
  2. 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.
  3. 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!