I created a map of where I’ve been, what I did and how I rated it for every day since 01/01/2022.

A brief introduction to the project

I have Google Maps Timeline enabled and I also keep a journal on my phone using an app called Pixels where I rate my day out of 5 and write a couple of sentences about what I did. So I thought, why not combine the two and have a map so I can look back on where I was and what I did.

Getting started

To start off the project, I went to Google Takeout and downloaded my timeline history as JSON. I also downloaded my journal data as JSON.

Cleaning the Data

Once the Google Timeline data was downloaded, I extracted the data and then went about processing it and combining it with my journal data using a Python script with Pandas and GeoPandas.

import pandas as pd
import geopandas as gpd
import os

To do so, I firstly cleaned my journal data. The keys were ‘date’, ‘scores’ and ‘notes’ with each score being in nested list. I therefore needed to replace the nested lists of integers with just the integers. I also needed to standardise the date to match the format of my Google Timeline data.

def cleanmyjournal(self):
        try:
            myjournaldf = pd.read_json(self.myjournal)
            myjournaldf_cleaned = myjournaldf[['date', 'scores', 'notes']]
            myjournaldf_cleaned['scores'] = myjournaldf_cleaned['scores'].apply(lambda x: x[0] if isinstance(x, list) and len(x) > 0 else None)
            # Convert 'date' column to datetime
            myjournaldf_cleaned['date'] = pd.to_datetime(myjournaldf_cleaned['date'], errors='coerce')
            # Format as string if necessary
            myjournaldf_cleaned['date'] = myjournaldf_cleaned['date'].dt.strftime('%d/%m/%Y')
            self.myjournaldf_cleaned = myjournaldf_cleaned
            print("Journal dates converted successfully")
        except KeyError as e:
            print(f"KeyError: Missing expected column: {e}")
        except Exception as e:
            print(f"There was an issue converting dates: {e}")

It was then time to clean my timeline data. The latitude and longitude are in E7 format, so the needed to be changed to standard latitude and longitude floats.

def cleanmytimeline(self):
        try:
            my_timeline = pd.read_json(self.mytimeline)
            self.my_timeline = pd.json_normalize(my_timeline['locations'])
            print("Timeline data loaded successfully")
            
            # Convert latitudeE7 and longitudeE7 to standard latitude and longitude
            self.my_timeline['latitude'] = self.my_timeline['latitudeE7'] / 1e7
            self.my_timeline['longitude'] = self.my_timeline['longitudeE7'] / 1e7

The timestamp was then changed to match that of the journal data.

# Convert timestamp to datetime
            self.my_timeline['timestamp'] = pd.to_datetime(self.my_timeline['timestamp'], errors='coerce')
            self.my_timeline['date'] = self.my_timeline['timestamp'].dt.strftime('%d/%m/%Y')
            
            # Create subset - date, latitude and longitude
            self.my_timeline_cleaned = self.my_timeline[['date', 'latitude', 'longitude']]
            print("Successfully created timeline subset")
        except KeyError as e:
            print(f"KeyError: Missing expected column: {e}")
        except Exception as e:
            print(f"There was an issue loading the timeline data: {e}")

And the two DataFrames were then combined using an inner merge. I was having a performance issue with my map initially and so I have generalised my latitude and longitude values to cut down the number of points being generated and then dropped points of the same date that are close to one another.

