@diagrammo/dgmo 0.27.0 → 0.28.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.
@@ -29,6 +29,11 @@ export interface CountryCandidate {
29
29
  /** Projected screen anchor `[x, y]` (mainland anchor or `path.centroid`), or
30
30
  * null when the feature doesn't project to a finite point. */
31
31
  readonly anchor: readonly [number, number] | null;
32
+ /** True when `anchor` came from a curated WORLD_LABEL_ANCHORS entry (a trusted
33
+ * mainland point) rather than the area-weighted centroid. Such a country
34
+ * bypasses the both-axes smear gate (its full-canvas bbox is an antimeridian
35
+ * artifact, not a real footprint, so the unreliable-centroid concern is moot). */
36
+ readonly curatedAnchor?: boolean;
32
37
  }
33
38
 
34
39
  export interface ContextLabelArgs {
@@ -102,10 +107,10 @@ export function labelBudget(
102
107
  band: TierBand
103
108
  ): number {
104
109
  const bandCap: Record<TierBand, number> = {
105
- world: 6,
106
- continental: 5,
107
- regional: 4,
108
- local: 3,
110
+ world: 7,
111
+ continental: 6,
112
+ regional: 5,
113
+ local: 4,
109
114
  };
110
115
  const area = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
111
116
  return Math.max(0, Math.min(area, bandCap[band]));
@@ -347,8 +352,11 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
347
352
  letterSpacing: WATER_LETTER_SPACING,
348
353
  color: waterColor,
349
354
  fontSize: FONT, // water names keep the base font (no footprint to scale on)
350
- // Water before any country (×1000), then by tier, then kind, then name.
351
- sort: tier * 10 + KIND_ORDER[kind],
355
+ // Orientation-value bands (lower = earlier): MAJOR water (oceans + major
356
+ // seas, tier ≤ 1) leads at `tier*10+kind` (0..~16); MINOR water (tier ≥ 2:
357
+ // smaller seas, bays, gulfs, straits) drops to band 2 (2000+) — BELOW the
358
+ // country band (1000+), so a big country outranks a minor basin.
359
+ sort: (tier <= 1 ? 0 : 2000) + tier * 10 + KIND_ORDER[kind],
352
360
  });
353
361
  }
354
362
 
@@ -369,15 +377,24 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
369
377
  let ci = 0;
370
378
  for (const r of ranked) {
371
379
  const { c, w, h, area } = r;
372
- // F2: an antimeridian-crossing / global-smear country yields a near-full-
373
- // canvas bbox while its real landmass is split — the `path.centroid` anchor
374
- // is then unreliable (mid-map, wrong basin). Drop such over-wide candidates
375
- // rather than spend a top-priority slot on a mispositioned name.
376
- if (w > width * 0.66 || h > height * 0.66) continue;
380
+ // F2: an antimeridian-crossing / global-smear country fills the canvas in
381
+ // BOTH dimensions while its real landmass is split — the `path.centroid`
382
+ // anchor is then unreliable (mid-map, wrong basin). Reject only that
383
+ // both-axes canvas-smear: a country that is merely wide-but-short (Canada
384
+ // hugging the top of a US frame) or tall-but-narrow (Chile) is a legitimate
385
+ // landmass whose clipExtent-clipped centroid still lands over its own ground,
386
+ // so it must NOT be dropped for footprint shape alone. A `curatedAnchor`
387
+ // (WORLD_LABEL_ANCHORS, e.g. Russia → European Russia) is trustworthy by
388
+ // construction — the smear concern is moot, so it bypasses this gate (the
389
+ // `insideViewport` check below still drops an anchor that projects off-frame).
390
+ if (!c.curatedAnchor && w > width * 0.66 && h > height * 0.66) continue;
377
391
  if (!insideViewport(c.anchor, width, height)) continue;
378
392
  // Footprint-driven scale (Decision: big landmass = large, faded backdrop
379
393
  // name). t∈[0,1] over the [MIN,MAX] linear-fraction band; font ramps up and
380
- // colour fades toward bg in lockstep so a bigger name is also a quieter one.
394
+ // ink rises in lockstep, so a bigger name reads as a large, softly-inked
395
+ // backdrop. A curated-anchor giant (Russia) keeps the standard big-country
396
+ // styling — its full-canvas bbox lands at the top of the ramp (font/ink like
397
+ // any large in-frame country), which is the intended subdued-backdrop look.
381
398
  const sizeFrac = Math.sqrt(area) / canvasLinear;
382
399
  const t = Math.min(
383
400
  1,
@@ -407,8 +424,12 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
407
424
  letterSpacing: 0,
408
425
  color,
409
426
  fontSize,
410
- // Always after every water body (+1e6); larger area = earlier.
411
- sort: 1_000_000 + ci++,
427
+ // Band 1 (orientation-value ranking): above MINOR water (band 2, 2000+) but
428
+ // below MAJOR water — oceans + major seas (band 0, ≤~16). So a big country
429
+ // (US, Canada, Russia) outranks a minor sea/bay (Sargasso, Bahía de
430
+ // Campeche) yet never displaces an ocean. Larger area = earlier within the
431
+ // band (`ci` is the area-desc rank index).
432
+ sort: 1000 + ci++,
412
433
  });
