Draw the pin as one circle with a white stroke, not three colour bands
Some checks are pending
build-gate / build (push) Waiting to run

The pins read as eyes. The cause is structural: two stacked circle layers
produced, outward from the centre at z1, navy disc r=1.6, white field r=3.4,
navy stroke 0.6px. Dark centre, light middle, dark rim -- pupil, sclera, iris
outline. A comment in the file already called it "a navy/white/navy sandwich"
without noticing what a sandwich of those bands draws.

Concentric rings sharing a centre are the pattern the visual system fuses into
one object, which is why a bullseye is named after an eye. The inner disc sat
at 47% of the halo radius at z1, the proportion that reads as a pupil rather
than a dot with a border, and worst when zoomed out -- where a world map lives.

The outer navy band is the specific culprit: it closes the iris. Removing it
leaves the same colours reading as an ordinary pin, so this deletes a band
rather than changing a colour. Both Driver rulings survive: blue dots, white
rings, one fixed colour at full opacity.

One layer with a stroke is also the documented Mapbox idiom --
circle-stroke-width draws outside circle-radius, so a single layer already
gives disc-plus-ring, and the old code hand-rolled that and collected a third
band for the trouble. Stroke now scales with zoom, the documented way to stop a
ring swallowing the fill at low zoom. Cartographic practice agrees: Axis Maps
and the ICA Commission both put casings on symbols that OVERLAP rather than a
global opaque halo, and Esri names halos the biggest cost in marker drawing
performance -- paid here on a second full circle layer per point.

Research and sources in
_qa-cycle/01-ideation-research/RESEARCH-pin-design-why-they-read-as-eyes.md

Not yet verified: no hardware WebGL here, so the appearance is unproven. The
pale end of the ramp needs measuring, since the outer band was what gave the
white ring a hard boundary on any ground, and a perception complaint needs the
Driver's eye rather than a contrast ratio.

Also removes the table header caption "Filtering or selecting above updates
this list". Its centring history is kept in a comment: it records a reversed
judgement, not a mistake -- v1 put it in a second header, it was moved to the
existing bar on request, and centred by absolute positioning because flex
spacers landed it 55px off centre. All correct, and it still had to go: it
narrates the one behaviour on this surface that needs no narration.
This commit is contained in:
doxa-maps-88655c70 2026-09-22 18:46:16 +00:00
parent fd41b4a207
commit 2f5cf655f4
2 changed files with 73 additions and 21 deletions

View file