def combinedatasets(self):
        try:
            # Ensure both 'date' columns are datetime
            self.myjournaldf_cleaned['date'] = pd.to_datetime(self.myjournaldf_cleaned['date'], format='%d/%m/%Y', errors='coerce')
            self.my_timeline_cleaned['date'] = pd.to_datetime(self.my_timeline_cleaned['date'], format='%d/%m/%Y', errors='coerce')
            
            # Drop NaT values
            self.myjournaldf_cleaned = self.myjournaldf_cleaned.dropna(subset=['date'])
            self.my_timeline_cleaned = self.my_timeline_cleaned.dropna(subset=['date'])
            
            # Merge datasets
            self.combined_df = pd.merge(self.myjournaldf_cleaned, self.my_timeline_cleaned, on='date', how='inner')
             # Round latitude and longitude to 1 decimal place
            self.combined_df['latitude_general'] = self.combined_df['latitude'].round(2)
            self.combined_df['longitude_general'] = self.combined_df['longitude'].round(2)
        
            # Drop duplicate rows with same date, latitude, and longitude
            self.combined_df = self.combined_df.drop_duplicates(subset=['date', 'latitude_general', 'longitude_general'])
            print("Successfully combined my_journal and my_timeline")
        except Exception as e:
            print(f"There was an error combining the datasets: {e}")

Finally, I converted the DataFrame to a GeoDataFrame so that I could export the data as GeoJSON to use in a Leaflet Map.

def exportAsSpatial(self, path, name):
        try:
            # Use geopandas to convert DataFrame to GeoDataFrame using columns 'Longitude' and 'Latitude'
            self.gdf = gpd.GeoDataFrame(self.combined_df, geometry=gpd.points_from_xy(self.combined_df.longitude, self.combined_df.latitude), crs='EPSG:4326')
            print("Successfully converted dataframe to geodataframe")
            # Export GeoDataFrame to GeoJSON
            self.gdf.to_file(os.path.join(path, name), driver="GeoJSON")
            print(f"Successfully exported data: {os.path.join(path, name)}")
            
        except Exception as e:
            print(f"There was an error converting the dataframe to a geodataframe: {e}")



data_export_path = "C://PATH"
data_export_name = "C://MYDATA.geojson"

# Create an instance of the DataCleaning class with the file paths
clean_my_data = DataCleaning('my_journal.json', 'Takeout/Location History/Timeline/Records.json')

Now that I had a GeoJSON dataset of my whereabouts and what I was up to, I could have a quick look in QGIS to make sure I was happy with it and see what it would look like with a graduated colour scheme of my scores column.

Looks good, though it could do with clustering at larger scales… On to the Leaflet Map.

Making a Leaflet Map

I’ve made a few leaflet maps but I’ll be honest, my JS programming is still very amateur so I got ChatGPT to help a bit with this section.

I firstly created an HTML file with an empty full screen Leaflet map that I could then start adding to. Here’s a template for you to get started too.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Leaflet Template</title>
    <!-- Leaflet CSS -->
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
    />
    <style>
      html,
      body {
        height: 100%;
        margin: 0;
      }
      #map {
        height: 100%;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <!-- Map container -->
    <div id="map"></div>

    <!-- Leaflet JavaScript -->
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <script>
      var map = L.map('map').setView([55.9533, -3.1883], 13);
      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution:
          '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
      }).addTo(map);
    </script>
  </body>
</html>

I then added in my data using AJAX, added a graduated colour scheme that I applied to the ‘scores’ property of my GeoJSON and change the default marker style.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Simple GeoJSON Map</title>
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
    />
    <style>
      body,
      html,
      #map {
        height: 100%;
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>

    <!-- Leaflet JS -->
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <!-- Leaflet AJAX Plugin -->
    <script src="https://unpkg.com/leaflet-ajax@2.0.0/dist/leaflet.ajax.min.js"></script>
    <!-- D3.js for color scaling -->
    <script src="https://d3js.org/d3.v7.min.js"></script>

    <script>
      // Initialize the map centered on Edinburgh
      var map = L.map('map').setView([55.943, -3.2], 13);

      // Add OpenStreetMap tiles
      L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution:
          '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
        maxZoom: 18,
        tileSize: 512,
        zoomOffset: -1,
      }).addTo(map);

      // Define a color scale using D3.js for marker colors
      var colorScale = d3.scaleLinear().domain([1, 5]).range(['red', 'green']);

      // Function to create a marker with styling and a popup
      function createMarker(feature, latlng) {
        var score = feature.properties.scores;
        var color = colorScale(score);

        var marker = L.circleMarker(latlng, {
          radius: 10,
          fillColor: color,
          color: color,
          weight: 1,
          opacity: 1,
          fillOpacity: 0.9,
        });

        var popupContent = `
                <strong>Date:</strong> ${feature.properties.date}<br>
                <strong>Notes:</strong> ${feature.properties.notes}
            `;
        marker.bindPopup(popupContent);

        return marker;
      }

      // Load and display GeoJSON data
      var geojsonLayer = new L.GeoJSON.AJAX('My_Timeline.geojson', {
        pointToLayer: function (feature, latlng) {
          return createMarker(feature, latlng);
        },
      }).addTo(map);
    </script>
  </body>
