This comprehensive tutorial will guide you through building an interactive web map designed to showcase a portfolio of development projects. The application allows users to explore project locations on a map, filter them by criteria like type or status, and click on individual custom markers to view a details panel with more information. The entire application is orchestrated by a central script that pieces together the map, data, and user interface components.
đ Try the Live Demo
Experience the final result before diving into the tutorial! The live demo showcases all the features weâll be building:
- Interactive map with custom markers
- Advanced filtering by project type, status, and year
- Detailed project information panels
- Responsive design that works on all devices
Click on markers, try the filters, and explore the map to see what weâll be creating together.
Application Architecture Overview
Our application consists of several interconnected components:
flowchart TD A0["Configuration"] A1["Project Data (GeoJSON)"] A2["Map View"] A3["Filtering Logic"] A4["Custom Markers"] A5["Details Panel"] A6["Application Orchestrator"] A6 -- "Uses" --> A0 A6 -- "Loads" --> A1 A4 -- "Are added to" --> A2 A3 -- "Shows/Hides" --> A4 A4 -- "Shows details in" --> A5
Table of Contents
- Chapter 1: Map View
- Chapter 2: Project Data (GeoJSON)
- Chapter 3: Custom Markers
- Chapter 4: Filtering Logic
- Chapter 5: Details Panel
- Chapter 6: Application Orchestrator
- Chapter 7: Configuration
Chapter 1: Map View
Welcome to the National Project Portfolio! This tutorial will guide you through the core concepts of how this interactive map application is built. Letâs start with the most fundamental piece: the map itself.
Imagine you want to display your favorite vacation spots. You could write them down in a list, but thatâs not very exciting. A much better way would be to get a big corkboard map and stick a pin in each location. This gives you immediate visual context. Where are the spots in relation to each other? Are they clustered on the coast?
In our application, the Map View is that digital corkboard map. Itâs the foundational canvas that provides the geographical background. All of our project âpinsâ will be placed on this canvas.
What is the Map View?
The Map View is the core visual component of our application. Itâs responsible for a few key things:
- Displaying the World: It renders a beautiful, detailed map that users can explore.
- Handling Interaction: It allows users to pan (move the map side-to-side) and zoom (get a closer or wider view).
- Providing a Canvas: It acts as the base layer upon which we will later add our project markers and other interactive elements.
Our project doesnât build this entire mapping system from scratch. Instead, we use a powerful service called Mapbox. Think of Mapbox as a company that provides all the tools and data needed to add high-quality maps to websites and applications. Our MapView
component is our projectâs way of setting up and managing the map that Mapbox provides.
Creating Our First Map
To get a map on the screen, we first need an HTML element to hold it. In our index.html
file, thereâs a simple <div>
with an ID of map
.
<!-- index.html -->
<div id="map"></div>
This is like reserving a space on our wall for the corkboard.
Next, our main JavaScript file (assets/js/main.js
) kicks everything off. It creates a new MapView
and tells it where to appear.
// assets/js/main.js
// 1. Get our Mapbox "password" from the configuration.
const accessToken = config.mapboxAccessToken;
// 2. Create a new map instance.
const mapView = new MapView({
mapContainer: 'map', // The ID of our HTML div
mapboxAccessToken: accessToken,
center: [-98.5795, 39.8283], // Center on the USA
zoom: 3.5 // A good starting zoom level
});
Letâs break this down:
- We create a new
MapView
object. This is our custom component that makes using Mapbox easier. - We pass it some options:
mapContainer: 'map'
: This tells ourMapView
to render inside the<div id="map">
.mapboxAccessToken
: This is a special key that gives us permission to use Mapboxâs services.center
andzoom
: These properties set the initial view of the map when the page loads. Here, weâre centered on the United States.
After this code runs, youâll see a beautiful, interactive map of the United States on the webpage. You can already click and drag to pan around and use your scroll wheel to zoom in and out!
How It Works Under the Hood
You might be wondering what happens when we call new MapView(...)
. How does that simple command turn into a fully interactive map?
Our MapView
class is a âwrapperâ around the Mapbox library. It simplifies the process and tailors it for our projectâs needs. Hereâs a step-by-step look at the process.
Letâs peek inside our MapView
class in assets/js/map.js
to see the most important part.
// assets/js/map.js
class MapView {
constructor(options) {
// Tell the Mapbox library what "password" to use
mapboxgl.accessToken = options.mapboxAccessToken;
// Create the actual map from the Mapbox library
this.map = new mapboxgl.Map({
container: options.mapContainer,
style: 'mapbox://styles/mapbox/light-v11', // A clean, light style
center: options.center,
zoom: options.zoom
});
}
}
As you can see, our MapView
class is quite simple right now. It takes our clean, easy-to-read options and translates them into the format the Mapbox library expects.
Chapter 2: Project Data (GeoJSON)
Now that weâve set up our âdigital corkboardâ â a beautiful, interactive map â we need to add our projects. This raises a crucial question: how do we store all the information for each project in a way that our application can understand?
This is where our projectâs âmaster fileâ comes in: projects.geojson
.
What is GeoJSON?
Imagine you have a rolodex or a box of index cards. Each card represents one project. On that card, you write down all the important details: the projectâs name, its status (e.g., âIn Progressâ), the year it was completed, and a short description. Critically, you also write down its address or map coordinates so you know where to place a pin on your corkboard.
GeoJSON is a standard format for organizing this kind of geographical data. Itâs essentially a specially structured text file that acts as our digital rolodex. Our entire application reads from this single file to get all the information it needs to display projects on the map.
The file is named projects.geojson
and it lives in the assets/data/
folder.
The Anatomy of a Single Project
Letâs look at one âindex cardâ from our GeoJSON file. In GeoJSON terms, each project is called a Feature
. A Feature
has two main parts:
geometry
: This tells the map where the project is located.properties
: This holds all the other information about the project.
Hereâs a simplified example of a single project Feature
:
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-118.2437, 34.0522]
},
"properties": {
"name": "Downtown Tower Renovation",
"status": "Completed",
"year": 2023,
"description": "A major overhaul of a historic skyscraper."
}
}
Letâs break this down:
"type": "Feature"
: This just tells any program reading the file, âHey, this block of text describes a single thing.âgeometry
:"type": "Point"
: Weâre marking a single spot on the map, not a line or a shape."coordinates": [-118.2437, 34.0522]
: This is the most important part for mapping! Itâs the projectâs exact location in[longitude, latitude]
format.
properties
: This is a simple list of key-value pairs, just like a dictionary or an index card.
The Master File: A Collection of Features
Our actual projects.geojson
file contains a list of all our projects. It wraps all the individual Feature
objects into a larger object called a FeatureCollection
.
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { ... },
"properties": { "name": "Project Alpha", ... }
},
{
"type": "Feature",
"geometry": { ... },
"properties": { "name": "Project Beta", ... }
}
]
}
How Does the Application Use This Data?
Our application starts by loading and reading this projects.geojson
file. Hereâs the simplified code in main.js
:
// A simplified look at main.js
async function initializeApp() {
// 1. Create the map (from Chapter 1)
const mapView = new MapView(...);
// 2. Fetch the project data from our file
const response = await fetch('assets/data/projects.geojson');
const projectData = await response.json();
// 'projectData' now holds our entire FeatureCollection!
console.log(projectData.features.length + " projects loaded!");
}
initializeApp();
Chapter 3: Custom Markers
Now we have our map and our data, but theyâre not connected yet. How do we take each project from our data file and place a pin for it on the map? And more importantly, how can we make those pins instantly meaningful? This is where Custom Markers come in.
Pins on a Digital Map
Custom Markers are our digital, color-coded pins. They are the visual icons on the map that represent each project. Their appearanceâlike their color or iconâis determined by the projectâs properties, giving you at-a-glance information.
Creating a Marker for Each Project
The basic idea is simple: we need to go through our list of projects one-by-one and, for each one, create a marker and add it to the map.
// A simplified look at main.js
async function initializeApp() {
// ... map creation and data fetching from previous chapters ...
const projectData = await loadProjectData();
// 1. Create a manager for our markers.
const markerManager = new MarkerManager(mapView.map);
// 2. Tell the manager to add markers for all our projects.
markerManager.addMarkers(projectData.features);
}
initializeApp();
How It Works: From Data to a Visual Pin
Letâs peek inside assets/js/markers.js
to see how we create a single marker:
// Simplified from assets/js/markers.js
// This function creates one marker for one project.
function createAndAddMarker(project, map) {
// Step 1: Create a blank HTML div element.
const el = document.createElement('div');
el.className = 'marker'; // Give it a standard class for styling
// Step 2: Determine the icon based on the project's type.
const iconUrl = getIconForType(project.properties.type);
el.style.backgroundImage = `url(${iconUrl})`;
// Step 3: Use the Mapbox library to create and place the marker.
new mapboxgl.Marker(el)
.setLngLat(project.geometry.coordinates) // Set its location
.addTo(map); // Add it to the map
}
The getIconForType
function is a simple helper that returns the correct image path:
// A helper function inside markers.js
function getIconForType(type) {
if (type === 'Commercial') {
return 'assets/images/markers/marker-commercial.png';
} else if (type === 'Residential') {
return 'assets/images/markers/marker-residential.png';
} else {
return 'assets/images/markers/marker-default.png';
}
}
Chapter 4: Filtering Logic
Our map is now full of pins, but what if a user only wants to see projects that are âCompletedâ? Or only âResidentialâ projects? This is where our Filtering Logic comes in.
The Sieve Analogy
The Filtering Logic is like a digital sieve. When a user selects a filter, like âStatus: Completed,â our application takes all the projects and âpoursâ them through the âCompletedâ sieve. Only the projects that match this criterion remain visible on the map.
How It Works: From a Click to a Filtered Map
The process starts when a user interacts with a filter control. Letâs follow the journey of filtering for âCompletedâ projects:
- User Action: The user clicks on the âStatusâ dropdown and selects âCompleted.â
- Notifying the Logic: The UI tells our
Filtering Logic
component about this change. - Applying the Filter: The logic goes through every single project we have loaded.
- The Check: For each project, it asks: âDoes your
status
property equal âCompletedâ?â - Show or Hide: Based on the answer, it either shows or hides the projectâs marker.
The Code Behind the Sieve
// Simplified from assets/js/filters.js
class FilterManager {
constructor() {
// Keep track of the current active filters.
this.activeFilters = {
status: 'all',
type: 'all',
search: ''
};
}
}
// This function loops through all projects and decides to show or hide them.
function applyFilters(allProjects, allMarkers) {
allProjects.forEach(project => {
const marker = allMarkers[project.properties.id];
if (projectMatchesFilters(project)) {
showMarker(marker); // Make it visible
} else {
hideMarker(marker); // Make it invisible
}
});
}
function projectMatchesFilters(project) {
const properties = project.properties;
const filters = this.activeFilters;
// Check the status filter
const statusMatch = (filters.status === 'all') || (properties.status === filters.status);
return statusMatch; // Must pass all checks to be true
}
Chapter 5: Details Panel
When a user clicks on a marker, they want to see more information. This is where the Details Panel comes in.
The Back of the Baseball Card
The Details Panel is like the back of a baseball card. Itâs a dedicated part of the user interface that slides into view when a user clicks on a project marker, displaying all the detailed information about that project.
How It Works: From Click to Detailed View
- The Click: The user clicks on a specific marker.
- The Listener: Each marker has a âlistenerâ that waits for clicks.
- The Hand-off: The listener takes the project data and hands it to the
DetailsPanel
. - Populating the Panel: The panel fills its HTML elements with the project information.
- The Reveal: The panel slides into view with an animation.
The Code Behind the Panel
// Simplified from assets/js/markers.js
function createAndAddMarker(project, map, detailsPanel) {
const el = document.createElement('div');
// ... code to style the marker ...
// **The important part!**
el.addEventListener('click', () => {
detailsPanel.show(project); // Tell the panel to show this project
});
new mapboxgl.Marker(el)
.setLngLat(project.geometry.coordinates)
.addTo(map);
}
The panel HTML structure:
<!-- Simplified from index.html -->
<div id="details-panel" class="details-panel">
<img id="panel-image" src="" alt="Project Image">
<h2 id="panel-title"></h2>
<p id="panel-description"></p>
<button id="close-panel-btn">Close</button>
</div>
And the panel logic:
// Simplified from assets/js/details-panel.js
class DetailsPanel {
constructor() {
this.panel = document.querySelector('#details-panel');
this.title = document.querySelector('#panel-title');
this.description = document.querySelector('#panel-description');
this.image = document.querySelector('#panel-image');
}
show(project) {
const props = project.properties;
// Fill the HTML elements with project data
this.title.textContent = props.name;
this.description.textContent = props.description;
this.image.src = props.imageUrl;
// Add a class to make the panel slide into view
this.panel.classList.add('is-visible');
}
}
Chapter 6: Application Orchestrator
All these components need to work together harmoniously. This is the job of our Application Orchestrator, which lives in main.js
.
The Conductor of the Orchestra
Think of main.js
as the conductor. It doesnât play an instrument itselfâinstead, it initializes and coordinates all the other components.
The Startup Sequence
When you open the application, main.js
performs a specific sequence:
- Create the Map: First, it creates the Map View.
- Load the Data: It fetches the
projects.geojson
file. - Initialize Components: It creates the
MarkerManager
,FilterManager
, andDetailsPanel
. - Wire Everything Together: It connects the components so they can communicate.
A Look at the Conductorâs Score
// Simplified from assets/js/main.js
async function initializeApp() {
// Step 1: Create the main pieces
const mapView = new MapView({ /* ... map options ... */ });
const detailsPanel = new DetailsPanel();
// Step 2: Get the sheet music
const response = await fetch('assets/data/projects.geojson');
const projectData = await response.json();
// Step 3: Wire everything together
const markerManager = new MarkerManager(mapView.map);
markerManager.addMarkers(projectData.features, detailsPanel);
const filterManager = new FilterManager();
filterManager.onFilterChange(() => {
markerManager.filter(filterManager.activeFilters);
});
}
// Start the entire application!
initializeApp();
Chapter 7: Configuration
Finally, we need to manage settings like API keys and default configurations. This is where our Configuration system comes in.
The Gadgetâs Settings Page
Our Configuration file, config.js
, is like a settings page for our application. It stores all the essential settings, especially the Mapbox Access Token.
The Template and Your Secret File
Our project handles configuration with a two-file system:
config.template.js
: The blueprint showing what settings you need:
// config.template.js
const config = {
mapboxAccessToken: 'YOUR_MAPBOX_ACCESS_TOKEN_HERE',
};
config.js
: Your personal settings file with real values:
// config.js (This file is private!)
const config = {
mapboxAccessToken: 'pk.eyA1...your...real...key...',
};
Connecting the Settings to the Application
The configuration is loaded first in index.html
:
<!-- Simplified from index.html -->
<html>
<body>
<!-- 1. Load the configuration file FIRST -->
<script src="config.js"></script>
<!-- 2. Load the main application script SECOND -->
<script src="assets/js/main.js"></script>
</body>
</html>
Our application orchestrator can then use these values:
// Simplified from assets/js/main.js
async function initializeApp() {
// Read the access token from the global config object
const accessToken = config.mapboxAccessToken;
// Create the map view and pass the token to it
const mapView = new MapView({
mapContainer: 'map',
mapboxAccessToken: accessToken,
// ... other options
});
// ... rest of the initialization ...
}
Conclusion
Congratulations! Youâve now learned how to build a complete interactive web map application from scratch. Youâve covered:
- Map View: The foundational canvas using Mapbox
- Project Data: Organizing information with GeoJSON
- Custom Markers: Visual representations of your projects
- Filtering Logic: User controls for data exploration
- Details Panel: Rich information display
- Application Orchestrator: Coordinating all components
- Configuration: Managing settings and API keys
This architecture provides a solid foundation for building sophisticated mapping applications. You can extend this pattern to add features like:
- User authentication
- Real-time data updates
- Advanced filtering options
- Custom map styles
- Export functionality
The key principles youâve learnedâseparation of concerns, modular design, and centralized orchestrationâwill serve you well in any web application project.
Happy mapping!