MIP-0009: A richer map — a wave marker per beach and, on hover, every aspect at that point¶
| Status | Implemented — all four stacked PRs merged: #144 (wind-level), #145 (site-check-harness), #146 (wave-marker-tooltip), #147 (card-aspect-row); tasks and v1 decisions: MIP-0009.tasks.md |
| Author | Claude Fable 5.1, for M. Hoffmann (request of 5 Sep 2026: "for now it just displays dots… hovering the dot should display each aspect: wind level with emoji, whale probability, jellyfish probability, water temperature — a wave icon, not a heat map") |
| Created | 2026-09-05 |
| Tasks | docs/MIPs/MIP-0009.tasks.md — four stacked PRs, one per task; note its decision #1: the §5 test harness (scripts/site_check.js) did not exist on 2026-09-06 and is task 2 |
| Phase | 1 — the map exists (MIP-0005) and this only changes what it draws; no earlier-phase prerequisite is missing for it |
| Related | MIP-0005 (the map and its board JSON), MIP-0008 task 6 (the footer panel; same app.js), FUTURE-WORK.md §1 (other activities as layers on the same map). See also MIP-0046 (Draft) — it replaces this MIP's emoji map (§5) and the 〰️-vs-WAVE_PATH split with one inline-SVG chart-mark set, and closes §8's "emoji fonts differ per platform — hence the words" caveat; the words, numbers and score colours are unchanged. See also MIP-0047 (Draft) — equation-drawn illustration motifs sharing the same inline-SVG sprite; its §5.5 deliberately leaves this MIP's WAVE_PATH marker and legend key untouched, because a hand-tuned filled path beats a sampled curve at 24 px |
| Effort | S — no new data; one pure Scala function (windLevel) plus a JS/CSS-only marker and tooltip change |
| Gain | user value (answers "where's it calm now" without 40 clicks) |
| Effort vs Gain | cheap win — small, self-contained, no dependency on any other Draft MIP |
| Depends on | MIP-0005 (the map and board this changes); no phase or cloud gate |
| Risk | fixed-pixel wave divIcons can overlap at 300m beach spacing (Ingleses/Santinho) — a legibility risk, not a data one |
| Cost so far | ~$3.18 shared bucket with MIP-0008's wrap-up (commit 44b848c, "not split further" per its own Cost: line) |
1. Summary¶
Every beach on the map becomes a wave marker coloured by its score instead of a plain dot, and hovering it (tapping, on a phone) shows every aspect marola already knows for that beach at the selected hour: wind level with an emoji, whale-sighting likelihood, jellyfish risk, water temperature, waves, water-quality verdict: the numbers the CLI prints, without opening the card. No new data: every value is in the board JSON today; the one addition is a deterministic wind band computed in Scala so the page never re-implements a scoring threshold.
2. Motivation¶
Today app.js draws L.circleMarker per beach and its tooltip says only Praia da Joaquina ·
55 at 10:00 (site/static/app.js, render()); wind, whales, jellyfish and water temperature are
all one click away in the card. On a map with 40 beaches the question "where is it calm and warm
right now?" means 40 clicks. The board already carries, per beach and per daylight hour,
wind_kmh, sea_temp_c, wave_m, jellyfish, whales and score (checked 2026-09-05 against
site/dist/data/floripa/2026-09-06.json, appendix A), but the page does not show them.
The marker itself is a dot because MIP-0005 §5.3 chose the cheapest thing that works; a wave glyph is what the product is about and, unlike an emoji marker, can still carry the score colour.
3. User-visible change¶
Hover (desktop) or tap (touch) on a beach:
🌊 Praia da Joaquina · 55/100 at 10:00
🌬️ breezy, 27 km/h S 🌡️ water 19.0 °C
〰️ waves 1.3 m every 6 s 🪼 jellyfish Low
🐋 whales Low (best 07:00) 💧 PRÓPRIA (1/1 pts, 25 Aug)
- The marker is a wave shape filled with the score colour (green ≥ 70, amber ≥ 40, orange ≥ 1, red = 0 or unfit water, grey = dark/no data, the same scale as today's legend). The selected beach's wave is larger; the hour slider changes both the colour and the tooltip, as it changes the dots today.
- The tooltip follows the mouse (Leaflet
sticky), one at a time, and closes on leave. On touch there is no hover: a tap opens the card as today, and the same aspect row is the first block of the card, so both inputs see the same thing. - Every emoji is followed by its word ("🪼 jellyfish Low"), so a platform without the glyph still
reads correctly; the legend gains a wave key, the same path the markers draw, at 18 px in
--ink, labelled "hover a wave". It is hidden under 640 px: a phone has no hover to offer and the hour bar has no room for the line (it clipped at 390 px). - The card, the list, the day picker and the footer panel (MIP-0008) are unchanged.
4. Data sources and dependencies reviewed¶
No new data source. Everything shown comes from the board JSON MIP-0005 already publishes:
| Aspect | Field (per beach / per hour) | Verified 2026-09-05 |
|---|---|---|
| Score, hour | hours[].score, hours[].h, best.* |
real board, appendix A |
| Wind | hours[].wind_kmh (per hour), sea.wind_dir_deg (best hour) |
real board |
| Water temperature | hours[].sea_temp_c |
real board |
| Waves | hours[].wave_m, sea.period_s, sea.swell_m |
real board |
| Jellyfish | hours[].jellyfish — Low/Moderate/High from Swimability |
real board, enum from core/scoring |
| Whales | hours[].whales (per hour), whales.peak, whales.season |
real board |
| Water quality | water.summary, water.unfit |
real board |
Wind band. The CLI's notes say "breezy (27km/h)" / "strong wind (…)" from
Swimability.windDelta (CalmWindKmh, StrongWindKmh, core/scoring/Swimability.scala:92),
which is private. The band shown on the map must be the same one, so it is computed in Scala
and written to the board as wind_level (§5), not re-derived in JavaScript from a copied number.
Leaflet 1.9.4 (vendored, site/static/vendor/leaflet.js): L.divIcon (HTML/SVG markers)
and tooltips with sticky/permanent/direction are present (confirmed by grepping the vendored
file on 2026-09-05, not from docs). No new library, no build step (MIP-0005's constraint holds).
Emoji. 🌊 U+1F30A, 🌬️ U+1F32C, 🌡️ U+1F321, 🐋 U+1F40B, 💧 U+1F4A7, 🍃 U+1F343, 💨 U+1F4A8 are Emoji 1.0-era; 🪼 jellyfish is U+1FABC, Emoji 14.0 (2021): older Android/Windows fonts show a box, which is why every emoji is followed by its word. Rendered by the system font (Noto Color Emoji, Apple Color Emoji, Segoe UI Emoji); nothing is downloaded. Not checked: how the wave SVG looks on a Retina display at 26 px; the manual check in §7.
5. Design¶
Scala (deterministic, tested).
Swimability.windLevel(kmh: Option[Double]): WindLevel:enum WindLevel { Calm, Breezy, Strong }(plusNonewhen the forecast lacks wind), using the sameCalmWindKmh/StrongWindKmhconstantswindDeltauses;windDeltacalls it, so there is one threshold.Board: eachhours[]entry gains"wind_level": "calm" | "breezy" | "strong" | null. Additive, optional → schema stays 1 (site/board.schema.jsonlists it as optional; the page tolerates its absence: an old board still renders, without the band).
JavaScript (site/static/app.js, plain, no framework).
waveIcon(colour, selected)→L.divIcon({ html: '<svg viewBox="0 0 24 24">…wave path…</svg>', className: 'wave', iconSize: selected ? [32, 32] : [24, 24], iconAnchor: centre }). One inline SVG path (own artwork, MIT with the repo),fill= the score colour, a white stroke for contrast on tiles.render()usesL.marker([lat, lon], { icon })instead ofL.circleMarker.aspectsHtml(beach, shown)builds the six-cell grid from theshown(beach)hour entry (the slider's hour, or the best hour) plus the best-hourseablock; used by the tooltip (bindTooltip(html, { sticky: true, direction: 'top', className: 'aspects', opacity: 0.97 })) and prepended torenderCard(). Emoji map: windcalm 🍃 / breezy 🌬️ / strong 💨, whales🐋, jellyfish🪼, water temperature🌡️, waves〰️, water quality💧(red when unfit).style.css:.wave svg { … },.leaflet-tooltip.aspects { grid, 2 columns, 0.85rem }, the selected-wave size, the legend line.scripts/site_check.js(Node, stdlib): the stub-DOM harness written for MIP-0008 task 6, checked in. It loadsapp.jsagainstsite/fixtures/(a two-beach board committed as a fixture, validated againstboard.schema.jsonby the harness) and asserts: one wave marker per beach, the tooltip HTML holds the six aspects with the fixture's numbers, the card starts with the same row, a board withoutwind_levelstill renders. Run byjust quality(Node is in the flake) and by ci.yml's quality job whensite/**changes.
What goes through the LLM: nothing. Every string on the tooltip is a number or an enum from
scoring/; the words are fixed labels.
6. Scoring / safety impact¶
None to Swimability.score or the notes. windLevel is a pure function over the existing
thresholds, and windDelta is refactored to use it; SwimabilitySpec gains three cases (below
calm, between, at/above strong) proving the band and the delta agree. Unfit water stays red on
the marker and says why in the tooltip (water.summary), exactly as the card does.
7. Verification plan¶
SwimabilitySpec:windLevelbands and their agreement withwindDelta's note text.BoardSpec: every hour entry carrieswind_level, consistent with itswind_kmh; the schema validator accepts it and accepts a board without it.scripts/site_check.jsinjust quality(assertions above);node --check app.js.- Manual:
just site-build floripa && just site-serve: hover Joaquina on a desktop, tap it on a phone (Safari + Chrome), zoom out to the whole area and check the waves stay legible over the OSM tiles; screenshot into the PR. - "Done": the live map after the merge shows waves and hover aspects; the footer panel and the list still work.
8. Risks, limitations, and honest caveats¶
- Hover does not exist on touch. The card's aspect row is the touch equivalent; no long-press gesture is invented (it fights the map's pan).
- Emoji fonts differ per platform and 🪼 is missing on older ones, hence the words.
- Clutter: tooltips are one at a time and only on hover, so 40 beaches stay readable; the
waves are fixed-pixel
divIcons and do not scale with zoom; at the area zoom they may overlap where beaches are 300 m apart (Ingleses/Santinho); the selected one is drawn on top. - Colour is not the only signal: the score number is in the tooltip and the card; unfit water says "IMPRÓPRIA" in words.
- The wind band shown is the forecast wind at that hour; the tooltip says the hour.
9. Alternatives considered¶
- An emoji as the marker (🌊 in a
divIcon): cannot take the score colour, renders differently everywhere, and overlaps badly. Rejected; emoji only in the text. - A heat/density layer (leaflet.heat): explicitly not wanted; it also hides the per-beach truth behind a blur. Rejected.
- Permanent labels on every marker: unreadable at area zoom. Rejected in favour of hover.
- Popups on hover: Leaflet popups close on mouse-out and steal the map's focus; tooltips are the hover primitive. Rejected.
- MapLibre with data-driven symbols: richer, but a build step and a bigger vendor blob for one tooltip. Rejected while MIP-0005's "plain files, no build" holds.
- Do nothing: the card works, but the map answers "where?" only after a click per beach.
11. Open questions¶
- Wind bands: keep exactly
Swimability's two thresholds (calm/breezy/strong), or add a fourth "gale" band for thestrongtail? Proposal: the two thresholds, so the map and the CLI agree. - Whales per hour (
hours[].whales) or the day's peak (whales.peak) on the tooltip? Proposal: the hour's value plus "best HH:MM" when it differs. - Should the same aspect row become the first line of the Telegram reply (MIP-0002)? It is the
compact form of
Report.line. Out of scope here; noted for MIP-0002. - Wave glyph: one shape for all, or a rougher wave for
strong wind/wave_m ≥ 1.5? Proposal: one shape in v1: the colour and the text already say it.
Appendix¶
A. One beach from the live board, 2026-09-05 (site/dist/data/floripa/2026-09-06.json):
jellyfish: "Moderate", whales: {now: "High", peak: "07:00", season: true}, sea: {temp_c:
19.6, wave_m: 0.58, period_s: 5.3, wind_kmh: 16.3, wind_dir_deg: 187, uv: 0.1, …}, hours[0]:
{h: "07:00", score: 60, wind_kmh: 16.3, sea_temp_c: 19.6, wave_m: 0.58, jellyfish: "Moderate",
whales: "High", notes: ["breezy (16km/h)", "cold water (19.6°C)", "some jellyfish likelihood"]}.
B. Leaflet checks (grep of the vendored 1.9.4 file, 2026-09-05): divIcon present,
bindTooltip present, tooltip options sticky and permanent present.
C. Emoji versions: 🪼 U+1FABC Emoji 14.0 (2021); all others ≤ Emoji 5.0.