</html>

This was a promising start but it looked messy and there was very little functionality other than being able to see the score and view my note about the day.

So I then added clustering which both improved performance and made it much easier to navigate the map.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>GeoJSON Map with Marker Clustering</title>
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css"
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css"
    />
    <style>
      body,
      html,
      #map {
        height: 100%;
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>

    <!-- Leaflet JS -->
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <!-- Leaflet MarkerCluster Plugin -->
    <script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>
    <!-- Leaflet AJAX Plugin -->
    <script src="https://unpkg.com/leaflet-ajax@2.0.0/dist/leaflet.ajax.min.js"></script>
    <!-- D3.js for color scaling -->
    <script src="https://d3js.org/d3.v7.min.js"></script>

    <script>
      // Initialise the map (centered on Edinburgh)
      var map = L.map('map').setView([55.943, -3.2], 13);

      // Add OpenStreetMap tiles
      L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution:
          '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
        maxZoom: 18,
        tileSize: 512,
        zoomOffset: -1,
      }).addTo(map);

      // Initialise marker cluster group
      var markers = L.markerClusterGroup();

      // Define a color scale using D3.js for marker colors
      var colorScale = d3.scaleLinear().domain([1, 5]).range(['red', 'green']);

      // Function to create a marker with styling and a popup
      function createMarker(feature, latlng) {
        var score = feature.properties.scores;
        var color = colorScale(score);

        var marker = L.circleMarker(latlng, {
          radius: 10,
          fillColor: color,
          color: color,
          weight: 1,
          opacity: 1,
          fillOpacity: 0.9,
        });

        var popupContent = `
                <strong>Date:</strong> ${feature.properties.date}<br>
                <strong>Notes:</strong> ${feature.properties.notes}
            `;
        marker.bindPopup(popupContent);

        return marker;
      }

      // Load and display GeoJSON data with clustering
      var geojsonLayer = new L.GeoJSON.AJAX('My_Timeline.geojson', {
        pointToLayer: function (feature, latlng) {
          return createMarker(feature, latlng);
        },
        onEachFeature: function (feature, layer) {
          markers.addLayer(layer); // Add each marker to the cluster group
        },
      });

      // Add the marker cluster group to the map
      map.addLayer(markers);
    </script>
  </body>
</html>

And tweaked the zoom levels and cluster breakup scales so that data displayed right down to individual points past a certain zoom level.

