@diagrammo/dgmo 0.20.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advanced.cjs +331 -109
- package/dist/advanced.d.cts +66 -25
- package/dist/advanced.d.ts +66 -25
- package/dist/advanced.js +331 -109
- package/dist/auto.cjs +334 -107
- package/dist/auto.js +109 -109
- package/dist/auto.mjs +334 -107
- package/dist/cli.cjs +151 -151
- package/dist/editor.cjs +5 -2
- package/dist/editor.js +5 -2
- package/dist/highlight.cjs +5 -2
- package/dist/highlight.js +5 -2
- package/dist/index.cjs +328 -104
- package/dist/index.js +328 -104
- package/dist/internal.cjs +331 -109
- package/dist/internal.d.cts +66 -25
- package/dist/internal.d.ts +66 -25
- package/dist/internal.js +331 -109
- package/gallery/fixtures/map-choropleth.dgmo +7 -7
- package/gallery/fixtures/map-pois.dgmo +4 -4
- package/gallery/fixtures/map-region-scope.dgmo +8 -8
- package/gallery/fixtures/map-route.dgmo +5 -6
- package/package.json +1 -1
- package/src/completion.ts +9 -4
- package/src/editor/keywords.ts +5 -2
- package/src/map/layout.ts +148 -67
- package/src/map/parser.ts +122 -33
- package/src/map/renderer.ts +13 -6
- package/src/map/resolved-types.ts +13 -2
- package/src/map/resolver.ts +179 -34
- package/src/map/types.ts +39 -14
- package/src/utils/reserved-key-registry.ts +7 -7
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
map US Sales by State
|
|
2
2
|
region us-states
|
|
3
|
-
metric Sales ($M)
|
|
3
|
+
region-metric Sales ($M)
|
|
4
4
|
|
|
5
|
-
California
|
|
6
|
-
Texas
|
|
7
|
-
New York
|
|
8
|
-
Florida
|
|
9
|
-
Washington
|
|
10
|
-
Colorado
|
|
5
|
+
California value: 92
|
|
6
|
+
Texas value: 78
|
|
7
|
+
New York value: 64
|
|
8
|
+
Florida value: 51
|
|
9
|
+
Washington value: 40
|
|
10
|
+
Colorado value: 30
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
map Data Center Footprint
|
|
2
|
-
|
|
2
|
+
poi-metric Requests/s
|
|
3
3
|
|
|
4
|
-
poi Denver as hub
|
|
5
|
-
poi Dallas
|
|
6
|
-
poi Seattle
|
|
4
|
+
poi Denver as hub value: 90
|
|
5
|
+
poi Dallas value: 320
|
|
6
|
+
poi Seattle value: 180
|
|
7
7
|
|
|
8
8
|
hub -> Dallas
|
|
9
9
|
hub -> Seattle
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
map Region Scope Disambiguation
|
|
2
2
|
region us-states
|
|
3
|
-
metric Sales ($M)
|
|
3
|
+
region-metric Sales ($M)
|
|
4
4
|
region-labels abbrev
|
|
5
5
|
subtitle Pin a country/state name clash by ISO code (US-GA) or name + scope (Georgia US)
|
|
6
6
|
|
|
7
|
-
California
|
|
8
|
-
Texas
|
|
7
|
+
California value: 92
|
|
8
|
+
Texas value: 78
|
|
9
9
|
// "Georgia" clashes with the country GE — pin the state. Both lines are
|
|
10
10
|
// equivalent; use whichever reads best:
|
|
11
|
-
// terse ISO code: US-GA
|
|
12
|
-
// name + scope: Georgia US
|
|
13
|
-
US-GA
|
|
14
|
-
Florida
|
|
15
|
-
Washington
|
|
11
|
+
// terse ISO code: US-GA value: 64
|
|
12
|
+
// name + scope: Georgia US value: 64
|
|
13
|
+
US-GA value: 64
|
|
14
|
+
Florida value: 51
|
|
15
|
+
Washington value: 40
|
package/package.json
CHANGED
package/src/completion.ts
CHANGED
|
@@ -509,7 +509,7 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
509
509
|
[
|
|
510
510
|
'map',
|
|
511
511
|
// Geographic map directives (§24B.2/.7). `poi`/`route` are content
|
|
512
|
-
// keywords, not directives; metadata keys (
|
|
512
|
+
// keywords, not directives; metadata keys (value/label/style) live in the
|
|
513
513
|
// reserved-key registry.
|
|
514
514
|
withGlobals({
|
|
515
515
|
region: {
|
|
@@ -521,9 +521,14 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
521
521
|
description: 'Override the auto projection',
|
|
522
522
|
values: ['equirectangular', 'natural-earth', 'albers-usa', 'mercator'],
|
|
523
523
|
},
|
|
524
|
-
metric: { description: 'Label for the region
|
|
525
|
-
'
|
|
526
|
-
|
|
524
|
+
'region-metric': { description: 'Label for the region value ramp' },
|
|
525
|
+
'poi-metric': {
|
|
526
|
+
description: 'Label for the POI value (marker size) channel',
|
|
527
|
+
},
|
|
528
|
+
'flow-metric': {
|
|
529
|
+
description: 'Label for the edge/leg value (thickness) channel',
|
|
530
|
+
},
|
|
531
|
+
scale: { description: 'Override value ramp anchors: scale <min> <max>' },
|
|
527
532
|
'region-labels': {
|
|
528
533
|
description: 'Subdivision name labels',
|
|
529
534
|
values: ['full', 'abbrev', 'off'],
|
package/src/editor/keywords.ts
CHANGED
|
@@ -153,13 +153,16 @@ export const DIRECTIVE_KEYWORDS = new Set([
|
|
|
153
153
|
// Map (§24B) directives
|
|
154
154
|
'region',
|
|
155
155
|
'projection',
|
|
156
|
-
'metric',
|
|
157
|
-
'
|
|
156
|
+
'region-metric',
|
|
157
|
+
'poi-metric',
|
|
158
|
+
'flow-metric',
|
|
158
159
|
'region-labels',
|
|
159
160
|
'poi-labels',
|
|
160
161
|
'default-country',
|
|
161
162
|
'default-state',
|
|
162
163
|
'no-legend',
|
|
164
|
+
'muted',
|
|
165
|
+
'natural',
|
|
163
166
|
'subtitle',
|
|
164
167
|
'caption',
|
|
165
168
|
'poi',
|
package/src/map/layout.ts
CHANGED
|
@@ -73,6 +73,21 @@ const RIVER_WIDTH = 1.3; // px stroke width for river lines
|
|
|
73
73
|
// a clear gray rather than sinking into the dark background.
|
|
74
74
|
const FOREIGN_TINT_LIGHT = 30;
|
|
75
75
|
const FOREIGN_TINT_DARK = 62;
|
|
76
|
+
// MUTED basemap — used when a colouring dimension (score ramp or a tag group) is
|
|
77
|
+
// active. The data hues may themselves be blue or green (e.g. `Core blue`,
|
|
78
|
+
// `Growth teal`), which collide with the decorative blue-water / green-land
|
|
79
|
+
// dress: a blue region vanishes into the ocean, a green one into the land. So
|
|
80
|
+
// when regions carry the data signal the basemap RECEDES to neutral grays —
|
|
81
|
+
// water and unscored/neighbour land become low-saturation gray, leaving the data
|
|
82
|
+
// fills as the only saturated thing on the map (the cartographic norm for a
|
|
83
|
+
// choropleth). Plain reference maps with no data keep the blue/green dress.
|
|
84
|
+
// Light land is left at the page bg (cleanest white ground for the data hues);
|
|
85
|
+
// dark land lifts off the near-black surface so dark-mixed tints stay legible.
|
|
86
|
+
const MUTED_WATER_LIGHT = 14; // % gray of bg — pale sea
|
|
87
|
+
const MUTED_WATER_DARK = 10;
|
|
88
|
+
const MUTED_FOREIGN_LIGHT = 28; // neighbour land — grayer than the sea
|
|
89
|
+
const MUTED_FOREIGN_DARK = 16;
|
|
90
|
+
const MUTED_LAND_DARK = 24; // subject land on dark (light land = palette.bg)
|
|
76
91
|
const COLO_R = 9; // spiderfy radius
|
|
77
92
|
const GOLDEN_ANGLE = 2.399963229728653; // rad (137.5deg) -- even spiral, no random
|
|
78
93
|
const FAN_STEP = 16; // px perpendicular offset between parallel edges
|
|
@@ -86,9 +101,9 @@ export interface MapLayoutRegion {
|
|
|
86
101
|
readonly label?: string;
|
|
87
102
|
readonly lineNumber: number;
|
|
88
103
|
readonly layer: 'base' | 'country' | 'us-state';
|
|
89
|
-
/** The region's
|
|
104
|
+
/** The region's value (if any) — emitted as `data-value` so the app can
|
|
90
105
|
* highlight by gradient-scrub proximity. */
|
|
91
|
-
readonly
|
|
106
|
+
readonly value?: number;
|
|
92
107
|
/** The region's tag values keyed by group (lowercased) — emitted as
|
|
93
108
|
* `data-tag-<group>` so the app can highlight on legend-entry hover. */
|
|
94
109
|
readonly tags?: Readonly<Record<string, string>>;
|
|
@@ -119,6 +134,9 @@ export interface MapLayoutPoi {
|
|
|
119
134
|
readonly implicit: boolean;
|
|
120
135
|
readonly isOrigin: boolean; // route origin -> distinct marker
|
|
121
136
|
readonly routeNumber?: number; // route stop badge
|
|
137
|
+
/** Tag values keyed by lowercased group name — emitted as `data-tag-<group>`
|
|
138
|
+
* so the app can spotlight markers on legend-entry hover (mirrors regions). */
|
|
139
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
/** A drawn connector -- an edge or a route leg (same geometry contract). */
|
|
@@ -276,19 +294,37 @@ const US_NON_CONUS = new Set([
|
|
|
276
294
|
|
|
277
295
|
/** The map's water / backdrop colour for a palette — the single source of truth
|
|
278
296
|
* shared by the renderer's `<rect>` fill and any host wrapper that needs to
|
|
279
|
-
* match it (so letterbox gaps around the SVG don't show a stray band).
|
|
280
|
-
|
|
297
|
+
* match it (so letterbox gaps around the SVG don't show a stray band). When
|
|
298
|
+
* `dataActive` (a score ramp or tag group is colouring regions) the sea recedes
|
|
299
|
+
* to a pale neutral so blue/green data hues don't blend into it. */
|
|
300
|
+
export function mapBackgroundColor(
|
|
301
|
+
palette: PaletteColors,
|
|
302
|
+
isDark = false,
|
|
303
|
+
dataActive = false
|
|
304
|
+
): string {
|
|
305
|
+
if (dataActive)
|
|
306
|
+
return mix(
|
|
307
|
+
palette.colors.gray,
|
|
308
|
+
palette.bg,
|
|
309
|
+
isDark ? MUTED_WATER_DARK : MUTED_WATER_LIGHT
|
|
310
|
+
);
|
|
281
311
|
return mix(palette.colors.blue, palette.bg, WATER_TINT);
|
|
282
312
|
}
|
|
283
313
|
|
|
284
|
-
/** The map's neutral (unscored/untagged) LAND colour — the
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
314
|
+
/** The map's neutral (unscored/untagged) LAND colour — the base every region
|
|
315
|
+
* blends from. Exported so a host can DIM a region to plain land (rather than
|
|
316
|
+
* lowering opacity, which would let the water show through and make the shape
|
|
317
|
+
* read as ocean). Matches the layout's `neutralFill`. Green reference dress by
|
|
318
|
+
* default; neutral (page bg on light, lifted gray on dark) when `dataActive`. */
|
|
288
319
|
export function mapNeutralLandColor(
|
|
289
320
|
palette: PaletteColors,
|
|
290
|
-
isDark: boolean
|
|
321
|
+
isDark: boolean,
|
|
322
|
+
dataActive = false
|
|
291
323
|
): string {
|
|
324
|
+
if (dataActive)
|
|
325
|
+
return isDark
|
|
326
|
+
? mix(palette.colors.gray, palette.bg, MUTED_LAND_DARK)
|
|
327
|
+
: palette.bg;
|
|
292
328
|
return mix(
|
|
293
329
|
palette.colors.green,
|
|
294
330
|
palette.bg,
|
|
@@ -344,21 +380,9 @@ export function layoutMap(
|
|
|
344
380
|
}
|
|
345
381
|
const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
|
|
346
382
|
|
|
347
|
-
// Land is a muted green; the ocean/backdrop is blue. Scored/tagged regions
|
|
348
|
-
// paint over the land base, and the score ramp blends FROM the land colour so
|
|
349
|
-
// low scores stay land-toned rather than fading out. In a US view the world
|
|
350
|
-
// layer is just neighbour context (Mexico/Canada at the frame edge) — fill it
|
|
351
|
-
// gray so the green US reads as the subject; world maps (no us-states layer)
|
|
352
|
-
// keep green land for every country.
|
|
353
|
-
const landTint = isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT;
|
|
354
|
-
const neutralFill = mix(palette.colors.green, palette.bg, landTint);
|
|
355
|
-
const water = mapBackgroundColor(palette);
|
|
356
383
|
const usContext = usLayer !== null;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
palette.bg,
|
|
360
|
-
isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
|
|
361
|
-
);
|
|
384
|
+
// Basemap fills (`water` / `neutralFill` / `foreignFill`) depend on whether a
|
|
385
|
+
// colouring dimension is active — defined below, once `activeGroup` is known.
|
|
362
386
|
// Region borders: a clearly dark outline in BOTH themes. palette.text flips
|
|
363
387
|
// (dark on light, light on dark), so mix toward whichever of text/bg is the
|
|
364
388
|
// dark one — never a light hairline over the land fills.
|
|
@@ -367,29 +391,30 @@ export function layoutMap(
|
|
|
367
391
|
: mix(palette.text, palette.bg, 78); // light theme: near-text dark outline
|
|
368
392
|
|
|
369
393
|
// -- Region fill model (choropleth + categorical; AR4/AR6) --
|
|
370
|
-
const
|
|
371
|
-
.filter((r) => r.
|
|
372
|
-
.map((r) => r.
|
|
394
|
+
const values = resolved.regions
|
|
395
|
+
.filter((r) => r.value !== undefined)
|
|
396
|
+
.map((r) => r.value!);
|
|
373
397
|
const scaleOverride = resolved.directives.scale;
|
|
374
|
-
const rampMin = scaleOverride ? scaleOverride.min : Math.min(...
|
|
375
|
-
const rampMax = scaleOverride ? scaleOverride.max : Math.max(...
|
|
376
|
-
//
|
|
398
|
+
const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
|
|
399
|
+
const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
|
|
400
|
+
// Value ramp is red so valued regions stand out against the blue water
|
|
377
401
|
// (palette.primary is a blue in most palettes and would blend in).
|
|
378
402
|
const rampHue = palette.colors.red;
|
|
379
|
-
const hasRamp =
|
|
403
|
+
const hasRamp = values.length > 0;
|
|
380
404
|
|
|
381
|
-
// Colouring dimension (AR4, bivariate): the
|
|
382
|
-
// mutually-exclusive selectable groups. `
|
|
383
|
-
// (the metric label, or "
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
405
|
+
// Colouring dimension (AR4, bivariate): the value ramp and each tag group are
|
|
406
|
+
// mutually-exclusive selectable groups. `VALUE_NAME` is the ramp's group name
|
|
407
|
+
// (the region-metric label, or "Value"). Exactly one dimension is active and
|
|
408
|
+
// drives every region's fill. The value ramp is the default-active dimension
|
|
409
|
+
// whenever any region has a value (the old `active-tag score` token is gone —
|
|
410
|
+
// there is nothing to force; selecting a tag group is what `active-tag` does).
|
|
411
|
+
const VALUE_NAME = hasRamp
|
|
412
|
+
? resolved.directives.regionMetric?.trim() || 'Value'
|
|
387
413
|
: null;
|
|
388
414
|
const matchColorGroup = (v: string): string | null => {
|
|
389
415
|
const lv = v.trim().toLowerCase();
|
|
390
416
|
if (lv === 'none') return null;
|
|
391
|
-
if (
|
|
392
|
-
return SCORE_NAME;
|
|
417
|
+
if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
|
|
393
418
|
const tg = resolved.tagGroups.find((g) => g.name.toLowerCase() === lv);
|
|
394
419
|
return tg ? tg.name : v; // unknown name passes through → renders neutral
|
|
395
420
|
};
|
|
@@ -400,13 +425,41 @@ export function layoutMap(
|
|
|
400
425
|
} else if (resolved.directives.activeTag !== undefined) {
|
|
401
426
|
activeGroup = matchColorGroup(resolved.directives.activeTag);
|
|
402
427
|
} else {
|
|
403
|
-
// Default: colour by
|
|
404
|
-
//
|
|
428
|
+
// Default: colour by the value ramp when values exist, else the first
|
|
429
|
+
// declared tag group.
|
|
405
430
|
activeGroup =
|
|
406
|
-
|
|
431
|
+
VALUE_NAME ??
|
|
407
432
|
(resolved.tagGroups.length > 0 ? resolved.tagGroups[0]!.name : null);
|
|
408
433
|
}
|
|
409
|
-
const activeIsScore =
|
|
434
|
+
const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
|
|
435
|
+
|
|
436
|
+
// Basemap dress. When a colouring dimension is active the regions carry the
|
|
437
|
+
// signal, so the sea/land recede to neutral grays (the data hues — which may be
|
|
438
|
+
// blue or green — would otherwise blend into a blue ocean / green land). A
|
|
439
|
+
// plain reference map (no score, no tag → activeGroup null) keeps the blue
|
|
440
|
+
// water + green land. The bare `muted` / `natural` flags force either dress
|
|
441
|
+
// regardless (so two maps in a deck can match); absent → this auto rule. In a
|
|
442
|
+
// US view the surrounding world layer is always recessive gray so the US reads
|
|
443
|
+
// as the subject.
|
|
444
|
+
const mutedBasemap =
|
|
445
|
+
resolved.directives.basemapStyle === 'muted'
|
|
446
|
+
? true
|
|
447
|
+
: resolved.directives.basemapStyle === 'natural'
|
|
448
|
+
? false
|
|
449
|
+
: activeGroup !== null;
|
|
450
|
+
const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
|
|
451
|
+
const water = mapBackgroundColor(palette, isDark, mutedBasemap);
|
|
452
|
+
const foreignFill = mix(
|
|
453
|
+
palette.colors.gray,
|
|
454
|
+
palette.bg,
|
|
455
|
+
mutedBasemap
|
|
456
|
+
? isDark
|
|
457
|
+
? MUTED_FOREIGN_DARK
|
|
458
|
+
: MUTED_FOREIGN_LIGHT
|
|
459
|
+
: isDark
|
|
460
|
+
? FOREIGN_TINT_DARK
|
|
461
|
+
: FOREIGN_TINT_LIGHT
|
|
462
|
+
);
|
|
410
463
|
|
|
411
464
|
// Score ramp base: a NEUTRAL tint of the page, NOT the (green) land colour —
|
|
412
465
|
// blending red toward green produced muddy brown mid-tones that blurred into
|
|
@@ -415,7 +468,7 @@ export function layoutMap(
|
|
|
415
468
|
// off the near-black surface so the lowest scores read as a clear muted red
|
|
416
469
|
// rather than sinking to maroon-black.
|
|
417
470
|
const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
|
|
418
|
-
const
|
|
471
|
+
const fillForValue = (s: number): string => {
|
|
419
472
|
const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
|
|
420
473
|
const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
|
|
421
474
|
return mix(rampHue, rampBase, pct);
|
|
@@ -451,14 +504,14 @@ export function layoutMap(
|
|
|
451
504
|
};
|
|
452
505
|
|
|
453
506
|
/** A region's fill under the ACTIVE colouring dimension (AR4, bivariate):
|
|
454
|
-
*
|
|
455
|
-
* active → that group's tag colour, neutral otherwise (
|
|
507
|
+
* value-active → ramp for valued regions, neutral otherwise; a tag group
|
|
508
|
+
* active → that group's tag colour, neutral otherwise (value ignored). */
|
|
456
509
|
const regionFill = (r: {
|
|
457
|
-
|
|
510
|
+
value?: number;
|
|
458
511
|
tags: Readonly<Record<string, string>>;
|
|
459
512
|
}): string => {
|
|
460
513
|
if (activeIsScore) {
|
|
461
|
-
return r.
|
|
514
|
+
return r.value !== undefined ? fillForValue(r.value) : neutralFill;
|
|
462
515
|
}
|
|
463
516
|
return tagFill(r.tags, activeGroup) ?? neutralFill;
|
|
464
517
|
};
|
|
@@ -778,7 +831,7 @@ export function layoutMap(
|
|
|
778
831
|
stroke: regionStroke,
|
|
779
832
|
lineNumber,
|
|
780
833
|
layer: 'us-state',
|
|
781
|
-
...(r?.
|
|
834
|
+
...(r?.value !== undefined && { value: r.value }),
|
|
782
835
|
...(r && Object.keys(r.tags).length > 0 && { tags: r.tags }),
|
|
783
836
|
});
|
|
784
837
|
const ctr = geoPath(proj).centroid(f as never);
|
|
@@ -976,6 +1029,13 @@ export function layoutMap(
|
|
|
976
1029
|
// redundant US country polygon underneath it (it only adds a coarser base
|
|
977
1030
|
// and a doubled outline).
|
|
978
1031
|
if (layerKind === 'country' && usContext && iso === 'US') continue;
|
|
1032
|
+
// Antarctica is omitted from the world basemap. The natural-earth world
|
|
1033
|
+
// frame is clamped to ~-58°N and global views take the stretch path (no
|
|
1034
|
+
// clipExtent), so AQ's -90° geometry projects below the frame and spills
|
|
1035
|
+
// out the bottom of the canvas as a distorted strip. Data world maps omit
|
|
1036
|
+
// Antarctica by convention anyway. Keep it only if explicitly referenced.
|
|
1037
|
+
if (layerKind === 'country' && iso === 'AQ' && !regionById.has('AQ'))
|
|
1038
|
+
continue;
|
|
979
1039
|
const r = regionById.get(iso);
|
|
980
1040
|
// Cull off-view land in a regional view; in a global view keep all land
|
|
981
1041
|
// but still drop antimeridian frame-fillers (Fiji et al.).
|
|
@@ -1005,7 +1065,7 @@ export function layoutMap(
|
|
|
1005
1065
|
lineNumber,
|
|
1006
1066
|
layer,
|
|
1007
1067
|
...(label !== undefined && { label }),
|
|
1008
|
-
...(isThisLayer && r.
|
|
1068
|
+
...(isThisLayer && r.value !== undefined && { value: r.value }),
|
|
1009
1069
|
...(isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }),
|
|
1010
1070
|
});
|
|
1011
1071
|
}
|
|
@@ -1060,14 +1120,14 @@ export function layoutMap(
|
|
|
1060
1120
|
}
|
|
1061
1121
|
}
|
|
1062
1122
|
|
|
1063
|
-
// -- POIs: project, size-scale, co-located spiderfy --
|
|
1123
|
+
// -- POIs: project, value→size-scale, co-located spiderfy --
|
|
1064
1124
|
const sizeVals = resolved.pois
|
|
1065
|
-
.map((p) => Number(p.meta['
|
|
1125
|
+
.map((p) => Number(p.meta['value']))
|
|
1066
1126
|
.filter((n) => Number.isFinite(n) && n > 0);
|
|
1067
1127
|
const sizeMin = sizeVals.length ? Math.min(...sizeVals) : 0;
|
|
1068
1128
|
const sizeMax = sizeVals.length ? Math.max(...sizeVals) : 0;
|
|
1069
1129
|
const radiusFor = (p: ResolvedPoi): number => {
|
|
1070
|
-
const v = Number(p.meta['
|
|
1130
|
+
const v = Number(p.meta['value']);
|
|
1071
1131
|
if (!Number.isFinite(v) || v <= 0 || sizeMax <= 0) return R_DEFAULT;
|
|
1072
1132
|
// sqrt so AREA encodes the value
|
|
1073
1133
|
const t =
|
|
@@ -1153,6 +1213,7 @@ export function layoutMap(
|
|
|
1153
1213
|
implicit: !!e.p.implicit,
|
|
1154
1214
|
isOrigin: originIds.has(e.p.id),
|
|
1155
1215
|
...(num !== undefined && { routeNumber: num }),
|
|
1216
|
+
...(Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }),
|
|
1156
1217
|
});
|
|
1157
1218
|
});
|
|
1158
1219
|
}
|
|
@@ -1201,31 +1262,50 @@ export function layoutMap(
|
|
|
1201
1262
|
return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
|
|
1202
1263
|
};
|
|
1203
1264
|
|
|
1204
|
-
// Routes:
|
|
1265
|
+
// Routes: each leg is an edge (fromId → toId) carrying its own label,
|
|
1266
|
+
// value→thickness, and arc shape. Loop-closing legs are explicit in `rt.legs`;
|
|
1267
|
+
// the origin is never double-marked because `stopIds` is unique.
|
|
1268
|
+
const routeLegVals = resolved.routes
|
|
1269
|
+
.flatMap((rt) => rt.legs)
|
|
1270
|
+
.map((l) => Number(l.value))
|
|
1271
|
+
.filter((n) => Number.isFinite(n) && n > 0);
|
|
1272
|
+
const rlMin = routeLegVals.length ? Math.min(...routeLegVals) : 0;
|
|
1273
|
+
const rlMax = routeLegVals.length ? Math.max(...routeLegVals) : 0;
|
|
1274
|
+
const routeWidthFor = (v: number): number => {
|
|
1275
|
+
if (!Number.isFinite(v) || v <= 0 || rlMax <= 0) return W_MIN;
|
|
1276
|
+
const t = rlMax > rlMin ? (v - rlMin) / (rlMax - rlMin) : 1;
|
|
1277
|
+
return W_MIN + t * (W_MAX - W_MIN);
|
|
1278
|
+
};
|
|
1205
1279
|
for (const rt of resolved.routes) {
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
const
|
|
1209
|
-
const b = poiScreen.get(rt.stopIds[i]!);
|
|
1280
|
+
for (const leg of rt.legs) {
|
|
1281
|
+
const a = poiScreen.get(leg.fromId);
|
|
1282
|
+
const b = poiScreen.get(leg.toId);
|
|
1210
1283
|
if (!a || !b) continue;
|
|
1284
|
+
const mx = (a.cx + b.cx) / 2;
|
|
1285
|
+
const my = (a.cy + b.cy) / 2;
|
|
1211
1286
|
legs.push({
|
|
1212
|
-
d: legPath(a, b,
|
|
1213
|
-
width:
|
|
1287
|
+
d: legPath(a, b, leg.style === 'arc', 0),
|
|
1288
|
+
width: routeWidthFor(Number(leg.value)),
|
|
1214
1289
|
color: mix(palette.text, palette.bg, 72),
|
|
1215
1290
|
arrow: true,
|
|
1216
|
-
lineNumber:
|
|
1291
|
+
lineNumber: leg.lineNumber,
|
|
1292
|
+
...(leg.label !== undefined && {
|
|
1293
|
+
label: leg.label,
|
|
1294
|
+
labelX: mx,
|
|
1295
|
+
labelY: my - 4,
|
|
1296
|
+
}),
|
|
1217
1297
|
});
|
|
1218
1298
|
}
|
|
1219
1299
|
}
|
|
1220
1300
|
|
|
1221
1301
|
// Edges: group by unordered endpoint pair for deterministic fan-out (AR9).
|
|
1222
1302
|
const weightVals = resolved.edges
|
|
1223
|
-
.map((e) => Number(e.meta['
|
|
1303
|
+
.map((e) => Number(e.meta['value']))
|
|
1224
1304
|
.filter((n) => Number.isFinite(n) && n > 0);
|
|
1225
1305
|
const wMin = weightVals.length ? Math.min(...weightVals) : 0;
|
|
1226
1306
|
const wMax = weightVals.length ? Math.max(...weightVals) : 0;
|
|
1227
1307
|
const widthFor = (e: ResolvedEdge): number => {
|
|
1228
|
-
const v = Number(e.meta['
|
|
1308
|
+
const v = Number(e.meta['value']);
|
|
1229
1309
|
if (!Number.isFinite(v) || v <= 0 || wMax <= 0) return W_MIN;
|
|
1230
1310
|
const t = wMax > wMin ? (v - wMin) / (wMax - wMin) : 1;
|
|
1231
1311
|
return W_MIN + t * (W_MAX - W_MIN);
|
|
@@ -1559,17 +1639,18 @@ export function layoutMap(
|
|
|
1559
1639
|
name: g.name,
|
|
1560
1640
|
entries: g.entries.map((e) => ({ value: e.value, color: e.color })),
|
|
1561
1641
|
}));
|
|
1562
|
-
// Only the colouring dimensions (
|
|
1563
|
-
// POI size and edge
|
|
1564
|
-
// intentionally carry no key
|
|
1642
|
+
// Only the colouring dimensions (value ramp + tag groups) get a legend.
|
|
1643
|
+
// POI size and edge thickness are self-evident from the marker/line scale and
|
|
1644
|
+
// intentionally carry no key (the poi-metric/flow-metric labels are captured
|
|
1645
|
+
// for future use but not rendered as legend keys in v1).
|
|
1565
1646
|
if (tagGroups.length > 0 || hasRamp) {
|
|
1566
1647
|
legend = {
|
|
1567
1648
|
tagGroups,
|
|
1568
1649
|
activeGroup,
|
|
1569
1650
|
...(hasRamp && {
|
|
1570
1651
|
ramp: {
|
|
1571
|
-
...(resolved.directives.
|
|
1572
|
-
metric: resolved.directives.
|
|
1652
|
+
...(resolved.directives.regionMetric !== undefined && {
|
|
1653
|
+
metric: resolved.directives.regionMetric,
|
|
1573
1654
|
}),
|
|
1574
1655
|
min: rampMin,
|
|
1575
1656
|
max: rampMax,
|