Learning objectives:

At the end of the lesson, learners should be able to:

  • Understand how to change the default positions of some elements of Leaflet. 
  • Understand how to fetch real time data using D3.js JavaScript library. 
  • Understand how to integrate Leaflet.timeline plugin to the Leaflet and visualize the data over time.

Overview:

This chapter guides users to fetch geographical data directly from API without manually downloading it using D3.js library , visualize that data over time using external Leaflet plugin, and customize that visualization. At the end of the lesson, users will be able to visualize and control the earthquake data in Kyrgyzstan and neighboring regions for a time frame. 

Requirements:

  • A code editor such as Visual Studio Code, Sublime Text, Notepad++ or your favorite code editor.
  • A web browser such as Google Chrome or Mozilla Firefox
  • Basic knowledge of HTML, CSS, and JavaScript.

Step 1: Create a new HTML file

The first step is to create a HTML file and include all the necessary CSS files. The CSS section describes the styles for different elements of the HTML body like map, legend, title, etc.  

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Leaflet - Advance</title>
    <!-- Leaflet CSS>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
        integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=" crossorigin="" />  
    <!-- Styles -->
    <style type="text/css">
        body {
            padding: 0;
            margin: 0;
        }

        #map,
        body,
        html {
            height: 100%;
        }

        .leaflet-bottom.leaflet-left {
            width: 100%;
        }

        .leaflet-control-container .leaflet-timeline-controls {
            box-sizing: border-box;
            width: 100%;
            margin: 0;
            margin-bottom: 15px;
        }

        .jumbotron {
            text-align: center;
            color: black;
            position: relative;
            width: 100%;
            height: 100%;
            background-size: cover;
            overflow: hidden;
        }

        #legend {
            position: absolute;
            z-index: 1000;
            top: 200px;
            width: 200px;
            height: 265px;
            border: 1px solid whitesmoke;
            background-color: grey;
            margin-left: 10px;

        }

        .title {
            position: absolute;
            z-index: 2000;
            color: white;
            margin-left: 20%;
            font-family: 'Courier New', Courier, monospace;
            font-size: 80;
        }
    </style>
</head>
<body>
    <div id="map">
        <h1 class="title">Earthquakes in Kyrgyzstan and neighbouring region (2010 - now) </h1>
        <div id="legend">
            <h2 style="padding-left: 20px;">Legend</h2>
            <h4 style="padding-left: 80px;">Magnitude (Richter Scale)</h4>
            <svg width="200" height="200">
                <circle cx="50" cy="20" r="8" fill="#fee5d9" />
                <text x="100" y="25">< 4 </text>
                <circle cx="50" cy="60" r="8" fill="#fcae91" />
                <text x="100" y="65">≥ 4 and < 5</text>
                <circle cx="50" cy="100" r="8" fill="#fb6a4a" />
                <text x="100" y="105">≥ 5 and < 6 </text>
                <circle cx="50" cy="140" r="8" fill="#cb181d" />
                <text x="100" y="145">≥ 6 </text>
            </svg>
        </div>
    </div>
    <script>
 // JavaScript codes go here
    </script>
</body>

</html>

Step 2: Importing necessary library and plugin

In this step, The CDN files for JavaScript files for D3.js and Leaflet.timeline plugin are incorporated in the head of the HTML file. D3.js is a JavaScript library for manipulating and visualizing data in the web browsers.

 <!--Leaflet CDN-->
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
        integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM=" crossorigin=""></script>

    <!--D3 CDN-->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>


    <!-- Leaflet Timeline CDN -->
    <script src="https://cdn.jsdelivr.net/npm/leaflet.timeline@1.2.0/dist/leaflet.timeline.min.js"></script>

Step 3: Fetching earthquake data

The URL link for API endpoint of the data is assigned to a variable and the administrative regions of the country is assigned to another variable. A GET request is performed to access the data from the link and a function is called with those two variables. The body of the function will additionally contain elements to display the data, color it, add basemaps, and importantly use Leaflet timeline plugin to visualize the time along with the data in the next stage. 