I finally wanted a way to filter my timeline by date and to do so used flatpickr. It was a bit tricky working out how to filter the data and then reset to display all the data again (I’m still a JS noobie) but eventually I got it to work.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My Timeline Map</title>
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet.markercluster@1.3.0/dist/MarkerCluster.css"
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet.markercluster@1.3.0/dist/MarkerCluster.Default.css"
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"
    />
    <style>
      body {
        padding: 0;
        margin: 0;
      }
      html,
      body,
      #map {
        height: 100%;
        width: 100%;
      }
      .controls {
        position: absolute;
        bottom: 10px;
        left: 10px;
        z-index: 1000;
        background-color: white;
        padding: 10px;
        border-radius: 5px;
        box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
      }
      .controls label,
      .controls button,
      .controls input {
        display: block;
        margin-bottom: 5px;
        width: 100%;
      }
      .controls button {
        cursor: pointer;
      }
      .date-picker {
        font-family: Arial, Helvetica, sans-serif;
        font-size: medium;
      }

      .flatpickr-calendar {
        font-family: Arial, sans-serif; /* Change to your desired font */
      }

      .flatpickr-input {
        font-family: Arial, sans-serif; /* Change to your desired font */
      }

      .flatpickr-day {
        font-family: Arial, sans-serif; /* Change to your desired font */
      }

      .flatpickr-month {
        font-family: Arial, sans-serif; /* Change to your desired font */
      }
    </style>
  </head>
  <body>
    <div class="controls">
      <label class="date-picker" for="date-picker">Select Date:</label>
      <input
        class="date-picker"
        id="date-picker"
        type="text"
        placeholder="YYYY-MM-DD"
      />
      <button class="date-picker" id="reset-btn">Show All</button>
    </div>
    <div id="map"></div>

    <!-- Include Leaflet JS first -->
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <!-- Include leaflet-ajax library -->
    <script src="https://unpkg.com/leaflet-ajax@2.0.0/dist/leaflet.ajax.min.js"></script>
    <!-- Include leaflet.markercluster library from CDN -->
    <script src="https://unpkg.com/leaflet.markercluster@1.3.0/dist/leaflet.markercluster.js"></script>
    <!-- Include Flatpickr -->
    <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
    <!-- Include D3.js library from CDN -->
    <script src="https://d3js.org/d3.v7.min.js"></script>

    <script>
      // Initialise map and store original view
      var map = L.map('map').setView([55.943, -3.2], 13);
      var originalView = { center: map.getCenter(), zoom: map.getZoom() };

      L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution:
          '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
        maxZoom: 18,
        tileSize: 512, // Larger tiles can be more efficient
        zoomOffset: -1,
      }).addTo(map);

      var markers = L.markerClusterGroup();
      var allMarkers = [];

      var colorScale = d3.scaleLinear().domain([1, 5]).range(['red', 'green']);

      function createMarker(feature, latlng) {
        var score = feature.properties.scores;
        var color = colorScale(score);

        var marker = L.circleMarker(latlng, {
          radius: 10,
          fillColor: color,
          color: color,
          weight: 1,
          opacity: 1,
          fillOpacity: 0.9,
        });

        var popupContent = `
            <strong>Date:</strong> ${feature.properties.date}<br>
            <strong>Notes:</strong> ${feature.properties.notes}
          `;
        marker.bindPopup(popupContent);

        marker.feature = feature; // Store feature data

        return marker;
      }

      var geojsonLayer = new L.GeoJSON.AJAX('My_Timeline.geojson');

      geojsonLayer.on('data:loaded', function (event) {
        allMarkers = [];
        markers.clearLayers();

        event.target.eachLayer(function (layer) {
          if (layer.feature.geometry.type === 'Point') {
            var marker = createMarker(layer.feature, layer.getLatLng());
            allMarkers.push(marker);
            markers.addLayer(marker);
          }
        });

        map.addLayer(markers);
      });

      geojsonLayer.on('data:error', function (error) {
        console.error('Error loading GeoJSON data:', error);
      });

      var datePicker = flatpickr('#date-picker', {
        dateFormat: 'Y-m-d',
        onChange: function (selectedDates) {
          var selectedDate = selectedDates[0]
            ? selectedDates[0].toISOString().split('T')[0]
            : null;

          markers.clearLayers();
          if (selectedDate) {
            var filteredMarkers = [];
            allMarkers.forEach((marker) => {
              var markerDate = marker.feature.properties.date.split('T')[0];
              if (markerDate === selectedDate) {
                filteredMarkers.push(marker);
                markers.addLayer(marker);
              }
            });

            // Zoom to the filtered markers if any
            if (filteredMarkers.length > 0) {
              var group = new L.featureGroup(filteredMarkers);
              map.fitBounds(group.getBounds());
            } else {
              // If no markers match, zoom out a bit
              map.setView(originalView.center, originalView.zoom - 1);
            }
          } else {
            allMarkers.forEach((marker) => markers.addLayer(marker));
            map.setView(originalView.center, originalView.zoom);
          }
        },
      });

      document
        .getElementById('reset-btn')
        .addEventListener('click', function () {
          datePicker.clear(); // Clear the date picker
          markers.clearLayers(); // Clear current markers
          allMarkers.forEach((marker) => markers.addLayer(marker)); // Restore all markers
          map.setView(originalView.center, originalView.zoom); // Restore original view
        });
    </script>
  </body>