413
434
  }
414
435
 
@@ -416,8 +437,17 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
416
437
  candidates.sort((a, b) => a.sort - b.sort);
417
438
  const placed: PlacedLabel[] = [];
418
439
  const placedRects: LabelRect[] = [];
440
+ // Guarantee country/state room: water can otherwise monopolise a small budget
441
+ // (a coastal view borders many oceans/seas), so reserve up to 2 slots for
442
+ // countries whenever any country candidate exists. No effect on pure-water
443
+ // views (`countryCount` 0 ⇒ cap = budget). Major water still leads by sort, so
444
+ // this only trims the LAST water bodies that would have crowded out a country.
445
+ const countryCount = candidates.reduce((n, c) => n + (c.italic ? 0 : 1), 0);
446
+ const waterCap = budget - Math.min(2, countryCount);
447
+ let waterPlaced = 0;
419
448
  for (const cand of candidates) {
420
449
  if (placed.length >= budget) break;
450
+ if (cand.italic && waterPlaced >= waterCap) continue;
421
451
  const rect = rectAround(
422
452
  cand.cx,
423
453
  cand.cy,
@@ -452,6 +482,7 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
452
482
  if (collides(rect)) continue;
453
483
  if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
454
484
  placedRects.push(rect);
485
+ if (cand.italic) waterPlaced++;
455
486
  placed.push({
456
487
  x: cand.cx,
457
488
  y: cand.cy,
package/src/map/layout.ts CHANGED
@@ -159,6 +159,13 @@ const FONT = 11; // on-map label font px
159
159
  // previously used for hover) mistook the wrapped sliver for half the shape.
160
160
  const WORLD_LABEL_ANCHORS: Record<string, [number, number]> = {
161
161
  US: [-98.5, 39.5], // CONUS geographic centre (near Lebanon, Kansas)
162
+ // Russia crosses the antimeridian (Chukotka at ~170°W), so on a non-global
163
+ // (e.g. Europe) projection its geometry smears across the whole frame and the
164
+ // area-weighted centroid lands mid-map (over Europe) — useless as a label
165
+ // anchor. Pin it to European Russia (~Volga) so a Europe view labels visible
166
+ // western Russia on its eastern margin; on a world view this still sits over
167
+ // Russian land. (See the curated-anchor smear-gate bypass in context-labels.)
168
+ RU: [45, 58],
162
169
  };
163
170
  // POI-cluster hover-only gate (Decision #1). A ≥2-member cluster's callout
164
171
  // column falls back to hover-only labels when it would sprawl or overflow:
@@ -3925,22 +3932,25 @@ export function layoutMap(
3925
3932
  name: (f.properties as { name?: string } | undefined)?.name ?? iso,
3926
3933
  bbox: [x0, y0, x1, y1],
3927
3934
  anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null,
3935
+ curatedAnchor: !!anchorLngLat,
3928
3936
  });
3929
3937
  }
3930
- // Neighbour US states (POI-only region framing): when the frame is snapped to
3931
- // a US-state container (e.g. California), label the surrounding in-frame states
3932
- // (Nevada, Oregon, Arizona…) in the muted context style for orientation. They
3933
- // are NOT containers and NOT data, so the region-label pass skipped them.
3938
+ // Framed US states (POI-only region framing): when the frame is snapped to a
3939
+ // US-state container (e.g. California), label the focus state AND the
3940
+ // surrounding in-frame states (Nevada, Oregon, Arizona…) in the muted context
3941
+ // style for orientation. None are data (the region-label pass skipped them).
3934
3942
  // Anchor each to the centroid of its VISIBLE (culled) geometry so a state only
3935
3943
  // partly in frame (a sliver of Oregon at the top) still anchors on-screen
3936
3944
  // rather than at an off-frame centroid that `insideViewport` would reject.
3945
+ // The focus container IS included (gives the map its headline name) — only a
3946
+ // data-referenced state is skipped, to avoid double-labelling what
3947
+ // region-labels already named.
3937
3948
  const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
3938
3949
  (id) => id.startsWith('US-')
3939
3950
  );
3940
3951
  if (usLayer && framedStateContainers) {
3941
- const containerSet = new Set(resolved.poiFrameContainers);
3942
3952
  for (const [iso, f] of usLayer) {
3943
- if (containerSet.has(iso) || regionById.has(iso)) continue;
3953
+ if (regionById.has(iso)) continue;
3944
3954
  const viewF = cullFeatureToView(f);
3945
3955
  if (!viewF) continue; // not in frame
3946
3956
  const b = path.bounds(viewF as never) as [