// Earthquakes
        var quakeUrl = "https://earthquake.usgs.gov/fdsnws/event/1/query.geojson?starttime=2010-01-01%2000:00:00&endtime=2023-05-04%2023:59:59&maxlatitude=43.405&minlatitude=39.147&maxlongitude=80.288&minlongitude=68.862&minmagnitude=1&eventtype=earthquake&orderby=time";


        // Faultiline boundaries found using github, based off of instructions.
        var boundaryURL = "https://gist.githubusercontent.com/ZhibekSolp/acebe83817b140e01a795c9d565b5088/raw/f64e71929cf6f6fd4d7351c6136735f38cf84b51/Kyrgyzstan.geojson";


        // Perform a GET request to the quake and boundary URL
        renderMap(quakeUrl, boundaryURL);


        function renderMap(quakeUrl, boundaryURL) {
d3.json(quakeUrl, function (data) {
                console.log(quakeUrl);
                // Store response into earthquakeData
                var earthquakeData = data;
                // Once we get a response, send the data.features object to the createFeatures function
                d3.json(boundaryURL, function (data) {
                    var boundaryData = data;
                    createFeatures(earthquakeData, boundaryData);
                });
            });




		// more function body here
}

Step 4: Add basemaps and additional layers

This part of the code will initialize Leaflet map with this center 41.20438, 74.766098. Add multiple basemaps and overlay boundaries and earthquake data obtained in previous step.

            // Function to create maps with base layer and overlay layer that allows for markers
            function createMap(earthquakes, boundaries, timelineLayer) {

                // Basemaps
                var osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
                    attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, ' +
                        '<a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
                        'Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
                    maxZoom: 18
                });


                var usgs = L.tileLayer('https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}', {
                    maxZoom: 20,
                    attribution: 'Tiles courtesy of the U.S. Geological Survey'
                });


                var CartoDB_DarkMatter = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
                    attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
                    subdomains: 'abcd',
                    maxZoom: 20
                });


                var baseMaps = {
                    "OpenStreetMap": osm,
                    "CartoDB": CartoDB_DarkMatter,
                    "USGS Imagery": usgs
                };


                // Create our map, giving it the osm and boundaries layers to display on load
                var myMap = L.map("map", {
                    center: [41.20438, 74.766098],
                    zoom: 6,
                    layers: [CartoDB_DarkMatter, boundaries]
                });


                // Overlay objects
                var overlayMaps = {
                    "Earthquakes": earthquakes,
                    "Boundaries": boundaries
                };


                // Layer control
                L.control.layers(baseMaps, overlayMaps, {
                    collapsed: false
                }).addTo(myMap);


                L.control.scale({
                    metric: true,
                    imperial: true,
                    position: 'topleft',
                }).addTo(myMap);


                var timelineControl = L.timelineSliderControl({
                    formatOutput: function (date) {
                        return new Date(date).toString();
                    },
                    steps: 500,
                    // speed: 1,
                    duration: 200000,
                    showTicks: false,
                    enablePlayback: true,
                });
                timelineControl.addTo(myMap);
                timelineControl.addTimelines(timelineLayer);
                timelineLayer.addTo(myMap);
} 

The scale of the map as well as other control layers are also added to the map. The last part of the above code block initializes L.TimelineSliderControl class of the Leaflet.timeline plugin.

Step 5: Creating markers, boundaries, popups and manipulating timeline plugin

Now, in this step, we create markers and pop ups in the markers. Clicking on the individual markers, the user will get additional information about that particular data. Additionally, L.timeline is tweaked to control the intervals between the data

                function createFeatures(earthquakeData, boundaryData) {
                // Function to create circles
                function createCircles(feature, layer) {
                    return new L.circleMarker([feature.geometry.coordinates[1], feature.geometry.coordinates[0]], {
                        fillOpacity: 1,
                        color: chooseColor(feature.properties.mag),
                        fillColor: chooseColor(feature.properties.mag),
                    });
                }

                function onEachEarthquake(feature, layer) {
                    layer.bindPopup("<h1>" + feature.properties.place + "</h3><hr><p>" + feature.properties.mag + " Magnitude " + new     Date(feature.properties.time) + "</p>");
                };

                function createBoundaries(feature, layer) {
                    L.polyline(feature.geometry.coordinates);
                };

                var earthquakes = L.geoJSON(earthquakeData, {
                    onEachFeature: onEachEarthquake,
                    pointToLayer: createCircles
                });

                var boundaries = L.geoJSON(boundaryData, {
                    onEachFeature: createBoundaries,
                    style: {
                        weight: 2,
                        color: "purple"
                    }
                });

                var timelineLayer = L.timeline(earthquakeData, {
                    getInterval: function (feature) {
                        return {
                            start: feature.properties.time,
                            end: feature.properties.time + feature.properties.mag * 100000000


                        };
                    },
                    pointToLayer: createCircles,
                    onEachFeature: onEachEarthquake
                });

                createMap(earthquakes, boundaries, timelineLayer);
            }