</html>

So that’s it. I now have a way of seeing all the places I’ve been, trips out, holidays etc and what I did on those days, as well as how I’ve rated them. It’s been a great little trip down memory lane. In the future I’d like to add images to my popups if I took a picture on that day as well as use the dataset to learn about natural language models and semantic analysis, as well as continue learning JS via visualisation repositories such as Chart.js.

Below, for those of you interested I’ll break down the leaflet map script to explain each section but otherwise, thank you very much for reading and if you have any questions then feel free to ask me via my LinkedIn.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My Timeline Map</title>
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet.markercluster@1.3.0/dist/MarkerCluster.css"
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet.markercluster@1.3.0/dist/MarkerCluster.Default.css"
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"
    />

Start with HTML boilerplate and add in the necessary links to the JS libraries you’ll be using.

<style>
      body {
        padding: 0;
        margin: 0;
      }
      html,
      body,
      #map {
        height: 100%;
        width: 100%;
      }
      .controls {
        position: absolute;
        bottom: 10px;
        left: 10px;
        z-index: 1000;
        background-color: white;
        padding: 10px;
        border-radius: 5px;
        box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
      }
      .controls label,
      .controls button,
      .controls input {
        display: block;
        margin-bottom: 5px;
        width: 100%;
      }
      .controls button {
        cursor: pointer;
      }
      .date-picker {
        font-family: Arial, Helvetica, sans-serif;
        font-size: medium;
      }

      .flatpickr-calendar {
        font-family: Arial, sans-serif; /* Change to your desired font */
      }

      .flatpickr-input {
        font-family: Arial, sans-serif; /* Change to your desired font */
      }

      .flatpickr-day {
        font-family: Arial, sans-serif; /* Change to your desired font */
      }

      .flatpickr-month {
        font-family: Arial, sans-serif; /* Change to your desired font */
      }

The styles include making the map fullscreen, moving the calendar to the bottom of the screen so it doesn’t overlap the zoom buttons and changing the default fonts.

 </style>
  </head>
  <body>
    <div class="controls">
      <label class="date-picker" for="date-picker">Select Date:</label>
      <input
        class="date-picker"
        id="date-picker"
        type="text"
        placeholder="YYYY-MM-DD"
      />
      <button class="date-picker" id="reset-btn">Show All</button>
    </div>
    <div id="map"></div>

The calendar and map are then added.

<!-- Include Leaflet JS first -->
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <!-- Include leaflet-ajax library -->
    <script src="https://unpkg.com/leaflet-ajax@2.0.0/dist/leaflet.ajax.min.js"></script>
    <!-- Include leaflet.markercluster library from CDN -->
    <script src="https://unpkg.com/leaflet.markercluster@1.3.0/dist/leaflet.markercluster.js"></script>
    <!-- Include Flatpickr -->
    <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
    <!-- Include D3.js library from CDN -->
    <script src="https://d3js.org/d3.v7.min.js"></script>

And the libraries.

