@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.cjs CHANGED
@@ -15950,10 +15950,13 @@ function parseMap(content) {
15950
15950
  );
15951
15951
  d.projection = value;
15952
15952
  break;
15953
- case "region-metric":
15953
+ case "region-metric": {
15954
15954
  dup(d.regionMetric);
15955
- d.regionMetric = value;
15955
+ const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
15956
+ d.regionMetric = rmLabel;
15957
+ if (rmColor) d.regionMetricColor = rmColor;
15956
15958
  break;
15959
+ }
15957
15960
  case "poi-metric":
15958
15961
  dup(d.poiMetric);
15959
15962
  d.poiMetric = value;
@@ -16002,6 +16005,12 @@ function parseMap(content) {
16002
16005
  case "no-legend":
16003
16006
  d.noLegend = true;
16004
16007
  break;
16008
+ case "no-insets":
16009
+ d.noInsets = true;
16010
+ break;
16011
+ case "relief":
16012
+ d.relief = true;
16013
+ break;
16005
16014
  case "muted":
16006
16015
  case "natural":
16007
16016
  if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
@@ -16114,6 +16123,7 @@ function parseMap(content) {
16114
16123
  };
16115
16124
  if (regionScope !== void 0) region.scope = regionScope;
16116
16125
  if (valueNum !== void 0) region.value = valueNum;
16126
+ if (split.color) region.color = split.color;
16117
16127
  regions.push(region);
16118
16128
  }
16119
16129
  function handlePoi(rest, line12, indent) {
@@ -16138,6 +16148,7 @@ function parseMap(content) {
16138
16148
  const poi = { pos, tags, meta, lineNumber: line12 };
16139
16149
  if (split.alias) poi.alias = split.alias;
16140
16150
  if (label !== void 0) poi.label = label;
16151
+ if (split.color) poi.color = split.color;
16141
16152
  pois.push(poi);
16142
16153
  open.poi = { poi, indent };
16143
16154
  }
@@ -16342,6 +16353,8 @@ var init_parser12 = __esm({
16342
16353
  "default-state",
16343
16354
  "active-tag",
16344
16355
  "no-legend",
16356
+ "no-insets",
16357
+ "relief",
16345
16358
  "subtitle",
16346
16359
  "caption"
16347
16360
  ]);
@@ -45566,6 +45579,74 @@ function featureBbox(topo, geomId) {
45566
45579
  [b[1][0], b[1][1]]
45567
45580
  ];
45568
45581
  }
