From 2f5cf655f4f448d5ea86e0d50b9bfae8b633f729 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:46:16 +0000 Subject: [PATCH] Draw the pin as one circle with a white stroke, not three colour bands 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. --- .../dashboards/DashboardDataTable.vue | 28 ++++---- .../dashboards/DashboardMapPanel.vue | 66 ++++++++++++++++--- 2 files changed, 73 insertions(+), 21 deletions(-) diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue index 096f23e51..04fa0625a 100644 --- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue +++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue @@ -491,19 +491,25 @@ useShadowStyles(` - Filtering or selecting above updates this list + {{ exportToast }} 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',