Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
60 changes: 11 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,15 @@
## Samples
# Philly Heat & Cooling — Blueprint Story Map

Find examples from previous years and elsewhere on the internet at https://github.com/Weitzman-MUSA-JavaScript/story-map-project-examples
A **PowerPoint-style** slideshow story map with a **blueprint basemap** and smooth fade scrolling transitions.

## Instructions
## Description
This maps involves with creating a scrolling Story Map that allows users to learn about heat vulnerability in Philadelphia. And use the scroll function on their devices to explore cooling locations in Philadelphia.

### Step 1: Choose a topic
The purpose of this story map is to guide users who are not familiar with cooling locations in Philadelphia, to explore their best options (nearest option) to take a small cool break in Philadelphia.

Choose a topic that is fruitfully explained with some combination of narrative and geographic elements. Think about what data you want to tell a story about. Whatever data you use, **be sure to include citations somewhere in your app interface**. You can choose a dataset from any of a number of sources, for example:

* Use data you've been working with for another class
* Create your own dataset (check out [geojson.io](https://geojson.io))
* Find data from an open data repository...

#### OpenDataPhilly.org

OpenDataPhilly has lots of Philadelphia-specific data, like:

- [Neighborhood Boundaries](https://opendataphilly.org/dataset/philadelphia-neighborhoods)
- Historic [Streets](https://opendataphilly.org/dataset/historic-streets), [Districts](https://opendataphilly.org/dataset/philadelphia-registered-historic-districts), or [Properties](https://opendataphilly.org/dataset/philadelphia-registered-historic-sites)
- [School Information](https://opendataphilly.org/dataset/school-information-data)
- [PA Horticultural Society Land Care](https://opendataphilly.org/dataset/land-care)

#### Other open data portals

Many other cities, counties, states, and countries have dedicated data portals as well. Here are a couple of lists of state-sponsored open data sites:

- [Data.gov - Open Government](https://data.gov/open-gov/)
- [Open Knowledge Foundation - DataPortals.org](https://dataportals.org/)

#### Independently compiled data sources

Sources like [Stop Demolishing Philly](https://www.stopdemolishingphilly.com/map/) or other privately compiled data sources.


Use one of the template story maps in the _templates/_ folder, modified as you see fit, to explain your topic. For example, open [templates/scrollytelly/](templates/scrollytelly/) and copy the contents to the root folder in this repository. You can then modify the HTML, CSS, JavaScript, and data to suit your needs.

### Step 2: Think About Slide Content

Your story will have multiple slides, each with a title, some additional text, maybe images, and geographic data. Your slide content will go straight into your HTML, and your map features will go in to separate GeoJSON files in the [data/](data/) folder.

### Step 3: Submit your story map

Commit your code and push it to your repository on GitHub. Set up GitHub pages on the repository and submit a new pull request into the original project repository in the class organization.

#### Submission Checklist

- [ ] Pushed latest code to the `main` branch of your repository
- [ ] Linted JS and CSS code
- [ ] Turned on GitHub Pages for the repository and verified that your site works when deployed
- [ ] Submitted a pull request to the original repository in the class organization
- [ ] In the PR **title**, included your name at least
- [ ] In the PR **description**, included a brief description of your topic, and your target audience
## Data
- `data/heat_vulnerability_ct.geojson`
- `data/philadelphia_neighborhoods.geojson`
- `data/ppr_program_sites.geojson`
- `data/ppr_swimming_pools.geojson`
The project used data from Open Data Philly, including heat_vulnerability, philadelphia neighborhood boundary, and cooling locations (swimming pools and recreation sites/parks).
391 changes: 391 additions & 0 deletions data/heat_vulnerability_ct.geojson
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: This file is about 1.5MB. It's on the edge of being too large for what I'd recommend for a public-facing app. There are a number of things you could do to make it smaller, such as remove fields you're not using, and reducing the precision of the coordinates (right now you're using 15 digits of precision, which is default in many GIS platforms; however seven decimal places corresponds to about a centimeter of precision in the real world: https://wiki.openstreetmap.org/wiki/Precision_of_coordinates)

Large diffs are not rendered by default.

166 changes: 166 additions & 0 deletions data/philadelphia_neighborhoods.geojson

Large diffs are not rendered by default.

178 changes: 178 additions & 0 deletions data/ppr_program_sites.geojson

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions data/ppr_swimming_pools.geojson

Large diffs are not rendered by default.

Binary file added img/skyline.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Philly Heat & Cooling — Blueprint Story Map</title>
<meta name="description" content="A slideshow-style story map on heat vulnerability and cooling resources in Philadelphia." />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="map" class="map blueprint" aria-label="Blueprint basemap of Philadelphia"></div>

<!-- Slide deck -->
<div class="slides" id="slides" role="region" aria-label="Slideshow">

<section class="slide active" data-step="0" data-layer="none" data-center="39.9526,-75.1652" data-zoom="11.5">
<div class="content">
<h1>Staying Cool in Philly</h1>
<p class="lead">Heat Vulnerability & Access to Cooling Resources</p>
<img src="img/skyline.jpg" alt="Philadelphia skyline" class="hero" />
<p class="credits">Scroll or press ↓ to advance • Zoom buttons control the map</p>
<button id="sourcesBtn" class="btn">Show sources</button>
</div>
</section>

<section class="slide" data-step="1" data-layer="hvi" data-center="39.965,-75.14" data-zoom="12.0">
<div class="content">
<h2>Where heat risk is highest</h2>
<p>The City’s Heat Vulnerability Index (HVI) highlights neighborhoods facing elevated risk. Darker tracts indicate higher vulnerability based on environmental and social factors.</p>
</div>
</section>

<section class="slide" data-step="2" data-layer="pools" data-center="39.94,-75.16" data-zoom="12.2">
<div class="content">
<h2>Swimming pools</h2>
<p>Public pools provide seasonal relief on extreme heat days.</p>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I would love to see more information in this slide content (and all the slides). There's so much more that can be (and has been) said about heat relief measures.

Ultimately this is a question of user need. Who are the intended users for this map? If it's residents, then perhaps explaining pool access schedules, and maybe things they should keep in mind when looking to access pools might be good to include. If it's a general educational tool (e.g. journalistic), then perhaps some of the history of pool access, closures, etc.

</div>
</section>

<section class="slide" data-step="3" data-layer="sites" data-center="39.99,-75.16" data-zoom="12.2">
<div class="content">
<h2>Recreation & program sites</h2>
<p>PPR program sites (recreation centers, older adult centers, environmental education sites) may serve as cooling resources during emergencies.</p>
</div>
</section>

</div>
<nav class="dots" id="dots" aria-label="Slide navigation"></nav>

<!-- Sources modal -->
<dialog id="sources">
<h3>Data Sources</h3>
<ul>
<li><strong>Heat Vulnerability by Census Tract</strong> — City of Philadelphia / OpenDataPhilly (GeoJSON). Accessed September 23, 2025.</li>
<li><strong>Parks & Recreation Program Sites</strong> — City of Philadelphia / OpenDataPhilly (GeoJSON). Accessed September 23, 2025.</li>
<li><strong>Parks & Recreation Swimming Pools</strong> — City of Philadelphia / OpenDataPhilly (GeoJSON). Accessed September 23, 2025.</li>
<li><strong>Philadelphia Neighborhoods</strong> — City of Philadelphia / OpenDataPhilly (GeoJSON). Accessed September 23, 2025.</li>
</ul>
<form method="dialog">
<button class="btn">Close</button>
</form>
</dialog>

<button id="prevBtn" class="navbtn" aria-label="Previous slide">‹</button>
<button id="nextBtn" class="navbtn" aria-label="Next slide">›</button>

<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script src="main.js"></script>
</body>
</html>
171 changes: 171 additions & 0 deletions main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Blueprint Story Map - v2

// --- Map setup ---
const map = L.map('map', {
zoomControl: false, // we'll add our own in bottom-right
preferCanvas: true,
scrollWheelZoom: false, // keep scroll for slide navigation
inertia: true
}).setView([39.9526, -75.1652], 11.5);

// CARTO light tiles => crisp linework; colorized via CSS overlay (see style.css)
const tiles = L.tileLayer(
'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
{
maxZoom: 19,
attribution: '&copy; OpenStreetMap contributors &copy; CARTO'
}
).addTo(map);

// Custom-positioned zoom control (bottom-right)
L.control.zoom({ position: 'bottomright' }).addTo(map);

// Scale
L.control.scale({ metric: false, position: 'bottomright' }).addTo(map);

// --- Performance-friendly renderers ---
const canvasRenderer = L.canvas({ padding: 0.5 });

// --- Utilities ---
function firstProp(obj, keys) {
for (const k of keys) {
if (obj[k] !== undefined && obj[k] !== null && obj[k] !== '') return obj[k];
}
return undefined;
}
function numberOr(obj, keys, fallback = 0) {
const v = firstProp(obj, keys);
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
}

// --- Layers ---
const layers = {
hvi: L.geoJSON(null, {
renderer: canvasRenderer,
smoothFactor: 0.5,
interactive: false,
style: f => {
const s = numberOr(f.properties, ['hvi_score','HVI_SCORE','HVI','hvi','score','SCORE'], 0);
// blueprint-friendly fill grades
let stroke = '#bfe8ff', fill = '#113a79';
if (s >= 4) fill = '#0e2c5b';
else if (s >= 3) fill = '#103468';
else if (s >= 2) fill = '#123e7b';
else fill = '#14508f';
return { color: stroke, weight: 0.45, fillColor: fill, fillOpacity: 0.55 };
}
}),

hoods: L.geoJSON(null, {
renderer: canvasRenderer,
interactive: false,
style: () => ({ color: '#ffffff', weight: 0.6, opacity: .7, fillOpacity: 0 })
}),

pools: L.geoJSON(null, {
renderer: canvasRenderer,
pointToLayer: (f, latlng) =>
L.circleMarker(latlng, {
radius: 6, color: '#fff', weight: 1,
fillColor: '#7cc6ff', fillOpacity: 0.95
}).bindPopup(`<strong>${firstProp(f.properties, ['NAME','POOL_NAME','SITE_NAME','name']) ?? 'Pool'}</strong><br>${firstProp(f.properties, ['ADDRESS','ADDRESS1','address']) ?? ''}`)
}),

sites: L.geoJSON(null, {
renderer: canvasRenderer,
pointToLayer: (f, latlng) =>
L.circleMarker(latlng, {
radius: 6, color: '#fff', weight: 1,
fillColor: '#2dd4bf', fillOpacity: 0.95
}).bindPopup(`<strong>${firstProp(f.properties, ['SITE_NAME','NAME','name']) ?? 'PPR Site'}</strong><br>${firstProp(f.properties, ['ADDRESS','ADDRESS1','address']) ?? ''}`)
})
};

// --- Data load ---
Promise.all([
fetch('data/heat_vulnerability_ct.geojson').then(r => r.json()),
fetch('data/philadelphia_neighborhoods.geojson').then(r => r.json()),
fetch('data/ppr_swimming_pools.geojson').then(r => r.json()),
fetch('data/ppr_program_sites.geojson').then(r => r.json())
]).then(([hvi, hoods, pools, sites]) => {
layers.hvi.addData(hvi);
layers.hoods.addData(hoods);
layers.pools.addData(pools);
layers.sites.addData(sites);
}).catch(err => console.error('Data load error:', err));

// --- Slide logic ---
const slides = [...document.querySelectorAll('.slide')];
const dots = document.getElementById('dots');
let idx = 0;

// Dot nav
slides.forEach((_, i) => {
const b = document.createElement('button');
b.setAttribute('aria-label', `Go to slide ${i+1}`);
b.addEventListener('click', () => goTo(i));
dots.appendChild(b);
});
function updateDots() {
[...dots.children].forEach((d,i) => d.classList.toggle('active', i === idx));
}
updateDots();

function activateLayers(stepEl){
const layerKey = stepEl.dataset.layer;
const fit = (stepEl.dataset.fit || '').split(',').filter(Boolean);
const center = (stepEl.dataset.center || '').split(',').map(Number);
const zoom = Number(stepEl.dataset.zoom || map.getZoom());

Object.values(layers).forEach(l => map.removeLayer(l));
if (layerKey && layers[layerKey]) layers[layerKey].addTo(map);
if (fit.length) {
const group = L.featureGroup(fit.map(k => layers[k]).filter(Boolean));
if (group.getLayers().length) map.fitBounds(group.getBounds().pad(0.15), { animate: true, duration: 1.0 });
} else if (center.length === 2 && !Number.isNaN(center[0])) {
map.flyTo(center, zoom, { animate: true, duration: 1.0 });
}
}

function goTo(i){
idx = Math.max(0, Math.min(slides.length - 1, i));
slides.forEach((s, j) => s.classList.toggle('active', j === idx));
slides[idx].scrollIntoView({ behavior: 'smooth', block: 'start' });
activateLayers(slides[idx]);
updateDots();
}

// IntersectionObserver for visible slide
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
const newIdx = slides.indexOf(e.target);
if (newIdx !== idx) {
idx = newIdx;
slides.forEach((s, j) => s.classList.toggle('active', j === idx));
activateLayers(slides[idx]);
updateDots();
}
}
});
}, {threshold: 0.6});
Comment on lines +140 to +152
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Interesting (and correct) use of InteractionObserver. Out of curiosity, where did you find this pattern?

slides.forEach(s => io.observe(s));

// Prev/Next + keyboard
document.getElementById('prevBtn').addEventListener('click', () => goTo(idx-1));
document.getElementById('nextBtn').addEventListener('click', () => goTo(idx+1));
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'PageDown' || e.key === ' ') goTo(idx+1);
if (e.key === 'ArrowUp' || e.key === 'PageUp') goTo(idx-1);
});

// Sources modal
const modal = document.getElementById('sources');
const sourcesBtn = document.getElementById('sourcesBtn');
if (sourcesBtn) {
sourcesBtn.addEventListener('click', () => modal.showModal());
}

// Initial activation
activateLayers(slides[0]);
Loading