@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/auto.cjs CHANGED
@@ -15875,10 +15875,13 @@ function parseMap(content) {
15875
15875
  );
15876
15876
  d.projection = value;
15877
15877
  break;
15878
- case "region-metric":
15878
+ case "region-metric": {
15879
15879
  dup(d.regionMetric);
15880
- d.regionMetric = value;
15880
+ const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
15881
+ d.regionMetric = rmLabel;
15882
+ if (rmColor) d.regionMetricColor = rmColor;
15881
15883
  break;
15884
+ }
15882
15885
  case "poi-metric":
15883
15886
  dup(d.poiMetric);
15884
15887
  d.poiMetric = value;
@@ -15927,6 +15930,12 @@ function parseMap(content) {
15927
15930
  case "no-legend":
15928
15931
  d.noLegend = true;
15929
15932
  break;
15933
+ case "no-insets":
15934
+ d.noInsets = true;
15935
+ break;
15936
+ case "relief":
15937
+ d.relief = true;
15938
+ break;
15930
15939
  case "muted":
15931
15940
  case "natural":
15932
15941
  if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
@@ -16039,6 +16048,7 @@ function parseMap(content) {
16039
16048
  };
16040
16049
  if (regionScope !== void 0) region.scope = regionScope;
16041
16050
  if (valueNum !== void 0) region.value = valueNum;
16051
+ if (split.color) region.color = split.color;
16042
16052
  regions.push(region);
16043
16053
  }
16044
16054
  function handlePoi(rest, line12, indent) {
@@ -16063,6 +16073,7 @@ function parseMap(content) {
16063
16073
  const poi = { pos, tags, meta, lineNumber: line12 };
16064
16074
  if (split.alias) poi.alias = split.alias;
16065
16075
  if (label !== void 0) poi.label = label;
16076
+ if (split.color) poi.color = split.color;
16066
16077
  pois.push(poi);
16067
16078
  open.poi = { poi, indent };
16068
16079
  }
@@ -16267,6 +16278,8 @@ var init_parser12 = __esm({
16267
16278
  "default-state",
16268
16279
  "active-tag",
16269
16280
  "no-legend",
16281
+ "no-insets",
16282
+ "relief",
16270
16283
  "subtitle",
16271
16284
  "caption"
16272
16285
  ]);
@@ -45491,6 +45504,74 @@ function featureBbox(topo, geomId) {
45491
45504
  [b[1][0], b[1][1]]
45492
45505
  ];
45493
45506
  }
45507
+ function explodePolygons(gj) {
45508
+ const g = gj.geometry ?? gj;
45509
+ const t = g.type;
45510
+ const coords = g.coordinates;
45511
+ if (t === "Polygon") {
45512
+ return [
45513
+ { type: "Feature", geometry: { type: "Polygon", coordinates: coords } }
45514
+ ];
45515
+ }
45516
+ if (t === "MultiPolygon") {
45517
+ return coords.map((rings) => ({
45518
+ type: "Feature",
45519
+ geometry: { type: "Polygon", coordinates: rings }
45520
+ }));
45521
+ }
45522
+ return [];
45523
+ }
45524
+ function bboxGap(a, b) {
45525
+ const lonGap = Math.max(0, a[0][0] - b[1][0], b[0][0] - a[1][0]);
45526
+ const latGap = Math.max(0, a[0][1] - b[1][1], b[0][1] - a[1][1]);
45527
+ return Math.max(lonGap, latGap);
45528
+ }
45529
+ function featureBboxPrimary(topo, geomId) {
45530
+ const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45531
+ if (!geom) return null;
45532
+ const gj = (0, import_topojson_client.feature)(topo, geom);
45533
+ const parts = explodePolygons(gj);
45534
+ if (parts.length <= 1) return featureBbox(topo, geomId);
45535
+ const polys = parts.map((p) => {
45536
+ const b = (0, import_d3_geo.geoBounds)(p);
45537
+ if (!b || !Number.isFinite(b[0][0])) return null;
45538
+ const wraps = b[1][0] < b[0][0];
45539
+ const bbox = [
45540
+ [b[0][0], b[0][1]],
45541
+ [b[1][0], b[1][1]]
45542
+ ];
45543
+ return { bbox, area: (0, import_d3_geo.geoArea)(p), wraps };
45544
+ }).filter(
45545
+ (p) => p !== null
45546
+ );
45547
+ if (polys.length <= 1 || polys.some((p) => p.wraps))
45548
+ return featureBbox(topo, geomId);
45549
+ const maxArea = Math.max(...polys.map((p) => p.area));
45550
+ const anchor = polys.find((p) => p.area === maxArea);
45551
+ const cluster = [
45552
+ [anchor.bbox[0][0], anchor.bbox[0][1]],
45553
+ [anchor.bbox[1][0], anchor.bbox[1][1]]
45554
+ ];
45555
+ const remaining = polys.filter((p) => p !== anchor);
45556
+ let added = true;
45557
+ while (added) {
45558
+ added = false;
45559
+ for (let i = remaining.length - 1; i >= 0; i--) {
45560
+ const p = remaining[i];
45561
+ const near = bboxGap(p.bbox, cluster) <= DETACH_GAP_DEG;
45562
+ const large = p.area >= DETACH_AREA_FRAC * maxArea;
45563
+ if (near || large) {
45564
+ cluster[0][0] = Math.min(cluster[0][0], p.bbox[0][0]);
45565
+ cluster[0][1] = Math.min(cluster[0][1], p.bbox[0][1]);
45566
+ cluster[1][0] = Math.max(cluster[1][0], p.bbox[1][0]);
45567
+ cluster[1][1] = Math.max(cluster[1][1], p.bbox[1][1]);
45568
+ remaining.splice(i, 1);
45569
+ added = true;
45570
+ }
45571
+ }
45572
+ }
45573
+ return cluster;
45574
+ }
45494
45575
  function unionExtent(boxes, points) {
45495
45576
  const lats = [];
45496
45577
  const lons = [];
@@ -45529,13 +45610,15 @@ function unionLongitudes(lons) {
45529
45610
  }
45530
45611
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45531
45612
  }
