-
Notifications
You must be signed in to change notification settings - Fork 39
Annie Kong's Story Map Project #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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). |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| 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> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
| 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: '© OpenStreetMap contributors © 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Interesting (and correct) use of |
||
| 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]); | ||
There was a problem hiding this comment.
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)