45582
+ function explodePolygons(gj) {
45583
+ const g = gj.geometry ?? gj;
45584
+ const t = g.type;
45585
+ const coords = g.coordinates;
45586
+ if (t === "Polygon") {
45587
+ return [
45588
+ { type: "Feature", geometry: { type: "Polygon", coordinates: coords } }
45589
+ ];
45590
+ }
45591
+ if (t === "MultiPolygon") {
45592
+ return coords.map((rings) => ({
45593
+ type: "Feature",
45594
+ geometry: { type: "Polygon", coordinates: rings }
45595
+ }));
45596
+ }
45597
+ return [];
45598
+ }
45599
+ function bboxGap(a, b) {
45600
+ const lonGap = Math.max(0, a[0][0] - b[1][0], b[0][0] - a[1][0]);
45601
+ const latGap = Math.max(0, a[0][1] - b[1][1], b[0][1] - a[1][1]);
45602
+ return Math.max(lonGap, latGap);
45603
+ }
45604
+ function featureBboxPrimary(topo, geomId) {
45605
+ const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45606
+ if (!geom) return null;
45607
+ const gj = (0, import_topojson_client.feature)(topo, geom);
45608
+ const parts = explodePolygons(gj);
45609
+ if (parts.length <= 1) return featureBbox(topo, geomId);
45610
+ const polys = parts.map((p) => {
45611
+ const b = (0, import_d3_geo.geoBounds)(p);
45612
+ if (!b || !Number.isFinite(b[0][0])) return null;
45613
+ const wraps = b[1][0] < b[0][0];
45614
+ const bbox = [
45615
+ [b[0][0], b[0][1]],
45616
+ [b[1][0], b[1][1]]
45617
+ ];
45618
+ return { bbox, area: (0, import_d3_geo.geoArea)(p), wraps };
45619
+ }).filter(
45620
+ (p) => p !== null
45621
+ );
45622
+ if (polys.length <= 1 || polys.some((p) => p.wraps))
45623
+ return featureBbox(topo, geomId);
45624
+ const maxArea = Math.max(...polys.map((p) => p.area));
45625
+ const anchor = polys.find((p) => p.area === maxArea);
45626
+ const cluster = [
45627
+ [anchor.bbox[0][0], anchor.bbox[0][1]],
45628
+ [anchor.bbox[1][0], anchor.bbox[1][1]]
45629
+ ];
45630
+ const remaining = polys.filter((p) => p !== anchor);
45631
+ let added = true;
45632
+ while (added) {
45633
+ added = false;
45634
+ for (let i = remaining.length - 1; i >= 0; i--) {
45635
+ const p = remaining[i];
45636
+ const near = bboxGap(p.bbox, cluster) <= DETACH_GAP_DEG;
45637
+ const large = p.area >= DETACH_AREA_FRAC * maxArea;
45638
+ if (near || large) {
45639
+ cluster[0][0] = Math.min(cluster[0][0], p.bbox[0][0]);
45640
+ cluster[0][1] = Math.min(cluster[0][1], p.bbox[0][1]);
45641
+ cluster[1][0] = Math.max(cluster[1][0], p.bbox[1][0]);
45642
+ cluster[1][1] = Math.max(cluster[1][1], p.bbox[1][1]);
45643
+ remaining.splice(i, 1);
45644
+ added = true;
45645
+ }
45646
+ }
45647
+ }
45648
+ return cluster;
45649
+ }
45569
45650
  function unionExtent(boxes, points) {
45570
45651
  const lats = [];
45571
45652
  const lons = [];
@@ -45604,13 +45685,15 @@ function unionLongitudes(lons) {
45604
45685
  }
45605
45686
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45606
45687
  }
45607
- var import_topojson_client, import_d3_geo, fold;
45688
+ var import_topojson_client, import_d3_geo, fold, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45608
45689
  var init_geo = __esm({
45609
45690
  "src/map/geo.ts"() {
45610
45691
  "use strict";
45611
45692
  import_topojson_client = require("topojson-client");
45612
45693
  import_d3_geo = require("d3-geo");
45613
45694
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
45695
+ DETACH_GAP_DEG = 10;
45696
+ DETACH_AREA_FRAC = 0.25;
45614
45697
  }
45615
45698
  });
45616
45699
 
@@ -45714,12 +45797,12 @@ function resolveMap(parsed, data) {
45714
45797
  chosen = { ...inState, layer: "us-state" };
45715
45798
  } else {
45716
45799
  chosen = { ...inCountry, layer: "country" };
45800
+ warn(
45801
+ r.lineNumber,
45802
+ `"${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}").`,
45803
+ "W_MAP_REGION_AMBIGUOUS"
45804
+ );
45717
45805
  }
