docs: migrate color-strategy contract to docs/architecture; simple README+graphic; add shared-vs-app-profiles balance graphic
This commit is contained in:
parent
81db8dbebb
commit
92be1ca4ef
3 changed files with 111 additions and 92 deletions
|
|
@ -5,6 +5,27 @@ You build your map by making a folder **here**, `app-profiles/<your-map>/`, and
|
|||
template's building blocks. Your folder is yours (colors, API, i18n); the template is shared
|
||||
(Apache-2.0, propagated to every map on the next build).
|
||||
|
||||
## Shared vs. yours — the balance
|
||||
|
||||
```
|
||||
library/ (@map) ── SHARED, reusable, parameterized ──► legend · map · map-controls
|
||||
│ (every map reuses these)
|
||||
│ imported by
|
||||
▼
|
||||
app-profiles/<your-map>/ ── YOURS ──► the map's "main" class (the .vue profile = the Vue
|
||||
│ instance for that map)
|
||||
└── src/ ── YOURS ──► this map's OWN non-reusable components, colors, API, utils
|
||||
|
||||
One bundle can hold as many maps as you want — add another .vue profile and rebuild.
|
||||
```
|
||||
|
||||
- **Reusable → `library/`.** The legend, the map, the map-controls are shared, parameterized
|
||||
components every map reuses. Fix once, every map benefits. Never edit it to customize one map.
|
||||
- **This map's own → `app-profiles/<your-map>/`.** It holds two things: the map's **"main" class**
|
||||
(each `.vue` profile *is* the Vue instance for that map) **and** that map's **own non-reusable
|
||||
components** (in `src/`). Nothing here leaks to another map.
|
||||
- **Many maps per bundle.** The bundler bundles one map or many — add another flat `.vue` and rebuild.
|
||||
|
||||
## The pattern in one picture
|
||||
|
||||
```
|
||||
|
|
|
|||
71
docs/architecture/color-strategies.md
Normal file
71
docs/architecture/color-strategies.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Architecture — color strategies (the full contract)
|
||||
|
||||
> The simple overview is in `library/colors/README.md`. This is the complete contract for
|
||||
> **writing or overriding** a color strategy.
|
||||
|
||||
A *color strategy* decides how people-group pins are colored for a given color mode (language
|
||||
family, prayer progress, religion, …). One strategy = one file. `_registry.js` auto-discovers
|
||||
every `<name>.js` via `import.meta.glob` and builds the `COLOR_MODES` enum + lookup from the
|
||||
filenames — no manual registration.
|
||||
|
||||
**Filename → mode mapping:** `prayer-progress.js` → `prayerProgress` / `COLOR_MODES.PRAYER_PROGRESS`;
|
||||
`religion.js` → `religion` / `COLOR_MODES.RELIGION`. Files starting with `_` or `.` are skipped.
|
||||
|
||||
## The strategy contract
|
||||
|
||||
Every `<name>.js` MUST `export default` this shape. A missing `name` (regression card #20) leaves
|
||||
the tab/legend label blank (the map still paints):
|
||||
|
||||
```js
|
||||
export const PROPERTY_KEY = 'myField' // GeoJSON feature property this reads
|
||||
export function getColor(properties) { ... } // (props) -> '#rrggbb' — JS-side resolver
|
||||
export function applyColor(options = {}) { ... } // -> Mapbox expression array (head = operator)
|
||||
export const buildColorExpression = applyColor // alias used by the tab-switch handler
|
||||
|
||||
export default {
|
||||
name: 'My Strategy', // REQUIRED — human label, never omit
|
||||
propertyKey: PROPERTY_KEY,
|
||||
getColor, applyColor, buildColorExpression,
|
||||
// …any palette/labels you also want on the default export
|
||||
}
|
||||
```
|
||||
|
||||
`applyColor()` must return a valid Mapbox paint expression — an array whose first element is an
|
||||
operator (`'match'`, `'case'`, `'interpolate'`, …). It feeds `circle-color` in
|
||||
`useMapLayers.addLanguageFamilyLayer`.
|
||||
|
||||
## Add a custom strategy (checklist)
|
||||
|
||||
1. **Create `<name>.js`** following the contract. Deriving color from a string? reuse
|
||||
`generateColorFromString` from `../utils/geoUtils.js` (see `doxa-region.js`). The mode key is the
|
||||
camelCased filename — pick the filename to match the mode string consumers pass.
|
||||
2. **Build — it auto-registers.** No `_registry.js` edit. (Needs a `export default` with a non-empty
|
||||
`name`; files without a default export are ignored.)
|
||||
3. **Make the pin feature carry your `PROPERTY_KEY`** — pin properties are built in
|
||||
`composables/useMapLayers.js → addLanguageFamilyLayer`. Missing field → every pin falls to default.
|
||||
4. **Wire the legend** (only if the mode shows a legend tab) in
|
||||
`composables/legends/useLegendData.js`: add `TITLE_KEYS`, `activeFilter`, `setFilter`, and `items`
|
||||
branches. Religion is the worked example (reads `PALETTE`, `RELIGION_FAMILIES`, `getReligionFamily`
|
||||
via named exports, so export those too).
|
||||
5. **Verify** — the structural tooling resolves every mode and asserts a non-empty `name`, a valid
|
||||
expression array, and a hex `getFeatureColor`.
|
||||
|
||||
## Profile-local strategies (override / add without touching the library)
|
||||
|
||||
`library/colors/` is the **shared** set. A single map that wants its own coloring drops a `<name>.js`
|
||||
in that bundle's `app-profiles/<bundle>/src/colors/`. At init, `index.js` calls
|
||||
`registerStrategies(import.meta.glob('./src/colors/*.js', { eager: true }))`, merging local files
|
||||
**over** the shared set:
|
||||
|
||||
- **Same mode key** as a shared strategy → local **overrides** it, for that bundle only.
|
||||
- **New mode key** → local **adds** a bundle-private strategy.
|
||||
|
||||
Each bundle is its own IIFE, so a local strategy can't leak to another map. This is the "local by
|
||||
default, promote deliberately" seam. See `contributing/COLORS.md` and
|
||||
`app-profiles/template-bundle/src/colors/` for the copy-me example.
|
||||
|
||||
## Status / custom modes
|
||||
|
||||
`COLOR_MODES.STATUS` and `COLOR_MODES.CUSTOM` are declared but have no strategy file —
|
||||
`getColorStrategy()` falls back to `LANGUAGE_FAMILY` for any unknown mode. Add a real file (steps
|
||||
above) when one graduates from placeholder.
|
||||
|
|
@ -1,97 +1,24 @@
|
|||
# Color Strategies — modular folder
|
||||
# library/colors/ — how pins get colored
|
||||
|
||||
One strategy = one file. Each strategy decides how people-group pins are colored on
|
||||
the map for a given "color mode" (language family, prayer progress, religion, …).
|
||||
`_registry.js` **auto-discovers** every `<name>.js` in this folder via Vite's
|
||||
`import.meta.glob` (the same pattern app-profiles use) and builds the `COLOR_MODES`
|
||||
enum + strategy lookup from the filenames — **no manual registration**. There is no
|
||||
monolith shim: import the real modules directly — `_registry.js`,
|
||||
`language-family.js`, `engagement.js`, `adoption.js`, `prayer-progress.js`.
|
||||
Each file here is one **color strategy** — a way to color the map's pins (by language family, prayer
|
||||
progress, religion, …). Drop a `<name>.js` in and it's auto-discovered; no registration.
|
||||
|
||||
**Filename → mode mapping:** `prayer-progress.js` → mode `prayerProgress` /
|
||||
`COLOR_MODES.PRAYER_PROGRESS`; `religion.js` → `religion` / `COLOR_MODES.RELIGION`.
|
||||
Files whose name starts with `_` (like this registry) or `.` are skipped.
|
||||
(Non-shared strategies — `doxa-region.js`, `affinity-block.js`, `resource.js` — live in
|
||||
`app-profiles/doxa-research-map/src/colors/` and merge over this set for that map only.)
|
||||
|
||||
## The strategy contract
|
||||
|
||||
Every `<name>.js` file MUST `export default` an object with this shape. Missing
|
||||
`name` is the exact regression card #20 fixed — `listStrategies()` and any UI that
|
||||
labels the strategy read `strategy.name`, so an absent `name` shows up as
|
||||
`undefined` (the map still paints, but the tab/legend label is blank).
|
||||
|
||||
```js
|
||||
export const PROPERTY_KEY = 'myField' // GeoJSON feature property this reads
|
||||
|
||||
export function getColor(properties) { ... } // (props) -> '#rrggbb' — JS-side resolver
|
||||
export function applyColor(options = {}) { ... } // -> Mapbox expression array (head = operator)
|
||||
export const buildColorExpression = applyColor // alias used by the tab-switch handler
|
||||
|
||||
export default {
|
||||
name: 'My Strategy', // REQUIRED — human label. Never omit.
|
||||
propertyKey: PROPERTY_KEY,
|
||||
getColor,
|
||||
applyColor,
|
||||
buildColorExpression,
|
||||
// …any palette/labels you also want on the default export
|
||||
}
|
||||
```
|
||||
library/colors/ a map picks a mode
|
||||
┌──────────────────────────┐ ┌───────────────────────┐
|
||||
│ language-family.js │ │ getColorStrategy( │
|
||||
│ prayer-progress.js │──auto───► │ 'language-family') │──► colored pins
|
||||
│ religion.js engagement.js│ glob │ │
|
||||
│ _registry.js (the scanner)│ └───────────────────────┘
|
||||
└──────────────────────────┘
|
||||
shared set a single map can add/override its own in
|
||||
app-profiles/<map>/src/colors/ (that map only)
|
||||
```
|
||||
|
||||
`applyColor()` must return a valid Mapbox paint expression — an array whose first
|
||||
element is an operator (`'match'`, `'case'`, `'interpolate'`, …). It is fed
|
||||
directly to `circle-color` in `useMapLayers.addLanguageFamilyLayer`.
|
||||
- **Shared here** = reused by every map. **Your map's own** color → put it in your bundle's
|
||||
`src/colors/` (it merges over the shared set, for your map only — never edit the library to
|
||||
recolor one map).
|
||||
- Filename = mode key (`prayer-progress.js` → `prayerProgress`). Files starting with `_`/`.` are skipped.
|
||||
|
||||
## Add a custom strategy (full checklist)
|
||||
|
||||
1. **Create `<name>.js`** next to this file, following the contract above.
|
||||
If you derive color from a string, reuse `generateColorFromString` from
|
||||
`../utils/geoUtils.js` (see `doxa-region.js`).
|
||||
The mode key is auto-derived from the filename (`my-thing.js` → `myThing` /
|
||||
`COLOR_MODES.MY_THING`) — pick the filename so that camelCase matches the mode
|
||||
string you want consumers to pass.
|
||||
2. **Build — it auto-registers.** `_registry.js` globs this folder; your file
|
||||
appears in `COLOR_MODES`, `getColorStrategy`, `listStrategies` automatically.
|
||||
**No `_registry.js` edit needed.** (Just make sure the file has a `export
|
||||
default` with a non-empty `name` — files without a default export are ignored.)
|
||||
3. **Make sure the pin feature carries your `PROPERTY_KEY`.** Pin properties are
|
||||
built in `composables/useMapLayers.js → addLanguageFamilyLayer` (the big
|
||||
`properties: { … }` block). If your field isn't there, the Mapbox expression
|
||||
reads `undefined` and every pin falls to the default color.
|
||||
4. **Wire the legend** (only if the map shows a legend tab for this mode) in
|
||||
`composables/legends/useLegendData.js`: add a `TITLE_KEYS` entry, an `activeFilter`
|
||||
branch, a `setFilter` branch, and an `items` branch that emits the rows.
|
||||
Religion is the worked example — it reads its palette/labels via the named
|
||||
exports (`PALETTE`, `RELIGION_FAMILIES`, `getReligionFamily`), not the default
|
||||
export, so export those too if the legend needs them.
|
||||
5. **Verify** by running the structural tooling (it resolves every mode, asserts a
|
||||
non-empty `name`, a valid expression array, and a hex `getFeatureColor`):
|
||||
|
||||
```
|
||||
node /tmp/colorstrat-verify.mjs # see card #20; copy into a repo test if you want it permanent
|
||||
```
|
||||
|
||||
## Profile-local strategies (override / add without touching the library)
|
||||
|
||||
This folder is the **shared** set — a strategy here is reused across profiles. A single
|
||||
map that wants its OWN coloring should NOT edit the library. Instead, drop a `<name>.js`
|
||||
in that bundle's `app-profiles/<bundle>/src/colors/` folder. At bundle init,
|
||||
`index.js` calls `registerStrategies(import.meta.glob('./src/colors/*.js', { eager:
|
||||
true }))`, which merges the bundle's local files **over** this shared set:
|
||||
|
||||
- **Same mode key** (derived from filename) as a shared strategy → the local file
|
||||
**overrides** it, for that bundle only.
|
||||
- **New mode key** → the local file **adds** a bundle-private strategy.
|
||||
|
||||
Each bundle builds as its own IIFE, so a local strategy affects only that bundle — it
|
||||
cannot leak into another map. This is the "local by default, promote deliberately" seam:
|
||||
keep a color local until a second profile genuinely needs it, then promote it here. See
|
||||
`../../contributing/COLORS.md` and `app-profiles/template-bundle/src/colors/`
|
||||
for the copy-me example.
|
||||
|
||||
## Status / custom modes
|
||||
|
||||
`COLOR_MODES.STATUS` and `COLOR_MODES.CUSTOM` are declared but have no dedicated
|
||||
strategy file — `getColorStrategy()` falls back to `LANGUAGE_FAMILY` for any
|
||||
unknown/unregistered mode. Add a real file + registration (steps above) when one
|
||||
of these graduates from placeholder to a real coloring.
|
||||
**Writing or overriding a strategy?** The full contract (required exports, legend wiring, checklist)
|
||||
is in **[../../docs/architecture/color-strategies.md](../../docs/architecture/color-strategies.md)**.
|
||||
|
|
|
|||
Loading…
Reference in a new issue