A tinted screenshot of a grayscale globe with a few hundred pins showing locations of various UNESCO World Heritage Sites across the world. One of the pins has a popup open above it with the name of the location:

Making a map of UNESCO World Heritage Sites

I’ve been really enjoying working with Wikidata lately, using it to make a map of government agencies with presence in the fediverse, a catalogue of prominent fediverse accounts, and a few fun bots. And this month’s Glitch Code Jams topic being “teaching a thing or two”, this is a great opportunity to teach how to use Wikidata and Mapbox together. Two things!

Here’s a quick look at what we will be making.

But first, a bit of introduction.

Wikidata is, in the words of its creators, a “central storage for the structured data” used by sites like Wikipedia, Wikivoyage, Wiktionary, Wikisource, and “many other sites and services”. You can use a special query language called SPARQL to get data out of it yourself, and they also offer an API that we will be using for our project.

And you can think of Mapbox as a Google Maps alternative, offering all the typical functionality of a good quality mapping software. We will use it to place the data retrieved from Wikidata on a map.

For this tutorial we are going to be using Glitch, a popular and user-friendly web-based editor that lets you make websites and apps. To follow along, you don’t really need an account, but you might want to create one if you’d like to keep your project, otherwise it will be removed after a few days.

Start by creating a “remix” of my starter website project.

Screenshot of the Glitch editor interface. It's split into three columns, with a list of project files and buttons for settings on the left side, code editor in the center, and a live preview of the website we're making on the right.

Let’s first create a new file using the big plus sign next to Files button. As a file name, use js/modules/wikidata.js. This will create a wikidata.js file inside the js/modules folder.

Inside this file we will define a function that will run our Wikidata query.

export default async (query) => {
  const apiUrl = `https://query.wikidata.org/sparql?query=${encodeURIComponent(query)}&format=json`;
  const resp = await fetch(apiUrl);
  const respJSON = await resp.json();
  const items = respJSON?.results?.bindings || [];
  return items;
};

Great. Now, before we start writing our first query, let’s get familiar with SPARQL. Let’s head over to the Wikidata Query Service website. Here, click on the Examples button, and pick the very first one, labeled “Cats”. You will see a fairly simple query. Try running it using the big blue “Play” button.

Screenshot of the Wikidata Query Service website. The interface consists of a large text field that has an example query in it for retrieving a list of data objects related to cats.

On the side of the query editor is a vertical sidebar with a few icons, and below it is a table listing results of the query.

Now try playing with the other examples, and clicking some of the links that come back with the results. Hopefully this will give you an idea of some of the possibilities that Wikidata gives us.

Let’s switch back to Glitch and open thejs/script.js script file where we can use the wikidata function we just wrote. I will go ahead and use the query from the first example.

import ready from "./modules/ready.js";
import wikidata from "./modules/wikidata.js";

ready(async () => {
  const data = wikidata(`
  SELECT ?item ?itemLabel
    WHERE
    {
      ?item wdt:P31 wd:Q146. # Must be a cat
      SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } # Helps get the label in your language, if not, then en language
    }
  `)
});

With this code in place, you can open your browser’s developer console and you will see the results of the query. Great start!

Now, let’s go back to the Wikidata query editor. For this project, I would like to list all the different UNESCO World Heritage Sites. Here’s a query that will do just that.

SELECT DISTINCT ?item ?itemLabel ?itemDescription ?lon ?lat ?image
{
   ?item wdt:P1435 wd:Q9259 .
   ?item wdt:P131 ?place .
   ?item schema:description ?itemDescription FILTER (LANG(?itemDescription) = "en") . 
   ?item wdt:P18 ?image;
         p:P625 [
           ps:P625 ?coord;
           psv:P625 [
             wikibase:geoLongitude ?lon;
             wikibase:geoLatitude ?lat;
           ] ;
         ]
  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
}

Note that I am doing a few things here in order to get location information and description. I won’t go too much into detail here, as this tutorial is meant to be a quick introduction, but you can browse the official SPARQL documentation yourself to learn more about how to build these kinds of queries.

Here’s the full code snippet.

import ready from "./modules/ready.js";
import wikidata from "./modules/wikidata.js";

ready(async () => {
  const data = await wikidata(`
    SELECT DISTINCT ?item ?itemLabel ?itemDescription ?lon ?lat ?image
    {
       ?item wdt:P1435 wd:Q9259 .
       ?item wdt:P131 ?place .
       ?item schema:description ?itemDescription FILTER (LANG(?itemDescription) = "en") . 
       ?item wdt:P18 ?image;
             p:P625 [
               ps:P625 ?coord;
               psv:P625 [
                 wikibase:geoLongitude ?lon;
                 wikibase:geoLatitude ?lat;
               ] ;
             ]
      SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
    }
  `);
  console.log(data);
});

If you check the browser console again, you will see a list of objects returned from Wikidata. (You can also run this query inside the Wikidata query editor to get a better visual representation.)

You can see that we get a lot of information, so let’s start by filtering out what we need by mapping the data variable into a new worldHeritageSites object, or rather an array of objects.