Step 6: Defining and assigning the colors based on magnitude

In the last part of the JavaScript section, sequential colors are defined in a variable and a function is defined which returns different colors for different magnitudes of the data. 

// Define colors of circle
        var color = ['#fee5d9', '#fcae91', '#fb6a4a', '#cb181d'];


        // Function to choose the color based on magnitude


        function chooseColor(magnitude) {
            return magnitude < 4 ? color[0] :
                (magnitude >= 4 && magnitude < 5) ? color[1] :
                    (magnitude >= 5 && magnitude < 6) ? color[2] :
                        color[3];
        };

Step 7: Saving the HTML file

Finally, the head and body tag of the HTML file are closed. CSS contents are stored inside the head tag and the JavaScript content is stored inside the body tag. The file should be saved and you should see the timeline of earthquakes in Kyrgyzstan and neighboring regions, when you open the file in any browser.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Leaflet - Advance</title>
    <!-- Leaflet CSS & JS -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
        integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=" crossorigin="" />
    <script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
        integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM=" crossorigin=""></script>
    <!--D3 CDN-->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
    <!-- Leaflet Timeline CDN -->
    <script src="https://cdn.jsdelivr.net/npm/leaflet.timeline@1.2.0/dist/leaflet.timeline.min.js"></script>

    <!-- Styles -->
    <style type="text/css">
        body {
            padding: 0;
            margin: 0;
        }
        #map,
        body,
        html {
            height: 100%;
        }
        .leaflet-bottom.leaflet-left {
            width: 100%;
        }
        .leaflet-control-container .leaflet-timeline-controls {
            box-sizing: border-box;
            width: 100%;
            margin: 0;
            margin-bottom: 15px;
        }
        .jumbotron {
            text-align: center;
            color: black;
            position: relative;
            width: 100%;
            height: 100%;
            background-size: cover;
            overflow: hidden;
        }
        #legend {
            position: absolute;
            z-index: 1000;
            top: 200px;
            width: 200px;
            height: 265px;
            border: 1px solid whitesmoke;
            background-color: grey;
            margin-left: 10px;


        }
        .title {
            position: absolute;
            z-index: 2000;
            color: white;
            margin-left: 20%;
            font-family: 'Courier New', Courier, monospace;
            font-size: 80;
        }
    </style>
