Skip to content

Commit 951f1b0

Browse files
ryan-williamsclaude
andcommitted
GeoSankey: persistent z-bump on hover/pin + flow-map screenshots
- Track a per-component \`bumpOrder\` of flow keys: every hover or pin appends (or moves-to-end) the active key. Render order sorts bumped flows last (most-recent on top), then falls back to the existing widest-first sort. Lincoln Bus no longer hides under Amtrak as soon as it's been engaged. - README: add Flow Map section + table row in Visualizations. - \`scrns.config.ts\`: add \`map-nj-ny\` / \`map-ny-nj\` shots (selector \`.geo-sankey\`, scrollTo same, \`headless: false\` because maplibre-gl needs WebGL). - Generated initial \`map-nj-ny.png\` / \`map-ny-nj.png\`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 13b6ac8 commit 951f1b0

5 files changed

Lines changed: 64 additions & 5 deletions

File tree

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ Reports are also mirrored in [Hudson County Complete Streets]' [Google Drive][gd
1414

1515
Each GIF cycles through peak 1hr, peak period (3hr), and 24hr views. Click to open the interactive chart.
1616

17+
## Flow Map
18+
19+
| NJ→NY (entering) | NY→NJ (leaving) |
20+
|:-:|:-:|
21+
| [![NJ→NY map][map-nj-ny]][map-nj-ny-live] | [![NY→NJ map][map-ny-nj]][map-ny-nj-live] |
22+
23+
Geographic Sankey of CBD crossings and ferry routes. Ribbon widths are proportional to passenger counts; flows merge/split at the ferry interchanges. Hovering or pinning a flow brings it to the top of the z-stack persistently.
24+
1725
| | NJ→NY | NY→NJ |
1826
|---|:-:|:-:|
1927
| Peak 1hr | [![][nj-ny-1h]][nj-ny-1h-live] | [![][ny-nj-1h]][ny-nj-1h-live] |
@@ -39,17 +47,18 @@ See [`specs/extraction.md`] for full schema documentation and extraction notes.
3947

4048
## Visualizations
4149

42-
The [web app][live] has 5 interactive charts built with [Plotly.js] and [pltly]:
50+
The [web app][live] has 6 interactive views — the Plotly charts use [Plotly.js] / [pltly]; the flow map uses [MapLibre GL] + [geo-sankey]:
4351

4452
| Chart | Description | Controls |
4553
|-------|-------------|----------|
4654
| **Unified Chart** | NJ→NY crossings: bubble scatter, grouped bars, stacked %, recovery index | View, direction, time period, granularity, palette |
55+
| **Flow Map** | Geographic Sankey ribbons over a basemap of NJ↔Manhattan, width-proportional to passengers per crossing/mode (incl. multi-source ferry network) | Direction, time period, year, inline-legend, geo-scale, width-scale, hover-padding |
4756
| **Hourly Profile** | Stacked area of CBD entries/exits by hour | Mode vs sector, direction, year |
4857
| **Mode Share** | Stacked bar of daily persons by mode across years | Absolute vs %, direction |
4958
| **Sector Vehicles** | Motor vehicles by sector (stacked or grouped bars) | View, direction, time period |
5059
| **Peak Accumulation** | 50-year timeline of max persons in Manhattan Hub | (annotated, no toggles) |
5160

52-
All charts support legend hover highlight and click-to-solo via [pltly].
61+
Plotly charts support legend hover highlight and click-to-solo via [pltly]; the flow map highlights on hover and pins on click, with the most recently engaged flow drawn on top.
5362

5463
## Extraction
5564

@@ -79,6 +88,8 @@ Data sourced from publicly available [NYMTC Hub Bound Travel reports][NYMTC HBT]
7988
[live]: https://cbd.hudcostreets.org
8089
[Plotly.js]: https://plotly.com/javascript/
8190
[pltly]: https://github.com/runsascoded/pltly
91+
[MapLibre GL]: https://maplibre.org/
92+
[geo-sankey]: https://github.com/runsascoded/geo-sankey
8293
[`specs/extraction.md`]: specs/done/extraction.md
8394
[nj-ny-gif]: www/public/screenshots/nj-ny.gif
8495
[ny-nj-gif]: www/public/screenshots/ny-nj.gif
@@ -96,3 +107,7 @@ Data sourced from publicly available [NYMTC Hub Bound Travel reports][NYMTC HBT]
96107
[ny-nj-1h-live]: https://cbd.hudcostreets.org?d=nynj
97108
[ny-nj-3h-live]: https://cbd.hudcostreets.org?d=nynj&t=3h
98109
[ny-nj-1d-live]: https://cbd.hudcostreets.org?d=nynj&t=1d
110+
[map-nj-ny]: www/public/screenshots/map-nj-ny.png
111+
[map-ny-nj]: www/public/screenshots/map-ny-nj.png
112+
[map-nj-ny-live]: https://cbd.hudcostreets.org/#map
113+
[map-ny-nj-live]: https://cbd.hudcostreets.org/?d=nynj#map
465 KB
Loading
476 KB
Loading

www/scrns.config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,28 @@ for (const dir of dirs) {
4242
}
4343
}
4444

45+
// Flow map — geographic Sankey of crossings + ferries. The map needs more
46+
// settling time than Plotly: maplibre-gl loads vector tiles asynchronously,
47+
// then we render the ribbon polygons on top. The flow map sits below the
48+
// bubble chart, so scroll to it before capturing. Inline-legend is on by
49+
// default so the labels appear at the arrow tips. `headless: false` is
50+
// required because maplibre-gl uses WebGL, which needs a real GPU context —
51+
// in headless Chromium the canvas renders blank.
52+
function mapShot(params: string): ScreenshotConfig {
53+
return {
54+
query: params ? `?${params}` : '',
55+
width: W,
56+
height: 900,
57+
selector: '.geo-sankey',
58+
scrollTo: '.geo-sankey',
59+
preScreenshotSleep: 3000,
60+
headless: false,
61+
}
62+
}
63+
for (const dir of dirs) {
64+
screenshots[`map-${dir.key}`] = mapShot(dir.params)
65+
}
66+
4567
// og:image — canonical bubble chart, clean mode (no toggles/SpeedDial, larger text)
4668
// Standard og:image is 1200x630
4769
screenshots['og'] = {

www/src/components/GeoSankey.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,15 @@ function GeoSankeyInner({ data }: Props) {
463463
const [hoveredKey, setHoveredKey] = useState<string | null>(null)
464464
const [pinnedKey, setPinnedKey] = useState<string | null>(null)
465465
const activeKey = pinnedKey ?? hoveredKey
466+
// Persistent z-bump: every hover (or pin) appends the flow's key to
467+
// `bumpOrder`; later bumps move it to the end. Render order sorts by
468+
// `bumpOrder` index after the default widest-first sort, so the most
469+
// recently engaged flow always sits on top.
470+
const [bumpOrder, setBumpOrder] = useState<string[]>([])
471+
useEffect(() => {
472+
if (!activeKey) return
473+
setBumpOrder(prev => prev[prev.length - 1] === activeKey ? prev : [...prev.filter(k => k !== activeKey), activeKey])
474+
}, [activeKey])
466475
// Edit mode is implicit: the ferry editor engages whenever the ferry flow
467476
// is pinned (click ferry ribbon on the map, or the "Ferries" legend row).
468477
// Click anywhere outside to exit.
@@ -589,10 +598,23 @@ function GeoSankeyInner({ data }: Props) {
589598
})
590599
}
591600

592-
// Sort widest first so narrower flows draw on top
593-
features.sort((a, b) => (b.properties?.width ?? 0) - (a.properties?.width ?? 0))
601+
// Sort: bumped flows last (in `bumpOrder` recency order) so the most
602+
// recently hovered/pinned flow draws on top; remaining flows fall back to
603+
// widest-first so smaller flows draw on top of larger neighbors.
604+
features.sort((a, b) => {
605+
const ka = a.properties?.key as string | undefined
606+
const kb = b.properties?.key as string | undefined
607+
const ai = ka ? bumpOrder.indexOf(ka) : -1
608+
const bi = kb ? bumpOrder.indexOf(kb) : -1
609+
if (ai !== bi) {
610+
if (ai === -1) return -1
611+
if (bi === -1) return 1
612+
return ai - bi
613+
}
614+
return (b.properties?.width ?? 0) - (a.properties?.width ?? 0)
615+
})
594616
return { type: 'FeatureCollection' as const, features }
595-
}, [flows, direction, maxPassengers, mapView.zoom, geoScale, widthScale, ferryGS.graph])
617+
}, [flows, direction, maxPassengers, mapView.zoom, geoScale, widthScale, ferryGS.graph, bumpOrder])
596618

597619

598620

0 commit comments

Comments
 (0)