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