45532
- var import_topojson_client, import_d3_geo, fold;
45613
+ var import_topojson_client, import_d3_geo, fold, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45533
45614
  var init_geo = __esm({
45534
45615
  "src/map/geo.ts"() {
45535
45616
  "use strict";
45536
45617
  import_topojson_client = require("topojson-client");
45537
45618
  import_d3_geo = require("d3-geo");
45538
45619
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
45620
+ DETACH_GAP_DEG = 10;
45621
+ DETACH_AREA_FRAC = 0.25;
45539
45622
  }
45540
45623
  });
45541
45624
 
@@ -45639,12 +45722,12 @@ function resolveMap(parsed, data) {
45639
45722
  chosen = { ...inState, layer: "us-state" };
45640
45723
  } else {
45641
45724
  chosen = { ...inCountry, layer: "country" };
45725
+ warn2(
45726
+ r.lineNumber,
45727
+ `"${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}").`,
45728
+ "W_MAP_REGION_AMBIGUOUS"
45729
+ );
45642
45730
  }
45643
- warn2(
45644
- r.lineNumber,
45645
- `"${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}").`,
45646
- "W_MAP_REGION_AMBIGUOUS"
45647
- );
45648
45731
  } else if (inState) {
45649
45732
  chosen = { ...inState, layer: "us-state" };
45650
45733
  } else if (inCountry) {
@@ -45668,6 +45751,7 @@ function resolveMap(parsed, data) {
45668
45751
  name: chosen.name,
45669
45752
  layer: chosen.layer,
45670
45753
  ...r.value !== void 0 && { value: r.value },
45754
+ ...r.color !== void 0 && { color: r.color },
45671
45755
  tags: r.tags,
45672
45756
  meta: r.meta,
45673
45757
  lineNumber: r.lineNumber
@@ -45809,6 +45893,7 @@ function resolveMap(parsed, data) {
45809
45893
  lat,
45810
45894
  lon,
45811
45895
  ...p.label !== void 0 && { label: p.label },
45896
+ ...p.color !== void 0 && { color: p.color },
45812
45897
  tags: p.tags,
45813
45898
  meta: p.meta,
45814
45899
  lineNumber: p.lineNumber
@@ -45951,7 +46036,7 @@ function resolveMap(parsed, data) {
45951
46036
  }
45952
46037
  for (const r of regions) {
45953
46038
  if (r.layer === "country") {
45954
- const bb = featureBbox(data.worldCoarse, r.iso);
46039
+ const bb = featureBboxPrimary(data.worldCoarse, r.iso);
45955
46040
  if (bb) regionBoxes.push(bb);
45956
46041
  }
45957
46042
  }
@@ -45965,6 +46050,7 @@ function resolveMap(parsed, data) {
45965
46050
  const lonSpan = extent2[1][0] - extent2[0][0];
45966
46051
  const latSpan = extent2[1][1] - extent2[0][1];
45967
46052
  const span = Math.max(lonSpan, latSpan);
46053
+ const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
45968
46054
  const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
45969
46055
  let projection;
45970
46056
  const override = parsed.directives.projection;
@@ -45972,12 +46058,10 @@ function resolveMap(parsed, data) {
45972
46058
  projection = override;
45973
46059
  } else if (usDominant) {
45974
46060
  projection = "albers-usa";
45975
- } else if (span > WORLD_SPAN) {
46061
+ } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
45976
46062
  projection = "equirectangular";
45977
- } else if (span < MERCATOR_MAX_SPAN) {
45978
- projection = "mercator";
45979
46063
  } else {
45980
- projection = "equirectangular";
46064
+ projection = "mercator";
45981
46065
  }
45982
46066
  if (lonSpan >= 180) {
45983
46067
  extent2 = [
@@ -46031,14 +46115,14 @@ function firstError(diags) {
46031
46115
  const e = diags.find((d) => d.severity === "error");
46032
46116
  return e ? formatDgmoError(e) : null;
46033
46117
  }
46034
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46118
+ var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46035
46119
  var init_resolver2 = __esm({
46036
46120
  "src/map/resolver.ts"() {
46037
46121
  "use strict";
46038
46122
  init_diagnostics();
46039
46123
  init_geo();
46040
46124
  WORLD_SPAN = 90;
46041
- MERCATOR_MAX_SPAN = 25;
46125
+ MERCATOR_MAX_LAT = 80;
46042
46126
  PAD_FRACTION = 0.05;
46043
46127
  WORLD_LAT_SOUTH = -58;
46044
46128
  WORLD_LAT_NORTH = 78;
@@ -46119,115 +46203,6 @@ var init_resolver2 = __esm({
46119
46203
  }
46120
46204
  });
46121
46205
 
46122
- // src/map/load-data.ts
46123
- var load_data_exports = {};
46124
- __export(load_data_exports, {
46125
- loadMapData: () => loadMapData
46126
- });
46127
- async function loadNodeBuiltins() {
46128
- const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
46129
- import("fs/promises"),
46130
- import("url"),
46131
- import("path")
46132
- ]);
46133
- return { readFile, fileURLToPath, dirname, resolve };
46134
- }
46135
- async function readJson(nb, dir, name) {
46136
- return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
46137
- }
46138
- async function firstExistingDir(nb, baseDir) {
46139
- for (const rel of CANDIDATE_DIRS) {
46140
- const dir = nb.resolve(baseDir, rel);
46141
- try {
46142
- await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
46143
- return dir;
46144
- } catch {
46145
- }
46146
- }
46147
- throw new Error(
46148
- `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
46149
- );
46150
- }
46151
- function validate(data) {
46152
- const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
46153
- if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
46154
- throw new Error("map data assets are malformed (failed shape validation)");
46155
- }
46156
- return data;
46157
- }
46158
- function moduleBaseDir(nb) {
46159
- try {
46160
- const url = import_meta.url;
46161
- if (url) return nb.dirname(nb.fileURLToPath(url));
46162
- } catch {
46163
- }
46164
- if (typeof __dirname !== "undefined") return __dirname;
46165
- return process.cwd();
46166
- }
46167
- function loadMapData() {
46168
- cache ??= (async () => {
46169
- const nb = await loadNodeBuiltins();
46170
- const dir = await firstExistingDir(nb, moduleBaseDir(nb));
46171
- const [
46172
- worldCoarse,
46173
- worldDetail,
46174
- usStates,
46175
- lakes,
46176
- rivers,
46177
- naLand,
46178
- naLakes,
46179
- gazetteer
46180
- ] = await Promise.all([
46181
- readJson(nb, dir, FILES.worldCoarse),
46182
- readJson(nb, dir, FILES.worldDetail),
46183
- readJson(nb, dir, FILES.usStates),
46184
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
46185
- readJson(nb, dir, FILES.lakes).catch(() => void 0),
46186
- readJson(nb, dir, FILES.rivers).catch(() => void 0),
46187
- readJson(nb, dir, FILES.naLand).catch(() => void 0),
46188
- readJson(nb, dir, FILES.naLakes).catch(() => void 0),
46189
- readJson(nb, dir, FILES.gazetteer)
46190
- ]);
46191
- return validate({
46192
- worldCoarse,
46193
- worldDetail,
46194
- usStates,
46195
- gazetteer,
46196
- ...lakes && { lakes },
46197
- ...rivers && { rivers },
46198
- ...naLand && { naLand },
46199
- ...naLakes && { naLakes }
46200
- });
46201
- })().catch((e) => {
46202
- cache = void 0;
46203
- throw e;
46204
- });
46205
- return cache;
46206
- }
46207
- var import_meta, FILES, CANDIDATE_DIRS, cache;
46208
- var init_load_data = __esm({
46209
- "src/map/load-data.ts"() {
46210
- "use strict";
46211
- import_meta = {};
46212
- FILES = {
46213
- worldCoarse: "world-coarse.json",
46214
- worldDetail: "world-detail.json",
46215
- usStates: "us-states.json",
46216
- lakes: "lakes.json",
46217
- rivers: "rivers.json",
46218
- naLand: "na-land.json",
46219
- naLakes: "na-lakes.json",
46220
- gazetteer: "gazetteer.json"
46221
- };
46222
- CANDIDATE_DIRS = [
46223
- "./data",
46224
- "./map-data",
46225
- "../map-data",
46226
- "../src/map/data"
46227
- ];
46228
- }
46229
- });
46230
-
46231
46206
  // src/map/layout.ts
46232
46207
  function geomObject2(topo) {
46233
46208
  const key = Object.keys(topo.objects)[0];
@@ -46254,18 +46229,14 @@ function projectionFor(family) {
46254
46229
  return (0, import_d3_geo2.geoEquirectangular)();
46255
46230
  }
46256
46231
  }
46257
- function mapBackgroundColor(palette, isDark = false, dataActive = false) {
46258
- if (dataActive)
46259
- return mix(
46260
- palette.colors.gray,
46261
- palette.bg,
46262
- isDark ? MUTED_WATER_DARK : MUTED_WATER_LIGHT
46263
- );
46264
- return mix(palette.colors.blue, palette.bg, WATER_TINT);
46232
+ function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
46233
+ return mix(
46234
+ palette.colors.blue,
46235
+ palette.bg,
46236
+ isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
46237
+ );
46265
46238
  }
46266
- function mapNeutralLandColor(palette, isDark, dataActive = false) {
46267
- if (dataActive)
46268
- return isDark ? mix(palette.colors.gray, palette.bg, MUTED_LAND_DARK) : palette.bg;
46239
+ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46269
46240
  return mix(
46270
46241
  palette.colors.green,
46271
46242
  palette.bg,
@@ -46297,7 +46268,7 @@ function layoutMap(resolved, data, size, opts) {
46297
46268
  const scaleOverride = resolved.directives.scale;
46298
46269
  const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46299
46270
  const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
46300
- const rampHue = palette.colors.red;
46271
+ const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46301
46272
  const hasRamp = values.length > 0;
46302
46273
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
46303
46274
  const matchColorGroup = (v) => {
@@ -46320,6 +46291,7 @@ function layoutMap(resolved, data, size, opts) {
46320
46291
  const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
46321
46292
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46322
46293
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
46294
+ const lakeStroke = mix(regionStroke, water, 45);
46323
46295
  const foreignFill = mix(
46324
46296
  palette.colors.gray,
46325
46297
  palette.bg,
@@ -46349,7 +46321,14 @@ function layoutMap(resolved, data, size, opts) {
46349
46321
  isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46350
46322
  );
46351
46323
  };
46324
+ const directFill = (name) => {
46325
+ const hex = name ? resolveColor(name, palette) : null;
46326
+ if (!hex) return null;
46327
+ return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
46328
+ };
46352
46329
  const regionFill = (r) => {
46330
+ const direct = directFill(r.color);
46331
+ if (direct) return direct;
46353
46332
  if (activeIsScore) {
46354
46333
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46355
46334
  }
@@ -46403,6 +46382,7 @@ function layoutMap(resolved, data, size, opts) {
46403
46382
  const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46404
46383
  let path;
46405
46384
  let project;
46385
+ let stretchParams = null;
46406
46386
  if (fitIsGlobal) {
46407
46387
  const cb = (0, import_d3_geo2.geoPath)(projection).bounds(fitTarget);
46408
46388
  const bx0 = cb[0][0];
@@ -46413,6 +46393,7 @@ function layoutMap(resolved, data, size, opts) {
46413
46393
  const oy = fitBox[0][1];
46414
46394
  const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46415
46395
  const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
46396
+ stretchParams = { sx, sy, ox, oy, bx0, by0 };
46416
46397
  const stretch = (x, y) => [
46417
46398
  ox + (x - bx0) * sx,
46418
46399
  oy + (y - by0) * sy
@@ -46444,7 +46425,7 @@ function layoutMap(resolved, data, size, opts) {
46444
46425
  const insets = [];
46445
46426
  const insetRegions = [];
46446
46427
  const insetLabelSeeds = [];
46447
- if (resolved.projection === "albers-usa" && usLayer) {
46428
+ if (resolved.projection === "albers-usa" && usLayer && !resolved.directives.noInsets) {
46448
46429
  const PAD = 8;
46449
46430
  const GAP = 12;
46450
46431
  const yB = height - FIT_PAD;
@@ -46475,38 +46456,14 @@ function layoutMap(resolved, data, size, opts) {
46475
46456
  }
46476
46457
  return y;
46477
46458
  };
46478
- const coastTop = (x0, xr) => {
46459
+ const coastFloor = (x0, xr) => {
46479
46460
  const n = 24;
46480
- const pts = [];
46481
46461
  let maxY = -Infinity;
46482
46462
  for (let i = 0; i <= n; i++) {
46483
- const x = x0 + (xr - x0) * i / n;
46484
- const y = at(x);
46485
- if (y > -Infinity) {
46486
- pts.push([x, y]);
46487
- if (y > maxY) maxY = y;
46488
- }
46489
- }
46490
- if (pts.length === 0) return () => yB - height * 0.42;
46491
- let m = 0;
46492
- if (pts.length >= 2) {
46493
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
46494
- for (const [x, y] of pts) {
46495
- sx += x;
46496
- sy += y;
46497
- sxx += x * x;
46498
- sxy += x * y;
46499
- }
46500
- const den = pts.length * sxx - sx * sx;
46501
- if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46502
- }
46503
- m = Math.max(-0.35, Math.min(0.35, m));
46504
- let c = -Infinity;
46505
- for (const [x, y] of pts) {
46506
- const need = y - m * x + GAP;
46507
- if (need > c) c = need;
46508
- }
46509
- return (x) => m * x + c;
46463
+ const y = at(x0 + (xr - x0) * i / n);
46464
+ if (y > maxY) maxY = y;
46465
+ }
46466
+ return maxY;
46510
46467
  };
46511
46468
  const placeInset = (iso, proj, boxX, iwReq) => {
46512
46469
  const f = usLayer.get(iso);
@@ -46515,19 +46472,15 @@ function layoutMap(resolved, data, size, opts) {
46515
46472
  const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46516
46473
  if (iw < 24) return boxX;
46517
46474
  const xr = x0 + iw + 2 * PAD;
46518
- const top = coastTop(x0, xr);
46519
- const yL = top(x0);
46520
- const yR = top(xr);
46475
+ const floor = coastFloor(x0, xr);
46476
+ const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
46521
46477
  proj.fitWidth(iw, f);
46522
46478
  const bb = (0, import_d3_geo2.geoPath)(proj).bounds(f);
46523
46479
  const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46524
46480
  const needH = sh + 2 * PAD;
46525
- let topFit = Math.max(yL, yR);
46481
+ let topFit = topGuess;
46526
46482
  const bottom = Math.min(topFit + needH, yB);
46527
46483
  if (bottom - topFit < needH) topFit = bottom - needH;
46528
- const lift = topFit - Math.max(yL, yR);
46529
- const topL = yL + lift;
46530
- const topR = yR + lift;
46531
46484
  proj.fitExtent(
46532
46485
  [
46533
46486
  [x0 + PAD, topFit + PAD],
@@ -46546,15 +46499,18 @@ function layoutMap(resolved, data, size, opts) {
46546
46499
  }
46547
46500
  insets.push({
46548
46501
  x: x0,
46549
- y: Math.min(topL, topR),
46502
+ y: topFit,
46550
46503
  w: xr - x0,
46551
- h: bottom - Math.min(topL, topR),
46504
+ h: bottom - topFit,
46552
46505
  points: [
46553
- [x0, topL],
46554
- [xr, topR],
46506
+ [x0, topFit],
46507
+ [xr, topFit],
46555
46508
  [xr, bottom],
46556
46509
  [x0, bottom]
46557
- ]
46510
+ ],
46511
+ // The FITTED inset projection (just fit to this box) — captured so the
46512
+ // geo-query can invert pixels inside the frame back to AK/HI coords.
46513
+ projection: proj
46558
46514
  });
46559
46515
  insetRegions.push({
46560
46516
  id: iso,
@@ -46724,13 +46680,40 @@ function layoutMap(resolved, data, size, opts) {
46724
46680
  id: "lake",
46725
46681
  d,
46726
46682
  fill: water,
46727
- stroke: "none",
46683
+ stroke: lakeStroke,
46728
46684
  lineNumber: -1,
46729
46685
  layer: "base"
46730
46686
  });
46731
46687
  }
46732
46688
  }
46733
- const riverColor = water;
46689
+ const relief = [];
46690
+ let reliefHatch = null;
46691
+ if (resolved.directives.relief === true && data.mountainRanges) {
46692
+ for (const [, f] of decodeLayer(data.mountainRanges)) {
46693
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46694
+ if (!viewF) continue;
46695
+ const area2 = path.area(viewF);
46696
+ if (!Number.isFinite(area2) || area2 < RELIEF_MIN_AREA) continue;
46697
+ const box = path.bounds(viewF);
46698
+ if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
46699
+ continue;
46700
+ const d = path(viewF) ?? "";
46701
+ if (!d) continue;
46702
+ relief.push({ d });
46703
+ }
46704
+ if (relief.length) {
46705
+ const darkTone = isDark ? palette.bg : palette.text;
46706
+ const lightTone = isDark ? palette.text : palette.bg;
46707
+ const landLum = relativeLuminance(neutralFill);
46708
+ const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
46709
+ reliefHatch = {
46710
+ color: mix(tone, neutralFill, RELIEF_HATCH_STRENGTH),
46711
+ spacing: RELIEF_HATCH_SPACING,
46712
+ width: RELIEF_HATCH_WIDTH
46713
+ };
46714
+ }
46715
+ }
46716
+ const riverColor = mix(water, regionStroke, 16);
46734
46717
  const rivers = [];
46735
46718
  if (data.rivers) {
46736
46719
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46751,6 +46734,9 @@ function layoutMap(resolved, data, size, opts) {
46751
46734
  return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
46752
46735
  };
46753
46736
  const poiFill = (p) => {
46737
+ const directHex = p.color ? resolveColor(p.color, palette) : null;
46738
+ if (directHex)
46739
+ return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
46754
46740
  for (const group of resolved.tagGroups) {
46755
46741
  const val = p.tags[group.name.toLowerCase()];
46756
46742
  if (!val) continue;
@@ -47166,21 +47152,26 @@ function layoutMap(resolved, data, size, opts) {
47166
47152
  ...resolved.caption !== void 0 && { caption: resolved.caption },
47167
47153
  regions,
47168
47154
  rivers,
47155
+ relief,
47156
+ reliefHatch,
47169
47157
  legs,
47170
47158
  pois,
47171
47159
  labels,
47172
47160
  legend,
47173
47161
  insets,
47174
- insetRegions
47162
+ insetRegions,
47163
+ projection,
47164
+ stretch: stretchParams
47175
47165
  };
47176
47166
  }
47177
- 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;
47167
+ 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;
47178
47168
  var init_layout15 = __esm({
47179
47169
  "src/map/layout.ts"() {
47180
47170
  "use strict";
47181
47171
  import_d3_geo2 = require("d3-geo");
47182
47172
  import_topojson_client2 = require("topojson-client");
47183
47173
  init_color_utils();
47174
+ init_colors();
47184
47175
  init_label_layout();
47185
47176
  init_legend_constants();
47186
47177
  init_title_constants();
@@ -47193,19 +47184,22 @@ var init_layout15 = __esm({
47193
47184
  W_MAX = 8;
47194
47185
  FONT = 11;
47195
47186
  COLO_EPS = 1.5;
47196
- LAND_TINT_LIGHT = 58;
47197
- LAND_TINT_DARK = 75;
47187
+ LAND_TINT_LIGHT = 12;
47188
+ LAND_TINT_DARK = 24;
47198
47189
  TAG_TINT_LIGHT = 60;
47199
47190
  TAG_TINT_DARK = 68;
47200
- WATER_TINT = 55;
47191
+ WATER_TINT_LIGHT = 13;
47192
+ WATER_TINT_DARK = 14;
47201
47193
  RIVER_WIDTH = 1.3;
47194
+ RELIEF_MIN_AREA = 12;
47195
+ RELIEF_MIN_DIM = 2;
47196
+ RELIEF_HATCH_SPACING = 3;
47197
+ RELIEF_HATCH_WIDTH = 0.25;
47198
+ RELIEF_HATCH_STRENGTH = 32;
47202
47199
  FOREIGN_TINT_LIGHT = 30;
47203
47200
  FOREIGN_TINT_DARK = 62;
47204
- MUTED_WATER_LIGHT = 14;
47205
- MUTED_WATER_DARK = 10;
47206
47201
  MUTED_FOREIGN_LIGHT = 28;
47207
47202
  MUTED_FOREIGN_DARK = 16;
47208
- MUTED_LAND_DARK = 24;
47209
47203
  COLO_R = 9;
47210
47204
  GOLDEN_ANGLE = 2.399963229728653;
47211
47205
  FAN_STEP = 16;
@@ -47277,6 +47271,20 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47277
47271
  }
47278
47272
  };
47279
47273
  for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
47274
+ if (layout.relief.length && layout.reliefHatch) {
47275
+ const h = layout.reliefHatch;
47276
+ const rangeClipId = "dgmo-relief-clip";
47277
+ const landClipId = "dgmo-relief-land";
47278
+ const rangeClip = defs.append("clipPath").attr("id", rangeClipId);
47279
+ for (const s of layout.relief) rangeClip.append("path").attr("d", s.d);
47280
+ const landClip = defs.append("clipPath").attr("id", landClipId);
47281
+ for (const r of layout.regions)
47282
+ if (r.id !== "lake") landClip.append("path").attr("d", r.d);
47283
+ 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");
47284
+ for (let y = h.spacing; y < height; y += h.spacing) {
47285
+ gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
47286
+ }
47287
+ }
47280
47288
  if (layout.rivers.length) {
47281
47289
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47282
47290
  for (const r of layout.rivers) {
@@ -47401,7 +47409,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47401
47409
  }
47402
47410
  }
47403
47411
  if (layout.title) {
47404
- 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);
47412
+ 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);
47405
47413
  }
47406
47414
  if (layout.subtitle) {
47407
47415
  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);
@@ -47434,6 +47442,121 @@ var init_renderer16 = __esm({
47434
47442
  }
47435
47443
  });
47436
47444
 
47445
+ // src/map/load-data.ts
47446
+ var load_data_exports = {};
47447
+ __export(load_data_exports, {
47448
+ loadMapData: () => loadMapData
47449
+ });
47450
+ async function loadNodeBuiltins() {
47451
+ const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
47452
+ import("fs/promises"),
47453
+ import("url"),
47454
+ import("path")
47455
+ ]);
47456
+ return { readFile, fileURLToPath, dirname, resolve };
47457
+ }
47458
+ async function readJson(nb, dir, name) {
47459
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
47460
+ }
47461
+ async function firstExistingDir(nb, baseDir) {
47462
+ for (const rel of CANDIDATE_DIRS) {
47463
+ const dir = nb.resolve(baseDir, rel);
47464
+ try {
47465
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
47466
+ return dir;
47467
+ } catch {
47468
+ }
47469
+ }
47470
+ throw new Error(
47471
+ `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
47472
+ );
47473
+ }
47474
+ function validate(data) {
47475
+ const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
47476
+ if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
47477
+ throw new Error("map data assets are malformed (failed shape validation)");
47478
+ }
47479
+ return data;
47480
+ }
47481
+ function moduleBaseDir(nb) {
47482
+ try {
47483
+ const url = import_meta.url;
47484
+ if (url) return nb.dirname(nb.fileURLToPath(url));
47485
+ } catch {
47486
+ }
47487
+ if (typeof __dirname !== "undefined") return __dirname;
47488
+ return process.cwd();
47489
+ }
47490
+ function loadMapData() {
47491
+ cache ??= (async () => {
47492
+ const nb = await loadNodeBuiltins();
47493
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
47494
+ const [
47495
+ worldCoarse,
47496
+ worldDetail,
47497
+ usStates,
47498
+ lakes,
47499
+ rivers,
47500
+ mountainRanges,
47501
+ naLand,
47502
+ naLakes,
47503
+ gazetteer
47504
+ ] = await Promise.all([
47505
+ readJson(nb, dir, FILES.worldCoarse),
47506
+ readJson(nb, dir, FILES.worldDetail),
47507
+ readJson(nb, dir, FILES.usStates),
47508
+ // Lakes/rivers/mountain/NA assets are optional — older bundles may predate them.
47509
+ readJson(nb, dir, FILES.lakes).catch(() => void 0),
47510
+ readJson(nb, dir, FILES.rivers).catch(() => void 0),
47511
+ readJson(nb, dir, FILES.mountainRanges).catch(
47512
+ () => void 0
47513
+ ),
47514
+ readJson(nb, dir, FILES.naLand).catch(() => void 0),
47515
+ readJson(nb, dir, FILES.naLakes).catch(() => void 0),
47516
+ readJson(nb, dir, FILES.gazetteer)
47517
+ ]);
47518
+ return validate({
47519
+ worldCoarse,
47520
+ worldDetail,
47521
+ usStates,
47522
+ gazetteer,
47523
+ ...lakes && { lakes },
47524
+ ...rivers && { rivers },
47525
+ ...mountainRanges && { mountainRanges },
47526
+ ...naLand && { naLand },
47527
+ ...naLakes && { naLakes }
47528
+ });
47529
+ })().catch((e) => {
47530
+ cache = void 0;
47531
+ throw e;
47532
+ });
47533
+ return cache;
47534
+ }
47535
+ var import_meta, FILES, CANDIDATE_DIRS, cache;
47536
+ var init_load_data = __esm({
47537
+ "src/map/load-data.ts"() {
47538
+ "use strict";
47539
+ import_meta = {};
47540
+ FILES = {
47541
+ worldCoarse: "world-coarse.json",
47542
+ worldDetail: "world-detail.json",
47543
+ usStates: "us-states.json",
47544
+ lakes: "lakes.json",
47545
+ rivers: "rivers.json",
47546
+ mountainRanges: "mountain-ranges.json",
47547
+ naLand: "na-land.json",
47548
+ naLakes: "na-lakes.json",
47549
+ gazetteer: "gazetteer.json"
47550
+ };
47551
+ CANDIDATE_DIRS = [
47552
+ "./data",
47553
+ "./map-data",
47554
+ "../map-data",
47555
+ "../src/map/data"
47556
+ ];
47557
+ }
47558
+ });
47559
+
47437
47560
  // src/pyramid/renderer.ts
47438
47561
  var renderer_exports17 = {};
47439
47562
  __export(renderer_exports17, {
@@ -55557,15 +55680,17 @@ async function renderForExport(content, theme, palette, viewState, options) {
55557
55680
  if (detectedType === "map") {
55558
55681
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55559
55682
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55560
- const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55561
55683
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
55562
55684
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55563
55685
  const mapParsed = parseMap2(content);
55564
- let mapData;
55565
- try {
55566
- mapData = await loadMapData2();
55567
- } catch {
55568
- return "";
55686
+ let mapData = options?.mapData;
55687
+ if (!mapData) {
55688
+ const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55689
+ try {
55690
+ mapData = await loadMapData2();
55691
+ } catch {
55692
+ return "";
55693
+ }
55569
55694
  }
55570
55695
  const mapResolved = resolveMap2(mapParsed, mapData);
55571
55696
  const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
@@ -56626,6 +56751,7 @@ var DIRECTIVE_KEYWORDS = /* @__PURE__ */ new Set([
56626
56751
  "default-country",
56627
56752
  "default-state",
56628
56753
  "no-legend",
56754
+ "no-insets",
56629
56755
  "muted",
56630
56756
  "natural",
56631
56757
  "subtitle",
@@ -57295,7 +57421,7 @@ pre.dgmo, code.language-dgmo, pre > code.language-dgmo,
57295
57421
 
57296
57422
  // src/auto/index.ts
57297
57423
  init_safe_href();
57298
- var VERSION = "0.21.0";
57424
+ var VERSION = "0.21.1";
57299
57425
  var DEFAULTS = {
57300
57426
  theme: "auto",
57301
57427
  palette: "nord",