graphein 0.13.0 → 0.14.0

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/dist/index.js CHANGED
@@ -6073,6 +6073,7 @@ function resolveSketch(spec) {
6073
6073
 
6074
6074
  // src/layout/frame.ts
6075
6075
  var TICK_SIZE = 6;
6076
+ var NO_PADDING = { top: 0, right: 0, bottom: 0, left: 0 };
6076
6077
  var AXIS_LABEL_GAP = 6;
6077
6078
  var AXIS_TITLE_GAP = 4;
6078
6079
  var LEGEND_SWATCH = 11;
@@ -6691,6 +6692,155 @@ function drawLegend(surface, items, model) {
6691
6692
  }
6692
6693
  }
6693
6694
 
6695
+ // src/charts/emphasis.ts
6696
+ function rowAlpha(emphasis, row) {
6697
+ if (!emphasis) return 1;
6698
+ return emphasis.match(row) ? 1 : emphasis.dim;
6699
+ }
6700
+ function seriesEmphasisMode(emphasis, series) {
6701
+ if (!emphasis) return "all";
6702
+ let matched = 0;
6703
+ for (const row of series.rows) if (emphasis.match(row)) matched++;
6704
+ if (matched === 0) return "none";
6705
+ if (matched === series.rows.length) return "all";
6706
+ return "partial";
6707
+ }
6708
+
6709
+ // src/charts/scatter.ts
6710
+ var QUANTIZE_MIN_POINTS = 4e3;
6711
+ var QUANTIZE_AREA_DIVISOR = 4;
6712
+ var FIXED_RADIUS = 3.75;
6713
+ var MIN_SIZED_RADIUS = 3;
6714
+ function maxSizedRadius(plotSize) {
6715
+ return clamp(plotSize * 0.05, 10, 26);
6716
+ }
6717
+ function maxScatterRadius(spec, plotW, plotH) {
6718
+ return spec.encoding.size?.field ? maxSizedRadius(Math.min(plotW, plotH)) : FIXED_RADIUS;
6719
+ }
6720
+ function isMissing(value) {
6721
+ return value == null || value === "";
6722
+ }
6723
+ function isInvalidPositionValue(value) {
6724
+ return isMissing(value) || typeof value === "number" && Number.isNaN(value) || value instanceof Date && Number.isNaN(value.getTime());
6725
+ }
6726
+ function fixedRadius() {
6727
+ return FIXED_RADIUS;
6728
+ }
6729
+ function shouldQuantize(count, plot, hasSize) {
6730
+ return !hasSize && count > QUANTIZE_MIN_POINTS && count > plot.width * plot.height / QUANTIZE_AREA_DIVISOR;
6731
+ }
6732
+ function quantizePoints(points) {
6733
+ if (points.length === 0) return [];
6734
+ let minX = Infinity;
6735
+ let maxX = -Infinity;
6736
+ let minY = Infinity;
6737
+ let maxY = -Infinity;
6738
+ for (const p of points) {
6739
+ if (p.x < minX) minX = p.x;
6740
+ if (p.x > maxX) maxX = p.x;
6741
+ if (p.y < minY) minY = p.y;
6742
+ if (p.y > maxY) maxY = p.y;
6743
+ }
6744
+ const seen = /* @__PURE__ */ new Set();
6745
+ const keep = [];
6746
+ for (const p of points) {
6747
+ const extentPoint = p.x === minX || p.x === maxX || p.y === minY || p.y === maxY;
6748
+ const ix = Math.round(p.x);
6749
+ const iy = Math.round(p.y);
6750
+ const key = `${p.seriesKey}|${ix}|${iy}`;
6751
+ if (extentPoint || !seen.has(key)) {
6752
+ keep.push(p);
6753
+ seen.add(key);
6754
+ }
6755
+ }
6756
+ return keep;
6757
+ }
6758
+ function radiusScale(rows, field, plotSize) {
6759
+ const rMin = MIN_SIZED_RADIUS;
6760
+ const rMax = maxSizedRadius(plotSize);
6761
+ const mid = (rMin + rMax) / 2;
6762
+ const domain = extent(rows, field);
6763
+ if (!domain) return () => mid;
6764
+ const [min, max] = domain;
6765
+ if (min === max) return () => mid;
6766
+ return (value) => {
6767
+ const n = toNumber(value);
6768
+ if (Number.isNaN(n)) return rMin;
6769
+ const norm = clamp((n - min) / (max - min), 0, 1);
6770
+ return rMin + (rMax - rMin) * Math.sqrt(norm);
6771
+ };
6772
+ }
6773
+ function drawScatter(surface, model) {
6774
+ const ctx = surface.marks.ctx;
6775
+ const spec = model.spec;
6776
+ const { plot, tokens } = model;
6777
+ const readX = accessor(model.x.field);
6778
+ const readY = accessor(model.y.field ?? "");
6779
+ const sizeField = spec.encoding.size?.field;
6780
+ const allRows = model.series.flatMap((series) => series.rows);
6781
+ const readSize = sizeField ? accessor(sizeField) : null;
6782
+ const radius = sizeField ? radiusScale(allRows, sizeField, Math.min(plot.width, plot.height)) : fixedRadius;
6783
+ const points = [];
6784
+ for (const series of model.series) {
6785
+ for (const row of series.rows) {
6786
+ const xValue = readX(row);
6787
+ const yValue = readY(row);
6788
+ const yNumber = toNumber(yValue);
6789
+ const px = model.x.pixel(xValue);
6790
+ if (px == null || isInvalidPositionValue(xValue) || isMissing(yValue) || Number.isNaN(yNumber)) {
6791
+ continue;
6792
+ }
6793
+ const py = model.y.pixel(yValue);
6794
+ if (!Number.isFinite(px) || !Number.isFinite(py)) continue;
6795
+ points.push({
6796
+ x: px,
6797
+ y: py,
6798
+ r: readSize ? radius(readSize(row)) : radius(),
6799
+ color: series.color,
6800
+ alpha: rowAlpha(model.emphasis, row),
6801
+ seriesKey: series.key
6802
+ });
6803
+ }
6804
+ }
6805
+ const originalCount = points.length;
6806
+ const drawPoints = shouldQuantize(originalCount, plot, !!sizeField) ? quantizePoints(points) : points;
6807
+ model.markStats = { drawn: drawPoints.length, original: originalCount };
6808
+ if (sizeField) drawPoints.sort((a, b) => b.r - a.r);
6809
+ ctx.save();
6810
+ ctx.beginPath();
6811
+ ctx.rect(plot.x, plot.y, plot.width, plot.height);
6812
+ ctx.clip();
6813
+ if (model.sketch) {
6814
+ const pen = new RoughPen(ctx, model.sketch);
6815
+ for (const point of drawPoints) {
6816
+ const prev = ctx.globalAlpha;
6817
+ if (point.alpha !== 1) ctx.globalAlpha = point.alpha;
6818
+ pen.circle(point.x, point.y, point.r, {
6819
+ fill: point.color,
6820
+ fillAlpha: 0.7,
6821
+ stroke: tokens.color.background,
6822
+ strokeWidth: 1
6823
+ });
6824
+ if (point.alpha !== 1) ctx.globalAlpha = prev;
6825
+ }
6826
+ ctx.restore();
6827
+ return;
6828
+ }
6829
+ ctx.lineWidth = 1;
6830
+ ctx.strokeStyle = tokens.color.background;
6831
+ for (const point of drawPoints) {
6832
+ ctx.beginPath();
6833
+ ctx.arc(point.x, point.y, point.r, 0, Math.PI * 2);
6834
+ ctx.fillStyle = point.color;
6835
+ ctx.globalAlpha = 0.7 * point.alpha;
6836
+ ctx.fill();
6837
+ ctx.globalAlpha = point.alpha;
6838
+ ctx.stroke();
6839
+ }
6840
+ ctx.globalAlpha = 1;
6841
+ ctx.restore();
6842
+ }
6843
+
6694
6844
  // src/runtime/cartesian.ts
6695
6845
  var CURVES = {
6696
6846
  linear: curveLinear,
@@ -6781,8 +6931,43 @@ function formatNumericTick(value, userFormat) {
6781
6931
  if (userFormat) return formatValue(value, userFormat);
6782
6932
  return formatNumber2(value, ",");
6783
6933
  }
6784
- var DEFAULT_PADDING = { top: 12, right: 16, bottom: 12, left: 12 };
6785
6934
  var LEGEND_SWATCH2 = 11;
6935
+ var SCATTER_EDGE_MARGIN = 2;
6936
+ var ZERO_INSETS = { top: 0, right: 0, bottom: 0, left: 0 };
6937
+ function annotationEdgeInsets(spec, yDomain, fontFamily, fontSize, fontWeight) {
6938
+ const explicit = spec.annotations;
6939
+ const points = Array.isArray(explicit) ? explicit.filter((a) => a.type === "point") : [];
6940
+ for (const a of autoInsightAnnotations(spec)) points.push(a);
6941
+ if (points.length === 0) return ZERO_INSETS;
6942
+ const span = yDomain[1] - yDomain[0] || 1;
6943
+ const font = fontString(fontSize, fontFamily, fontWeight);
6944
+ let maxR = 0;
6945
+ let maxLabelHalf = 0;
6946
+ let anyLabel = false;
6947
+ let maxFy = -Infinity;
6948
+ let minFy = Infinity;
6949
+ for (const ann of points) {
6950
+ const r = ann.markerRadius ?? 3.5;
6951
+ if (r > maxR) maxR = r;
6952
+ if (ann.label) {
6953
+ anyLabel = true;
6954
+ const half = (measureText(ann.label, font).width + 6) / 2;
6955
+ if (half > maxLabelHalf) maxLabelHalf = half;
6956
+ }
6957
+ const yv = toNumber(ann.y);
6958
+ if (!Number.isNaN(yv)) {
6959
+ const fy = (yv - yDomain[0]) / span;
6960
+ if (fy > maxFy) maxFy = fy;
6961
+ if (fy < minFy) minFy = fy;
6962
+ }
6963
+ }
6964
+ const halo = maxR + 1.6;
6965
+ const gap = maxR + 4;
6966
+ const hReserve = Math.ceil(Math.max(maxLabelHalf, halo)) + 2;
6967
+ const top = maxFy >= 0.5 ? Math.ceil(anyLabel ? gap + fontSize : halo) + 2 : 0;
6968
+ const bottom = minFy <= 0.5 ? Math.ceil(halo) + 2 : 0;
6969
+ return { top, right: hReserve, bottom, left: hReserve };
6970
+ }
6786
6971
  function legendInteractive(spec) {
6787
6972
  return typeof spec.legend === "object" && spec.legend.interactive === true;
6788
6973
  }
@@ -6947,7 +7132,7 @@ function buildCartesianModel(spec, tokens, opts) {
6947
7132
  color: s.color,
6948
7133
  symbol: spec.type === "scatter" ? "circle" : spec.type === "line" ? "line" : "square"
6949
7134
  }));