</head>
<body>
    <div id="map">
        <h1 class="title">Earthquakes in Kyrgyzstan and neighbouring region (2010 - now) </h1>
        <div id="legend">
            <h2 style="padding-left: 20px;">Legend</h2>
            <h4 style="padding-left: 80px;">Magnitude (Richter Scale)</h4>
            <svg width="200" height="200">
                <circle cx="50" cy="20" r="8" fill="#fee5d9" />
                <text x="100" y="25">< 4 </text>
                <circle cx="50" cy="60" r="8" fill="#fcae91" />
                <text x="100" y="65">≥ 4 and < 5</text>
                <circle cx="50" cy="100" r="8" fill="#fb6a4a" />
                <text x="100" y="105">≥ 5 and < 6 </text>
                <circle cx="50" cy="140" r="8" fill="#cb181d" />
                <text x="100" y="145">≥ 6 </text>
            </svg>
        </div>
    </div>
    <script>
        // Earthquakes
        var quakeUrl = "https://earthquake.usgs.gov/fdsnws/event/1/query.geojson?starttime=2010-01-01%2000:00:00&endtime=2023-05-04%2023:59:59&maxlatitude=43.405&minlatitude=39.147&maxlongitude=80.288&minlongitude=68.862&minmagnitude=1&eventtype=earthquake&orderby=time";


        // Faultiline boundaries found using github, based off of instructions.
        var boundaryURL = "https://gist.githubusercontent.com/ZhibekSolp/acebe83817b140e01a795c9d565b5088/raw/f64e71929cf6f6fd4d7351c6136735f38cf84b51/Kyrgyzstan.geojson";


        // Perform a GET request to the quake and boundary URL
        renderMap(quakeUrl, boundaryURL);

        function renderMap(quakeUrl, boundaryURL) {
            d3.json(quakeUrl, function (data) {
                console.log(quakeUrl);
                // Store response into earthquakeData
                var earthquakeData = data;
                // Once we get a response, send the data.features object to the createFeatures function
                d3.json(boundaryURL, function (data) {
                    var boundaryData = data;
                    createFeatures(earthquakeData, boundaryData);
                });
            });
            // Function to create maps with base layer and overlay layer that allows for markers
            function createMap(earthquakes, boundaries, timelineLayer) {

                // Basemaps
                var osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
                    attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, ' +
                        '<a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
                        'Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
                    maxZoom: 18
                });


                var usgs = L.tileLayer('https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}', {
                    maxZoom: 20,
                    attribution: 'Tiles courtesy of the U.S. Geological Survey'
                });


                var CartoDB_DarkMatter = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
                    attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
                    subdomains: 'abcd',
                    maxZoom: 20
                });


                var baseMaps = {
                    "OpenStreetMap": osm,
                    "CartoDB": CartoDB_DarkMatter,
                    "USGS Imagery": usgs
                };


                // Create our map, giving it the osm and boundaries layers to display on load
                var myMap = L.map("map", {
                    center: [41.20438, 74.766098],
                    zoom: 6,
                    layers: [CartoDB_DarkMatter, boundaries]
                });


                // Overlay objects
                var overlayMaps = {
                    "Earthquakes": earthquakes,
                    "Boundaries": boundaries
                };


                // Layer control
                L.control.layers(baseMaps, overlayMaps, {
                    collapsed: false
                }).addTo(myMap);

                L.control.scale({
                    metric: true,
                    imperial: true,
                    position: 'topleft',
                }).addTo(myMap);

                var timelineControl = L.timelineSliderControl({
                    formatOutput: function (date) {
                        return new Date(date).toString();
                    },
                    steps: 500,
                    // speed: 1,
                    duration: 200000,
                    showTicks: false,
                    enablePlayback: true,
                });
                timelineControl.addTo(myMap);
                timelineControl.addTimelines(timelineLayer);
                timelineLayer.addTo(myMap);
            }
            // Function to create markers
            function createFeatures(earthquakeData, boundaryData) {
                // Function to create circles
                function createCircles(feature, layer) {
                    return new L.circleMarker([feature.geometry.coordinates[1], feature.geometry.coordinates[0]], {
                        fillOpacity: 1,
                        color: chooseColor(feature.properties.mag),
                        fillColor: chooseColor(feature.properties.mag),


                    });
                }


                function onEachEarthquake(feature, layer) {
                    layer.bindPopup("<h1>" + feature.properties.place + "</h3><hr><p>" + feature.properties.mag + " Magnitude " + new Date(feature.properties.time) + "</p>");
                };


                function createBoundaries(feature, layer) {
                    L.polyline(feature.geometry.coordinates);
                };


                var earthquakes = L.geoJSON(earthquakeData, {
                    onEachFeature: onEachEarthquake,
                    pointToLayer: createCircles
                });


                var boundaries = L.geoJSON(boundaryData, {
                    onEachFeature: createBoundaries,
                    style: {
                        weight: 2,
                        color: "purple"
                    }
                });

                var timelineLayer = L.timeline(earthquakeData, {
                    getInterval: function (feature) {
                        return {
                            start: feature.properties.time,
                            end: feature.properties.time + feature.properties.mag * 100000000
                        };
                    },
                    pointToLayer: createCircles,
                    onEachFeature: onEachEarthquake
                });
                createMap(earthquakes, boundaries, timelineLayer);
            }
        }
        // Define colors of circle
        var color = ['#fee5d9', '#fcae91', '#fb6a4a', '#cb181d'];


        // Function to choose the color based on magnitude
        function chooseColor(magnitude) {
            return magnitude < 4 ? color[0] :
                (magnitude >= 4 && magnitude < 5) ? color[1] :
                    (magnitude >= 5 && magnitude < 6) ? color[2] :
                        color[3];
        };
    </script>
</body>
</html>

Conclusion:

In this lesson, we learned how to fetch API based data and retrieve them using d3.js, utilize the Leaflet.timeline plugin to visualize the data with respect to time as animation. Although around 13 years of visualization is shown here, the plugin can be used to visualize longer time frames by tweaking some components of it.