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
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,49 @@
Add a readme for your story map here. Include content overview, data citations, and any relevant technical details.
# When blood takes a shortcut

An interactive story map by **Frederick Abalo**, developed as an independent JavaScript course project at the University of Pennsylvania.

[Read the story](https://freddie999.github.io/story-map-project/) · [Portfolio](https://freddie999.github.io/portfolio/)

## The question

How does geography shape the journey of urgently needed medical supplies in Rwanda? The story follows road and aerial connections, then compares modeled outbound travel times across a screened set of delivery-site records. Published maternal-health research provides context, separately from the modeled route comparison.

My target audience is health planners, medical-logistics practitioners, and readers interested in how geography affects access to essential supplies.

## Implementation

- JavaScript modules and Leaflet, with a persistent map and scroll-driven scenes.
- Precomputed GeoJSON layers, route reveals, consistent time legends, and interactive point details.
- HTML narrative and a [companion travel-time table](accessibility-table.html).
- Reduced-motion handling in the map transitions.

On phones, the map stays above the scrolling story. A skip link leads to the narrative, and covered desktop map controls stay out of the keyboard navigation order. Routes draw outward from Muhanga as each scene becomes active. Roads appear first in the six-facility scene, followed by flights. These animations illustrate the paths; their speed does not represent travel time. Readers who prefer reduced motion see complete, static routes.

Scenes load individually and are cached as you read. If a scene fails, a message and retry button appear while the story and travel-time table remain available.

## Read locally

Serve the repository root over HTTP so the scene data can load:

```sh
uv run --no-project python -m http.server 8000
```

Open http://localhost:8000. An internet connection is required for the basemap and externally hosted assets. The Mapbox basemap uses a browser-visible public token; deployments should use a token restricted to their own allowed URLs. No private credential is required for the narrative.

## Interpretation

Travel times are modeled outbound transport estimates, not observed complete deliveries. They exclude preparation, waiting, unloading, return travel, traffic, and weather. Name-screened delivery-site records are not a verified register of blood recipients or a national facility census. Route comparisons do not estimate lives saved.

Flight minutes are calculated as distance in meters ÷ 28.7 meters per second ÷ 60, assuming a constant speed of about 103 km/h. The network overview uses mean outbound distances across route options. The six examples use the supplied compiled three-dimensional distance of each selected planned route. Map lines are simplified for display; that geometry is not used to recalculate the published times or distances.

This project relates to my prior work at Zipline and is an independent academic analysis. Public availability of the existing map does not establish redistribution rights for underlying source datasets; those sources are not bundled by this portfolio update.

## Sources and credit

- [Knowledge at Wharton research summary](https://knowledge.wharton.upenn.edu/article/how-rwanda-is-using-drones-to-improve-health-care/) — health-outcome context.
- OpenStreetMap contributors and OSRM — modeled road-network context; source details appear in the story.
- Mapbox — basemap and boundary metadata.
- Course-provided scrolling-map structure — adapted for this authored narrative.

The story contains additional citations and comparison notes alongside the relevant scenes.
860 changes: 860 additions & 0 deletions accessibility-table.html

Large diffs are not rendered by default.

991 changes: 991 additions & 0 deletions analysis/accessibility/facility-accessibility.csv

Large diffs are not rendered by default.

325 changes: 325 additions & 0 deletions css/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
body {
margin: 0;
border-width: 0;
box-sizing: border-box;
height: 100vh;

font-family: 'Open Sans', sans-serif;
}

[hidden] {
display: none !important;
}

button {
min-height: 44px;
padding: 8px 12px;
border: 1px solid #155e75;
border-radius: 4px;
background: #fff;
color: #123e4a;
font: inherit;
cursor: pointer;
}

button:disabled {
color: #505050;
border-color: #777;
cursor: default;
}

.skip-link {
position: fixed;
top: 8px;
left: 8px;
z-index: 1000;
padding: 12px;
background: #fff;
transform: translateY(-150%);
}

.skip-link:focus {
transform: translateY(0);
}

.map-covered {
visibility: hidden;
}

.map-status {
position: fixed;
top: 12px;
left: 60px;
z-index: 200;
max-width: min(24rem, calc(100% - 90px));
padding: 10px;
border-radius: 4px;
background: #fff;
color: #111;
}

.map-status p {
margin: 0 0 8px;
}

.animation-note {
font-size: 0.85rem;
}

.map-container {
position: sticky;
top: 0;
height: 100vh;
z-index: 0;
}

.map {
height: 100%;
}

.slide-section {
position: relative;
margin-top: -100vh;
z-index: 100;
pointer-events: none;
border-bottom: 1px solid rgb(0 0 0 / 10%);
}


.slide {
margin-top: 90vh;
margin-bottom: 90vh;
margin-left: auto;
margin-right: 1rem;
padding: 1rem;
box-shadow: -0.5rem 0 1rem rgb(0 0 0 / 50%);
border: 1px solid gray;
border-radius: 0.5rem;
pointer-events: initial;

box-sizing: border-box;
width: calc(100% - 2rem);
max-width: 25rem;
line-height: 1.6;
overflow-wrap: anywhere;

background-color: rgb(243 238 182);
color: black;
}

.slide:first-child {
margin-top: 0;
}

.slide:last-child {
margin-bottom: 40vh;
}

.slide-controls {
display: flex;
justify-content: space-around;
align-items: center;
column-gap: 0.5rem;

width: 100%;
}

.slide-nav-button {
transition: all 0.1s;

padding: 0;
margin: 1rem;
width: 4rem;
height: 3rem;
border: 1px solid #888;
border-radius: 0.25rem;

background-color: #eee;
font-size: 1.5em;
}

.slide-nav-button:hover {
background-color: #444;
color: #fff;
}

.slide-nav-select {
min-width: 0;
}

.comparison-table {
width: 100%;
border-collapse: collapse;
font-variant-numeric: tabular-nums;
}

.comparison-table caption {
margin-bottom: 0.75rem;
text-align: left;
font-weight: 600;
}

.comparison-table th,
.comparison-table td {
padding: 0.5rem 0.25rem;
border-bottom: 1px solid #767676;
text-align: right;
}

.accessibility-page th,
.accessibility-page td {
overflow-wrap: anywhere;
}

.comparison-table th:first-child {
text-align: left;
}

/* A shared legend for the road and drone overview cards. */
.time-legend {
list-style: none;
padding: 0;
}

.time-legend li {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0.25rem 0;
}

.time-dot {
display: inline-block;
flex-shrink: 0;
border: 1px solid #333;
border-radius: 50%;
}

.time-30 {
width: 8px;
height: 8px;
background: #eff3ff;
}

.time-60 {
width: 10px;
height: 10px;
background: #bdd7e7;
}

.time-120 {
width: 12px;
height: 12px;
background: #6baed6;
}

.time-long {
width: 14px;
height: 14px;
background: #2171b5;
}

summary {
cursor: pointer;
}

a:focus-visible,
summary:focus-visible,
button:focus-visible,
[tabindex]:focus-visible {
outline: 3px solid #155e75;
outline-offset: 3px;
}

/* Plain text alternative to the map, on accessibility-table.html. */
.accessibility-page {
max-width: 55rem;
margin: 0 auto;
padding: 1rem;
line-height: 1.6;
}

.accessibility-page table {
font-size: 0.9rem;
}


/* Matching full-width opening and closing slides. */
#title-slide,
#conclusion {
width: 100%;
max-width: none;
min-height: 100vh;
min-height: 100svh;
margin: 0;
padding: 4rem 1.5rem;

display: flex;
align-items: center;
justify-content: center;

border: none;
border-radius: 0;
box-shadow: none;
}

/* Leave room to see Rwanda before the closing panel covers the map. */
#conclusion {
margin-top: 90vh;
}

/* Keep long paragraphs from stretching across the screen. */
.intro-content {
width: 100%;
max-width: 46rem;
}

.intro-content h1,
.intro-content h2 {
font-size: clamp(2rem, 5vw, 3.5rem);
line-height: 1.15;
}

/* Keep the map above the text on phones rather than behind an opaque card. */
@media (width <= 760px) {
.map-container {
height: 40vh;
height: 40svh;
z-index: 150;
background: #262626;
border-bottom: 2px solid #555;
}

.slide-section {
margin-top: 0;
}

.slide,
#title-slide,
#conclusion {
width: calc(100% - 2rem);
min-height: 0;
margin: 2rem 1rem 40vh;
padding: 1rem;
border-radius: 0.5rem;
scroll-margin-top: calc(40svh + 16px);
}

.slide :is(a, summary, button, h1, h2) {
scroll-margin-top: calc(40svh + 16px);
scroll-margin-bottom: 16px;
}

#title-slide {
margin-top: 1rem;
}

#conclusion {
margin-bottom: 2rem;
}

.map .leaflet-tooltip {
font-size: 11px;
padding: 3px 5px;
}
}
Loading