<script>
      // Initialise map and store original view
      var map = L.map('map').setView([55.943, -3.2], 13);
      var originalView = { center: map.getCenter(), zoom: map.getZoom() };

      L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution:
          '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
        maxZoom: 18,
        tileSize: 512, // Larger tiles can be more efficient
        zoomOffset: -1,
      }).addTo(map);

      var markers = L.markerClusterGroup();
      var allMarkers = [];

The map is then initialised. I set the view to Edinburgh as this is where I’m based. I then added OSM as the basemap and changed the tile size to improve performance. A variable for the clusters and a variable for all markers were created.

var colorScale = d3.scaleLinear().domain([1, 5]).range(['red', 'green']);

      function createMarker(feature, latlng) {
        var score = feature.properties.scores;
        var color = colorScale(score);

        var marker = L.circleMarker(latlng, {
          radius: 10,
          fillColor: color,
          color: color,
          weight: 1,
          opacity: 1,
          fillOpacity: 0.9,
        });

        var popupContent = `
            <strong>Date:</strong> ${feature.properties.date}<br>
            <strong>Notes:</strong> ${feature.properties.notes}
          `;
        marker.bindPopup(popupContent);

        marker.feature = feature; // Store feature data

        return marker;
      }

Using D3, as the scores go from 1 to 5, a colour gradient of red to green was given to the scores property. A custom marker – a simple circle was created for each score and a variable was created to format the date and notes of that score. ‘bindPopup’ is then used to apply that content to each geoJSON feature.

var geojsonLayer = new L.GeoJSON.AJAX('My_Timeline.geojson');

      geojsonLayer.on('data:loaded', function (event) {
        allMarkers = [];
        markers.clearLayers();

        event.target.eachLayer(function (layer) {
          if (layer.feature.geometry.type === 'Point') {
            var marker = createMarker(layer.feature, layer.getLatLng());
            allMarkers.push(marker);
            markers.addLayer(marker);
          }
        });

        map.addLayer(markers);
      });

      geojsonLayer.on('data:error', function (error) {
        console.error('Error loading GeoJSON data:', error);
      });

A new AJAX object is then created to load the GeoJSON data from my GeoJSON file. If the data can be accessed via a URL then this is primarily what AJAX is useful for.

To handle resetting the data for when the calendar was implemented, data:loaded is used as an event handler to wipe clean the map ready for new data to be displayed.

The eachLayer function then iterates over each feature in the GeoJSON dataset and adds the feature to allMarkers and the markers cluster group.

The cluster group is then added to the map.

The date picker function is where I ran into issues loading and resetting the data as well as CORS issues when trying to display my filtered data but eventually with the help of ChatGPT and Stack Overflow I got it working, though resetting the data is a little bit slow.

var datePicker = flatpickr('#date-picker', {
    dateFormat: 'Y-m-d',
    onChange: function (selectedDates) {

The date picker is initialised in the format Y-m-d, this can be customised and a function then triggers once a date has been picked.

    var selectedDate = selectedDates[0]
        ? selectedDates[0].toISOString().split('T')[0]
        : null;

When a date is selected, the date is converted to an ISO string and chopped down to match the Y-m-d format.

    markers.clearLayers();

The map is then cleared.

    if (selectedDate) {
        var filteredMarkers = [];
        allMarkers.forEach((marker) => {
            var markerDate = marker.feature.properties.date.split('T')[0];
            if (markerDate === selectedDate) {
                filteredMarkers.push(marker);
                markers.addLayer(marker);
            }
        });

allMarkers is iterated over and those with matching dates to the selected date are added to the map.

        // Zoom to the filtered markers if any
        if (filteredMarkers.length > 0) {
            var group = new L.featureGroup(filteredMarkers);
            map.fitBounds(group.getBounds());
        } else {
            // If no markers match, zoom out a bit
            map.setView(originalView.center, originalView.zoom - 1);
        }
    } else {
        allMarkers.forEach((marker) => markers.addLayer(marker));
        map.setView(originalView.center, originalView.zoom);
    }

The map then zooms to the filtered markers.

Leave a comment

Latest Stories