doxa-maps/docs/reference.md
doxa-maps 756bb32c9f
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
docs: close retest gaps — engine-swap recipe (provider param), instances.json recipe + schema, adopt embed-example into router
2026-09-11 18:50:31 +00:00

15 KiB

Reference

Look-up tables and contracts for doxa-maps. For learning, see the tutorial; for tasks, how-to.md; for the why, why.md; for the short "what it is", explanation.md.

doxa-maps is a Vite bundler: one shared library, N independent embeddable bundles. Each bundle is a single self-contained .js file that registers a custom HTML element (<doxa-map>, <doxa-research-map>, …) you can drop on any web page.


The three zones

Zone Path Alias License You edit it…
Shared library library/ @map Apache-2.0 …once; the fix reaches every bundle on next build
Your app-profile app-profiles/<name>/ — yours …freely; nothing here leaks to other maps
The build machine internal/ — Apache-2.0 …never — it's the bundler's own scripts

Terminology. "Profile" is used at two levels: a bundle / app-profile is a folder under app-profiles/; a profile screen is one .vue file sitting flat in that folder (no profiles/ subfolder — the entry globs ./*.vue). A bundle can hold several screens; the host picks which to render with the profile field of profile-config.

Three profile patterns (mix freely in one bundle): Individual (one .vue per bundle) · Parameterized (one screen driven by profile-config props at runtime) · Nested (one screen that renders others via tabs, workbench-style).


Directory tree

doxa-maps/
├── library/                      ← SHARED LIBRARY (alias @map, Apache-2.0)
│   ├── index.js                  ← thin barrel; prefer subpath imports for tree-shaking
│   ├── ProfileLoader.vue         ← the ONE component that reads profile-config & loads a screen
│   ├── components/               ← map UI: legends/, map-controls/, poster/, dashboards/, drawers
│   ├── composables/              ← useMapInstance, useMapData, useMapLayers, … (+ legends/, poster/)
│   ├── api/                      ← DataSourceManager (csv | api | rest-api) + sources.json
│   ├── colors/                   ← one color strategy per file, auto-registered by _registry.js
│   ├── constants/                ← never-change values: mapDefaults.js, zoom.js, poster*.js
│   ├── i18n/locales/             ← 11 base locales: ar de en es fr hi it pt ro ru zh
│   ├── stores/                   ← Pinia: dataStore, mapStore, uiStore (per-instance)
│   └── utils/                    ← pure helpers (geoUtils, clustering)
│
├── app-profiles/                 ← ALL bundles live here (each subfolder = 1 bundle)
│   ├── <bundle>/
│   │   ├── index.js              ← bundle ENTRY — globs ./*.vue, registers the web component
│   │   ├── index.html            ← Vite html entry = the dev STAGING page
│   │   ├── <profile>.vue         ← 1..N FLAT profile screens (no profiles/ subfolder)
│   │   └── src/                  ← everything else the map needs (its own colors/, api/, …)
│   └── template-bundle/          ← minimal copy-me reference bundle
│                                    (folders starting with _ or . are SKIPPED by the build)
│
├── internal/                     ← the bundler's own build scripts — never edited by map builders
├── layouts/                      ← the app shell: mounts several bundles as one single-page app
├── capacitor/                    ← native projects (android/, ios/)
├── public/                       ← THE SERVED WEB-ROOT, committed:
│   ├── index.html                    the app shell
│   ├── doxa-maps/<bundle>/           built bundles + manifest.json + staging HTML
│   └── assets/                       posters & data the maps fetch at runtime
├── vite.config.js                ← multi-entry: discovers app-profiles/<name>/ folders
└── vite.doxa-maps-build-config.json ← output tree name + copy destinations + token settings

The profile-config runtime contract

A bundle exposes ONE attribute, profile-config — a JSON string parsed by ProfileLoader.vue at runtime. It is the entire wiring between host page and map, so the same built bundle re-points to different data/colors/language with no rebuild.

<doxa-map profile-config='{
  "profile":    "doxa-simple-map",   // which flat .vue screen to render (required)
  "tk":         "pk.eyJ...",         // Mapbox token (required; host-supplied, never baked)
  "instanceId": "my-map",            // optional; isolates window events between embeds
  "dataSource": "pray-tools",        // or { "type":"rest-api","endpoint":"https://…" }
  "colorSet":   "default",
  "locale":     "en",
  "provider":   "mapbox",           // map engine: "mapbox" (default) | "maplibre" (preview);
                                     //   also flippable per page via ?provider= or window.MAP_PROVIDER
  "tabs":       [ /* parameterized screens read this */ ]
}'></doxa-map>
  • profile must match a flat .vue filename in the bundle. Mismatch → an inline "Profile not found" error box listing the available names.
  • tk is supplied at runtime so the token is never compiled in (dev uses .env's VITE_MAPBOX_TOKEN).
  • instanceId scopes cross-instance window events so multiple embeds never interfere.
  • dataSource is handed to DataSourceManager (csv | api | rest-api).
  • ProfileLoader provide()s to descendants: mapboxToken, dataSource (both computed refs — unref before use), colorSet, profileConfig, instanceId, lang, and the instance-scoped Pinia stores uiStore / mapStore / dataStore. Descendants inject; never call useXxxStore() directly (it would cross-bleed between embeds).

Each bundle registers a shadow-DOM web component via defineCustomElement, with its own ShadowRoot, styles, Pinia, and i18n. customElements.define() is one-shot per tag name, so each bundle registers a distinct tag.


Build scripts & output

Script What it does
bun run dev Vite dev server + HMR (default http://localhost:5173). Serves every bundle at its html path. Writes nothing to public/ — no HMR across that boundary.
bun run build clean → one vite build per app-profiles/*/ folder → tree → check:drop → check:parse.
bun run build:bundle Builds ONE bundle (needs BUNDLE=<name>).
bun run build:watch Rebuilds into public/ on every change (integration loop).
bun run clean Wipes + recreates the public/<name>/ output tree.
bun run tree Writes manifest.json + self-contained staging HTML, then copies the tree to every build-config paths destination.
bun run verify Data-integrity + shipped-bundle + network-policy guards (fast, no browser).
bun run verify:full + rendered posters and live apps in Firefox.

How a build works. vite.config.js is multi-entry by discovery: at build time it registers every app-profiles/ subfolder that (a) doesn't start with _/. and (b) has both index.html and index.js. One vite build builds exactly one bundle, selected by BUNDLE=<name>, so the output is a self-contained IIFE (inlineDynamicImports: true). bun run build loops every folder. Each bundle only bundles what its screens import, so sizes differ (simple ≈ 420 KB, research ≈ 1.3 MB).

Where output goes. One bundle folder → one file: public/doxa-maps/<bundle>/<bundle>.js. CSS is injected by JS (cssCodeSplit: false), so there is no separate .css. The tree root name comes from vite.doxa-maps-build-config.json name (default doxa-maps). emptyOutDir: false keeps sibling bundles intact when you rebuild one.

What a built bundle folder contains. Beyond the IIFE, bun run tree generates an index.html per bundle (staging/showcase), per profile, and per instance — each profile/instance page is a standalone, directly linkable/iframe-able map — plus public/doxa-maps/manifest.json with ready-made embed.iframe/embed.script snippets for every profile. Full design: architecture/embeddability.md.

Preset instances. A <profile>.instances.json beside a parameterized profile ({ profile, apiBaseUrl?, instances: [{ id, title?, props }] }) makes the build emit one standalone page per instance at <bundle>/<id>/index.html. Recipe: how-to.md.

Bun is required. bun.lock is the committed lockfile; bun x vite runs project-local Vite. npm run … resolves the same scripts but Bun must be on PATH.


vite.doxa-maps-build-config.json fields

Edit this one file, then bun run build.

Field What it does
name Output folder name (default doxa-maps).
paths Extra folders to copy the whole built tree into. Point at a host repo's public/ → that host serves the maps and becomes the CDN. Empty (the default) ⇒ written to public/<name>/ only, since public/ is already the committed web-root.
embedBaseUrl The public URL the tree is served from, used to generate absolute, paste-anywhere embed snippets. Empty ⇒ relative URLs (position-independent; keeps the drop-portability gate green; required for Capacitor, which loads from https://localhost).
tokenUrl Endpoint each generated page fetches its Mapbox token from — a URL, not a secret. Set absolute (https://…/api/maps/token) so a CDN folder and the mobile app both work; endpoint must send CORS for the embedding origins. window.MAP_TOKEN_URL overrides at runtime.
token Opt-in literal token baked into every page. Leave empty — prefer tokenUrl.
assetsBaseUrl Where the shared asset tree (previews, posters, boundaries) is served from. Empty ⇒ a relative path per page depth (self-contained under one web-root). Set absolute for partner embeds and mobile, where there is no shared web-root to climb to.

Token resolution ladder: per-map tk prop > ?tk= > baked token > window.MAP_TOKEN_URL > tokenUrl > same-origin /api/maps/token.


Data sources

Maps read data through @map/api/ driven by library/api/sources.json. Three source types:

Type What Points at
rest-api versioned REST endpoint (default) the live people-group API; base URL resolves at runtime (window.MAP_APP_API_URL → VITE_API_BASE_URL → same-origin)
csv a local CSV parsed at load the Joshua Project dataset (+ photo fallbacks)
api a single JSON URL any plain JSON endpoint

Country/region polygons are not a sources.json connection — they render from Mapbox's built-in country-boundaries-v1 tileset plus a lazy-loaded WAGF-regions GeoJSON.

Adding or changing a source? Full contract + checklist: architecture/data-sources.md.


Colors

Reused by more than one map → shared library/colors/. Only your map uses it → your bundle's src/colors/ (merges over the shared set, that bundle only). Filename derives the mode key (language-family.js → languageFamily).

Writing or overriding a strategy? Full contract + checklist: architecture/color-strategies.md · two homes in depth: ../contributing/COLORS.md.


Importing from @map

Everything under library/ is importable via the @map/… alias (vite.config.js maps @map → library/). The import path mirrors the file path: a file at library/<sub>/<file> imports as @map/<sub>/<file>. Examples:

import SemanticTreeLegend from '@map/components/legends/SemanticTreeLegend.vue'
import { useMapInstance }  from '@map/composables/useMapInstance.js'
import { useMapData }      from '@map/composables/useMapData.js'
import langFamilyByIso     from '@map/data/langFamilyByIso.json'

Prefer subpath imports over the thin @map barrel (which exports MAP_CORE_VERSION only) so tree-shaking works. The per-file catalog in architecture/file-map.md is the authoritative menu — every library/ file listed is importable this way. A generated one-line import index can also be produced with node internal/generate-library-index.mjs.


Going deeper — the architecture folder

Simple lookups live here; the full contracts, catalogs, and wiring detail live in architecture/:

Doc What it holds
file-map.md every file, one line each, with the gotcha each carries
data-sources.md the api/ seam: connection contract + checklist
color-strategies.md the strategy contract + legend wiring
components.md shared component catalog + conventions
i18n.md the locale contract + add-a-language checklist
embeddability.md generated per-map pages + manifest — embeddable by default
app-wrappers.md the SPA shell: nav.js + PROFILES contracts
poster-generation.md the poster/preview generator pipeline
patterns-and-provenance.md the two multi-map patterns + provenance

The Semantic Tree Legend component (a reusable hierarchical legend) is documented in file-map.md.


Quick reference — "I want to…"

I want to… Do this
Add a new map copy app-profiles/template-bundle/ → rename folder + tag in index.js
Add a screen to an existing bundle drop a .vue flat in that bundle's folder, request it via profile-config.profile
Fix a legend / control for ALL maps edit it once in library/ (@map); rebuild bundles
Add a color strategy new file in @map/colors/ (shared) or the bundle's src/colors/ (map-private)
Build everything bun run build → public/doxa-maps/<bundle>/<bundle>.js
Build just one BUNDLE=<name> bun run build:bundle
Preview in isolation bun run dev → http://localhost:5173/app-profiles/<name>/index.html
See a change on the served site bun run build, then refresh (NOT dev)
Repoint the API without a rebuild set window.MAP_APP_API_URL before the bundle loads
Repoint the Mapbox token pass tk in profile-config, or set window.MAP_TOKEN_URL / build-config tokenUrl