From 092585af6cc45ad7b87c41aefc5825b2a7d7067d Mon Sep 17 00:00:00 2001
From: Doxa Maps
Date: Mon, 28 Sep 2026 17:02:03 +0000
Subject: [PATCH] Put the bounded set on the whiteboard too, with its boundary
in map units
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both set charts now pan and zoom. The chart still makes one claim — which side
of the line a group is on — so nothing here encodes distance or rank; what zoom
buys is reading the names, because at two thousand dots the inside is a cluster
until you can get close to it.
The boundary is the shared dish layer, which is the same layer a language
family's circle uses, so it is not a new type for either of us. Its radius takes
an exponential-base-2 zoom interpolation rather than a fixed pixel value.
circle-radius is screen pixels: a constant would hold its size while the dots
spread out from under it, leaving the circle correct at exactly one zoom and
wrong at every other — and for a dot near the edge the inside/outside reading
would silently invert, which is the only thing this chart says. Pixels per degree
double with each zoom level, measured at exactly 2.000000 between adjacent
levels, so the interpolation tracks map units exactly rather than approximately.
The boundary is drawn as a line with a transparent fill. A filled disc would say
the inside is a region, when what the chart means is a threshold.
The cross rides in the point collection as its own tier rather than as an
overlay, so it pans and zooms with the drawing instead of floating above it.
---
.../src/components/GiBoundedSet.vue | 30 ++---
.../src/components/GiBoundedSetMap.vue | 108 ++++++++++++++++++
.../gospel-internet/src/setChartFeatures.js | 17 +++
3 files changed, 136 insertions(+), 19 deletions(-)
create mode 100644 plugin-designs/gospel-internet/src/components/GiBoundedSetMap.vue
diff --git a/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue b/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
index 5c622642c..5f4777e1a 100644
--- a/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
+++ b/plugin-designs/gospel-internet/src/components/GiBoundedSet.vue
@@ -28,7 +28,12 @@ import { formatCount } from '@map/utils/formatCount.js'
import { useBoardState } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
import { loadNearRows, indexFor } from '../nearRows.js'
+<<<<<<< HEAD
import { layoutBoundedSet, boundedSetTally } from '@map/components/charts/boundedSet.js'
+=======
+import { layoutBoundedSet, boundedSetTally } from '../boundedSet.js'
+import GiBoundedSetMap from './GiBoundedSetMap.vue'
+>>>>>>> ea8dbe45 (Put the bounded set on the whiteboard too, with its boundary in map units)
const T = useGiT()
const board = useBoardState()
@@ -73,7 +78,7 @@ useShadowStyles(
// can never push the figure out of its pane the way the legend did on the centered set.
'.gbs{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;' +
'justify-content:center;padding:46px 10px 10px;box-sizing:border-box;overflow:hidden;background:#f3f3f1}' +
- '.gbs svg{max-width:100%;max-height:100%}' +
+ '.gbs-fig{position:relative;flex:1 1 auto;align-self:stretch;min-height:0;min-width:0}' +
'.gbs-ring{fill:none;stroke:#3b463d;stroke-width:1.5}' +
'.gbs-in{fill:#92b195}.gbs-out{fill:#6d766e;opacity:.65}' +
'.gbs-cross-halo{stroke:var(--gbs-bg,#f3f3f1);stroke-width:7;stroke-linecap:square}' +
@@ -91,24 +96,11 @@ useShadowStyles(
{{ failed }}
+ {{ T('gospelInternet.bounded.failed', 'The reached/unreached table could not be loaded, so this chart is not drawn.') }}
+ {{ failed }}
+
+
+
diff --git a/plugin-designs/gospel-internet/src/setChartFeatures.js b/plugin-designs/gospel-internet/src/setChartFeatures.js
index 14c85bb7e..66bc15a47 100644
--- a/plugin-designs/gospel-internet/src/setChartFeatures.js
+++ b/plugin-designs/gospel-internet/src/setChartFeatures.js
@@ -118,3 +118,20 @@ export function toFeatureCollection(features, project) {
export function dishRadiusDegrees(r, project) {
return Number(r || 0) * (project?.K ?? 0) * 360
}
+
+/**
+ * A zoom expression that keeps a radius fixed in MAP units while the paint wants PIXELS.
+ *
+ * ★ circle-radius IS SCREEN PIXELS, and for the bounded set's boundary that is the wrong unit.
+ * A fixed pixel radius stays the same size on screen while the dots spread out from under it as
+ * the reader zooms — the circle would be right at exactly one zoom level and wrong at every
+ * other, with the inside/outside reading silently inverting for dots near the edge.
+ *
+ * The world is 512 x 2^zoom pixels across 360 degrees, so pixels-per-degree doubles with every
+ * zoom level — measured exactly 2.000000 between adjacent levels. An exponential-base-2
+ * interpolation therefore tracks map units exactly rather than approximately, with two stops.
+ */
+export function degreesToPixelRadius(degrees, { minZoom = 0, maxZoom = 22 } = {}) {
+ const px = (z) => Number(degrees || 0) * 512 * Math.pow(2, z) / 360
+ return ['interpolate', ['exponential', 2], ['zoom'], minZoom, px(minZoom), maxZoom, px(maxZoom)]
+}