@ -491,19 +491,25 @@ useShadowStyles(`
</template>
<span class="rd-bar-sp"></span>
<!--
★ THE HINT LIVES IN THE EXISTING HEADER — NOT A SECOND ONE (Driver, v1 rejected).
v1 added this as its own <p> above the grid. The Driver's correction: "you dont have to add
a SECOND HEADER to make this show up... it could be CENTRED on the same header where it says
total 2107. Why? because we do not [want the extra chrome]". So it rides the bar that is
already there, beside the live count, and costs ZERO extra height.
★ NO EXPLANATORY HINT HERE (Driver, 2026-09-22: "there does not need to be this text in the
table header / Filtering or selecting above updates this list").
CENTRED BY ABSOLUTE POSITIONING against the bar, not by flex spacers. A first attempt put
it between two spacers and MEASURED 55px OFF CENTRE at 1440px and 1280px: spacers split the
LEFTOVER space, so the hint lands mid-gap, not mid-bar, because the left cluster
(dock/title/count/selection) and the right cluster (toast/Export CSV) differ in width.
Out of flow also means it adds zero height by construction.
The text read "Filtering or selecting above updates this list". Its history is worth keeping
because it records a judgement that got REVERSED rather than a mistake: v1 put it in a second
header above the grid, the Driver asked for it to ride the EXISTING bar instead, and it was
centred by absolute positioning against the bar (flex spacers split the LEFTOVER space, so
the hint landed 55px off centre at 1440px and 1280px because the left and right clusters
differ in width). All of that was right, and the text still had to go.
THE REASON IT GOES: the behaviour it described is the one thing on this surface that needs no
description. Filter above, the list below changes — the user sees the count move the moment
they act. A caption narrating visible feedback is chrome that costs attention on every view
to explain something learned once, and never again.
Keep .rd-bar-hint's CSS: it is the centring recipe, and re-deriving it would repeat the
55px-off-centre measurement. If a future caption genuinely earns its place, reuse the class.
-->
<span class="rd-bar-hint" data-grid-hint>Filtering or selecting above updates this list</span>
<span v-if="exportToast" class="rd-toast" data-export-toast>{{ exportToast }}</span>
<span class="rd-csv" :class="{ busy: exporting }" role="button" tabindex="0" data-csv-btn
:aria-busy="exporting ? 'true' : 'false'"

View file

@ -320,11 +320,12 @@ function addLayers() {
// '#fff' at full opacity, because the Driver asked for WHITE rings and 0.95 alpha over a
// grey choropleth bin was rendering them off-white.
//
// HIT-TESTING is unchanged: 'rd-bubble-layer' keeps its id and stays the TOP layer —
// onBubbleClick, onMapClick's pin-guard and the __rdashPinAt verify hook all query it by
// name. 'rd-bubble-halo' remains a separate, NON-interactive layer beneath it. Note the
// hit target shrinks with the dot at low zoom; __rdashPinAt already queries a +/-7px bbox,
// which still covers the z1 footprint.
// HIT-TESTING is unchanged: 'rd-bubble-layer' keeps its id — onBubbleClick, onMapClick's
// pin-guard and the __rdashPinAt verify hook all query it by name. As of 2026-09-22 it is the
// ONLY bubble layer: 'rd-bubble-halo' was deleted when the pin collapsed to one circle with a
// white stroke (see the long note below). Note the hit target shrinks with the dot at low zoom;
// __rdashPinAt already queries a +/-7px bbox, which still covers the z1 footprint — and the
// footprint GREW slightly (r 1.6 -> 2.2 at z1), so the existing bbox remains sufficient.
// ── ★ THE PIN IS NOW SELF-CONTAINED (card BUILD-MAP-STYLE-MUST-BE-BEAUTIFUL) ──────────
// The white ring used to depend on the GROUND for its visibility, and measured only
// 1.09–1.24 against the light bins — so on pale countries the ring vanished and the dots
@ -346,12 +347,57 @@ function addLayers() {
//
// HIT-TESTING unchanged: 'rd-bubble-layer' keeps its id and stays the TOP layer; the halo
// is a separate NON-interactive layer beneath it and must not be queried.
m.addLayer({ id: 'rd-bubble-halo', type: 'circle', source: 'rd-bubbles',
paint: { 'circle-radius': ['interpolate', ['linear'], ['zoom'], 1, 3.4, 6, 7.4], 'circle-color': '#fff', 'circle-opacity': 1,
// The outer edge: thin, navy, so the white ring has a hard boundary on ANY ground.
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 1, 0.6, 6, 1], 'circle-stroke-color': '#1a237e', 'circle-stroke-opacity': 0.9 } })
// ── ★ ONE CIRCLE WITH A WHITE STROKE — NOT A NAVY/WHITE/NAVY SANDWICH ──────────────────────
// (Driver 2026-09-22: "the pins look like eyes, they look kind of creepy")
//
// THE SANDWICH WAS THE BUG, and the comment above described it accurately without noticing what
// it draws. Read outward from the centre at z1 the old construction was:
// navy disc r=1.6 → white field r=3.4 → navy stroke 0.6px
// dark centre, light middle, dark rim. That is the anatomy of an eye — pupil, sclera, iris
// outline — and it is why a bullseye is NAMED after one. Concentric rings sharing a centre are
// exactly the pattern the visual system fuses into a single object; sighting equipment exploits
// the same involuntary alignment. The inner disc sat at 47% of the halo radius at z1, squarely in
// the proportion that reads as a PUPIL rather than as a dot with a border. Worst when zoomed out,
// which is where a world map lives.
//
// THE OUTER NAVY BAND IS THE SPECIFIC CULPRIT: it closes the iris. Without it the same colours
// read as an ordinary pin. So the fix removes a band rather than changing a colour.
//
// MATCHES THE MAPBOX IDIOM, which is ONE layer with a stroke, not two stacked circles. The style
// spec draws `circle-stroke-width` OUTSIDE `circle-radius`, so a single layer already produces
// disc-plus-ring; the old code hand-rolled that and collected a third colour band for the
// trouble. Scaling the stroke with zoom (1.2 → 2) is the documented way to stop the ring
// swallowing the fill when circles are small and dense at low zoom. Cartographic practice agrees:
// Axis Maps and the ICA Commission both call for casings on symbols that OVERLAP rather than a
// global opaque halo, and Esri names halos the single biggest cost in marker drawing performance
// — we were paying that on a second full circle layer across every point. Research + sources:
// _qa-cycle/01-ideation-research/RESEARCH-pin-design-why-they-read-as-eyes.md
//
// ★ THE TWO DRIVER RULINGS THIS PRESERVES — do not undo either:
// 1. ONE FIXED COLOUR AT FULL OPACITY. Pins were once '#e67e22' at circle-opacity 0.6; the
// alpha was the real fault, compositing each pin with whatever bin sat beneath it so it
// rendered a different colour per country at 1.18–1.22 contrast. Never reintroduce pin
// alpha, never tint per bin.
// 2. WHITE RINGS around blue dots so the field does not read as a heat map, and all rings stay
// visible when zoomed out. Still here — it is now the stroke instead of a second layer.
// Navy #1a237e measures 12.24 / 11.09 / 10.20 / 9.26 / 8.20 / 7.29 across the ramp; unchanged.
//
// WHAT IS GIVEN UP, stated honestly: the outer navy band gave the white ring a hard boundary on
// ANY ground, so over the palest bin the ring now blends into the background. The ring's job is
// separating pins from EACH OTHER (it still does that) while the navy disc carries pin-vs-ground
// contrast. MEASURE the pale end before calling this settled. If separation genuinely fails
// there, the fix is a casing on overlapping symbols only — not a restored global dark rim.
//
// HIT-TESTING: 'rd-bubble-layer' keeps its id and is now the ONLY bubble layer, so every
// click/hover handler bound to that id is unaffected. 'rd-bubble-halo' no longer exists.
m.addLayer({ id: 'rd-bubble-layer', type: 'circle', source: 'rd-bubbles',
paint: { 'circle-radius': ['interpolate', ['linear'], ['zoom'], 1, 1.6, 6, 5], 'circle-color': '#1a237e', 'circle-opacity': 1 } })
paint: {
'circle-radius': ['interpolate', ['linear'], ['zoom'], 1, 2.2, 6, 5.5],
'circle-color': '#1a237e', 'circle-opacity': 1,
'circle-stroke-color': '#ffffff',
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 1, 1.2, 6, 2],
'circle-stroke-opacity': 1,
} })
// selection pen — a prominent neon ring over the selected PG pin(s), drawn ABOVE the normal bubbles.
m.addSource('rd-bubble-selected', { type: 'geojson', data: selectedGeo() })
m.addLayer({ id: 'rd-bubble-selected', type: 'circle', source: 'rd-bubble-selected',