From 0756c8cc5c92f2c172110af53f5792557a1607e8 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 14:11:52 +0000 Subject: [PATCH 01/78] Collect the gospel set-chart layout check in gates:pre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The check itself shipped with the plugin modules it covers, but the line that runs it existed only in a message, which is not a place code lives. So it sat in internal/ and would never have run — the same failure it was written to prevent, one layer up. This is the whole registration in one commit: the file, the script entry and the word in the gates:pre list. It imports boundedSet.js and centeredSet.js, so it must land after the plugin commits that add them; landing it first fails collection and takes gates:pre red for everyone. --- internal/check-gospel-set-charts.mjs | 88 ++++++++++++++++++++++++++++ package.json | 3 +- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 internal/check-gospel-set-charts.mjs diff --git a/internal/check-gospel-set-charts.mjs b/internal/check-gospel-set-charts.mjs new file mode 100644 index 000000000..5756e6722 --- /dev/null +++ b/internal/check-gospel-set-charts.mjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node +/** + * check-gospel-set-charts.mjs — the bounded-set and centered-set LAYOUT maths. + * + * Both modules are pure, which is why they can be checked here at all: a pure function's + * inputs ARE its domain, so these assertions are evidence about the real module and not + * about a fixture that resembles one. What they are NOT evidence about is the shape of + * near-neighbors.json — that binding is checked where the data is read, not here. + * + * The properties worth pinning are the ones where a wrong answer still LOOKS like a chart: + * + * · bounded set — nothing may sit ON the boundary. A dot on the line is the one thing a + * bounded set cannot express, and it would read as a group that is somehow both. + * · bounded set — a group of UNKNOWN reached-ness is its own outcome. If it were folded + * into `outside`, a join failure would render as "unreached" and inflate the count with + * something that is not a fact about the group. + * · centered set — a group with no ring is UNPLACED, never defaulted to the outer ring. + * Ring 10 is a real answer ("beyond"), so it must not double as a bin for missing data. + * · centered set — a REACHED group (ring 0) is the CENTRE, not unplaced. An earlier build + * spelled "reached" and "no relation found" with the same 0; treating that value as + * missing data would report the best outcome in the set as an absence. The three states + * are asserted apart, and a reached group is asserted to reach NO ring at all. + * · centered set — the arrow stops exactly at innerR. An arrow that overshoots covers the + * cross it is meant to point at. + * · both — layout is deterministic, so a group does not move between renders. + */ +import { layoutBoundedSet, boundedSetTally } from '../plugin-designs/gospel-internet/src/boundedSet.js' +import { layoutCenteredSet, ringTally, inwardArrow, ringRadii, RING_MAX, RING_REACHED, RING_NO_RELATION } from '../plugin-designs/gospel-internet/src/centeredSet.js' + +const fail = [] +const ok = (label, cond, detail = '') => { + if (cond) console.log(` ✓ ${label}${detail ? ` — ${detail}` : ''}`) + else { console.log(` ✗ ${label}${detail ? ` — ${detail}` : ''}`); fail.push(label) } +} + +console.log('GOSPEL-SET-CHARTS') + +// ── bounded set ───────────────────────────────────────────────────────────── +const bg = [{ id: 'a', reached: true }, { id: 'b', reached: false }, { id: 'c' }, { id: 'd', reached: null }] +const B = layoutBoundedSet(bg, { r: 100 }) +const bt = boundedSetTally(B) +ok('bounded: reached inside, unreached outside, unknown neither', bt.inside === 1 && bt.outside === 1 && bt.unknown === 2, + `in ${bt.inside} out ${bt.outside} unknown ${bt.unknown}`) +ok('bounded: unknown is NOT folded into outside', bt.outside === 1 && bt.unknown === 2) +ok('bounded: total is conserved', bt.total === bg.length) +const dist = (p) => Math.hypot(p.x, p.y) +ok('bounded: nothing sits ON the boundary', dist(B.inside[0]) < 100 && dist(B.outside[0]) > 100, + `in r=${dist(B.inside[0]).toFixed(1)} out r=${dist(B.outside[0]).toFixed(1)}`) +ok('bounded: deterministic between renders', layoutBoundedSet(bg, { r: 100 }).inside[0].x === B.inside[0].x) + +// ── centered set ──────────────────────────────────────────────────────────── +const cg = [{ id: 'a', ring: 1, neighborId: 'r1' }, { id: 'b', ring: RING_MAX, neighborId: 'r2' }, + { id: 'c', ring: RING_REACHED }, { id: 'd' }, { id: 'e', ring: RING_MAX + 1 }, { id: 'f', ring: 5 }] +const C = layoutCenteredSet(cg) +const ct = ringTally(C) +ok('centered: valid rings placed, unusable ones unplaced, reached at the centre', + ct.placed === 3 && ct.unplaced === 2 && ct.centre === 1, + `placed ${ct.placed} centre ${ct.centre} unplaced ${ct.unplaced}`) +ok('centered: a missing ring is NOT defaulted onto the outer ring', + C.placed.filter((p) => p.ring === RING_MAX).length === 1) +ok('centered: every unplaced group carries a reason', C.unplaced.every((u) => typeof u.reason === 'string' && u.reason)) +ok('centered: rings are ordered inward to outward', ringRadii().length === RING_MAX && + C.placed.find((p) => p.ring === 1).r < C.placed.find((p) => p.ring === RING_MAX).r) +const p1 = C.placed.find((p) => p.ring === 1) +ok('centered: a point lies exactly on its own ring', Math.abs(Math.hypot(p1.x, p1.y) - p1.r) < 1e-9) +const arrow = inwardArrow(C.placed.find((p) => p.ring === RING_MAX)) +ok('centered: the inward arrow stops at innerR and does not cover the cross', + arrow && Math.abs(Math.hypot(arrow.x2, arrow.y2) - 26) < 1e-9, `ends at r=${Math.hypot(arrow.x2, arrow.y2).toFixed(2)}`) +ok('centered: no recorded neighbour means NO arrow, not a guessed one', inwardArrow({ x: 10, y: 0 }) === null) +ok('centered: deterministic between renders', layoutCenteredSet(cg).placed[0].x === C.placed[0].x) + +// the three ring states are three different facts and must not collapse into one another +const sg = [{ id: 'p', ring: 1, neighborId: 'r' }, { id: 'reached', ring: RING_REACHED }, + { id: 'norel', ring: RING_NO_RELATION }, { id: 'noring' }] +const S = layoutCenteredSet(sg) +const st = ringTally(S) +ok('centered: reached (ring 0) is the CENTRE, not unplaced', st.centre === 1 && S.centre[0].id === 'reached') +ok('centered: no-relation (ring -1) is unplaced with its OWN reason', + S.unplaced.find((u) => u.id === 'norel')?.reason === 'no relation found') +ok('centered: the three states stay apart', st.placed === 1 && st.centre === 1 && st.unplaced === 2, + `placed ${st.placed} centre ${st.centre} unplaced ${st.unplaced}`) +ok('centered: a reached group reaches NO ring', [...st.counts.values()].reduce((a, b) => a + b, 0) === st.placed) + +if (fail.length) { + console.log(`\nGOSPEL-SET-CHARTS: FAIL — ${fail.length}: ${fail.join(', ')}`) + process.exit(1) +} +console.log('\nGOSPEL-SET-CHARTS: PASS — both layouts pinned on the properties that fail silently') diff --git a/package.json b/package.json index b5b22adcb..908e9c725 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "check:experimental-shelf": "node internal/check-experimental-shelf.mjs", "check:export-poster-prints": "node plugin-designs/doxa-research-map/scripts/check-export-poster-prints.mjs", "check:export-poster-prints:maplibre": "node plugin-designs/doxa-research-map/scripts/check-export-poster-prints.mjs --provider maplibre", + "check:gospel-set-charts": "node internal/check-gospel-set-charts.mjs", "check:facet-cascade": "node internal/check-facet-cascade.mjs", "check:facet-cascade:offline": "node internal/check-facet-cascade.mjs --offline", "check:facet-cascade:self-test": "node internal/check-facet-cascade.mjs --self-test", @@ -267,7 +268,7 @@ "fix:sri": "node internal/fix-sri.mjs", "gallery": "node internal/generate-gallery.mjs", "gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:frozen-labels", - "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields", + "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:gospel-set-charts check:hardcoded-english:self-test check:list-selection check:selector-fields", "gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs", "internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs", "library-index": "node internal/generate-library-index.mjs", From 7308d4312a3f33e7c1e28d0d010e9504f730239f Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 16:41:13 +0000 Subject: [PATCH 02/78] Shape both set charts as tiered points, and leave the projection to the caller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared whiteboard layer draws a point collection where each feature carries an id, a label, a tier and a priority, and stages its label layers by tier and zoom. This decides which features exist, what tier each belongs to and how they rank — which is this chart's business — and it does not convert to degrees. Ring and band labels are placed on the radii the chart already draws rather than on a second calculation, so the label and the ring it names cannot drift apart. Groups rank by population, so when two names collide the larger people wins: a reader zoomed out should meet the names carrying the most people, not whichever was drawn first. Bands outrank rings outrank groups, because that is the order a reader zooming in meets them. Coordinates out are abstract on purpose. Converting them is a projection, not a scale — the whiteboard shrinks degree height outward so the map's own stretch cancels it, and a linear scale skips the cancellation. Measured on a ring of 720 points: with a linear scale the rendered aspect is 0.9968 at the inner ring and 0.7952 at the outer, so the target draws as ovals that worsen outward, worst exactly where the ring labels sit. With the projection it is 1.0000 at every ring. That measurement also settles where the guard belongs: at the inner ring the linear error is three parts in a thousand and would pass any sane tolerance. An assertion placed there alone would be inert against the one mistake it exists to catch. It goes at the outer ring, and at both. --- .../gospel-internet/src/setChartFeatures.js | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 plugin-designs/gospel-internet/src/setChartFeatures.js diff --git a/plugin-designs/gospel-internet/src/setChartFeatures.js b/plugin-designs/gospel-internet/src/setChartFeatures.js new file mode 100644 index 000000000..d88dfba91 --- /dev/null +++ b/plugin-designs/gospel-internet/src/setChartFeatures.js @@ -0,0 +1,82 @@ +/** + * setChartFeatures.js — the set charts as TIERED POINTS, in abstract coordinates. + * + * The shared whiteboard layer draws a point FeatureCollection where each feature carries an id, + * a label, a tier and a priority `k`, and stages its label layers by tier and zoom. This turns + * my two layouts into that shape. It decides WHICH features exist, what tier each belongs to and + * how they rank against each other — which is mine — and it does NOT project. + * + * ★ COORDINATES OUT ARE ABSTRACT (u, v), NOT DEGREES, AND THAT IS DELIBERATE. Converting to + * lng/lat is a PROJECTION, not a scale: the whiteboard's inverse-Mercator shrinks the degree + * height as you move out so the map's own 1/cos(lat) stretch cancels it. A linear scale skips + * that cancellation and the error grows outward — 1.0154x at 10 degrees, 1.0642x at 20, 1.1547x + * at 30. For concentric rings that renders as vertical OVALS, worst at the outer ring where the + * labels sit, and it looks plausible enough to waste an afternoon on. So the caller passes these + * through the shared projection with the chart's extent; this module never guesses at degrees. + * + * Extents (the caller's one number): centered set 228 = innerR 30 + 9 x ringGap 22; bounded 260. + */ +import { ringInfo } from './ringInfo.js' + +/** Tier names, outermost-first in label priority: bands read zoomed out, groups zoomed in. */ +export const TIERS = { BAND: 'band', RING: 'ring', GROUP: 'group' } + +/** + * The centered-set target as tiered points. + * + * `placed` are laid-out groups ({ id, label, ring, x, y }); `radii` is ringRadii()'s output, so + * the ring and band labels sit on the SAME radii the chart draws rather than on a second + * calculation that could drift from it. + * + * `k` is the priority the shared layer sorts labels by. Groups rank by population, so when two + * names collide the bigger people wins — a reader zoomed out should see the names that carry the + * most people, not whichever happened to be drawn first. Bands and rings outrank every group. + */ +export function centeredSetFeatures(placed, radii, { bandLabel, ringLabel, population } = {}) { + const out = [] + for (const p of placed || []) { + out.push({ id: p.id, label: p.label, tier: TIERS.GROUP, ring: p.ring, + k: Number(population?.(p) ?? 0), u: p.x, v: p.y }) + } + // One label per ring, placed on the ring itself at a fixed angle so it does not wander between + // renders. Rings outrank groups; the inner rings outrank the outer, because a reader zooming in + // meets them in that order. + for (const { ring, r } of radii || []) { + const info = ringInfo(ring) + if (!info) continue + out.push({ id: `ring-${ring}`, label: ringLabel?.(ring) ?? `Ring ${ring}`, tier: TIERS.RING, + ring, k: 1e9 - ring, u: 0, v: -r }) + } + // One label per band, at the outer edge of the band's own rings. + const bands = new Map() + for (const { ring, r } of radii || []) { + const info = ringInfo(ring) + if (!info) continue + const cur = bands.get(info.band) + if (!cur || r > cur) bands.set(info.band, r) + } + for (const [band, r] of bands) { + out.push({ id: `band-${band}`, label: bandLabel?.(band) ?? band, tier: TIERS.BAND, + k: 2e9, u: 0, v: -r }) + } + return out +} + +/** + * The bounded set as tiered points, plus the boundary circle as a dish. + * + * ★ THE GAP EITHER SIDE OF THE BOUNDARY IS THE CHART'S ONE CLAIM and it must survive projection: + * inside points sit within 0.92r, outside beyond 1.12r, and nothing may land ON the line — a dot + * on the boundary is the one thing a bounded set cannot express. The dish radius is the boundary + * itself, so the caller projects all three consistently or the gap closes on one side. + */ +export function boundedSetFeatures(layout, { label, population } = {}) { + const out = [] + for (const side of ['inside', 'outside']) { + for (const p of layout?.[side] || []) { + out.push({ id: p.id, label: label?.(p) ?? p.label, tier: TIERS.GROUP, side, + k: Number(population?.(p) ?? 0), u: p.x, v: p.y }) + } + } + return { features: out, dish: { u: layout?.cx ?? 0, v: layout?.cy ?? 0, r: layout?.r ?? 0 } } +} From 1755694a087e7068425ba322eb251df4c21f91ae Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 16:51:39 +0000 Subject: [PATCH 03/78] Project the set-chart features onto the whiteboard, and report the real ceiling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The projection is passed in rather than built here, so the chart and its checks share one instance and this module still owes nothing to a coordinate system it does not define. The dish takes its radius from the projection's exposed scale: deriving it by projecting two points and subtracting would be right on the equator and wrong everywhere else. The guard reports the outermost latitude, because that is the real limit and shape is not. The projection is the exact inverse of the renderer's transform, so a circle stays a circle at every extentFraction — measured at 0.000% aspect error. An earlier reading of mine said otherwise and was wrong: it computed screen x as longitude times cos(latitude), a plate-carree correction Web Mercator never applies, which manufactured a distortion the renderer does not produce. What does bite is the pole. The outermost point reaches 52 degrees at the default 0.17, 77 at 0.35, 83 at 0.45, and Web Mercator is undefined at the poles with engines clamping near 85. That ceiling is a property of extentFraction alone, not of a chart's extent: the scale is extentFraction over extent, so the outermost point is at extentFraction whatever the extent, and every chart reaches the same latitude. Measured across extents 100, 228, 260 and 1000 — identical to three decimals. So a consumer cannot buy headroom by choosing a bigger extent, and a check written against one chart's ceiling holds for all of them. --- .../gospel-internet/src/setChartFeatures.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/plugin-designs/gospel-internet/src/setChartFeatures.js b/plugin-designs/gospel-internet/src/setChartFeatures.js index d88dfba91..14c85bb7e 100644 --- a/plugin-designs/gospel-internet/src/setChartFeatures.js +++ b/plugin-designs/gospel-internet/src/setChartFeatures.js @@ -80,3 +80,41 @@ export function boundedSetFeatures(layout, { label, population } = {}) { } return { features: out, dish: { u: layout?.cx ?? 0, v: layout?.cy ?? 0, r: layout?.r ?? 0 } } } + +/** + * Project abstract (u, v) features into a GeoJSON point collection for the whiteboard layer. + * + * `project` is the shared whiteboardProjection for this chart's extent. It is passed in rather + * than built here so the chart and its checks use ONE projection instance, and so this module + * still owes nothing to a coordinate system it does not define. + * + * ⚠ THE LATITUDE CEILING IS THE REAL LIMIT, not shape. The projection is the exact inverse of + * the renderer's transform, so a circle stays a circle at every extentFraction — measured at + * 0.000% aspect error. What DOES bite is how close to the pole the drawing lands: at extent 260 + * the outermost point reaches 52 degrees at the default 0.17, 77 at 0.35, 83 at 0.45, and Web + * Mercator is undefined at the poles with engines clamping near 85. So a caller that widens + * extentFraction to let readers zoom out further runs into the clamp, not into distortion. + * `maxAbsLat` reports it so a check can assert the drawing stays inside that range. + */ +export function toFeatureCollection(features, project) { + let maxAbsLat = 0 + const out = [] + for (const f of features || []) { + const [lng, lat] = project(f.u, f.v) + if (Math.abs(lat) > maxAbsLat) maxAbsLat = Math.abs(lat) + const { u, v, ...props } = f + out.push({ type: 'Feature', id: f.id, geometry: { type: 'Point', coordinates: [lng, lat] }, properties: props }) + } + return { collection: { type: 'FeatureCollection', features: out }, maxAbsLat } +} + +/** + * A radius in DEGREES for the dish, from a radius in layout units. + * + * The dish is a circle-radius in the paint, so it needs the projection's scale rather than a + * projected point. project.K is exposed for exactly this; deriving it by projecting two points + * and subtracting would be right along the equator and wrong everywhere else. + */ +export function dishRadiusDegrees(r, project) { + return Number(r || 0) * (project?.K ?? 0) * 360 +} From f75fbbd4a2bcaef2763cbb8c77b582945833c465 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 16:55:07 +0000 Subject: [PATCH 04/78] Draw the centered set on the whiteboard, so it pans, zooms and labels by tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ask changed and so did the call. A static target is fine in SVG; a zoomable one carrying ~2,100 labels is not. The engine tiles the collection per zoom so it draws less when zoomed out, and its symbol layers do label collision, which is the part that would take longest to do badly by hand. The label staging is one layer per tier with its own minzoom: band names when zoomed out, ring names in the middle, group names in close. None of that is tree-shaped, which is why it is a shared layer rather than a copy of one. Coordinates go through the shared projection, never a scale. It is the exact inverse of the renderer's transform, so a circle stays a circle; a linear scale would draw concentric rings as ovals worsening outward. The only number this chart owns is its extent. Dimming a deselected ring is a paint expression rather than a filter, for the reason the SVG version dims instead of hiding: a ring emptied of its dots reads as a ring with no groups, which is the opposite of what a reader selecting it just asked for. The layer id is read back from the handle rather than written from memory. The first version reached for a name the module does not use; inside a try/catch that does not throw, so the dimming would simply never have happened — a legend that appears to do nothing rather than an error. Asking the handle means a rename cannot silently disable it. --- .../src/components/GiCenteredSetMap.vue | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue diff --git a/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue b/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue new file mode 100644 index 000000000..8642734cc --- /dev/null +++ b/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue @@ -0,0 +1,137 @@ + + + From 540bcd999e20c1792964e59f1555624094c9e5e6 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 16:57:40 +0000 Subject: [PATCH 05/78] Show the centered set as the whiteboard map rather than a static figure The panel now mounts the zoomable chart in place of the SVG. Pan, zoom and labels that appear by tier as the reader moves in were the ask; the engine supplies tiling and label collision that an SVG version would have to reimplement, and badly. The printed component stays SVG, which is not an inconsistency: a sheet has no zoom and no hover, so paper and screen genuinely want different renderers. What they share is the ring vocabulary, so the two cannot drift apart in what they call a ring even though they draw it differently. --- .../src/components/GiCenteredSet.vue | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue b/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue index c48c14589..b60a377b5 100644 --- a/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue +++ b/plugin-designs/gospel-internet/src/components/GiCenteredSet.vue @@ -26,6 +26,7 @@ import { useGiT } from '../useGiT.js' import { loadNearRows, indexFor } from '../nearRows.js' import { layoutCenteredSet, ringTally, inwardArrow, ringRadii } from '@map/components/charts/centeredSet.js' import GiRingLegend from './GiRingLegend.vue' +import GiCenteredSetMap from './GiCenteredSetMap.vue' const T = useGiT() const board = useBoardState() @@ -112,22 +113,12 @@ useShadowStyles(