mapshaper 0.7.72 → 0.7.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mapshaper.js CHANGED
@@ -78,7 +78,7 @@
78
78
  return obj === Object(obj); // via underscore
79
79
  }
80
80
 
81
- function clamp$3(val, min, max) {
81
+ function clamp$4(val, min, max) {
82
82
  return val < min ? min : (val > max ? max : val);
83
83
  }
84
84
 
@@ -721,7 +721,7 @@
721
721
  function findValueByRank(arr, rank) {
722
722
  if (!arr.length || rank < 1 || rank > arr.length) error$1("[findValueByRank()] invalid input");
723
723
 
724
- rank = clamp$3(rank | 0, 1, arr.length);
724
+ rank = clamp$4(rank | 0, 1, arr.length);
725
725
  var k = rank - 1, // conv. rank to array index
726
726
  n = arr.length,
727
727
  l = 0,
@@ -1095,7 +1095,7 @@
1095
1095
  // self-import and the resulting Rollup circular-dependency warning.
1096
1096
  var utils = {
1097
1097
  addThousandsSep, addslashes, arrayToIndex,
1098
- clamp: clamp$3, cleanNumericString, contains, copyElements, countValues, createBuffer,
1098
+ clamp: clamp$4, cleanNumericString, contains, copyElements, countValues, createBuffer,
1099
1099
  defaults, difference,
1100
1100
  endsWith, every, expandoBuffer, extend: extend$1, extendBuffer,
1101
1101
  find: find$1, findMedian, findQuantile, findRankByValue, findStringPrefix,
@@ -6581,7 +6581,7 @@
6581
6581
  return lon;
6582
6582
  }
6583
6583
 
6584
- function clamp$2(value, min, max) {
6584
+ function clamp$3(value, min, max) {
6585
6585
  return Math.max(min, Math.min(max, value));
6586
6586
  }
6587
6587
 
@@ -7105,21 +7105,21 @@
7105
7105
  return [
7106
7106
  Math.atan2(y * cosDeltaGamma + z * sinDeltaGamma,
7107
7107
  x * cosDeltaPhi + k * sinDeltaPhi),
7108
- Math.asin(clamp$2(k * cosDeltaPhi - x * sinDeltaPhi, -1, 1))
7108
+ Math.asin(clamp$3(k * cosDeltaPhi - x * sinDeltaPhi, -1, 1))
7109
7109
  ];
7110
7110
  }
7111
7111
  k = z * cosDeltaPhi + x * sinDeltaPhi;
7112
7112
  return [
7113
7113
  Math.atan2(y * cosDeltaGamma - k * sinDeltaGamma,
7114
7114
  x * cosDeltaPhi - z * sinDeltaPhi),
7115
- Math.asin(clamp$2(k * cosDeltaGamma + y * sinDeltaGamma, -1, 1))
7115
+ Math.asin(clamp$3(k * cosDeltaGamma + y * sinDeltaGamma, -1, 1))
7116
7116
  ];
7117
7117
  }
7118
7118
 
7119
7119
  function interpolateGreatCircle(a, b, interval) {
7120
7120
  var av = degreesToVector$2(a[0], a[1]);
7121
7121
  var bv = degreesToVector$2(b[0], b[1]);
7122
- var angle = Math.acos(clamp$2(dot$2(av, bv), -1, 1));
7122
+ var angle = Math.acos(clamp$3(dot$2(av, bv), -1, 1));
7123
7123
  var n = Math.max(1, Math.ceil(angle * R2D$7 / interval));
7124
7124
  var sinAngle = Math.sin(angle);
7125
7125
  var points = [];
@@ -7178,7 +7178,7 @@
7178
7178
  }
7179
7179
 
7180
7180
  function angularDistance(a, b) {
7181
- return Math.acos(clamp$2(dot$2(degreesToVector$2(a[0], a[1]),
7181
+ return Math.acos(clamp$3(dot$2(degreesToVector$2(a[0], a[1]),
7182
7182
  degreesToVector$2(b[0], b[1])), -1, 1));
7183
7183
  }
7184
7184
 
@@ -7194,7 +7194,7 @@
7194
7194
  function vectorToDegrees$2(p) {
7195
7195
  return [
7196
7196
  Math.atan2(p[1], p[0]) * R2D$7,
7197
- Math.asin(clamp$2(p[2], -1, 1)) * R2D$7
7197
+ Math.asin(clamp$3(p[2], -1, 1)) * R2D$7
7198
7198
  ];
7199
7199
  }
7200
7200
 
@@ -8904,7 +8904,7 @@
8904
8904
  function interpolateSphericalRadians(a, b, interval) {
8905
8905
  var av = radiansToVector(a[0], a[1]);
8906
8906
  var bv = radiansToVector(b[0], b[1]);
8907
- var angle = Math.acos(clamp$2(dot$1(av, bv), -1, 1));
8907
+ var angle = Math.acos(clamp$3(dot$1(av, bv), -1, 1));
8908
8908
  var count = Math.max(1, Math.ceil(angle / interval));
8909
8909
  var sinAngle = Math.sin(angle);
8910
8910
  var points = [];
@@ -8924,7 +8924,7 @@
8924
8924
  function vectorToRadians(p) {
8925
8925
  return [
8926
8926
  Math.atan2(p[1], p[0]),
8927
- Math.asin(clamp$2(p[2], -1, 1))
8927
+ Math.asin(clamp$3(p[2], -1, 1))
8928
8928
  ];
8929
8929
  }
8930
8930
 
@@ -8998,8 +8998,8 @@
8998
8998
  }
8999
8999
  x = qx - BLOCK_HEIGHT;
9000
9000
  y = qy + 1.5;
9001
- x = clamp$2(x, XMIN, XMAX);
9002
- y = clamp$2(y, YMIN, YMAX);
9001
+ x = clamp$3(x, XMIN, XMAX);
9002
+ y = clamp$3(y, YMIN, YMAX);
9003
9003
  return {
9004
9004
  x: x,
9005
9005
  y: y,
@@ -9109,7 +9109,7 @@
9109
9109
  var v = radiansToVector(lam, phi);
9110
9110
  return [
9111
9111
  Math.atan2(dot$1(v, orientation.y), dot$1(v, orientation.x)),
9112
- Math.asin(clamp$2(dot$1(v, orientation.z), -1, 1))
9112
+ Math.asin(clamp$3(dot$1(v, orientation.z), -1, 1))
9113
9113
  ];
9114
9114
  }
9115
9115
 
@@ -9120,7 +9120,7 @@
9120
9120
  orientation.x[1] * v[0] + orientation.y[1] * v[1] + orientation.z[1] * v[2],
9121
9121
  orientation.x[2] * v[0] + orientation.y[2] * v[1] + orientation.z[2] * v[2]
9122
9122
  ];
9123
- return [Math.atan2(p[1], p[0]), Math.asin(clamp$2(p[2], -1, 1))];
9123
+ return [Math.atan2(p[1], p[0]), Math.asin(clamp$3(p[2], -1, 1))];
9124
9124
  }
9125
9125
 
9126
9126
  // Adapted from Justin Kunimune's Imago implementation.
@@ -9134,7 +9134,7 @@
9134
9134
  lat1 = lat;
9135
9135
  lon1 = lon - lon0;
9136
9136
  } else {
9137
- lat1 = Math.asin(clamp$2(
9137
+ lat1 = Math.asin(clamp$3(
9138
9138
  Math.sin(lat0) * Math.sin(lat) +
9139
9139
  Math.cos(lat0) * Math.cos(lat) * Math.cos(lon0 - lon),
9140
9140
  -1,
@@ -9145,7 +9145,7 @@
9145
9145
  (Math.cos(lat0) * Math.sin(lat) -
9146
9146
  Math.sin(lat0) * Math.cos(lat) * Math.cos(lon0 - lon)) /
9147
9147
  denominator;
9148
- lon1 = Math.acos(clamp$2(value, -1, 1)) - Math.PI;
9148
+ lon1 = Math.acos(clamp$3(value, -1, 1)) - Math.PI;
9149
9149
  if (Math.sin(lon - lon0) > 0) lon1 = -lon1;
9150
9150
  }
9151
9151
  return [lat1, normalizeRadians(lon1 - theta0)];
@@ -9155,7 +9155,7 @@
9155
9155
  var lat0 = pole.lat;
9156
9156
  var lon0 = pole.lon;
9157
9157
  lon += pole.meridian;
9158
- var latOut = Math.asin(clamp$2(
9158
+ var latOut = Math.asin(clamp$3(
9159
9159
  Math.sin(lat0) * Math.sin(lat) -
9160
9160
  Math.cos(lat0) * Math.cos(lon) * Math.cos(lat),
9161
9161
  -1,
@@ -9167,8 +9167,8 @@
9167
9167
  } else {
9168
9168
  var value = Math.sin(lat) / Math.cos(lat0) / Math.cos(latOut) -
9169
9169
  Math.tan(lat0) * Math.tan(latOut);
9170
- if (Math.sin(lon) > 0) lonOut = lon0 + Math.acos(clamp$2(value, -1, 1));
9171
- else lonOut = lon0 - Math.acos(clamp$2(value, -1, 1));
9170
+ if (Math.sin(lon) > 0) lonOut = lon0 + Math.acos(clamp$3(value, -1, 1));
9171
+ else lonOut = lon0 - Math.acos(clamp$3(value, -1, 1));
9172
9172
  }
9173
9173
  return [latOut, normalizeRadians(lonOut)];
9174
9174
  }
@@ -9643,8 +9643,8 @@
9643
9643
  var copy = getLayoutCopy(p);
9644
9644
  var q = applyMatrix(copy.matrix, p);
9645
9645
  q[0] = wrapLayoutX(q[0] + layoutPhase);
9646
- q[0] = clamp$2(q[0], RECT_XMIN, RECT_XMAX);
9647
- q[1] = clamp$2(q[1], RECT_YMIN, RECT_YMAX);
9646
+ q[0] = clamp$3(q[0], RECT_XMIN, RECT_XMAX);
9647
+ q[1] = clamp$3(q[1], RECT_YMIN, RECT_YMAX);
9648
9648
  return centerOutputPoint(q);
9649
9649
  }
9650
9650
 
@@ -9748,8 +9748,8 @@
9748
9748
  var copy = getLayoutCopy(p);
9749
9749
  var q = applyMatrix(copy.matrix, p);
9750
9750
  q[0] = wrapLayoutX(q[0] + layoutPhase);
9751
- q[0] = clamp$2(q[0], RECT_XMIN, RECT_XMAX);
9752
- q[1] = clamp$2(q[1], RECT_YMIN, RECT_YMAX);
9751
+ q[0] = clamp$3(q[0], RECT_XMIN, RECT_XMAX);
9752
+ q[1] = clamp$3(q[1], RECT_YMIN, RECT_YMAX);
9753
9753
  q = centerOutputPoint(q);
9754
9754
  var ids = rasterPieceIndex.get(getPieceKey(face, copy.id)) || [];
9755
9755
  var region = findPieceContainingPoint(rasterPieces, ids, q);
@@ -26883,12 +26883,14 @@
26883
26883
  'line-start': 'lineend',
26884
26884
  'line-end': 'lineend',
26885
26885
  'line-end-size': 'number',
26886
+ 'line-fade': 'number',
26886
26887
  'letter-spacing': 'measure',
26887
26888
  'line-height': 'measure',
26888
26889
  opacity: 'number',
26889
26890
  r: 'number',
26890
26891
  stroke: 'color',
26891
26892
  'stroke-dasharray': 'dasharray',
26893
+ 'stroke-linecap': 'linecap',
26892
26894
  'stroke-width': 'number',
26893
26895
  'stroke-opacity': 'number',
26894
26896
  'stroke-miterlimit': 'number',
@@ -26981,9 +26983,17 @@
26981
26983
  // symType: point, polygon, polyline, label
26982
26984
  function applyStyleAttributes(svgObj, symType, rec, filter) {
26983
26985
  var fields = findStylePropertiesBySymbolGeom(Object.keys(rec || {}), symType);
26986
+ var dashed = false, capped = false;
26984
26987
  for (var i=0, n=fields.length; i<n; i++) {
26985
26988
  if (filter && !filter(fields[i])) continue;
26986
26989
  setAttribute(svgObj, fields[i], rec[fields[i]]);
26990
+ if (fields[i] == 'stroke-dasharray' && rec[fields[i]]) dashed = true;
26991
+ if (fields[i] == 'stroke-linecap' && rec[fields[i]]) capped = true;
26992
+ }
26993
+ // kludge for cleaner dashes: butt caps, unless the line has a cap of its own
26994
+ // (the GUI canvas renderer does the same)
26995
+ if (dashed && !capped) {
26996
+ setAttribute(svgObj, 'stroke-linecap', 'butt');
26987
26997
  }
26988
26998
  // kludge to prevent default black fill on polygons with stroke styles
26989
26999
  if ((symType == 'polygon' || symType == 'circle') && rec.stroke && !rec.fill) {
@@ -26994,10 +27004,6 @@
26994
27004
  function setAttribute(obj, k, v) {
26995
27005
  if (!obj.properties) obj.properties = {};
26996
27006
  obj.properties[k] = v;
26997
- if (k == 'stroke-dasharray' && v) {
26998
- // kludge for cleaner dashes... make butt the default?
26999
- obj.properties['stroke-linecap'] = 'butt';
27000
- }
27001
27007
  }
27002
27008
 
27003
27009
  function isSupportedSvgStyleProperty(name) {
@@ -27196,6 +27202,8 @@
27196
27202
  val = parseCalloutEnd(strVal);
27197
27203
  } else if (type == 'lineend') {
27198
27204
  val = parseLineEnd(strVal);
27205
+ } else if (type == 'linecap') {
27206
+ val = parseLineCap(strVal);
27199
27207
  } else if (type == 'pointpair') {
27200
27208
  val = parsePointPair(strVal);
27201
27209
  val = val ? formatPointPair(val) : null;
@@ -27253,6 +27261,11 @@
27253
27261
  return /^(arrow|open-arrow|dot|none)$/.test(end) ? end : null;
27254
27262
  }
27255
27263
 
27264
+ function parseLineCap(str) {
27265
+ var cap = String(str).trim().toLowerCase();
27266
+ return /^(round|butt|square)$/.test(cap) ? cap : null;
27267
+ }
27268
+
27256
27269
  // "x,y" -> [x, y], or null. Stored as a string so that a pair is set and
27257
27270
  // cleared as one value and reads plainly in a table.
27258
27271
  function parsePointPair(str) {
@@ -27372,6 +27385,7 @@
27372
27385
  parseCalloutEnd: parseCalloutEnd,
27373
27386
  parseCalloutType: parseCalloutType,
27374
27387
  parseLabelPosition: parseLabelPosition,
27388
+ parseLineCap: parseLineCap,
27375
27389
  parseLineEnd: parseLineEnd,
27376
27390
  parsePointPair: parsePointPair,
27377
27391
  parseStyleLiteral: parseStyleLiteral,
@@ -27400,7 +27414,8 @@
27400
27414
  // In the GUI the halo is the text element's own stroke, with paint-order
27401
27415
  // putting it underneath. Export draws it as a second copy of the text instead
27402
27416
  // -- see splitLabelHalos() -- because Illustrator and Figma ignore paint-order
27403
- // when they import SVG and would draw the stroke over the letters.
27417
+ // when they import SVG and would draw the stroke over the letters, and Chrome
27418
+ // and Safari draw paint-order halos badly where letters overlap.
27404
27419
 
27405
27420
  var DEFAULT_HALO_COLOR = '#ffffff';
27406
27421
  var HALO_PAINT_ORDER = 'stroke fill';
@@ -27461,6 +27476,10 @@
27461
27476
  // which places it, and its opacity, which would otherwise fade each copy on its
27462
27477
  // own and let the halo show through the letters. That is also how opacity
27463
27478
  // reads on a single element with paint-order, which is faded as one image.
27479
+ //
27480
+ // Browsers need the copy too: Chrome and Safari paint a text element's stroke
27481
+ // and fill one glyph at a time, so with paint-order the halos of overlapping
27482
+ // letters compound a translucent halo, and cover the letters beside them.
27464
27483
  function splitLabelHalos(o) {
27465
27484
  if (!o) return o;
27466
27485
  if (o.tag == 'text' && isHaloText(o)) return splitHalo(o);
@@ -29542,6 +29561,48 @@
29542
29561
  return len;
29543
29562
  }
29544
29563
 
29564
+ // The point @dist along the fitted curve from its first knot, clamped to the
29565
+ // curve's ends, or null if the knots collapse to a single point.
29566
+ function getPointAtCurveLength(knots, dist) {
29567
+ var segments = getCurveSegments(knots);
29568
+ var seg, len, i;
29569
+ if (segments.length === 0) return null;
29570
+ for (i = 0; i < segments.length; i++) {
29571
+ seg = segments[i];
29572
+ len = getCubicLength(seg, 0);
29573
+ if (dist <= len) return getPointAtCubicLength(seg, Math.max(dist, 0));
29574
+ dist -= len;
29575
+ }
29576
+ return [seg.p3[0], seg.p3[1]];
29577
+ }
29578
+
29579
+ // Walks a flattening of the segment, which is shorter than the true curve by
29580
+ // a negligible amount at this many steps.
29581
+ function getPointAtCubicLength(seg, dist) {
29582
+ var steps = 64;
29583
+ var prev = seg.p0, p, d, k, i;
29584
+ for (i = 1; i <= steps; i++) {
29585
+ p = getCubicPoint(seg, i / steps);
29586
+ d = distance2D$1(prev[0], prev[1], p[0], p[1]);
29587
+ if (d > 0 && d >= dist) {
29588
+ k = dist / d;
29589
+ return [prev[0] + (p[0] - prev[0]) * k, prev[1] + (p[1] - prev[1]) * k];
29590
+ }
29591
+ dist -= d;
29592
+ prev = p;
29593
+ }
29594
+ return [seg.p3[0], seg.p3[1]];
29595
+ }
29596
+
29597
+ function getCubicPoint(seg, t) {
29598
+ var mt = 1 - t;
29599
+ var a = mt * mt * mt, b = 3 * mt * mt * t, c = 3 * mt * t * t, d = t * t * t;
29600
+ return [
29601
+ a * seg.p0[0] + b * seg.c1[0] + c * seg.c2[0] + d * seg.p3[0],
29602
+ a * seg.p0[1] + b * seg.c1[1] + c * seg.c2[1] + d * seg.p3[1]
29603
+ ];
29604
+ }
29605
+
29545
29606
  // Hobby's velocity function constants (Hobby 1986, eq. 11), the approximation
29546
29607
  // Metafont uses in place of the transcendental form.
29547
29608
  var VEL_A = Math.SQRT2,
@@ -29822,13 +29883,14 @@
29822
29883
  getCurveCurl: getCurveCurl,
29823
29884
  getCurveLength: getCurveLength,
29824
29885
  getCurveSegments: getCurveSegments,
29886
+ getPointAtCurveLength: getPointAtCurveLength,
29825
29887
  setCurveCurl: setCurveCurl
29826
29888
  });
29827
29889
 
29828
- var roundCoord$2 = getRoundingFunction(0.01);
29890
+ var roundCoord$3 = getRoundingFunction(0.01);
29829
29891
 
29830
29892
  function stringifyVertex(p) {
29831
- return ' ' + roundCoord$2(p[0]) + ' ' + roundCoord$2(p[1]);
29893
+ return ' ' + roundCoord$3(p[0]) + ' ' + roundCoord$3(p[1]);
29832
29894
  }
29833
29895
 
29834
29896
  function isCubicCtrl(p) {
@@ -30125,6 +30187,7 @@
30125
30187
  LABEL_OVERFLOW_CLASS: LABEL_OVERFLOW_CLASS,
30126
30188
  LABEL_PATH_PROPERTY: LABEL_PATH_PROPERTY,
30127
30189
  featureIsPathLabel: featureIsPathLabel,
30190
+ getDefaultStartOffset: getDefaultStartOffset,
30128
30191
  getLabelPathCoords: getLabelPathCoords,
30129
30192
  getLabelPathData: getLabelPathData,
30130
30193
  initPathLabelReport: initPathLabelReport,
@@ -30463,139 +30526,6 @@
30463
30526
  renderGlowFilter: renderGlowFilter
30464
30527
  });
30465
30528
 
30466
- // convert object properties to definitions for images and hatch fills
30467
- function convertPropertiesToDefinitions(obj, defs) {
30468
- procNode(obj);
30469
-
30470
- function procNode(obj) {
30471
- if (obj.glowFilter) {
30472
- convertGlowFilter(obj, defs);
30473
- }
30474
- if (obj.tag == 'textPath' && obj.properties && obj.properties[LABEL_PATH_PROPERTY]) {
30475
- convertLabelPath(obj.properties, defs);
30476
- }
30477
- if (obj.tag == 'path' && obj.properties['fill-pattern']) {
30478
- convertFillPattern(obj.properties, defs);
30479
- }
30480
- if (obj.tag == 'path' && obj.properties['fill-effect']) {
30481
- convertFillEffect(obj.properties, defs);
30482
- }
30483
- if (obj.tag == 'image') {
30484
- if (/\.svg/.test(obj.properties.href || '')) {
30485
- convertSvgImage(obj, defs);
30486
- }
30487
- } else if (obj.children) {
30488
- obj.children.forEach(procNode);
30489
- }
30490
- }
30491
- }
30492
-
30493
- // Moves a label's baseline into <defs>, where it is referenced but never
30494
- // painted. It must not be left in the document body: a <path> with no stroke or
30495
- // fill of its own picks up SVG's defaults -- no stroke and *black fill* -- and
30496
- // renders as a filled blob instead of a line.
30497
- //
30498
- // The id is derived from the path data, so identical paths share one
30499
- // definition and repeated exports of the same map produce identical output.
30500
- function convertLabelPath(properties, defs) {
30501
- var d = properties[LABEL_PATH_PROPERTY];
30502
- var item = utils.find(defs, function(item) {return item.labelPath === d;});
30503
- delete properties[LABEL_PATH_PROPERTY];
30504
- if (!item) {
30505
- item = {
30506
- labelPath: d,
30507
- id: 'label-path-' + sha1(d).substr(0, 12)
30508
- };
30509
- item.svg = '<path id="' + item.id + '" d="' + d + '"/>\n';
30510
- defs.push(item);
30511
- }
30512
- properties.href = '#' + item.id;
30513
- }
30514
-
30515
- function convertSvgImage(obj, defs) {
30516
- // Same-origin policy prevents embedding images in the web UI
30517
- var href = obj.properties.href;
30518
- // Remote URLs were previously fetched at export time so the SVG could be
30519
- // embedded inline. That capability has been removed (it required a
30520
- // synchronous-HTTP dependency built on a child-process hack); download
30521
- // the asset first and reference it by local path instead.
30522
- if (href.indexOf('http') === 0) {
30523
- stop$1('Remote SVG asset references are not supported. Download the file first and reference it by a local path:', href);
30524
- }
30525
- // look for a previously added definition to use
30526
- // (assumes that images that share the same href can also use the same defn)
30527
- var item = utils.find(defs, function(item) {return item.href == href;});
30528
- if (!item) {
30529
- item = {
30530
- href: href,
30531
- id: urlToId(href) // generating id from href, to try to support multiple inline svgs on page
30532
- };
30533
- item.svg = serializeSvgImage(href, item.id);
30534
- defs.push(item);
30535
- }
30536
- if (item.svg) {
30537
- obj.tag = 'use';
30538
- obj.properties.href = '#' + item.id;
30539
- }
30540
- }
30541
-
30542
- // Returns the content of an SVG file from a local path.
30543
- // Returns '' if unable to get the content; the caller leaves the original
30544
- // <image> tag in the output so the SVG renderer can attempt to load it.
30545
- function serializeSvgImage(href, id) {
30546
- var svg = '';
30547
- try {
30548
- svg = convertSvgToDefn(getSvgContent(href), id) + '\n';
30549
- svg = '<!-- ' + href + '-->\n' + svg; // add href as a comment, to aid in debugging
30550
- } catch(e) {
30551
- // tried creating a symbol as a fallback... encounted problems with icon
30552
- // size and placement, giving up on this for now
30553
- // svg = `<symbol><image xlink:href="${obj.properties.href}" id="${id}"></image></symbol>`;
30554
- }
30555
- return svg;
30556
- }
30557
-
30558
- // href: A local path
30559
- function getSvgContent(href) {
30560
- if (require$1('fs').existsSync(href)) {
30561
- return require$1('fs').readFileSync(href, 'utf8');
30562
- }
30563
- stop$1("Invalid SVG location:", href);
30564
- }
30565
-
30566
- function convertSvgToDefn(svg, id) {
30567
- // Remove stuff before <svg> tag
30568
- svg = svg.replace(/[^]*<svg/, '<svg');
30569
- return svg.replace(/^<svg[^>]*>/, function(a) {
30570
- // set id property of <svg>
30571
- a = a.replace(/ id="[^"]*"/, '');
30572
- a = a.replace(/<svg/, '<svg id="' + id + '"');
30573
- return a;
30574
- });
30575
- }
30576
-
30577
- function urlToId(url) {
30578
- return sha1(url).substr(0, 12);
30579
- }
30580
-
30581
- /*
30582
- function convertSvgToSymbol(svg, id) {
30583
- svg = svg.replace(/[^]*<svg/, '<svg');
30584
- // Remove inkscape tags (there were errors caused when namespaces were
30585
- // stripped when converting <svg> to <symbol> ... this may be futile, may
30586
- // have to go back to embedding entire SVG document instead of using symbols)
30587
- svg = svg.replace(/<metadata[^]*?metadata>/, '');
30588
- svg = svg.replace(/<sodipodi[^>]*>/, '');
30589
- // convert <svg> to <symbol>
30590
- svg = svg.replace(/^<svg[^>]*>/, function(a) {
30591
- var viewBox = a.match(/viewBox=".*?"/)[0];
30592
- return '<symbol id="' + id + '" ' + viewBox + '>';
30593
- });
30594
- svg = svg.replace('svg>', 'symbol>');
30595
- return svg;
30596
- }
30597
- */
30598
-
30599
30529
  // Markers at the ends of line features: line-start= and line-end= take
30600
30530
  // arrow (a filled triangle), open-arrow (a stroked chevron), dot (a filled
30601
30531
  // circle centred on the end) or none, and line-end-size= is the length of a
@@ -30608,6 +30538,14 @@
30608
30538
  // part of a multi-part line gets its own heads: part order is not something
30609
30539
  // the commands that make multi-part lines preserve.
30610
30540
  //
30541
+ // line-fade= fades the line in from its tail: 0-1, the share of the line's
30542
+ // length over which it goes from transparent to its full opacity. The tail is
30543
+ // the start, unless only the start has a head. The fade is a straight linear
30544
+ // gradient from the tail to the point that far along the line, which follows a
30545
+ // straight or gently curved line closely; SVG and canvas have no gradient that
30546
+ // follows a path, so a line that bends sharply within its fade will not fade
30547
+ // evenly along its length.
30548
+ //
30611
30549
  // Coordinates are px, y down: the SVG export's space, or the canvas's.
30612
30550
 
30613
30551
  var DEFAULT_LINE_WIDTH = 1;
@@ -30624,16 +30562,26 @@
30624
30562
  getLineEndType(rec, 'line-end') != 'none';
30625
30563
  }
30626
30564
 
30565
+ // The share of the line that fades, 0 for none
30566
+ function parseLineFade(val) {
30567
+ return isSvgNumber(val) && Number(val) > 0 ? Math.min(Number(val), 1) : 0;
30568
+ }
30569
+
30570
+ // Whether a line is drawn by renderArrowLine() rather than as a plain path
30571
+ function lineHasEndStyles(rec) {
30572
+ return lineHasArrows(rec) || parseLineFade(rec && rec['line-fade']) > 0;
30573
+ }
30574
+
30627
30575
  // What getLineArrowShape() needs from a record, at scale @scale (the factor
30628
30576
  // the canvas multiplies line widths by; 1 for SVG).
30629
30577
  function getLineArrowOpts(rec, scale) {
30630
30578
  return makeLineArrowOpts(rec['line-start'], rec['line-end'], rec['line-end-size'],
30631
- rec['stroke-width'], scale);
30579
+ rec['stroke-width'], scale, rec['line-fade']);
30632
30580
  }
30633
30581
 
30634
30582
  // The same, from the values themselves, for the canvas, which has them in a
30635
30583
  // style object rather than a record.
30636
- function makeLineArrowOpts(start, end, size, strokeWidth, scale) {
30584
+ function makeLineArrowOpts(start, end, size, strokeWidth, scale, fade) {
30637
30585
  var k = scale > 0 ? scale : 1;
30638
30586
  var w = isSvgNumber(strokeWidth) && Number(strokeWidth) >= 0 ?
30639
30587
  Number(strokeWidth) : DEFAULT_LINE_WIDTH;
@@ -30644,18 +30592,20 @@
30644
30592
  end: end && parseLineEnd(end) || 'none',
30645
30593
  size: side * k,
30646
30594
  dotSize: (hasSize ? side : side * DOT_SIZE_RATIO) * k,
30647
- width: w * k
30595
+ width: w * k,
30596
+ fade: parseLineFade(fade)
30648
30597
  };
30649
30598
  }
30650
30599
 
30651
30600
  // Field names, for a layer that may have them
30652
- var lineArrowFields = ['line-start', 'line-end', 'line-end-size'];
30601
+ var lineArrowFields = ['line-start', 'line-end', 'line-end-size', 'line-fade'];
30653
30602
 
30654
- // One part of a line, with its heads: {coords, heads}, where coords is the
30655
- // line to stroke -- cut back into a solid head, so that its cap does not show
30656
- // past the tip -- and heads is a list of {type, points}: a solid head's
30603
+ // One part of a line, with its heads: {coords, heads, fade}, where coords is
30604
+ // the line to stroke -- cut back into a solid head, so that its cap does not
30605
+ // show past the tip -- and heads is a list of {type, points}: a solid head's
30657
30606
  // triangle, tip first, or an open head's chevron, wing to tip to wing. A dot
30658
- // is {type, center, radius}.
30607
+ // is {type, center, radius}. fade is the axis of the line's fade (see
30608
+ // getLineFadeAxis()), or null.
30659
30609
  //
30660
30610
  // A head points along the chord from its tip to where the line first reaches
30661
30611
  // the head's length from it, rather than along the last segment, which on
@@ -30663,8 +30613,11 @@
30663
30613
  // anywhere. The line is then straightened to meet the head along its axis. An
30664
30614
  // end whose part is too short to hold its head gets none.
30665
30615
  function getLineArrowShape(coords, opts) {
30666
- var out = {coords: coords, heads: []};
30616
+ var out = {coords: coords, heads: [], fade: null};
30667
30617
  var end;
30618
+ if (opts.fade > 0) {
30619
+ out.fade = getLineFadeAxis(coords, opts.start != 'none' && opts.end == 'none', opts.fade);
30620
+ }
30668
30621
  if (opts.start != 'none') {
30669
30622
  addHead(out, opts.start, opts);
30670
30623
  }
@@ -30676,6 +30629,45 @@
30676
30629
  return out;
30677
30630
  }
30678
30631
 
30632
+ // {from, to}: the line's tail, where the fade is transparent, and the point
30633
+ // @fraction of the line's length along it from there, where the fade ends.
30634
+ // Measured on the whole line, before the heads trim it. Null for a line too
30635
+ // short to fade, or a closed one faded all the way round, whose two points
30636
+ // would be the same.
30637
+ function getLineFadeAxis(coords, fromEnd, fraction) {
30638
+ var path = fromEnd ? coords.slice().reverse() : coords;
30639
+ var total = 0, target, d, i, to, k;
30640
+ for (i=1; i<path.length; i++) {
30641
+ total += distance$1(path[i - 1], path[i]);
30642
+ }
30643
+ target = total * fraction;
30644
+ to = path[path.length - 1];
30645
+ for (i=1; i<path.length && target > 0; i++) {
30646
+ d = distance$1(path[i - 1], path[i]);
30647
+ if (d >= target) {
30648
+ k = target / d;
30649
+ to = [path[i - 1][0] + (path[i][0] - path[i - 1][0]) * k,
30650
+ path[i - 1][1] + (path[i][1] - path[i - 1][1]) * k];
30651
+ break;
30652
+ }
30653
+ target -= d;
30654
+ }
30655
+ if (!(total > 0) || distance$1(path[0], to) < 0.5) return null;
30656
+ return {from: path[0], to: to};
30657
+ }
30658
+
30659
+ // The fade's two colours: the stroke's, and the same with no opacity, so
30660
+ // that the colour does not shift on its way to transparent. Null for a stroke
30661
+ // colour that cannot be faded (e.g. 'none').
30662
+ function getLineFadeColors(color) {
30663
+ var rgb = parseColor(color || 'black');
30664
+ if (!rgb) return null;
30665
+ return {
30666
+ transparent: 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',0)',
30667
+ solid: color || 'black'
30668
+ };
30669
+ }
30670
+
30679
30671
  // Puts a head of @type at the first point of @shape.coords
30680
30672
  function addHead(shape, type, opts) {
30681
30673
  var coords = shape.coords;
@@ -30725,23 +30717,64 @@
30725
30717
  if (rest) shape.coords = rest;
30726
30718
  }
30727
30719
 
30728
- // A line feature with arrowheads, as SVG: a group holding the line and its
30729
- // heads, for the feature's style attributes to go on. @parts is a list of
30730
- // polylines (a LineString's coordinates are one).
30720
+ // A line feature with arrowheads or a fade, as SVG: a group holding the line
30721
+ // and its heads, for the feature's style attributes to go on. @parts is a list
30722
+ // of polylines (a LineString's coordinates are one). A faded part is a path of
30723
+ // its own, since each part has its own gradient.
30731
30724
  function renderArrowLine(parts, rec) {
30732
30725
  var opts = getLineArrowOpts(rec, 1);
30733
30726
  var color = rec.stroke || 'black';
30727
+ var fadeColors = opts.fade > 0 ? getLineFadeColors(color) : null;
30734
30728
  var lines = [];
30735
- var children = [];
30729
+ var fadedLines = [];
30730
+ var heads = [];
30736
30731
  parts.forEach(function(part) {
30737
30732
  var shape = getLineArrowShape(part, opts);
30738
- lines.push(shape.coords.map(roundPoint));
30733
+ var coords = shape.coords.map(roundPoint);
30734
+ if (shape.fade && fadeColors) {
30735
+ fadedLines.push(renderFadedLine(coords, shape.fade, fadeColors));
30736
+ } else {
30737
+ lines.push(coords);
30738
+ }
30739
30739
  shape.heads.forEach(function(head) {
30740
- children.push(renderHead(head, color, rec));
30740
+ heads.push(renderHead(head, color, rec));
30741
30741
  });
30742
30742
  });
30743
- children.unshift(importMultiLineString(lines));
30744
- return {tag: 'g', properties: {}, children: children};
30743
+ var children = lines.length > 0 ? [importMultiLineString(lines)] : [];
30744
+ return {tag: 'g', properties: {}, children: children.concat(fadedLines, heads)};
30745
+ }
30746
+
30747
+ // The gradient is added to the SVG's <defs> by convertLineFade(), which also
30748
+ // points the path's stroke at it.
30749
+ function renderFadedLine(coords, axis, colors) {
30750
+ var obj = importLineString(coords);
30751
+ var from = roundPoint(axis.from);
30752
+ var to = roundPoint(axis.to);
30753
+ obj.lineFade = {x1: from[0], y1: from[1], x2: to[0], y2: to[1], color: colors.solid};
30754
+ return obj;
30755
+ }
30756
+
30757
+ // Replaces the fade an object was marked with by a reference to a
30758
+ // <linearGradient> in @defs, which lines with the same fade share. The
30759
+ // gradient is in the path's own coordinates (userSpaceOnUse): the default, the
30760
+ // path's bounding box, has no height for a horizontal line, and a gradient
30761
+ // over an empty box paints nothing.
30762
+ function convertLineFade(obj, defs) {
30763
+ var f = obj.lineFade;
30764
+ var color = String(f.color).replace(/"/g, '');
30765
+ var body = 'gradientUnits="userSpaceOnUse" x1="' + f.x1 + '" y1="' + f.y1 +
30766
+ '" x2="' + f.x2 + '" y2="' + f.y2 + '"><stop offset="0" stop-color="' + color +
30767
+ '" stop-opacity="0"/><stop offset="1" stop-color="' + color + '"/></linearGradient>';
30768
+ var item = defs.find(function(o) { return o.lineFadeKey === body; });
30769
+ var count;
30770
+ if (!item) {
30771
+ count = defs.filter(function(o) { return !!o.lineFadeKey; }).length;
30772
+ item = {lineFadeKey: body, id: 'line-fade-' + (count + 1)};
30773
+ item.svg = '<linearGradient id="' + item.id + '" ' + body + '\n';
30774
+ defs.push(item);
30775
+ }
30776
+ delete obj.lineFade;
30777
+ obj.properties.stroke = 'url(#' + item.id + ')';
30745
30778
  }
30746
30779
 
30747
30780
  function renderHead(head, color, rec) {
@@ -30779,15 +30812,156 @@
30779
30812
 
30780
30813
  var SvgLineArrows = /*#__PURE__*/Object.freeze({
30781
30814
  __proto__: null,
30815
+ convertLineFade: convertLineFade,
30782
30816
  getLineArrowOpts: getLineArrowOpts,
30783
30817
  getLineArrowShape: getLineArrowShape,
30784
30818
  getLineEndType: getLineEndType,
30819
+ getLineFadeAxis: getLineFadeAxis,
30820
+ getLineFadeColors: getLineFadeColors,
30785
30821
  lineArrowFields: lineArrowFields,
30786
30822
  lineHasArrows: lineHasArrows,
30823
+ lineHasEndStyles: lineHasEndStyles,
30787
30824
  makeLineArrowOpts: makeLineArrowOpts,
30825
+ parseLineFade: parseLineFade,
30788
30826
  renderArrowLine: renderArrowLine
30789
30827
  });
30790
30828
 
30829
+ // convert object properties to definitions for images and hatch fills
30830
+ function convertPropertiesToDefinitions(obj, defs) {
30831
+ procNode(obj);
30832
+
30833
+ function procNode(obj) {
30834
+ if (obj.glowFilter) {
30835
+ convertGlowFilter(obj, defs);
30836
+ }
30837
+ if (obj.lineFade) {
30838
+ convertLineFade(obj, defs);
30839
+ }
30840
+ if (obj.tag == 'textPath' && obj.properties && obj.properties[LABEL_PATH_PROPERTY]) {
30841
+ convertLabelPath(obj.properties, defs);
30842
+ }
30843
+ if (obj.tag == 'path' && obj.properties['fill-pattern']) {
30844
+ convertFillPattern(obj.properties, defs);
30845
+ }
30846
+ if (obj.tag == 'path' && obj.properties['fill-effect']) {
30847
+ convertFillEffect(obj.properties, defs);
30848
+ }
30849
+ if (obj.tag == 'image') {
30850
+ if (/\.svg/.test(obj.properties.href || '')) {
30851
+ convertSvgImage(obj, defs);
30852
+ }
30853
+ } else if (obj.children) {
30854
+ obj.children.forEach(procNode);
30855
+ }
30856
+ }
30857
+ }
30858
+
30859
+ // Moves a label's baseline into <defs>, where it is referenced but never
30860
+ // painted. It must not be left in the document body: a <path> with no stroke or
30861
+ // fill of its own picks up SVG's defaults -- no stroke and *black fill* -- and
30862
+ // renders as a filled blob instead of a line.
30863
+ //
30864
+ // The id is derived from the path data, so identical paths share one
30865
+ // definition and repeated exports of the same map produce identical output.
30866
+ function convertLabelPath(properties, defs) {
30867
+ var d = properties[LABEL_PATH_PROPERTY];
30868
+ var item = utils.find(defs, function(item) {return item.labelPath === d;});
30869
+ delete properties[LABEL_PATH_PROPERTY];
30870
+ if (!item) {
30871
+ item = {
30872
+ labelPath: d,
30873
+ id: 'label-path-' + sha1(d).substr(0, 12)
30874
+ };
30875
+ item.svg = '<path id="' + item.id + '" d="' + d + '"/>\n';
30876
+ defs.push(item);
30877
+ }
30878
+ properties.href = '#' + item.id;
30879
+ }
30880
+
30881
+ function convertSvgImage(obj, defs) {
30882
+ // Same-origin policy prevents embedding images in the web UI
30883
+ var href = obj.properties.href;
30884
+ // Remote URLs were previously fetched at export time so the SVG could be
30885
+ // embedded inline. That capability has been removed (it required a
30886
+ // synchronous-HTTP dependency built on a child-process hack); download
30887
+ // the asset first and reference it by local path instead.
30888
+ if (href.indexOf('http') === 0) {
30889
+ stop$1('Remote SVG asset references are not supported. Download the file first and reference it by a local path:', href);
30890
+ }
30891
+ // look for a previously added definition to use
30892
+ // (assumes that images that share the same href can also use the same defn)
30893
+ var item = utils.find(defs, function(item) {return item.href == href;});
30894
+ if (!item) {
30895
+ item = {
30896
+ href: href,
30897
+ id: urlToId(href) // generating id from href, to try to support multiple inline svgs on page
30898
+ };
30899
+ item.svg = serializeSvgImage(href, item.id);
30900
+ defs.push(item);
30901
+ }
30902
+ if (item.svg) {
30903
+ obj.tag = 'use';
30904
+ obj.properties.href = '#' + item.id;
30905
+ }
30906
+ }
30907
+
30908
+ // Returns the content of an SVG file from a local path.
30909
+ // Returns '' if unable to get the content; the caller leaves the original
30910
+ // <image> tag in the output so the SVG renderer can attempt to load it.
30911
+ function serializeSvgImage(href, id) {
30912
+ var svg = '';
30913
+ try {
30914
+ svg = convertSvgToDefn(getSvgContent(href), id) + '\n';
30915
+ svg = '<!-- ' + href + '-->\n' + svg; // add href as a comment, to aid in debugging
30916
+ } catch(e) {
30917
+ // tried creating a symbol as a fallback... encounted problems with icon
30918
+ // size and placement, giving up on this for now
30919
+ // svg = `<symbol><image xlink:href="${obj.properties.href}" id="${id}"></image></symbol>`;
30920
+ }
30921
+ return svg;
30922
+ }
30923
+
30924
+ // href: A local path
30925
+ function getSvgContent(href) {
30926
+ if (require$1('fs').existsSync(href)) {
30927
+ return require$1('fs').readFileSync(href, 'utf8');
30928
+ }
30929
+ stop$1("Invalid SVG location:", href);
30930
+ }
30931
+
30932
+ function convertSvgToDefn(svg, id) {
30933
+ // Remove stuff before <svg> tag
30934
+ svg = svg.replace(/[^]*<svg/, '<svg');
30935
+ return svg.replace(/^<svg[^>]*>/, function(a) {
30936
+ // set id property of <svg>
30937
+ a = a.replace(/ id="[^"]*"/, '');
30938
+ a = a.replace(/<svg/, '<svg id="' + id + '"');
30939
+ return a;
30940
+ });
30941
+ }
30942
+
30943
+ function urlToId(url) {
30944
+ return sha1(url).substr(0, 12);
30945
+ }
30946
+
30947
+ /*
30948
+ function convertSvgToSymbol(svg, id) {
30949
+ svg = svg.replace(/[^]*<svg/, '<svg');
30950
+ // Remove inkscape tags (there were errors caused when namespaces were
30951
+ // stripped when converting <svg> to <symbol> ... this may be futile, may
30952
+ // have to go back to embedding entire SVG document instead of using symbols)
30953
+ svg = svg.replace(/<metadata[^]*?metadata>/, '');
30954
+ svg = svg.replace(/<sodipodi[^>]*>/, '');
30955
+ // convert <svg> to <symbol>
30956
+ svg = svg.replace(/^<svg[^>]*>/, function(a) {
30957
+ var viewBox = a.match(/viewBox=".*?"/)[0];
30958
+ return '<symbol id="' + id + '" ' + viewBox + '>';
30959
+ });
30960
+ svg = svg.replace('svg>', 'symbol>');
30961
+ return svg;
30962
+ }
30963
+ */
30964
+
30791
30965
  var geojsonImporters = {
30792
30966
  Point: importPoint,
30793
30967
  Polygon: importPolygon,
@@ -30810,7 +30984,7 @@
30810
30984
  // rather than several labels at several points
30811
30985
  svgObj = renderPathLabel(d, geom.coordinates,
30812
30986
  {report: opts.path_label_report, id: featureId});
30813
- } else if (msType == 'polyline' && geom.coordinates && lineHasArrows(d)) {
30987
+ } else if (msType == 'polyline' && geom.coordinates && lineHasEndStyles(d)) {
30814
30988
  svgObj = renderArrowLine(geomType == 'LineString' ?
30815
30989
  [geom.coordinates] : geom.coordinates, d);
30816
30990
  } else if (geomType && geom.coordinates) {
@@ -30895,20 +31069,19 @@
30895
31069
 
30896
31070
  //
30897
31071
  function exportSVG(dataset, opts) {
30898
- var namespace = 'xmlns="http://www.w3.org/2000/svg"';
30899
- var defs = [];
30900
- var frame, frameLyr, frameBackground, frameNeatline;
30901
- var svg, layers, metadataJSON, files, svgFile;
30902
- var style = '';
30903
-
30904
- // kludge for map keys
30905
- if (opts.crisp_paths) {
30906
- style = `
30907
- <style>
30908
- path {shape-rendering: crispEdges;}
30909
- </style>`;
30910
- }
31072
+ var o = prepareDatasetForSVG(dataset, opts);
31073
+ var svgFile = {
31074
+ content: renderSVGDocument(o.dataset, o.frame, o.dataset.layers, o.opts),
31075
+ filename: o.opts.file || getOutputFileBase(o.dataset) + '.svg'
31076
+ };
31077
+ return [svgFile].concat(o.opts.svg_image_files);
31078
+ }
30911
31079
 
31080
+ // Returns a copy of @dataset (unless opts.final is set) with coordinates
31081
+ // transformed to the pixel space of the output frame, along with the frame and
31082
+ // the export options, which are given SVG defaults.
31083
+ function prepareDatasetForSVG(dataset, opts) {
31084
+ var frame;
30912
31085
  // TODO: consider moving this logic to mapshaper-export.js
30913
31086
  if (opts.final) {
30914
31087
  if (dataset.arcs) dataset.arcs.flatten();
@@ -30924,16 +31097,35 @@
30924
31097
  frame = getFrameData(dataset, opts);
30925
31098
  fitDatasetToFrame(dataset, frame);
30926
31099
  setCoordinatePrecision(dataset, opts.precision || 0.01);
30927
- if (opts.metadata) {
30928
- metadataJSON = JSON.stringify(getGeospatialMetadata(dataset, frame));
30929
- }
30930
31100
 
30931
31101
  // error if one or more svg_data fields are not present in any layers
30932
31102
  if (opts.svg_data) validateSvgDataFields(dataset.layers, opts.svg_data);
31103
+ return {dataset: dataset, frame: frame, opts: opts};
31104
+ }
31105
+
31106
+ // Renders @layers, a subset of the layers of a dataset prepared by
31107
+ // prepareDatasetForSVG(), as an SVG document.
31108
+ function renderSVGDocument(dataset, frame, layers, opts) {
31109
+ var namespace = 'xmlns="http://www.w3.org/2000/svg"';
31110
+ var defs = [];
31111
+ var frameLyr, frameBackground, frameNeatline;
31112
+ var svg, metadataJSON;
31113
+ var style = '';
31114
+
31115
+ // kludge for map keys
31116
+ if (opts.crisp_paths) {
31117
+ style = `
31118
+ <style>
31119
+ path {shape-rendering: crispEdges;}
31120
+ </style>`;
31121
+ }
31122
+
31123
+ if (opts.metadata) {
31124
+ metadataJSON = JSON.stringify(getGeospatialMetadata(dataset, frame));
31125
+ }
30933
31126
 
30934
- layers = dataset.layers;
30935
31127
  if (opts.scalebar) {
30936
- layers.push(getScalebarLayer({})); // default options
31128
+ layers = layers.concat(getScalebarLayer({})); // default options
30937
31129
  }
30938
31130
  frameLyr = layers.find(function(lyr) {
30939
31131
  return isFrameLayer(lyr, dataset.arcs);
@@ -30989,13 +31181,7 @@
30989
31181
  <svg ${namespace} version="1.2" baseProfile="tiny" width="%d" height="%d" viewBox="%s %s %s %s" ${lineProps}>${style}
30990
31182
  ${svg}
30991
31183
  </svg>`;
30992
- svg = utils.format(template, frame.width, frame.height, 0, 0, frame.width, frame.height);
30993
- svgFile = {
30994
- content: svg,
30995
- filename: opts.file || getOutputFileBase(dataset) + '.svg'
30996
- };
30997
- files = [svgFile].concat(opts.svg_image_files);
30998
- return files;
31184
+ return utils.format(template, frame.width, frame.height, 0, 0, frame.width, frame.height);
30999
31185
  }
31000
31186
 
31001
31187
  function exportFrameStylePhase(lyr, dataset, opts, phase) {
@@ -31059,7 +31245,7 @@ ${svg}
31059
31245
 
31060
31246
  function getSvgFileBase(dataset, opts) {
31061
31247
  var file = opts.file || getOutputFileBase(dataset) + '.svg';
31062
- return file.replace(/\.svg$/i, '');
31248
+ return file.replace(/\.(svg|html?)$/i, '');
31063
31249
  }
31064
31250
 
31065
31251
  function getMetadataBlock(metadataJSON, viewBox) {
@@ -31558,12 +31744,1064 @@ ${svg}
31558
31744
  featureHasLabel: featureHasLabel,
31559
31745
  featureHasSvgSymbol: featureHasSvgSymbol,
31560
31746
  getEmptyLayerForSVG: getEmptyLayerForSVG,
31747
+ getJpegQuality: getJpegQuality,
31561
31748
  getLabelTextDefaults: getLabelTextDefaults,
31562
31749
  layerHasLabels: layerHasLabels,
31563
31750
  layerHasSvgSymbols: layerHasSvgSymbols,
31751
+ prepareDatasetForSVG: prepareDatasetForSVG,
31752
+ renderSVGDocument: renderSVGDocument,
31564
31753
  validateSvgDataFields: validateSvgDataFields
31565
31754
  });
31566
31755
 
31756
+ // Finding the font file a label's font-family names, among the fonts installed
31757
+ // on this computer.
31758
+ //
31759
+ // The browser does this for us in the GUI; in Node there is nothing between
31760
+ // mapshaper and the filesystem, so a family name has to be matched against the
31761
+ // name tables of the files themselves. Names cannot be inferred from
31762
+ // filenames: NYTFranklinLight.otf calls itself "NYTFranklin Light", and a
31763
+ // label asking for NYTFranklin at weight 300 has to reach it.
31764
+ //
31765
+ // See docs/development/label-tool-design.md.
31766
+
31767
+ // Collections (.ttc/.otc) hold several faces in one file and are how macOS
31768
+ // ships Helvetica, Menlo and Avenir, so a lookup that skipped them would miss
31769
+ // the font mapshaper's own tool writes by default there.
31770
+ var FONT_FILE_RXP = /\.(ttf|otf|ttc|otc)$/i;
31771
+
31772
+ // What a generic family means when there is no browser to resolve it. A guess,
31773
+ // but an ordered one: the first of these that is installed is what a browser
31774
+ // on this platform would almost certainly have picked.
31775
+ var GENERIC_FAMILIES$1 = {
31776
+ 'sans-serif': ['Helvetica', 'Arial', 'Liberation Sans', 'DejaVu Sans', 'Roboto', 'Segoe UI'],
31777
+ serif: ['Times New Roman', 'Times', 'Liberation Serif', 'DejaVu Serif', 'Georgia'],
31778
+ monospace: ['Menlo', 'Courier New', 'Liberation Mono', 'DejaVu Sans Mono', 'Consolas'],
31779
+ 'system-ui': ['Helvetica Neue', 'Segoe UI', 'Cantarell', 'Roboto'],
31780
+ cursive: [],
31781
+ fantasy: []
31782
+ };
31783
+
31784
+ var faceCache = {};
31785
+ var familyCache = {};
31786
+ var fullIndex = null;
31787
+ var fileList = null;
31788
+
31789
+ // Width classes, as OS/2 numbers them 1 to 9. A font-stretch is one of these
31790
+ // keywords or a percentage of normal width.
31791
+ var STRETCH_NAMES = {
31792
+ 'ultra-condensed': 1, 'extra-condensed': 2, condensed: 3,
31793
+ 'semi-condensed': 4, normal: 5, 'semi-expanded': 6, expanded: 7,
31794
+ 'extra-expanded': 8, 'ultra-expanded': 9
31795
+ };
31796
+ var STRETCH_PERCENTS = [50, 62.5, 75, 87.5, 100, 112.5, 125, 150, 200];
31797
+
31798
+ // The face to open for @family in (@weight, @italic, @stretch), or null if this
31799
+ // computer has no such font: {path, postscriptName}.
31800
+ //
31801
+ // postscriptName is how a face inside a collection is named to fontkit, and is
31802
+ // null for a file holding one face.
31803
+ function findFontFace(family, weight, italic, stretch) {
31804
+ var key = [family, weight, italic ? 'i' : 'n', stretch || ''].join('|');
31805
+ if (!(key in faceCache)) {
31806
+ faceCache[key] = lookupFace(family, weight, italic, stretch);
31807
+ }
31808
+ return faceCache[key];
31809
+ }
31810
+
31811
+ // Which of a family's faces answers a request for (@weight, @italic,
31812
+ // @stretch).
31813
+ //
31814
+ // Width first, then upright before oblique, then the nearest weight, then the
31815
+ // heavier of two equally near. The first three are the order CSS matches fonts
31816
+ // in, and the last is the rule the style menu uses to carry a face across a
31817
+ // change of font (getNearestVariant() in gui-label-fonts.mjs), applied here to
31818
+ // faces read from files rather than measured in a browser.
31819
+ //
31820
+ // A missing face is answered with a near one rather than refused: the browser
31821
+ // would synthesize the missing weight or slant from exactly this face, so its
31822
+ // widths are much closer to what is drawn than no measurement at all.
31823
+ function pickFace(faces, weight, italic, stretch) {
31824
+ var wanted = weight > 0 ? weight : 400;
31825
+ var wantedWidth = parseFontStretch(stretch);
31826
+ var best = null;
31827
+ var bestScore = null;
31828
+ (faces || []).forEach(function(face) {
31829
+ var score = [
31830
+ Math.abs((face.width || 5) - wantedWidth),
31831
+ !!face.italic === !!italic ? 0 : 1,
31832
+ Math.abs(face.weight - wanted),
31833
+ face.weight < wanted ? 1 : 0
31834
+ ];
31835
+ if (!best || compareScores(score, bestScore) < 0) {
31836
+ best = face;
31837
+ bestScore = score;
31838
+ }
31839
+ });
31840
+ return best;
31841
+ }
31842
+
31843
+ // A font-stretch as the width class it names, or normal width for anything
31844
+ // unreadable. A percentage is taken to the nearest class, the way CSS defines
31845
+ // the keywords.
31846
+ function parseFontStretch(stretch) {
31847
+ var str = String(stretch === null || stretch === undefined ? '' : stretch).trim().toLowerCase();
31848
+ var pct = /^([.0-9]+)%$/.exec(str);
31849
+ var best = 5;
31850
+ if (STRETCH_NAMES[str]) return STRETCH_NAMES[str];
31851
+ if (!pct) return 5;
31852
+ STRETCH_PERCENTS.forEach(function(val, i) {
31853
+ if (Math.abs(val - Number(pct[1])) <
31854
+ Math.abs(STRETCH_PERCENTS[best - 1] - Number(pct[1]))) {
31855
+ best = i + 1;
31856
+ }
31857
+ });
31858
+ return best;
31859
+ }
31860
+
31861
+ // Family names are compared without spaces, punctuation or case, so that
31862
+ // "NYTFranklin" finds "NYT Franklin" and "Helvetica Neue" finds
31863
+ // "HelveticaNeue". Font vendors are not consistent about any of the three.
31864
+ function normalizeFamilyName(name) {
31865
+ return String(name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
31866
+ }
31867
+
31868
+ // The families a face answers to. The typographic family (name ID 16) is the
31869
+ // one a stylesheet means: every weight of NYTFranklin has its own ID 1 family
31870
+ // ("NYTFranklin Light", "NYTFranklin Medium") and they are one family only
31871
+ // under ID 16. Both are indexed, because plenty of fonts have no ID 16.
31872
+ function getFaceFamilies(font) {
31873
+ var records = font && font.name && font.name.records || {};
31874
+ return [pickName(records.preferredFamily), pickName(records.fontFamily),
31875
+ font && font.familyName].filter(Boolean);
31876
+ }
31877
+
31878
+ function clearFontCache() {
31879
+ faceCache = {};
31880
+ familyCache = {};
31881
+ fullIndex = null;
31882
+ fileList = null;
31883
+ }
31884
+
31885
+ function lookupFace(family, weight, italic, stretch) {
31886
+ var names = resolveFamilyNames(family);
31887
+ var face = findBestFace(names, weight, italic, stretch);
31888
+ // The filename guess can find a family and still miss one of its faces:
31889
+ // Segoe UI Semibold lives in seguisb.ttf, which does not begin with the
31890
+ // family's name. Anything other than the face that was asked for is worth
31891
+ // the full index -- once per session -- to be sure it is the nearest this
31892
+ // machine has.
31893
+ if (!fullIndex && !faceAnswersRequest(face, weight, italic, stretch)) {
31894
+ fullIndex = readFaces(getFontFiles());
31895
+ face = findBestFace(names, weight, italic, stretch) || face;
31896
+ }
31897
+ return face;
31898
+ }
31899
+
31900
+ function findBestFace(names, weight, italic, stretch) {
31901
+ for (var i = 0; i < names.length; i++) {
31902
+ var faces = findFamilyFaces(names[i]);
31903
+ if (faces && faces.length > 0) return pickFace(faces, weight, italic, stretch);
31904
+ }
31905
+ return null;
31906
+ }
31907
+
31908
+ function faceAnswersRequest(face, weight, italic, stretch) {
31909
+ return !!face && face.weight == (weight > 0 ? weight : 400) &&
31910
+ !!face.italic === !!italic && (face.width || 5) == parseFontStretch(stretch);
31911
+ }
31912
+
31913
+ // A font-family is a list, and may end in a generic: each name is tried in
31914
+ // turn, exactly as a browser would, and a generic stands for the first of its
31915
+ // candidates that is installed.
31916
+ function resolveFamilyNames(family) {
31917
+ var out = [];
31918
+ splitFamilyList(family).forEach(function(name) {
31919
+ var generic = GENERIC_FAMILIES$1[name.toLowerCase()];
31920
+ if (generic) {
31921
+ out = out.concat(generic);
31922
+ } else {
31923
+ out.push(name);
31924
+ }
31925
+ });
31926
+ return out;
31927
+ }
31928
+
31929
+ function splitFamilyList(family) {
31930
+ return String(family || '').split(',').map(function(name) {
31931
+ return name.trim().replace(/^['"]|['"]$/g, '');
31932
+ }).filter(Boolean);
31933
+ }
31934
+
31935
+ // Two passes, because parsing every font on the computer costs the best part
31936
+ // of a second and most lookups do not need it: the file holding a family is
31937
+ // usually named after it, so files whose name begins with the family's are
31938
+ // parsed first. Whether that was good enough is lookupFace()'s decision.
31939
+ function findFamilyFaces(family) {
31940
+ var key = normalizeFamilyName(family);
31941
+ if (!key) return null;
31942
+ if (fullIndex) return fullIndex[key] || null;
31943
+ if (!(key in familyCache)) {
31944
+ familyCache[key] = readFaces(getLikelyFiles(key))[key] || null;
31945
+ }
31946
+ return familyCache[key];
31947
+ }
31948
+
31949
+ function getLikelyFiles(key) {
31950
+ return getFontFiles().filter(function(file) {
31951
+ return normalizeFamilyName(basename(file)).indexOf(key) === 0;
31952
+ });
31953
+ }
31954
+
31955
+ // Faces by normalized family name. A file that cannot be parsed is skipped
31956
+ // rather than reported: a font directory can hold anything, and a broken font
31957
+ // is not an error in the user's data.
31958
+ function readFaces(files) {
31959
+ var index = {};
31960
+ files.forEach(function(file) {
31961
+ getFileFaces(file).forEach(function(face) {
31962
+ face.families.forEach(function(name) {
31963
+ var key = normalizeFamilyName(name);
31964
+ if (!key) return;
31965
+ if (!index[key]) index[key] = [];
31966
+ index[key].push(face);
31967
+ });
31968
+ });
31969
+ });
31970
+ return index;
31971
+ }
31972
+
31973
+ function getFileFaces(file) {
31974
+ var fontkit = getFontkit();
31975
+ var font, fonts;
31976
+ if (!fontkit) return [];
31977
+ try {
31978
+ font = fontkit.openSync(file);
31979
+ // A collection reports its members in .fonts; a single font is its own.
31980
+ fonts = font && font.fonts || [font];
31981
+ return fonts.filter(Boolean).map(function(one) {
31982
+ return {
31983
+ path: file,
31984
+ // Named rather than numbered because fontkit takes a name, and because
31985
+ // a name survives a font being reinstalled in a different order.
31986
+ postscriptName: font.fonts ? one.postscriptName : null,
31987
+ families: getFaceFamilies(one),
31988
+ weight: getFaceWeight(one),
31989
+ width: getFaceWidth(one),
31990
+ italic: isItalicFace(one)
31991
+ };
31992
+ }).filter(function(face) {
31993
+ return face.families.length > 0;
31994
+ });
31995
+ } catch (e) {
31996
+ return [];
31997
+ }
31998
+ }
31999
+
32000
+ function getFaceWeight(font) {
32001
+ var os2 = font['OS/2'];
32002
+ var weight = os2 && os2.usWeightClass;
32003
+ return weight > 0 ? weight : 400;
32004
+ }
32005
+
32006
+ function getFaceWidth(font) {
32007
+ var os2 = font['OS/2'];
32008
+ var width = os2 && os2.usWidthClass;
32009
+ return width >= 1 && width <= 9 ? width : 5;
32010
+ }
32011
+
32012
+ function isItalicFace(font) {
32013
+ var os2 = font['OS/2'];
32014
+ if (os2 && os2.fsSelection && typeof os2.fsSelection.italic == 'boolean') {
32015
+ return os2.fsSelection.italic;
32016
+ }
32017
+ return !!font.italicAngle;
32018
+ }
32019
+
32020
+ // A name record is a string, or an object of translations to pick English out
32021
+ // of, depending on the font and the version of fontkit.
32022
+ function pickName(rec) {
32023
+ if (!rec) return '';
32024
+ if (typeof rec == 'string') return rec;
32025
+ return rec.en || Object.keys(rec).map(function(k) { return rec[k]; })[0] || '';
32026
+ }
32027
+
32028
+ function compareScores(a, b) {
32029
+ for (var i = 0; i < a.length; i++) {
32030
+ if (a[i] != b[i]) return a[i] - b[i];
32031
+ }
32032
+ return 0;
32033
+ }
32034
+
32035
+ // Every font file on this computer, listed once. Listing is cheap -- a few
32036
+ // hundred directory entries -- next to parsing them, so this is not the part
32037
+ // worth avoiding.
32038
+ function getFontFiles() {
32039
+ var fs = getFs();
32040
+ var files = [];
32041
+ if (fileList) return fileList;
32042
+ if (!fs) return [];
32043
+ getFontDirs().forEach(function(dir) {
32044
+ var entries;
32045
+ try {
32046
+ entries = fs.readdirSync(dir, {recursive: true});
32047
+ } catch (e) {
32048
+ return; // a directory this platform does not have
32049
+ }
32050
+ entries.forEach(function(entry) {
32051
+ var file = dir + '/' + String(entry).split('\\').join('/');
32052
+ if (FONT_FILE_RXP.test(file)) files.push(file);
32053
+ });
32054
+ });
32055
+ fileList = files;
32056
+ return files;
32057
+ }
32058
+
32059
+ // Where each platform keeps fonts, or MAPSHAPER_FONT_PATH if it is set.
32060
+ //
32061
+ // It replaces the platform's directories rather than adding to them, which is
32062
+ // what makes a machine's font situation something a caller can state: a
32063
+ // container with its fonts somewhere of its own, a build that has to produce
32064
+ // the same SVG wherever it runs, a test that needs to know there is nothing to
32065
+ // find. Several directories are separated by : or ;.
32066
+ function getFontDirs() {
32067
+ var home = getHomeDir();
32068
+ var platform = typeof process == 'object' && process.platform || '';
32069
+ var dirs = getEnvDirs();
32070
+ if (dirs.length > 0) return dirs;
32071
+ if (platform == 'darwin') {
32072
+ dirs = ['/System/Library/Fonts', '/Library/Fonts',
32073
+ '/Network/Library/Fonts'];
32074
+ if (home) dirs.push(home + '/Library/Fonts');
32075
+ } else if (platform == 'win32') {
32076
+ dirs = [(getEnv('WINDIR') || 'C:\\Windows') + '/Fonts'];
32077
+ if (home) dirs.push(home + '/AppData/Local/Microsoft/Windows/Fonts');
32078
+ } else {
32079
+ dirs = ['/usr/share/fonts', '/usr/local/share/fonts', '/run/host/fonts'];
32080
+ if (home) {
32081
+ dirs.push(home + '/.fonts', home + '/.local/share/fonts');
32082
+ }
32083
+ }
32084
+ return dirs;
32085
+ }
32086
+
32087
+ function getEnvDirs() {
32088
+ var val = getEnv('MAPSHAPER_FONT_PATH');
32089
+ if (!val) return [];
32090
+ return val.split(/[:;]/).filter(Boolean);
32091
+ }
32092
+
32093
+ function getEnv(name) {
32094
+ return typeof process == 'object' && process.env && process.env[name] || '';
32095
+ }
32096
+
32097
+ function getHomeDir() {
32098
+ var os = safeRequire('os');
32099
+ try {
32100
+ return os && os.homedir() || '';
32101
+ } catch (e) {
32102
+ return '';
32103
+ }
32104
+ }
32105
+
32106
+ function basename(file) {
32107
+ var parts = file.split('/');
32108
+ return parts[parts.length - 1].replace(FONT_FILE_RXP, '');
32109
+ }
32110
+
32111
+ function getFs() {
32112
+ return safeRequire('fs');
32113
+ }
32114
+
32115
+ // Loaded through the require shim and only when a label actually needs
32116
+ // measuring, so that the browser bundle -- which is this same file -- never
32117
+ // reaches for a module it does not have, and a CLI run that touches no labels
32118
+ // never pays for loading it.
32119
+ function getFontkit() {
32120
+ return safeRequire('fontkit');
32121
+ }
32122
+
32123
+ function safeRequire(name) {
32124
+ if (runningInBrowser()) return null;
32125
+ try {
32126
+ return require$1(name) || null;
32127
+ } catch (e) {
32128
+ return null;
32129
+ }
32130
+ }
32131
+
32132
+ var FontLookup = /*#__PURE__*/Object.freeze({
32133
+ __proto__: null,
32134
+ clearFontCache: clearFontCache,
32135
+ findFontFace: findFontFace,
32136
+ getFaceFamilies: getFaceFamilies,
32137
+ normalizeFamilyName: normalizeFamilyName,
32138
+ parseFontStretch: parseFontStretch,
32139
+ pickFace: pickFace
32140
+ });
32141
+
32142
+ // Renders an SVG document as a PNG or JPEG image.
32143
+ //
32144
+ // Node uses resvg, compiled to WebAssembly, which cannot see the fonts
32145
+ // installed on the computer: the files for the families a document names are
32146
+ // found the way label measurement finds them (see mapshaper-font-lookup.mjs)
32147
+ // and handed to it. The browser draws the document on a canvas with its own
32148
+ // fonts.
32149
+
32150
+ var resvgPromise = null;
32151
+
32152
+ // opts:
32153
+ // width, height size of the document, in CSS pixels
32154
+ // scale image pixels per CSS pixel
32155
+ // format 'png' or 'jpeg' (JPEG is drawn over a white background)
32156
+ // quality JPEG quality, 1-100
32157
+ // Returns the bytes of the image file.
32158
+ async function rasterizeSVG(svg, opts) {
32159
+ if (runningInBrowser()) {
32160
+ return rasterizeInBrowser(svg, opts);
32161
+ }
32162
+ return rasterizeWithResvg(svg, opts);
32163
+ }
32164
+
32165
+ async function rasterizeWithResvg(svg, opts) {
32166
+ var resvg = await loadResvg();
32167
+ var renderer = new resvg.Resvg(svg, {
32168
+ fitTo: {mode: 'width', value: getImageSize(opts.width, opts.scale)},
32169
+ background: opts.format == 'jpeg' ? 'white' : undefined,
32170
+ font: svgHasText(svg) ? getResvgFontOptions(svg) : {fontBuffers: []}
32171
+ });
32172
+ var img = renderer.render();
32173
+ var content;
32174
+ try {
32175
+ if (opts.format == 'jpeg') {
32176
+ content = require$1('jpeg-js').encode({
32177
+ data: Buffer.from(img.pixels),
32178
+ width: img.width,
32179
+ height: img.height
32180
+ }, opts.quality).data;
32181
+ } else {
32182
+ content = Buffer.from(img.asPng());
32183
+ }
32184
+ } finally {
32185
+ // wasm memory is not garbage-collected
32186
+ img.free();
32187
+ renderer.free();
32188
+ }
32189
+ return content;
32190
+ }
32191
+
32192
+ function loadResvg() {
32193
+ var resvg, fs;
32194
+ if (!resvgPromise) {
32195
+ resvg = require$1('@resvg/resvg-wasm');
32196
+ fs = require$1('fs');
32197
+ // initWasm() throws if it is called a second time
32198
+ resvgPromise = resvg.initWasm(fs.readFileSync(require$1.resolve('@resvg/resvg-wasm/index_bg.wasm')))
32199
+ .then(function() { return resvg; });
32200
+ }
32201
+ return resvgPromise;
32202
+ }
32203
+
32204
+ function svgHasText(svg) {
32205
+ return /<text\b/.test(svg);
32206
+ }
32207
+
32208
+ // Regular and bold faces of each family named in @svg, plus the faces that
32209
+ // the generic families resolve to on this computer.
32210
+ function getResvgFontOptions(svg) {
32211
+ var paths = [];
32212
+ var opts = {fontBuffers: []};
32213
+ var generics = {
32214
+ sansSerifFamily: 'sans-serif',
32215
+ serifFamily: 'serif',
32216
+ monospaceFamily: 'monospace'
32217
+ };
32218
+ getFontFamilies(svg).concat(Object.values(generics)).forEach(function(family) {
32219
+ [400, 700].forEach(function(weight) {
32220
+ var face = findFontFace(family, weight, false);
32221
+ if (face && !paths.includes(face.path)) paths.push(face.path);
32222
+ });
32223
+ });
32224
+ Object.keys(generics).forEach(function(key) {
32225
+ var face = findFontFace(generics[key], 400, false);
32226
+ if (face) opts[key] = face.families[0];
32227
+ });
32228
+ opts.defaultFontFamily = opts.sansSerifFamily;
32229
+ opts.fontBuffers = paths.map(function(path) {
32230
+ return require$1('fs').readFileSync(path);
32231
+ });
32232
+ return opts;
32233
+ }
32234
+
32235
+ function getFontFamilies(svg) {
32236
+ var families = [];
32237
+ var rxp = /font-family(?:="|:\s*)([^";]+)/g;
32238
+ var match, family;
32239
+ while ((match = rxp.exec(svg)) !== null) {
32240
+ family = match[1].replace(/&quot;|&apos;/g, '"').trim();
32241
+ if (!families.includes(family)) families.push(family);
32242
+ }
32243
+ return families;
32244
+ }
32245
+
32246
+ async function rasterizeInBrowser(svg, opts) {
32247
+ var url = URL.createObjectURL(new Blob([svg], {type: 'image/svg+xml'}));
32248
+ var canvas = document.createElement('canvas');
32249
+ var img = new Image();
32250
+ var ctx, blob;
32251
+ try {
32252
+ img.src = url;
32253
+ await img.decode();
32254
+ canvas.width = getImageSize(opts.width, opts.scale);
32255
+ canvas.height = getImageSize(opts.height, opts.scale);
32256
+ ctx = canvas.getContext('2d');
32257
+ if (opts.format == 'jpeg') {
32258
+ ctx.fillStyle = 'white';
32259
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
32260
+ }
32261
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
32262
+ blob = await new Promise(function(resolve) {
32263
+ canvas.toBlob(resolve, 'image/' + opts.format, opts.quality / 100);
32264
+ });
32265
+ } finally {
32266
+ URL.revokeObjectURL(url);
32267
+ }
32268
+ if (!blob) stop$1('Unable to render the map image');
32269
+ return new Uint8Array(await blob.arrayBuffer());
32270
+ }
32271
+
32272
+ function getImageSize(cssPixels, scale) {
32273
+ return Math.max(1, Math.round(cssPixels * scale));
32274
+ }
32275
+
32276
+ // Font families in HTML output, which differ from the families a map's labels
32277
+ // are given (and that SVG output keeps) in two ways.
32278
+ //
32279
+ // Web fonts: a site's own fonts may be loaded under names unlike those of the
32280
+ // same fonts installed on a computer (NYTFranklin is nyt-franklin on
32281
+ // nytimes.com). They are listed before the installed fonts they stand for.
32282
+ // Only the two NYT fonts are mapped.
32283
+ //
32284
+ // Fallbacks: for pages viewed on devices that lack a map's fonts (or before
32285
+ // its web fonts load). A family list that contains a generic family is the
32286
+ // user's own fallback plan and is left alone. Any other list is given the
32287
+ // common fonts of the category its first font belongs to, then that category's
32288
+ // generic family -- the convention ai2html follows, e.g.
32289
+ // nyt-franklin,arial,helvetica,sans-serif.
32290
+
32291
+ var GENERIC_FAMILIES = ['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
32292
+ 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
32293
+ 'math', 'emoji', 'fangsong'];
32294
+
32295
+ var FALLBACKS = {
32296
+ 'sans-serif': ['Helvetica', 'Arial', 'sans-serif'],
32297
+ serif: ['Times New Roman', 'Times', 'serif'],
32298
+ monospace: ['Menlo', 'Consolas', 'Courier New', 'monospace']
32299
+ };
32300
+
32301
+ // Matched anywhere in a normalized family name, so that the list covers
32302
+ // variants like "Times New Roman", "Bodoni 72" and nyt-cheltenham.
32303
+ var SERIF_FONTS = ['times', 'georgia', 'garamond', 'baskerville', 'palatino',
32304
+ 'bookantiqua', 'cambria', 'didot', 'bodoni', 'minion', 'caslon',
32305
+ 'centuryschoolbook', 'charter', 'hoefler', 'merriweather', 'playfair',
32306
+ 'cheltenham', 'imperial'];
32307
+
32308
+ var MONOSPACE_FONTS = ['courier', 'menlo', 'monaco', 'consolas',
32309
+ 'lucidaconsole', 'andalemono', 'sfmono', 'inconsolata'];
32310
+
32311
+ // The web fonts that HTML maps on nytimes.com use. Installed NYT fonts have a
32312
+ // family for each weight ("NYTFranklin Medium") as well as one family for all
32313
+ // of them, which is the name mapshaper writes.
32314
+ var NYT_WEB_FONTS = [['NYTFranklin', 'nyt-franklin'], ['NYTCheltenham', 'nyt-cheltenham']];
32315
+ var NYT_WEIGHTS = {
32316
+ '': null, ExtraLight: '200', Light: '300', Book: '400', Medium: '500',
32317
+ Semibold: '600', Bold: '700', ExtraBold: '800', Headline: '700'
32318
+ };
32319
+ var WEB_FONTS = getNytWebFonts();
32320
+
32321
+ // Returns a CSS font-family value for @family, a font-family list
32322
+ function addFontFallbacks(family) {
32323
+ var names = parseFamilyList(family);
32324
+ var lowerNames, category;
32325
+ if (names.length === 0) return String(family);
32326
+ lowerNames = names.map(function(name) { return name.toLowerCase(); });
32327
+ if (!lowerNames.some(isGenericFamily)) {
32328
+ category = guessFontCategory(lowerNames[0]);
32329
+ FALLBACKS[category].forEach(function(name) {
32330
+ if (!lowerNames.includes(name.toLowerCase())) names.push(name);
32331
+ });
32332
+ }
32333
+ return names.map(formatFamilyName).join(', ');
32334
+ }
32335
+
32336
+ function getNytWebFonts() {
32337
+ var fonts = [];
32338
+ NYT_WEB_FONTS.forEach(function(pair) {
32339
+ Object.keys(NYT_WEIGHTS).forEach(function(weightName) {
32340
+ fonts.push({
32341
+ system: weightName ? pair[0] + ' ' + weightName : pair[0],
32342
+ web: pair[1],
32343
+ weight: NYT_WEIGHTS[weightName]
32344
+ });
32345
+ });
32346
+ });
32347
+ return fonts;
32348
+ }
32349
+
32350
+ // Adds the web font that stands for each system font in @style, a text class's
32351
+ // properties, just before that system font, which stays in the list for
32352
+ // computers that have it installed. The weight of a system family with a weight
32353
+ // in its name (like "NYTFranklin Medium") replaces the label's font-weight, if
32354
+ // it is the label's first font.
32355
+ function applyWebFonts(style) {
32356
+ var names = parseFamilyList(style['font-family']);
32357
+ var out = [];
32358
+ var weight = null;
32359
+ if (names.length === 0) return style;
32360
+ names.forEach(function(name, i) {
32361
+ var font = findWebFont(WEB_FONTS, name);
32362
+ if (font && !containsName(names, font.web) && !containsName(out, font.web)) {
32363
+ out.push(font.web);
32364
+ if (i === 0) weight = font.weight;
32365
+ }
32366
+ out.push(name);
32367
+ });
32368
+ style['font-family'] = out.map(formatFamilyName).join(', ');
32369
+ if (weight) style['font-weight'] = weight;
32370
+ return style;
32371
+ }
32372
+
32373
+ // System font names are compared as the font lookup compares them, so that
32374
+ // NYTFranklin also matches "NYT Franklin"
32375
+ function findWebFont(webFonts, name) {
32376
+ var key = normalizeFamilyName(name);
32377
+ return webFonts.find(function(font) {
32378
+ return normalizeFamilyName(font.system) == key;
32379
+ }) || null;
32380
+ }
32381
+
32382
+ // Web font names are compared exactly (but for case): nyt-franklin and
32383
+ // NYTFranklin normalize to the same name
32384
+ function containsName(names, name) {
32385
+ return names.some(function(name2) {
32386
+ return name2.toLowerCase() == name.toLowerCase();
32387
+ });
32388
+ }
32389
+
32390
+ function guessFontCategory(name) {
32391
+ var key = normalizeFamilyName(name);
32392
+ name = name.toLowerCase();
32393
+ if (containsFontKey(MONOSPACE_FONTS, key) || /\b(mono|code)\b/.test(name)) {
32394
+ return 'monospace';
32395
+ }
32396
+ if (containsFontKey(SERIF_FONTS, key) || /\bslab\b/.test(name) ||
32397
+ /serif/.test(name) && !/sans/.test(name)) {
32398
+ return 'serif';
32399
+ }
32400
+ return 'sans-serif';
32401
+ }
32402
+
32403
+ function containsFontKey(keys, key) {
32404
+ return keys.some(function(k) {
32405
+ return key.includes(k);
32406
+ });
32407
+ }
32408
+
32409
+ function isGenericFamily(name) {
32410
+ return GENERIC_FAMILIES.includes(name);
32411
+ }
32412
+
32413
+ function parseFamilyList(family) {
32414
+ return String(family || '').split(',').map(unquote).filter(Boolean);
32415
+ }
32416
+
32417
+ function unquote(name) {
32418
+ return name.trim().replace(/^['"]|['"]$/g, '').trim();
32419
+ }
32420
+
32421
+ // A name is quoted unless it is a generic family or a run of plain words, so
32422
+ // that CSS cannot read it as a keyword or a malformed identifier.
32423
+ function formatFamilyName(name) {
32424
+ if (isGenericFamily(name.toLowerCase())) return name;
32425
+ if (/^[a-z][a-z-]*$/i.test(name)) return name;
32426
+ return '"' + name.replace(/"/g, '') + '"';
32427
+ }
32428
+
32429
+ // Text styles in HTML output are written as CSS classes rather than as
32430
+ // attributes on each label: one class for each combination of style properties
32431
+ // in the map, so that a page can restyle a map's labels by editing a few rules.
32432
+
32433
+ var TEXT_TAGS = {text: true, tspan: true, textPath: true};
32434
+
32435
+ var TEXT_STYLE_PROPERTIES = [
32436
+ 'font-family', 'font-size', 'font-style', 'font-weight', 'font-stretch',
32437
+ 'letter-spacing', 'text-anchor', 'dominant-baseline', 'fill', 'fill-opacity',
32438
+ 'opacity', 'stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray',
32439
+ 'stroke-linejoin', 'paint-order'
32440
+ ];
32441
+
32442
+ // The text defaults a layer's group carries for its labels to inherit (see
32443
+ // getLabelTextDefaults()), which are folded into the labels' own classes.
32444
+ var LAYER_TEXT_PROPERTIES = ['font-family', 'font-size', 'text-anchor'];
32445
+
32446
+ // A bare number is px in an SVG attribute, and not valid CSS for these.
32447
+ var LENGTH_PROPERTIES = {'font-size': true, 'letter-spacing': true, 'stroke-width': true};
32448
+
32449
+ // Moves the text style properties of the elements in @layers (SVG objects, one
32450
+ // per layer) into classes named @prefix + n. Returns the classes in the order
32451
+ // they were first used: [{name, style: {property: value, ...}}, ...]
32452
+ function convertTextStylesToClasses(layers, prefix) {
32453
+ var classes = [];
32454
+ var index = {};
32455
+ layers.forEach(function(lyr) {
32456
+ var inherited = moveProperties(lyr.properties, {}, LAYER_TEXT_PROPERTIES);
32457
+ procNode(lyr, inherited, false);
32458
+ });
32459
+ return classes;
32460
+
32461
+ function procNode(node, inherited, inText) {
32462
+ var style, name;
32463
+ if (TEXT_TAGS[node.tag]) {
32464
+ // text inside a text element inherits from it, not from the layer
32465
+ style = Object.assign({}, inText ? null : inherited);
32466
+ moveProperties(node.properties, style, TEXT_STYLE_PROPERTIES);
32467
+ if (Object.keys(style).length > 0) {
32468
+ name = getClassName(style);
32469
+ node.properties.class = node.properties.class ?
32470
+ node.properties.class + ' ' + name : name;
32471
+ }
32472
+ inText = true;
32473
+ }
32474
+ (node.children || []).forEach(function(child) {
32475
+ procNode(child, inherited, inText);
32476
+ });
32477
+ }
32478
+
32479
+ function getClassName(style) {
32480
+ var key = getStyleKey(style);
32481
+ if (!(key in index)) {
32482
+ index[key] = prefix + classes.length;
32483
+ classes.push({name: index[key], style: style});
32484
+ }
32485
+ return index[key];
32486
+ }
32487
+ }
32488
+
32489
+ // Returns CSS rules for @classes, scoped to @selector
32490
+ function formatTextClassesAsCss(classes, selector) {
32491
+ return classes.map(function(o) {
32492
+ var decl = Object.keys(o.style).map(function(k) {
32493
+ return k + ':' + formatCssValue(k, o.style[k]) + ';';
32494
+ }).join('');
32495
+ return selector + ' .' + o.name + ' {' + decl + '}';
32496
+ }).join('\n');
32497
+ }
32498
+
32499
+ function formatCssValue(name, val) {
32500
+ var str = sanitizeCssValue(val);
32501
+ if (LENGTH_PROPERTIES[name] && /^-?(\d+\.?\d*|\.\d+)$/.test(str)) {
32502
+ str += 'px';
32503
+ } else if (name == 'font-family') {
32504
+ str = addFontFallbacks(str);
32505
+ }
32506
+ return str;
32507
+ }
32508
+
32509
+ // Values come from data, and must not be able to end the declaration, the
32510
+ // rule or the <style> element they are written into.
32511
+ function sanitizeCssValue(val) {
32512
+ return String(val).replace(/[{}<>;\\]/g, '').trim();
32513
+ }
32514
+
32515
+ function getStyleKey(style) {
32516
+ return Object.keys(style).sort().map(function(k) {
32517
+ return k + ':' + style[k];
32518
+ }).join(';');
32519
+ }
32520
+
32521
+ function moveProperties(src, dest, names) {
32522
+ if (!src) return dest;
32523
+ names.forEach(function(k) {
32524
+ var val = src[k];
32525
+ delete src[k];
32526
+ // stringify() omits these, so they are not style
32527
+ if (!val && val !== 0) return;
32528
+ dest[k] = val;
32529
+ });
32530
+ return dest;
32531
+ }
32532
+
32533
+ // HTML output, modeled on the ai2html script for Adobe Illustrator: the map's
32534
+ // shapes are drawn in an image, and its point symbols -- labels, icons, dots --
32535
+ // are drawn over it as inline SVG. Each symbol is anchored to a position given
32536
+ // as a percentage of the map's width and height, so that a map that resizes
32537
+ // with its container keeps its symbols where they belong without scaling them.
32538
+ //
32539
+ // A label along a path is anchored the same way, at the point on its path
32540
+ // where its text is attached, and keeps the shape and size of its path.
32541
+
32542
+ var CLASS_PREFIX = 'ms-';
32543
+
32544
+ async function exportHTML(dataset, opts) {
32545
+ var responsiveness = getResponsiveness(opts);
32546
+ var imageFormat = getImageFormat(opts);
32547
+ var pixelRatio = getPixelRatio(opts);
32548
+ var o = prepareDatasetForSVG(dataset, Object.assign({}, opts, {
32549
+ // raster layers are resampled to the pixel density of the image
32550
+ raster_res: opts.raster_res || pixelRatio,
32551
+ linked_images: false
32552
+ }));
32553
+ var frame = o.frame;
32554
+ var base = getHtmlFileBase(o.dataset, opts);
32555
+ var layers = splitLayersForHTML(o.dataset.layers, o.dataset);
32556
+ var imageFile = base + (imageFormat == 'jpeg' ? '.jpg' : '.png');
32557
+ var image = await rasterizeSVG(renderSVGDocument(o.dataset, frame, layers.image, o.opts), {
32558
+ width: frame.width,
32559
+ height: frame.height,
32560
+ scale: pixelRatio,
32561
+ format: imageFormat,
32562
+ quality: getJpegQuality(opts)
32563
+ });
32564
+ var html = renderHtmlFragment({
32565
+ id: getContainerId(base),
32566
+ frame: frame,
32567
+ responsiveness: responsiveness,
32568
+ imageFile: imageFile,
32569
+ overlay: renderOverlay(o.dataset, frame, layers.overlay, o.opts)
32570
+ });
32571
+ return [{
32572
+ filename: opts.file || base + '.html',
32573
+ content: html
32574
+ }, {
32575
+ filename: imageFile,
32576
+ content: image
32577
+ }];
32578
+ }
32579
+
32580
+ // Point layers go in the overlay; everything else is drawn in the image,
32581
+ // beneath them. Layers are listed bottom to top.
32582
+ function splitLayersForHTML(layers, dataset) {
32583
+ var image = [], overlay = [];
32584
+ layers.forEach(function(lyr) {
32585
+ if (layerGoesInOverlay(lyr)) {
32586
+ overlay.push(lyr);
32587
+ } else {
32588
+ if (overlay.length > 0 && !isFrameLayer(lyr, dataset.arcs)) {
32589
+ warn(utils.format('Layer "%s" is drawn in the map image, beneath point layer "%s", which is above it in the layer order.',
32590
+ lyr.name || '[unnamed]', overlay[overlay.length - 1].name || '[unnamed]'));
32591
+ }
32592
+ image.push(lyr);
32593
+ }
32594
+ });
32595
+ return {image: image, overlay: overlay};
32596
+ }
32597
+
32598
+ function layerGoesInOverlay(lyr) {
32599
+ return lyr.geometry_type == 'point' && !layerHasFurniture(lyr) &&
32600
+ !layerHasRaster(lyr);
32601
+ }
32602
+
32603
+ function renderOverlay(dataset, frame, layers, opts) {
32604
+ var defs = [];
32605
+ var objects = layers.map(function(lyr) {
32606
+ var obj = renderOverlayLayer(lyr, dataset, frame, opts);
32607
+ convertPropertiesToDefinitions(obj, defs);
32608
+ return obj;
32609
+ });
32610
+ var classes = convertTextStylesToClasses(objects, CLASS_PREFIX + 'text-');
32611
+ classes.forEach(function(o) {
32612
+ applyWebFonts(o.style);
32613
+ });
32614
+ return {
32615
+ defs: defs,
32616
+ layers: objects,
32617
+ classes: classes
32618
+ };
32619
+ }
32620
+
32621
+ function renderOverlayLayer(lyr, dataset, frame, opts) {
32622
+ var layerObj = getEmptyLayerForSVG(lyr, opts);
32623
+ var geojson = exportDatasetAsGeoJSON(utils.defaults({layers: [lyr]}, dataset), opts);
32624
+ var features = geojson.features || geojson.geometries || (geojson.type ? [geojson] : []);
32625
+ var dataAttributes = getDataAttributes(lyr, opts);
32626
+ var report = initPathLabelReport();
32627
+ var anchors = [];
32628
+ // each feature is rendered at the origin, and placed by a container
32629
+ var localFeatures = features.map(function(feat, i) {
32630
+ var geom = feat && feat.type == 'Feature' ? feat.geometry : feat;
32631
+ var props = feat && feat.properties || {};
32632
+ var localGeom = null;
32633
+ anchors[i] = [];
32634
+ if (!geom || !geom.coordinates) ; else if (featureIsPathLabel(geom, props)) {
32635
+ anchors[i].push(getPathLabelAnchor(geom.coordinates, props));
32636
+ localGeom = {
32637
+ type: 'MultiPoint',
32638
+ coordinates: shiftCoords(geom.coordinates, anchors[i][0])
32639
+ };
32640
+ } else if (geom.type == 'Point' || geom.type == 'MultiPoint') {
32641
+ // the points of a multipoint feature (other than a path label) are drawn
32642
+ // with the same symbol
32643
+ anchors[i] = geom.type == 'Point' ? [geom.coordinates] : geom.coordinates;
32644
+ localGeom = {type: 'Point', coordinates: [0, 0]};
32645
+ }
32646
+ return {
32647
+ type: 'Feature',
32648
+ id: feat && feat.id,
32649
+ properties: props,
32650
+ geometry: localGeom
32651
+ };
32652
+ }).map(function(feat) {
32653
+ if (feat.id === undefined) delete feat.id;
32654
+ return feat;
32655
+ });
32656
+ var symbols = importGeoJSONFeatures(localFeatures, utils.defaults({
32657
+ path_label_report: report
32658
+ }, opts));
32659
+ reportPathLabels(report, lyr);
32660
+
32661
+ symbols.forEach(function(sym, i) {
32662
+ if (isEmptySymbol(sym)) return;
32663
+ removeOriginTransform(sym);
32664
+ anchors[i].forEach(function(xy, j) {
32665
+ layerObj.children.push({
32666
+ tag: 'svg',
32667
+ properties: Object.assign({
32668
+ x: formatPct(xy[0], frame.width),
32669
+ y: formatPct(xy[1], frame.height),
32670
+ overflow: 'visible'
32671
+ }, dataAttributes ? dataAttributes[i] : null),
32672
+ children: [j === 0 ? sym : copySymbolWithoutId(sym)]
32673
+ });
32674
+ });
32675
+ });
32676
+ return layerObj;
32677
+ }
32678
+
32679
+ // The point on a label's path where its text is attached: its start offset,
32680
+ // which is where text-anchor places the text.
32681
+ function getPathLabelAnchor(knots, rec) {
32682
+ var offset = String(rec['label-start-offset'] || getDefaultStartOffset(rec));
32683
+ var len = getCurveLength(knots);
32684
+ var dist = parseFloat(offset);
32685
+ var p;
32686
+ if (/%$/.test(offset)) {
32687
+ dist = dist / 100 * len;
32688
+ }
32689
+ p = getPointAtCurveLength(knots, utils.isFiniteNumber(dist) ? dist : len / 2) || knots[0];
32690
+ return [roundCoord$2(p[0]), roundCoord$2(p[1])];
32691
+ }
32692
+
32693
+ function shiftCoords(coords, origin) {
32694
+ return coords.map(function(p) {
32695
+ return [roundCoord$2(p[0] - origin[0]), roundCoord$2(p[1] - origin[1])];
32696
+ });
32697
+ }
32698
+
32699
+ function roundCoord$2(c) {
32700
+ return Math.round(c * 100) / 100;
32701
+ }
32702
+
32703
+ function getDataAttributes(lyr, opts) {
32704
+ var fields;
32705
+ if (!opts.svg_data || !lyr.data) return null;
32706
+ fields = opts.svg_data.includes('*') ? lyr.data.getFields() :
32707
+ opts.svg_data.filter(function(name) { return lyr.data.fieldExists(name); });
32708
+ return exportDataAttributesForSVG(lyr.data.getRecords(), fields);
32709
+ }
32710
+
32711
+ function isEmptySymbol(o) {
32712
+ return !o || o.tag == 'g' && (!o.children || o.children.length === 0);
32713
+ }
32714
+
32715
+ // Symbols rendered at the origin are given a transform that places them there
32716
+ function removeOriginTransform(o) {
32717
+ if (o.properties && o.properties.transform == 'translate(0 0)') {
32718
+ delete o.properties.transform;
32719
+ }
32720
+ }
32721
+
32722
+ // ids must be unique, so only the first copy of a multipoint symbol keeps it
32723
+ function copySymbolWithoutId(o) {
32724
+ var copy = JSON.parse(JSON.stringify(o));
32725
+ if (copy.properties) delete copy.properties.id;
32726
+ return copy;
32727
+ }
32728
+
32729
+ function formatPct(val, total) {
32730
+ var pct = total > 0 ? val / total * 100 : 0;
32731
+ return String(Math.round(pct * 1000) / 1000) + '%';
32732
+ }
32733
+
32734
+ function renderHtmlFragment(o) {
32735
+ var id = o.id;
32736
+ var w = o.frame.width;
32737
+ var h = o.frame.height;
32738
+ var overlay = o.overlay;
32739
+ var selector = '#' + id;
32740
+ var boxCss = o.responsiveness == 'dynamic' ?
32741
+ `width:100%;aspect-ratio:${w} / ${h};` :
32742
+ `width:${w}px;height:${h}px;`;
32743
+ var css = [
32744
+ `${selector} {position:relative;overflow:hidden;${boxCss}}`,
32745
+ `${selector} .${CLASS_PREFIX}image {position:absolute;top:0;left:0;width:100%;height:100%;max-width:none;margin:0;display:block;}`,
32746
+ `${selector} .${CLASS_PREFIX}overlay {position:absolute;top:0;left:0;width:100%;height:100%;overflow:visible;}`
32747
+ ];
32748
+ var classCss = formatTextClassesAsCss(overlay.classes, selector);
32749
+ if (classCss) css.push(classCss);
32750
+ // Hidden from screen readers, which would otherwise read the labels as a
32751
+ // list of unrelated words, and each halo'd label twice
32752
+ var html = `<div id="${stringEscape(id)}" class="${CLASS_PREFIX}map" aria-hidden="true">
32753
+ <style>
32754
+ ${css.join('\n')}
32755
+ </style>
32756
+ <img class="${CLASS_PREFIX}image" src="${stringEscape(o.imageFile)}" width="${w}" height="${h}" alt="">`;
32757
+ if (overlay.layers.length > 0) {
32758
+ html += '\n' + renderOverlaySvg(overlay);
32759
+ }
32760
+ return html + '\n</div>\n';
32761
+ }
32762
+
32763
+ function renderOverlaySvg(overlay) {
32764
+ var svg = `<svg class="${CLASS_PREFIX}overlay" stroke-linecap="round" stroke-linejoin="round">\n`;
32765
+ if (overlay.defs.length > 0) {
32766
+ svg += '<defs>\n' + utils.pluck(overlay.defs, 'svg').join('') + '</defs>\n';
32767
+ }
32768
+ svg += overlay.layers.map(stringify).join('\n');
32769
+ return svg + '\n</svg>';
32770
+ }
32771
+
32772
+ function getHtmlFileBase(dataset, opts) {
32773
+ return opts.file ? opts.file.replace(/\.html?$/i, '') : getOutputFileBase(dataset);
32774
+ }
32775
+
32776
+ function getContainerId(base) {
32777
+ return CLASS_PREFIX + base.replace(/[^\w-]+/g, '-');
32778
+ }
32779
+
32780
+ function getResponsiveness(opts) {
32781
+ var val = opts.responsiveness || 'fixed';
32782
+ if (val != 'fixed' && val != 'dynamic') {
32783
+ stop$1('Unsupported responsiveness= option:', val, '(expected fixed or dynamic)');
32784
+ }
32785
+ return val;
32786
+ }
32787
+
32788
+ function getImageFormat(opts) {
32789
+ var fmt = String(opts.image_format || 'png').toLowerCase();
32790
+ if (fmt == 'jpg') fmt = 'jpeg';
32791
+ if (fmt != 'png' && fmt != 'jpeg') {
32792
+ stop$1('Unsupported image-format= option:', opts.image_format, '(expected png or jpg)');
32793
+ }
32794
+ return fmt;
32795
+ }
32796
+
32797
+ function getPixelRatio(opts) {
32798
+ var val = opts.pixel_ratio === undefined ? 2 : opts.pixel_ratio;
32799
+ if (val > 0 === false || val > 8) {
32800
+ stop$1('Expected pixel-ratio= to be a number greater than 0 and no more than 8');
32801
+ }
32802
+ return val;
32803
+ }
32804
+
31567
32805
  function exportKML(dataset, opts) {
31568
32806
  var toKML = require$1("@placemarkio/tokml").toKML;
31569
32807
  var geojsonOpts = Object.assign({combine_layers: true, geojson_type: 'FeatureCollection'}, opts);
@@ -40261,6 +41499,8 @@ ${svg}
40261
41499
  format = 'geotiff';
40262
41500
  } else if (ext == 'svg') {
40263
41501
  format = 'svg';
41502
+ } else if (ext == 'html' || ext == 'htm') {
41503
+ format = 'html';
40264
41504
  } else if (ext == 'kml' || ext == 'kmz') {
40265
41505
  format = 'kml';
40266
41506
  } else if (/json$/.test(ext)) {
@@ -40319,7 +41559,7 @@ ${svg}
40319
41559
  async function exportDatasets(datasets, opts) {
40320
41560
  var format = getOutputFormat(datasets[0], opts);
40321
41561
  var files;
40322
- if (format != 'svg' && format != PACKAGE_EXT) {
41562
+ if (format != 'svg' && format != 'html' && format != PACKAGE_EXT) {
40323
41563
  datasets = removeFurnitureLayers(datasets);
40324
41564
  }
40325
41565
  validateRasterExportFormat(datasets, format);
@@ -40333,8 +41573,8 @@ ${svg}
40333
41573
  opts = utils.defaults({compact: true}, opts);
40334
41574
  return exportPackedDatasets(datasets, opts);
40335
41575
  }
40336
- if (format == 'kml' || format == 'svg' || format == 'topojson' || format == 'geopackage' ||
40337
- format == 'geojson' && opts.combine_layers) {
41576
+ if (format == 'kml' || format == 'svg' || format == 'html' || format == 'topojson' ||
41577
+ format == 'geopackage' || format == 'geojson' && opts.combine_layers) {
40338
41578
  // multi-layer formats: combine multiple datasets into one
40339
41579
  if (datasets.length > 1) {
40340
41580
  datasets = [mergeDatasetsForExport(datasets)];
@@ -40353,7 +41593,12 @@ ${svg}
40353
41593
  datasets = datasets.map(copyDatasetForRenaming);
40354
41594
  assignUniqueLayerNames2(datasets);
40355
41595
  }
40356
- if (format == 'geopackage') {
41596
+ if (format == 'html') {
41597
+ sortExportLayers(datasets[0]);
41598
+ // HTML bypasses exportFileContent(), because rendering its image is async.
41599
+ files = await exportHTML(copyDatasetForHTMLExport(datasets[0]),
41600
+ utils.defaults({format: format}, opts));
41601
+ } else if (format == 'geopackage') {
40357
41602
  if (datasets.length > 1) {
40358
41603
  datasets = [mergeDatasetsForExport(datasets)];
40359
41604
  }
@@ -40424,8 +41669,19 @@ ${svg}
40424
41669
  return;
40425
41670
  }
40426
41671
  if (!datasetsHaveRasterLayers(datasets)) return;
40427
- if (format == 'svg' || format == PACKAGE_EXT) return;
40428
- stop$1('Raster layers can only be exported as GeoTIFF, SVG or ' + PACKAGE_EXT + ' files');
41672
+ if (format == 'svg' || format == 'html' || format == PACKAGE_EXT) return;
41673
+ stop$1('Raster layers can only be exported as GeoTIFF, SVG, HTML or ' + PACKAGE_EXT + ' files');
41674
+ }
41675
+
41676
+ // The parts of exportFileContent() that apply to HTML output: layers are
41677
+ // shallow-copied so they can be given unique names, which become element ids.
41678
+ function copyDatasetForHTMLExport(dataset) {
41679
+ dataset = utils.defaults({
41680
+ layers: dataset.layers.map(function(lyr) {return utils.extend({}, lyr);})
41681
+ }, dataset);
41682
+ assignUniqueLayerNames(dataset.layers);
41683
+ validateLayerData(dataset.layers);
41684
+ return dataset;
40429
41685
  }
40430
41686
 
40431
41687
  function datasetsHaveRasterLayers(datasets) {
@@ -41509,7 +42765,7 @@ ${svg}
41509
42765
  });
41510
42766
 
41511
42767
  function isSupportedOutputFormat(fmt) {
41512
- var types = ['geojson', 'topojson', 'json', 'dsv', 'dbf', 'shapefile', 'svg', 'kml', PACKAGE_EXT, 'flatgeobuf', 'geopackage', 'geoparquet', 'geotiff'];
42768
+ var types = ['geojson', 'topojson', 'json', 'dsv', 'dbf', 'shapefile', 'svg', 'html', 'kml', PACKAGE_EXT, 'flatgeobuf', 'geopackage', 'geoparquet', 'geotiff'];
41513
42769
  return types.indexOf(fmt) > -1;
41514
42770
  }
41515
42771
 
@@ -41528,7 +42784,8 @@ ${svg}
41528
42784
  geopackage: 'GeoPackage',
41529
42785
  geoparquet: 'GeoParquet',
41530
42786
  geotiff: 'GeoTIFF',
41531
- svg: 'SVG'
42787
+ svg: 'SVG',
42788
+ html: 'HTML'
41532
42789
  }[fmt] || '';
41533
42790
  }
41534
42791
 
@@ -42715,6 +43972,14 @@ ${svg}
42715
43972
  offsetOpt = {
42716
43973
  describe: 'offset distance or pct of h/w (single value or l,b,r,t list)',
42717
43974
  type: 'distance'
43975
+ },
43976
+ frameMarginOpt = {
43977
+ describe: 'padding in display units or pct of frame width; 1-4 values in CSS order, e.g. 5% or 10%,2%',
43978
+ type: 'strings'
43979
+ },
43980
+ frameOffsetOpt = {
43981
+ // undocumented: older form of margin=, one value or an l,b,r,t list
43982
+ type: 'strings'
42718
43983
  };
42719
43984
 
42720
43985
  // The label options -style used to document, before -labels took them over.
@@ -42893,7 +44158,7 @@ ${svg}
42893
44158
  }
42894
44159
  })
42895
44160
  .option('format', {
42896
- describe: 'options: shapefile,geojson,topojson,flatgeobuf,geopackage,geoparquet,geotiff,json,dbf,csv,tsv,svg'
44161
+ describe: 'options: shapefile,geojson,topojson,flatgeobuf,geopackage,geoparquet,geotiff,json,dbf,csv,tsv,svg,html'
42897
44162
  })
42898
44163
  .option('target', targetOpt)
42899
44164
  .option('force', {
@@ -43020,11 +44285,11 @@ ${svg}
43020
44285
  type: 'flag'
43021
44286
  })
43022
44287
  .option('width', {
43023
- describe: '[SVG/TopoJSON] pixel width of output (SVG default is 800)',
44288
+ describe: '[SVG/HTML/TopoJSON] pixel width of output (SVG default is 800)',
43024
44289
  type: 'number'
43025
44290
  })
43026
44291
  .option('height', {
43027
- describe: '[SVG/TopoJSON] pixel height of output (optional)',
44292
+ describe: '[SVG/HTML/TopoJSON] pixel height of output (optional)',
43028
44293
  type: 'number'
43029
44294
  })
43030
44295
  .option('max-height', {
@@ -43059,7 +44324,17 @@ ${svg}
43059
44324
  type: 'flag'
43060
44325
  })
43061
44326
  .option('jpeg-quality', {
43062
- describe: '[SVG] JPEG quality for raster images, 1-100 (default is 85)',
44327
+ describe: '[SVG/HTML] JPEG quality for raster images, 1-100 (default is 85)',
44328
+ type: 'number'
44329
+ })
44330
+ .option('responsiveness', {
44331
+ describe: '[HTML] fixed or dynamic (resizes to fill its container)'
44332
+ })
44333
+ .option('image-format', {
44334
+ describe: '[HTML] format of the map image: png or jpg (default is png)'
44335
+ })
44336
+ .option('pixel-ratio', {
44337
+ describe: '[HTML] image pixels per CSS pixel (default is 2)',
43063
44338
  type: 'number'
43064
44339
  })
43065
44340
  .option('fit-extent', {
@@ -43348,17 +44623,39 @@ ${svg}
43348
44623
  type: 'numbers'
43349
44624
  })
43350
44625
  .option('classes', {
43351
- describe: 'number of classes (can be inferred from other options)',
43352
- type: 'integer'
44626
+ describe: 'number of classes (with pivot=, or below,above the pivot)',
44627
+ type: 'numbers'
44628
+ })
44629
+ .option('pivot', {
44630
+ describe: 'diverging classes: a value, median, mean or auto (0 or median)'
44631
+ })
44632
+ .option('pivot-range', {
44633
+ describe: 'low,high range of the pivot class (diverging classes)',
44634
+ type: 'numbers'
44635
+ })
44636
+ .option('no-pivot-class', {
44637
+ describe: 'diverging classes meet at the pivot (no pivot class)',
44638
+ type: 'flag'
44639
+ })
44640
+ .option('pivot-class', {
44641
+ describe: 'add a pivot class to continuous diverging colors',
44642
+ type: 'flag'
43353
44643
  })
43354
44644
  .option('invert', {
43355
44645
  describe: 'reverse the order of colors/values',
43356
44646
  type: 'flag'
43357
44647
  })
43358
44648
  .option('continuous', {
43359
- describe: 'output interpolated values, for unclassed colors',
44649
+ describe: 'interpolate between values at the class breaks (unclassed colors)',
43360
44650
  type: 'flag'
43361
44651
  })
44652
+ .option('interpolation', {
44653
+ describe: 'color interpolation: rgb (default) or oklch'
44654
+ })
44655
+ .option('vibrance', {
44656
+ describe: '(oklch) more vivid midtones, 0-1 (default 0)',
44657
+ type: 'number'
44658
+ })
43362
44659
  .option('index-field', {
43363
44660
  describe: 'apply pre-calculated classes (0 ... n-1, -1)'
43364
44661
  })
@@ -44685,6 +45982,9 @@ ${svg}
44685
45982
  .option('stroke-dasharray', {
44686
45983
  describe: 'stroke dashes. Examples: "4" "2 4"'
44687
45984
  })
45985
+ .option('stroke-linecap', {
45986
+ describe: 'line caps: round, butt or square (dashed lines default to butt)'
45987
+ })
44688
45988
  .option('stroke-opacity', {
44689
45989
  describe: 'stroke opacity'
44690
45990
  })
@@ -44697,6 +45997,9 @@ ${svg}
44697
45997
  .option('line-end-size', {
44698
45998
  describe: 'length of an arrowhead\'s sides in px (default grows with stroke-width)'
44699
45999
  })
46000
+ .option('line-fade', {
46001
+ describe: 'share of a line (0-1) that fades in from its tail'
46002
+ })
44700
46003
  .option('opacity', {
44701
46004
  describe: 'opacity; example: 0.5'
44702
46005
  })
@@ -45019,14 +46322,9 @@ ${svg}
45019
46322
  describe: 'hold the map scale; derive the size from the new extent',
45020
46323
  type: 'flag'
45021
46324
  })
45022
- .option('offset', {
45023
- describe: 'padding in display units or pct of width/height, e.g. 5cm 20px 5%',
45024
- type: 'strings'
45025
- })
45026
- .option('offsets', {
45027
- describe: 'separate offsets for each side, in l,b,r,t order',
45028
- type: 'strings'
45029
- })
46325
+ .option('margin', frameMarginOpt)
46326
+ .option('offset', frameOffsetOpt)
46327
+ .option('offsets', frameOffsetOpt)
45030
46328
  .option('remove', {
45031
46329
  describe: 'demote the frame to an ordinary rectangle layer',
45032
46330
  type: 'flag'
@@ -45144,14 +46442,9 @@ ${svg}
45144
46442
  describe: 'frame coordinates (xmin,ymin,xmax,ymax)',
45145
46443
  type: 'bbox'
45146
46444
  })
45147
- .option('offset', {
45148
- describe: 'padding in display units or pct of width/height, e.g. 5cm 20px 5%',
45149
- type: 'strings'
45150
- })
45151
- .option('offsets', {
45152
- describe: 'separate offsets for each side, in l,b,r,t order',
45153
- type: 'strings'
45154
- })
46445
+ .option('margin', frameMarginOpt)
46446
+ .option('offset', frameOffsetOpt)
46447
+ .option('offsets', frameOffsetOpt)
45155
46448
  .option('ignore-symbols', {
45156
46449
  describe: 'fit to point locations, not the extent of symbols and labels',
45157
46450
  type: 'flag'
@@ -54064,6 +55357,7 @@ ${svg}
54064
55357
  cmd.frame = function(catalog, targets, opts) {
54065
55358
  var widthPx, heightPx, bbox;
54066
55359
  var existingFrame = getActiveFrame(catalog);
55360
+ var offsets = parseFrameOffsets(opts);
54067
55361
  if (opts.width) {
54068
55362
  widthPx = parseFrameSize(opts.width).valuePx;
54069
55363
  if (widthPx > 0 === false) {
@@ -54099,7 +55393,7 @@ ${svg}
54099
55393
  var datasets = utils.pluck(targets, 'dataset');
54100
55394
  requireDatasetsHaveCompatibleCRS(datasets, 'Targets include both projected and unprojected coordinates');
54101
55395
  bbox = getFrameContentBbox(expandCommandTargets(targets), function(contentBbox) {
54102
- var extent = getFrameExtent(contentBbox, widthPx, heightPx, opts);
55396
+ var extent = getFrameExtent(contentBbox, widthPx, heightPx, offsets);
54103
55397
  return getFrameScale(extent.bbox, extent.width, getFixedAspect$1(extent, opts));
54104
55398
  }, opts);
54105
55399
  if (!bbox && !expandCommandTargets(targets).some(function(o) {
@@ -54112,7 +55406,7 @@ ${svg}
54112
55406
  }
54113
55407
  }
54114
55408
 
54115
- var extent = getFrameExtent(bbox, widthPx, heightPx, opts);
55409
+ var extent = getFrameExtent(bbox, widthPx, heightPx, offsets);
54116
55410
  if (!extent.valid) {
54117
55411
  stop$1('Frame has a collapsed bbox');
54118
55412
  }
@@ -54146,12 +55440,10 @@ ${svg}
54146
55440
  // The frame's extent and nominal size, from the extent of its content and the
54147
55441
  // size options. Pure, so that fitting to symbols can ask what scale an extent
54148
55442
  // would give the frame.
54149
- function getFrameExtent(contentBbox, widthPx, heightPx, opts) {
54150
- var offsets = opts.offset || opts.offsets;
55443
+ function getFrameExtent(contentBbox, widthPx, heightPx, offsets) {
54151
55444
  var bbox = contentBbox.slice();
54152
55445
  var aspectRatio;
54153
- applyPercentageOffsets(bbox, offsets);
54154
- applyPixelOffsets(bbox, widthPx, heightPx, offsets);
55446
+ applyFrameOffsets(bbox, offsets, {width: widthPx, height: heightPx});
54155
55447
  aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
54156
55448
  if (!widthPx) {
54157
55449
  widthPx = roundToDigits(heightPx * aspectRatio, 1);
@@ -54200,70 +55492,113 @@ ${svg}
54200
55492
  bbox[3] += vpad / 2;
54201
55493
  }
54202
55494
 
54203
- function applyPercentageOffsets(bbox, arg) {
54204
- var sides = getPctOffsets(arg);
54205
- var l = sides[0],
54206
- b = sides[1],
54207
- r = sides[2],
54208
- t = sides[3],
54209
- w2 = (bbox[2] - bbox[0]) / (1 - l - r),
54210
- h2 = (bbox[3] - bbox[1]) / (1 - t - b);
54211
- bbox[0] -= l * w2;
54212
- bbox[1] -= b * h2;
54213
- bbox[2] += r * w2;
54214
- bbox[3] += t * h2;
54215
- }
54216
-
54217
- function applyPixelOffsets(bbox, widthPx, heightPx, arg) {
54218
- var sides = getPixelOffsets(arg);
54219
- var l = sides[0],
54220
- b = sides[1],
54221
- r = sides[2],
54222
- t = sides[3],
54223
- scale, w;
54224
-
54225
- if (widthPx && heightPx) {
54226
- // add padding to bbox to match pixel dimensions, if needed
54227
- fillOutBbox(bbox, widthPx, heightPx);
55495
+ // Margins from the margin=, offset= or offsets= option, in l,b,r,t order, or
55496
+ // null if none is given. margin= takes one to four values in CSS order (all
55497
+ // sides; vertical horizontal; top horizontal bottom; top right bottom left).
55498
+ // offset= and offsets= are the older form, and take one value or four in
55499
+ // l,b,r,t order, the order of a bbox. Each side is a length in px plus a share
55500
+ // of the frame's width: as in CSS, a percentage is of the width on every side,
55501
+ // so that 5% is an even margin.
55502
+ function parseFrameOffsets(opts) {
55503
+ var names = ['margin', 'offset', 'offsets'].filter(name => opts[name]);
55504
+ var name = names[0];
55505
+ var tokens;
55506
+ if (names.length > 1) {
55507
+ stop$1(names.map(name => name + '=').join(' and ') + ' are mutually exclusive');
54228
55508
  }
54229
-
54230
- w = bbox[2] - bbox[0];
54231
- bbox[3] - bbox[1];
54232
-
54233
- if (widthPx) {
54234
- scale = w / (widthPx - l - r);
55509
+ if (!name) return null;
55510
+ tokens = splitOffsetList(opts[name]);
55511
+ if (name == 'margin') {
55512
+ tokens = expandCssSides(tokens);
55513
+ } else if (tokens.length == 1) {
55514
+ tokens = [tokens[0], tokens[0], tokens[0], tokens[0]];
55515
+ } else if (tokens.length != 4) {
55516
+ stop$1(name + '= expects one value or four, in l,b,r,t order');
55517
+ }
55518
+ return {
55519
+ px: tokens.map(str => str.includes('%') ? 0 : parseSizeParam(str)),
55520
+ pct: tokens.map(str => str.includes('%') ? parsePercent(str) : 0)
55521
+ };
55522
+ }
55523
+
55524
+ // Accepts comma- or space-separated values, e.g. margin=10%,2% or
55525
+ // margin='10% 2%'
55526
+ function splitOffsetList(arg) {
55527
+ return [].concat(arg).join(' ').split(/[\s,]+/).filter(Boolean);
55528
+ }
55529
+
55530
+ // CSS shorthand to l,b,r,t
55531
+ function expandCssSides(arr) {
55532
+ var t, r, b, l;
55533
+ if (arr.length < 1 || arr.length > 4) {
55534
+ stop$1('margin= expects one to four values, in CSS order (top right bottom left)');
55535
+ }
55536
+ t = arr[0];
55537
+ r = arr.length > 1 ? arr[1] : t;
55538
+ b = arr.length > 2 ? arr[2] : t;
55539
+ l = arr.length > 3 ? arr[3] : r;
55540
+ return [l, b, r, t];
55541
+ }
55542
+
55543
+ // Pads @bbox in place by @offsets (from parseFrameOffsets(), or null for no
55544
+ // padding). The frame's display width, which percentage margins are a share
55545
+ // of, is given by @size.width, or follows from @size.height (when the width is
55546
+ // derived from the padded extent) or from @size.scale (map units per px, when
55547
+ // the scale is held and the size follows the extent). Given both a width and
55548
+ // a height, the content is centered on the page and the bbox takes the page's
55549
+ // shape, so the margins are a minimum on two of the sides.
55550
+ function applyFrameOffsets(bbox, offsets, size) {
55551
+ var px = offsets ? offsets.px : [0, 0, 0, 0];
55552
+ var pct = offsets ? offsets.pct : [0, 0, 0, 0];
55553
+ var cw = bbox[2] - bbox[0];
55554
+ var ch = bbox[3] - bbox[1];
55555
+ var hpct = pct[0] + pct[2],
55556
+ vpct = pct[1] + pct[3],
55557
+ hpx = px[0] + px[2],
55558
+ vpx = px[1] + px[3];
55559
+ var widthPx = size.width, heightPx = size.height, scale = size.scale;
55560
+ var margins, innerW, innerH, k, hpad = 0, vpad = 0;
55561
+
55562
+ if (hpct >= 1) {
55563
+ stop$1('Left and right offsets add up to 100% or more of the frame width');
55564
+ }
55565
+ if (widthPx > 0) ; else if (scale > 0) {
55566
+ widthPx = (cw / scale + hpx) / (1 - hpct);
55567
+ } else if (heightPx > 0) {
55568
+ // heightPx = ch / scale + vpx + vpct * widthPx, where
55569
+ // widthPx = (cw / scale + hpx) / (1 - hpct)
55570
+ k = vpct / (1 - hpct);
55571
+ innerH = heightPx - vpx - k * hpx;
55572
+ if (!(innerH > 0)) stopNoRoom();
55573
+ scale = (ch + k * cw) / innerH;
55574
+ widthPx = (cw / scale + hpx) / (1 - hpct);
54235
55575
  } else {
54236
- scale = w / (heightPx - t - b);
55576
+ stop$1('Unable to apply offsets to a frame with no display size');
55577
+ }
55578
+ margins = px.map((n, i) => n + pct[i] * widthPx);
55579
+
55580
+ if (!(scale > 0)) {
55581
+ innerW = widthPx - margins[0] - margins[2];
55582
+ if (!(innerW > 0)) stopNoRoom();
55583
+ if (heightPx > 0) {
55584
+ innerH = heightPx - margins[1] - margins[3];
55585
+ if (!(innerH > 0)) stopNoRoom();
55586
+ scale = Math.max(cw / innerW, ch / innerH);
55587
+ hpad = innerW * scale - cw;
55588
+ vpad = innerH * scale - ch;
55589
+ } else {
55590
+ scale = cw / innerW;
55591
+ }
54237
55592
  }
54238
55593
 
54239
- bbox[0] -= scale * l;
54240
- bbox[1] -= scale * b;
54241
- bbox[2] += scale * r;
54242
- bbox[3] += scale * t;
54243
- return scale;
55594
+ bbox[0] -= scale * margins[0] + hpad / 2;
55595
+ bbox[1] -= scale * margins[1] + vpad / 2;
55596
+ bbox[2] += scale * margins[2] + hpad / 2;
55597
+ bbox[3] += scale * margins[3] + vpad / 2;
54244
55598
  }
54245
55599
 
54246
- function getPctOffsets(arg) {
54247
- return adjustOffsetsArg(arg).map(str => {
54248
- return str.includes('%') ? parsePercent(str) : 0;
54249
- });
54250
- }
54251
-
54252
- function getPixelOffsets(arg) {
54253
- return adjustOffsetsArg(arg).map(str => {
54254
- return str.includes('%') ? 0 : parseSizeParam(str);
54255
- });
54256
- }
54257
-
54258
- function adjustOffsetsArg(arg) {
54259
- if (!arg) arg = ['0'];
54260
- if (arg.length == 1) {
54261
- return [arg[0], arg[0], arg[0], arg[0]];
54262
- }
54263
- if (arg.length != 4) {
54264
- stop$1('List of offsets should have 4 values');
54265
- }
54266
- return arg;
55600
+ function stopNoRoom() {
55601
+ stop$1('Frame offsets leave no room for the map');
54267
55602
  }
54268
55603
 
54269
55604
  // Convert width and height args to aspect ratio arg for the rectangle() function
@@ -54280,10 +55615,10 @@ ${svg}
54280
55615
 
54281
55616
  var Frame = /*#__PURE__*/Object.freeze({
54282
55617
  __proto__: null,
54283
- applyPercentageOffsets: applyPercentageOffsets,
54284
- applyPixelOffsets: applyPixelOffsets,
55618
+ applyFrameOffsets: applyFrameOffsets,
54285
55619
  fillOutBbox: fillOutBbox,
54286
- getAspectRatioArg: getAspectRatioArg
55620
+ getAspectRatioArg: getAspectRatioArg,
55621
+ parseFrameOffsets: parseFrameOffsets
54287
55622
  });
54288
55623
 
54289
55624
  // A frame is a viewport, not data, so it can legitimately extend past the
@@ -54442,6 +55777,7 @@ ${svg}
54442
55777
  var rec = lyr.data.getRecords()[0];
54443
55778
  var frame = getFrameLayerData(lyr, dataset.arcs);
54444
55779
  var fixedAspect = getFixedAspect(rec);
55780
+ var offsets = parseFrameOffsets(opts);
54445
55781
  var sizes, contentBbox, update;
54446
55782
 
54447
55783
  // 1. Aspect mode, resolved first because whether the page shape is fixed
@@ -54459,7 +55795,7 @@ ${svg}
54459
55795
  // 2. Extent, which fit= finds by trying extents out on steps 3 and 4
54460
55796
  if (opts.fit !== undefined) {
54461
55797
  contentBbox = getFrameContentBbox(getFitTargets(fitTargets, dataset), function(bbox) {
54462
- var o = resolveFrameUpdate(bbox, frame, fixedAspect, sizes, opts);
55798
+ var o = resolveFrameUpdate(bbox, frame, fixedAspect, sizes, offsets, opts);
54463
55799
  return getFrameScale(o.bbox, o.width, o.fixedAspect);
54464
55800
  }, opts);
54465
55801
  if (!contentBbox) {
@@ -54469,7 +55805,7 @@ ${svg}
54469
55805
  contentBbox = opts.bbox || frame.bbox;
54470
55806
  }
54471
55807
 
54472
- update = resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, opts);
55808
+ update = resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, offsets, opts);
54473
55809
  if (!update.valid) {
54474
55810
  stop$1('Frame has a collapsed bbox');
54475
55811
  }
@@ -54527,21 +55863,17 @@ ${svg}
54527
55863
 
54528
55864
  // Steps 3 and 4 as a pure function of the content extent, so that fit= can
54529
55865
  // ask what scale an extent would give the frame.
54530
- function resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, opts) {
55866
+ function resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, offsets, opts) {
54531
55867
  var bbox = contentBbox.slice();
54532
- var offsetArg = opts.offset || opts.offsets;
54533
55868
  var width = frame.width;
54534
55869
  var units = frame.units || 'px';
54535
55870
  var valid, effectiveAspect;
54536
55871
 
54537
55872
  // 3. Padding
54538
- if (offsetArg) {
54539
- applyPercentageOffsets(bbox, offsetArg);
54540
- // Pass a page height only when the shape is fixed. A derived height
54541
- // follows the extent, so letting it pad the bbox here would hold a
54542
- // re-fitted frame to its old shape instead of its new bounds.
54543
- applyPixelOffsets(bbox, frame.width,
54544
- fixedAspect ? frame.width / fixedAspect : null, offsetArg);
55873
+ if (offsets) {
55874
+ applyFrameOffsets(bbox, offsets,
55875
+ getPaddingSize(frame, sizes.width && sizes.height ?
55876
+ sizes.width.valuePx / sizes.height.valuePx : fixedAspect, sizes, opts));
54545
55877
  }
54546
55878
  valid = isValidBbox(bbox);
54547
55879
  if (fixedAspect) {
@@ -54578,6 +55910,25 @@ ${svg}
54578
55910
  };
54579
55911
  }
54580
55912
 
55913
+ // The size the frame is padded for, which is the size it ends up with: a
55914
+ // percentage offset is a share of the final width, and a px offset is px at
55915
+ // the final scale. A page height is given only when the shape is fixed; a
55916
+ // derived height follows the extent, so letting it pad the bbox would hold a
55917
+ // re-fitted frame to its old shape instead of its new bounds.
55918
+ function getPaddingSize(frame, fixedAspect, sizes, opts) {
55919
+ var width = frame.width;
55920
+ if (opts.fix_scale) {
55921
+ return {scale: getBboxWidth(frame.bbox) / frame.width};
55922
+ }
55923
+ if (sizes.width) {
55924
+ width = sizes.width.valuePx;
55925
+ } else if (sizes.height) {
55926
+ if (!fixedAspect) return {height: sizes.height.valuePx};
55927
+ width = sizes.height.valuePx * fixedAspect;
55928
+ }
55929
+ return {width: width, height: fixedAspect ? width / fixedAspect : null};
55930
+ }
55931
+
54581
55932
  function hasUpdateOptions(opts) {
54582
55933
  return opts.bbox !== undefined ||
54583
55934
  opts.fit !== undefined ||
@@ -54586,6 +55937,7 @@ ${svg}
54586
55937
  opts.aspect_ratio !== undefined ||
54587
55938
  opts.auto_aspect ||
54588
55939
  opts.fix_scale ||
55940
+ opts.margin !== undefined ||
54589
55941
  opts.offset !== undefined ||
54590
55942
  opts.offsets !== undefined;
54591
55943
  }
@@ -67886,7 +69238,7 @@ ${svg}
67886
69238
  function sphericalPointDistance(a, b) {
67887
69239
  var av = lonLatToVector(a);
67888
69240
  var bv = lonLatToVector(b);
67889
- return Math.acos(clamp$1(
69241
+ return Math.acos(clamp$2(
67890
69242
  av[0] * bv[0] + av[1] * bv[1] + av[2] * bv[2], -1, 1
67891
69243
  )) * 180 / Math.PI;
67892
69244
  }
@@ -67901,7 +69253,7 @@ ${svg}
67901
69253
  function vectorToLonLat(p) {
67902
69254
  return [
67903
69255
  Math.atan2(p[1], p[0]) * 180 / Math.PI,
67904
- Math.asin(clamp$1(p[2], -1, 1)) * 180 / Math.PI
69256
+ Math.asin(clamp$2(p[2], -1, 1)) * 180 / Math.PI
67905
69257
  ];
67906
69258
  }
67907
69259
 
@@ -68669,10 +70021,10 @@ ${svg}
68669
70021
 
68670
70022
  function getTrianglePixelBounds(grid, p1, p2, p3) {
68671
70023
  return [
68672
- clamp$1(Math.floor(Math.min(p1.x, p2.x, p3.x)), 0, grid.width - 1),
68673
- clamp$1(Math.floor(Math.min(p1.y, p2.y, p3.y)), 0, grid.height - 1),
68674
- clamp$1(Math.ceil(Math.max(p1.x, p2.x, p3.x)), 0, grid.width - 1),
68675
- clamp$1(Math.ceil(Math.max(p1.y, p2.y, p3.y)), 0, grid.height - 1)
70024
+ clamp$2(Math.floor(Math.min(p1.x, p2.x, p3.x)), 0, grid.width - 1),
70025
+ clamp$2(Math.floor(Math.min(p1.y, p2.y, p3.y)), 0, grid.height - 1),
70026
+ clamp$2(Math.ceil(Math.max(p1.x, p2.x, p3.x)), 0, grid.width - 1),
70027
+ clamp$2(Math.ceil(Math.max(p1.y, p2.y, p3.y)), 0, grid.height - 1)
68676
70028
  ];
68677
70029
  }
68678
70030
 
@@ -68710,8 +70062,8 @@ ${svg}
68710
70062
  function copyNearestRasterSample(srcGrid, destGrid, sx, sy, dx, dy, wrapX) {
68711
70063
  var srcX = wrapX ?
68712
70064
  modulo(Math.floor(sx), srcGrid.width) :
68713
- clamp$1(Math.floor(sx), 0, srcGrid.width - 1);
68714
- var srcY = clamp$1(Math.floor(sy), 0, srcGrid.height - 1);
70065
+ clamp$2(Math.floor(sx), 0, srcGrid.width - 1);
70066
+ var srcY = clamp$2(Math.floor(sy), 0, srcGrid.height - 1);
68715
70067
  var src = (srcY * srcGrid.width + srcX) * srcGrid.bands;
68716
70068
  var dest = (dy * destGrid.width + dx) * destGrid.bands;
68717
70069
  // Nearest sampling copies a nodata sample through unchanged, rather than
@@ -68728,13 +70080,13 @@ ${svg}
68728
70080
  if (wrapX) sx = modulo(sx, srcGrid.width);
68729
70081
  var srcX = wrapX ?
68730
70082
  modulo(Math.floor(sx - 0.5), srcGrid.width) :
68731
- clamp$1(Math.floor(sx - 0.5), 0, srcGrid.width - 1);
68732
- var srcY = clamp$1(Math.floor(sy - 0.5), 0, srcGrid.height - 1);
70083
+ clamp$2(Math.floor(sx - 0.5), 0, srcGrid.width - 1);
70084
+ var srcY = clamp$2(Math.floor(sy - 0.5), 0, srcGrid.height - 1);
68733
70085
  var srcX2 = wrapX ? (srcX + 1) % srcGrid.width :
68734
- clamp$1(srcX + 1, 0, srcGrid.width - 1);
68735
- var srcY2 = clamp$1(srcY + 1, 0, srcGrid.height - 1);
68736
- var tx = clamp$1(sx - 0.5 - srcX, 0, 1);
68737
- var ty = clamp$1(sy - 0.5 - srcY, 0, 1);
70086
+ clamp$2(srcX + 1, 0, srcGrid.width - 1);
70087
+ var srcY2 = clamp$2(srcY + 1, 0, srcGrid.height - 1);
70088
+ var tx = clamp$2(sx - 0.5 - srcX, 0, 1);
70089
+ var ty = clamp$2(sy - 0.5 - srcY, 0, 1);
68738
70090
  var src00 = (srcY * srcGrid.width + srcX) * srcGrid.bands;
68739
70091
  var src10 = (srcY * srcGrid.width + srcX2) * srcGrid.bands;
68740
70092
  var src01 = (srcY2 * srcGrid.width + srcX) * srcGrid.bands;
@@ -68841,7 +70193,7 @@ ${svg}
68841
70193
  return (x - a.x) * (b.y - a.y) - (y - a.y) * (b.x - a.x);
68842
70194
  }
68843
70195
 
68844
- function clamp$1(val, min, max) {
70196
+ function clamp$2(val, min, max) {
68845
70197
  return val < min ? min : val > max ? max : val;
68846
70198
  }
68847
70199
 
@@ -70473,6 +71825,367 @@ ${svg}
70473
71825
  cubehelix$1(hue);
70474
71826
  var cubehelixLong = cubehelix$1(nogamma);
70475
71827
 
71828
+ // OKLab conversion (Björn Ottosson, https://bottosson.github.io/posts/oklab/)
71829
+ // RGB objects use 0-255 channels, as returned by parseColor().
71830
+
71831
+ function rgbToOklab(rgb) {
71832
+ var r = toLinear(rgb.r / 255),
71833
+ g = toLinear(rgb.g / 255),
71834
+ b = toLinear(rgb.b / 255);
71835
+ var l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b),
71836
+ m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b),
71837
+ s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
71838
+ return {
71839
+ l: 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
71840
+ a: 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
71841
+ b: 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s
71842
+ };
71843
+ }
71844
+
71845
+ // Colors outside the sRGB gamut are clipped per channel.
71846
+ function oklabToRgb(lab) {
71847
+ var rgb = oklabToLinearRgb(lab);
71848
+ return {
71849
+ r: fromLinear(rgb.r) * 255,
71850
+ g: fromLinear(rgb.g) * 255,
71851
+ b: fromLinear(rgb.b) * 255
71852
+ };
71853
+ }
71854
+
71855
+ // OKLCH is OKLab in polar form: lightness, chroma (colorfulness) and hue
71856
+ // angle in degrees.
71857
+ function oklabToOklch(lab) {
71858
+ var h = Math.atan2(lab.b, lab.a) * 180 / Math.PI;
71859
+ return {l: lab.l, c: Math.sqrt(lab.a * lab.a + lab.b * lab.b), h: h < 0 ? h + 360 : h};
71860
+ }
71861
+
71862
+ function oklchToOklab(lch) {
71863
+ var rad = lch.h * Math.PI / 180;
71864
+ return {l: lch.l, a: lch.c * Math.cos(rad), b: lch.c * Math.sin(rad)};
71865
+ }
71866
+
71867
+ // Below this chroma a color is treated as a gray, whose hue means nothing.
71868
+ var ACHROMATIC = 0.002;
71869
+
71870
+ // Lightness may move this far (OKLCH L) to make room for chroma ...
71871
+ var MAX_LIGHTNESS_SHIFT = 0.05;
71872
+ // ... and no more than this share of the lightness step between tiles, so
71873
+ // that steps in lightness stay close to even.
71874
+ var LIGHTNESS_SHIFT_SHARE = 0.2;
71875
+ // Lightness differences count this many times as much as chroma differences
71876
+ // when fitting a color to the gamut (see fitLightness()).
71877
+ var LIGHTNESS_WEIGHT = 2;
71878
+ // Shifts and chroma losses smaller than this are not reported.
71879
+ var DEVIATION = 0.003;
71880
+
71881
+ // Where the light end of a ramp gains less chroma from vibrance than the
71882
+ // color next to it (the gamut has little room near white), the step between
71883
+ // them looks larger than the others. So that color's lightness moves toward
71884
+ // the light end by this many times the difference in chroma gained. Set by
71885
+ // eye; see getSecondLightness().
71886
+ var VIBRANCE_LIGHTNESS = 0.2;
71887
+
71888
+ // The OKLCH chroma that -classify's vibrance=1 adds, and the top of the color
71889
+ // palette panel's slider. More than this fills the gamut at most lightnesses,
71890
+ // leaving the midtones of a ramp all as vivid as they can be.
71891
+ var MAX_VIBRANCE_CHROMA = 0.09;
71892
+
71893
+ // vibrance=: 0-1, scaled to OKLCH chroma
71894
+ function getVibranceChroma(vibrance) {
71895
+ return vibrance * MAX_VIBRANCE_CHROMA;
71896
+ }
71897
+
71898
+ // A user's color with vibrance added to its chroma, as far as the gamut has
71899
+ // room at its lightness and hue. Grays are left alone, having no hue.
71900
+ function getVibrantColor(color, vibrance) {
71901
+ return getVibrantTile(color, vibrance).color;
71902
+ }
71903
+
71904
+ // Like getVibrantColor(), but returns {color, l, c, h, ideal, adjusted}, as
71905
+ // the function from getOklchInterpolator() does. A color is adjusted if the
71906
+ // gamut didn't have room for all of the vibrance.
71907
+ function getVibrantTile(color, vibrance) {
71908
+ var rgb = parseColorOrStop(color);
71909
+ var lch = oklabToOklch(rgbToOklab(rgb));
71910
+ var ideal = {l: lch.l, c: lch.c, h: lch.h};
71911
+ var c = lch.c, out;
71912
+ if (vibrance > 0 && lch.c >= ACHROMATIC) {
71913
+ ideal.c += vibrance;
71914
+ c = getVibrantChroma(lch, vibrance);
71915
+ out = oklchToRgb({l: lch.l, c: c, h: lch.h});
71916
+ out.a = rgb.a;
71917
+ }
71918
+ return {
71919
+ color: formatColor(out || rgb),
71920
+ l: lch.l, c: c, h: lch.h,
71921
+ ideal: ideal,
71922
+ adjusted: ideal.c - c > DEVIATION
71923
+ };
71924
+ }
71925
+
71926
+ function getVibrantChroma(lch, vibrance) {
71927
+ if (!(vibrance > 0) || lch.c < ACHROMATIC) return lch.c;
71928
+ return Math.max(lch.c, Math.min(lch.c + vibrance, getMaxChroma(lch.l, lch.h)));
71929
+ }
71930
+
71931
+ // Returns a function that maps t in [0, 1] to a color string. Lightness,
71932
+ // chroma and hue are each interpolated on their own, so that equal steps in
71933
+ // t are equal steps in each of them; hue takes the shorter way around the
71934
+ // color wheel, unless opts.hue is 'longer'. A gray end takes the hue of the other end, so that a ramp from
71935
+ // white to blue is blue all the way rather than passing through other hues.
71936
+ // See getOklchInterpolator() for options, and for colors outside the sRGB
71937
+ // gamut.
71938
+ function interpolateOklch(color1, color2, opts) {
71939
+ var interpolate = getOklchInterpolator(color1, color2, opts);
71940
+ return function(t) {
71941
+ return interpolate(t).color;
71942
+ };
71943
+ }
71944
+
71945
+ // Like interpolateOklch(), but the function returns {color, l, c, h, ideal,
71946
+ // adjusted}: the color, its OKLCH values, the ideal OKLCH values it was
71947
+ // fitted from ({l, c, h}), and whether fitting changed it noticeably.
71948
+ //
71949
+ // opts.vibrance (OKLCH chroma, default 0) raises the chroma of the midtones.
71950
+ // The sRGB gamut holds little chroma near black and white, so a ramp between
71951
+ // a dark and a light color has dull midtones when its chroma goes in equal
71952
+ // steps. The amount is the same at every hue. Ramps between two grays get no
71953
+ // boost. Vibrance is added to the chroma of every color of the straight
71954
+ // interpolation. The two end colors get it as far as the gamut has room at
71955
+ // their lightness, without being marked as adjusted (see getVibrantColor());
71956
+ // gray ends are left alone. In a classed ramp, the color next to the lighter
71957
+ // end moves toward it in lightness if it gained more chroma than that end,
71958
+ // and the others are evenly spaced from it to the darker end (see
71959
+ // getSecondLightness()).
71960
+ //
71961
+ // opts.steps: the number of tiles from one color to the other, if the ramp
71962
+ // is classed, which limits how far lightness may move (see below).
71963
+ //
71964
+ // opts.hue: 'shorter' (default) or 'longer', the way around the color wheel
71965
+ // that hue takes. A gray end has no hue to go around from, so a ramp with
71966
+ // one always takes the other end's hue.
71967
+ //
71968
+ // A color outside the sRGB gamut keeps its hue. Its lightness may move, up
71969
+ // to MAX_LIGHTNESS_SHIFT (and a share of the lightness step between tiles),
71970
+ // to where the gamut has room for more of its chroma, if the chroma gained
71971
+ // is worth the lightness lost (see fitLightness()). Chroma is then reduced
71972
+ // to fit.
71973
+ function getOklchInterpolator(color1, color2, opts) {
71974
+ var rgb1 = parseColorOrStop(color1),
71975
+ rgb2 = parseColorOrStop(color2);
71976
+ var lch1 = oklabToOklch(rgbToOklab(rgb1)),
71977
+ lch2 = oklabToOklch(rgbToOklab(rgb2));
71978
+ var vibrance = opts && opts.vibrance || 0;
71979
+ var steps = opts && opts.steps;
71980
+ var longHue = opts && opts.hue == 'longer' &&
71981
+ lch1.c >= ACHROMATIC && lch2.c >= ACHROMATIC;
71982
+ var maxShift = MAX_LIGHTNESS_SHIFT;
71983
+ var dh, lSecond, vivid1, vivid2;
71984
+ if (steps > 0) {
71985
+ maxShift = Math.min(maxShift, LIGHTNESS_SHIFT_SHARE * Math.abs(lch2.l - lch1.l) / steps);
71986
+ }
71987
+ if (lch1.c < ACHROMATIC && lch2.c < ACHROMATIC) vibrance = 0;
71988
+ // the ends' chroma with vibrance, before a gray end takes the other's hue
71989
+ vivid1 = getVibrantChroma(lch1, vibrance);
71990
+ vivid2 = getVibrantChroma(lch2, vibrance);
71991
+ if (lch1.c < ACHROMATIC && lch2.c >= ACHROMATIC) lch1.h = lch2.h;
71992
+ if (lch2.c < ACHROMATIC && lch1.c >= ACHROMATIC) lch2.h = lch1.h;
71993
+ dh = lch2.h - lch1.h;
71994
+ if (dh > 180) dh -= 360;
71995
+ if (dh < -180) dh += 360;
71996
+ if (longHue) dh = dh > 0 ? dh - 360 : dh + 360;
71997
+ if (vibrance > 0 && steps >= 2) {
71998
+ lSecond = getSecondLightness(lch1, lch2, dh, vibrance, steps, vivid1, vivid2);
71999
+ }
72000
+ return function(t) {
72001
+ var h = lch1.h + dh * t;
72002
+ var ideal = {
72003
+ l: lch1.l + (lch2.l - lch1.l) * t,
72004
+ c: t <= 0 ? vivid1 : t >= 1 ? vivid2 : lch1.c + (lch2.c - lch1.c) * t,
72005
+ h: normalizeHue(h)
72006
+ };
72007
+ var l = ideal.l, c = ideal.c, rgb;
72008
+ if (t > 0 && t < 1) {
72009
+ if (lSecond !== undefined) {
72010
+ ideal.l = getLightnessFromSecond(t, lch1.l, lch2.l, lSecond, steps);
72011
+ }
72012
+ ideal.c += vibrance;
72013
+ l = fitLightness(ideal.l, ideal.c, ideal.h, maxShift);
72014
+ c = Math.min(ideal.c, getMaxChroma(l, ideal.h));
72015
+ }
72016
+ rgb = oklchToRgb({l: l, c: c, h: ideal.h});
72017
+ rgb.a = rgb1.a + (rgb2.a - rgb1.a) * t;
72018
+ return {
72019
+ color: formatColor(rgb),
72020
+ l: l, c: c, h: ideal.h,
72021
+ ideal: ideal,
72022
+ adjusted: Math.abs(l - ideal.l) > DEVIATION || ideal.c - c > DEVIATION
72023
+ };
72024
+ };
72025
+ }
72026
+
72027
+ // The lightness of the color next to the lighter end of a classed ramp from
72028
+ // lch1 to lch2, boosted by vibrance. Each gains chroma as far as the gamut
72029
+ // has room at its unshifted lightness; where the color gains more than the
72030
+ // end, it moves toward the end by VIBRANCE_LIGHTNESS times the difference,
72031
+ // stopping at the end's lightness.
72032
+ function getSecondLightness(lch1, lch2, dh, vibrance, steps, vivid1, vivid2) {
72033
+ var lightFirst = lch1.l >= lch2.l;
72034
+ var light = lightFirst ? lch1 : lch2;
72035
+ var endGain = (lightFirst ? vivid1 : vivid2) - light.c;
72036
+ var t = lightFirst ? 1 / steps : 1 - 1 / steps;
72037
+ var l = lch1.l + (lch2.l - lch1.l) * t;
72038
+ var c = lch1.c + (lch2.c - lch1.c) * t;
72039
+ var h = normalizeHue(lch1.h + dh * t);
72040
+ var gain = Math.max(0, Math.min(c + vibrance, getMaxChroma(l, h)) - c);
72041
+ var shift = Math.min(VIBRANCE_LIGHTNESS * Math.max(0, gain - endGain), Math.abs(light.l - l));
72042
+ return light.l > l ? l + shift : l - shift;
72043
+ }
72044
+
72045
+ // The lightness at t of a classed ramp from lightness l1 to l2 whose color
72046
+ // next to the lighter end has lightness lSecond; the others are evenly
72047
+ // spaced from it to the darker end.
72048
+ function getLightnessFromSecond(t, l1, l2, lSecond, steps) {
72049
+ var lightFirst = l1 >= l2;
72050
+ var lDark = lightFirst ? l2 : l1;
72051
+ var u = lightFirst ? t : 1 - t; // distance from the light end
72052
+ var uSecond = 1 / steps;
72053
+ if (u <= uSecond) return lSecond;
72054
+ return lSecond + (lDark - lSecond) * (u - uSecond) / (1 - uSecond);
72055
+ }
72056
+
72057
+ function normalizeHue(h) {
72058
+ return h < 0 ? h + 360 : h >= 360 ? h - 360 : h;
72059
+ }
72060
+
72061
+ // Returns the lightness within maxShift of l that brings a color with ideal
72062
+ // lightness l and chroma c (at hue h) closest to its ideal, by a color
72063
+ // difference that weights lightness LIGHTNESS_WEIGHT times as heavily as
72064
+ // chroma: viewers notice lightness differences more, and even lightness
72065
+ // steps matter more than even chroma steps.
72066
+ function fitLightness(l, c, h, maxShift) {
72067
+ var n = 20;
72068
+ var lo = clamp$1(l - maxShift, 0, 1), hi = clamp$1(l + maxShift, 0, 1);
72069
+ var best = l, bestCost = getFitCost(l, l, c, h);
72070
+ var step = (hi - lo) / n, l2, cost, i, a, b, m1, m2;
72071
+ if (bestCost === 0 || !(maxShift > 0)) return l;
72072
+ for (i=0; i<=n; i++) {
72073
+ l2 = lo + step * i;
72074
+ cost = getFitCost(l2, l, c, h);
72075
+ if (cost < bestCost) {
72076
+ best = l2;
72077
+ bestCost = cost;
72078
+ }
72079
+ }
72080
+ // refine between the samples on either side of the best one
72081
+ a = Math.max(lo, best - step);
72082
+ b = Math.min(hi, best + step);
72083
+ for (i=0; i<20; i++) {
72084
+ m1 = a + (b - a) / 3;
72085
+ m2 = b - (b - a) / 3;
72086
+ if (getFitCost(m1, l, c, h) < getFitCost(m2, l, c, h)) {
72087
+ b = m2;
72088
+ } else {
72089
+ a = m1;
72090
+ }
72091
+ }
72092
+ l2 = (a + b) / 2;
72093
+ return getFitCost(l2, l, c, h) < bestCost ? l2 : best;
72094
+ }
72095
+
72096
+ function getFitCost(l2, l, c, h) {
72097
+ var dl = (l2 - l) * LIGHTNESS_WEIGHT;
72098
+ var dc = Math.max(0, c - getMaxChroma(l2, h));
72099
+ return dl * dl + dc * dc;
72100
+ }
72101
+
72102
+ function clamp$1(val, min, max) {
72103
+ return val < min ? min : val > max ? max : val;
72104
+ }
72105
+
72106
+ // The most chroma the sRGB gamut has at a lightness and hue
72107
+ function getMaxChroma(l, h) {
72108
+ return findMaxChroma(l, h, 0.4);
72109
+ }
72110
+
72111
+ // Converts an OKLCH color to sRGB (0-255 channels). A color outside the sRGB
72112
+ // gamut keeps its hue, then its lightness, and gives up chroma: chroma is
72113
+ // reduced to the most the gamut has at that hue and lightness.
72114
+ function oklchToRgb(lch) {
72115
+ var lab = oklchToOklab(lch);
72116
+ if (!oklabIsInGamut(lab)) {
72117
+ lab = oklchToOklab({l: lch.l, c: findMaxChroma(lch.l, lch.h, lch.c), h: lch.h});
72118
+ }
72119
+ return oklabToRgb(lab);
72120
+ }
72121
+
72122
+ function findMaxChroma(l, h, maxChroma) {
72123
+ var lo = 0, hi = maxChroma, mid;
72124
+ for (var i=0; i<24; i++) {
72125
+ mid = (lo + hi) / 2;
72126
+ if (oklabIsInGamut(oklchToOklab({l: l, c: mid, h: h}))) {
72127
+ lo = mid;
72128
+ } else {
72129
+ hi = mid;
72130
+ }
72131
+ }
72132
+ return lo;
72133
+ }
72134
+
72135
+ function oklabIsInGamut(lab) {
72136
+ var rgb = oklabToLinearRgb(lab);
72137
+ var e = 1e-6;
72138
+ return rgb.r >= -e && rgb.r <= 1 + e && rgb.g >= -e && rgb.g <= 1 + e &&
72139
+ rgb.b >= -e && rgb.b <= 1 + e;
72140
+ }
72141
+
72142
+ function oklabToLinearRgb(lab) {
72143
+ var l = cube(lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b),
72144
+ m = cube(lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b),
72145
+ s = cube(lab.l - 0.0894841775 * lab.a - 1.2914855480 * lab.b);
72146
+ return {
72147
+ r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
72148
+ g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
72149
+ b: -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
72150
+ };
72151
+ }
72152
+
72153
+ function parseColorOrStop(color) {
72154
+ var rgb = parseColor(color);
72155
+ if (!rgb) stop$1('Unsupported color:', color);
72156
+ return rgb;
72157
+ }
72158
+
72159
+ function toLinear(c) {
72160
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
72161
+ }
72162
+
72163
+ function fromLinear(c) {
72164
+ c = c < 0 ? 0 : c > 1 ? 1 : c;
72165
+ return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
72166
+ }
72167
+
72168
+ function cube(x) {
72169
+ return x * x * x;
72170
+ }
72171
+
72172
+ var Oklab = /*#__PURE__*/Object.freeze({
72173
+ __proto__: null,
72174
+ MAX_VIBRANCE_CHROMA: MAX_VIBRANCE_CHROMA,
72175
+ getMaxChroma: getMaxChroma,
72176
+ getOklchInterpolator: getOklchInterpolator,
72177
+ getVibranceChroma: getVibranceChroma,
72178
+ getVibrantColor: getVibrantColor,
72179
+ getVibrantTile: getVibrantTile,
72180
+ interpolateOklch: interpolateOklch,
72181
+ oklabIsInGamut: oklabIsInGamut,
72182
+ oklabToOklch: oklabToOklch,
72183
+ oklabToRgb: oklabToRgb,
72184
+ oklchToOklab: oklchToOklab,
72185
+ oklchToRgb: oklchToRgb,
72186
+ rgbToOklab: rgbToOklab
72187
+ });
72188
+
70476
72189
  // TODO: support three or more stops
70477
72190
  function getGradientFunction(stops) {
70478
72191
  var min = stops[0] / 100,
@@ -70488,8 +72201,41 @@ ${svg}
70488
72201
  };
70489
72202
  }
70490
72203
 
70491
- function getStoppedValues(values, stops) {
70492
- var interpolate = getInterpolatedValueGetter(values, null);
72204
+ // Returns the interpolation options of -classify, checked, with
72205
+ // interpolation=oklch filled in when vibrance= is given without a method.
72206
+ // vibrance= (0-1) is returned as OKLCH chroma, as the interpolators take it.
72207
+ function getInterpolationOptions(opts) {
72208
+ var method = opts.interpolation;
72209
+ var vibrance = opts.vibrance;
72210
+ if (method && method != 'rgb' && method != 'oklch') {
72211
+ stop$1('Unsupported interpolation method:', method, '(expected rgb or oklch)');
72212
+ }
72213
+ if (vibrance === undefined) {
72214
+ return {interpolation: method};
72215
+ }
72216
+ if (method && method != 'oklch') {
72217
+ stop$1('vibrance= requires interpolation=oklch');
72218
+ }
72219
+ if (!(vibrance >= 0 && vibrance <= 1)) {
72220
+ stop$1('vibrance= takes a value from 0 to 1');
72221
+ }
72222
+ return {interpolation: 'oklch', vibrance: getVibranceChroma(vibrance)};
72223
+ }
72224
+
72225
+ // opts.interpolation: 'rgb' (default) or 'oklch'; oklch applies only to pairs
72226
+ // of colors. opts.vibrance goes with oklch (see
72227
+ // interpolateOklch()).
72228
+ function getPairInterpolator(a, b, opts) {
72229
+ var method = opts && opts.interpolation;
72230
+ if (method == 'oklch' && parseColor(a) && parseColor(b)) {
72231
+ return interpolateOklch(a, b, opts);
72232
+ }
72233
+ return d3_interpolate(a, b);
72234
+ }
72235
+
72236
+ // opts: interpolation options (see getPairInterpolator())
72237
+ function getStoppedValues(values, stops, opts) {
72238
+ var interpolate = getInterpolatedValueGetter(values, null, opts);
70493
72239
  var n = values.length;
70494
72240
  var fstop = getGradientFunction(stops);
70495
72241
  var values2 = [];
@@ -70503,11 +72249,11 @@ ${svg}
70503
72249
  }
70504
72250
 
70505
72251
  // convert a continuous index ([0, n-1], -1) to a corresponding interpolated value
70506
- function getInterpolatedValueGetter(values, nullValue) {
72252
+ function getInterpolatedValueGetter(values, nullValue, opts) {
70507
72253
  var interpolators = [];
70508
72254
  var tmax = values.length - 1;
70509
72255
  for (var i=1; i<values.length; i++) {
70510
- interpolators.push(d3_interpolate(values[i-1], values[i]));
72256
+ interpolators.push(getPairInterpolator(values[i-1], values[i], opts));
70511
72257
  }
70512
72258
  return function(t) {
70513
72259
  if (t == -1) return nullValue;
@@ -70523,26 +72269,38 @@ ${svg}
70523
72269
  // return an array of n values
70524
72270
  // assumes that values can be interpolated by d3-interpolate
70525
72271
  // (colors and numbers should work)
70526
- function interpolateValuesToClasses(values, n, stops) {
70527
- if (values.length == n && !stops) return values;
72272
+ function interpolateValuesToClasses(values, n, stops, opts) {
72273
+ if (values.length == n && !stops) return getVibrantValues(values, opts);
70528
72274
  var numPairs = values.length - 1;
70529
- var output = [values[0]];
72275
+ var pairOpts = Object.assign({}, opts, {steps: (n - 1) / numPairs});
72276
+ var output = [getVibrantValues(values, opts)[0]];
70530
72277
  var k, j, t, intVal;
70531
72278
  for (var i=1; i<n-1; i++) {
70532
72279
  k = i / (n-1) * numPairs;
70533
72280
  j = Math.floor(k);
70534
72281
  t = k - j;
70535
72282
  // if (convert) t = convert(t);
70536
- intVal = d3_interpolate(values[j], values[j+1])(t);
72283
+ intVal = getPairInterpolator(values[j], values[j+1], pairOpts)(t);
70537
72284
  output.push(intVal);
70538
72285
  }
70539
- output.push(values[values.length - 1]);
72286
+ output.push(getVibrantValues(values, opts)[values.length - 1]);
70540
72287
  if (stops) {
70541
- output = getStoppedValues(output, stops);
72288
+ // the colors already have their vibrance
72289
+ output = getStoppedValues(output, stops, {interpolation: opts && opts.interpolation});
70542
72290
  }
70543
72291
  return output;
70544
72292
  }
70545
72293
 
72294
+ // The user's colors, with vibrance added (see getVibrantColor()), as the
72295
+ // oklch interpolator gives them at the ends of each pair
72296
+ function getVibrantValues(values, opts) {
72297
+ var vibrance = opts && opts.interpolation == 'oklch' && opts.vibrance;
72298
+ if (!(vibrance > 0)) return values;
72299
+ return values.map(function(val) {
72300
+ return parseColor(val) ? getVibrantColor(val, vibrance) : val;
72301
+ });
72302
+ }
72303
+
70546
72304
  // convert an index (0 ... n-1, -1, -2) to a corresponding discreet value
70547
72305
  function getDiscreteValueGetter(values, nullValue, otherValue) {
70548
72306
  var n = values.length;
@@ -70561,7 +72319,7 @@ ${svg}
70561
72319
  // get a function to convert class indexes to output values
70562
72320
  //
70563
72321
  if (opts.continuous) {
70564
- return getInterpolatedValueGetter(classValues, nullValue);
72322
+ return getInterpolatedValueGetter(classValues, nullValue, opts);
70565
72323
  } else {
70566
72324
  return getDiscreteValueGetter(classValues, nullValue, opts.other);
70567
72325
  }
@@ -70886,17 +72644,9 @@ ${svg}
70886
72644
  } else if (opts.breaks) {
70887
72645
  // user-defined breaks
70888
72646
  breaks = opts.breaks;
70889
- } else if (method == 'equal-interval') {
70890
- breaks = getEqualIntervalBreaks(ascending, numBreaks);
70891
- } else if (method == 'quantile') {
70892
- breaks = getQuantileBreaks(ascending, numBreaks);
70893
- } else if (method == 'hybrid') {
70894
- breaks = getHybridBreaks(ascending, numBreaks);
70895
- } else if (method == 'nice') {
70896
- breaks = getNiceBreaks(ascending, numBreaks);
70897
- message('Nice breaks:', breaks);
70898
72647
  } else {
70899
- stop$1('Unknown classification method:', method);
72648
+ breaks = getSequentialBreaks(ascending, method, numBreaks);
72649
+ if (method == 'nice') message('Nice breaks:', breaks);
70900
72650
  }
70901
72651
 
70902
72652
  printDistributionInfo(ascending, breaks, nullCount);
@@ -70928,6 +72678,18 @@ ${svg}
70928
72678
  return classifier;
70929
72679
  }
70930
72680
 
72681
+ // The inner breaks of a sequential method's classes (also used by the GUI,
72682
+ // to show each class's range)
72683
+ // ascending: the data values, in ascending order
72684
+ function getSequentialBreaks(ascending, method, numBreaks) {
72685
+ if (numBreaks === 0) return [];
72686
+ if (method == 'equal-interval') return getEqualIntervalBreaks(ascending, numBreaks);
72687
+ if (method == 'quantile') return getQuantileBreaks(ascending, numBreaks);
72688
+ if (method == 'hybrid') return getHybridBreaks(ascending, numBreaks);
72689
+ if (method == 'nice') return getNiceBreaks(ascending, numBreaks);
72690
+ stop$1('Unknown classification method:', method);
72691
+ }
72692
+
70931
72693
  function getClassRanges(breaks, ascending) {
70932
72694
  var ranges = [];
70933
72695
  var ids, geBound, ltBound, range;
@@ -71001,7 +72763,7 @@ ${svg}
71001
72763
  return function(val) {
71002
72764
  var i = -1;
71003
72765
  if (Number(val) === val) { // exclude null, NaN, strings, etc.
71004
- if (round) val = val(round);
72766
+ if (round) val = round(val);
71005
72767
  i = getClassId(val, breaks);
71006
72768
  }
71007
72769
  if (inverted && i > -1) {
@@ -71074,6 +72836,458 @@ ${svg}
71074
72836
  return arraysAreIdentical(arr, utils.genericSort(arr.map(parseFloat), false));
71075
72837
  }
71076
72838
 
72839
+ // Diverging classification: classes on either side of a pivot value, and an
72840
+ // optional neutral class at the pivot. Each side is classified on its own
72841
+ // data. See docs/development/color-scheme-panel-design.md
72842
+ //
72843
+ // A layout is {pivot, neutral, below, above, breaks, extent, dataRange}:
72844
+ // neutral is the range [lo, hi) of the neutral class, or null; below and
72845
+ // above are the numbers of classes on either side (outside the neutral
72846
+ // class); breaks are the inner breaks of all the classes, in ascending
72847
+ // order, for the sequential classifier. Classes are numbered from the
72848
+ // lowest: below, then the neutral class, then above. extent is where the
72849
+ // outer classes end: the data's min and max, or for equal interval and nice,
72850
+ // whole steps out from the pivot. dataRange is the data's [min, max].
72851
+
72852
+ var intervalMethods = ['equal-interval', 'nice'];
72853
+ var quantileMethods = ['quantile', 'hybrid'];
72854
+
72855
+ function isDivergingClassification(opts) {
72856
+ return opts.pivot !== undefined || !!opts.pivot_range;
72857
+ }
72858
+
72859
+ // Whether a diverging classification has a pivot (neutral) class: classed
72860
+ // output has one unless no-pivot-class is given; continuous output only with
72861
+ // pivot-class or pivot-range=
72862
+ function hasPivotClass(opts) {
72863
+ if (opts.pivot_class && opts.no_pivot_class) {
72864
+ stop$1('Use pivot-class or no-pivot-class, not both');
72865
+ }
72866
+ if (opts.continuous) {
72867
+ return !!(opts.pivot_class || opts.pivot_range);
72868
+ }
72869
+ return !opts.no_pivot_class;
72870
+ }
72871
+
72872
+ // ascending: the data values, in ascending order
72873
+ // opts: pivot, pivot_range, pivot_class, no_pivot_class, continuous,
72874
+ // classes (a total, or the numbers of classes [below, above]), breaks
72875
+ function getDivergingLayout(ascending, method, opts) {
72876
+ var range = opts.pivot_range || null;
72877
+ var hasNeutral = hasPivotClass(opts);
72878
+ var pivot, counts, layout;
72879
+ if (ascending.length === 0) {
72880
+ stop$1('No numeric data to classify');
72881
+ }
72882
+ if (range) {
72883
+ if (range.length != 2 || !(range[0] <= range[1])) {
72884
+ stop$1('pivot-range= takes two numbers, low,high');
72885
+ }
72886
+ if (opts.no_pivot_class) stop$1('pivot-range= sets the range of the pivot class (remove no-pivot-class)');
72887
+ }
72888
+ pivot = resolvePivot(opts.pivot, ascending, range);
72889
+ if (range && (pivot < range[0] || pivot > range[1])) {
72890
+ stop$1('The pivot', pivot, 'is outside pivot-range=', range.join(','));
72891
+ }
72892
+ if (opts.breaks) {
72893
+ if (range) stop$1('Use breaks= or pivot-range=, not both');
72894
+ layout = getBreaksLayout(opts.breaks, pivot, hasNeutral);
72895
+ } else {
72896
+ counts = parseClassCounts(opts.classes);
72897
+ if (intervalMethods.includes(method)) {
72898
+ layout = getIntervalLayout(ascending, pivot, hasNeutral, range, counts, method == 'nice');
72899
+ } else if (quantileMethods.includes(method)) {
72900
+ layout = getQuantileLayout(ascending, pivot, hasNeutral, range, counts, method);
72901
+ } else {
72902
+ stop$1('The', method, 'method does not support pivot=');
72903
+ }
72904
+ }
72905
+ if (layout.below + layout.above === 0 && !layout.neutral) {
72906
+ stop$1('Unable to classify the data around the pivot');
72907
+ }
72908
+ layout.breaks = layout.breaks.map(tidyNumber);
72909
+ if (layout.neutral) layout.neutral = layout.neutral.map(tidyNumber);
72910
+ layout.dataRange = [ascending[0], ascending[ascending.length - 1]];
72911
+ layout.extent = (layout.extent || layout.dataRange).map(tidyNumber);
72912
+ return layout;
72913
+ }
72914
+
72915
+ // The values at the color stops of continuous output: each side with
72916
+ // classes is a ramp of its own, with a stop at each of its breaks, from the
72917
+ // side's inner edge (the pivot or the pivot class) to the end of its outer
72918
+ // class. Returns {below, above}, the stops of each side in ascending order
72919
+ // (m + 1 stops for m classes, none for a side without classes).
72920
+ function getDivergingStops(layout) {
72921
+ var edges = getSideEdges(layout);
72922
+ var below = [], above = [];
72923
+ if (layout.below > 0) {
72924
+ below = [Math.min(layout.extent[0], edges[0])]
72925
+ .concat(layout.breaks.filter(function(b) { return b < edges[0]; }), [edges[0]]);
72926
+ }
72927
+ if (layout.above > 0) {
72928
+ above = [edges[1]]
72929
+ .concat(layout.breaks.filter(function(b) { return b > edges[1]; }),
72930
+ [Math.max(layout.extent[1], edges[1])]);
72931
+ }
72932
+ return {below: below, above: above};
72933
+ }
72934
+
72935
+ // The inner edges of the two sides: the pivot, or the pivot class's range
72936
+ function getSideEdges(layout) {
72937
+ return layout.neutral ? layout.neutral : [layout.pivot, layout.pivot];
72938
+ }
72939
+
72940
+ // The number of colors one side of continuous output takes: one per stop
72941
+ function getContinuousSideStops(classes) {
72942
+ return classes > 0 ? classes + 1 : 0;
72943
+ }
72944
+
72945
+ // Returns a function for continuous output: each side's value is
72946
+ // interpolated between the values at its stops, and the pivot class has the
72947
+ // center value.
72948
+ // values: the side below's values (one per stop, ascending), the center
72949
+ // value (with a pivot class), and the side above's (see
72950
+ // getDivergingClassValues())
72951
+ function getContinuousDivergingClassifier(layout, values, nullValue, opts) {
72952
+ var stops = getDivergingStops(layout);
72953
+ var nb = stops.below.length;
72954
+ var na = stops.above.length;
72955
+ var low = nb > 0 ? getStopInterpolator(stops.below, values.slice(0, nb), opts) : null;
72956
+ var high = na > 0 ? getStopInterpolator(stops.above, values.slice(values.length - na), opts) : null;
72957
+ var center = layout.neutral ? values[nb] : null;
72958
+ var edges = getSideEdges(layout);
72959
+ return function(val) {
72960
+ if (!utils.isFiniteNumber(val)) return nullValue;
72961
+ if (layout.neutral && val >= edges[0] && val < edges[1]) return center;
72962
+ if (val < edges[0]) return (low || high)(val);
72963
+ return (high || low)(val);
72964
+ };
72965
+ }
72966
+
72967
+ function getStopInterpolator(stops, values, opts) {
72968
+ var getValue = getInterpolatedValueGetter(values, null, opts);
72969
+ return function(val) {
72970
+ return getValue(getStopPosition(stops, val));
72971
+ };
72972
+ }
72973
+
72974
+ // The position of a value among ascending stops (0 at the first stop, 1 at
72975
+ // the second, ...), clamped to the stops
72976
+ function getStopPosition(stops, val) {
72977
+ var n = stops.length;
72978
+ if (!(val > stops[0])) return 0;
72979
+ for (var i=1; i<n; i++) {
72980
+ if (val <= stops[i]) {
72981
+ return stops[i] > stops[i - 1] ? i - 1 + (val - stops[i - 1]) / (stops[i] - stops[i - 1]) : i;
72982
+ }
72983
+ }
72984
+ return n - 1;
72985
+ }
72986
+
72987
+ // The CLI's message about a layout
72988
+ function formatDivergingLayout(layout) {
72989
+ var parts = [layout.below + ' below'];
72990
+ if (layout.neutral) {
72991
+ parts.push('a pivot class from ' + layout.neutral[0] + ' to ' + layout.neutral[1]);
72992
+ }
72993
+ parts.push(layout.above + ' above');
72994
+ return 'Pivot: ' + tidyNumber(layout.pivot) + ' (classes: ' + parts.join(', ') + ')';
72995
+ }
72996
+
72997
+ // pivotOpt: a number, 'median', 'mean' or 'auto' (0 if the data has
72998
+ // values on both sides of 0, or else the median); with no pivot=, the
72999
+ // middle of pivot-range=
73000
+ function resolvePivot(pivotOpt, ascending, range) {
73001
+ var n = ascending.length;
73002
+ var val = pivotOpt;
73003
+ if (val === undefined || val === null || val === '') {
73004
+ if (range) return (range[0] + range[1]) / 2;
73005
+ val = 'auto';
73006
+ }
73007
+ if (val == 'auto') {
73008
+ return ascending[0] < 0 && ascending[n - 1] > 0 ? 0 : getMedian(ascending);
73009
+ }
73010
+ if (val == 'median') return getMedian(ascending);
73011
+ if (val == 'mean') return utils.sum(ascending) / n;
73012
+ if (utils.isString(val) && val.trim() !== '' && !isNaN(+val)) val = +val;
73013
+ if (!utils.isFiniteNumber(val)) {
73014
+ stop$1('Invalid pivot:', pivotOpt, '(expected a number, median, mean or auto)');
73015
+ }
73016
+ return val;
73017
+ }
73018
+
73019
+ function getMedian(ascending) {
73020
+ var n = ascending.length;
73021
+ var mid = Math.floor(n / 2);
73022
+ return n % 2 == 1 ? ascending[mid] : (ascending[mid - 1] + ascending[mid]) / 2;
73023
+ }
73024
+
73025
+ // Returns {total} (all the classes, including a neutral class) or
73026
+ // {below, above} (the classes on either side of it)
73027
+ function parseClassCounts(classes, hasNeutral) {
73028
+ var arr = Array.isArray(classes) ? classes : [classes];
73029
+ if (arr.length == 1 && utils.isInteger(arr[0]) && arr[0] >= 2) {
73030
+ return {total: arr[0]};
73031
+ }
73032
+ if (arr.length == 2 && arr.every(isCount) && arr[0] + arr[1] > 0) {
73033
+ return {below: arr[0], above: arr[1]};
73034
+ }
73035
+ stop$1('Invalid classes= for pivot=:', String(classes),
73036
+ '(expected a total number of classes, or the numbers below and above the pivot)');
73037
+ }
73038
+
73039
+ function isCount(n) {
73040
+ return utils.isInteger(n) && n >= 0;
73041
+ }
73042
+
73043
+ // Equal-interval and nice breaks: each side's classes step out from the
73044
+ // pivot. Given a total, the two sides share one step, and the side with
73045
+ // less data gets fewer classes. An automatic neutral class is one step
73046
+ // wide, centered on the pivot.
73047
+ function getIntervalLayout(ascending, pivot, hasNeutral, range, counts, nice) {
73048
+ var extents = [Math.max(0, pivot - ascending[0]), Math.max(0, ascending[ascending.length - 1] - pivot)];
73049
+ var auto = hasNeutral && !range;
73050
+ var fixed = range ? [pivot - range[0], range[1] - pivot] : [0, 0];
73051
+ var steps, offsets, sides;
73052
+
73053
+ // distances from the pivot to each side's classes
73054
+ function getOffsets(steps) {
73055
+ var half;
73056
+ if (!auto) return fixed;
73057
+ half = Math.min(steps[0] || Infinity, steps[1] || Infinity) / 2;
73058
+ return isFinite(half) ? [half, half] : [0, 0];
73059
+ }
73060
+
73061
+ function countSides(steps) {
73062
+ var offsets = getOffsets(steps);
73063
+ return [0, 1].map(function(i) {
73064
+ return getSideCount(extents[i], offsets[i], steps[i]);
73065
+ });
73066
+ }
73067
+
73068
+ if (counts.total) {
73069
+ steps = getCommonStep(extents, counts.total - (hasNeutral ? 1 : 0), auto ? null : fixed, nice);
73070
+ sides = countSides(steps);
73071
+ } else {
73072
+ sides = [counts.below, counts.above];
73073
+ if (auto) {
73074
+ // the neutral class is one step wide, the narrower side's step
73075
+ offsets = getOffsets([0, 1].map(function(i) {
73076
+ return sides[i] > 0 ? extents[i] / (sides[i] + 0.5) : 0;
73077
+ }));
73078
+ } else {
73079
+ offsets = fixed;
73080
+ }
73081
+ sides = sides.map(function(n, i) {
73082
+ return sideHasData(extents[i] > offsets[i], n);
73083
+ });
73084
+ steps = [0, 1].map(function(i) {
73085
+ return sides[i] > 0 ? (extents[i] - offsets[i]) / sides[i] : 0;
73086
+ });
73087
+ if (nice) {
73088
+ steps = steps.map(getNiceStep);
73089
+ sides = countSides(steps);
73090
+ }
73091
+ }
73092
+ offsets = getOffsets(steps);
73093
+ return Object.assign(makeLayout(pivot, hasNeutral, pivot - offsets[0], pivot + offsets[1], sides[0], sides[1],
73094
+ getSteppedBreaks(pivot - offsets[0], -steps[0], sides[0]),
73095
+ getSteppedBreaks(pivot + offsets[1], steps[1], sides[1])), {
73096
+ extent: [pivot - offsets[0] - sides[0] * steps[0], pivot + offsets[1] + sides[1] * steps[1]]
73097
+ });
73098
+ }
73099
+
73100
+ // The number of classes of a given step that reach from a side's inner edge
73101
+ // (offset from the pivot) to its data extent
73102
+ function getSideCount(extent, offset, step) {
73103
+ if (!(extent > offset) || !(step > 0)) return 0;
73104
+ return Math.max(1, Math.ceil((extent - offset) / step - 1e-9));
73105
+ }
73106
+
73107
+ function sideHasData(hasData, count) {
73108
+ return hasData ? count : 0;
73109
+ }
73110
+
73111
+ // Returns [step, step]: the step that gives the two sides a total of n
73112
+ // classes, with the longer side's classes reaching just to the end of its
73113
+ // data (or a total as close to n as the data allows, without going over).
73114
+ // fixed: distances from the pivot to each side's classes, or null for half
73115
+ // a step (an automatic neutral class)
73116
+ // nice: use the nice step that comes closest to n classes
73117
+ function getCommonStep(extents, n, fixed, nice) {
73118
+ var candidates = [];
73119
+ var best = null;
73120
+ var count = function(s) {
73121
+ return extents.reduce(function(memo, extent, i) {
73122
+ return memo + getSideCount(extent, fixed ? fixed[i] : s / 2, s);
73123
+ }, 0);
73124
+ };
73125
+ // the steps at which a side's count of classes changes
73126
+ extents.forEach(function(extent, i) {
73127
+ for (var j=fixed ? 1 : 0; j<=n; j++) {
73128
+ candidates.push(fixed ? (extent - fixed[i]) / j : extent / (j + 0.5));
73129
+ }
73130
+ });
73131
+ candidates.filter(function(s) { return s > 0 && isFinite(s); })
73132
+ .sort(function(a, b) { return b - a; })
73133
+ .forEach(function(s) {
73134
+ var total = count(s);
73135
+ if (total <= n && (!best || total > best.total)) {
73136
+ best = {step: s, total: total};
73137
+ }
73138
+ });
73139
+ if (!best) return [0, 0];
73140
+ if (nice) {
73141
+ best = getNiceSteps(best.step).reduce(function(memo, s) {
73142
+ var total = count(s);
73143
+ var diff = Math.abs(total - n);
73144
+ return !memo || diff < memo.diff || diff == memo.diff && s > memo.step ?
73145
+ {step: s, diff: diff} : memo;
73146
+ }, null);
73147
+ }
73148
+ return [best.step, best.step];
73149
+ }
73150
+
73151
+ // nice numbers: 1, 2, 2.5 or 5 times a power of 10
73152
+ function getNiceSteps(val) {
73153
+ var exp = Math.floor(Math.log10(val));
73154
+ var steps = [];
73155
+ [exp - 1, exp, exp + 1].forEach(function(e) {
73156
+ [1, 2, 2.5, 5].forEach(function(m) {
73157
+ steps.push(tidyNumber(m * Math.pow(10, e)));
73158
+ });
73159
+ });
73160
+ return steps;
73161
+ }
73162
+
73163
+ // the nice number nearest to val (on a log scale)
73164
+ function getNiceStep(val) {
73165
+ if (!(val > 0)) return val;
73166
+ return getNiceSteps(val).reduce(function(memo, s) {
73167
+ return Math.abs(Math.log(s / val)) < Math.abs(Math.log(memo / val)) ? s : memo;
73168
+ });
73169
+ }
73170
+
73171
+ // the inner breaks of n classes stepping out from edge (step < 0 below the
73172
+ // pivot), in ascending order
73173
+ function getSteppedBreaks(edge, step, n) {
73174
+ var breaks = [];
73175
+ for (var i=1; i<n; i++) {
73176
+ breaks.push(edge + step * i);
73177
+ }
73178
+ return step < 0 ? breaks.reverse() : breaks;
73179
+ }
73180
+
73181
+ // Quantile and hybrid breaks: each side's classes divide its own data.
73182
+ // Given a total, the two sides' classes hold the same number of features,
73183
+ // so the side with fewer features gets fewer classes. An automatic neutral
73184
+ // class holds as many features as a class does on average, taken from those
73185
+ // nearest the pivot, so its range is centered on the pivot.
73186
+ function getQuantileLayout(ascending, pivot, hasNeutral, range, counts, method) {
73187
+ var n = ascending.length;
73188
+ var getBreaks = method == 'hybrid' ? getHybridBreaks : getQuantileBreaks;
73189
+ var lo = pivot, hi = pivot;
73190
+ var classCount, inNeutral, dists, d, belowVals, aboveVals, sideClasses, below, above;
73191
+ if (hasNeutral && range) {
73192
+ lo = range[0];
73193
+ hi = range[1];
73194
+ } else if (hasNeutral) {
73195
+ classCount = counts.total || counts.below + counts.above + 1;
73196
+ inNeutral = Math.max(1, Math.round(n / classCount));
73197
+ dists = ascending.map(function(val) { return Math.abs(val - pivot); })
73198
+ .sort(function(a, b) { return a - b; });
73199
+ // halfway to the next value out, so that no value sits on an edge
73200
+ d = inNeutral < n ? (dists[inNeutral - 1] + dists[inNeutral]) / 2 : dists[n - 1] * 2 + 1;
73201
+ lo = pivot - d;
73202
+ hi = pivot + d;
73203
+ }
73204
+ belowVals = ascending.filter(function(val) { return val < lo; });
73205
+ aboveVals = ascending.filter(function(val) { return val >= hi; });
73206
+ if (counts.total) {
73207
+ sideClasses = counts.total - (hasNeutral ? 1 : 0);
73208
+ below = splitClasses(sideClasses, belowVals.length, aboveVals.length);
73209
+ above = belowVals.length + aboveVals.length > 0 ? sideClasses - below : 0;
73210
+ } else {
73211
+ below = sideHasData(belowVals.length > 0, counts.below);
73212
+ above = sideHasData(aboveVals.length > 0, counts.above);
73213
+ }
73214
+ return makeLayout(pivot, hasNeutral, lo, hi, below, above,
73215
+ below > 1 ? getBreaks(belowVals, below - 1) : [],
73216
+ above > 1 ? getBreaks(aboveVals, above - 1) : []);
73217
+ }
73218
+
73219
+ // The number of n classes that go below the pivot, in proportion to the
73220
+ // counts of features, giving each side that has features at least one
73221
+ function splitClasses(n, countBelow, countAbove) {
73222
+ var below;
73223
+ if (countBelow + countAbove === 0) return 0;
73224
+ below = Math.round(n * countBelow / (countBelow + countAbove));
73225
+ if (countBelow > 0 && below === 0 && n > 1) below = 1;
73226
+ if (countAbove > 0 && below == n && n > 1) below = n - 1;
73227
+ return below;
73228
+ }
73229
+
73230
+ // User-defined breaks: a pivot that is one of the breaks divides the
73231
+ // classes; one inside a class makes that class the neutral class.
73232
+ function getBreaksLayout(breaks, pivot, hasNeutral) {
73233
+ var b = breaks.length;
73234
+ var k = breaks.indexOf(pivot);
73235
+ var i;
73236
+ if (k > -1) {
73237
+ return {pivot: pivot, neutral: null, below: k + 1, above: b - k, breaks: breaks};
73238
+ }
73239
+ for (i=0; i<b && breaks[i] < pivot; i++);
73240
+ // the pivot is in class i
73241
+ if (!hasNeutral) {
73242
+ stop$1('Without a pivot class, the pivot must be one of the breaks');
73243
+ }
73244
+ return {
73245
+ pivot: pivot,
73246
+ neutral: [i > 0 ? breaks[i - 1] : -Infinity, i < b ? breaks[i] : Infinity],
73247
+ below: i,
73248
+ above: b - i,
73249
+ breaks: breaks
73250
+ };
73251
+ }
73252
+
73253
+ function makeLayout(pivot, hasNeutral, lo, hi, below, above, breaksBelow, breaksAbove) {
73254
+ var breaks = breaksBelow.concat();
73255
+ if (hasNeutral) {
73256
+ if (below > 0) breaks.push(lo);
73257
+ if (above > 0) breaks.push(hi);
73258
+ } else if (below > 0 && above > 0) {
73259
+ breaks.push(pivot);
73260
+ }
73261
+ return {
73262
+ pivot: pivot,
73263
+ neutral: hasNeutral ? [lo, hi] : null,
73264
+ below: below,
73265
+ above: above,
73266
+ breaks: breaks.concat(breaksAbove)
73267
+ };
73268
+ }
73269
+
73270
+ // rounds off floating point noise, like 0.30000000000000004
73271
+ function tidyNumber(val) {
73272
+ return isFinite(val) ? +val.toPrecision(12) : val;
73273
+ }
73274
+
73275
+ var Diverging = /*#__PURE__*/Object.freeze({
73276
+ __proto__: null,
73277
+ formatDivergingLayout: formatDivergingLayout,
73278
+ getContinuousDivergingClassifier: getContinuousDivergingClassifier,
73279
+ getContinuousSideStops: getContinuousSideStops,
73280
+ getDivergingLayout: getDivergingLayout,
73281
+ getDivergingStops: getDivergingStops,
73282
+ getNiceStep: getNiceStep,
73283
+ getStopPosition: getStopPosition,
73284
+ hasPivotClass: hasPivotClass,
73285
+ isDivergingClassification: isDivergingClassification,
73286
+ parseClassCounts: parseClassCounts,
73287
+ resolvePivot: resolvePivot,
73288
+ splitClasses: splitClasses
73289
+ });
73290
+
71077
73291
  function getCategoricalClassifier(classValues, nullVal, opts) {
71078
73292
  // categories: strings to match in the data
71079
73293
  var categories = opts.categories;
@@ -71885,7 +74099,7 @@ ${svg}
71885
74099
  addSchemesFromD3('categorical', 'Category10,Accent,Dark2,Paired,Pastel1,Pastel2,Set1,Set2,Set3,Tableau10');
71886
74100
  addSchemesFromD3('sequential', 'Blues,Greens,Greys,Purples,Reds,Oranges,BuGn,BuPu,GnBu,OrRd,PuBuGn,PuBu,PuRd,RdPu,YlGnBu,YlGn,YlOrBr,YlOrRd');
71887
74101
  addSchemesFromD3('rainbow', 'Cividis,CubehelixDefault,Rainbow,Warm,Cool,Sinebow,Turbo,Viridis,Magma,Inferno,Plasma');
71888
- addSchemesFromD3('diverging', 'BrBG,PRGn,PRGn,PiYG,PuOr,RdBu,RdGy,RdYlBu,RdYlGn,Spectral');
74102
+ addSchemesFromD3('diverging', 'BrBG,PRGn,PiYG,PuOr,RdBu,RdGy,RdYlBu,RdYlGn,Spectral');
71889
74103
  testLib(); // make sure these schemes are all available
71890
74104
  addCategoricalScheme('Category20',
71891
74105
  '1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5');
@@ -71967,6 +74181,13 @@ ${svg}
71967
74181
  print ('\nMulti-hue/rainbow\n' + formatStringsAsGrid(index.rainbow));
71968
74182
  }
71969
74183
 
74184
+ // type: categorical, sequential, rainbow or diverging
74185
+ function getColorSchemeNames(type) {
74186
+ initSchemes();
74187
+ if (!index[type]) error('Unknown color scheme type:', type);
74188
+ return index[type].concat();
74189
+ }
74190
+
71970
74191
  function pickRandomColorScheme(type) {
71971
74192
  initSchemes();
71972
74193
  var names = index[type];
@@ -72016,6 +74237,14 @@ ${svg}
72016
74237
  return colors;
72017
74238
  }
72018
74239
 
74240
+ // All the colors of a categorical scheme
74241
+ function getCategoricalColors(name) {
74242
+ initSchemes();
74243
+ name = standardName(name);
74244
+ if (!isCategoricalColorScheme(name)) stop$1('Not a categorical color scheme:', name);
74245
+ return (ramps[name] || d3Scales['scheme' + name]).concat();
74246
+ }
74247
+
72019
74248
  function wrapColors(colors, n) {
72020
74249
  while (colors.length > 0 && colors.length < n) {
72021
74250
  colors = colors.concat(colors.slice(0, n - colors.length));
@@ -72033,7 +74262,8 @@ ${svg}
72033
74262
  return index.categorical.includes(standardName(name));
72034
74263
  }
72035
74264
 
72036
- function getColorRamp(name, n, stops) {
74265
+ // interpOpts: how stops= colors are interpolated (see getPairInterpolator())
74266
+ function getColorRamp(name, n, stops, interpOpts) {
72037
74267
  initSchemes();
72038
74268
  name = standardName(name);
72039
74269
  var ramps = d3Scales['scheme' + name];
@@ -72051,7 +74281,7 @@ ${svg}
72051
74281
  ramp = getInterpolatedRamp(interpolate, n);
72052
74282
  }
72053
74283
  if (stops) {
72054
- ramp = getStoppedValues(ramp, stops);
74284
+ ramp = getStoppedValues(ramp, stops, interpOpts);
72055
74285
  }
72056
74286
  return ramp;
72057
74287
  }
@@ -72067,6 +74297,21 @@ ${svg}
72067
74297
  return ramp;
72068
74298
  }
72069
74299
 
74300
+ var ColorSchemes = /*#__PURE__*/Object.freeze({
74301
+ __proto__: null,
74302
+ getCategoricalColorScheme: getCategoricalColorScheme,
74303
+ getCategoricalColors: getCategoricalColors,
74304
+ getColorRamp: getColorRamp,
74305
+ getColorSchemeNames: getColorSchemeNames,
74306
+ getRandomizedCategoricalColorScheme: getRandomizedCategoricalColorScheme,
74307
+ isCategoricalColorScheme: isCategoricalColorScheme,
74308
+ isColorSchemeName: isColorSchemeName,
74309
+ pickRandomCategoricalScheme: pickRandomCategoricalScheme,
74310
+ pickRandomColorScheme: pickRandomColorScheme,
74311
+ printColorSchemeNames: printColorSchemeNames,
74312
+ wrapColors: wrapColors
74313
+ });
74314
+
72070
74315
  function getNullValue(opts) {
72071
74316
  var nullValue;
72072
74317
  if ('null_value' in opts) {
@@ -72118,11 +74363,11 @@ ${svg}
72118
74363
  if (categorical && isCategoricalColorScheme(colorScheme)) {
72119
74364
  return getCategoricalColorScheme(colorScheme, n);
72120
74365
  } else {
72121
- return getColorRamp(colorScheme, n, opts.stops);
74366
+ return getColorRamp(colorScheme, n, opts.stops, opts);
72122
74367
  }
72123
74368
  } else if (opts.colors || opts.values) {
72124
74369
  if (categorical) {
72125
- return getCategoricalValues(opts.colors || opts.values, n);
74370
+ return getCategoricalValues(opts.colors || opts.values, n, !!opts.colors);
72126
74371
  } else {
72127
74372
  return getInterpolableValues(opts.colors || opts.values, n, opts);
72128
74373
  }
@@ -72132,7 +74377,87 @@ ${svg}
72132
74377
  }
72133
74378
  }
72134
74379
 
72135
- function getCategoricalValues(values, n) {
74380
+ // The values of the classes of a diverging layout (see getDivergingLayout()):
74381
+ // colors (or values) are spread over the longer side, and the shorter side
74382
+ // uses the ones nearest the center, so equal steps from the pivot get
74383
+ // equally strong colors on both sides. The middle of a scheme or list of
74384
+ // colors is the center color (an even list interpolates one).
74385
+ // With opts.continuous, each side has a value per color stop (see
74386
+ // getDivergingStops()) instead of one per class.
74387
+ function getDivergingClassValues(layoutArg, opts) {
74388
+ var layout = opts.continuous ? Object.assign({}, layoutArg, {
74389
+ below: getContinuousSideStops(layoutArg.below),
74390
+ above: getContinuousSideStops(layoutArg.above)
74391
+ }) : layoutArg;
74392
+ var n = layout.below + layout.above + (layout.neutral ? 1 : 0);
74393
+ var colorArg = opts.colors && opts.colors.length == 1 ? opts.colors[0] : null;
74394
+ var list = opts.colors || opts.values;
74395
+ var colorScheme, values;
74396
+ if (opts.stops) stop$1('stops= is not supported with pivot=');
74397
+ if (colorArg == 'random') {
74398
+ colorScheme = pickRandomColorScheme('diverging');
74399
+ message('Randomly selected color scheme:', colorScheme);
74400
+ } else if (isColorSchemeName(colorArg)) {
74401
+ colorScheme = colorArg;
74402
+ } else if (colorArg && !parseColor(colorArg)) {
74403
+ stop$1('Unrecognized color scheme name:', colorArg);
74404
+ } else if (opts.colors) {
74405
+ opts.colors.forEach(validateColor);
74406
+ }
74407
+ if (!colorScheme && !list) {
74408
+ values = getIndexes(n);
74409
+ } else if (!colorScheme && list.length == n && listFitsLayout(layout, opts)) {
74410
+ // one value per class
74411
+ values = interpolateValuesToClasses(parseValues(list), n, null, opts);
74412
+ } else if (!colorScheme && list.length < 2) {
74413
+ stop$1('Expected a color scheme or a list of two or more values');
74414
+ } else if (opts.invert) {
74415
+ // the sides trade places
74416
+ return getDivergingValues(colorScheme, list, layout.above, layout.below, !!layout.neutral, opts).reverse();
74417
+ } else {
74418
+ return getDivergingValues(colorScheme, list, layout.below, layout.above, !!layout.neutral, opts);
74419
+ }
74420
+ return opts.invert ? values.reverse() : values;
74421
+ }
74422
+
74423
+ // A list with one value per class is used as given if its middle value
74424
+ // falls at the pivot, or if classes= gave the numbers of classes on each side
74425
+ function listFitsLayout(layout, opts) {
74426
+ return layout.below == layout.above ||
74427
+ Array.isArray(opts.classes) && opts.classes.length == 2;
74428
+ }
74429
+
74430
+ function getDivergingValues(colorScheme, list, below, above, neutral, opts) {
74431
+ var k = Math.max(below, above, 1);
74432
+ var full = colorScheme ?
74433
+ getColorRamp(colorScheme, k * 2 + 1, null, opts) :
74434
+ getDivergingRamp(parseValues(list), k, opts);
74435
+ // full: k values below the center value, and k above
74436
+ return full.slice(k - below, k)
74437
+ .concat(neutral ? [full[k]] : [])
74438
+ .concat(full.slice(k + 1, k + 1 + above));
74439
+ }
74440
+
74441
+ // Interpolates each half of a list separately, so the center value stays in
74442
+ // the middle
74443
+ function getDivergingRamp(values, k, opts) {
74444
+ var mid = Math.floor(values.length / 2);
74445
+ var odd = values.length % 2 == 1;
74446
+ var center = odd ? values[mid] : getPairInterpolator(values[mid - 1], values[mid], opts)(0.5);
74447
+ var lower = values.slice(0, mid).concat([center]);
74448
+ var upper = [center].concat(values.slice(odd ? mid + 1 : mid));
74449
+ var lowerRamp = interpolateValuesToClasses(lower, k + 1, null, opts);
74450
+ var upperRamp = interpolateValuesToClasses(upper, k + 1, null, opts);
74451
+ return lowerRamp.concat(upperRamp.slice(1));
74452
+ }
74453
+
74454
+ // A list of colors shorter than the list of categories is repeated, as the
74455
+ // colors of a color scheme are
74456
+ function getCategoricalValues(values, n, valuesAreColors) {
74457
+ if (valuesAreColors && values.length < n) {
74458
+ message('Repeating', values.length, 'colors to match', n, 'categories.');
74459
+ values = wrapColors(values, n);
74460
+ }
72136
74461
  if (n != values.length) {
72137
74462
  stop$1('Mismatch in number of categories and number of values');
72138
74463
  }
@@ -72149,18 +74474,15 @@ ${svg}
72149
74474
 
72150
74475
  // TODO: check for non-interpolatable value types (e.g. boolean, text)
72151
74476
  function getInterpolableValues(arr, n, opts) {
72152
- var values = parseValues(arr);
72153
- if (n != values.length || opts.stops) {
72154
- return interpolateValuesToClasses(values, n, opts.stops);
72155
- }
72156
- return values;
74477
+ // with one value per class, this only adds vibrance= to colors
74478
+ return interpolateValuesToClasses(parseValues(arr), n, opts.stops, opts);
72157
74479
  }
72158
74480
 
72159
74481
  // convert strings to numbers if they all parse as numbers
72160
74482
  // arr: an array of strings
72161
74483
  function parseValues(strings) {
72162
74484
  var values = strings;
72163
- if (strings.every(utils.parseNumber)) {
74485
+ if (strings.every(str => utils.parseNumber(str) !== null)) {
72164
74486
  values = strings.map(function(str) {
72165
74487
  return +str;
72166
74488
  });
@@ -72293,11 +74615,12 @@ ${svg}
72293
74615
  var values, nullValue;
72294
74616
  var classifyByValue, classifyByRecordId;
72295
74617
  var numClasses, numValues;
72296
- var method;
74618
+ var method, diverging, divergingLayout;
72297
74619
 
72298
74620
  if (opts.color_scheme) {
72299
74621
  stop$1('color-scheme is not a valid option, use colors instead');
72300
74622
  }
74623
+ opts = Object.assign({}, opts, getInterpolationOptions(opts));
72301
74624
 
72302
74625
  // get data field to use for classification
72303
74626
  //
@@ -72332,17 +74655,35 @@ ${svg}
72332
74655
  //
72333
74656
  // expand categories if value is '*'
72334
74657
  // use all unique values if categories option is missing
74658
+ // (empty values are not categories: they get the null value)
72335
74659
  if (method == 'categorical') {
72336
74660
  if ((!opts.categories || opts.categories.includes('*')) && dataField) {
72337
- opts.categories = getUniqFieldValues(records, dataField);
74661
+ opts.categories = getUniqFieldValues(records, dataField).filter(isCategoryValue);
72338
74662
  }
72339
74663
  if (opts.categories && fieldType == 'number') {
72340
74664
  opts.categories = opts.categories.map(str => +str);
72341
74665
  }
72342
74666
  }
72343
74667
 
72344
- if (opts.classes) {
72345
- if (!utils.isInteger(opts.classes) || opts.classes > 1 === false) {
74668
+ diverging = isDivergingClassification(opts);
74669
+ if (diverging) {
74670
+ validateDivergingOptions(method, fieldType);
74671
+ } else if (opts.no_pivot_class || opts.pivot_class) {
74672
+ stop$1(opts.pivot_class ? 'pivot-class' : 'no-pivot-class', 'requires pivot= or pivot-range=');
74673
+ }
74674
+ if (Array.isArray(opts.classes) && !diverging) {
74675
+ if (opts.classes.length != 1) {
74676
+ stop$1('classes= takes two numbers (classes below and above the pivot) only with pivot=');
74677
+ }
74678
+ opts.classes = opts.classes[0];
74679
+ }
74680
+
74681
+ if (diverging) {
74682
+ // the classes depend on the data (see getDivergingLayout())
74683
+ numClasses = 0;
74684
+ } else if (opts.classes) {
74685
+ // continuous output can have a single interval, from the min to the max
74686
+ if (!utils.isInteger(opts.classes) || opts.classes >= (opts.continuous ? 1 : 2) === false) {
72346
74687
  stop$1('Invalid number of classes:', opts.classes, '(expected a value greater than 1)');
72347
74688
  }
72348
74689
  numClasses = opts.classes;
@@ -72355,24 +74696,37 @@ ${svg}
72355
74696
  } else if (method == 'categorical' && opts.categories) {
72356
74697
  numClasses = opts.categories.length;
72357
74698
  } else if (opts.colors && opts.colors.length > 1) {
72358
- numClasses = opts.colors.length;
74699
+ // continuous: each color is a stop, at the ends and breaks of the classes
74700
+ numClasses = opts.continuous ? opts.colors.length - 1 : opts.colors.length;
72359
74701
  } else if (opts.values && opts.values.length > 1) {
72360
- numClasses = opts.values.length;
74702
+ numClasses = opts.continuous ? opts.values.length - 1 : opts.values.length;
72361
74703
  } else if (method == 'non-adjacent') {
72362
74704
  numClasses = 5;
72363
74705
  } else {
72364
74706
  numClasses = 4;
72365
74707
  }
72366
74708
  numValues = opts.continuous ? numClasses + 1 : numClasses;
72367
- if (numValues > 1 === false) {
74709
+ if (numValues > 1 === false && !diverging) {
72368
74710
  stop$1('Missing a valid number of values');
72369
74711
  }
72370
74712
 
72371
74713
  // get colors or other values
72372
74714
  //
72373
- values = getClassValues(method, numValues, opts);
72374
- if (opts.invert) {
72375
- values = values.concat().reverse();
74715
+ if (diverging && fieldType === null) {
74716
+ values = []; // no data: every feature gets the null value
74717
+ } else if (diverging) {
74718
+ divergingLayout = getDivergingLayout(getDivergingData(records, dataField, opts), method,
74719
+ Object.assign({}, opts, {classes: getDivergingClassCount(opts)}));
74720
+ message(formatDivergingLayout(divergingLayout));
74721
+ if (opts.continuous) {
74722
+ message(formatDivergingStops(divergingLayout));
74723
+ }
74724
+ values = getDivergingClassValues(divergingLayout, opts);
74725
+ } else {
74726
+ values = getClassValues(method, numValues, opts);
74727
+ if (opts.invert) {
74728
+ values = values.concat().reverse();
74729
+ }
72376
74730
  }
72377
74731
  if (valuesAreColors) {
72378
74732
  message('Colors:', formatValuesForLogging(values));
@@ -72394,6 +74748,11 @@ ${svg}
72394
74748
  classifyByValue = getIndexedClassifier(values, nullValue, opts);
72395
74749
  } else if (method == 'categorical') {
72396
74750
  classifyByValue = getCategoricalClassifier(values, nullValue, opts);
74751
+ } else if (diverging && opts.continuous) {
74752
+ classifyByValue = getContinuousDivergingDataClassifier(divergingLayout, values, nullValue, opts);
74753
+ } else if (diverging) {
74754
+ classifyByValue = getSequentialClassifier$1(values, nullValue, getFieldValues(records, dataField), method,
74755
+ Object.assign({}, opts, {breaks: divergingLayout.breaks}));
72397
74756
  } else {
72398
74757
  classifyByValue = getSequentialClassifier$1(values, nullValue, getFieldValues(records, dataField), method, opts);
72399
74758
  }
@@ -72426,6 +74785,73 @@ ${svg}
72426
74785
  lyr.data.markSchemaChanged({operation: 'classify', field: outputField});
72427
74786
  };
72428
74787
 
74788
+ function validateDivergingOptions(method, fieldType, opts) {
74789
+ if (fieldType === null) return; // no data
74790
+ if (fieldType != 'number') {
74791
+ stop$1('pivot= requires a numeric data field');
74792
+ }
74793
+ if (!['quantile', 'equal-interval', 'nice', 'hybrid', 'breaks'].includes(method)) {
74794
+ stop$1('The', method, 'method does not support pivot=');
74795
+ }
74796
+ }
74797
+
74798
+ // Rounds and clamps data values as getDivergingData() does
74799
+ function getContinuousDivergingDataClassifier(layout, values, nullValue, opts) {
74800
+ var classify = getContinuousDivergingClassifier(layout, values, nullValue, opts);
74801
+ var round = opts.precision ? getRoundingFunction(opts.precision) : null;
74802
+ var range = opts.outer_breaks;
74803
+ return function(val) {
74804
+ if (!utils.isFiniteNumber(val)) return nullValue;
74805
+ if (round) val = round(val);
74806
+ if (range) val = Math.min(Math.max(val, range[0]), range[1]);
74807
+ return classify(val);
74808
+ };
74809
+ }
74810
+
74811
+ function formatDivergingStops(layout) {
74812
+ var stops = getDivergingStops(layout);
74813
+ var parts = [];
74814
+ if (stops.below.length > 0) parts.push('below: ' + stops.below.join(', '));
74815
+ if (stops.above.length > 0) parts.push('above: ' + stops.above.join(', '));
74816
+ return 'Color stops (' + parts.join('; ') + ')';
74817
+ }
74818
+
74819
+ // The data that the classes divide: numbers, rounded and clamped as the
74820
+ // sequential classifier does
74821
+ function getDivergingData(records, field, opts) {
74822
+ var values = getFieldValues(records, field);
74823
+ var ascending;
74824
+ if (opts.precision) {
74825
+ values = values.map(getRoundingFunction(opts.precision));
74826
+ }
74827
+ ascending = getAscendingNumbers(values);
74828
+ if (opts.outer_breaks) {
74829
+ ascending = applyDataRange(ascending, opts.outer_breaks);
74830
+ }
74831
+ return ascending;
74832
+ }
74833
+
74834
+ // The total number of classes (including a pivot class), or the numbers of
74835
+ // classes [below, above] the pivot
74836
+ function getDivergingClassCount(opts) {
74837
+ var hasNeutral = hasPivotClass(opts);
74838
+ var list = opts.colors && opts.colors.length > 1 && opts.colors ||
74839
+ opts.values && opts.values.length > 1 && opts.values;
74840
+ if (opts.classes) return opts.classes;
74841
+ if (list && opts.continuous) {
74842
+ // a list of 2K+1 colors has K stops on each side, for K-1 classes
74843
+ return Math.max(2, (Math.floor(list.length / 2) - 1) * 2) + (hasNeutral ? 1 : 0);
74844
+ }
74845
+ if (list) return list.length;
74846
+ return hasNeutral ? 7 : 6;
74847
+ }
74848
+
74849
+ // Like the categorical classifier, counts 0 as a value and other falsy
74850
+ // values (null, undefined, '', NaN) as empty
74851
+ function isCategoryValue(val) {
74852
+ return !!val || val === 0;
74853
+ }
74854
+
72429
74855
  function formatValuesForLogging(arr) {
72430
74856
  if (arr.some(val => utils.isString(val) && val.indexOf('rgb(') === 0)) {
72431
74857
  return formatColorsAsHex(arr);
@@ -85345,7 +87771,7 @@ ${svg}
85345
87771
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
85346
87772
  }
85347
87773
 
85348
- var version = "0.7.72";
87774
+ var version = "0.7.74";
85349
87775
 
85350
87776
  // Parse command line args into commands and run them
85351
87777
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -85736,392 +88162,6 @@ ${svg}
85736
88162
  testCommands: testCommands
85737
88163
  });
85738
88164
 
85739
- // Finding the font file a label's font-family names, among the fonts installed
85740
- // on this computer.
85741
- //
85742
- // The browser does this for us in the GUI; in Node there is nothing between
85743
- // mapshaper and the filesystem, so a family name has to be matched against the
85744
- // name tables of the files themselves. Names cannot be inferred from
85745
- // filenames: NYTFranklinLight.otf calls itself "NYTFranklin Light", and a
85746
- // label asking for NYTFranklin at weight 300 has to reach it.
85747
- //
85748
- // See docs/development/label-tool-design.md.
85749
-
85750
- // Collections (.ttc/.otc) hold several faces in one file and are how macOS
85751
- // ships Helvetica, Menlo and Avenir, so a lookup that skipped them would miss
85752
- // the font mapshaper's own tool writes by default there.
85753
- var FONT_FILE_RXP = /\.(ttf|otf|ttc|otc)$/i;
85754
-
85755
- // What a generic family means when there is no browser to resolve it. A guess,
85756
- // but an ordered one: the first of these that is installed is what a browser
85757
- // on this platform would almost certainly have picked.
85758
- var GENERIC_FAMILIES = {
85759
- 'sans-serif': ['Helvetica', 'Arial', 'Liberation Sans', 'DejaVu Sans', 'Roboto', 'Segoe UI'],
85760
- serif: ['Times New Roman', 'Times', 'Liberation Serif', 'DejaVu Serif', 'Georgia'],
85761
- monospace: ['Menlo', 'Courier New', 'Liberation Mono', 'DejaVu Sans Mono', 'Consolas'],
85762
- 'system-ui': ['Helvetica Neue', 'Segoe UI', 'Cantarell', 'Roboto'],
85763
- cursive: [],
85764
- fantasy: []
85765
- };
85766
-
85767
- var faceCache = {};
85768
- var familyCache = {};
85769
- var fullIndex = null;
85770
- var fileList = null;
85771
-
85772
- // Width classes, as OS/2 numbers them 1 to 9. A font-stretch is one of these
85773
- // keywords or a percentage of normal width.
85774
- var STRETCH_NAMES = {
85775
- 'ultra-condensed': 1, 'extra-condensed': 2, condensed: 3,
85776
- 'semi-condensed': 4, normal: 5, 'semi-expanded': 6, expanded: 7,
85777
- 'extra-expanded': 8, 'ultra-expanded': 9
85778
- };
85779
- var STRETCH_PERCENTS = [50, 62.5, 75, 87.5, 100, 112.5, 125, 150, 200];
85780
-
85781
- // The face to open for @family in (@weight, @italic, @stretch), or null if this
85782
- // computer has no such font: {path, postscriptName}.
85783
- //
85784
- // postscriptName is how a face inside a collection is named to fontkit, and is
85785
- // null for a file holding one face.
85786
- function findFontFace(family, weight, italic, stretch) {
85787
- var key = [family, weight, italic ? 'i' : 'n', stretch || ''].join('|');
85788
- if (!(key in faceCache)) {
85789
- faceCache[key] = lookupFace(family, weight, italic, stretch);
85790
- }
85791
- return faceCache[key];
85792
- }
85793
-
85794
- // Which of a family's faces answers a request for (@weight, @italic,
85795
- // @stretch).
85796
- //
85797
- // Width first, then upright before oblique, then the nearest weight, then the
85798
- // heavier of two equally near. The first three are the order CSS matches fonts
85799
- // in, and the last is the rule the style menu uses to carry a face across a
85800
- // change of font (getNearestVariant() in gui-label-fonts.mjs), applied here to
85801
- // faces read from files rather than measured in a browser.
85802
- //
85803
- // A missing face is answered with a near one rather than refused: the browser
85804
- // would synthesize the missing weight or slant from exactly this face, so its
85805
- // widths are much closer to what is drawn than no measurement at all.
85806
- function pickFace(faces, weight, italic, stretch) {
85807
- var wanted = weight > 0 ? weight : 400;
85808
- var wantedWidth = parseFontStretch(stretch);
85809
- var best = null;
85810
- var bestScore = null;
85811
- (faces || []).forEach(function(face) {
85812
- var score = [
85813
- Math.abs((face.width || 5) - wantedWidth),
85814
- !!face.italic === !!italic ? 0 : 1,
85815
- Math.abs(face.weight - wanted),
85816
- face.weight < wanted ? 1 : 0
85817
- ];
85818
- if (!best || compareScores(score, bestScore) < 0) {
85819
- best = face;
85820
- bestScore = score;
85821
- }
85822
- });
85823
- return best;
85824
- }
85825
-
85826
- // A font-stretch as the width class it names, or normal width for anything
85827
- // unreadable. A percentage is taken to the nearest class, the way CSS defines
85828
- // the keywords.
85829
- function parseFontStretch(stretch) {
85830
- var str = String(stretch === null || stretch === undefined ? '' : stretch).trim().toLowerCase();
85831
- var pct = /^([.0-9]+)%$/.exec(str);
85832
- var best = 5;
85833
- if (STRETCH_NAMES[str]) return STRETCH_NAMES[str];
85834
- if (!pct) return 5;
85835
- STRETCH_PERCENTS.forEach(function(val, i) {
85836
- if (Math.abs(val - Number(pct[1])) <
85837
- Math.abs(STRETCH_PERCENTS[best - 1] - Number(pct[1]))) {
85838
- best = i + 1;
85839
- }
85840
- });
85841
- return best;
85842
- }
85843
-
85844
- // Family names are compared without spaces, punctuation or case, so that
85845
- // "NYTFranklin" finds "NYT Franklin" and "Helvetica Neue" finds
85846
- // "HelveticaNeue". Font vendors are not consistent about any of the three.
85847
- function normalizeFamilyName(name) {
85848
- return String(name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
85849
- }
85850
-
85851
- // The families a face answers to. The typographic family (name ID 16) is the
85852
- // one a stylesheet means: every weight of NYTFranklin has its own ID 1 family
85853
- // ("NYTFranklin Light", "NYTFranklin Medium") and they are one family only
85854
- // under ID 16. Both are indexed, because plenty of fonts have no ID 16.
85855
- function getFaceFamilies(font) {
85856
- var records = font && font.name && font.name.records || {};
85857
- return [pickName(records.preferredFamily), pickName(records.fontFamily),
85858
- font && font.familyName].filter(Boolean);
85859
- }
85860
-
85861
- function clearFontCache() {
85862
- faceCache = {};
85863
- familyCache = {};
85864
- fullIndex = null;
85865
- fileList = null;
85866
- }
85867
-
85868
- function lookupFace(family, weight, italic, stretch) {
85869
- var names = resolveFamilyNames(family);
85870
- var face = findBestFace(names, weight, italic, stretch);
85871
- // The filename guess can find a family and still miss one of its faces:
85872
- // Segoe UI Semibold lives in seguisb.ttf, which does not begin with the
85873
- // family's name. Anything other than the face that was asked for is worth
85874
- // the full index -- once per session -- to be sure it is the nearest this
85875
- // machine has.
85876
- if (!fullIndex && !faceAnswersRequest(face, weight, italic, stretch)) {
85877
- fullIndex = readFaces(getFontFiles());
85878
- face = findBestFace(names, weight, italic, stretch) || face;
85879
- }
85880
- return face;
85881
- }
85882
-
85883
- function findBestFace(names, weight, italic, stretch) {
85884
- for (var i = 0; i < names.length; i++) {
85885
- var faces = findFamilyFaces(names[i]);
85886
- if (faces && faces.length > 0) return pickFace(faces, weight, italic, stretch);
85887
- }
85888
- return null;
85889
- }
85890
-
85891
- function faceAnswersRequest(face, weight, italic, stretch) {
85892
- return !!face && face.weight == (weight > 0 ? weight : 400) &&
85893
- !!face.italic === !!italic && (face.width || 5) == parseFontStretch(stretch);
85894
- }
85895
-
85896
- // A font-family is a list, and may end in a generic: each name is tried in
85897
- // turn, exactly as a browser would, and a generic stands for the first of its
85898
- // candidates that is installed.
85899
- function resolveFamilyNames(family) {
85900
- var out = [];
85901
- splitFamilyList(family).forEach(function(name) {
85902
- var generic = GENERIC_FAMILIES[name.toLowerCase()];
85903
- if (generic) {
85904
- out = out.concat(generic);
85905
- } else {
85906
- out.push(name);
85907
- }
85908
- });
85909
- return out;
85910
- }
85911
-
85912
- function splitFamilyList(family) {
85913
- return String(family || '').split(',').map(function(name) {
85914
- return name.trim().replace(/^['"]|['"]$/g, '');
85915
- }).filter(Boolean);
85916
- }
85917
-
85918
- // Two passes, because parsing every font on the computer costs the best part
85919
- // of a second and most lookups do not need it: the file holding a family is
85920
- // usually named after it, so files whose name begins with the family's are
85921
- // parsed first. Whether that was good enough is lookupFace()'s decision.
85922
- function findFamilyFaces(family) {
85923
- var key = normalizeFamilyName(family);
85924
- if (!key) return null;
85925
- if (fullIndex) return fullIndex[key] || null;
85926
- if (!(key in familyCache)) {
85927
- familyCache[key] = readFaces(getLikelyFiles(key))[key] || null;
85928
- }
85929
- return familyCache[key];
85930
- }
85931
-
85932
- function getLikelyFiles(key) {
85933
- return getFontFiles().filter(function(file) {
85934
- return normalizeFamilyName(basename(file)).indexOf(key) === 0;
85935
- });
85936
- }
85937
-
85938
- // Faces by normalized family name. A file that cannot be parsed is skipped
85939
- // rather than reported: a font directory can hold anything, and a broken font
85940
- // is not an error in the user's data.
85941
- function readFaces(files) {
85942
- var index = {};
85943
- files.forEach(function(file) {
85944
- getFileFaces(file).forEach(function(face) {
85945
- face.families.forEach(function(name) {
85946
- var key = normalizeFamilyName(name);
85947
- if (!key) return;
85948
- if (!index[key]) index[key] = [];
85949
- index[key].push(face);
85950
- });
85951
- });
85952
- });
85953
- return index;
85954
- }
85955
-
85956
- function getFileFaces(file) {
85957
- var fontkit = getFontkit();
85958
- var font, fonts;
85959
- if (!fontkit) return [];
85960
- try {
85961
- font = fontkit.openSync(file);
85962
- // A collection reports its members in .fonts; a single font is its own.
85963
- fonts = font && font.fonts || [font];
85964
- return fonts.filter(Boolean).map(function(one) {
85965
- return {
85966
- path: file,
85967
- // Named rather than numbered because fontkit takes a name, and because
85968
- // a name survives a font being reinstalled in a different order.
85969
- postscriptName: font.fonts ? one.postscriptName : null,
85970
- families: getFaceFamilies(one),
85971
- weight: getFaceWeight(one),
85972
- width: getFaceWidth(one),
85973
- italic: isItalicFace(one)
85974
- };
85975
- }).filter(function(face) {
85976
- return face.families.length > 0;
85977
- });
85978
- } catch (e) {
85979
- return [];
85980
- }
85981
- }
85982
-
85983
- function getFaceWeight(font) {
85984
- var os2 = font['OS/2'];
85985
- var weight = os2 && os2.usWeightClass;
85986
- return weight > 0 ? weight : 400;
85987
- }
85988
-
85989
- function getFaceWidth(font) {
85990
- var os2 = font['OS/2'];
85991
- var width = os2 && os2.usWidthClass;
85992
- return width >= 1 && width <= 9 ? width : 5;
85993
- }
85994
-
85995
- function isItalicFace(font) {
85996
- var os2 = font['OS/2'];
85997
- if (os2 && os2.fsSelection && typeof os2.fsSelection.italic == 'boolean') {
85998
- return os2.fsSelection.italic;
85999
- }
86000
- return !!font.italicAngle;
86001
- }
86002
-
86003
- // A name record is a string, or an object of translations to pick English out
86004
- // of, depending on the font and the version of fontkit.
86005
- function pickName(rec) {
86006
- if (!rec) return '';
86007
- if (typeof rec == 'string') return rec;
86008
- return rec.en || Object.keys(rec).map(function(k) { return rec[k]; })[0] || '';
86009
- }
86010
-
86011
- function compareScores(a, b) {
86012
- for (var i = 0; i < a.length; i++) {
86013
- if (a[i] != b[i]) return a[i] - b[i];
86014
- }
86015
- return 0;
86016
- }
86017
-
86018
- // Every font file on this computer, listed once. Listing is cheap -- a few
86019
- // hundred directory entries -- next to parsing them, so this is not the part
86020
- // worth avoiding.
86021
- function getFontFiles() {
86022
- var fs = getFs();
86023
- var files = [];
86024
- if (fileList) return fileList;
86025
- if (!fs) return [];
86026
- getFontDirs().forEach(function(dir) {
86027
- var entries;
86028
- try {
86029
- entries = fs.readdirSync(dir, {recursive: true});
86030
- } catch (e) {
86031
- return; // a directory this platform does not have
86032
- }
86033
- entries.forEach(function(entry) {
86034
- var file = dir + '/' + String(entry).split('\\').join('/');
86035
- if (FONT_FILE_RXP.test(file)) files.push(file);
86036
- });
86037
- });
86038
- fileList = files;
86039
- return files;
86040
- }
86041
-
86042
- // Where each platform keeps fonts, or MAPSHAPER_FONT_PATH if it is set.
86043
- //
86044
- // It replaces the platform's directories rather than adding to them, which is
86045
- // what makes a machine's font situation something a caller can state: a
86046
- // container with its fonts somewhere of its own, a build that has to produce
86047
- // the same SVG wherever it runs, a test that needs to know there is nothing to
86048
- // find. Several directories are separated by : or ;.
86049
- function getFontDirs() {
86050
- var home = getHomeDir();
86051
- var platform = typeof process == 'object' && process.platform || '';
86052
- var dirs = getEnvDirs();
86053
- if (dirs.length > 0) return dirs;
86054
- if (platform == 'darwin') {
86055
- dirs = ['/System/Library/Fonts', '/Library/Fonts',
86056
- '/Network/Library/Fonts'];
86057
- if (home) dirs.push(home + '/Library/Fonts');
86058
- } else if (platform == 'win32') {
86059
- dirs = [(getEnv('WINDIR') || 'C:\\Windows') + '/Fonts'];
86060
- if (home) dirs.push(home + '/AppData/Local/Microsoft/Windows/Fonts');
86061
- } else {
86062
- dirs = ['/usr/share/fonts', '/usr/local/share/fonts', '/run/host/fonts'];
86063
- if (home) {
86064
- dirs.push(home + '/.fonts', home + '/.local/share/fonts');
86065
- }
86066
- }
86067
- return dirs;
86068
- }
86069
-
86070
- function getEnvDirs() {
86071
- var val = getEnv('MAPSHAPER_FONT_PATH');
86072
- if (!val) return [];
86073
- return val.split(/[:;]/).filter(Boolean);
86074
- }
86075
-
86076
- function getEnv(name) {
86077
- return typeof process == 'object' && process.env && process.env[name] || '';
86078
- }
86079
-
86080
- function getHomeDir() {
86081
- var os = safeRequire('os');
86082
- try {
86083
- return os && os.homedir() || '';
86084
- } catch (e) {
86085
- return '';
86086
- }
86087
- }
86088
-
86089
- function basename(file) {
86090
- var parts = file.split('/');
86091
- return parts[parts.length - 1].replace(FONT_FILE_RXP, '');
86092
- }
86093
-
86094
- function getFs() {
86095
- return safeRequire('fs');
86096
- }
86097
-
86098
- // Loaded through the require shim and only when a label actually needs
86099
- // measuring, so that the browser bundle -- which is this same file -- never
86100
- // reaches for a module it does not have, and a CLI run that touches no labels
86101
- // never pays for loading it.
86102
- function getFontkit() {
86103
- return safeRequire('fontkit');
86104
- }
86105
-
86106
- function safeRequire(name) {
86107
- if (runningInBrowser()) return null;
86108
- try {
86109
- return require$1(name) || null;
86110
- } catch (e) {
86111
- return null;
86112
- }
86113
- }
86114
-
86115
- var FontLookup = /*#__PURE__*/Object.freeze({
86116
- __proto__: null,
86117
- clearFontCache: clearFontCache,
86118
- findFontFace: findFontFace,
86119
- getFaceFamilies: getFaceFamilies,
86120
- normalizeFamilyName: normalizeFamilyName,
86121
- parseFontStretch: parseFontStretch,
86122
- pickFace: pickFace
86123
- });
86124
-
86125
88165
  // Measuring a label's text outside a browser, from the font files installed on
86126
88166
  // this computer.
86127
88167
  //
@@ -87118,6 +89158,139 @@ ${svg}
87118
89158
  blurRasterLayers: blurRasterLayers
87119
89159
  });
87120
89160
 
89161
+ // A ramp is defined by pins: [{t, color}], where t is a position from 0 (left
89162
+ // end) to 1 (right end). Both ends must be pinned. Pins are stored by position
89163
+ // rather than by tile index so they survive changes in the number of tiles.
89164
+ // See docs/development/color-scheme-panel-design.md
89165
+
89166
+ // Returns an array of n colors. Pinned tiles take their pin's color; the
89167
+ // others are interpolated in OKLCH between the nearest pinned tiles, so that
89168
+ // lightness, chroma and hue each change by equal steps from tile to tile.
89169
+ // opts.vibrance: a midtone chroma boost between each
89170
+ // pair of pinned tiles (see getOklchInterpolator()).
89171
+ function resolveRamp(pins, n, opts) {
89172
+ return resolveRampTiles(pins, n, opts).map(function(tile) {
89173
+ return tile.color;
89174
+ });
89175
+ }
89176
+
89177
+ // Like resolveRamp(), but returns a {color, pinned, adjusted, l, c, h, ideal}
89178
+ // object for each tile. Tiles that had to be fitted to the sRGB gamut are
89179
+ // adjusted, and ideal has the OKLCH values they were fitted from.
89180
+ function resolveRampTiles(pins, n, opts) {
89181
+ var slots = getPinnedSlots(pins, n);
89182
+ var tiles = [];
89183
+ var left = 0, right, interpolate, tile;
89184
+ for (var i=0; i<n; i++) {
89185
+ if (slots[i] > -1) {
89186
+ // vibrance raises a pinned color's chroma only as far as the gamut has
89187
+ // room at its own lightness; it is adjusted if that cut the vibrance short
89188
+ tile = getVibrantTile(pins[slots[i]].color, opts && opts.vibrance);
89189
+ tile.pinned = true;
89190
+ tiles.push(tile);
89191
+ left = i;
89192
+ continue;
89193
+ }
89194
+ if (!interpolate || right < i) {
89195
+ right = i + 1;
89196
+ while (slots[right] == -1) right++;
89197
+ interpolate = getOklchInterpolator(pins[slots[left]].color, pins[slots[right]].color,
89198
+ Object.assign({}, opts, {steps: right - left}));
89199
+ }
89200
+ tile = interpolate((i - left) / (right - left));
89201
+ tile.pinned = false;
89202
+ tiles.push(tile);
89203
+ }
89204
+ return tiles;
89205
+ }
89206
+
89207
+ // Returns an array of n tile slots, each containing the index of the pin
89208
+ // that sets the tile's color, or -1 if the tile is interpolated.
89209
+ // When several pins snap to the same tile, an end pin wins, otherwise the pin
89210
+ // closest to the tile's own position.
89211
+ function getPinnedSlots(pins, n) {
89212
+ var slots = [], dists = [];
89213
+ validateRamp(pins, n);
89214
+ for (var i=0; i<n; i++) {
89215
+ slots.push(-1);
89216
+ dists.push(Infinity);
89217
+ }
89218
+ pins.forEach(function(pin, pinId) {
89219
+ var k = pin.t * (n - 1);
89220
+ var slot = Math.round(k);
89221
+ var dist = isEndPin(pin) ? -1 : Math.abs(k - slot);
89222
+ if (dist < dists[slot]) {
89223
+ slots[slot] = pinId;
89224
+ dists[slot] = dist;
89225
+ }
89226
+ });
89227
+ return slots;
89228
+ }
89229
+
89230
+ // Returns a new pin array with tile i of an n-tile ramp pinned to color,
89231
+ // replacing any pins that snap to the same tile.
89232
+ function setRampPin(pins, n, i, color) {
89233
+ validateTileIndex(i, n);
89234
+ if (!parseColor(color)) stop$1('Unsupported color:', color);
89235
+ return removePinsAtSlot(pins, n, i)
89236
+ .concat({t: getTilePosition(i, n), color: color})
89237
+ .sort(function(a, b) { return a.t - b.t; });
89238
+ }
89239
+
89240
+ // Returns a new pin array with tile i unpinned. End tiles stay pinned.
89241
+ function clearRampPin(pins, n, i) {
89242
+ validateTileIndex(i, n);
89243
+ if (i === 0 || i == n - 1) return pins.concat();
89244
+ return removePinsAtSlot(pins, n, i);
89245
+ }
89246
+
89247
+ function removePinsAtSlot(pins, n, i) {
89248
+ validateRamp(pins, n);
89249
+ return pins.filter(function(pin) {
89250
+ return Math.round(pin.t * (n - 1)) != i;
89251
+ });
89252
+ }
89253
+
89254
+ function getTilePosition(i, n) {
89255
+ return i == n - 1 ? 1 : i / (n - 1);
89256
+ }
89257
+
89258
+ function isEndPin(pin) {
89259
+ return pin.t === 0 || pin.t === 1;
89260
+ }
89261
+
89262
+ function validateTileIndex(i, n) {
89263
+ if (!(i >= 0 && i < n && Math.floor(i) === i)) {
89264
+ stop$1('Invalid ramp tile index:', i);
89265
+ }
89266
+ }
89267
+
89268
+ function validateRamp(pins, n) {
89269
+ if (!(n >= 2 && Math.floor(n) === n)) {
89270
+ stop$1('Invalid number of ramp colors:', n);
89271
+ }
89272
+ if (!Array.isArray(pins)) {
89273
+ stop$1('Expected an array of ramp pins');
89274
+ }
89275
+ pins.forEach(function(pin) {
89276
+ if (!(pin.t >= 0 && pin.t <= 1)) stop$1('Invalid ramp pin position:', pin.t);
89277
+ if (!parseColor(pin.color)) stop$1('Unsupported color:', pin.color);
89278
+ });
89279
+ if (!pins.some(function(pin) { return pin.t === 0; }) ||
89280
+ !pins.some(function(pin) { return pin.t === 1; })) {
89281
+ stop$1('A ramp must have pins at both ends');
89282
+ }
89283
+ }
89284
+
89285
+ var ColorRamps = /*#__PURE__*/Object.freeze({
89286
+ __proto__: null,
89287
+ clearRampPin: clearRampPin,
89288
+ getPinnedSlots: getPinnedSlots,
89289
+ resolveRamp: resolveRamp,
89290
+ resolveRampTiles: resolveRampTiles,
89291
+ setRampPin: setRampPin
89292
+ });
89293
+
87121
89294
  function getPolygonRewinder(nodes) {
87122
89295
  var splitter = getSelfIntersectionSplitter(nodes);
87123
89296
  return rewindPolygon;
@@ -87958,10 +90131,10 @@ ${svg}
87958
90131
  // Vertices never move once they are placed.
87959
90132
  //
87960
90133
  // The trace of the pointer samples is densified to even spacing and smoothed
87961
- // with the Gaussian kernel that -smooth uses, as a plain weighted average in
87962
- // arc length. (-smooth adds a quadratic correction that keeps bends from being
87963
- // flattened, but it lets through far more of the jitter of a slow stroke.) A
87964
- // smoothed point depends only on the trace within a window around it, so it
90134
+ // with -smooth's Gaussian-weighted quadratic fit, in arc length. The quadratic
90135
+ // keeps small bends from being flattened the way a plain weighted average
90136
+ // flattens them, at the cost of letting a little more of the jitter of a slow
90137
+ // stroke through. A smoothed point depends only on the trace within a window around it, so it
87965
90138
  // is final as soon as the trace extends a window radius beyond it. At the
87966
90139
  // start of the stroke the trace is extended by an odd reflection about its
87967
90140
  // first point, which keeps the smoothed curve starting exactly there, and the
@@ -87976,14 +90149,15 @@ ${svg}
87976
90149
  // Distances are in pixels. For samples in other units, the pixelSize option
87977
90150
  // gives the size of a pixel in those units.
87978
90151
 
87979
- // Standard deviation of the smoothing kernel, in arc length. Below about 5px,
87980
- // the jitter of a slow stroke comes through; larger values round off bends a
87981
- // few pixels across.
90152
+ // Standard deviation of the smoothing kernel, in arc length. Smaller values
90153
+ // follow the hand more closely, which gives a feeling of control; larger ones
90154
+ // are smoother but feel less precise and place vertices further behind the
90155
+ // pointer.
87982
90156
  var SIGMA = 8;
87983
90157
 
87984
90158
  // Half-width of the smoothing window, in multiples of SIGMA. This is also how
87985
90159
  // far the pointer has to be ahead of a smoothed point for it to be final.
87986
- var WINDOW_RADIUS = 3;
90160
+ var WINDOW_RADIUS = 4;
87987
90161
 
87988
90162
  // Spacing of the densified trace, and of the smoothed points that are tested
87989
90163
  // as vertices. Small enough that a bend of radius SIGMA turns by only a few
@@ -88004,10 +90178,16 @@ ${svg}
88004
90178
  var PREVIEW_SPACING = 2;
88005
90179
  var PREVIEW_SIGMA = 2;
88006
90180
 
90181
+ // Weight of -smooth's quadratic correction, which keeps bends from being
90182
+ // flattened: 0 is the plain weighted average, 1 the fully corrected fit.
90183
+ // At 1, small bends keep most of their amplitude.
90184
+ var GAIN = 1;
90185
+
88007
90186
  // start: [x, y] first sample
88008
- // opts: (optional) {sigma, previewSigma, bendAngle (degrees), maxDeviation, pixelSize}
90187
+ // opts: (optional) {sigma, previewSigma, gain, bendAngle (degrees), maxDeviation, pixelSize}
88009
90188
  function GaussianStrokeFitter(start, opts) {
88010
90189
  var px = opts && opts.pixelSize > 0 ? opts.pixelSize : 1;
90190
+ var gain = opts && opts.gain >= 0 ? opts.gain : GAIN;
88011
90191
  var sigma = getOpt(opts, 'sigma', SIGMA) * px;
88012
90192
  var radius = sigma * WINDOW_RADIUS;
88013
90193
  var previewSigma = Math.min(getOpt(opts, 'previewSigma', PREVIEW_SIGMA) * px, sigma);
@@ -88111,7 +90291,7 @@ ${svg}
88111
90291
  wy.push(2 * end[1] - yy[i]);
88112
90292
  }
88113
90293
  }
88114
- return smoothPoint(wt, [wx, wy], 0, wt.length, t, 'gaussian', s || sigma, r, 0);
90294
+ return smoothPoint(wt, [wx, wy], 0, wt.length, t, 'gaussian', s || sigma, r, gain);
88115
90295
  }
88116
90296
 
88117
90297
  // first index of the trace with arc length > @t
@@ -88270,12 +90450,17 @@ ${svg}
88270
90450
  ClipErase,
88271
90451
  ClipPoints,
88272
90452
  ColorUtils,
90453
+ ColorRamps,
90454
+ ColorSchemes,
90455
+ Oklab,
88273
90456
  Colorizer,
88274
90457
  CustomProjections,
88275
90458
  Dymaxion,
88276
90459
  ButterflyProjections,
88277
90460
  Narukawa2022,
88278
90461
  DataAggregation,
90462
+ Diverging,
90463
+ {getSequentialBreaks, getInterpolatedValueGetter},
88279
90464
  DatasetUtils,
88280
90465
  DataUtils,
88281
90466
  DbfImport,