mapshaper 0.7.72 → 0.7.75

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/www/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;
54244
- }
54245
-
54246
- function getPctOffsets(arg) {
54247
- return adjustOffsetsArg(arg).map(str => {
54248
- return str.includes('%') ? parsePercent(str) : 0;
54249
- });
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;
54250
55598
  }
54251
55599
 
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
 
@@ -69981,7 +71333,7 @@ ${svg}
69981
71333
  return new Rgb(o.r, o.g, o.b, o.opacity);
69982
71334
  }
69983
71335
 
69984
- function rgb$1(r, g, b, opacity) {
71336
+ function rgb(r, g, b, opacity) {
69985
71337
  return arguments.length === 1 ? rgbConvert(r) : new Rgb(r, g, b, opacity == null ? 1 : opacity);
69986
71338
  }
69987
71339
 
@@ -69992,7 +71344,7 @@ ${svg}
69992
71344
  this.opacity = +opacity;
69993
71345
  }
69994
71346
 
69995
- define(Rgb, rgb$1, extend(Color, {
71347
+ define(Rgb, rgb, extend(Color, {
69996
71348
  brighter(k) {
69997
71349
  k = k == null ? brighter : Math.pow(brighter, k);
69998
71350
  return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);
@@ -70254,11 +71606,11 @@ ${svg}
70254
71606
  return d ? linear(a, d) : constant(isNaN(a) ? b : a);
70255
71607
  }
70256
71608
 
70257
- var rgb = (function rgbGamma(y) {
71609
+ var interpolateRgb = (function rgbGamma(y) {
70258
71610
  var color = gamma(y);
70259
71611
 
70260
- function rgb(start, end) {
70261
- var r = color((start = rgb$1(start)).r, (end = rgb$1(end)).r),
71612
+ function rgb$1(start, end) {
71613
+ var r = color((start = rgb(start)).r, (end = rgb(end)).r),
70262
71614
  g = color(start.g, end.g),
70263
71615
  b = color(start.b, end.b),
70264
71616
  opacity = nogamma(start.opacity, end.opacity);
@@ -70271,9 +71623,9 @@ ${svg}
70271
71623
  };
70272
71624
  }
70273
71625
 
70274
- rgb.gamma = rgbGamma;
71626
+ rgb$1.gamma = rgbGamma;
70275
71627
 
70276
- return rgb;
71628
+ return rgb$1;
70277
71629
  })(1);
70278
71630
 
70279
71631
  function rgbSpline(spline) {
@@ -70284,7 +71636,7 @@ ${svg}
70284
71636
  b = new Array(n),
70285
71637
  i, color;
70286
71638
  for (i = 0; i < n; ++i) {
70287
- color = rgb$1(colors[i]);
71639
+ color = rgb(colors[i]);
70288
71640
  r[i] = color.r || 0;
70289
71641
  g[i] = color.g || 0;
70290
71642
  b[i] = color.b || 0;
@@ -70437,8 +71789,8 @@ ${svg}
70437
71789
  var t = typeof b, c;
70438
71790
  return b == null || t === "boolean" ? constant(b)
70439
71791
  : (t === "number" ? number
70440
- : t === "string" ? ((c = color(b)) ? (b = c, rgb) : string)
70441
- : b instanceof color ? rgb
71792
+ : t === "string" ? ((c = color(b)) ? (b = c, interpolateRgb) : string)
71793
+ : b instanceof color ? interpolateRgb
70442
71794
  : b instanceof Date ? date
70443
71795
  : isNumberArray(b) ? numberArray
70444
71796
  : Array.isArray(b) ? genericArray
@@ -70473,6 +71825,377 @@ ${svg}
70473
71825
  cubehelix$1(hue);
70474
71826
  var cubehelixLong = cubehelix$1(nogamma);
70475
71827
 
71828
+ function piecewise(interpolate, values) {
71829
+ if (values === undefined) values = interpolate, interpolate = d3_interpolate;
71830
+ var i = 0, n = values.length - 1, v = values[0], I = new Array(n < 0 ? 0 : n);
71831
+ while (i < n) I[i] = interpolate(v, v = values[++i]);
71832
+ return function(t) {
71833
+ var i = Math.max(0, Math.min(n - 1, Math.floor(t *= n)));
71834
+ return I[i](t - i);
71835
+ };
71836
+ }
71837
+
71838
+ // OKLab conversion (Björn Ottosson, https://bottosson.github.io/posts/oklab/)
71839
+ // RGB objects use 0-255 channels, as returned by parseColor().
71840
+
71841
+ function rgbToOklab(rgb) {
71842
+ var r = toLinear(rgb.r / 255),
71843
+ g = toLinear(rgb.g / 255),
71844
+ b = toLinear(rgb.b / 255);
71845
+ var l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b),
71846
+ m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b),
71847
+ s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
71848
+ return {
71849
+ l: 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
71850
+ a: 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
71851
+ b: 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s
71852
+ };
71853
+ }
71854
+
71855
+ // Colors outside the sRGB gamut are clipped per channel.
71856
+ function oklabToRgb(lab) {
71857
+ var rgb = oklabToLinearRgb(lab);
71858
+ return {
71859
+ r: fromLinear(rgb.r) * 255,
71860
+ g: fromLinear(rgb.g) * 255,
71861
+ b: fromLinear(rgb.b) * 255
71862
+ };
71863
+ }
71864
+
71865
+ // OKLCH is OKLab in polar form: lightness, chroma (colorfulness) and hue
71866
+ // angle in degrees.
71867
+ function oklabToOklch(lab) {
71868
+ var h = Math.atan2(lab.b, lab.a) * 180 / Math.PI;
71869
+ return {l: lab.l, c: Math.sqrt(lab.a * lab.a + lab.b * lab.b), h: h < 0 ? h + 360 : h};
71870
+ }
71871
+
71872
+ function oklchToOklab(lch) {
71873
+ var rad = lch.h * Math.PI / 180;
71874
+ return {l: lch.l, a: lch.c * Math.cos(rad), b: lch.c * Math.sin(rad)};
71875
+ }
71876
+
71877
+ // Below this chroma a color is treated as a gray, whose hue means nothing.
71878
+ var ACHROMATIC = 0.002;
71879
+
71880
+ // Lightness may move this far (OKLCH L) to make room for chroma ...
71881
+ var MAX_LIGHTNESS_SHIFT = 0.05;
71882
+ // ... and no more than this share of the lightness step between tiles, so
71883
+ // that steps in lightness stay close to even.
71884
+ var LIGHTNESS_SHIFT_SHARE = 0.2;
71885
+ // Lightness differences count this many times as much as chroma differences
71886
+ // when fitting a color to the gamut (see fitLightness()).
71887
+ var LIGHTNESS_WEIGHT = 2;
71888
+ // Shifts and chroma losses smaller than this are not reported.
71889
+ var DEVIATION = 0.003;
71890
+
71891
+ // Where the light end of a ramp gains less chroma from vibrance than the
71892
+ // color next to it (the gamut has little room near white), the step between
71893
+ // them looks larger than the others. So that color's lightness moves toward
71894
+ // the light end by this many times the difference in chroma gained. Set by
71895
+ // eye; see getSecondLightness().
71896
+ var VIBRANCE_LIGHTNESS = 0.2;
71897
+
71898
+ // The OKLCH chroma that -classify's vibrance=1 adds, and the top of the color
71899
+ // palette panel's slider. More than this fills the gamut at most lightnesses,
71900
+ // leaving the midtones of a ramp all as vivid as they can be.
71901
+ var MAX_VIBRANCE_CHROMA = 0.09;
71902
+
71903
+ // vibrance=: 0-1, scaled to OKLCH chroma
71904
+ function getVibranceChroma(vibrance) {
71905
+ return vibrance * MAX_VIBRANCE_CHROMA;
71906
+ }
71907
+
71908
+ // A user's color with vibrance added to its chroma, as far as the gamut has
71909
+ // room at its lightness and hue. Grays are left alone, having no hue.
71910
+ function getVibrantColor(color, vibrance) {
71911
+ return getVibrantTile(color, vibrance).color;
71912
+ }
71913
+
71914
+ // Like getVibrantColor(), but returns {color, l, c, h, ideal, adjusted}, as
71915
+ // the function from getOklchInterpolator() does. A color is adjusted if the
71916
+ // gamut didn't have room for all of the vibrance.
71917
+ function getVibrantTile(color, vibrance) {
71918
+ var rgb = parseColorOrStop(color);
71919
+ var lch = oklabToOklch(rgbToOklab(rgb));
71920
+ var ideal = {l: lch.l, c: lch.c, h: lch.h};
71921
+ var c = lch.c, out;
71922
+ if (vibrance > 0 && lch.c >= ACHROMATIC) {
71923
+ ideal.c += vibrance;
71924
+ c = getVibrantChroma(lch, vibrance);
71925
+ out = oklchToRgb({l: lch.l, c: c, h: lch.h});
71926
+ out.a = rgb.a;
71927
+ }
71928
+ return {
71929
+ color: formatColor(out || rgb),
71930
+ l: lch.l, c: c, h: lch.h,
71931
+ ideal: ideal,
71932
+ adjusted: ideal.c - c > DEVIATION
71933
+ };
71934
+ }
71935
+
71936
+ function getVibrantChroma(lch, vibrance) {
71937
+ if (!(vibrance > 0) || lch.c < ACHROMATIC) return lch.c;
71938
+ return Math.max(lch.c, Math.min(lch.c + vibrance, getMaxChroma(lch.l, lch.h)));
71939
+ }
71940
+
71941
+ // Returns a function that maps t in [0, 1] to a color string. Lightness,
71942
+ // chroma and hue are each interpolated on their own, so that equal steps in
71943
+ // t are equal steps in each of them; hue takes the shorter way around the
71944
+ // color wheel, unless opts.hue is 'longer'. A gray end takes the hue of the other end, so that a ramp from
71945
+ // white to blue is blue all the way rather than passing through other hues.
71946
+ // See getOklchInterpolator() for options, and for colors outside the sRGB
71947
+ // gamut.
71948
+ function interpolateOklch(color1, color2, opts) {
71949
+ var interpolate = getOklchInterpolator(color1, color2, opts);
71950
+ return function(t) {
71951
+ return interpolate(t).color;
71952
+ };
71953
+ }
71954
+
71955
+ // Like interpolateOklch(), but the function returns {color, l, c, h, ideal,
71956
+ // adjusted}: the color, its OKLCH values, the ideal OKLCH values it was
71957
+ // fitted from ({l, c, h}), and whether fitting changed it noticeably.
71958
+ //
71959
+ // opts.vibrance (OKLCH chroma, default 0) raises the chroma of the midtones.
71960
+ // The sRGB gamut holds little chroma near black and white, so a ramp between
71961
+ // a dark and a light color has dull midtones when its chroma goes in equal
71962
+ // steps. The amount is the same at every hue. Ramps between two grays get no
71963
+ // boost. Vibrance is added to the chroma of every color of the straight
71964
+ // interpolation. The two end colors get it as far as the gamut has room at
71965
+ // their lightness, without being marked as adjusted (see getVibrantColor());
71966
+ // gray ends are left alone. In a classed ramp, the color next to the lighter
71967
+ // end moves toward it in lightness if it gained more chroma than that end,
71968
+ // and the others are evenly spaced from it to the darker end (see
71969
+ // getSecondLightness()).
71970
+ //
71971
+ // opts.steps: the number of tiles from one color to the other, if the ramp
71972
+ // is classed, which limits how far lightness may move (see below).
71973
+ //
71974
+ // opts.hue: 'shorter' (default) or 'longer', the way around the color wheel
71975
+ // that hue takes. A gray end has no hue to go around from, so a ramp with
71976
+ // one always takes the other end's hue.
71977
+ //
71978
+ // A color outside the sRGB gamut keeps its hue. Its lightness may move, up
71979
+ // to MAX_LIGHTNESS_SHIFT (and a share of the lightness step between tiles),
71980
+ // to where the gamut has room for more of its chroma, if the chroma gained
71981
+ // is worth the lightness lost (see fitLightness()). Chroma is then reduced
71982
+ // to fit.
71983
+ function getOklchInterpolator(color1, color2, opts) {
71984
+ var rgb1 = parseColorOrStop(color1),
71985
+ rgb2 = parseColorOrStop(color2);
71986
+ var lch1 = oklabToOklch(rgbToOklab(rgb1)),
71987
+ lch2 = oklabToOklch(rgbToOklab(rgb2));
71988
+ var vibrance = opts && opts.vibrance || 0;
71989
+ var steps = opts && opts.steps;
71990
+ var longHue = opts && opts.hue == 'longer' &&
71991
+ lch1.c >= ACHROMATIC && lch2.c >= ACHROMATIC;
71992
+ var maxShift = MAX_LIGHTNESS_SHIFT;
71993
+ var dh, lSecond, vivid1, vivid2;
71994
+ if (steps > 0) {
71995
+ maxShift = Math.min(maxShift, LIGHTNESS_SHIFT_SHARE * Math.abs(lch2.l - lch1.l) / steps);
71996
+ }
71997
+ if (lch1.c < ACHROMATIC && lch2.c < ACHROMATIC) vibrance = 0;
71998
+ // the ends' chroma with vibrance, before a gray end takes the other's hue
71999
+ vivid1 = getVibrantChroma(lch1, vibrance);
72000
+ vivid2 = getVibrantChroma(lch2, vibrance);
72001
+ if (lch1.c < ACHROMATIC && lch2.c >= ACHROMATIC) lch1.h = lch2.h;
72002
+ if (lch2.c < ACHROMATIC && lch1.c >= ACHROMATIC) lch2.h = lch1.h;
72003
+ dh = lch2.h - lch1.h;
72004
+ if (dh > 180) dh -= 360;
72005
+ if (dh < -180) dh += 360;
72006
+ if (longHue) dh = dh > 0 ? dh - 360 : dh + 360;
72007
+ if (vibrance > 0 && steps >= 2) {
72008
+ lSecond = getSecondLightness(lch1, lch2, dh, vibrance, steps, vivid1, vivid2);
72009
+ }
72010
+ return function(t) {
72011
+ var h = lch1.h + dh * t;
72012
+ var ideal = {
72013
+ l: lch1.l + (lch2.l - lch1.l) * t,
72014
+ c: t <= 0 ? vivid1 : t >= 1 ? vivid2 : lch1.c + (lch2.c - lch1.c) * t,
72015
+ h: normalizeHue(h)
72016
+ };
72017
+ var l = ideal.l, c = ideal.c, rgb;
72018
+ if (t > 0 && t < 1) {
72019
+ if (lSecond !== undefined) {
72020
+ ideal.l = getLightnessFromSecond(t, lch1.l, lch2.l, lSecond, steps);
72021
+ }
72022
+ ideal.c += vibrance;
72023
+ l = fitLightness(ideal.l, ideal.c, ideal.h, maxShift);
72024
+ c = Math.min(ideal.c, getMaxChroma(l, ideal.h));
72025
+ }
72026
+ rgb = oklchToRgb({l: l, c: c, h: ideal.h});
72027
+ rgb.a = rgb1.a + (rgb2.a - rgb1.a) * t;
72028
+ return {
72029
+ color: formatColor(rgb),
72030
+ l: l, c: c, h: ideal.h,
72031
+ ideal: ideal,
72032
+ adjusted: Math.abs(l - ideal.l) > DEVIATION || ideal.c - c > DEVIATION
72033
+ };
72034
+ };
72035
+ }
72036
+
72037
+ // The lightness of the color next to the lighter end of a classed ramp from
72038
+ // lch1 to lch2, boosted by vibrance. Each gains chroma as far as the gamut
72039
+ // has room at its unshifted lightness; where the color gains more than the
72040
+ // end, it moves toward the end by VIBRANCE_LIGHTNESS times the difference,
72041
+ // stopping at the end's lightness.
72042
+ function getSecondLightness(lch1, lch2, dh, vibrance, steps, vivid1, vivid2) {
72043
+ var lightFirst = lch1.l >= lch2.l;
72044
+ var light = lightFirst ? lch1 : lch2;
72045
+ var endGain = (lightFirst ? vivid1 : vivid2) - light.c;
72046
+ var t = lightFirst ? 1 / steps : 1 - 1 / steps;
72047
+ var l = lch1.l + (lch2.l - lch1.l) * t;
72048
+ var c = lch1.c + (lch2.c - lch1.c) * t;
72049
+ var h = normalizeHue(lch1.h + dh * t);
72050
+ var gain = Math.max(0, Math.min(c + vibrance, getMaxChroma(l, h)) - c);
72051
+ var shift = Math.min(VIBRANCE_LIGHTNESS * Math.max(0, gain - endGain), Math.abs(light.l - l));
72052
+ return light.l > l ? l + shift : l - shift;
72053
+ }
72054
+
72055
+ // The lightness at t of a classed ramp from lightness l1 to l2 whose color
72056
+ // next to the lighter end has lightness lSecond; the others are evenly
72057
+ // spaced from it to the darker end.
72058
+ function getLightnessFromSecond(t, l1, l2, lSecond, steps) {
72059
+ var lightFirst = l1 >= l2;
72060
+ var lDark = lightFirst ? l2 : l1;
72061
+ var u = lightFirst ? t : 1 - t; // distance from the light end
72062
+ var uSecond = 1 / steps;
72063
+ if (u <= uSecond) return lSecond;
72064
+ return lSecond + (lDark - lSecond) * (u - uSecond) / (1 - uSecond);
72065
+ }
72066
+
72067
+ function normalizeHue(h) {
72068
+ return h < 0 ? h + 360 : h >= 360 ? h - 360 : h;
72069
+ }
72070
+
72071
+ // Returns the lightness within maxShift of l that brings a color with ideal
72072
+ // lightness l and chroma c (at hue h) closest to its ideal, by a color
72073
+ // difference that weights lightness LIGHTNESS_WEIGHT times as heavily as
72074
+ // chroma: viewers notice lightness differences more, and even lightness
72075
+ // steps matter more than even chroma steps.
72076
+ function fitLightness(l, c, h, maxShift) {
72077
+ var n = 20;
72078
+ var lo = clamp$1(l - maxShift, 0, 1), hi = clamp$1(l + maxShift, 0, 1);
72079
+ var best = l, bestCost = getFitCost(l, l, c, h);
72080
+ var step = (hi - lo) / n, l2, cost, i, a, b, m1, m2;
72081
+ if (bestCost === 0 || !(maxShift > 0)) return l;
72082
+ for (i=0; i<=n; i++) {
72083
+ l2 = lo + step * i;
72084
+ cost = getFitCost(l2, l, c, h);
72085
+ if (cost < bestCost) {
72086
+ best = l2;
72087
+ bestCost = cost;
72088
+ }
72089
+ }
72090
+ // refine between the samples on either side of the best one
72091
+ a = Math.max(lo, best - step);
72092
+ b = Math.min(hi, best + step);
72093
+ for (i=0; i<20; i++) {
72094
+ m1 = a + (b - a) / 3;
72095
+ m2 = b - (b - a) / 3;
72096
+ if (getFitCost(m1, l, c, h) < getFitCost(m2, l, c, h)) {
72097
+ b = m2;
72098
+ } else {
72099
+ a = m1;
72100
+ }
72101
+ }
72102
+ l2 = (a + b) / 2;
72103
+ return getFitCost(l2, l, c, h) < bestCost ? l2 : best;
72104
+ }
72105
+
72106
+ function getFitCost(l2, l, c, h) {
72107
+ var dl = (l2 - l) * LIGHTNESS_WEIGHT;
72108
+ var dc = Math.max(0, c - getMaxChroma(l2, h));
72109
+ return dl * dl + dc * dc;
72110
+ }
72111
+
72112
+ function clamp$1(val, min, max) {
72113
+ return val < min ? min : val > max ? max : val;
72114
+ }
72115
+
72116
+ // The most chroma the sRGB gamut has at a lightness and hue
72117
+ function getMaxChroma(l, h) {
72118
+ return findMaxChroma(l, h, 0.4);
72119
+ }
72120
+
72121
+ // Converts an OKLCH color to sRGB (0-255 channels). A color outside the sRGB
72122
+ // gamut keeps its hue, then its lightness, and gives up chroma: chroma is
72123
+ // reduced to the most the gamut has at that hue and lightness.
72124
+ function oklchToRgb(lch) {
72125
+ var lab = oklchToOklab(lch);
72126
+ if (!oklabIsInGamut(lab)) {
72127
+ lab = oklchToOklab({l: lch.l, c: findMaxChroma(lch.l, lch.h, lch.c), h: lch.h});
72128
+ }
72129
+ return oklabToRgb(lab);
72130
+ }
72131
+
72132
+ function findMaxChroma(l, h, maxChroma) {
72133
+ var lo = 0, hi = maxChroma, mid;
72134
+ for (var i=0; i<24; i++) {
72135
+ mid = (lo + hi) / 2;
72136
+ if (oklabIsInGamut(oklchToOklab({l: l, c: mid, h: h}))) {
72137
+ lo = mid;
72138
+ } else {
72139
+ hi = mid;
72140
+ }
72141
+ }
72142
+ return lo;
72143
+ }
72144
+
72145
+ function oklabIsInGamut(lab) {
72146
+ var rgb = oklabToLinearRgb(lab);
72147
+ var e = 1e-6;
72148
+ return rgb.r >= -e && rgb.r <= 1 + e && rgb.g >= -e && rgb.g <= 1 + e &&
72149
+ rgb.b >= -e && rgb.b <= 1 + e;
72150
+ }
72151
+
72152
+ function oklabToLinearRgb(lab) {
72153
+ var l = cube(lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b),
72154
+ m = cube(lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b),
72155
+ s = cube(lab.l - 0.0894841775 * lab.a - 1.2914855480 * lab.b);
72156
+ return {
72157
+ r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
72158
+ g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
72159
+ b: -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
72160
+ };
72161
+ }
72162
+
72163
+ function parseColorOrStop(color) {
72164
+ var rgb = parseColor(color);
72165
+ if (!rgb) stop$1('Unsupported color:', color);
72166
+ return rgb;
72167
+ }
72168
+
72169
+ function toLinear(c) {
72170
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
72171
+ }
72172
+
72173
+ function fromLinear(c) {
72174
+ c = c < 0 ? 0 : c > 1 ? 1 : c;
72175
+ return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
72176
+ }
72177
+
72178
+ function cube(x) {
72179
+ return x * x * x;
72180
+ }
72181
+
72182
+ var Oklab = /*#__PURE__*/Object.freeze({
72183
+ __proto__: null,
72184
+ MAX_VIBRANCE_CHROMA: MAX_VIBRANCE_CHROMA,
72185
+ getMaxChroma: getMaxChroma,
72186
+ getOklchInterpolator: getOklchInterpolator,
72187
+ getVibranceChroma: getVibranceChroma,
72188
+ getVibrantColor: getVibrantColor,
72189
+ getVibrantTile: getVibrantTile,
72190
+ interpolateOklch: interpolateOklch,
72191
+ oklabIsInGamut: oklabIsInGamut,
72192
+ oklabToOklch: oklabToOklch,
72193
+ oklabToRgb: oklabToRgb,
72194
+ oklchToOklab: oklchToOklab,
72195
+ oklchToRgb: oklchToRgb,
72196
+ rgbToOklab: rgbToOklab
72197
+ });
72198
+
70476
72199
  // TODO: support three or more stops
70477
72200
  function getGradientFunction(stops) {
70478
72201
  var min = stops[0] / 100,
@@ -70488,8 +72211,41 @@ ${svg}
70488
72211
  };
70489
72212
  }
70490
72213
 
70491
- function getStoppedValues(values, stops) {
70492
- var interpolate = getInterpolatedValueGetter(values, null);
72214
+ // Returns the interpolation options of -classify, checked, with
72215
+ // interpolation=oklch filled in when vibrance= is given without a method.
72216
+ // vibrance= (0-1) is returned as OKLCH chroma, as the interpolators take it.
72217
+ function getInterpolationOptions(opts) {
72218
+ var method = opts.interpolation;
72219
+ var vibrance = opts.vibrance;
72220
+ if (method && method != 'rgb' && method != 'oklch') {
72221
+ stop$1('Unsupported interpolation method:', method, '(expected rgb or oklch)');
72222
+ }
72223
+ if (vibrance === undefined) {
72224
+ return {interpolation: method};
72225
+ }
72226
+ if (method && method != 'oklch') {
72227
+ stop$1('vibrance= requires interpolation=oklch');
72228
+ }
72229
+ if (!(vibrance >= 0 && vibrance <= 1)) {
72230
+ stop$1('vibrance= takes a value from 0 to 1');
72231
+ }
72232
+ return {interpolation: 'oklch', vibrance: getVibranceChroma(vibrance)};
72233
+ }
72234
+
72235
+ // opts.interpolation: 'rgb' (default) or 'oklch'; oklch applies only to pairs
72236
+ // of colors. opts.vibrance goes with oklch (see
72237
+ // interpolateOklch()).
72238
+ function getPairInterpolator(a, b, opts) {
72239
+ var method = opts && opts.interpolation;
72240
+ if (method == 'oklch' && parseColor(a) && parseColor(b)) {
72241
+ return interpolateOklch(a, b, opts);
72242
+ }
72243
+ return d3_interpolate(a, b);
72244
+ }
72245
+
72246
+ // opts: interpolation options (see getPairInterpolator())
72247
+ function getStoppedValues(values, stops, opts) {
72248
+ var interpolate = getInterpolatedValueGetter(values, null, opts);
70493
72249
  var n = values.length;
70494
72250
  var fstop = getGradientFunction(stops);
70495
72251
  var values2 = [];
@@ -70503,11 +72259,11 @@ ${svg}
70503
72259
  }
70504
72260
 
70505
72261
  // convert a continuous index ([0, n-1], -1) to a corresponding interpolated value
70506
- function getInterpolatedValueGetter(values, nullValue) {
72262
+ function getInterpolatedValueGetter(values, nullValue, opts) {
70507
72263
  var interpolators = [];
70508
72264
  var tmax = values.length - 1;
70509
72265
  for (var i=1; i<values.length; i++) {
70510
- interpolators.push(d3_interpolate(values[i-1], values[i]));
72266
+ interpolators.push(getPairInterpolator(values[i-1], values[i], opts));
70511
72267
  }
70512
72268
  return function(t) {
70513
72269
  if (t == -1) return nullValue;
@@ -70523,26 +72279,38 @@ ${svg}
70523
72279
  // return an array of n values
70524
72280
  // assumes that values can be interpolated by d3-interpolate
70525
72281
  // (colors and numbers should work)
70526
- function interpolateValuesToClasses(values, n, stops) {
70527
- if (values.length == n && !stops) return values;
72282
+ function interpolateValuesToClasses(values, n, stops, opts) {
72283
+ if (values.length == n && !stops) return getVibrantValues(values, opts);
70528
72284
  var numPairs = values.length - 1;
70529
- var output = [values[0]];
72285
+ var pairOpts = Object.assign({}, opts, {steps: (n - 1) / numPairs});
72286
+ var output = [getVibrantValues(values, opts)[0]];
70530
72287
  var k, j, t, intVal;
70531
72288
  for (var i=1; i<n-1; i++) {
70532
72289
  k = i / (n-1) * numPairs;
70533
72290
  j = Math.floor(k);
70534
72291
  t = k - j;
70535
72292
  // if (convert) t = convert(t);
70536
- intVal = d3_interpolate(values[j], values[j+1])(t);
72293
+ intVal = getPairInterpolator(values[j], values[j+1], pairOpts)(t);
70537
72294
  output.push(intVal);
70538
72295
  }
70539
- output.push(values[values.length - 1]);
72296
+ output.push(getVibrantValues(values, opts)[values.length - 1]);
70540
72297
  if (stops) {
70541
- output = getStoppedValues(output, stops);
72298
+ // the colors already have their vibrance
72299
+ output = getStoppedValues(output, stops, {interpolation: opts && opts.interpolation});
70542
72300
  }
70543
72301
  return output;
70544
72302
  }
70545
72303
 
72304
+ // The user's colors, with vibrance added (see getVibrantColor()), as the
72305
+ // oklch interpolator gives them at the ends of each pair
72306
+ function getVibrantValues(values, opts) {
72307
+ var vibrance = opts && opts.interpolation == 'oklch' && opts.vibrance;
72308
+ if (!(vibrance > 0)) return values;
72309
+ return values.map(function(val) {
72310
+ return parseColor(val) ? getVibrantColor(val, vibrance) : val;
72311
+ });
72312
+ }
72313
+
70546
72314
  // convert an index (0 ... n-1, -1, -2) to a corresponding discreet value
70547
72315
  function getDiscreteValueGetter(values, nullValue, otherValue) {
70548
72316
  var n = values.length;
@@ -70561,7 +72329,7 @@ ${svg}
70561
72329
  // get a function to convert class indexes to output values
70562
72330
  //
70563
72331
  if (opts.continuous) {
70564
- return getInterpolatedValueGetter(classValues, nullValue);
72332
+ return getInterpolatedValueGetter(classValues, nullValue, opts);
70565
72333
  } else {
70566
72334
  return getDiscreteValueGetter(classValues, nullValue, opts.other);
70567
72335
  }
@@ -70886,17 +72654,9 @@ ${svg}
70886
72654
  } else if (opts.breaks) {
70887
72655
  // user-defined breaks
70888
72656
  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
72657
  } else {
70899
- stop$1('Unknown classification method:', method);
72658
+ breaks = getSequentialBreaks(ascending, method, numBreaks);
72659
+ if (method == 'nice') message('Nice breaks:', breaks);
70900
72660
  }
70901
72661
 
70902
72662
  printDistributionInfo(ascending, breaks, nullCount);
@@ -70928,6 +72688,18 @@ ${svg}
70928
72688
  return classifier;
70929
72689
  }
70930
72690
 
72691
+ // The inner breaks of a sequential method's classes (also used by the GUI,
72692
+ // to show each class's range)
72693
+ // ascending: the data values, in ascending order
72694
+ function getSequentialBreaks(ascending, method, numBreaks) {
72695
+ if (numBreaks === 0) return [];
72696
+ if (method == 'equal-interval') return getEqualIntervalBreaks(ascending, numBreaks);
72697
+ if (method == 'quantile') return getQuantileBreaks(ascending, numBreaks);
72698
+ if (method == 'hybrid') return getHybridBreaks(ascending, numBreaks);
72699
+ if (method == 'nice') return getNiceBreaks(ascending, numBreaks);
72700
+ stop$1('Unknown classification method:', method);
72701
+ }
72702
+
70931
72703
  function getClassRanges(breaks, ascending) {
70932
72704
  var ranges = [];
70933
72705
  var ids, geBound, ltBound, range;
@@ -71001,7 +72773,7 @@ ${svg}
71001
72773
  return function(val) {
71002
72774
  var i = -1;
71003
72775
  if (Number(val) === val) { // exclude null, NaN, strings, etc.
71004
- if (round) val = val(round);
72776
+ if (round) val = round(val);
71005
72777
  i = getClassId(val, breaks);
71006
72778
  }
71007
72779
  if (inverted && i > -1) {
@@ -71074,6 +72846,458 @@ ${svg}
71074
72846
  return arraysAreIdentical(arr, utils.genericSort(arr.map(parseFloat), false));
71075
72847
  }
71076
72848
 
72849
+ // Diverging classification: classes on either side of a pivot value, and an
72850
+ // optional neutral class at the pivot. Each side is classified on its own
72851
+ // data. See docs/development/color-scheme-panel-design.md
72852
+ //
72853
+ // A layout is {pivot, neutral, below, above, breaks, extent, dataRange}:
72854
+ // neutral is the range [lo, hi) of the neutral class, or null; below and
72855
+ // above are the numbers of classes on either side (outside the neutral
72856
+ // class); breaks are the inner breaks of all the classes, in ascending
72857
+ // order, for the sequential classifier. Classes are numbered from the
72858
+ // lowest: below, then the neutral class, then above. extent is where the
72859
+ // outer classes end: the data's min and max, or for equal interval and nice,
72860
+ // whole steps out from the pivot. dataRange is the data's [min, max].
72861
+
72862
+ var intervalMethods = ['equal-interval', 'nice'];
72863
+ var quantileMethods = ['quantile', 'hybrid'];
72864
+
72865
+ function isDivergingClassification(opts) {
72866
+ return opts.pivot !== undefined || !!opts.pivot_range;
72867
+ }
72868
+
72869
+ // Whether a diverging classification has a pivot (neutral) class: classed
72870
+ // output has one unless no-pivot-class is given; continuous output only with
72871
+ // pivot-class or pivot-range=
72872
+ function hasPivotClass(opts) {
72873
+ if (opts.pivot_class && opts.no_pivot_class) {
72874
+ stop$1('Use pivot-class or no-pivot-class, not both');
72875
+ }
72876
+ if (opts.continuous) {
72877
+ return !!(opts.pivot_class || opts.pivot_range);
72878
+ }
72879
+ return !opts.no_pivot_class;
72880
+ }
72881
+
72882
+ // ascending: the data values, in ascending order
72883
+ // opts: pivot, pivot_range, pivot_class, no_pivot_class, continuous,
72884
+ // classes (a total, or the numbers of classes [below, above]), breaks
72885
+ function getDivergingLayout(ascending, method, opts) {
72886
+ var range = opts.pivot_range || null;
72887
+ var hasNeutral = hasPivotClass(opts);
72888
+ var pivot, counts, layout;
72889
+ if (ascending.length === 0) {
72890
+ stop$1('No numeric data to classify');
72891
+ }
72892
+ if (range) {
72893
+ if (range.length != 2 || !(range[0] <= range[1])) {
72894
+ stop$1('pivot-range= takes two numbers, low,high');
72895
+ }
72896
+ if (opts.no_pivot_class) stop$1('pivot-range= sets the range of the pivot class (remove no-pivot-class)');
72897
+ }
72898
+ pivot = resolvePivot(opts.pivot, ascending, range);
72899
+ if (range && (pivot < range[0] || pivot > range[1])) {
72900
+ stop$1('The pivot', pivot, 'is outside pivot-range=', range.join(','));
72901
+ }
72902
+ if (opts.breaks) {
72903
+ if (range) stop$1('Use breaks= or pivot-range=, not both');
72904
+ layout = getBreaksLayout(opts.breaks, pivot, hasNeutral);
72905
+ } else {
72906
+ counts = parseClassCounts(opts.classes);
72907
+ if (intervalMethods.includes(method)) {
72908
+ layout = getIntervalLayout(ascending, pivot, hasNeutral, range, counts, method == 'nice');
72909
+ } else if (quantileMethods.includes(method)) {
72910
+ layout = getQuantileLayout(ascending, pivot, hasNeutral, range, counts, method);
72911
+ } else {
72912
+ stop$1('The', method, 'method does not support pivot=');
72913
+ }
72914
+ }
72915
+ if (layout.below + layout.above === 0 && !layout.neutral) {
72916
+ stop$1('Unable to classify the data around the pivot');
72917
+ }
72918
+ layout.breaks = layout.breaks.map(tidyNumber);
72919
+ if (layout.neutral) layout.neutral = layout.neutral.map(tidyNumber);
72920
+ layout.dataRange = [ascending[0], ascending[ascending.length - 1]];
72921
+ layout.extent = (layout.extent || layout.dataRange).map(tidyNumber);
72922
+ return layout;
72923
+ }
72924
+
72925
+ // The values at the color stops of continuous output: each side with
72926
+ // classes is a ramp of its own, with a stop at each of its breaks, from the
72927
+ // side's inner edge (the pivot or the pivot class) to the end of its outer
72928
+ // class. Returns {below, above}, the stops of each side in ascending order
72929
+ // (m + 1 stops for m classes, none for a side without classes).
72930
+ function getDivergingStops(layout) {
72931
+ var edges = getSideEdges(layout);
72932
+ var below = [], above = [];
72933
+ if (layout.below > 0) {
72934
+ below = [Math.min(layout.extent[0], edges[0])]
72935
+ .concat(layout.breaks.filter(function(b) { return b < edges[0]; }), [edges[0]]);
72936
+ }
72937
+ if (layout.above > 0) {
72938
+ above = [edges[1]]
72939
+ .concat(layout.breaks.filter(function(b) { return b > edges[1]; }),
72940
+ [Math.max(layout.extent[1], edges[1])]);
72941
+ }
72942
+ return {below: below, above: above};
72943
+ }
72944
+
72945
+ // The inner edges of the two sides: the pivot, or the pivot class's range
72946
+ function getSideEdges(layout) {
72947
+ return layout.neutral ? layout.neutral : [layout.pivot, layout.pivot];
72948
+ }
72949
+
72950
+ // The number of colors one side of continuous output takes: one per stop
72951
+ function getContinuousSideStops(classes) {
72952
+ return classes > 0 ? classes + 1 : 0;
72953
+ }
72954
+
72955
+ // Returns a function for continuous output: each side's value is
72956
+ // interpolated between the values at its stops, and the pivot class has the
72957
+ // center value.
72958
+ // values: the side below's values (one per stop, ascending), the center
72959
+ // value (with a pivot class), and the side above's (see
72960
+ // getDivergingClassValues())
72961
+ function getContinuousDivergingClassifier(layout, values, nullValue, opts) {
72962
+ var stops = getDivergingStops(layout);
72963
+ var nb = stops.below.length;
72964
+ var na = stops.above.length;
72965
+ var low = nb > 0 ? getStopInterpolator(stops.below, values.slice(0, nb), opts) : null;
72966
+ var high = na > 0 ? getStopInterpolator(stops.above, values.slice(values.length - na), opts) : null;
72967
+ var center = layout.neutral ? values[nb] : null;
72968
+ var edges = getSideEdges(layout);
72969
+ return function(val) {
72970
+ if (!utils.isFiniteNumber(val)) return nullValue;
72971
+ if (layout.neutral && val >= edges[0] && val < edges[1]) return center;
72972
+ if (val < edges[0]) return (low || high)(val);
72973
+ return (high || low)(val);
72974
+ };
72975
+ }
72976
+
72977
+ function getStopInterpolator(stops, values, opts) {
72978
+ var getValue = getInterpolatedValueGetter(values, null, opts);
72979
+ return function(val) {
72980
+ return getValue(getStopPosition(stops, val));
72981
+ };
72982
+ }
72983
+
72984
+ // The position of a value among ascending stops (0 at the first stop, 1 at
72985
+ // the second, ...), clamped to the stops
72986
+ function getStopPosition(stops, val) {
72987
+ var n = stops.length;
72988
+ if (!(val > stops[0])) return 0;
72989
+ for (var i=1; i<n; i++) {
72990
+ if (val <= stops[i]) {
72991
+ return stops[i] > stops[i - 1] ? i - 1 + (val - stops[i - 1]) / (stops[i] - stops[i - 1]) : i;
72992
+ }
72993
+ }
72994
+ return n - 1;
72995
+ }
72996
+
72997
+ // The CLI's message about a layout
72998
+ function formatDivergingLayout(layout) {
72999
+ var parts = [layout.below + ' below'];
73000
+ if (layout.neutral) {
73001
+ parts.push('a pivot class from ' + layout.neutral[0] + ' to ' + layout.neutral[1]);
73002
+ }
73003
+ parts.push(layout.above + ' above');
73004
+ return 'Pivot: ' + tidyNumber(layout.pivot) + ' (classes: ' + parts.join(', ') + ')';
73005
+ }
73006
+
73007
+ // pivotOpt: a number, 'median', 'mean' or 'auto' (0 if the data has
73008
+ // values on both sides of 0, or else the median); with no pivot=, the
73009
+ // middle of pivot-range=
73010
+ function resolvePivot(pivotOpt, ascending, range) {
73011
+ var n = ascending.length;
73012
+ var val = pivotOpt;
73013
+ if (val === undefined || val === null || val === '') {
73014
+ if (range) return (range[0] + range[1]) / 2;
73015
+ val = 'auto';
73016
+ }
73017
+ if (val == 'auto') {
73018
+ return ascending[0] < 0 && ascending[n - 1] > 0 ? 0 : getMedian(ascending);
73019
+ }
73020
+ if (val == 'median') return getMedian(ascending);
73021
+ if (val == 'mean') return utils.sum(ascending) / n;
73022
+ if (utils.isString(val) && val.trim() !== '' && !isNaN(+val)) val = +val;
73023
+ if (!utils.isFiniteNumber(val)) {
73024
+ stop$1('Invalid pivot:', pivotOpt, '(expected a number, median, mean or auto)');
73025
+ }
73026
+ return val;
73027
+ }
73028
+
73029
+ function getMedian(ascending) {
73030
+ var n = ascending.length;
73031
+ var mid = Math.floor(n / 2);
73032
+ return n % 2 == 1 ? ascending[mid] : (ascending[mid - 1] + ascending[mid]) / 2;
73033
+ }
73034
+
73035
+ // Returns {total} (all the classes, including a neutral class) or
73036
+ // {below, above} (the classes on either side of it)
73037
+ function parseClassCounts(classes, hasNeutral) {
73038
+ var arr = Array.isArray(classes) ? classes : [classes];
73039
+ if (arr.length == 1 && utils.isInteger(arr[0]) && arr[0] >= 2) {
73040
+ return {total: arr[0]};
73041
+ }
73042
+ if (arr.length == 2 && arr.every(isCount) && arr[0] + arr[1] > 0) {
73043
+ return {below: arr[0], above: arr[1]};
73044
+ }
73045
+ stop$1('Invalid classes= for pivot=:', String(classes),
73046
+ '(expected a total number of classes, or the numbers below and above the pivot)');
73047
+ }
73048
+
73049
+ function isCount(n) {
73050
+ return utils.isInteger(n) && n >= 0;
73051
+ }
73052
+
73053
+ // Equal-interval and nice breaks: each side's classes step out from the
73054
+ // pivot. Given a total, the two sides share one step, and the side with
73055
+ // less data gets fewer classes. An automatic neutral class is one step
73056
+ // wide, centered on the pivot.
73057
+ function getIntervalLayout(ascending, pivot, hasNeutral, range, counts, nice) {
73058
+ var extents = [Math.max(0, pivot - ascending[0]), Math.max(0, ascending[ascending.length - 1] - pivot)];
73059
+ var auto = hasNeutral && !range;
73060
+ var fixed = range ? [pivot - range[0], range[1] - pivot] : [0, 0];
73061
+ var steps, offsets, sides;
73062
+
73063
+ // distances from the pivot to each side's classes
73064
+ function getOffsets(steps) {
73065
+ var half;
73066
+ if (!auto) return fixed;
73067
+ half = Math.min(steps[0] || Infinity, steps[1] || Infinity) / 2;
73068
+ return isFinite(half) ? [half, half] : [0, 0];
73069
+ }
73070
+
73071
+ function countSides(steps) {
73072
+ var offsets = getOffsets(steps);
73073
+ return [0, 1].map(function(i) {
73074
+ return getSideCount(extents[i], offsets[i], steps[i]);
73075
+ });
73076
+ }
73077
+
73078
+ if (counts.total) {
73079
+ steps = getCommonStep(extents, counts.total - (hasNeutral ? 1 : 0), auto ? null : fixed, nice);
73080
+ sides = countSides(steps);
73081
+ } else {
73082
+ sides = [counts.below, counts.above];
73083
+ if (auto) {
73084
+ // the neutral class is one step wide, the narrower side's step
73085
+ offsets = getOffsets([0, 1].map(function(i) {
73086
+ return sides[i] > 0 ? extents[i] / (sides[i] + 0.5) : 0;
73087
+ }));
73088
+ } else {
73089
+ offsets = fixed;
73090
+ }
73091
+ sides = sides.map(function(n, i) {
73092
+ return sideHasData(extents[i] > offsets[i], n);
73093
+ });
73094
+ steps = [0, 1].map(function(i) {
73095
+ return sides[i] > 0 ? (extents[i] - offsets[i]) / sides[i] : 0;
73096
+ });
73097
+ if (nice) {
73098
+ steps = steps.map(getNiceStep);
73099
+ sides = countSides(steps);
73100
+ }
73101
+ }
73102
+ offsets = getOffsets(steps);
73103
+ return Object.assign(makeLayout(pivot, hasNeutral, pivot - offsets[0], pivot + offsets[1], sides[0], sides[1],
73104
+ getSteppedBreaks(pivot - offsets[0], -steps[0], sides[0]),
73105
+ getSteppedBreaks(pivot + offsets[1], steps[1], sides[1])), {
73106
+ extent: [pivot - offsets[0] - sides[0] * steps[0], pivot + offsets[1] + sides[1] * steps[1]]
73107
+ });
73108
+ }
73109
+
73110
+ // The number of classes of a given step that reach from a side's inner edge
73111
+ // (offset from the pivot) to its data extent
73112
+ function getSideCount(extent, offset, step) {
73113
+ if (!(extent > offset) || !(step > 0)) return 0;
73114
+ return Math.max(1, Math.ceil((extent - offset) / step - 1e-9));
73115
+ }
73116
+
73117
+ function sideHasData(hasData, count) {
73118
+ return hasData ? count : 0;
73119
+ }
73120
+
73121
+ // Returns [step, step]: the step that gives the two sides a total of n
73122
+ // classes, with the longer side's classes reaching just to the end of its
73123
+ // data (or a total as close to n as the data allows, without going over).
73124
+ // fixed: distances from the pivot to each side's classes, or null for half
73125
+ // a step (an automatic neutral class)
73126
+ // nice: use the nice step that comes closest to n classes
73127
+ function getCommonStep(extents, n, fixed, nice) {
73128
+ var candidates = [];
73129
+ var best = null;
73130
+ var count = function(s) {
73131
+ return extents.reduce(function(memo, extent, i) {
73132
+ return memo + getSideCount(extent, fixed ? fixed[i] : s / 2, s);
73133
+ }, 0);
73134
+ };
73135
+ // the steps at which a side's count of classes changes
73136
+ extents.forEach(function(extent, i) {
73137
+ for (var j=fixed ? 1 : 0; j<=n; j++) {
73138
+ candidates.push(fixed ? (extent - fixed[i]) / j : extent / (j + 0.5));
73139
+ }
73140
+ });
73141
+ candidates.filter(function(s) { return s > 0 && isFinite(s); })
73142
+ .sort(function(a, b) { return b - a; })
73143
+ .forEach(function(s) {
73144
+ var total = count(s);
73145
+ if (total <= n && (!best || total > best.total)) {
73146
+ best = {step: s, total: total};
73147
+ }
73148
+ });
73149
+ if (!best) return [0, 0];
73150
+ if (nice) {
73151
+ best = getNiceSteps(best.step).reduce(function(memo, s) {
73152
+ var total = count(s);
73153
+ var diff = Math.abs(total - n);
73154
+ return !memo || diff < memo.diff || diff == memo.diff && s > memo.step ?
73155
+ {step: s, diff: diff} : memo;
73156
+ }, null);
73157
+ }
73158
+ return [best.step, best.step];
73159
+ }
73160
+
73161
+ // nice numbers: 1, 2, 2.5 or 5 times a power of 10
73162
+ function getNiceSteps(val) {
73163
+ var exp = Math.floor(Math.log10(val));
73164
+ var steps = [];
73165
+ [exp - 1, exp, exp + 1].forEach(function(e) {
73166
+ [1, 2, 2.5, 5].forEach(function(m) {
73167
+ steps.push(tidyNumber(m * Math.pow(10, e)));
73168
+ });
73169
+ });
73170
+ return steps;
73171
+ }
73172
+
73173
+ // the nice number nearest to val (on a log scale)
73174
+ function getNiceStep(val) {
73175
+ if (!(val > 0)) return val;
73176
+ return getNiceSteps(val).reduce(function(memo, s) {
73177
+ return Math.abs(Math.log(s / val)) < Math.abs(Math.log(memo / val)) ? s : memo;
73178
+ });
73179
+ }
73180
+
73181
+ // the inner breaks of n classes stepping out from edge (step < 0 below the
73182
+ // pivot), in ascending order
73183
+ function getSteppedBreaks(edge, step, n) {
73184
+ var breaks = [];
73185
+ for (var i=1; i<n; i++) {
73186
+ breaks.push(edge + step * i);
73187
+ }
73188
+ return step < 0 ? breaks.reverse() : breaks;
73189
+ }
73190
+
73191
+ // Quantile and hybrid breaks: each side's classes divide its own data.
73192
+ // Given a total, the two sides' classes hold the same number of features,
73193
+ // so the side with fewer features gets fewer classes. An automatic neutral
73194
+ // class holds as many features as a class does on average, taken from those
73195
+ // nearest the pivot, so its range is centered on the pivot.
73196
+ function getQuantileLayout(ascending, pivot, hasNeutral, range, counts, method) {
73197
+ var n = ascending.length;
73198
+ var getBreaks = method == 'hybrid' ? getHybridBreaks : getQuantileBreaks;
73199
+ var lo = pivot, hi = pivot;
73200
+ var classCount, inNeutral, dists, d, belowVals, aboveVals, sideClasses, below, above;
73201
+ if (hasNeutral && range) {
73202
+ lo = range[0];
73203
+ hi = range[1];
73204
+ } else if (hasNeutral) {
73205
+ classCount = counts.total || counts.below + counts.above + 1;
73206
+ inNeutral = Math.max(1, Math.round(n / classCount));
73207
+ dists = ascending.map(function(val) { return Math.abs(val - pivot); })
73208
+ .sort(function(a, b) { return a - b; });
73209
+ // halfway to the next value out, so that no value sits on an edge
73210
+ d = inNeutral < n ? (dists[inNeutral - 1] + dists[inNeutral]) / 2 : dists[n - 1] * 2 + 1;
73211
+ lo = pivot - d;
73212
+ hi = pivot + d;
73213
+ }
73214
+ belowVals = ascending.filter(function(val) { return val < lo; });
73215
+ aboveVals = ascending.filter(function(val) { return val >= hi; });
73216
+ if (counts.total) {
73217
+ sideClasses = counts.total - (hasNeutral ? 1 : 0);
73218
+ below = splitClasses(sideClasses, belowVals.length, aboveVals.length);
73219
+ above = belowVals.length + aboveVals.length > 0 ? sideClasses - below : 0;
73220
+ } else {
73221
+ below = sideHasData(belowVals.length > 0, counts.below);
73222
+ above = sideHasData(aboveVals.length > 0, counts.above);
73223
+ }
73224
+ return makeLayout(pivot, hasNeutral, lo, hi, below, above,
73225
+ below > 1 ? getBreaks(belowVals, below - 1) : [],
73226
+ above > 1 ? getBreaks(aboveVals, above - 1) : []);
73227
+ }
73228
+
73229
+ // The number of n classes that go below the pivot, in proportion to the
73230
+ // counts of features, giving each side that has features at least one
73231
+ function splitClasses(n, countBelow, countAbove) {
73232
+ var below;
73233
+ if (countBelow + countAbove === 0) return 0;
73234
+ below = Math.round(n * countBelow / (countBelow + countAbove));
73235
+ if (countBelow > 0 && below === 0 && n > 1) below = 1;
73236
+ if (countAbove > 0 && below == n && n > 1) below = n - 1;
73237
+ return below;
73238
+ }
73239
+
73240
+ // User-defined breaks: a pivot that is one of the breaks divides the
73241
+ // classes; one inside a class makes that class the neutral class.
73242
+ function getBreaksLayout(breaks, pivot, hasNeutral) {
73243
+ var b = breaks.length;
73244
+ var k = breaks.indexOf(pivot);
73245
+ var i;
73246
+ if (k > -1) {
73247
+ return {pivot: pivot, neutral: null, below: k + 1, above: b - k, breaks: breaks};
73248
+ }
73249
+ for (i=0; i<b && breaks[i] < pivot; i++);
73250
+ // the pivot is in class i
73251
+ if (!hasNeutral) {
73252
+ stop$1('Without a pivot class, the pivot must be one of the breaks');
73253
+ }
73254
+ return {
73255
+ pivot: pivot,
73256
+ neutral: [i > 0 ? breaks[i - 1] : -Infinity, i < b ? breaks[i] : Infinity],
73257
+ below: i,
73258
+ above: b - i,
73259
+ breaks: breaks
73260
+ };
73261
+ }
73262
+
73263
+ function makeLayout(pivot, hasNeutral, lo, hi, below, above, breaksBelow, breaksAbove) {
73264
+ var breaks = breaksBelow.concat();
73265
+ if (hasNeutral) {
73266
+ if (below > 0) breaks.push(lo);
73267
+ if (above > 0) breaks.push(hi);
73268
+ } else if (below > 0 && above > 0) {
73269
+ breaks.push(pivot);
73270
+ }
73271
+ return {
73272
+ pivot: pivot,
73273
+ neutral: hasNeutral ? [lo, hi] : null,
73274
+ below: below,
73275
+ above: above,
73276
+ breaks: breaks.concat(breaksAbove)
73277
+ };
73278
+ }
73279
+
73280
+ // rounds off floating point noise, like 0.30000000000000004
73281
+ function tidyNumber(val) {
73282
+ return isFinite(val) ? +val.toPrecision(12) : val;
73283
+ }
73284
+
73285
+ var Diverging = /*#__PURE__*/Object.freeze({
73286
+ __proto__: null,
73287
+ formatDivergingLayout: formatDivergingLayout,
73288
+ getContinuousDivergingClassifier: getContinuousDivergingClassifier,
73289
+ getContinuousSideStops: getContinuousSideStops,
73290
+ getDivergingLayout: getDivergingLayout,
73291
+ getDivergingStops: getDivergingStops,
73292
+ getNiceStep: getNiceStep,
73293
+ getStopPosition: getStopPosition,
73294
+ hasPivotClass: hasPivotClass,
73295
+ isDivergingClassification: isDivergingClassification,
73296
+ parseClassCounts: parseClassCounts,
73297
+ resolvePivot: resolvePivot,
73298
+ splitClasses: splitClasses
73299
+ });
73300
+
71077
73301
  function getCategoricalClassifier(classValues, nullVal, opts) {
71078
73302
  // categories: strings to match in the data
71079
73303
  var categories = opts.categories;
@@ -71754,7 +73978,7 @@ ${svg}
71754
73978
  return c$1 + "";
71755
73979
  }
71756
73980
 
71757
- var c = rgb$1(),
73981
+ var c = rgb(),
71758
73982
  pi_1_3 = Math.PI / 3,
71759
73983
  pi_2_3 = Math.PI * 2 / 3;
71760
73984
 
@@ -71870,6 +74094,59 @@ ${svg}
71870
74094
  schemeYlOrRd: scheme$6
71871
74095
  });
71872
74096
 
74097
+ // Scientific colour maps, version 8.0.1, by Fabio Crameri
74098
+ // https://www.fabiocrameri.ch/colourmaps/ (https://doi.org/10.5281/zenodo.8409685)
74099
+ //
74100
+ // Each ramp is 33 evenly spaced colors (as packed hex), sampled from the
74101
+ // 256-color version, which they reproduce to within 0.008 OKLab deltaE; the
74102
+ // odd count gives diverging ramps a color at their center. batlowS is the
74103
+ // first 20 swatches of batlow's categorical palette.
74104
+ //
74105
+ // The Scientific colour maps are licensed under a MIT License
74106
+ // Copyright (c) 2023, Fabio Crameri
74107
+ // Permission is hereby granted, free of charge, to any person obtaining a
74108
+ // copy of this software and associated documentation files (the "Software"),
74109
+ // to deal in the Software without restriction, including without limitation
74110
+ // the rights to use, copy, modify, merge, publish, distribute, sublicense,
74111
+ // and/or sell copies of the Software, and to permit persons to whom the
74112
+ // Software is furnished to do so, subject to the following conditions:
74113
+ // The above copyright notice and this permission notice shall be included in
74114
+ // all copies or substantial portions of the Software.
74115
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
74116
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
74117
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
74118
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
74119
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
74120
+ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
74121
+ // DEALINGS IN THE SOFTWARE.
74122
+
74123
+ var crameriSequential = {
74124
+ batlow: '01195908255b0d315d0f3b5f114360134b611652621b59622260612a655e356a59406f544d734d597646667a3f737e3881823290862da08a2bb08d2ebf9035ce923fdb954ce8985bf29d6cf8a27efca891fdaea3fdb4b4fdbac5fcc0d6fbc6e8faccfa',
74125
+ batlowW: '01195908255b0d315d0f3b5f114360134a611651621a5862205f6228655f326a5c3d6f5749745155794b627d446f823e7d87388c8c339d9232ae9838be9d44cca154d7a565e0a875e8ac84efb094f5b8a6fbc3bafed1ceffdfddffebeafff5f5fffefe',
74126
+ acton: '260d402d17493420523a2a5a413362473b6a4d4472534c79595481615b876a608b74628d7d638e87648e91648f9c658fa76690b36791bf6a93c97097cf789fd482a7d78db0da97b9dda2c2e0accae3b6d2e5c0dae8cae1ead3e8ecdbeeeee3f4f0eafa',
74127
+ bamako: '003b47043d45083f430c424010453e14483b194b381f4e342552312b562d325b29395f2440642048691b516f16597410627a0b6b7f057584017f87008a8900948c019f9108a99815b4a122bfa930cab240d4bb52ddc465e6cc78efd58af7dd9cffe5ad',
74128
+ bilbao: '4c0001580b1064161a6e202478292e8232398b3b4293444a994e509c56549f5d56a16458a26a59a4705aa6765ca77c5da9825eaa885fac8e61ae9463b09c66b4a46eb8ac7bbcb389bfb897c2bda4c5c2b0c9c7bccfcdc8d7d7d5e3e3e2f1f1f1ffffff',
74129
+ davos: '00054a031256081c610e276c1431761a3c802146892850902f5a9636639a3e6b9c46729d4e789d557e9b5d84996489966c8d937392907b978d839d8a8ba38895aa87a0b289adbc8dbcc894cbd49fdadfade6e9bceff0cbf6f6d9faf9e6fcfcf3fefefe',
74130
+ devon: '2c1a4c2b21542a285b29306329376a283f7227477b274f8429588f2c5f9a3265a5386bb04271bc4e78c65e80cf6e87d67d8edd8c95e29a9ce7a6a3ebb0aaeeb7b1f0beb8f2c4bff3cbc6f4d1cdf6d7d4f7dedbf8e4e2faebe9fbf2f0fcf8f8feffffff',
74131
+ lajolla: '191900201c04271e082f210d3724114127164c2b1c592f22663429753931853d3895423fa64644b64a48c54f4ad1564cd95f4ede6a4fe1744fe37e50e58751e79152e99a52eba353edad54efb755f1c159f4cd62f7d972fae588fcef9ffef7b6fffecb',
74132
+ lapaz: '1a0c641d176b1f207122297824327e263a842842892a4a8e2c52932f5a9733629b386a9e3d71a04378a24a7fa35286a45b8ca36591a27096a07a9b9e859e9b90a1999ba496a7a895b4ac96c2b299d0baa1dec4aceaceb9f3d8c8f9e1d6fceae5fef2f3',
74133
+ oslo: '010101050a100a121a0d18240e1e2e0f2439112a4413314f15395b1840671c4773214f7f26568c2d5e993667a54270b14f7abc5c83c3678bc77292c97c99ca859fca8fa4c998aac9a2b0caacb7cbb7bfcdc2c7d1ced1d7dadcdfe6e7e9f3f3f4ffffff',
74134
+ tokyo: '1c0e3429123837163d441d415024465b2d4963364c683f4e6b47506e4e506f5351705951715d52716152726652736a53746f53757554767c54778355798c567b95587e9f5b81ab5f87b7668fc3709ad17da8dd8eb8e9a2c9f1b4d8f7c5e5fad2effcdd',
74135
+ turku: '0000000e0e0d18181620201d28282330302a39382f41413549493951513e5a59426261456a6a4973724d7c7a51878355928b5a9e9360ab9a66b79f6dc2a373cca579d5a780dda887e4aa90ebae9af2b4a5f7bbb1fbc3bdfdccc8fed5d3ffdddcffe6e6'
74136
+ };
74137
+
74138
+ var crameriDiverging = {
74139
+ bam: '65024b77175d88266e97357da5428cb25099bd5ea5c76db0d17ebbd990c6e0a2d0e6b4d9ecc5e2f1d4e9f4e1eef6ebf1f6f1f1f3f3ecedf2e3e4eed4d7e7c1c7deaab4d191a0c3798cb5657aa6546999465a8c3a4c802f3e7425306819205a0d0d4c00',
74140
+ broc: '2c1a4c2b26582a3164293e70294a7d2d588939669449749f5a82a86c8fb27e9dbc91acc5a4bacfb7c8d9cad7e3dde5ebebeeecedeee0e7e7ceddddbcd4d4a9c9c995bbbb82abab709b9b618b8b547b7b476c6c3a5d5d2d4e4e2140401632330c262600',
74141
+ cork: '2c194c2b26592a3265283f71284b7e2d588a38669446729e567ea6668bae7798b889a6c19db4cbb1c4d6c7d4e1dbe4ebe6edecdfeae0cedfcebbd2bba8c5a794b89481ab816e9e6e5d935c4c884c3c7c3c2d702c20621e18541414450e1137090f2903',
74142
+ roma: '7e1700862c068e3c0c964b129d5818a3651ea97223af7e2ab68c32bd9a3cc4aa4acaba5cd0c971d2d789d1e19fcbe8b3c1eac2b2e9cda1e4d48ddcd778d2d764c5d452b8d044aacc399dc63190c12b83bc2677b7226ab11e5dab184fa511409f033198',
74143
+ vik: '001261021f69022b710237790344810450890c5e921b6d9b2f7ca6478db1609dbc79adc792bdd2acccdcc5dbe5dce5e9ece5e1eedacfe9cbb9e2bba4dcab8fd59c7bcf8e68c97f55c37243bc6331b3531fa5400e932e06832106741506660a07590008'
74144
+ };
74145
+
74146
+ var crameriCategorical = {
74147
+ batlowS: '011959faccfa828231226061f29d6d4d734d114360c09036fdb4b4dd954d356a59fcbfd6175262677b3ea18a2b0d315dfca890fbc6e82b655eb18d2f'
74148
+ };
74149
+
71873
74150
  var index = {
71874
74151
  categorical: [],
71875
74152
  sequential: [],
@@ -71878,14 +74155,33 @@ ${svg}
71878
74155
  all: []
71879
74156
  };
71880
74157
  var ramps;
74158
+ // interpolators of the ramps that are stored as stops (not from d3)
74159
+ var stopInterpolators;
74160
+ // scheme name -> where the scheme comes from (see getColorSchemeGroups())
74161
+ var sources;
74162
+ // the order of the sources in lists of schemes
74163
+ var sourceOrder = ['ColorBrewer', 'Tableau', 'Matplotlib', 'Crameri', 'd3'];
74164
+ // Schemes are listed by their designers, as far as they're known. 'd3' is
74165
+ // d3-scale-chromatic's own schemes, and others that it brings in: Turbo
74166
+ // (Google) and CubehelixDefault (D. A. Green's cubehelix).
74167
+ var d3Sources = {
74168
+ ColorBrewer: 'Accent,Dark2,Paired,Pastel1,Pastel2,Set1,Set2,Set3,' +
74169
+ 'Blues,Greens,Greys,Purples,Reds,Oranges,BuGn,BuPu,GnBu,OrRd,PuBuGn,PuBu,PuRd,RdPu,YlGnBu,YlGn,YlOrBr,YlOrRd,' +
74170
+ 'BrBG,PRGn,PiYG,PuOr,RdBu,RdGy,RdYlBu,RdYlGn,Spectral',
74171
+ Tableau: 'Tableau10,Tableau20',
74172
+ Matplotlib: 'Cividis,Viridis,Magma,Inferno,Plasma',
74173
+ d3: 'Category10,Category20,Category20b,Category20c,CubehelixDefault,Rainbow,Warm,Cool,Sinebow,Turbo'
74174
+ };
71881
74175
 
71882
74176
  function initSchemes() {
71883
74177
  if (ramps) return;
71884
74178
  ramps = {};
74179
+ stopInterpolators = {};
74180
+ sources = {};
71885
74181
  addSchemesFromD3('categorical', 'Category10,Accent,Dark2,Paired,Pastel1,Pastel2,Set1,Set2,Set3,Tableau10');
71886
74182
  addSchemesFromD3('sequential', 'Blues,Greens,Greys,Purples,Reds,Oranges,BuGn,BuPu,GnBu,OrRd,PuBuGn,PuBu,PuRd,RdPu,YlGnBu,YlGn,YlOrBr,YlOrRd');
71887
74183
  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');
74184
+ addSchemesFromD3('diverging', 'BrBG,PRGn,PiYG,PuOr,RdBu,RdGy,RdYlBu,RdYlGn,Spectral');
71889
74185
  testLib(); // make sure these schemes are all available
71890
74186
  addCategoricalScheme('Category20',
71891
74187
  '1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5');
@@ -71895,9 +74191,29 @@ ${svg}
71895
74191
  '3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9');
71896
74192
  addCategoricalScheme('Tableau20',
71897
74193
  '4c78a89ecae9f58518ffbf7954a24b88d27ab79a20f2cf5b43989483bcb6e45756ff9d9879706ebab0acd67195fcbfd2b279a2d6a5c99e765fd8b5a5');
74194
+ Object.keys(d3Sources).forEach(function(source) {
74195
+ d3Sources[source].split(',').forEach(function(name) {
74196
+ sources[name] = source;
74197
+ });
74198
+ });
74199
+ addStoppedSchemes('sequential', crameriSequential, 'Crameri');
74200
+ addStoppedSchemes('diverging', crameriDiverging, 'Crameri');
74201
+ Object.keys(crameriCategorical).forEach(function(name) {
74202
+ addCategoricalScheme(name, crameriCategorical[name]);
74203
+ sources[name] = 'Crameri';
74204
+ });
71898
74205
  index.all = [].concat(index.sequential, index.rainbow, index.diverging, index.categorical);
71899
74206
  }
71900
74207
 
74208
+ // Ramps stored as evenly spaced colors, interpolated linearly between them
74209
+ function addStoppedSchemes(type, schemes, source) {
74210
+ Object.keys(schemes).forEach(function(name) {
74211
+ index[type].push(name);
74212
+ stopInterpolators[name] = piecewise(interpolateRgb, unpackRamp(schemes[name]));
74213
+ sources[name] = source;
74214
+ });
74215
+ }
74216
+
71901
74217
  function standardName(name) {
71902
74218
  if (!name) return null;
71903
74219
  var lcname = name.toLowerCase();
@@ -71959,12 +74275,35 @@ ${svg}
71959
74275
  }
71960
74276
 
71961
74277
  function printColorSchemeNames() {
74278
+ var types = [['categorical', 'Categorical'], ['sequential', 'Sequential'],
74279
+ ['diverging', 'Diverging'], ['rainbow', 'Multi-hue/rainbow']];
71962
74280
  initSchemes();
71963
- print('Built-in color schemes (from d3):');
71964
- print ('Categorical\n' + formatStringsAsGrid(index.categorical));
71965
- print ('\nSequential\n' + formatStringsAsGrid(index.sequential));
71966
- print ('\nDiverging\n' + formatStringsAsGrid(index.diverging));
71967
- print ('\nMulti-hue/rainbow\n' + formatStringsAsGrid(index.rainbow));
74281
+ print('Built-in color schemes');
74282
+ types.forEach(function(type) {
74283
+ getColorSchemeGroups(type[0]).forEach(function(group) {
74284
+ print('\n' + type[1] + ' (' + group.source + ')\n' + formatStringsAsGrid(group.names));
74285
+ });
74286
+ });
74287
+ }
74288
+
74289
+ // type: categorical, sequential, rainbow or diverging
74290
+ function getColorSchemeNames(type) {
74291
+ initSchemes();
74292
+ if (!index[type]) error('Unknown color scheme type:', type);
74293
+ return index[type].concat();
74294
+ }
74295
+
74296
+ // The schemes of one or more types, by source: [{source, names}]
74297
+ // (sources: ColorBrewer, Tableau, Matplotlib, Crameri, d3)
74298
+ function getColorSchemeGroups(types) {
74299
+ var names = [].concat(types).reduce(function(memo, type) {
74300
+ return memo.concat(getColorSchemeNames(type));
74301
+ }, []);
74302
+ return sourceOrder.map(function(source) {
74303
+ return {source: source, names: names.filter(function(name) { return sources[name] == source; })};
74304
+ }).filter(function(group) {
74305
+ return group.names.length > 0;
74306
+ });
71968
74307
  }
71969
74308
 
71970
74309
  function pickRandomColorScheme(type) {
@@ -72016,6 +74355,14 @@ ${svg}
72016
74355
  return colors;
72017
74356
  }
72018
74357
 
74358
+ // All the colors of a categorical scheme
74359
+ function getCategoricalColors(name) {
74360
+ initSchemes();
74361
+ name = standardName(name);
74362
+ if (!isCategoricalColorScheme(name)) stop$1('Not a categorical color scheme:', name);
74363
+ return (ramps[name] || d3Scales['scheme' + name]).concat();
74364
+ }
74365
+
72019
74366
  function wrapColors(colors, n) {
72020
74367
  while (colors.length > 0 && colors.length < n) {
72021
74368
  colors = colors.concat(colors.slice(0, n - colors.length));
@@ -72033,11 +74380,12 @@ ${svg}
72033
74380
  return index.categorical.includes(standardName(name));
72034
74381
  }
72035
74382
 
72036
- function getColorRamp(name, n, stops) {
74383
+ // interpOpts: how stops= colors are interpolated (see getPairInterpolator())
74384
+ function getColorRamp(name, n, stops, interpOpts) {
72037
74385
  initSchemes();
72038
74386
  name = standardName(name);
72039
74387
  var ramps = d3Scales['scheme' + name];
72040
- var interpolate = d3Scales['interpolate' + name];
74388
+ var interpolate = d3Scales['interpolate' + name] || stopInterpolators[name];
72041
74389
  var ramp;
72042
74390
  if (!ramps && !interpolate) {
72043
74391
  stop$1('Unknown color scheme name:', name);
@@ -72051,11 +74399,31 @@ ${svg}
72051
74399
  ramp = getInterpolatedRamp(interpolate, n);
72052
74400
  }
72053
74401
  if (stops) {
72054
- ramp = getStoppedValues(ramp, stops);
74402
+ ramp = getStoppedValues(ramp, stops, interpOpts);
72055
74403
  }
72056
74404
  return ramp;
72057
74405
  }
72058
74406
 
74407
+ // n colors from part of a ramp, evenly spaced from start to end (0-1).
74408
+ // base: interpolate between the colors of the scheme's set of this size, if
74409
+ // it has one (ColorBrewer's hand-picked sets), so that a section of a set
74410
+ // keeps its colors; otherwise the scheme's own interpolator is used
74411
+ function getColorRampSection(name, n, start, end, base) {
74412
+ initSchemes();
74413
+ name = standardName(name);
74414
+ var sets = d3Scales['scheme' + name];
74415
+ var interpolate = d3Scales['interpolate' + name] || stopInterpolators[name];
74416
+ var colors = [];
74417
+ if (!interpolate || index.categorical.includes(name)) {
74418
+ stop$1('Not a sequential or diverging color scheme:', name);
74419
+ }
74420
+ if (sets && sets[base]) interpolate = piecewise(interpolateRgb, sets[base]);
74421
+ for (var i=0; i<n; i++) {
74422
+ colors.push(interpolate(n > 1 ? start + (end - start) * i / (n - 1) : (start + end) / 2));
74423
+ }
74424
+ return colors;
74425
+ }
74426
+
72059
74427
  function getInterpolatedRamp(interpolate, n) {
72060
74428
  if (n > 0 === false || !utils.isInteger(n)) {
72061
74429
  error('Expected a positive integer');
@@ -72067,6 +74435,23 @@ ${svg}
72067
74435
  return ramp;
72068
74436
  }
72069
74437
 
74438
+ var ColorSchemes = /*#__PURE__*/Object.freeze({
74439
+ __proto__: null,
74440
+ getCategoricalColorScheme: getCategoricalColorScheme,
74441
+ getCategoricalColors: getCategoricalColors,
74442
+ getColorRamp: getColorRamp,
74443
+ getColorRampSection: getColorRampSection,
74444
+ getColorSchemeGroups: getColorSchemeGroups,
74445
+ getColorSchemeNames: getColorSchemeNames,
74446
+ getRandomizedCategoricalColorScheme: getRandomizedCategoricalColorScheme,
74447
+ isCategoricalColorScheme: isCategoricalColorScheme,
74448
+ isColorSchemeName: isColorSchemeName,
74449
+ pickRandomCategoricalScheme: pickRandomCategoricalScheme,
74450
+ pickRandomColorScheme: pickRandomColorScheme,
74451
+ printColorSchemeNames: printColorSchemeNames,
74452
+ wrapColors: wrapColors
74453
+ });
74454
+
72070
74455
  function getNullValue(opts) {
72071
74456
  var nullValue;
72072
74457
  if ('null_value' in opts) {
@@ -72118,11 +74503,11 @@ ${svg}
72118
74503
  if (categorical && isCategoricalColorScheme(colorScheme)) {
72119
74504
  return getCategoricalColorScheme(colorScheme, n);
72120
74505
  } else {
72121
- return getColorRamp(colorScheme, n, opts.stops);
74506
+ return getColorRamp(colorScheme, n, opts.stops, opts);
72122
74507
  }
72123
74508
  } else if (opts.colors || opts.values) {
72124
74509
  if (categorical) {
72125
- return getCategoricalValues(opts.colors || opts.values, n);
74510
+ return getCategoricalValues(opts.colors || opts.values, n, !!opts.colors);
72126
74511
  } else {
72127
74512
  return getInterpolableValues(opts.colors || opts.values, n, opts);
72128
74513
  }
@@ -72132,7 +74517,87 @@ ${svg}
72132
74517
  }
72133
74518
  }
72134
74519
 
72135
- function getCategoricalValues(values, n) {
74520
+ // The values of the classes of a diverging layout (see getDivergingLayout()):
74521
+ // colors (or values) are spread over the longer side, and the shorter side
74522
+ // uses the ones nearest the center, so equal steps from the pivot get
74523
+ // equally strong colors on both sides. The middle of a scheme or list of
74524
+ // colors is the center color (an even list interpolates one).
74525
+ // With opts.continuous, each side has a value per color stop (see
74526
+ // getDivergingStops()) instead of one per class.
74527
+ function getDivergingClassValues(layoutArg, opts) {
74528
+ var layout = opts.continuous ? Object.assign({}, layoutArg, {
74529
+ below: getContinuousSideStops(layoutArg.below),
74530
+ above: getContinuousSideStops(layoutArg.above)
74531
+ }) : layoutArg;
74532
+ var n = layout.below + layout.above + (layout.neutral ? 1 : 0);
74533
+ var colorArg = opts.colors && opts.colors.length == 1 ? opts.colors[0] : null;
74534
+ var list = opts.colors || opts.values;
74535
+ var colorScheme, values;
74536
+ if (opts.stops) stop$1('stops= is not supported with pivot=');
74537
+ if (colorArg == 'random') {
74538
+ colorScheme = pickRandomColorScheme('diverging');
74539
+ message('Randomly selected color scheme:', colorScheme);
74540
+ } else if (isColorSchemeName(colorArg)) {
74541
+ colorScheme = colorArg;
74542
+ } else if (colorArg && !parseColor(colorArg)) {
74543
+ stop$1('Unrecognized color scheme name:', colorArg);
74544
+ } else if (opts.colors) {
74545
+ opts.colors.forEach(validateColor);
74546
+ }
74547
+ if (!colorScheme && !list) {
74548
+ values = getIndexes(n);
74549
+ } else if (!colorScheme && list.length == n && listFitsLayout(layout, opts)) {
74550
+ // one value per class
74551
+ values = interpolateValuesToClasses(parseValues(list), n, null, opts);
74552
+ } else if (!colorScheme && list.length < 2) {
74553
+ stop$1('Expected a color scheme or a list of two or more values');
74554
+ } else if (opts.invert) {
74555
+ // the sides trade places
74556
+ return getDivergingValues(colorScheme, list, layout.above, layout.below, !!layout.neutral, opts).reverse();
74557
+ } else {
74558
+ return getDivergingValues(colorScheme, list, layout.below, layout.above, !!layout.neutral, opts);
74559
+ }
74560
+ return opts.invert ? values.reverse() : values;
74561
+ }
74562
+
74563
+ // A list with one value per class is used as given if its middle value
74564
+ // falls at the pivot, or if classes= gave the numbers of classes on each side
74565
+ function listFitsLayout(layout, opts) {
74566
+ return layout.below == layout.above ||
74567
+ Array.isArray(opts.classes) && opts.classes.length == 2;
74568
+ }
74569
+
74570
+ function getDivergingValues(colorScheme, list, below, above, neutral, opts) {
74571
+ var k = Math.max(below, above, 1);
74572
+ var full = colorScheme ?
74573
+ getColorRamp(colorScheme, k * 2 + 1, null, opts) :
74574
+ getDivergingRamp(parseValues(list), k, opts);
74575
+ // full: k values below the center value, and k above
74576
+ return full.slice(k - below, k)
74577
+ .concat(neutral ? [full[k]] : [])
74578
+ .concat(full.slice(k + 1, k + 1 + above));
74579
+ }
74580
+
74581
+ // Interpolates each half of a list separately, so the center value stays in
74582
+ // the middle
74583
+ function getDivergingRamp(values, k, opts) {
74584
+ var mid = Math.floor(values.length / 2);
74585
+ var odd = values.length % 2 == 1;
74586
+ var center = odd ? values[mid] : getPairInterpolator(values[mid - 1], values[mid], opts)(0.5);
74587
+ var lower = values.slice(0, mid).concat([center]);
74588
+ var upper = [center].concat(values.slice(odd ? mid + 1 : mid));
74589
+ var lowerRamp = interpolateValuesToClasses(lower, k + 1, null, opts);
74590
+ var upperRamp = interpolateValuesToClasses(upper, k + 1, null, opts);
74591
+ return lowerRamp.concat(upperRamp.slice(1));
74592
+ }
74593
+
74594
+ // A list of colors shorter than the list of categories is repeated, as the
74595
+ // colors of a color scheme are
74596
+ function getCategoricalValues(values, n, valuesAreColors) {
74597
+ if (valuesAreColors && values.length < n) {
74598
+ message('Repeating', values.length, 'colors to match', n, 'categories.');
74599
+ values = wrapColors(values, n);
74600
+ }
72136
74601
  if (n != values.length) {
72137
74602
  stop$1('Mismatch in number of categories and number of values');
72138
74603
  }
@@ -72149,18 +74614,15 @@ ${svg}
72149
74614
 
72150
74615
  // TODO: check for non-interpolatable value types (e.g. boolean, text)
72151
74616
  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;
74617
+ // with one value per class, this only adds vibrance= to colors
74618
+ return interpolateValuesToClasses(parseValues(arr), n, opts.stops, opts);
72157
74619
  }
72158
74620
 
72159
74621
  // convert strings to numbers if they all parse as numbers
72160
74622
  // arr: an array of strings
72161
74623
  function parseValues(strings) {
72162
74624
  var values = strings;
72163
- if (strings.every(utils.parseNumber)) {
74625
+ if (strings.every(str => utils.parseNumber(str) !== null)) {
72164
74626
  values = strings.map(function(str) {
72165
74627
  return +str;
72166
74628
  });
@@ -72293,11 +74755,12 @@ ${svg}
72293
74755
  var values, nullValue;
72294
74756
  var classifyByValue, classifyByRecordId;
72295
74757
  var numClasses, numValues;
72296
- var method;
74758
+ var method, diverging, divergingLayout;
72297
74759
 
72298
74760
  if (opts.color_scheme) {
72299
74761
  stop$1('color-scheme is not a valid option, use colors instead');
72300
74762
  }
74763
+ opts = Object.assign({}, opts, getInterpolationOptions(opts));
72301
74764
 
72302
74765
  // get data field to use for classification
72303
74766
  //
@@ -72332,17 +74795,35 @@ ${svg}
72332
74795
  //
72333
74796
  // expand categories if value is '*'
72334
74797
  // use all unique values if categories option is missing
74798
+ // (empty values are not categories: they get the null value)
72335
74799
  if (method == 'categorical') {
72336
74800
  if ((!opts.categories || opts.categories.includes('*')) && dataField) {
72337
- opts.categories = getUniqFieldValues(records, dataField);
74801
+ opts.categories = getUniqFieldValues(records, dataField).filter(isCategoryValue);
72338
74802
  }
72339
74803
  if (opts.categories && fieldType == 'number') {
72340
74804
  opts.categories = opts.categories.map(str => +str);
72341
74805
  }
72342
74806
  }
72343
74807
 
72344
- if (opts.classes) {
72345
- if (!utils.isInteger(opts.classes) || opts.classes > 1 === false) {
74808
+ diverging = isDivergingClassification(opts);
74809
+ if (diverging) {
74810
+ validateDivergingOptions(method, fieldType);
74811
+ } else if (opts.no_pivot_class || opts.pivot_class) {
74812
+ stop$1(opts.pivot_class ? 'pivot-class' : 'no-pivot-class', 'requires pivot= or pivot-range=');
74813
+ }
74814
+ if (Array.isArray(opts.classes) && !diverging) {
74815
+ if (opts.classes.length != 1) {
74816
+ stop$1('classes= takes two numbers (classes below and above the pivot) only with pivot=');
74817
+ }
74818
+ opts.classes = opts.classes[0];
74819
+ }
74820
+
74821
+ if (diverging) {
74822
+ // the classes depend on the data (see getDivergingLayout())
74823
+ numClasses = 0;
74824
+ } else if (opts.classes) {
74825
+ // continuous output can have a single interval, from the min to the max
74826
+ if (!utils.isInteger(opts.classes) || opts.classes >= (opts.continuous ? 1 : 2) === false) {
72346
74827
  stop$1('Invalid number of classes:', opts.classes, '(expected a value greater than 1)');
72347
74828
  }
72348
74829
  numClasses = opts.classes;
@@ -72355,24 +74836,37 @@ ${svg}
72355
74836
  } else if (method == 'categorical' && opts.categories) {
72356
74837
  numClasses = opts.categories.length;
72357
74838
  } else if (opts.colors && opts.colors.length > 1) {
72358
- numClasses = opts.colors.length;
74839
+ // continuous: each color is a stop, at the ends and breaks of the classes
74840
+ numClasses = opts.continuous ? opts.colors.length - 1 : opts.colors.length;
72359
74841
  } else if (opts.values && opts.values.length > 1) {
72360
- numClasses = opts.values.length;
74842
+ numClasses = opts.continuous ? opts.values.length - 1 : opts.values.length;
72361
74843
  } else if (method == 'non-adjacent') {
72362
74844
  numClasses = 5;
72363
74845
  } else {
72364
74846
  numClasses = 4;
72365
74847
  }
72366
74848
  numValues = opts.continuous ? numClasses + 1 : numClasses;
72367
- if (numValues > 1 === false) {
74849
+ if (numValues > 1 === false && !diverging) {
72368
74850
  stop$1('Missing a valid number of values');
72369
74851
  }
72370
74852
 
72371
74853
  // get colors or other values
72372
74854
  //
72373
- values = getClassValues(method, numValues, opts);
72374
- if (opts.invert) {
72375
- values = values.concat().reverse();
74855
+ if (diverging && fieldType === null) {
74856
+ values = []; // no data: every feature gets the null value
74857
+ } else if (diverging) {
74858
+ divergingLayout = getDivergingLayout(getDivergingData(records, dataField, opts), method,
74859
+ Object.assign({}, opts, {classes: getDivergingClassCount(opts)}));
74860
+ message(formatDivergingLayout(divergingLayout));
74861
+ if (opts.continuous) {
74862
+ message(formatDivergingStops(divergingLayout));
74863
+ }
74864
+ values = getDivergingClassValues(divergingLayout, opts);
74865
+ } else {
74866
+ values = getClassValues(method, numValues, opts);
74867
+ if (opts.invert) {
74868
+ values = values.concat().reverse();
74869
+ }
72376
74870
  }
72377
74871
  if (valuesAreColors) {
72378
74872
  message('Colors:', formatValuesForLogging(values));
@@ -72394,6 +74888,11 @@ ${svg}
72394
74888
  classifyByValue = getIndexedClassifier(values, nullValue, opts);
72395
74889
  } else if (method == 'categorical') {
72396
74890
  classifyByValue = getCategoricalClassifier(values, nullValue, opts);
74891
+ } else if (diverging && opts.continuous) {
74892
+ classifyByValue = getContinuousDivergingDataClassifier(divergingLayout, values, nullValue, opts);
74893
+ } else if (diverging) {
74894
+ classifyByValue = getSequentialClassifier$1(values, nullValue, getFieldValues(records, dataField), method,
74895
+ Object.assign({}, opts, {breaks: divergingLayout.breaks}));
72397
74896
  } else {
72398
74897
  classifyByValue = getSequentialClassifier$1(values, nullValue, getFieldValues(records, dataField), method, opts);
72399
74898
  }
@@ -72426,6 +74925,73 @@ ${svg}
72426
74925
  lyr.data.markSchemaChanged({operation: 'classify', field: outputField});
72427
74926
  };
72428
74927
 
74928
+ function validateDivergingOptions(method, fieldType, opts) {
74929
+ if (fieldType === null) return; // no data
74930
+ if (fieldType != 'number') {
74931
+ stop$1('pivot= requires a numeric data field');
74932
+ }
74933
+ if (!['quantile', 'equal-interval', 'nice', 'hybrid', 'breaks'].includes(method)) {
74934
+ stop$1('The', method, 'method does not support pivot=');
74935
+ }
74936
+ }
74937
+
74938
+ // Rounds and clamps data values as getDivergingData() does
74939
+ function getContinuousDivergingDataClassifier(layout, values, nullValue, opts) {
74940
+ var classify = getContinuousDivergingClassifier(layout, values, nullValue, opts);
74941
+ var round = opts.precision ? getRoundingFunction(opts.precision) : null;
74942
+ var range = opts.outer_breaks;
74943
+ return function(val) {
74944
+ if (!utils.isFiniteNumber(val)) return nullValue;
74945
+ if (round) val = round(val);
74946
+ if (range) val = Math.min(Math.max(val, range[0]), range[1]);
74947
+ return classify(val);
74948
+ };
74949
+ }
74950
+
74951
+ function formatDivergingStops(layout) {
74952
+ var stops = getDivergingStops(layout);
74953
+ var parts = [];
74954
+ if (stops.below.length > 0) parts.push('below: ' + stops.below.join(', '));
74955
+ if (stops.above.length > 0) parts.push('above: ' + stops.above.join(', '));
74956
+ return 'Color stops (' + parts.join('; ') + ')';
74957
+ }
74958
+
74959
+ // The data that the classes divide: numbers, rounded and clamped as the
74960
+ // sequential classifier does
74961
+ function getDivergingData(records, field, opts) {
74962
+ var values = getFieldValues(records, field);
74963
+ var ascending;
74964
+ if (opts.precision) {
74965
+ values = values.map(getRoundingFunction(opts.precision));
74966
+ }
74967
+ ascending = getAscendingNumbers(values);
74968
+ if (opts.outer_breaks) {
74969
+ ascending = applyDataRange(ascending, opts.outer_breaks);
74970
+ }
74971
+ return ascending;
74972
+ }
74973
+
74974
+ // The total number of classes (including a pivot class), or the numbers of
74975
+ // classes [below, above] the pivot
74976
+ function getDivergingClassCount(opts) {
74977
+ var hasNeutral = hasPivotClass(opts);
74978
+ var list = opts.colors && opts.colors.length > 1 && opts.colors ||
74979
+ opts.values && opts.values.length > 1 && opts.values;
74980
+ if (opts.classes) return opts.classes;
74981
+ if (list && opts.continuous) {
74982
+ // a list of 2K+1 colors has K stops on each side, for K-1 classes
74983
+ return Math.max(2, (Math.floor(list.length / 2) - 1) * 2) + (hasNeutral ? 1 : 0);
74984
+ }
74985
+ if (list) return list.length;
74986
+ return hasNeutral ? 7 : 6;
74987
+ }
74988
+
74989
+ // Like the categorical classifier, counts 0 as a value and other falsy
74990
+ // values (null, undefined, '', NaN) as empty
74991
+ function isCategoryValue(val) {
74992
+ return !!val || val === 0;
74993
+ }
74994
+
72429
74995
  function formatValuesForLogging(arr) {
72430
74996
  if (arr.some(val => utils.isString(val) && val.indexOf('rgb(') === 0)) {
72431
74997
  return formatColorsAsHex(arr);
@@ -85345,7 +87911,7 @@ ${svg}
85345
87911
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
85346
87912
  }
85347
87913
 
85348
- var version = "0.7.72";
87914
+ var version = "0.7.75";
85349
87915
 
85350
87916
  // Parse command line args into commands and run them
85351
87917
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -85736,392 +88302,6 @@ ${svg}
85736
88302
  testCommands: testCommands
85737
88303
  });
85738
88304
 
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
88305
  // Measuring a label's text outside a browser, from the font files installed on
86126
88306
  // this computer.
86127
88307
  //
@@ -87118,6 +89298,139 @@ ${svg}
87118
89298
  blurRasterLayers: blurRasterLayers
87119
89299
  });
87120
89300
 
89301
+ // A ramp is defined by pins: [{t, color}], where t is a position from 0 (left
89302
+ // end) to 1 (right end). Both ends must be pinned. Pins are stored by position
89303
+ // rather than by tile index so they survive changes in the number of tiles.
89304
+ // See docs/development/color-scheme-panel-design.md
89305
+
89306
+ // Returns an array of n colors. Pinned tiles take their pin's color; the
89307
+ // others are interpolated in OKLCH between the nearest pinned tiles, so that
89308
+ // lightness, chroma and hue each change by equal steps from tile to tile.
89309
+ // opts.vibrance: a midtone chroma boost between each
89310
+ // pair of pinned tiles (see getOklchInterpolator()).
89311
+ function resolveRamp(pins, n, opts) {
89312
+ return resolveRampTiles(pins, n, opts).map(function(tile) {
89313
+ return tile.color;
89314
+ });
89315
+ }
89316
+
89317
+ // Like resolveRamp(), but returns a {color, pinned, adjusted, l, c, h, ideal}
89318
+ // object for each tile. Tiles that had to be fitted to the sRGB gamut are
89319
+ // adjusted, and ideal has the OKLCH values they were fitted from.
89320
+ function resolveRampTiles(pins, n, opts) {
89321
+ var slots = getPinnedSlots(pins, n);
89322
+ var tiles = [];
89323
+ var left = 0, right, interpolate, tile;
89324
+ for (var i=0; i<n; i++) {
89325
+ if (slots[i] > -1) {
89326
+ // vibrance raises a pinned color's chroma only as far as the gamut has
89327
+ // room at its own lightness; it is adjusted if that cut the vibrance short
89328
+ tile = getVibrantTile(pins[slots[i]].color, opts && opts.vibrance);
89329
+ tile.pinned = true;
89330
+ tiles.push(tile);
89331
+ left = i;
89332
+ continue;
89333
+ }
89334
+ if (!interpolate || right < i) {
89335
+ right = i + 1;
89336
+ while (slots[right] == -1) right++;
89337
+ interpolate = getOklchInterpolator(pins[slots[left]].color, pins[slots[right]].color,
89338
+ Object.assign({}, opts, {steps: right - left}));
89339
+ }
89340
+ tile = interpolate((i - left) / (right - left));
89341
+ tile.pinned = false;
89342
+ tiles.push(tile);
89343
+ }
89344
+ return tiles;
89345
+ }
89346
+
89347
+ // Returns an array of n tile slots, each containing the index of the pin
89348
+ // that sets the tile's color, or -1 if the tile is interpolated.
89349
+ // When several pins snap to the same tile, an end pin wins, otherwise the pin
89350
+ // closest to the tile's own position.
89351
+ function getPinnedSlots(pins, n) {
89352
+ var slots = [], dists = [];
89353
+ validateRamp(pins, n);
89354
+ for (var i=0; i<n; i++) {
89355
+ slots.push(-1);
89356
+ dists.push(Infinity);
89357
+ }
89358
+ pins.forEach(function(pin, pinId) {
89359
+ var k = pin.t * (n - 1);
89360
+ var slot = Math.round(k);
89361
+ var dist = isEndPin(pin) ? -1 : Math.abs(k - slot);
89362
+ if (dist < dists[slot]) {
89363
+ slots[slot] = pinId;
89364
+ dists[slot] = dist;
89365
+ }
89366
+ });
89367
+ return slots;
89368
+ }
89369
+
89370
+ // Returns a new pin array with tile i of an n-tile ramp pinned to color,
89371
+ // replacing any pins that snap to the same tile.
89372
+ function setRampPin(pins, n, i, color) {
89373
+ validateTileIndex(i, n);
89374
+ if (!parseColor(color)) stop$1('Unsupported color:', color);
89375
+ return removePinsAtSlot(pins, n, i)
89376
+ .concat({t: getTilePosition(i, n), color: color})
89377
+ .sort(function(a, b) { return a.t - b.t; });
89378
+ }
89379
+
89380
+ // Returns a new pin array with tile i unpinned. End tiles stay pinned.
89381
+ function clearRampPin(pins, n, i) {
89382
+ validateTileIndex(i, n);
89383
+ if (i === 0 || i == n - 1) return pins.concat();
89384
+ return removePinsAtSlot(pins, n, i);
89385
+ }
89386
+
89387
+ function removePinsAtSlot(pins, n, i) {
89388
+ validateRamp(pins, n);
89389
+ return pins.filter(function(pin) {
89390
+ return Math.round(pin.t * (n - 1)) != i;
89391
+ });
89392
+ }
89393
+
89394
+ function getTilePosition(i, n) {
89395
+ return i == n - 1 ? 1 : i / (n - 1);
89396
+ }
89397
+
89398
+ function isEndPin(pin) {
89399
+ return pin.t === 0 || pin.t === 1;
89400
+ }
89401
+
89402
+ function validateTileIndex(i, n) {
89403
+ if (!(i >= 0 && i < n && Math.floor(i) === i)) {
89404
+ stop$1('Invalid ramp tile index:', i);
89405
+ }
89406
+ }
89407
+
89408
+ function validateRamp(pins, n) {
89409
+ if (!(n >= 2 && Math.floor(n) === n)) {
89410
+ stop$1('Invalid number of ramp colors:', n);
89411
+ }
89412
+ if (!Array.isArray(pins)) {
89413
+ stop$1('Expected an array of ramp pins');
89414
+ }
89415
+ pins.forEach(function(pin) {
89416
+ if (!(pin.t >= 0 && pin.t <= 1)) stop$1('Invalid ramp pin position:', pin.t);
89417
+ if (!parseColor(pin.color)) stop$1('Unsupported color:', pin.color);
89418
+ });
89419
+ if (!pins.some(function(pin) { return pin.t === 0; }) ||
89420
+ !pins.some(function(pin) { return pin.t === 1; })) {
89421
+ stop$1('A ramp must have pins at both ends');
89422
+ }
89423
+ }
89424
+
89425
+ var ColorRamps = /*#__PURE__*/Object.freeze({
89426
+ __proto__: null,
89427
+ clearRampPin: clearRampPin,
89428
+ getPinnedSlots: getPinnedSlots,
89429
+ resolveRamp: resolveRamp,
89430
+ resolveRampTiles: resolveRampTiles,
89431
+ setRampPin: setRampPin
89432
+ });
89433
+
87121
89434
  function getPolygonRewinder(nodes) {
87122
89435
  var splitter = getSelfIntersectionSplitter(nodes);
87123
89436
  return rewindPolygon;
@@ -87958,10 +90271,10 @@ ${svg}
87958
90271
  // Vertices never move once they are placed.
87959
90272
  //
87960
90273
  // 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
90274
+ // with -smooth's Gaussian-weighted quadratic fit, in arc length. The quadratic
90275
+ // keeps small bends from being flattened the way a plain weighted average
90276
+ // flattens them, at the cost of letting a little more of the jitter of a slow
90277
+ // stroke through. A smoothed point depends only on the trace within a window around it, so it
87965
90278
  // is final as soon as the trace extends a window radius beyond it. At the
87966
90279
  // start of the stroke the trace is extended by an odd reflection about its
87967
90280
  // first point, which keeps the smoothed curve starting exactly there, and the
@@ -87976,14 +90289,15 @@ ${svg}
87976
90289
  // Distances are in pixels. For samples in other units, the pixelSize option
87977
90290
  // gives the size of a pixel in those units.
87978
90291
 
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.
90292
+ // Standard deviation of the smoothing kernel, in arc length. Smaller values
90293
+ // follow the hand more closely, which gives a feeling of control; larger ones
90294
+ // are smoother but feel less precise and place vertices further behind the
90295
+ // pointer.
87982
90296
  var SIGMA = 8;
87983
90297
 
87984
90298
  // Half-width of the smoothing window, in multiples of SIGMA. This is also how
87985
90299
  // far the pointer has to be ahead of a smoothed point for it to be final.
87986
- var WINDOW_RADIUS = 3;
90300
+ var WINDOW_RADIUS = 4;
87987
90301
 
87988
90302
  // Spacing of the densified trace, and of the smoothed points that are tested
87989
90303
  // as vertices. Small enough that a bend of radius SIGMA turns by only a few
@@ -88004,10 +90318,16 @@ ${svg}
88004
90318
  var PREVIEW_SPACING = 2;
88005
90319
  var PREVIEW_SIGMA = 2;
88006
90320
 
90321
+ // Weight of -smooth's quadratic correction, which keeps bends from being
90322
+ // flattened: 0 is the plain weighted average, 1 the fully corrected fit.
90323
+ // At 1, small bends keep most of their amplitude.
90324
+ var GAIN = 1;
90325
+
88007
90326
  // start: [x, y] first sample
88008
- // opts: (optional) {sigma, previewSigma, bendAngle (degrees), maxDeviation, pixelSize}
90327
+ // opts: (optional) {sigma, previewSigma, gain, bendAngle (degrees), maxDeviation, pixelSize}
88009
90328
  function GaussianStrokeFitter(start, opts) {
88010
90329
  var px = opts && opts.pixelSize > 0 ? opts.pixelSize : 1;
90330
+ var gain = opts && opts.gain >= 0 ? opts.gain : GAIN;
88011
90331
  var sigma = getOpt(opts, 'sigma', SIGMA) * px;
88012
90332
  var radius = sigma * WINDOW_RADIUS;
88013
90333
  var previewSigma = Math.min(getOpt(opts, 'previewSigma', PREVIEW_SIGMA) * px, sigma);
@@ -88111,7 +90431,7 @@ ${svg}
88111
90431
  wy.push(2 * end[1] - yy[i]);
88112
90432
  }
88113
90433
  }
88114
- return smoothPoint(wt, [wx, wy], 0, wt.length, t, 'gaussian', s || sigma, r, 0);
90434
+ return smoothPoint(wt, [wx, wy], 0, wt.length, t, 'gaussian', s || sigma, r, gain);
88115
90435
  }
88116
90436
 
88117
90437
  // first index of the trace with arc length > @t
@@ -88270,12 +90590,17 @@ ${svg}
88270
90590
  ClipErase,
88271
90591
  ClipPoints,
88272
90592
  ColorUtils,
90593
+ ColorRamps,
90594
+ ColorSchemes,
90595
+ Oklab,
88273
90596
  Colorizer,
88274
90597
  CustomProjections,
88275
90598
  Dymaxion,
88276
90599
  ButterflyProjections,
88277
90600
  Narukawa2022,
88278
90601
  DataAggregation,
90602
+ Diverging,
90603
+ {getSequentialBreaks, getInterpolatedValueGetter},
88279
90604
  DatasetUtils,
88280
90605
  DataUtils,
88281
90606
  DbfImport,