@diagrammo/dgmo 0.21.0 → 0.21.1

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.
Files changed (43) hide show
  1. package/dist/advanced.cjs +556 -195
  2. package/dist/advanced.js +555 -195
  3. package/dist/auto.cjs +322 -196
  4. package/dist/auto.js +113 -113
  5. package/dist/auto.mjs +322 -196
  6. package/dist/cli.cjs +156 -156
  7. package/dist/editor.cjs +1 -0
  8. package/dist/editor.js +1 -0
  9. package/dist/highlight.cjs +1 -0
  10. package/dist/highlight.js +1 -0
  11. package/dist/index.cjs +320 -195
  12. package/dist/index.js +320 -195
  13. package/dist/internal.cjs +556 -195
  14. package/dist/internal.js +555 -195
  15. package/dist/map-data/PROVENANCE.json +1 -1
  16. package/dist/map-data/mountain-ranges.json +1 -0
  17. package/docs/language-reference.md +27 -25
  18. package/gallery/fixtures/map-direct-color.dgmo +10 -0
  19. package/package.json +1 -1
  20. package/src/advanced.ts +14 -0
  21. package/src/completion.ts +1 -0
  22. package/src/d3.ts +15 -9
  23. package/src/editor/keywords.ts +1 -0
  24. package/src/map/data/PROVENANCE.json +1 -1
  25. package/src/map/data/mountain-ranges.json +1 -0
  26. package/src/map/geo-query.ts +277 -0
  27. package/src/map/geo.ts +258 -1
  28. package/src/map/invert.ts +111 -0
  29. package/src/map/layout.ts +233 -113
  30. package/src/map/load-data.ts +7 -1
  31. package/src/map/parser.ts +22 -2
  32. package/src/map/renderer.ts +44 -0
  33. package/src/map/resolved-types.ts +8 -0
  34. package/src/map/resolver.ts +40 -19
  35. package/src/map/types.ts +18 -0
  36. package/dist/advanced.d.cts +0 -5331
  37. package/dist/advanced.d.ts +0 -5331
  38. package/dist/auto.d.cts +0 -39
  39. package/dist/auto.d.ts +0 -39
  40. package/dist/index.d.cts +0 -336
  41. package/dist/index.d.ts +0 -336
  42. package/dist/internal.d.cts +0 -5331
  43. package/dist/internal.d.ts +0 -5331
package/dist/index.js CHANGED
@@ -15966,10 +15966,13 @@ function parseMap(content) {
15966
15966
  );
15967
15967
  d.projection = value;
15968
15968
  break;
15969
- case "region-metric":
15969
+ case "region-metric": {
15970
15970
  dup(d.regionMetric);
15971
- d.regionMetric = value;
15971
+ const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
15972
+ d.regionMetric = rmLabel;
15973
+ if (rmColor) d.regionMetricColor = rmColor;
15972
15974
  break;
15975
+ }
15973
15976
  case "poi-metric":
15974
15977
  dup(d.poiMetric);
15975
15978
  d.poiMetric = value;
@@ -16018,6 +16021,12 @@ function parseMap(content) {
16018
16021
  case "no-legend":
16019
16022
  d.noLegend = true;
16020
16023
  break;
16024
+ case "no-insets":
16025
+ d.noInsets = true;
16026
+ break;
16027
+ case "relief":
16028
+ d.relief = true;
16029
+ break;
16021
16030
  case "muted":
16022
16031
  case "natural":
16023
16032
  if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
@@ -16130,6 +16139,7 @@ function parseMap(content) {
16130
16139
  };
16131
16140
  if (regionScope !== void 0) region.scope = regionScope;
16132
16141
  if (valueNum !== void 0) region.value = valueNum;
16142
+ if (split.color) region.color = split.color;
16133
16143
  regions.push(region);
16134
16144
  }