6950
- const padding = { ...DEFAULT_PADDING, ...spec.padding };
7135
+ const padding = { ...NO_PADDING, ...spec.padding };
6951
7136
  const titleInput4 = resolveTitle(spec);
6952
7137
  const categoryAxisInput = {
6953
7138
  show: xShow,
@@ -6990,7 +7175,18 @@ function buildCartesianModel(spec, tokens, opts) {
6990
7175
  };
6991
7176
  }) : void 0;
6992
7177
  const plot = frame.plot;
6993
- const valueRange = horizontal ? [plot.x, plot.x + plot.width] : [plot.y + plot.height, plot.y];
7178
+ const scatterInset = spec.type === "scatter" ? Math.min(
7179
+ maxScatterRadius(spec, plot.width, plot.height) + SCATTER_EDGE_MARGIN,
7180
+ Math.min(plot.width, plot.height) * 0.4
7181
+ ) : 0;
7182
+ const annIns = horizontal ? ZERO_INSETS : annotationEdgeInsets(spec, yDomain, tokens.font.family, tokens.font.size.small, tokens.font.weight.medium);
7183
+ const capV = plot.height * 0.4;
7184
+ const capH = plot.width * 0.4;
7185
+ const insTop = Math.min(Math.max(scatterInset, annIns.top), capV);
7186
+ const insBottom = Math.min(Math.max(scatterInset, annIns.bottom), capV);
7187
+ const insLeft = Math.min(Math.max(scatterInset, annIns.left), capH);
7188
+ const insRight = Math.min(Math.max(scatterInset, annIns.right), capH);
7189
+ const valueRange = horizontal ? [plot.x + insLeft, plot.x + plot.width - insRight] : [plot.y + plot.height - insBottom, plot.y + insTop];
6994
7190
  const yScale = yIsLog ? logScale({ domain: yDomain, range: valueRange, base: yBase, clamp: yClamp }) : linearScale({ domain: yDomain, range: valueRange, clamp: yClamp });
6995
7191
  const yPixel = (value) => yScale.map(toNumber(value));
6996
7192
  const valueMin = horizontal ? plot.x : plot.y;
@@ -7001,7 +7197,7 @@ function buildCartesianModel(spec, tokens, opts) {
7001
7197
  type: xType,
7002
7198
  categories,
7003
7199
  domainNum: xDomainNum,
7004
- range: horizontal ? [plot.y, plot.y + plot.height] : [plot.x, plot.x + plot.width],
7200
+ range: horizontal ? [plot.y + insTop, plot.y + plot.height - insBottom] : [plot.x + insLeft, plot.x + plot.width - insRight],
7005
7201
  paddingInner: spec.type === "bar" ? 0.2 : spec.type === "box" ? 0.32 : 0.5,
7006
7202
  scaleCfg: enc.x.scale
7007
7203
  });
@@ -7128,21 +7324,32 @@ function maxFinite(values, fallback) {
7128
7324
  return max === -Infinity ? fallback : max;
7129
7325
  }
7130
7326
 