const worldHeritageSites = data.map((item) => {
  return {
    label: item?.itemLabel?.value,
    description: item?.itemDescription?.value,
    image: item?.image?.value,
    url: item?.item?.value,
    lat: item?.lat?.value,
    lon: item?.lon?.value,
  };
});
console.log(worldHeritageSites);

Let me pause here for a moment.

One golden rule of working with APIs is that we should be caching the results, or saving them and reusing them, instead of repeatedly retrieving the live data. This can be a bit tricky without a back end. Luckily, If you go back to the Wikidata query editor, notice the option to export the data.

Screenshot of the Wikidata query interface with the

Create a new file data/wikidata/unesco.json and drag this exported file into the Glitch editor. This JSON file will be available at https://YOUR-PROJECT-NAME.glitch.me/data/wikidata/unesco.json. To get the full URL of your project, you can use the menu on top of the screen and append /data/wikidata/unesco.json to it.

A screenshot of a portion of the Glitch interface, showing the site preview with the main menu open, listing various actions you can take, including

Back in our main script, this is how we load the data:

const data = await fetch("https://YOUR-PROJECT-NAME.glitch.me/data/wikidata/unesco.json");
const dataJSON = await data.json();

The JSON has a slightly different format compared to the live data, so with a small adjustment, our script should look like this.

import ready from "./modules/ready.js";
import mapbox from "./modules/mapbox.js";

ready(async () => {
  const data = await fetch(
    "https://YOUR-PROJECT-NAME.glitch.me/data/wikidata/unesco.json"
  );
  const dataJSON = await data.json();

  const worldHeritageSites = dataJSON.map((item) => {
    return {
      label: item?.itemLabel,
      description: item?.itemDescription,
      image: item?.image,
      url: item?.item,
      lat: item?.lat,
      lon: item?.lon,
    };
  });
  console.log(worldHeritageSites);
  mapbox("map", worldHeritageSites);
});

Great, things are moving along.

Now it’s time to look at Mapbox. This will require you to set up a free account. Once you have one, go to your account dashboard, and create an API token for your project.

You can keep all of the default settings for now, just give it a name, and optionally restrict it to only work on your project’s URL, which you can get from the menu of your project’s preview.

We can use the example code from this article Mapbox tutorial, which shows how to add a marker to a map with a popup.

First, we will need to load some external files that contain the main Mapbox JavaScript and CSS code. Add the following two lines before the closing </head> tag.

<link href="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>

Next, add a DIV element with its id attribute set to "map", where you want the map to load, like this.

<main class="flex-shrink-0">
  <div class="container">
    <h1 class="mt-5">Hello world</h1>
    <div id="map"></div>
  </div>
</main>

Now, let’s add some styles from the Mapbox tutorial to our css/style.css file. We can skip the custom marker icon as we will be using the default one for now.

#map {
  width: 100%;
  min-height: 700px;
}

.mapboxgl-popup {
  max-width: 200px;
}

And now, let’s create a new JavaScript module called js/modules/mapbox.js where we will set up our main mapbox function, similar to how we did with wikidata.js.

Referencing the same Mapbox tutorial, with some minor adjustments, our script will look like this:

export default (elementId, data) => {
  mapboxgl.accessToken = "YOUR_MAPBOX_TOKEN";

  const map = new mapboxgl.Map({
    container: elementId,
    style: "mapbox://styles/mapbox/light-v11",
    center: [0, 0],
    zoom: 1,
  });

  map.on("load", () => {
    map.flyTo({
      center: [data[0].lon, data[1].lat],
      duration: 3000,
      zoom: 2,
    });

    data.forEach((item, index) => {
      const popup = new mapboxgl.Popup({ offset: 25 }).setHTML(
        `<p>
           ${item.label}, ${item.description}
         </p>
         <p>
           <a href="${item.url}">Learn more</a>
         </p>`
      );

      new mapboxgl.Marker()
        .setLngLat([item.lon, item.lat])
        .setPopup(popup)
        .addTo(map);
    });
  });
};

Here’s a breakdown of what we’re doing here:

  • First, we create a new Mapbox map.
  • Once the map is ready, we initiate a zoom animation from the globe’s center to the location of the first item in the dataset.
  • And finally, we cycle through all the data points and add them to our map.

And now we have a pretty nice prototype. There’s a few things you could do to further improve it, as homework.

  • Consider the large amount of data points on the map. Perhaps these can be added dynamically? Or maybe you could update your SPARQL query to only get a certain number of them, maybe sorted by a specific quality?
  • Because Mapbox adds all the popups at once, adding images would mean loading them all at the same time. Is there a better way to handle this?
  • Maybe there is a way to use Wikipedia for the “Learn more” links?
  • How about playing around with the design of the page?

Hope you enjoyed this walkthrough. Until next time!

More tutorials

A tinted collage screenshot of a donations table in Google Sheets overlaid with a post from a bot summarizing the total amount of donations.
A tinted screenshot of the finished project showing multiple line charts.

Introduction to Apache ECharts

Learn to make data visualizations with Apache ECharts and Bootstrap, and host them for free on Glitch.

#apache-echarts #bootstrap #dataviz #glitch

💻 Browse all tutorials