16135
16145
  function handlePoi(rest, line12, indent) {
@@ -16154,6 +16164,7 @@ function parseMap(content) {
16154
16164
  const poi = { pos, tags, meta, lineNumber: line12 };
16155
16165
  if (split.alias) poi.alias = split.alias;
16156
16166
  if (label !== void 0) poi.label = label;
16167
+ if (split.color) poi.color = split.color;
16157
16168
  pois.push(poi);
16158
16169
  open.poi = { poi, indent };
16159
16170
  }
@@ -16358,6 +16369,8 @@ var init_parser12 = __esm({
16358
16369
  "default-state",
16359
16370
  "active-tag",
16360
16371
  "no-legend",
16372
+ "no-insets",
16373
+ "relief",
16361
16374
  "subtitle",
16362
16375
  "caption"
16363
16376
  ]);
@@ -45556,7 +45569,7 @@ var init_renderer15 = __esm({
45556
45569
 
45557
45570
  // src/map/geo.ts
45558
45571
  import { feature } from "topojson-client";
45559
- import { geoBounds } from "d3-geo";
45572
+ import { geoBounds, geoArea } from "d3-geo";
45560
45573
  function geomObject(topo) {
45561
45574
  const key = Object.keys(topo.objects)[0];
45562
45575
  return topo.objects[key];
@@ -45584,6 +45597,74 @@ function featureBbox(topo, geomId) {
45584
45597
  [b[1][0], b[1][1]]
45585
45598
  ];
45586
45599
  }
45600
+ function explodePolygons(gj) {
45601
+ const g = gj.geometry ?? gj;
45602
+ const t = g.type;
45603
+ const coords = g.coordinates;
45604
+ if (t === "Polygon") {
45605
+ return [
45606
+ { type: "Feature", geometry: { type: "Polygon", coordinates: coords } }
45607
+ ];
45608
+ }
45609
+ if (t === "MultiPolygon") {
45610
+ return coords.map((rings) => ({
45611
+ type: "Feature",
45612
+ geometry: { type: "Polygon", coordinates: rings }
45613
+ }));
45614
+ }
45615
+ return [];
45616
+ }
45617
+ function bboxGap(a, b) {
45618
+ const lonGap = Math.max(0, a[0][0] - b[1][0], b[0][0] - a[1][0]);
45619
+ const latGap = Math.max(0, a[0][1] - b[1][1], b[0][1] - a[1][1]);
45620
+ return Math.max(lonGap, latGap);
45621
+ }
45622
+ function featureBboxPrimary(topo, geomId) {
45623
+ const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45624
+ if (!geom) return null;
45625
+ const gj = feature(topo, geom);
45626
+ const parts = explodePolygons(gj);
45627
+ if (parts.length <= 1) return featureBbox(topo, geomId);
45628
+ const polys = parts.map((p) => {
45629
+ const b = geoBounds(p);
45630
+ if (!b || !Number.isFinite(b[0][0])) return null;
45631
+ const wraps = b[1][0] < b[0][0];
45632
+ const bbox = [
45633
+ [b[0][0], b[0][1]],
45634
+ [b[1][0], b[1][1]]
45635
+ ];
45636
+ return { bbox, area: geoArea(p), wraps };
45637
+ }).filter(
45638
+ (p) => p !== null
45639
+ );
45640
+ if (polys.length <= 1 || polys.some((p) => p.wraps))
45641
+ return featureBbox(topo, geomId);
45642
+ const maxArea = Math.max(...polys.map((p) => p.area));
45643
+ const anchor = polys.find((p) => p.area === maxArea);
45644
+ const cluster = [
45645
+ [anchor.bbox[0][0], anchor.bbox[0][1]],
45646
+ [anchor.bbox[1][0], anchor.bbox[1][1]]
45647
+ ];
45648
+ const remaining = polys.filter((p) => p !== anchor);
45649
+ let added = true;
45650
+ while (added) {
45651
+ added = false;
45652
+ for (let i = remaining.length - 1; i >= 0; i--) {
45653
+ const p = remaining[i];
45654
+ const near = bboxGap(p.bbox, cluster) <= DETACH_GAP_DEG;
45655
+ const large = p.area >= DETACH_AREA_FRAC * maxArea;
45656
+ if (near || large) {
45657
+ cluster[0][0] = Math.min(cluster[0][0], p.bbox[0][0]);
45658
+ cluster[0][1] = Math.min(cluster[0][1], p.bbox[0][1]);
45659
+ cluster[1][0] = Math.max(cluster[1][0], p.bbox[1][0]);
45660
+ cluster[1][1] = Math.max(cluster[1][1], p.bbox[1][1]);
45661
+ remaining.splice(i, 1);
45662
+ added = true;
45663
+ }
45664
+ }
45665
+ }
45666
+ return cluster;
45667
+ }
45587
45668
  function unionExtent(boxes, points) {
45588
45669
  const lats = [];
45589
45670
  const lons = [];
@@ -45622,11 +45703,13 @@ function unionLongitudes(lons) {
45622
45703
  }
45623
45704
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45624
45705
  }
45625
- var fold;
45706
+ var fold, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45626
45707
  var init_geo = __esm({
45627
45708
  "src/map/geo.ts"() {
45628
45709
  "use strict";
45629
45710
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
45711
+ DETACH_GAP_DEG = 10;
45712
+ DETACH_AREA_FRAC = 0.25;
45630
45713
  }
45631
45714
  });
45632
45715
 
@@ -45730,12 +45813,12 @@ function resolveMap(parsed, data) {
45730
45813
  chosen = { ...inState, layer: "us-state" };
45731
45814
  } else {
45732
45815
  chosen = { ...inCountry, layer: "country" };
45816
+ warn(
45817
+ r.lineNumber,
45818
+ `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
45819
+ "W_MAP_REGION_AMBIGUOUS"
45820
+ );
45733
45821
  }
45734
- warn(
45735
- r.lineNumber,
45736
- `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
45737
- "W_MAP_REGION_AMBIGUOUS"
45738
- );
45739
45822
  } else if (inState) {
45740
45823
  chosen = { ...inState, layer: "us-state" };
45741
45824
  } else if (inCountry) {
@@ -45759,6 +45842,7 @@ function resolveMap(parsed, data) {
45759
45842
  name: chosen.name,
45760
45843
  layer: chosen.layer,
45761
45844
  ...r.value !== void 0 && { value: r.value },
45845
+ ...r.color !== void 0 && { color: r.color },
45762
45846
  tags: r.tags,
45763
45847
  meta: r.meta,
45764
45848
  lineNumber: r.lineNumber
@@ -45900,6 +45984,7 @@ function resolveMap(parsed, data) {
45900
45984
  lat,
45901
45985
  lon,
45902
45986
  ...p.label !== void 0 && { label: p.label },
45987
+ ...p.color !== void 0 && { color: p.color },
45903
45988
  tags: p.tags,
45904
45989
  meta: p.meta,
45905
45990
  lineNumber: p.lineNumber
@@ -46042,7 +46127,7 @@ function resolveMap(parsed, data) {
46042
46127
  }
46043
46128
  for (const r of regions) {
46044
46129
  if (r.layer === "country") {
46045
- const bb = featureBbox(data.worldCoarse, r.iso);
46130
+ const bb = featureBboxPrimary(data.worldCoarse, r.iso);
46046
46131
  if (bb) regionBoxes.push(bb);
46047
46132
  }
46048
46133
  }
@@ -46056,6 +46141,7 @@ function resolveMap(parsed, data) {
46056
46141
  const lonSpan = extent2[1][0] - extent2[0][0];
46057
46142
  const latSpan = extent2[1][1] - extent2[0][1];
46058
46143
  const span = Math.max(lonSpan, latSpan);
46144
+ const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46059
46145
  const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46060
46146
  let projection;
46061
46147
  const override = parsed.directives.projection;
@@ -46063,12 +46149,10 @@ function resolveMap(parsed, data) {
46063
46149
  projection = override;
46064
46150
  } else if (usDominant) {
46065
46151
  projection = "albers-usa";
46066
- } else if (span > WORLD_SPAN) {
46152
+ } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46067
46153
  projection = "equirectangular";
46068
- } else if (span < MERCATOR_MAX_SPAN) {
46069
- projection = "mercator";
46070
46154
  } else {
46071
- projection = "equirectangular";
46155
+ projection = "mercator";
46072
46156
  }
46073
46157
  if (lonSpan >= 180) {
46074
46158
  extent2 = [
@@ -46122,14 +46206,14 @@ function firstError(diags) {
46122
46206
  const e = diags.find((d) => d.severity === "error");
46123
46207
  return e ? formatDgmoError(e) : null;
46124
46208
  }
46125
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46209
+ var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46126
46210
  var init_resolver2 = __esm({
46127
46211
  "src/map/resolver.ts"() {
46128
46212
  "use strict";
46129
46213
  init_diagnostics();
46130
46214
  init_geo();
46131
46215
  WORLD_SPAN = 90;
46132
- MERCATOR_MAX_SPAN = 25;
46216
+ MERCATOR_MAX_LAT = 80;
46133
46217
  PAD_FRACTION = 0.05;
46134
46218
  WORLD_LAT_SOUTH = -58;
46135
46219
  WORLD_LAT_NORTH = 78;
@@ -46210,114 +46294,6 @@ var init_resolver2 = __esm({
46210
46294
  }
46211
46295
  });
46212
46296
 
46213
- // src/map/load-data.ts
46214
- var load_data_exports = {};
46215
- __export(load_data_exports, {
46216
- loadMapData: () => loadMapData
46217
- });
46218
- async function loadNodeBuiltins() {
46219
- const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
46220
- import("fs/promises"),
46221
- import("url"),
46222
- import("path")
46223
- ]);
46224
- return { readFile, fileURLToPath, dirname, resolve };
46225
- }
46226
- async function readJson(nb, dir, name) {
46227
- return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
46228
- }
46229
- async function firstExistingDir(nb, baseDir) {
46230
- for (const rel of CANDIDATE_DIRS) {
46231
- const dir = nb.resolve(baseDir, rel);
46232
- try {
46233
- await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
46234
- return dir;
46235
- } catch {
46236
- }
46237
- }
46238
- throw new Error(
46239
- `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
46240
- );
46241
- }
46242
- function validate(data) {
46243
- const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
46244
- if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
46245
- throw new Error("map data assets are malformed (failed shape validation)");
46246
- }
46247
- return data;
46248
- }
46249
- function moduleBaseDir(nb) {
46250
- try {
46251
- const url = import.meta.url;
46252
- if (url) return nb.dirname(nb.fileURLToPath(url));
46253
- } catch {
46254
- }
46255
- if (typeof __dirname !== "undefined") return __dirname;
46256
- return process.cwd();
46257
- }
46258
- function loadMapData() {
46259
- cache ??= (async () => {
46260
- const nb = await loadNodeBuiltins();
46261
- const dir = await firstExistingDir(nb, moduleBaseDir(nb));
46262
- const [
46263
- worldCoarse,
46264
- worldDetail,
46265
- usStates,
46266
- lakes,
46267
- rivers,
46268
- naLand,
46269
- naLakes,
46270
- gazetteer
46271
- ] = await Promise.all([
46272
- readJson(nb, dir, FILES.worldCoarse),
46273
- readJson(nb, dir, FILES.worldDetail),
46274
- readJson(nb, dir, FILES.usStates),
46275
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
46276
- readJson(nb, dir, FILES.lakes).catch(() => void 0),
46277
- readJson(nb, dir, FILES.rivers).catch(() => void 0),
46278
- readJson(nb, dir, FILES.naLand).catch(() => void 0),
46279
- readJson(nb, dir, FILES.naLakes).catch(() => void 0),
46280
- readJson(nb, dir, FILES.gazetteer)
46281
- ]);
46282
- return validate({
46283
- worldCoarse,
46284
- worldDetail,
46285
- usStates,
46286
- gazetteer,
46287
- ...lakes && { lakes },
46288
- ...rivers && { rivers },
46289
- ...naLand && { naLand },
46290
- ...naLakes && { naLakes }
46291
- });
46292
- })().catch((e) => {
46293
- cache = void 0;
46294
- throw e;
46295
- });
46296
- return cache;
46297
- }
46298
- var FILES, CANDIDATE_DIRS, cache;
46299
- var init_load_data = __esm({
46300
- "src/map/load-data.ts"() {
46301
- "use strict";
46302
- FILES = {
46303
- worldCoarse: "world-coarse.json",
46304
- worldDetail: "world-detail.json",
46305
- usStates: "us-states.json",
46306
- lakes: "lakes.json",
46307
- rivers: "rivers.json",
46308
- naLand: "na-land.json",
46309
- naLakes: "na-lakes.json",
46310
- gazetteer: "gazetteer.json"
46311
- };
46312
- CANDIDATE_DIRS = [
46313
- "./data",
46314
- "./map-data",
46315
- "../map-data",
46316
- "../src/map/data"
46317
- ];
46318
- }
46319
- });
46320
-
46321
46297
  // src/map/layout.ts
46322
46298
  import {
46323
46299
  geoPath,
@@ -46354,18 +46330,14 @@ function projectionFor(family) {
46354
46330
  return geoEquirectangular();
46355
46331
  }
46356
46332
  }
46357
- function mapBackgroundColor(palette, isDark = false, dataActive = false) {
46358
- if (dataActive)
46359
- return mix(
46360
- palette.colors.gray,
46361
- palette.bg,
46362
- isDark ? MUTED_WATER_DARK : MUTED_WATER_LIGHT
46363
- );
46364
- return mix(palette.colors.blue, palette.bg, WATER_TINT);
46333
+ function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
46334
+ return mix(
46335
+ palette.colors.blue,
46336
+ palette.bg,
46337
+ isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
46338
+ );
46365
46339
  }
46366
- function mapNeutralLandColor(palette, isDark, dataActive = false) {
46367
- if (dataActive)
46368
- return isDark ? mix(palette.colors.gray, palette.bg, MUTED_LAND_DARK) : palette.bg;
46340
+ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46369
46341
  return mix(
46370
46342
  palette.colors.green,
46371
46343
  palette.bg,
@@ -46397,7 +46369,7 @@ function layoutMap(resolved, data, size, opts) {
46397
46369
  const scaleOverride = resolved.directives.scale;
46398
46370
  const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46399
46371
  const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
46400
- const rampHue = palette.colors.red;
46372
+ const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46401
46373
  const hasRamp = values.length > 0;
46402
46374
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
46403
46375
  const matchColorGroup = (v) => {
@@ -46420,6 +46392,7 @@ function layoutMap(resolved, data, size, opts) {
46420
46392
  const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
46421
46393
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46422
46394
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
46395
+ const lakeStroke = mix(regionStroke, water, 45);
46423
46396
  const foreignFill = mix(
46424
46397
  palette.colors.gray,
46425
46398
  palette.bg,
@@ -46449,7 +46422,14 @@ function layoutMap(resolved, data, size, opts) {
46449
46422
  isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46450
46423
  );
46451
46424
  };
46425
+ const directFill = (name) => {
46426
+ const hex = name ? resolveColor(name, palette) : null;
46427
+ if (!hex) return null;
46428
+ return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
46429
+ };
46452
46430
  const regionFill = (r) => {
46431
+ const direct = directFill(r.color);
46432
+ if (direct) return direct;
46453
46433
  if (activeIsScore) {
46454
46434
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46455
46435
  }
@@ -46503,6 +46483,7 @@ function layoutMap(resolved, data, size, opts) {
46503
46483
  const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46504
46484
  let path;
46505
46485
  let project;
46486
+ let stretchParams = null;
46506
46487
  if (fitIsGlobal) {
46507
46488
  const cb = geoPath(projection).bounds(fitTarget);
46508
46489
  const bx0 = cb[0][0];
@@ -46513,6 +46494,7 @@ function layoutMap(resolved, data, size, opts) {
46513
46494
  const oy = fitBox[0][1];
46514
46495
  const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46515
46496
  const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
46497
+ stretchParams = { sx, sy, ox, oy, bx0, by0 };
46516
46498
  const stretch = (x, y) => [
46517
46499
  ox + (x - bx0) * sx,
46518
46500
  oy + (y - by0) * sy
@@ -46544,7 +46526,7 @@ function layoutMap(resolved, data, size, opts) {
46544
46526
  const insets = [];
46545
46527
  const insetRegions = [];
46546
46528
  const insetLabelSeeds = [];
46547
- if (resolved.projection === "albers-usa" && usLayer) {
46529
+ if (resolved.projection === "albers-usa" && usLayer && !resolved.directives.noInsets) {
46548
46530
  const PAD = 8;
46549
46531
  const GAP = 12;
46550
46532
  const yB = height - FIT_PAD;
@@ -46575,38 +46557,14 @@ function layoutMap(resolved, data, size, opts) {
46575
46557
  }
46576
46558
  return y;
46577
46559
  };
46578
- const coastTop = (x0, xr) => {
46560
+ const coastFloor = (x0, xr) => {
46579
46561
  const n = 24;
46580
- const pts = [];
46581
46562
  let maxY = -Infinity;
46582
46563
  for (let i = 0; i <= n; i++) {
46583
- const x = x0 + (xr - x0) * i / n;
46584
- const y = at(x);
46585
- if (y > -Infinity) {
46586
- pts.push([x, y]);
46587
- if (y > maxY) maxY = y;
46588
- }
46589
- }
46590
- if (pts.length === 0) return () => yB - height * 0.42;
46591
- let m = 0;
46592
- if (pts.length >= 2) {
46593
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
46594
- for (const [x, y] of pts) {
46595
- sx += x;
46596
- sy += y;
46597
- sxx += x * x;
46598
- sxy += x * y;
46599
- }
46600
- const den = pts.length * sxx - sx * sx;
46601
- if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46602
- }
46603
- m = Math.max(-0.35, Math.min(0.35, m));
46604
- let c = -Infinity;
46605
- for (const [x, y] of pts) {
46606
- const need = y - m * x + GAP;
46607
- if (need > c) c = need;
46608
- }
46609
- return (x) => m * x + c;
46564
+ const y = at(x0 + (xr - x0) * i / n);
46565
+ if (y > maxY) maxY = y;
46566
+ }
46567
+ return maxY;
46610
46568
  };
46611
46569
  const placeInset = (iso, proj, boxX, iwReq) => {
46612
46570
  const f = usLayer.get(iso);
@@ -46615,19 +46573,15 @@ function layoutMap(resolved, data, size, opts) {
46615
46573
  const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46616
46574
  if (iw < 24) return boxX;
46617
46575
  const xr = x0 + iw + 2 * PAD;
46618
- const top = coastTop(x0, xr);
46619
- const yL = top(x0);
46620
- const yR = top(xr);
46576
+ const floor = coastFloor(x0, xr);
46577
+ const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
46621
46578
  proj.fitWidth(iw, f);
46622
46579
  const bb = geoPath(proj).bounds(f);
46623
46580
  const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46624
46581
  const needH = sh + 2 * PAD;
46625
- let topFit = Math.max(yL, yR);
46582
+ let topFit = topGuess;
46626
46583
  const bottom = Math.min(topFit + needH, yB);
46627
46584
  if (bottom - topFit < needH) topFit = bottom - needH;
46628
- const lift = topFit - Math.max(yL, yR);
46629
- const topL = yL + lift;
46630
- const topR = yR + lift;
46631
46585
  proj.fitExtent(
46632
46586
  [
46633
46587
  [x0 + PAD, topFit + PAD],
@@ -46646,15 +46600,18 @@ function layoutMap(resolved, data, size, opts) {
46646
46600
  }
46647
46601
  insets.push({
46648
46602
  x: x0,
46649
- y: Math.min(topL, topR),
46603
+ y: topFit,
46650
46604
  w: xr - x0,
46651
- h: bottom - Math.min(topL, topR),
46605
+ h: bottom - topFit,
46652
46606
  points: [
46653
- [x0, topL],
46654
- [xr, topR],
46607
+ [x0, topFit],
46608
+ [xr, topFit],
46655
46609
  [xr, bottom],
46656
46610
  [x0, bottom]
46657
- ]
46611
+ ],
46612
+ // The FITTED inset projection (just fit to this box) — captured so the
46613
+ // geo-query can invert pixels inside the frame back to AK/HI coords.
46614
+ projection: proj
46658
46615
  });
46659
46616
  insetRegions.push({
46660
46617
  id: iso,
@@ -46824,13 +46781,40 @@ function layoutMap(resolved, data, size, opts) {
46824
46781
  id: "lake",
46825
46782
  d,
46826
46783
  fill: water,
46827
- stroke: "none",
46784
+ stroke: lakeStroke,
46828
46785
  lineNumber: -1,
46829
46786
  layer: "base"
46830
46787
  });
46831
46788
  }
46832
46789
  }
46833
- const riverColor = water;
46790
+ const relief = [];
46791
+ let reliefHatch = null;
46792
+ if (resolved.directives.relief === true && data.mountainRanges) {
46793
+ for (const [, f] of decodeLayer(data.mountainRanges)) {
46794
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46795
+ if (!viewF) continue;
46796
+ const area2 = path.area(viewF);
46797
+ if (!Number.isFinite(area2) || area2 < RELIEF_MIN_AREA) continue;
46798
+ const box = path.bounds(viewF);
46799
+ if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
46800
+ continue;
46801
+ const d = path(viewF) ?? "";
46802
+ if (!d) continue;
46803
+ relief.push({ d });
46804
+ }
46805
+ if (relief.length) {
46806
+ const darkTone = isDark ? palette.bg : palette.text;
46807
+ const lightTone = isDark ? palette.text : palette.bg;
46808
+ const landLum = relativeLuminance(neutralFill);
46809
+ const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
46810
+ reliefHatch = {
46811
+ color: mix(tone, neutralFill, RELIEF_HATCH_STRENGTH),
46812
+ spacing: RELIEF_HATCH_SPACING,
46813
+ width: RELIEF_HATCH_WIDTH
46814
+ };
46815
+ }
46816
+ }
46817
+ const riverColor = mix(water, regionStroke, 16);
46834
46818
  const rivers = [];
46835
46819
  if (data.rivers) {
46836
46820
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46851,6 +46835,9 @@ function layoutMap(resolved, data, size, opts) {
46851
46835
  return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
46852
46836
  };
46853
46837
  const poiFill = (p) => {
46838
+ const directHex = p.color ? resolveColor(p.color, palette) : null;
46839
+ if (directHex)
46840
+ return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
46854
46841
  for (const group of resolved.tagGroups) {
46855
46842
  const val = p.tags[group.name.toLowerCase()];
46856
46843
  if (!val) continue;
@@ -47266,19 +47253,24 @@ function layoutMap(resolved, data, size, opts) {
47266
47253
  ...resolved.caption !== void 0 && { caption: resolved.caption },
47267
47254
  regions,
47268
47255
  rivers,
47256
+ relief,
47257
+ reliefHatch,
47269
47258
  legs,
47270
47259
  pois,
47271
47260
  labels,
47272
47261
  legend,
47273
47262
  insets,
47274
- insetRegions
47263
+ insetRegions,
47264
+ projection,
47265
+ stretch: stretchParams
47275
47266
  };
47276
47267
  }
47277
- var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT, RIVER_WIDTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_WATER_LIGHT, MUTED_WATER_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, MUTED_LAND_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
47268
+ var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
47278
47269
  var init_layout15 = __esm({
47279
47270
  "src/map/layout.ts"() {
47280
47271
  "use strict";
47281
47272
  init_color_utils();
47273
+ init_colors();
47282
47274
  init_label_layout();
47283
47275
  init_legend_constants();
47284
47276
  init_title_constants();
@@ -47291,19 +47283,22 @@ var init_layout15 = __esm({
47291
47283
  W_MAX = 8;
47292
47284
  FONT = 11;
47293
47285
  COLO_EPS = 1.5;
47294
- LAND_TINT_LIGHT = 58;
47295
- LAND_TINT_DARK = 75;
47286
+ LAND_TINT_LIGHT = 12;
47287
+ LAND_TINT_DARK = 24;
47296
47288
  TAG_TINT_LIGHT = 60;
47297
47289
  TAG_TINT_DARK = 68;
47298
- WATER_TINT = 55;
47290
+ WATER_TINT_LIGHT = 13;
47291
+ WATER_TINT_DARK = 14;
47299
47292
  RIVER_WIDTH = 1.3;
47293
+ RELIEF_MIN_AREA = 12;
47294
+ RELIEF_MIN_DIM = 2;
47295
+ RELIEF_HATCH_SPACING = 3;
47296
+ RELIEF_HATCH_WIDTH = 0.25;
47297
+ RELIEF_HATCH_STRENGTH = 32;
47300
47298
  FOREIGN_TINT_LIGHT = 30;
47301
47299
  FOREIGN_TINT_DARK = 62;
47302
- MUTED_WATER_LIGHT = 14;
47303
- MUTED_WATER_DARK = 10;
47304
47300
  MUTED_FOREIGN_LIGHT = 28;
47305
47301
  MUTED_FOREIGN_DARK = 16;
47306
- MUTED_LAND_DARK = 24;
47307
47302
  COLO_R = 9;
47308
47303
  GOLDEN_ANGLE = 2.399963229728653;
47309
47304
  FAN_STEP = 16;
@@ -47376,6 +47371,20 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47376
47371
  }
47377
47372
  };
47378
47373
  for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
47374
+ if (layout.relief.length && layout.reliefHatch) {
47375
+ const h = layout.reliefHatch;
47376
+ const rangeClipId = "dgmo-relief-clip";
47377
+ const landClipId = "dgmo-relief-land";
47378
+ const rangeClip = defs.append("clipPath").attr("id", rangeClipId);
47379
+ for (const s of layout.relief) rangeClip.append("path").attr("d", s.d);
47380
+ const landClip = defs.append("clipPath").attr("id", landClipId);
47381
+ for (const r of layout.regions)
47382
+ if (r.id !== "lake") landClip.append("path").attr("d", r.d);
47383
+ const gRelief = svg.append("g").attr("clip-path", `url(#${landClipId})`).append("g").attr("class", "dgmo-map-relief").attr("clip-path", `url(#${rangeClipId})`).attr("stroke", h.color).attr("stroke-width", h.width).attr("vector-effect", "non-scaling-stroke");
47384
+ for (let y = h.spacing; y < height; y += h.spacing) {
47385
+ gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
47386
+ }
47387
+ }
47379
47388
  if (layout.rivers.length) {
47380
47389
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47381
47390
  for (const r of layout.rivers) {
@@ -47500,7 +47509,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47500
47509
  }
47501
47510
  }
47502
47511
  if (layout.title) {
47503
- svg.append("text").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 4).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.title);
47512
+ svg.append("text").attr("class", "dgmo-map-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 4).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.title);
47504
47513
  }
47505
47514
  if (layout.subtitle) {
47506
47515
  svg.append("text").attr("x", width / 2).attr("y", TITLE_Y + TITLE_FONT_SIZE).attr("text-anchor", "middle").attr("font-size", LABEL_FONT + 1).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.subtitle);
@@ -47532,6 +47541,120 @@ var init_renderer16 = __esm({
47532
47541
  }
47533
47542
  });
47534
47543
 
47544
+ // src/map/load-data.ts
47545
+ var load_data_exports = {};
47546
+ __export(load_data_exports, {
47547
+ loadMapData: () => loadMapData
47548
+ });
47549
+ async function loadNodeBuiltins() {
47550
+ const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
47551
+ import("fs/promises"),
47552
+ import("url"),
47553
+ import("path")
47554
+ ]);
47555
+ return { readFile, fileURLToPath, dirname, resolve };
47556
+ }
47557
+ async function readJson(nb, dir, name) {
47558
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
47559
+ }
47560
+ async function firstExistingDir(nb, baseDir) {
47561
+ for (const rel of CANDIDATE_DIRS) {
47562
+ const dir = nb.resolve(baseDir, rel);
47563
+ try {
47564
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
47565
+ return dir;
47566
+ } catch {
47567
+ }
47568
+ }
47569
+ throw new Error(
47570
+ `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
47571
+ );
47572
+ }
47573
+ function validate(data) {
47574
+ const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
47575
+ if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
47576
+ throw new Error("map data assets are malformed (failed shape validation)");
47577
+ }
47578
+ return data;
47579
+ }
47580
+ function moduleBaseDir(nb) {
47581
+ try {
47582
+ const url = import.meta.url;
47583
+ if (url) return nb.dirname(nb.fileURLToPath(url));
47584
+ } catch {
47585
+ }
47586
+ if (typeof __dirname !== "undefined") return __dirname;
47587
+ return process.cwd();
47588
+ }
47589
+ function loadMapData() {
47590
+ cache ??= (async () => {
47591
+ const nb = await loadNodeBuiltins();
47592
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
47593
+ const [
47594
+ worldCoarse,
47595
+ worldDetail,
47596
+ usStates,
47597
+ lakes,
47598
+ rivers,
47599
+ mountainRanges,
47600
+ naLand,
47601
+ naLakes,
47602
+ gazetteer
47603
+ ] = await Promise.all([
47604
+ readJson(nb, dir, FILES.worldCoarse),
47605
+ readJson(nb, dir, FILES.worldDetail),
47606
+ readJson(nb, dir, FILES.usStates),
47607
+ // Lakes/rivers/mountain/NA assets are optional — older bundles may predate them.
47608
+ readJson(nb, dir, FILES.lakes).catch(() => void 0),
47609
+ readJson(nb, dir, FILES.rivers).catch(() => void 0),
47610
+ readJson(nb, dir, FILES.mountainRanges).catch(
47611
+ () => void 0
47612
+ ),
47613
+ readJson(nb, dir, FILES.naLand).catch(() => void 0),
47614
+ readJson(nb, dir, FILES.naLakes).catch(() => void 0),
47615
+ readJson(nb, dir, FILES.gazetteer)
47616
+ ]);
47617
+ return validate({
47618
+ worldCoarse,
47619
+ worldDetail,
47620
+ usStates,
47621
+ gazetteer,
47622
+ ...lakes && { lakes },
47623
+ ...rivers && { rivers },
47624
+ ...mountainRanges && { mountainRanges },
47625
+ ...naLand && { naLand },
47626
+ ...naLakes && { naLakes }
47627
+ });
47628
+ })().catch((e) => {
47629
+ cache = void 0;
47630
+ throw e;
47631
+ });
47632
+ return cache;
47633
+ }
47634
+ var FILES, CANDIDATE_DIRS, cache;
47635
+ var init_load_data = __esm({
47636
+ "src/map/load-data.ts"() {
47637
+ "use strict";
47638
+ FILES = {
47639
+ worldCoarse: "world-coarse.json",
47640
+ worldDetail: "world-detail.json",
47641
+ usStates: "us-states.json",
47642
+ lakes: "lakes.json",
47643
+ rivers: "rivers.json",
47644
+ mountainRanges: "mountain-ranges.json",
47645
+ naLand: "na-land.json",
47646
+ naLakes: "na-lakes.json",
47647
+ gazetteer: "gazetteer.json"
47648
+ };
47649
+ CANDIDATE_DIRS = [
47650
+ "./data",
47651
+ "./map-data",
47652
+ "../map-data",
47653
+ "../src/map/data"
47654
+ ];
47655
+ }
47656
+ });
47657
+
47535
47658
  // src/pyramid/renderer.ts
47536
47659
  var renderer_exports17 = {};
47537
47660
  __export(renderer_exports17, {
@@ -55660,15 +55783,17 @@ async function renderForExport(content, theme, palette, viewState, options) {
55660
55783
  if (detectedType === "map") {
55661
55784
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55662
55785
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55663
- const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55664
55786
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
55665
55787
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55666
55788
  const mapParsed = parseMap2(content);
55667
- let mapData;
55668
- try {
55669
- mapData = await loadMapData2();
55670
- } catch {
55671
- return "";
55789
+ let mapData = options?.mapData;
55790
+ if (!mapData) {
55791
+ const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55792
+ try {
55793
+ mapData = await loadMapData2();
55794
+ } catch {
55795
+ return "";
55796
+ }
55672
55797
  }
55673
55798
  const mapResolved = resolveMap2(mapParsed, mapData);
55674
55799
  const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);