7131
- // src/charts/emphasis.ts
7132
- function rowAlpha(emphasis, row) {
7133
- if (!emphasis) return 1;
7134
- return emphasis.match(row) ? 1 : emphasis.dim;
7327
+ // src/render/metrics.ts
7328
+ function diagonal(plot) {
7329
+ return Math.hypot(plot.width, plot.height);
7135
7330
  }
7136
- function seriesEmphasisMode(emphasis, series) {
7137
- if (!emphasis) return "all";
7138
- let matched = 0;
7139
- for (const row of series.rows) if (emphasis.match(row)) matched++;
7140
- if (matched === 0) return "none";
7141
- if (matched === series.rows.length) return "all";
7142
- return "partial";
7331
+ function lineStrokeWidth(plot, base = 2) {
7332
+ return clamp(diagonal(plot) / 260, base, 4.5);
7333
+ }
7334
+ function markerRadius(plot, count, base = 3) {
7335
+ const bySize = diagonal(plot) / 150;
7336
+ const spacing = plot.width / Math.max(2, count);
7337
+ const bySpace = spacing * 0.34;
7338
+ return clamp(Math.min(bySize, bySpace), Math.min(base, 2.5), 8);
7339
+ }
7340
+ function markerStroke(radius) {
7341
+ return clamp(radius * 0.32, 1, 2.5);
7143
7342
  }
7144
7343
 
7145
7344
  // src/charts/line.ts
7345
+ function maxSeriesLength(allSeries) {
7346
+ let n = 0;
7347
+ for (const s of allSeries) {
7348
+ const finite4 = s.points.filter((p) => !Number.isNaN(p.x) && !Number.isNaN(p.y)).length;
7349
+ if (finite4 > n) n = finite4;
7350
+ }
7351
+ return n;
7352
+ }
7146
7353
  function buildSeriesPoints(model, s) {
7147
7354
  const readX = accessor(model.x.field);
7148
7355
  const readY = accessor(model.y.field ?? "");
@@ -7199,7 +7406,7 @@ function finiteRuns(points) {
7199
7406
  if (run.length) runs.push(run);
7200
7407
  return runs;
7201
7408
  }
7202
- function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
7409
+ function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints, strokeWidth, markerR, markerSW) {
7203
7410
  if (filled) {
7204
7411
  for (const { series, points, alpha } of allSeries) {
7205
7412
  const prev = ctx.globalAlpha;
@@ -7218,7 +7425,7 @@ function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
7218
7425
  for (const { series, points, alpha } of allSeries) {
7219
7426
  const prev = ctx.globalAlpha;
7220
7427
  if (alpha !== 1) ctx.globalAlpha = alpha;
7221
- pen.trendStroke(points, { stroke: series.color });
7428
+ pen.trendStroke(points, { stroke: series.color, strokeWidth });
7222
7429
  if (alpha !== 1) ctx.globalAlpha = prev;
7223
7430
  }
7224
7431
  if (showPoints) {
@@ -7227,10 +7434,10 @@ function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
7227
7434
  if (alpha !== 1) ctx.globalAlpha = alpha;
7228
7435
  for (const p of points) {
7229
7436
  if (Number.isNaN(p.x) || Number.isNaN(p.y)) continue;
7230
- pen.circle(p.x, p.y, 3.4, {
7437
+ pen.circle(p.x, p.y, markerR, {
7231
7438
  fill: series.color,
7232
7439
  stroke: model.tokens.color.background,
7233
- strokeWidth: 1.5
7440
+ strokeWidth: markerSW
7234
7441
  });
7235
7442
  }
7236
7443
  if (alpha !== 1) ctx.globalAlpha = prev;
@@ -7240,10 +7447,10 @@ function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
7240
7447
  if (mode !== "partial") continue;
7241
7448
  ctx.globalAlpha = 1;
7242
7449
  for (const p of matchPoints) {
7243
- pen.circle(p.x, p.y, 3.4, {
7450
+ pen.circle(p.x, p.y, markerR, {
7244
7451
  fill: series.color,
7245
7452
  stroke: model.tokens.color.background,
7246
- strokeWidth: 1.5
7453
+ strokeWidth: markerSW
7247
7454
  });
7248
7455
  }
7249
7456
  }
@@ -7267,12 +7474,15 @@ function drawLine(surface, model) {
7267
7474
  };
7268
7475
  });
7269
7476
  const showPoints = spec.points || allSeries.some((s) => s.points.filter((p) => !Number.isNaN(p.x)).length === 1);
7477
+ const strokeWidth = lineStrokeWidth(plot, Math.max(1.75, tokens.stroke.thick));
7478
+ const markerR = markerRadius(plot, maxSeriesLength(allSeries));
7479
+ const markerSW = markerStroke(markerR);
7270
7480
  ctx.save();
7271
7481
  ctx.beginPath();
7272
7482
  ctx.rect(plot.x, plot.y, plot.width, plot.height);
7273
7483
  ctx.clip();
7274
7484
  if (model.sketch) {
7275
- drawLineSketch(ctx, model, allSeries, new RoughPen(ctx, model.sketch), Boolean(areaGen), showPoints);
7485
+ drawLineSketch(ctx, model, allSeries, new RoughPen(ctx, model.sketch), Boolean(areaGen), showPoints, strokeWidth, markerR, markerSW);
7276
7486
  ctx.restore();
7277
7487
  return;
7278
7488
  }
@@ -7295,7 +7505,7 @@ function drawLine(surface, model) {
7295
7505
  }
7296
7506
  ctx.lineJoin = "round";
7297
7507
  ctx.lineCap = "round";
7298
- ctx.lineWidth = Math.max(1.75, tokens.stroke.thick);
7508
+ ctx.lineWidth = strokeWidth;
7299
7509
  for (const { series, points, alpha } of allSeries) {
7300
7510
  ctx.beginPath();
7301
7511
  lineGen(points, ctx);
@@ -7308,12 +7518,12 @@ function drawLine(surface, model) {
7308
7518
  for (const { series, points, alpha } of allSeries) {
7309
7519
  ctx.fillStyle = series.color;
7310
7520
  ctx.strokeStyle = tokens.color.background;
7311
- ctx.lineWidth = 1.5;
7521
+ ctx.lineWidth = markerSW;
7312
7522
  ctx.globalAlpha = alpha;
7313
7523
  for (const p of points) {
7314
7524
  if (Number.isNaN(p.x) || Number.isNaN(p.y)) continue;
7315
7525
  ctx.beginPath();
7316
- ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
7526
+ ctx.arc(p.x, p.y, markerR, 0, Math.PI * 2);
7317
7527
  ctx.fill();
7318
7528
  ctx.stroke();
7319
7529
  }
@@ -7324,11 +7534,11 @@ function drawLine(surface, model) {
7324
7534
  if (mode !== "partial") continue;
7325
7535
  ctx.fillStyle = series.color;
7326
7536
  ctx.strokeStyle = tokens.color.background;
7327
- ctx.lineWidth = 1.5;
7537
+ ctx.lineWidth = markerSW;
7328
7538
  ctx.globalAlpha = 1;
7329
7539
  for (const p of matchPoints) {
7330
7540
  ctx.beginPath();
7331
- ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
7541
+ ctx.arc(p.x, p.y, markerR, 0, Math.PI * 2);
7332
7542
  ctx.fill();
7333
7543
  ctx.stroke();
7334
7544
  }
@@ -7338,6 +7548,14 @@ function drawLine(surface, model) {
7338
7548
  }
7339
7549
 
7340
7550
  // src/charts/area.ts
7551
+ function maxBandLength(bands) {
7552
+ let n = 0;
7553
+ for (const b of bands) {
7554
+ const finite4 = b.points.filter((p) => !Number.isNaN(p.x) && !Number.isNaN(p.y1)).length;
7555
+ if (finite4 > n) n = finite4;
7556
+ }
7557
+ return n;
7558
+ }
7341
7559
  function buildBands(model, stacked) {
7342
7560
  const readX = accessor(model.x.field);
7343
7561
  const readY = accessor(model.y.field ?? "");
@@ -7387,7 +7605,7 @@ function buildBands(model, stacked) {
7387
7605
  return { series, points: reduced, alpha: mode === "all" ? 1 : model.emphasis?.dim ?? 1, mode, matchPoints };
7388
7606
  });
7389
7607
  }
7390
- function drawAreaSketch(ctx, model, bands, pen, stacked) {
7608
+ function drawAreaSketch(ctx, model, bands, pen, stacked, strokeWidth, markerR, markerSW) {
7391
7609
  for (const { series, points, alpha } of bands) {
7392
7610
  const prev = ctx.globalAlpha;
7393
7611
  if (alpha !== 1) ctx.globalAlpha = alpha;
@@ -7415,7 +7633,7 @@ function drawAreaSketch(ctx, model, bands, pen, stacked) {
7415
7633
  if (alpha !== 1) ctx.globalAlpha = alpha;
7416
7634
  pen.trendStroke(
7417
7635
  points.map((p) => ({ x: p.x, y: p.y1 })),
7418
- { stroke: series.color }
7636
+ { stroke: series.color, strokeWidth }
7419
7637
  );
7420
7638
  if (alpha !== 1) ctx.globalAlpha = prev;
7421
7639
  }
@@ -7423,10 +7641,10 @@ function drawAreaSketch(ctx, model, bands, pen, stacked) {
7423
7641
  if (mode !== "partial") continue;
7424
7642
  ctx.globalAlpha = 1;
7425
7643
  for (const p of matchPoints) {
7426
- pen.circle(p.x, p.y, 3.4, {
7644
+ pen.circle(p.x, p.y, markerR, {
7427
7645
  fill: series.color,
7428
7646
  stroke: model.tokens.color.background,
7429
- strokeWidth: 1.5
7647
+ strokeWidth: markerSW
7430
7648
  });
7431
7649
  }
7432
7650
  }
@@ -7441,12 +7659,15 @@ function drawArea(surface, model) {
7441
7659
  const lineGen = line({ curve });
7442
7660
  const stacked = model.stacked;
7443
7661
  const bands = buildBands(model, stacked);
7662
+ const strokeWidth = lineStrokeWidth(plot, Math.max(1.5, tokens.stroke.base));
7663
+ const markerR = markerRadius(plot, maxBandLength(bands));
7664
+ const markerSW = markerStroke(markerR);
7444
7665
  ctx.save();
7445
7666
  ctx.beginPath();
7446
7667
  ctx.rect(plot.x, plot.y, plot.width, plot.height);
7447
7668
  ctx.clip();
7448
7669
  if (model.sketch) {
7449
- drawAreaSketch(ctx, model, bands, new RoughPen(ctx, model.sketch), stacked);
7670
+ drawAreaSketch(ctx, model, bands, new RoughPen(ctx, model.sketch), stacked, strokeWidth, markerR, markerSW);
7450
7671
  ctx.restore();
7451
7672
  return;
7452
7673
  }
@@ -7468,7 +7689,7 @@ function drawArea(surface, model) {
7468
7689
  }
7469
7690
  ctx.lineJoin = "round";
7470
7691
  ctx.lineCap = "round";
7471
- ctx.lineWidth = Math.max(1.5, tokens.stroke.base);
7692
+ ctx.lineWidth = strokeWidth;
7472
7693
  for (const { series, points, alpha } of bands) {
7473
7694
  ctx.beginPath();
7474
7695
  lineGen(
@@ -7484,11 +7705,11 @@ function drawArea(surface, model) {
7484
7705
  if (mode !== "partial") continue;
7485
7706
  ctx.fillStyle = series.color;
7486
7707
  ctx.strokeStyle = tokens.color.background;
7487
- ctx.lineWidth = 1.5;
7708
+ ctx.lineWidth = markerSW;
7488
7709
  ctx.globalAlpha = 1;
7489
7710
  for (const p of matchPoints) {
7490
7711
  ctx.beginPath();
7491
- ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
7712
+ ctx.arc(p.x, p.y, markerR, 0, Math.PI * 2);
7492
7713
  ctx.fill();
7493
7714
  ctx.stroke();
7494
7715
  }
@@ -7632,133 +7853,6 @@ function drawBar(surface, model) {
7632
7853
  ctx.restore();
7633
7854
  }
7634
7855
 
7635
- // src/charts/scatter.ts
7636
- var QUANTIZE_MIN_POINTS = 4e3;
7637
- var QUANTIZE_AREA_DIVISOR = 4;
7638
- function isMissing(value) {
7639
- return value == null || value === "";
7640
- }
7641
- function isInvalidPositionValue(value) {
7642
- return isMissing(value) || typeof value === "number" && Number.isNaN(value) || value instanceof Date && Number.isNaN(value.getTime());
7643
- }
7644
- function fixedRadius() {
7645
- return 3.75;
7646
- }
7647
- function shouldQuantize(count, plot, hasSize) {
7648
- return !hasSize && count > QUANTIZE_MIN_POINTS && count > plot.width * plot.height / QUANTIZE_AREA_DIVISOR;
7649
- }
7650
- function quantizePoints(points) {
7651
- if (points.length === 0) return [];
7652
- let minX = Infinity;
7653
- let maxX = -Infinity;
7654
- let minY = Infinity;
7655
- let maxY = -Infinity;
7656
- for (const p of points) {
7657
- if (p.x < minX) minX = p.x;
7658
- if (p.x > maxX) maxX = p.x;
7659
- if (p.y < minY) minY = p.y;
7660
- if (p.y > maxY) maxY = p.y;
7661
- }
7662
- const seen = /* @__PURE__ */ new Set();
7663
- const keep = [];
7664
- for (const p of points) {
7665
- const extentPoint = p.x === minX || p.x === maxX || p.y === minY || p.y === maxY;
7666
- const ix = Math.round(p.x);
7667
- const iy = Math.round(p.y);
7668
- const key = `${p.seriesKey}|${ix}|${iy}`;
7669
- if (extentPoint || !seen.has(key)) {
7670
- keep.push(p);
7671
- seen.add(key);
7672
- }
7673
- }
7674
- return keep;
7675
- }
7676
- function radiusScale(rows, field, plotSize) {
7677
- const rMin = 3;
7678
- const rMax = clamp(plotSize * 0.05, 10, 26);
7679
- const mid = (rMin + rMax) / 2;
7680
- const domain = extent(rows, field);
7681
- if (!domain) return () => mid;
7682
- const [min, max] = domain;
7683
- if (min === max) return () => mid;
7684
- return (value) => {
7685
- const n = toNumber(value);
7686
- if (Number.isNaN(n)) return rMin;
7687
- const norm = clamp((n - min) / (max - min), 0, 1);
7688
- return rMin + (rMax - rMin) * Math.sqrt(norm);
7689
- };
7690
- }
7691
- function drawScatter(surface, model) {
7692
- const ctx = surface.marks.ctx;
7693
- const spec = model.spec;
7694
- const { plot, tokens } = model;
7695
- const readX = accessor(model.x.field);
7696
- const readY = accessor(model.y.field ?? "");
7697
- const sizeField = spec.encoding.size?.field;
7698
- const allRows = model.series.flatMap((series) => series.rows);
7699
- const readSize = sizeField ? accessor(sizeField) : null;
7700
- const radius = sizeField ? radiusScale(allRows, sizeField, Math.min(plot.width, plot.height)) : fixedRadius;
7701
- const points = [];
7702
- for (const series of model.series) {
7703
- for (const row of series.rows) {
7704
- const xValue = readX(row);
7705
- const yValue = readY(row);
7706
- const yNumber = toNumber(yValue);
7707
- const px = model.x.pixel(xValue);
7708
- if (px == null || isInvalidPositionValue(xValue) || isMissing(yValue) || Number.isNaN(yNumber)) {
7709
- continue;
7710
- }
7711
- const py = model.y.pixel(yValue);
7712
- if (!Number.isFinite(px) || !Number.isFinite(py)) continue;
7713
- points.push({
7714
- x: px,
7715
- y: py,
7716
- r: readSize ? radius(readSize(row)) : radius(),
7717
- color: series.color,
7718
- alpha: rowAlpha(model.emphasis, row),
7719
- seriesKey: series.key
7720
- });
7721
- }
7722
- }
7723
- const originalCount = points.length;
7724
- const drawPoints = shouldQuantize(originalCount, plot, !!sizeField) ? quantizePoints(points) : points;
7725
- model.markStats = { drawn: drawPoints.length, original: originalCount };
7726
- if (sizeField) drawPoints.sort((a, b) => b.r - a.r);
7727
- ctx.save();
7728
- ctx.beginPath();
7729
- ctx.rect(plot.x, plot.y, plot.width, plot.height);
7730
- ctx.clip();
7731
- if (model.sketch) {
7732
- const pen = new RoughPen(ctx, model.sketch);
7733
- for (const point of drawPoints) {
7734
- const prev = ctx.globalAlpha;
7735
- if (point.alpha !== 1) ctx.globalAlpha = point.alpha;
7736
- pen.circle(point.x, point.y, point.r, {
7737
- fill: point.color,
7738
- fillAlpha: 0.7,
7739
- stroke: tokens.color.background,
7740
- strokeWidth: 1
7741
- });
7742
- if (point.alpha !== 1) ctx.globalAlpha = prev;
7743
- }
7744
- ctx.restore();
7745
- return;
7746
- }
7747
- ctx.lineWidth = 1;
7748
- ctx.strokeStyle = tokens.color.background;
7749
- for (const point of drawPoints) {
7750
- ctx.beginPath();
7751
- ctx.arc(point.x, point.y, point.r, 0, Math.PI * 2);
7752
- ctx.fillStyle = point.color;
7753
- ctx.globalAlpha = 0.7 * point.alpha;
7754
- ctx.fill();
7755
- ctx.globalAlpha = point.alpha;
7756
- ctx.stroke();
7757
- }
7758
- ctx.globalAlpha = 1;
7759
- ctx.restore();
7760
- }
7761
-
7762
7856
  // src/charts/box.ts
7763
7857
  function quantileSorted(sorted, p) {
7764
7858
  const n = sorted.length;
@@ -8003,7 +8097,7 @@ function buildBoxInteraction(model) {
8003
8097
  }
8004
8098
 
8005
8099
  // src/charts/chrome.ts
8006
- var CHROME_PAD = { top: 14, right: 16, bottom: 14, left: 16 };
8100
+ var CHROME_PAD = { top: 0, right: 0, bottom: 0, left: 0 };
8007
8101
  function resolveTitle2(title2) {
8008
8102
  if (typeof title2 === "string") return { text: title2, align: "left" };
8009
8103
  if (title2 && typeof title2 === "object") {
@@ -9905,7 +9999,7 @@ function layout(graph, area2, nodeWidth, pad3) {
9905
9999
  const kx = maxLayer > 0 ? (area2.width - nodeWidth) / maxLayer : 0;
9906
10000
  const maxColumnSize = columns.reduce((m, col) => Math.max(m, col.length), 0);
9907
10001
  const desiredPad = Math.max(0, pad3);
9908
- const effectivePad = maxColumnSize > 1 && desiredPad * (maxColumnSize - 1) >= area2.height && area2.height > 0 ? area2.height * 0.25 / (maxColumnSize - 1) : desiredPad;
10002
+ const effectivePad = maxColumnSize > 1 ? Math.min(desiredPad, area2.height * 0.35 / (maxColumnSize - 1)) : desiredPad;
9909
10003
  for (const col of columns) {
9910
10004
  for (const n of col) {
9911
10005
  n.x0 = area2.x + n.layer * kx;
@@ -10027,15 +10121,24 @@ function drawSankey(surface, spec, tokens, size) {
10027
10121
  const graph = buildGraph(sankey, tokens.color.palette);
10028
10122
  if (graph.nodes.length === 0 || content.width <= 0 || content.height <= 0) return;
10029
10123
  const labelFont = fontString(f.size.small, f.family, f.weight.medium);
10030
- let maxLabel = 0;
10031
- for (const n of graph.nodes) maxLabel = Math.max(maxLabel, measureText(n.name, labelFont).width);
10032
- const gutter = Math.min(maxLabel + 12, content.width * 0.24);
10124
+ const valueFont = fontString(f.size.tiny, f.family, f.weight.normal);
10125
+ const showValues = sankey.nodeValues !== false;
10126
+ const labelExtent = (n) => {
10127
+ const nameW = measureText(n.name, labelFont).width;
10128
+ const valW = showValues ? measureText(formatValue(n.value, sankey.encoding.value.format), valueFont).width : 0;
10129
+ return Math.max(nameW, valW);
10130
+ };
10131
+ const colExtent = (col) => col.reduce((m, n) => Math.max(m, labelExtent(n)), 0);
10132
+ const LABEL_GAP = 6;
10133
+ const capGutter = content.width * 0.3;
10134
+ const leftGutter = Math.min(colExtent(graph.columns[0] ?? []) + LABEL_GAP + 4, capGutter);
10135
+ const rightGutter = graph.maxLayer > 0 ? Math.min(colExtent(graph.columns[graph.maxLayer] ?? []) + LABEL_GAP + 4, capGutter) : 0;
10033
10136
  const nodeWidth = sankey.nodeWidth ?? 16;
10034
10137
  const pad3 = sankey.nodePadding ?? 14;
10035
10138
  const area2 = {
10036
- x: content.x + gutter,
10139
+ x: content.x + leftGutter,
10037
10140
  y: content.y + 4,
10038
- width: Math.max(10, content.width - gutter * 2),
10141
+ width: Math.max(10, content.width - leftGutter - rightGutter),
10039
10142
  height: Math.max(10, content.height - 8)
10040
10143
  };
10041
10144
  layout(graph, area2, nodeWidth, pad3);
@@ -10059,12 +10162,13 @@ function drawSankey(surface, spec, tokens, size) {
10059
10162
  }
10060
10163
  }
10061
10164
  ctx.restore();
10062
- const showValues = sankey.nodeValues !== false;
10063
10165
  const mid = area2.x + area2.width / 2;
10064
10166
  for (const n of graph.nodes) {
10065
- const labelRight = n.x0 < mid;
10167
+ const outerLeft = n.layer === 0;
10168
+ const outerRight = n.layer === graph.maxLayer && graph.maxLayer > 0;
10169
+ const labelRight = outerRight ? true : outerLeft ? false : n.x0 < mid;
10066
10170
  const cy = center(n);
10067
- const left = labelRight ? n.x1 + 6 : n.x0 - 6;
10171
+ const left = labelRight ? n.x1 + LABEL_GAP : n.x0 - LABEL_GAP;
10068
10172
  const transform2 = labelRight ? "translateY(-50%)" : "translate(-100%,-50%)";
10069
10173
  addOverlayText(surface, tokens, {
10070
10174
  left,
@@ -13422,7 +13526,6 @@ function scatterInteraction(model) {
13422
13526
  }
13423
13527
 
13424
13528
  // src/runtime/combo.ts
13425
- var DEFAULT_PADDING2 = { top: 12, right: 16, bottom: 12, left: 12 };
13426
13529
  function fmtTick(v, f) {
13427
13530
  return f ? formatValue(v, f) : formatNumber2(v, ",");
13428
13531
  }
@@ -13608,7 +13711,7 @@ function buildComboModel(spec, tokens, opts) {
13608
13711
  const legendPosition = typeof spec.legend === "object" && spec.legend.position || (legendItems.length > 8 ? "right" : "top");
13609
13712
  const xShow = xAxisCfg.show !== false;
13610
13713
  const yShow = spec.axes?.y?.show !== false;
13611
- const padding = { ...DEFAULT_PADDING2, ...spec.padding };
13714
+ const padding = { ...NO_PADDING, ...spec.padding };
13612
13715
  const titleInput4 = typeof spec.title === "string" ? { text: spec.title } : spec.title ? { text: spec.title.text, subtitle: spec.title.subtitle } : void 0;
13613
13716
  const frame = computeFrame({
13614
13717
  width: opts.width,
@@ -13830,7 +13933,6 @@ function renderLayer(surface, lm) {
13830
13933
  }
13831
13934
 
13832
13935
  // src/runtime/histogram.ts
13833
- var DEFAULT_PADDING3 = { top: 12, right: 16, bottom: 12, left: 12 };
13834
13936
  function firstPaletteColor(tokens) {
13835
13937
  return rgbaToCss(ordinalColorScale({ domain: ["0"], palette: tokens.color.palette }).map("0"));
13836
13938
  }
@@ -13882,7 +13984,7 @@ function buildHistogramModel(spec, tokens, opts) {
13882
13984
  const xLabels = xTickValues.map((v) => xFmt ? formatValue(v, xFmt) : formatNumber2(v, ","));
13883
13985
  const yFmt = spec.axes?.y?.format ?? (spec.density ? ".3~f" : void 0);
13884
13986
  const yLabels = yTickValues.map((v) => yFmt ? formatValue(v, yFmt) : formatNumber2(v, ","));
13885
- const padding = { ...DEFAULT_PADDING3, ...spec.padding };
13987
+ const padding = { ...NO_PADDING, ...spec.padding };
13886
13988
  const titleInput4 = typeof spec.title === "string" ? { text: spec.title } : spec.title ? { text: spec.title.text, subtitle: spec.title.subtitle } : void 0;
13887
13989
  const xShow = spec.axes?.x?.show !== false;
13888
13990
  const yShow = spec.axes?.y?.show !== false;
@@ -14088,6 +14190,12 @@ function labelFor(value) {
14088
14190
  const key = toKey(value);
14089
14191
  return key === "" ? "(blank)" : key;
14090
14192
  }
14193
+ function textureShade(fill) {
14194
+ const lum = (0.299 * fill.r + 0.587 * fill.g + 0.114 * fill.b) / 255;
14195
+ const k = lum > 0.5 ? 0.68 : 1.4;
14196
+ const c = (n) => Math.max(0, Math.min(255, Math.round(n * k)));
14197
+ return rgbaToCss({ r: c(fill.r), g: c(fill.g), b: c(fill.b), a: 1 });
14198
+ }
14091
14199
  function buildLeaves(treemap) {
14092
14200
  const readCategory = accessor(treemap.encoding.category.field);
14093
14201
  const readValue = accessor(treemap.encoding.value.field);
@@ -14171,13 +14279,20 @@ function tileProbe(treemap, tile) {
14171
14279
  if (groupField) probe[groupField] = tile.group;
14172
14280
  return probe;
14173
14281
  }
14174
- function drawRect2(ctx, pen, rect, fill, stroke) {
14282
+ function drawRect2(ctx, pen, rect, fill, fillCss, stroke) {
14175
14283
  if (rect.width <= 0 || rect.height <= 0) return;
14176
14284
  if (pen) {
14177
- pen.rect(rect.x, rect.y, rect.width, rect.height, { fill, stroke, fillStyle: "solid" });
14285
+ pen.rect(rect.x, rect.y, rect.width, rect.height, { fill: fillCss, fillStyle: "solid" });
14286
+ pen.rect(rect.x, rect.y, rect.width, rect.height, {
14287
+ fill: textureShade(fill),
14288
+ fillStyle: "hachure",
14289
+ fillAlpha: 0.4,
14290
+ hachureGap: 7,
14291
+ stroke
14292
+ });
14178
14293
  return;
14179
14294
  }
14180
- ctx.fillStyle = fill;
14295
+ ctx.fillStyle = fillCss;
14181
14296
  ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
14182
14297
  ctx.strokeStyle = stroke;
14183
14298
  ctx.lineWidth = 1;
@@ -14247,7 +14362,7 @@ function drawTreemap(surface, spec, tokens, size, context) {
14247
14362
  const drawTile = (tile) => {
14248
14363
  const alpha = emphasis ? emphasis.match(tileProbe(treemap, tile)) ? 1 : emphasis.dim : 1;
14249
14364
  if (alpha !== 1) ctx.globalAlpha = alpha;
14250
- drawRect2(ctx, pen, tile.rect, tile.fillCss, stroke);
14365
+ drawRect2(ctx, pen, tile.rect, tile.fill, tile.fillCss, stroke);
14251
14366
  if (alpha !== 1) ctx.globalAlpha = 1;
14252
14367
  };
14253
14368
  ctx.save();
@@ -14284,11 +14399,13 @@ function drawTreemap(surface, spec, tokens, size, context) {
14284
14399
  ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, Math.max(0, rect.width - 1), Math.max(0, rect.height - 1));
14285
14400
  }
14286
14401
  if (headerH > 0) {
14402
+ const headerFont = fontString(tokens.font.size.tiny, tokens.font.family, tokens.font.weight.bold);
14403
+ const headerW = Math.max(0, rect.width - 10);
14287
14404
  addOverlayText(surface, tokens, {
14288
14405
  left: rect.x + 5,
14289
14406
  top: rect.y + 4,
14290
- width: Math.max(0, rect.width - 10),
14291
- text: group.label,
14407
+ width: headerW,
14408
+ text: ellipsize(group.label, headerW, headerFont),
14292
14409
  color: tokens.color.text,
14293
14410
  size: tokens.font.size.tiny,
14294
14411
  weight: tokens.font.weight.bold,
@@ -14497,12 +14614,11 @@ function drawGauge(surface, spec, tokens, size, context) {
14497
14614
  const legendFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
14498
14615
  const legendW = legendEntries.length ? measureLegendWidth(legendEntries, legendFont) + 12 : 0;
14499
14616
  const dialW = Math.max(40, content.width - legendW);
14500
- const radius = clamp8(
14501
- Math.min((dialW - 16) / 2, content.height - captionReserve - 16),
14502
- 18,
14503
- Math.min(dialW, content.height * 2)
14504
- );
14505
- const trackWidth = clamp8(radius * 0.17, 14, 54);
14617
+ const availH = Math.max(0, content.height - captionReserve);
14618
+ const provR = Math.max(18, Math.min(dialW / 2, availH));
14619
+ const trackWidth = clamp8(provR * 0.17, 14, 54);
14620
+ const edgeGuard = trackWidth / 2 + clamp8(trackWidth * 0.3, 4, 10) + 1;
14621
+ const radius = Math.max(18, Math.min(provR, dialW / 2 - edgeGuard, availH - edgeGuard));
14506
14622
  const cx = content.x + dialW / 2;
14507
14623
  const blockH = radius + trackWidth / 2 + captionReserve;
14508
14624
  const topMargin = Math.max(0, (content.height - blockH) / 2);
@@ -14645,10 +14761,7 @@ function drawBullet(surface, spec, tokens, size, context) {
14645
14761
  }
14646
14762
  const labelFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.bold);
14647
14763
  const valueFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
14648
- const labelW = Math.min(
14649
- Math.max(72, content.width * 0.18, measureText(caption, labelFont).width + 18),
14650
- Math.max(72, content.width * 0.38)
14651
- );
14764
+ const labelW = clamp9(measureText(caption, labelFont).width + 14, 44, Math.max(72, content.width * 0.4));
14652
14765
  const axisReserve = Math.max(24, tokens.font.size.tiny * 2);
14653
14766
  const availableTrackW = Math.max(0, content.width - labelW);
14654
14767
  const trackW = Math.max(0, availableTrackW - 4);
@@ -14766,7 +14879,6 @@ function drawBullet(surface, spec, tokens, size, context) {
14766
14879
  // src/charts/calendarHeatmap.ts
14767
14880
  var DAY_MS2 = 864e5;
14768
14881
  var GAP2 = 2;
14769
- var MAX_CELL = 16;
14770
14882
  var WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
14771
14883
  var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
14772
14884
  function pad22(n) {
@@ -14789,9 +14901,9 @@ function addDays(date, days) {
14789
14901
  d.setDate(d.getDate() + days);
14790
14902
  return d;
14791
14903
  }
14792
- function drawRoundedCell2(ctx, x, y, size, radius) {
14904
+ function drawRoundedCell2(ctx, x, y, w, h, radius) {
14793
14905
  ctx.beginPath();
14794
- roundedRect(ctx, x, y, size, size, radius);
14906
+ roundedRect(ctx, x, y, w, h, radius);
14795
14907
  ctx.fill();
14796
14908
  }
14797
14909
  function noData(surface, tokens, content) {
@@ -14902,16 +15014,26 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14902
15014
  const labelHeight = Math.ceil(labelSize * 1.7) + 2;
14903
15015
  const availableW = Math.max(0, content.width - leftGutter);
14904
15016
  const availableH = Math.max(0, content.height - labelHeight);
14905
- const cell = Math.floor(Math.min(MAX_CELL, (availableW - GAP2 * Math.max(0, grid.weeks - 1)) / grid.weeks, (availableH - GAP2 * 6) / 7));
14906
- if (!Number.isFinite(cell) || cell <= 0) {
15017
+ const MAX_ASPECT = 1.75;
15018
+ const rawW = (availableW - GAP2 * Math.max(0, grid.weeks - 1)) / grid.weeks;
15019
+ const rawH = (availableH - GAP2 * 6) / 7;
15020
+ const cellW = Math.floor(Math.max(1, Math.min(rawW, rawH * MAX_ASPECT)));
15021
+ const cellH = Math.floor(Math.max(1, Math.min(rawH, rawW * MAX_ASPECT)));
15022
+ if (!Number.isFinite(cellW) || !Number.isFinite(cellH) || cellW <= 0 || cellH <= 0) {
14907
15023
  noData(surface, tokens, content);
14908
15024
  return;
14909
15025
  }
15026
+ const stepX = cellW + GAP2;
15027
+ const stepY = cellH + GAP2;
15028
+ const gridW = grid.weeks * cellW + Math.max(0, grid.weeks - 1) * GAP2;
15029
+ const gridH = 7 * cellH + 6 * GAP2;
15030
+ const vpad = Math.max(0, (availableH - gridH) / 2);
15031
+ const hpad = Math.max(0, (availableW - gridW) / 2);
14910
15032
  const gridRect = {
14911
- x: content.x + leftGutter,
14912
- y: content.y + labelHeight,
14913
- width: grid.weeks * cell + Math.max(0, grid.weeks - 1) * GAP2,
14914
- height: 7 * cell + 6 * GAP2
15033
+ x: content.x + leftGutter + hpad,
15034
+ y: content.y + labelHeight + vpad,
15035
+ width: gridW,
15036
+ height: gridH
14915
15037
  };
14916
15038
  const values = days.map((d) => d.value);
14917
15039
  const min = Math.min(...values);
@@ -14920,35 +15042,35 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14920
15042
  const cscale = sequentialColorScale({ domain: [min, max], interpolator: interp });
14921
15043
  const sketch = resolveSketch(spec);
14922
15044
  const pen = sketch ? new RoughPen(ctx, sketch) : null;
14923
- const radius = Math.min(3, tokens.radius.sm, cell / 3);
15045
+ const radius = Math.min(3, tokens.radius.sm, Math.min(cellW, cellH) / 3);
14924
15046
  const laidOut = /* @__PURE__ */ new Map();
14925
15047
  ctx.save();
14926
15048
  for (const c of grid.cells) {
14927
- const x = gridRect.x + c.week * (cell + GAP2);
14928
- const y = gridRect.y + c.weekday * (cell + GAP2);
15049
+ const x = gridRect.x + c.week * stepX;
15050
+ const y = gridRect.y + c.weekday * stepY;
14929
15051
  const fill = c.present && c.value !== void 0 ? rgbaToCss(cscale.map(c.value)) : tokens.color.surface;
14930
- laidOut.set(c.isoDate, { ...c, x, y, size: cell, fill });
15052
+ laidOut.set(c.isoDate, { ...c, x, y, w: cellW, h: cellH, fill });
14931
15053
  ctx.globalAlpha = c.present ? rowAlpha(emphasis, c.row ?? { [dateField]: c.isoDate }) : 1;
14932
15054
  if (pen) {
14933
- pen.rect(x, y, cell, cell, {
15055
+ pen.rect(x, y, cellW, cellH, {
14934
15056
  fill,
14935
15057
  fillStyle: "solid",
14936
15058
  stroke: c.present ? fill : tokens.color.border
14937
15059
  });
14938
15060
  } else {
14939
15061
  ctx.fillStyle = fill;
14940
- drawRoundedCell2(ctx, x, y, cell, radius);
15062
+ drawRoundedCell2(ctx, x, y, cellW, cellH, radius);
14941
15063
  ctx.strokeStyle = tokens.color.border;
14942
15064
  ctx.lineWidth = 1;
14943
- ctx.strokeRect(x + 0.5, y + 0.5, Math.max(0, cell - 1), Math.max(0, cell - 1));
15065
+ ctx.strokeRect(x + 0.5, y + 0.5, Math.max(0, cellW - 1), Math.max(0, cellH - 1));
14944
15066
  }
14945
15067
  ctx.globalAlpha = 1;
14946
15068
  }
14947
15069
  ctx.restore();
14948
15070
  for (const m of monthAnchors(grid)) {
14949
15071
  addOverlayText(surface, tokens, {
14950
- left: gridRect.x + m.week * (cell + GAP2),
14951
- top: content.y,
15072
+ left: gridRect.x + m.week * stepX,
15073
+ top: gridRect.y - labelHeight,
14952
15074
  text: m.label,
14953
15075
  color: tokens.color.textMuted,
14954
15076
  size: labelSize
@@ -14957,7 +15079,7 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14957
15079
  for (const weekday of [1, 3, 5]) {
14958
15080
  addOverlayText(surface, tokens, {
14959
15081
  left: content.x,
14960
- top: gridRect.y + weekday * (cell + GAP2) + cell / 2,
15082
+ top: gridRect.y + weekday * stepY + cellH / 2,
14961
15083
  width: Math.max(0, leftGutter - tokens.spacing.xs),
14962
15084
  text: WEEKDAY_LABELS[weekday],
14963
15085
  color: tokens.color.textMuted,
@@ -14969,11 +15091,11 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14969
15091
  const colorLabel = cal.encoding.color.title ?? cal.encoding.color.field;
14970
15092
  const hitCell = (px, py) => {
14971
15093
  if (px < gridRect.x || py < gridRect.y || px > gridRect.x + gridRect.width || py > gridRect.y + gridRect.height) return null;
14972
- const week = Math.floor((px - gridRect.x) / (cell + GAP2));
14973
- const weekday = Math.floor((py - gridRect.y) / (cell + GAP2));
14974
- const x = gridRect.x + week * (cell + GAP2);
14975
- const y = gridRect.y + weekday * (cell + GAP2);
14976
- if (week < 0 || week >= grid.weeks || weekday < 0 || weekday > 6 || px > x + cell || py > y + cell) return null;
15094
+ const week = Math.floor((px - gridRect.x) / stepX);
15095
+ const weekday = Math.floor((py - gridRect.y) / stepY);
15096
+ const x = gridRect.x + week * stepX;
15097
+ const y = gridRect.y + weekday * stepY;
15098
+ if (week < 0 || week >= grid.weeks || weekday < 0 || weekday > 6 || px > x + cellW || py > y + cellH) return null;
14977
15099
  const match = grid.cells.find((c) => c.week === week && c.weekday === weekday);
14978
15100
  if (!match || !match.present) return null;
14979
15101
  return laidOut.get(match.isoDate) ?? null;
@@ -15002,7 +15124,6 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
15002
15124
  }
15003
15125
 
15004
15126
  // src/runtime/waterfall.ts
15005
- var DEFAULT_PADDING4 = { top: 12, right: 16, bottom: 12, left: 12 };
15006
15127
  function labelFor2(value) {
15007
15128
  if (value == null) return "";
15008
15129
  if (value instanceof Date) return value.toISOString().slice(0, 10);
@@ -15110,7 +15231,7 @@ function buildWaterfallModel(spec, tokens, opts) {
15110
15231
  const fmt = spec.encoding.value.format ?? spec.axes?.y?.format;
15111
15232
  const yLabels = yTickValues.map((v) => fmt ? formatValue(v, fmt) : formatNumber2(v, ","));
15112
15233
  const categories = bars.map((b) => b.label);
15113
- const padding = { ...DEFAULT_PADDING4, ...spec.padding };
15234
+ const padding = { ...NO_PADDING, ...spec.padding };
15114
15235
  const xShow = spec.axes?.x?.show !== false;
15115
15236
  const yShow = spec.axes?.y?.show !== false;
15116
15237
  const xTitle = spec.axes?.x?.title ?? spec.encoding.stage.title;
@@ -15351,7 +15472,6 @@ function drawWaterfall(surface, spec, tokens, size, context) {
15351
15472
  }
15352
15473
 
15353
15474
  // src/runtime/slope.ts
15354
- var DEFAULT_PADDING5 = { top: 16, right: 76, bottom: 26, left: 76 };
15355
15475
  function labelFor3(value) {
15356
15476
  if (value == null) return "";
15357
15477
  if (value instanceof Date) return value.toISOString().slice(0, 10);
@@ -15387,7 +15507,44 @@ function buildSlopeModel(spec, tokens, opts) {
15387
15507
  ];
15388
15508
  const fmt = spec.format ?? spec.encoding.y.format ?? spec.axes?.y?.format;
15389
15509
  const yLabels = yTickValues.map((v) => fmtTick2(v, fmt));
15390
- const padding = { ...DEFAULT_PADDING5, ...spec.padding };
15510
+ const grouped = /* @__PURE__ */ new Map();
15511
+ for (const row of data) {
15512
+ const rawSeries = readSeries(row);
15513
+ const key = toKey(rawSeries);
15514
+ let group = grouped.get(key);
15515
+ if (!group) {
15516
+ group = { key, label: labelFor3(rawSeries), value: rawSeries, values: /* @__PURE__ */ new Map() };
15517
+ grouped.set(key, group);
15518
+ }
15519
+ const rawX = readX(row);
15520
+ const catKey = labelFor3(rawX);
15521
+ const value = toNumber(readY(row));
15522
+ if (!Number.isNaN(value) && !group.values.has(catKey)) group.values.set(catKey, value);
15523
+ }
15524
+ const showLabels = spec.labels !== false;
15525
+ let leftLabelW = 0;
15526
+ let rightLabelW = 0;
15527
+ if (showLabels) {
15528
+ const boldTiny = fontString(tokens.font.size.tiny, tokens.font.family, tokens.font.weight.bold);
15529
+ const normTiny = fontString(tokens.font.size.tiny, tokens.font.family, tokens.font.weight.normal);
15530
+ for (const group of grouped.values()) {
15531
+ const present = categories.filter((c) => group.values.has(c));
15532
+ if (present.length === 0) continue;
15533
+ const firstV = group.values.get(present[0]);
15534
+ const lastV = group.values.get(present[present.length - 1]);
15535
+ leftLabelW = Math.max(
15536
+ leftLabelW,
15537
+ measureText(group.label, boldTiny).width,
15538
+ measureText(fmtTick2(firstV, fmt), normTiny).width
15539
+ );
15540
+ rightLabelW = Math.max(rightLabelW, measureText(fmtTick2(lastV, fmt), normTiny).width);
15541
+ }
15542
+ leftLabelW = Math.ceil(leftLabelW);
15543
+ rightLabelW = Math.ceil(rightLabelW);
15544
+ }
15545
+ const labelBand = tokens.font.size.tiny + 4;
15546
+ const basePadding = showLabels ? { top: labelBand, right: rightLabelW + 8, bottom: labelBand, left: leftLabelW + 8 } : { top: 0, right: 0, bottom: 0, left: 0 };
15547
+ const padding = { ...basePadding, ...spec.padding };
15391
15548
  const xShow = spec.axes?.x?.show !== false;
15392
15549
  const yShow = spec.axes?.y?.show === true;
15393
15550
  const xTitle = spec.axes?.x?.title ?? spec.encoding.x.title;
@@ -15404,20 +15561,6 @@ function buildSlopeModel(spec, tokens, opts) {
15404
15561
  const plot = frame.plot;
15405
15562
  const point = pointScale({ domain: categories, range: [plot.x, plot.x + plot.width], padding: 0.5 });
15406
15563
  const yScale = linearScale({ domain: yDomain, range: [plot.y + plot.height, plot.y] });
15407
- const grouped = /* @__PURE__ */ new Map();
15408
- for (const row of data) {
15409
- const rawSeries = readSeries(row);
15410
- const key = toKey(rawSeries);
15411
- let group = grouped.get(key);
15412
- if (!group) {
15413
- group = { key, label: labelFor3(rawSeries), value: rawSeries, values: /* @__PURE__ */ new Map() };
15414
- grouped.set(key, group);
15415
- }
15416
- const rawX = readX(row);
15417
- const catKey = labelFor3(rawX);
15418
- const value = toNumber(readY(row));
15419
- if (!Number.isNaN(value) && !group.values.has(catKey)) group.values.set(catKey, value);
15420
- }
15421
15564
  const palette = ordinalColorScale({ domain: [...grouped.keys()], palette: tokens.color.palette });
15422
15565
  const series = [...grouped.values()].map((group) => {
15423
15566
  const points = categories.map((catKey) => {
@@ -15486,7 +15629,9 @@ function buildSlopeModel(spec, tokens, opts) {
15486
15629
  series,
15487
15630
  categories,
15488
15631
  format: fmt,
15489
- showLabels: spec.labels !== false,
15632
+ showLabels,
15633
+ leftLabelW,
15634
+ rightLabelW,
15490
15635
  base,
15491
15636
  sketch
15492
15637
  };
@@ -15494,13 +15639,15 @@ function buildSlopeModel(spec, tokens, opts) {
15494
15639
 
15495
15640
  // src/charts/slope.ts
15496
15641
  var HIT_RADIUS2 = 8;
15497
- var LABEL_WIDTH = 68;
15498
15642
  function fmtValue(v, format) {
15499
15643
  return format ? formatValue(v, format) : formatNumber2(v, ",");
15500
15644
  }
15501
15645
  function drawLines(surface, model, alphaOf) {
15502
15646
  const ctx = surface.marks.ctx;
15503
15647
  const pen = model.sketch ? new RoughPen(ctx, model.sketch) : null;
15648
+ const strokeW = lineStrokeWidth(model.plot);
15649
+ const dotR = Math.min(6, markerRadius(model.plot, 2));
15650
+ const dotSW = markerStroke(dotR);
15504
15651
  ctx.save();
15505
15652
  ctx.beginPath();
15506
15653
  ctx.rect(model.plot.x, model.plot.y, model.plot.width, model.plot.height);
@@ -15510,11 +15657,11 @@ function drawLines(surface, model, alphaOf) {
15510
15657
  if (points.length === 0) continue;
15511
15658
  ctx.globalAlpha = alphaOf(series);
15512
15659
  if (pen && points.length >= 2) {
15513
- pen.polyline(points, { stroke: series.color });
15660
+ pen.polyline(points, { stroke: series.color, strokeWidth: strokeW });
15514
15661
  } else if (points.length >= 2) {
15515
15662
  ctx.beginPath();
15516
15663
  ctx.strokeStyle = series.color;
15517
- ctx.lineWidth = 2;
15664
+ ctx.lineWidth = strokeW;
15518
15665
  ctx.lineJoin = "round";
15519
15666
  ctx.lineCap = "round";
15520
15667
  ctx.moveTo(points[0].x, points[0].y);
@@ -15523,13 +15670,13 @@ function drawLines(surface, model, alphaOf) {
15523
15670
  }
15524
15671
  for (const p of points) {
15525
15672
  if (pen) {
15526
- pen.circle(p.x, p.y, 3.5, { fill: series.color, stroke: series.color });
15673
+ pen.circle(p.x, p.y, dotR, { fill: series.color, stroke: series.color });
15527
15674
  } else {
15528
15675
  ctx.beginPath();
15529
15676
  ctx.fillStyle = series.color;
15530
15677
  ctx.strokeStyle = model.tokens.color.background;
15531
- ctx.lineWidth = 1.5;
15532
- ctx.arc(p.x, p.y, 3.5, 0, Math.PI * 2);
15678
+ ctx.lineWidth = dotSW;
15679
+ ctx.arc(p.x, p.y, dotR, 0, Math.PI * 2);
15533
15680
  ctx.fill();
15534
15681
  ctx.stroke();
15535
15682
  }
@@ -15543,15 +15690,17 @@ function drawEndLabels(surface, model, alphaOf) {
15543
15690
  const { tokens } = model;
15544
15691
  const labelSize = tokens.font.size.tiny;
15545
15692
  const valueSize = tokens.font.size.tiny;
15693
+ const leftW = model.leftLabelW;
15694
+ const rightW = model.rightLabelW;
15546
15695
  for (const series of model.series) {
15547
15696
  if (series.points.length === 0) continue;
15548
15697
  const opacity = alphaOf(series) === 1 ? 1 : 0.5;
15549
15698
  const first = series.points[0];
15550
15699
  const last = series.points[series.points.length - 1];
15551
15700
  addOverlayText(surface, tokens, {
15552
- left: first.x - 8 - LABEL_WIDTH,
15701
+ left: first.x - 8 - leftW,
15553
15702
  top: first.y - labelSize - 2,
15554
- width: LABEL_WIDTH,
15703
+ width: leftW,
15555
15704
  text: series.label,
15556
15705
  color: series.color,
15557
15706
  size: labelSize,
@@ -15560,9 +15709,9 @@ function drawEndLabels(surface, model, alphaOf) {
15560
15709
  opacity
15561
15710
  });
15562
15711
  addOverlayText(surface, tokens, {
15563
- left: first.x - 8 - LABEL_WIDTH,
15712
+ left: first.x - 8 - leftW,
15564
15713
  top: first.y + 1,
15565
- width: LABEL_WIDTH,
15714
+ width: leftW,
15566
15715
  text: fmtValue(first.value, model.format),
15567
15716
  color: tokens.color.textMuted,
15568
15717
  size: valueSize,
@@ -15573,7 +15722,7 @@ function drawEndLabels(surface, model, alphaOf) {
15573
15722
  addOverlayText(surface, tokens, {
15574
15723
  left: last.x + 8,
15575
15724
  top: last.y - valueSize / 2,
15576
- width: LABEL_WIDTH,
15725
+ width: rightW,
15577
15726
  text: fmtValue(last.value, model.format),
15578
15727
  color: series.color,
15579
15728
  size: valueSize,
@@ -15747,7 +15896,7 @@ function measureMax2(labels, font) {
15747
15896
  function valueLabel(value, format) {
15748
15897
  return format ? formatValue(value, format) : formatNumber2(value, ",");
15749
15898
  }
15750
- function drawConnector(ctx, pen, x1, x2, y, tokens) {
15899
+ function drawConnector(ctx, pen, x1, x2, y, width, tokens) {
15751
15900
  if (pen) {
15752
15901
  pen.polyline(
15753
15902
  [
@@ -15763,13 +15912,12 @@ function drawConnector(ctx, pen, x1, x2, y, tokens) {
15763
15912
  ctx.moveTo(x1, y);
15764
15913
  ctx.lineTo(x2, y);
15765
15914
  ctx.strokeStyle = tokens.color.axis;
15766
- ctx.lineWidth = 3;
15915
+ ctx.lineWidth = width;
15767
15916
  ctx.lineCap = "round";
15768
15917
  ctx.stroke();
15769
15918
  ctx.restore();
15770
15919
  }
15771
- function drawDot(ctx, pen, x, y, color, tokens) {
15772
- const r = 5;
15920
+ function drawDot(ctx, pen, x, y, r, color, tokens) {
15773
15921
  if (pen) {
15774
15922
  pen.circle(x, y, r + 1.5, { fill: tokens.color.background, stroke: tokens.color.background });
15775
15923
  pen.circle(x, y, r, { fill: color, stroke: tokens.color.background });
@@ -15780,16 +15928,16 @@ function drawDot(ctx, pen, x, y, color, tokens) {
15780
15928
  ctx.arc(x, y, r, 0, Math.PI * 2);
15781
15929
  ctx.fillStyle = color;
15782
15930
  ctx.fill();
15783
- ctx.lineWidth = 1.5;
15931
+ ctx.lineWidth = Math.max(1.5, r * 0.3);
15784
15932
  ctx.strokeStyle = tokens.color.background;
15785
15933
  ctx.stroke();
15786
15934
  ctx.restore();
15787
15935
  }
15788
- function layoutPlot(content, labelWidth, tokens) {
15936
+ function layoutPlot(content, labelWidth, rightReserve, tokens) {
15789
15937
  const leftGutter = Math.ceil(Math.min(content.width * 0.3, Math.max(42, labelWidth + tokens.spacing.sm)));
15790
15938
  const axisHeight = 28;
15791
15939
  const topPad = Math.min(6, CHROME_PAD.top / 2);
15792
- const rightPad = 8;
15940
+ const rightPad = Math.max(8, Math.ceil(rightReserve));
15793
15941
  return {
15794
15942
  leftGutter,
15795
15943
  axisHeight,
@@ -15824,7 +15972,16 @@ function drawDumbbell(surface, spec, tokens, size, context) {
15824
15972
  }
15825
15973
  const labelFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
15826
15974
  const widestCategory = measureMax2(model.rows.map((row) => row.catLabel), labelFont);
15827
- const { plot, leftGutter } = layoutPlot(content, widestCategory, tokens);
15975
+ let valueReserve = 0;
15976
+ if (dumbbell.labels) {
15977
+ const valueFont = fontString(tokens.font.size.tiny, tokens.font.family, tokens.font.weight.normal);
15978
+ const widestValue = measureMax2(
15979
+ model.rows.flatMap((row) => row.dots.map((dot) => formatDumbbellValue(dot.value, model.format))),
15980
+ valueFont
15981
+ );
15982
+ valueReserve = Math.min(content.width * 0.2, widestValue + 9);
15983
+ }
15984
+ const { plot, leftGutter } = layoutPlot(content, widestCategory, valueReserve, tokens);
15828
15985
  if (plot.width <= 8 || plot.height <= 8) return;
15829
15986
  const xScale = linearScale({ domain: model.valueDomain, range: [plot.x, plot.x + plot.width] });
15830
15987
  const yBand = bandScale({ domain: model.categories, range: [plot.y, plot.y + plot.height], paddingInner: 0.45, paddingOuter: 0.25 });
@@ -15851,6 +16008,9 @@ function drawDumbbell(surface, spec, tokens, size, context) {
15851
16008
  }
15852
16009
  ctx.restore();
15853
16010
  const pixelRows = [];
16011
+ const dotR = Math.max(4, Math.min(13, yBand.bandwidth * 0.16));
16012
+ const connectorW = Math.max(3, Math.min(9, dotR * 0.7));
16013
+ const labelGap = Math.round(dotR + 4);
15854
16014
  for (const row of model.rows) {
15855
16015
  const bandTop = yBand.map(row.catKey);
15856
16016
  if (bandTop === void 0) continue;
@@ -15868,8 +16028,8 @@ function drawDumbbell(surface, spec, tokens, size, context) {
15868
16028
  if (pixelDots.length === 0) continue;
15869
16029
  pixelRows.push({ ...row, y: cy, pixelDots });
15870
16030
  ctx.globalAlpha = rowAlpha(emphasis, { [categoryField]: row.catRawValue });
15871
- drawConnector(ctx, pen, pixelDots[0].x, pixelDots[pixelDots.length - 1].x, cy, tokens);
15872
- for (const dot of pixelDots) drawDot(ctx, pen, dot.x, cy, dot.color, tokens);
16031
+ drawConnector(ctx, pen, pixelDots[0].x, pixelDots[pixelDots.length - 1].x, cy, connectorW, tokens);
16032
+ for (const dot of pixelDots) drawDot(ctx, pen, dot.x, cy, dotR, dot.color, tokens);
15873
16033
  ctx.globalAlpha = 1;
15874
16034
  addOverlayText(surface, tokens, {
15875
16035
  left: content.x,
@@ -15886,7 +16046,7 @@ function drawDumbbell(surface, spec, tokens, size, context) {
15886
16046
  const dot = pixelDots[i];
15887
16047
  const atLeft = i === 0 && pixelDots.length > 1;
15888
16048
  addOverlayText(surface, tokens, {
15889
- left: atLeft ? dot.x - 7 : dot.x + 7,
16049
+ left: atLeft ? dot.x - labelGap : dot.x + labelGap,
15890
16050
  top: cy,
15891
16051
  width: 54,
15892
16052
  text: formatDumbbellValue(dot.value, model.format),
@@ -16074,10 +16234,13 @@ function labelBox(model, horizontalRule, pos, p) {
16074
16234
  const inset = 6;
16075
16235
  if (horizontalRule) {
16076
16236
  const top = p - 3;
16077
- const transform3 = "translateY(-100%)";
16078
- if (pos === "start") return { left: plot.x + inset, top, width: plot.width - inset, align: "left", transform: transform3 };
16079
- if (pos === "middle") return { left: plot.x, top, width: plot.width, align: "center", transform: transform3 };
16080
- return { left: plot.x, top, width: plot.width - inset, align: "right", transform: transform3 };
16237
+ if (pos === "start") {
16238
+ return { left: plot.x + inset, top, align: "left", transform: "translateY(-100%)" };
16239
+ }
16240
+ if (pos === "middle") {
16241
+ return { left: plot.x + plot.width / 2, top, align: "center", transform: "translateX(-50%) translateY(-100%)" };
16242
+ }
16243
+ return { left: plot.x + plot.width - inset, top, align: "right", transform: "translateX(-100%) translateY(-100%)" };
16081
16244
  }
16082
16245
  const left = p;
16083
16246
  const transform2 = "translateX(-50%)";
@@ -16252,7 +16415,8 @@ function drawTrendlineLabels(surface, model) {
16252
16415
  const y1 = model.y.pixel(g.fit.predict(g.xMax));
16253
16416
  if (x1 == null || !Number.isFinite(x1) || !Number.isFinite(y1)) continue;
16254
16417
  const text = `R\xB2=${g.fit.r2.toFixed(2)}`;
16255
- const nearRight = x1 > plot.x + plot.width - 44;
16418
+ const labelW = measureText(text, font).width;
16419
+ const nearRight = x1 + 6 + labelW > plot.x + plot.width;
16256
16420
  const left = nearRight ? x1 - 6 : x1 + 6;
16257
16421
  const align = nearRight ? "right" : "left";
16258
16422
  const transform2 = nearRight ? "translateX(-100%) translateY(-50%)" : "translateY(-50%)";
@@ -16767,6 +16931,7 @@ function dependentParams(highlight, filter, ownParams = []) {
16767
16931
  var MARK_CONTRAST_MIN = 1.5;
16768
16932
  var TEXT_CONTRAST_MIN = 4.5;
16769
16933
  var MAX_DISTINGUISHABLE_SERIES = 8;
16934
+ var DEFAULT_POINT_MARKER_RADIUS = 3.5;
16770
16935
  var SEVERITY_RANK = { error: 0, warning: 1, info: 2 };
16771
16936
  var VALUE_CHART_TYPES = /* @__PURE__ */ new Set(["kpi", "gauge", "bullet"]);
16772
16937
  var CONTINUOUS_COLOR_TYPES = /* @__PURE__ */ new Set([
@@ -16824,6 +16989,7 @@ function buildRenderReport(input) {
16824
16989
  if (model) {
16825
16990
  checkDegenerateAxis(model, add);
16826
16991
  checkClippedMarks(model, spec, add);
16992
+ checkAnnotationOverflow(model, spec, tokens, add);
16827
16993
  checkAxisLabelOverlap(model, spec, tokens, add);
16828
16994
  checkLegendOverflow(model, spec, add);
16829
16995
  }
@@ -16943,6 +17109,69 @@ function checkClippedMarks(model, spec, add) {
16943
17109
  });
16944
17110
  }
16945
17111
  }
17112
+ function checkAnnotationOverflow(model, spec, tokens, add) {
17113
+ if (model.orientation === "horizontal") return;
17114
+ const explicit = spec.annotations;
17115
+ const points = Array.isArray(explicit) ? explicit.filter((a) => a.type === "point") : [];
17116
+ for (const a of autoInsightAnnotations(spec)) points.push(a);
17117
+ if (points.length === 0) return;
17118
+ const { plot } = model;
17119
+ const fontSize = tokens.font.size.small;
17120
+ const font = fontString(fontSize, tokens.font.family, tokens.font.weight.medium);
17121
+ const EPS = 0.75;
17122
+ const sides = /* @__PURE__ */ new Set();
17123
+ const artifacts = /* @__PURE__ */ new Set();
17124
+ let clippedAnnotations = 0;
17125
+ let clippedArtifacts = 0;
17126
+ for (const ann of points) {
17127
+ const cat = model.x.pixel(ann.x);
17128
+ const val = model.y.pixel(ann.y);
17129
+ if (cat == null || val == null || !Number.isFinite(cat) || !Number.isFinite(val)) continue;
17130
+ const { x: px, y: py } = model.project(cat, val);
17131
+ const r = ann.markerRadius ?? DEFAULT_POINT_MARKER_RADIUS;
17132
+ const halo = r + 1.6;
17133
+ const markerSides = overflowSides(px - halo, py - halo, px + halo, py + halo, plot, EPS);
17134
+ let annClipped = false;
17135
+ if (markerSides.length) {
17136
+ clippedArtifacts++;
17137
+ annClipped = true;
17138
+ artifacts.add("marker");
17139
+ markerSides.forEach((s) => sides.add(s));
17140
+ }
17141
+ if (!ann.label) continue;
17142
+ const gap = r + 4;
17143
+ const width = measureText(ann.label, font).width + 6;
17144
+ const aboveTop = py - gap - fontSize;
17145
+ const top = aboveTop < plot.y ? py + gap : aboveTop;
17146
+ const labelSides = overflowSides(px - width / 2, top, px + width / 2, top + fontSize, plot, EPS);
17147
+ if (labelSides.length) {
17148
+ clippedArtifacts++;
17149
+ annClipped = true;
17150
+ artifacts.add("label");
17151
+ labelSides.forEach((s) => sides.add(s));
17152
+ }
17153
+ if (annClipped) clippedAnnotations++;
17154
+ }
17155
+ if (clippedAnnotations > 0) {
17156
+ const artifactList = [...artifacts].join(" and ");
17157
+ const sideList = [...sides].join(", ");
17158
+ add({
17159
+ code: "annotation-clipped",
17160
+ severity: "warning",
17161
+ message: `${clippedAnnotations} point annotation ${artifactList} overflow${clippedAnnotations === 1 ? "s" : ""} the plot (${sideList}); the chart is too small to fit its annotation labels.`,
17162
+ details: { clipped: clippedAnnotations, clippedArtifacts, artifacts: [...artifacts], sides: [...sides] },
17163
+ hint: "Enlarge the chart, shorten annotation labels, or remove labels/insights that cannot fit."
17164
+ });
17165
+ }
17166
+ }
17167
+ function overflowSides(left, top, right, bottom, plot, eps) {
17168
+ const sides = [];
17169
+ if (left < plot.x - eps) sides.push("left");
17170
+ if (right > plot.x + plot.width + eps) sides.push("right");
17171
+ if (top < plot.y - eps) sides.push("top");
17172
+ if (bottom > plot.y + plot.height + eps) sides.push("bottom");
17173
+ return sides;
17174
+ }
16946
17175
  function checkAxisLabelOverlap(model, spec, tokens, add) {
16947
17176
  const ticks2 = model.xTicks;
16948
17177
  if (ticks2.length < 2) return;
@@ -17030,7 +17259,7 @@ function manualYDomainFix(spec) {
17030
17259
  // src/runtime/facet.ts
17031
17260
  var FACETABLE_TYPES2 = /* @__PURE__ */ new Set(["line", "area", "bar", "scatter"]);
17032
17261
  var FACET_GAP = 16;
17033
- var FACET_PADDING = { top: 12, right: 16, bottom: 12, left: 12 };
17262
+ var FACET_PADDING = { top: 0, right: 0, bottom: 0, left: 0 };
17034
17263
  var SEVERITY_RANK2 = { error: 0, warning: 1, info: 2 };
17035
17264
  function isFaceted(spec) {
17036
17265
  const facet = spec.facet;
@@ -18719,6 +18948,6 @@ function renderToContext(target, spec) {
18719
18948
  // src/index.ts
18720
18949
  var VERSION = "0.1.0";
18721
18950
 
18722
- export { CARTESIAN_TYPES, CHART_TYPES, CanvasLayer, DEFAULT_ENTRANCE_DURATION, DEFAULT_ROUGH_STYLE, DEFAULT_UPDATE_DURATION, DIM_ALPHA, ExpressionError, FACETABLE_TYPES2 as FACETABLE_TYPES, FUNCTION_NAMES, InteractionController, RoughPen, SKETCH_FONT_FAMILY, SKETCH_FONT_NAME, SLICER_TYPES, SR_ONLY_STYLE, Surface, TICK_SIZE, TIME_UNITS, TRANSFORM_KINDS, Tooltip, VERSION, aggregateValues, analyzeChart, animate, applyA11y, applyAggregate, applyBin, applyCalculate, applyFilter, applyFold, applyPatch, applyPick, applyTimeUnit, applyTransform, applyTransforms, arc, area, assertValidSpec, autoInsightAnnotations, backOut, bandScale, bounceOut, buildCartesianInteraction, buildCartesianModel, buildComboModel, buildDataTableFallback, buildFacetModels, buildHistogramModel, buildRenderReport, cartesianInteractionBuilders, cartesianRenderers, categorical, categoricalSchemes, chartTitleText, chartTypeLabel, checkExpression, clamp012 as clamp01, clamp255, compileExpression, compilePredicate, computeBackingSize, computeBins, computeFrame, computeSeriesInsights, contrastRatio, createDiv, createRoughPen, createSelectionStore, cubicIn, cubicInOut, cubicOut, curveCatmullRom, curveLinear, curveMonotoneX, curveStep, curveStepAfter, curveStepBefore, customRenderers, darkTheme, decimate, defaultSpan, dependentParams, diverging, divergingColorScale, divergingSchemes, drawAnnotationLabels, drawAnnotations, drawAxesUnderlay, drawFacet, drawLegend, drawOverlay, drawTitle, drawTrendlineLabels, drawTrendlines, easings, elasticOut, ellipsize, ensureSketchFont, expoInOut, expoOut, facetReport, fillParentStyle, filterRows, fontString, formatDate, formatNumber2 as formatNumber, formatValue, getDevicePixelRatio, groupBy, hashString, inferFieldType, inferFieldTypes, inferValueType, inheritInto, interpolateArray, interpolateNumber, interpolateNumberArray, interpolateObject, interpolateOklab, interpolateRgb, isBrowser, isEmptyValue, isFaceted, isParamClause, isSlicerType, keyFields, legendSwatchWidth, lightTheme, line, linear, linearRegression, linearScale, linearToSrgb, lintSpec, literalToValue, logScale, lttbRun, makeMatcher, matchesValue, measureText, monotoneXTangents, mulberry32, niceDomain, oklabToOklch, oklabToRgb, oklchToOklab, ordinalColorScale, overlayTextToCanvasCmd, paintCanvasText, paintLegendSwatch, parseColor, parseNumberFormat, pivot, placeViews, pointScale, polygonHachureLines, prefersReducedMotion, prettyDate, profileColumns, quadIn, quadInOut, quadOut, rampFromStops, readableTextColor, recommendChart, relativeLuminance, render, renderDashboard, renderToContext, repairReport, repairSpec, resolveCurve, resolveDashboardLayout, resolveDashboardSections, resolveEmphasis, resolveEntrance, resolveFilterValues, resolveInsightOptions, resolveSketch, resolveTheme, resolveUpdate, resolveValueRef, rgbToOklab, rgbaToCss, rotatePoint, roundedRect, sampleArc, sequential, sequentialColorScale, sequentialSchemes, setMeasureContext, setStyle, sinInOut, slicerParamName, smartDate, specFields, srgbToLinear, summarize, summarizeChart, themes, tickIncrement, tickStep, ticks, timeScale, timeTickFormat, timeTicks, toHex, toPointer, tooltipEnabled, truncateTo, validateDashboard, validateSpec, wireViews, withAlpha, withSketchFont };
18951
+ export { CARTESIAN_TYPES, CHART_TYPES, CanvasLayer, DEFAULT_ENTRANCE_DURATION, DEFAULT_ROUGH_STYLE, DEFAULT_UPDATE_DURATION, DIM_ALPHA, ExpressionError, FACETABLE_TYPES2 as FACETABLE_TYPES, FUNCTION_NAMES, InteractionController, NO_PADDING, RoughPen, SKETCH_FONT_FAMILY, SKETCH_FONT_NAME, SLICER_TYPES, SR_ONLY_STYLE, Surface, TICK_SIZE, TIME_UNITS, TRANSFORM_KINDS, Tooltip, VERSION, aggregateValues, analyzeChart, animate, applyA11y, applyAggregate, applyBin, applyCalculate, applyFilter, applyFold, applyPatch, applyPick, applyTimeUnit, applyTransform, applyTransforms, arc, area, assertValidSpec, autoInsightAnnotations, backOut, bandScale, bounceOut, buildCartesianInteraction, buildCartesianModel, buildComboModel, buildDataTableFallback, buildFacetModels, buildHistogramModel, buildRenderReport, cartesianInteractionBuilders, cartesianRenderers, categorical, categoricalSchemes, chartTitleText, chartTypeLabel, checkExpression, clamp012 as clamp01, clamp255, compileExpression, compilePredicate, computeBackingSize, computeBins, computeFrame, computeSeriesInsights, contrastRatio, createDiv, createRoughPen, createSelectionStore, cubicIn, cubicInOut, cubicOut, curveCatmullRom, curveLinear, curveMonotoneX, curveStep, curveStepAfter, curveStepBefore, customRenderers, darkTheme, decimate, defaultSpan, dependentParams, diverging, divergingColorScale, divergingSchemes, drawAnnotationLabels, drawAnnotations, drawAxesUnderlay, drawFacet, drawLegend, drawOverlay, drawTitle, drawTrendlineLabels, drawTrendlines, easings, elasticOut, ellipsize, ensureSketchFont, expoInOut, expoOut, facetReport, fillParentStyle, filterRows, fontString, formatDate, formatNumber2 as formatNumber, formatValue, getDevicePixelRatio, groupBy, hashString, inferFieldType, inferFieldTypes, inferValueType, inheritInto, interpolateArray, interpolateNumber, interpolateNumberArray, interpolateObject, interpolateOklab, interpolateRgb, isBrowser, isEmptyValue, isFaceted, isParamClause, isSlicerType, keyFields, legendSwatchWidth, lightTheme, line, linear, linearRegression, linearScale, linearToSrgb, lintSpec, literalToValue, logScale, lttbRun, makeMatcher, matchesValue, measureText, monotoneXTangents, mulberry32, niceDomain, oklabToOklch, oklabToRgb, oklchToOklab, ordinalColorScale, overlayTextToCanvasCmd, paintCanvasText, paintLegendSwatch, parseColor, parseNumberFormat, pivot, placeViews, pointScale, polygonHachureLines, prefersReducedMotion, prettyDate, profileColumns, quadIn, quadInOut, quadOut, rampFromStops, readableTextColor, recommendChart, relativeLuminance, render, renderDashboard, renderToContext, repairReport, repairSpec, resolveCurve, resolveDashboardLayout, resolveDashboardSections, resolveEmphasis, resolveEntrance, resolveFilterValues, resolveInsightOptions, resolveSketch, resolveTheme, resolveUpdate, resolveValueRef, rgbToOklab, rgbaToCss, rotatePoint, roundedRect, sampleArc, sequential, sequentialColorScale, sequentialSchemes, setMeasureContext, setStyle, sinInOut, slicerParamName, smartDate, specFields, srgbToLinear, summarize, summarizeChart, themes, tickIncrement, tickStep, ticks, timeScale, timeTickFormat, timeTicks, toHex, toPointer, tooltipEnabled, truncateTo, validateDashboard, validateSpec, wireViews, withAlpha, withSketchFont };
18723
18952
  //# sourceMappingURL=index.js.map
18724
18953
  //# sourceMappingURL=index.js.map