45718
- warn(
45719
- r.lineNumber,
45720
- `"${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}").`,
45721
- "W_MAP_REGION_AMBIGUOUS"
45722
- );
45723
45806
  } else if (inState) {
45724
45807
  chosen = { ...inState, layer: "us-state" };
45725
45808
  } else if (inCountry) {
@@ -45743,6 +45826,7 @@ function resolveMap(parsed, data) {
45743
45826
  name: chosen.name,
45744
45827
  layer: chosen.layer,
45745
45828
  ...r.value !== void 0 && { value: r.value },
45829
+ ...r.color !== void 0 && { color: r.color },
45746
45830
  tags: r.tags,
45747
45831
  meta: r.meta,
45748
45832
  lineNumber: r.lineNumber
@@ -45884,6 +45968,7 @@ function resolveMap(parsed, data) {
45884
45968
  lat,
45885
45969
  lon,
45886
45970
  ...p.label !== void 0 && { label: p.label },
45971
+ ...p.color !== void 0 && { color: p.color },
45887
45972
  tags: p.tags,
45888
45973
  meta: p.meta,
45889
45974
  lineNumber: p.lineNumber
@@ -46026,7 +46111,7 @@ function resolveMap(parsed, data) {
46026
46111
  }
46027
46112
  for (const r of regions) {
46028
46113
  if (r.layer === "country") {
46029
- const bb = featureBbox(data.worldCoarse, r.iso);
46114
+ const bb = featureBboxPrimary(data.worldCoarse, r.iso);
46030
46115
  if (bb) regionBoxes.push(bb);
46031
46116
  }
46032
46117
  }
@@ -46040,6 +46125,7 @@ function resolveMap(parsed, data) {
46040
46125
  const lonSpan = extent2[1][0] - extent2[0][0];
46041
46126
  const latSpan = extent2[1][1] - extent2[0][1];
46042
46127
  const span = Math.max(lonSpan, latSpan);
46128
+ const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46043
46129
  const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46044
46130
  let projection;
46045
46131
  const override = parsed.directives.projection;
@@ -46047,12 +46133,10 @@ function resolveMap(parsed, data) {
46047
46133
  projection = override;
46048
46134
  } else if (usDominant) {
46049
46135
  projection = "albers-usa";
46050
- } else if (span > WORLD_SPAN) {
46136
+ } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46051
46137
  projection = "equirectangular";
46052
- } else if (span < MERCATOR_MAX_SPAN) {
46053
- projection = "mercator";
46054
46138
  } else {
46055
- projection = "equirectangular";
46139
+ projection = "mercator";
46056
46140
  }
46057
46141
  if (lonSpan >= 180) {
46058
46142
  extent2 = [
@@ -46106,14 +46190,14 @@ function firstError(diags) {
46106
46190
  const e = diags.find((d) => d.severity === "error");
46107
46191
  return e ? formatDgmoError(e) : null;
46108
46192
  }
46109
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46193
+ var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46110
46194
  var init_resolver2 = __esm({
46111
46195
  "src/map/resolver.ts"() {
46112
46196
  "use strict";
46113
46197
  init_diagnostics();
46114
46198
  init_geo();
46115
46199
  WORLD_SPAN = 90;
46116
- MERCATOR_MAX_SPAN = 25;
46200
+ MERCATOR_MAX_LAT = 80;
46117
46201
  PAD_FRACTION = 0.05;
46118
46202
  WORLD_LAT_SOUTH = -58;
46119
46203
  WORLD_LAT_NORTH = 78;
@@ -46194,115 +46278,6 @@ var init_resolver2 = __esm({
46194
46278
  }
46195
46279
  });
46196
46280
 
46197
- // src/map/load-data.ts
46198
- var load_data_exports = {};
46199
- __export(load_data_exports, {
46200
- loadMapData: () => loadMapData
46201
- });
46202
- async function loadNodeBuiltins() {
46203
- const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
46204
- import("fs/promises"),
46205
- import("url"),
46206
- import("path")
46207
- ]);
46208
- return { readFile, fileURLToPath, dirname, resolve };
46209
- }
46210
- async function readJson(nb, dir, name) {
46211
- return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
46212
- }
46213
- async function firstExistingDir(nb, baseDir) {
46214
- for (const rel of CANDIDATE_DIRS) {
46215
- const dir = nb.resolve(baseDir, rel);
46216
- try {
46217
- await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
46218
- return dir;
46219
- } catch {
46220
- }
46221
- }
46222
- throw new Error(
46223
- `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
46224
- );
46225
- }
46226
- function validate(data) {
46227
- const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
46228
- if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
46229
- throw new Error("map data assets are malformed (failed shape validation)");
46230
- }
46231
- return data;
46232
- }
46233
- function moduleBaseDir(nb) {
46234
- try {
46235
- const url = import_meta.url;
46236
- if (url) return nb.dirname(nb.fileURLToPath(url));
46237
- } catch {
46238
- }
46239
- if (typeof __dirname !== "undefined") return __dirname;
46240
- return process.cwd();
46241
- }
46242
- function loadMapData() {
46243
- cache ??= (async () => {
46244
- const nb = await loadNodeBuiltins();
46245
- const dir = await firstExistingDir(nb, moduleBaseDir(nb));
46246
- const [
46247
- worldCoarse,
46248
- worldDetail,
46249
- usStates,
46250
- lakes,
46251
- rivers,
46252
- naLand,
46253
- naLakes,
46254
- gazetteer
46255
- ] = await Promise.all([
46256
- readJson(nb, dir, FILES.worldCoarse),
46257
- readJson(nb, dir, FILES.worldDetail),
46258
- readJson(nb, dir, FILES.usStates),
46259
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
46260
- readJson(nb, dir, FILES.lakes).catch(() => void 0),
46261
- readJson(nb, dir, FILES.rivers).catch(() => void 0),
46262
- readJson(nb, dir, FILES.naLand).catch(() => void 0),
46263
- readJson(nb, dir, FILES.naLakes).catch(() => void 0),
46264
- readJson(nb, dir, FILES.gazetteer)
46265
- ]);
46266
- return validate({
46267
- worldCoarse,
46268
- worldDetail,
46269
- usStates,
46270
- gazetteer,
46271
- ...lakes && { lakes },
46272
- ...rivers && { rivers },
46273
- ...naLand && { naLand },
46274
- ...naLakes && { naLakes }
46275
- });
46276
- })().catch((e) => {
46277
- cache = void 0;
46278
- throw e;
46279
- });
46280
- return cache;
46281
- }
46282
- var import_meta, FILES, CANDIDATE_DIRS, cache;
46283
- var init_load_data = __esm({
46284
- "src/map/load-data.ts"() {
46285
- "use strict";
46286
- import_meta = {};
46287
- FILES = {
46288
- worldCoarse: "world-coarse.json",
46289
- worldDetail: "world-detail.json",
46290
- usStates: "us-states.json",
46291
- lakes: "lakes.json",
46292
- rivers: "rivers.json",
46293
- naLand: "na-land.json",
46294
- naLakes: "na-lakes.json",
46295
- gazetteer: "gazetteer.json"
46296
- };
46297
- CANDIDATE_DIRS = [
46298
- "./data",
46299
- "./map-data",
46300
- "../map-data",
46301
- "../src/map/data"
46302
- ];
46303
- }
46304
- });
46305
-
46306
46281
  // src/map/layout.ts
46307
46282
  function geomObject2(topo) {
46308
46283
  const key = Object.keys(topo.objects)[0];
@@ -46329,18 +46304,14 @@ function projectionFor(family) {
46329
46304
  return (0, import_d3_geo2.geoEquirectangular)();
46330
46305
  }
46331
46306
  }
46332
- function mapBackgroundColor(palette, isDark = false, dataActive = false) {
46333
- if (dataActive)
46334
- return mix(
46335
- palette.colors.gray,
46336
- palette.bg,
46337
- isDark ? MUTED_WATER_DARK : MUTED_WATER_LIGHT
46338
- );
46339
- return mix(palette.colors.blue, palette.bg, WATER_TINT);
46307
+ function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
46308
+ return mix(
46309
+ palette.colors.blue,
46310
+ palette.bg,
46311
+ isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
46312
+ );
46340
46313
  }
46341
- function mapNeutralLandColor(palette, isDark, dataActive = false) {
46342
- if (dataActive)
46343
- return isDark ? mix(palette.colors.gray, palette.bg, MUTED_LAND_DARK) : palette.bg;
46314
+ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46344
46315
  return mix(
46345
46316
  palette.colors.green,
46346
46317
  palette.bg,
@@ -46372,7 +46343,7 @@ function layoutMap(resolved, data, size, opts) {
46372
46343
  const scaleOverride = resolved.directives.scale;
46373
46344
  const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46374
46345
  const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
46375
- const rampHue = palette.colors.red;
46346
+ const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46376
46347
  const hasRamp = values.length > 0;
46377
46348
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
46378
46349
  const matchColorGroup = (v) => {
@@ -46395,6 +46366,7 @@ function layoutMap(resolved, data, size, opts) {
46395
46366
  const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
46396
46367
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46397
46368
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
46369
+ const lakeStroke = mix(regionStroke, water, 45);
46398
46370
  const foreignFill = mix(
46399
46371
  palette.colors.gray,
46400
46372
  palette.bg,
@@ -46424,7 +46396,14 @@ function layoutMap(resolved, data, size, opts) {
46424
46396
  isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46425
46397
  );
46426
46398
  };
46399
+ const directFill = (name) => {
46400
+ const hex = name ? resolveColor(name, palette) : null;
46401
+ if (!hex) return null;
46402
+ return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
46403
+ };
46427
46404
  const regionFill = (r) => {
46405
+ const direct = directFill(r.color);
46406
+ if (direct) return direct;
46428
46407
  if (activeIsScore) {
46429
46408
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46430
46409
  }
@@ -46478,6 +46457,7 @@ function layoutMap(resolved, data, size, opts) {
46478
46457
  const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46479
46458
  let path;
46480
46459
  let project;
46460
+ let stretchParams = null;
46481
46461
  if (fitIsGlobal) {
46482
46462
  const cb = (0, import_d3_geo2.geoPath)(projection).bounds(fitTarget);
46483
46463
  const bx0 = cb[0][0];
@@ -46488,6 +46468,7 @@ function layoutMap(resolved, data, size, opts) {
46488
46468
  const oy = fitBox[0][1];
46489
46469
  const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46490
46470
  const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
46471
+ stretchParams = { sx, sy, ox, oy, bx0, by0 };
46491
46472
  const stretch = (x, y) => [
46492
46473
  ox + (x - bx0) * sx,
46493
46474
  oy + (y - by0) * sy
@@ -46519,7 +46500,7 @@ function layoutMap(resolved, data, size, opts) {
46519
46500
  const insets = [];
46520
46501
  const insetRegions = [];
46521
46502
  const insetLabelSeeds = [];
46522
- if (resolved.projection === "albers-usa" && usLayer) {
46503
+ if (resolved.projection === "albers-usa" && usLayer && !resolved.directives.noInsets) {
46523
46504
  const PAD = 8;
46524
46505
  const GAP = 12;
46525
46506
  const yB = height - FIT_PAD;
@@ -46550,38 +46531,14 @@ function layoutMap(resolved, data, size, opts) {
46550
46531
  }
46551
46532
  return y;
46552
46533
  };
46553
- const coastTop = (x0, xr) => {
46534
+ const coastFloor = (x0, xr) => {
46554
46535
  const n = 24;
46555
- const pts = [];
46556
46536
  let maxY = -Infinity;
46557
46537
  for (let i = 0; i <= n; i++) {
46558
- const x = x0 + (xr - x0) * i / n;
46559
- const y = at(x);
46560
- if (y > -Infinity) {
46561
- pts.push([x, y]);
46562
- if (y > maxY) maxY = y;
46563
- }
46564
- }
46565
- if (pts.length === 0) return () => yB - height * 0.42;
46566
- let m = 0;
46567
- if (pts.length >= 2) {
46568
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
46569
- for (const [x, y] of pts) {
46570
- sx += x;
46571
- sy += y;
46572
- sxx += x * x;
46573
- sxy += x * y;
46574
- }
46575
- const den = pts.length * sxx - sx * sx;
46576
- if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46577
- }
46578
- m = Math.max(-0.35, Math.min(0.35, m));
46579
- let c = -Infinity;
46580
- for (const [x, y] of pts) {
46581
- const need = y - m * x + GAP;
46582
- if (need > c) c = need;
46583
- }
46584
- return (x) => m * x + c;
46538
+ const y = at(x0 + (xr - x0) * i / n);
46539
+ if (y > maxY) maxY = y;
46540
+ }
46541
+ return maxY;
46585
46542
  };
46586
46543
  const placeInset = (iso, proj, boxX, iwReq) => {
46587
46544
  const f = usLayer.get(iso);
@@ -46590,19 +46547,15 @@ function layoutMap(resolved, data, size, opts) {
46590
46547
  const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46591
46548
  if (iw < 24) return boxX;
46592
46549
  const xr = x0 + iw + 2 * PAD;
46593
- const top = coastTop(x0, xr);
46594
- const yL = top(x0);
46595
- const yR = top(xr);
46550
+ const floor = coastFloor(x0, xr);
46551
+ const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
46596
46552
  proj.fitWidth(iw, f);
46597
46553
  const bb = (0, import_d3_geo2.geoPath)(proj).bounds(f);
46598
46554
  const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46599
46555
  const needH = sh + 2 * PAD;
46600
- let topFit = Math.max(yL, yR);
46556
+ let topFit = topGuess;
46601
46557
  const bottom = Math.min(topFit + needH, yB);
46602
46558
  if (bottom - topFit < needH) topFit = bottom - needH;
46603
- const lift = topFit - Math.max(yL, yR);
46604
- const topL = yL + lift;
46605
- const topR = yR + lift;
46606
46559
  proj.fitExtent(
46607
46560
  [
46608
46561
  [x0 + PAD, topFit + PAD],
@@ -46621,15 +46574,18 @@ function layoutMap(resolved, data, size, opts) {
46621
46574
  }
46622
46575
  insets.push({
46623
46576
  x: x0,
46624
- y: Math.min(topL, topR),
46577
+ y: topFit,
46625
46578
  w: xr - x0,
46626
- h: bottom - Math.min(topL, topR),
46579
+ h: bottom - topFit,
46627
46580
  points: [
46628
- [x0, topL],
46629
- [xr, topR],
46581
+ [x0, topFit],
46582
+ [xr, topFit],
46630
46583
  [xr, bottom],
46631
46584
  [x0, bottom]
46632
- ]
46585
+ ],
46586
+ // The FITTED inset projection (just fit to this box) — captured so the
46587
+ // geo-query can invert pixels inside the frame back to AK/HI coords.
46588
+ projection: proj
46633
46589
  });
46634
46590
  insetRegions.push({
46635
46591
  id: iso,
@@ -46799,13 +46755,40 @@ function layoutMap(resolved, data, size, opts) {
46799
46755
  id: "lake",
46800
46756
  d,
46801
46757
  fill: water,
46802
- stroke: "none",
46758
+ stroke: lakeStroke,
46803
46759
  lineNumber: -1,
46804
46760
  layer: "base"
46805
46761
  });
46806
46762
  }
46807
46763
  }
46808
- const riverColor = water;
46764
+ const relief = [];
46765
+ let reliefHatch = null;
46766
+ if (resolved.directives.relief === true && data.mountainRanges) {
46767
+ for (const [, f] of decodeLayer(data.mountainRanges)) {
46768
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46769
+ if (!viewF) continue;
46770
+ const area2 = path.area(viewF);
46771
+ if (!Number.isFinite(area2) || area2 < RELIEF_MIN_AREA) continue;
46772
+ const box = path.bounds(viewF);
46773
+ if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
46774
+ continue;
46775
+ const d = path(viewF) ?? "";
46776
+ if (!d) continue;
46777
+ relief.push({ d });
46778
+ }
46779
+ if (relief.length) {
46780
+ const darkTone = isDark ? palette.bg : palette.text;
46781
+ const lightTone = isDark ? palette.text : palette.bg;
46782
+ const landLum = relativeLuminance(neutralFill);
46783
+ const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
46784
+ reliefHatch = {
46785
+ color: mix(tone, neutralFill, RELIEF_HATCH_STRENGTH),
46786
+ spacing: RELIEF_HATCH_SPACING,
46787
+ width: RELIEF_HATCH_WIDTH
46788
+ };
46789
+ }
46790
+ }
46791
+ const riverColor = mix(water, regionStroke, 16);
46809
46792
  const rivers = [];
46810
46793
  if (data.rivers) {
46811
46794
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46826,6 +46809,9 @@ function layoutMap(resolved, data, size, opts) {
46826
46809
  return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
46827
46810
  };
46828
46811
  const poiFill = (p) => {
46812
+ const directHex = p.color ? resolveColor(p.color, palette) : null;
46813
+ if (directHex)
46814
+ return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
46829
46815
  for (const group of resolved.tagGroups) {
46830
46816
  const val = p.tags[group.name.toLowerCase()];
46831
46817
  if (!val) continue;
@@ -47241,21 +47227,26 @@ function layoutMap(resolved, data, size, opts) {
47241
47227
  ...resolved.caption !== void 0 && { caption: resolved.caption },
47242
47228
  regions,
47243
47229
  rivers,
47230
+ relief,
47231
+ reliefHatch,
47244
47232
  legs,
47245
47233
  pois,
47246
47234
  labels,
47247
47235
  legend,
47248
47236
  insets,
47249
- insetRegions
47237
+ insetRegions,
47238
+ projection,
47239
+ stretch: stretchParams
47250
47240
  };
47251
47241
  }
47252
- var import_d3_geo2, import_topojson_client2, 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;
47242
+ var import_d3_geo2, import_topojson_client2, 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;
47253
47243
  var init_layout15 = __esm({
47254
47244
  "src/map/layout.ts"() {
47255
47245
  "use strict";
47256
47246
  import_d3_geo2 = require("d3-geo");
47257
47247
  import_topojson_client2 = require("topojson-client");
47258
47248
  init_color_utils();
47249
+ init_colors();
47259
47250
  init_label_layout();
47260
47251
  init_legend_constants();
47261
47252
  init_title_constants();
@@ -47268,19 +47259,22 @@ var init_layout15 = __esm({
47268
47259
  W_MAX = 8;
47269
47260
  FONT = 11;
47270
47261
  COLO_EPS = 1.5;
47271
- LAND_TINT_LIGHT = 58;
47272
- LAND_TINT_DARK = 75;
47262
+ LAND_TINT_LIGHT = 12;
47263
+ LAND_TINT_DARK = 24;
47273
47264
  TAG_TINT_LIGHT = 60;
47274
47265
  TAG_TINT_DARK = 68;
47275
- WATER_TINT = 55;
47266
+ WATER_TINT_LIGHT = 13;
47267
+ WATER_TINT_DARK = 14;
47276
47268
  RIVER_WIDTH = 1.3;
47269
+ RELIEF_MIN_AREA = 12;
47270
+ RELIEF_MIN_DIM = 2;
47271
+ RELIEF_HATCH_SPACING = 3;
47272
+ RELIEF_HATCH_WIDTH = 0.25;
47273
+ RELIEF_HATCH_STRENGTH = 32;
47277
47274
  FOREIGN_TINT_LIGHT = 30;
47278
47275
  FOREIGN_TINT_DARK = 62;
47279
- MUTED_WATER_LIGHT = 14;
47280
- MUTED_WATER_DARK = 10;
47281
47276
  MUTED_FOREIGN_LIGHT = 28;
47282
47277
  MUTED_FOREIGN_DARK = 16;
47283
- MUTED_LAND_DARK = 24;
47284
47278
  COLO_R = 9;
47285
47279
  GOLDEN_ANGLE = 2.399963229728653;
47286
47280
  FAN_STEP = 16;
@@ -47352,6 +47346,20 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47352
47346
  }
47353
47347
  };
47354
47348
  for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
47349
+ if (layout.relief.length && layout.reliefHatch) {
47350
+ const h = layout.reliefHatch;
47351
+ const rangeClipId = "dgmo-relief-clip";
47352
+ const landClipId = "dgmo-relief-land";
47353
+ const rangeClip = defs.append("clipPath").attr("id", rangeClipId);
47354
+ for (const s of layout.relief) rangeClip.append("path").attr("d", s.d);
47355
+ const landClip = defs.append("clipPath").attr("id", landClipId);
47356
+ for (const r of layout.regions)
47357
+ if (r.id !== "lake") landClip.append("path").attr("d", r.d);
47358
+ 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");
47359
+ for (let y = h.spacing; y < height; y += h.spacing) {
47360
+ gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
47361
+ }
47362
+ }
47355
47363
  if (layout.rivers.length) {
47356
47364
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47357
47365
  for (const r of layout.rivers) {
@@ -47476,7 +47484,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47476
47484
  }
47477
47485
  }
47478
47486
  if (layout.title) {
47479
- 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);
47487
+ 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);
47480
47488
  }
47481
47489
  if (layout.subtitle) {
47482
47490
  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);
@@ -47509,6 +47517,121 @@ var init_renderer16 = __esm({
47509
47517
  }
47510
47518
  });
47511
47519
 
47520
+ // src/map/load-data.ts
47521
+ var load_data_exports = {};
47522
+ __export(load_data_exports, {
47523
+ loadMapData: () => loadMapData
47524
+ });
47525
+ async function loadNodeBuiltins() {
47526
+ const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
47527
+ import("fs/promises"),
47528
+ import("url"),
47529
+ import("path")
47530
+ ]);
47531
+ return { readFile, fileURLToPath, dirname, resolve };
47532
+ }
47533
+ async function readJson(nb, dir, name) {
47534
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
47535
+ }
47536
+ async function firstExistingDir(nb, baseDir) {
47537
+ for (const rel of CANDIDATE_DIRS) {
47538
+ const dir = nb.resolve(baseDir, rel);
47539
+ try {
47540
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
47541
+ return dir;
47542
+ } catch {
47543
+ }
47544
+ }
47545
+ throw new Error(
47546
+ `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
47547
+ );
47548
+ }
47549
+ function validate(data) {
47550
+ const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
47551
+ if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
47552
+ throw new Error("map data assets are malformed (failed shape validation)");
47553
+ }
47554
+ return data;
47555
+ }
47556
+ function moduleBaseDir(nb) {
47557
+ try {
47558
+ const url = import_meta.url;
47559
+ if (url) return nb.dirname(nb.fileURLToPath(url));
47560
+ } catch {
47561
+ }
47562
+ if (typeof __dirname !== "undefined") return __dirname;
47563
+ return process.cwd();
47564
+ }
47565
+ function loadMapData() {
47566
+ cache ??= (async () => {
47567
+ const nb = await loadNodeBuiltins();
47568
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
47569
+ const [
47570
+ worldCoarse,
47571
+ worldDetail,
47572
+ usStates,
47573
+ lakes,
47574
+ rivers,
47575
+ mountainRanges,
47576
+ naLand,
47577
+ naLakes,
47578
+ gazetteer
47579
+ ] = await Promise.all([
47580
+ readJson(nb, dir, FILES.worldCoarse),
47581
+ readJson(nb, dir, FILES.worldDetail),
47582
+ readJson(nb, dir, FILES.usStates),
47583
+ // Lakes/rivers/mountain/NA assets are optional — older bundles may predate them.
47584
+ readJson(nb, dir, FILES.lakes).catch(() => void 0),
47585
+ readJson(nb, dir, FILES.rivers).catch(() => void 0),
47586
+ readJson(nb, dir, FILES.mountainRanges).catch(
47587
+ () => void 0
47588
+ ),
47589
+ readJson(nb, dir, FILES.naLand).catch(() => void 0),
47590
+ readJson(nb, dir, FILES.naLakes).catch(() => void 0),
47591
+ readJson(nb, dir, FILES.gazetteer)
47592
+ ]);
47593
+ return validate({
47594
+ worldCoarse,
47595
+ worldDetail,
47596
+ usStates,
47597
+ gazetteer,
47598
+ ...lakes && { lakes },
47599
+ ...rivers && { rivers },
47600
+ ...mountainRanges && { mountainRanges },
47601
+ ...naLand && { naLand },
47602
+ ...naLakes && { naLakes }
47603
+ });
47604
+ })().catch((e) => {
47605
+ cache = void 0;
47606
+ throw e;
47607
+ });
47608
+ return cache;
47609
+ }
47610
+ var import_meta, FILES, CANDIDATE_DIRS, cache;
47611
+ var init_load_data = __esm({
47612
+ "src/map/load-data.ts"() {
47613
+ "use strict";
47614
+ import_meta = {};
47615
+ FILES = {
47616
+ worldCoarse: "world-coarse.json",
47617
+ worldDetail: "world-detail.json",
47618
+ usStates: "us-states.json",
47619
+ lakes: "lakes.json",
47620
+ rivers: "rivers.json",
47621
+ mountainRanges: "mountain-ranges.json",
47622
+ naLand: "na-land.json",
47623
+ naLakes: "na-lakes.json",
47624
+ gazetteer: "gazetteer.json"
47625
+ };
47626
+ CANDIDATE_DIRS = [
47627
+ "./data",
47628
+ "./map-data",
47629
+ "../map-data",
47630
+ "../src/map/data"
47631
+ ];
47632
+ }
47633
+ });
47634
+
47512
47635
  // src/pyramid/renderer.ts
47513
47636
  var renderer_exports17 = {};
47514
47637
  __export(renderer_exports17, {
@@ -55632,15 +55755,17 @@ async function renderForExport(content, theme, palette, viewState, options) {
55632
55755
  if (detectedType === "map") {
55633
55756
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55634
55757
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55635
- const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55636
55758
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
55637
55759
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55638
55760
  const mapParsed = parseMap2(content);
55639
- let mapData;
55640
- try {
55641
- mapData = await loadMapData2();
55642
- } catch {
55643
- return "";
55761
+ let mapData = options?.mapData;
55762
+ if (!mapData) {
55763
+ const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55764
+ try {
55765
+ mapData = await loadMapData2();
55766
+ } catch {
55767
+ return "";
55768
+ }
55644
55769
  }
55645
55770
  const mapResolved = resolveMap2(mapParsed, mapData);
55646
55771
  const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);