graphein 0.12.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
@@ -2349,6 +2349,15 @@ function aggregateValues(values, op) {
2349
2349
  }
2350
2350
  return null;
2351
2351
  }
2352
+ function resolveValueRef(ref, data) {
2353
+ if (ref == null) return null;
2354
+ if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
2355
+ const read = accessor(ref.field);
2356
+ return aggregateValues(
2357
+ (data ?? []).map((d) => read(d)),
2358
+ ref.aggregate ?? "sum"
2359
+ );
2360
+ }
2352
2361
  function groupBy(rows, keyFields2) {
2353
2362
  const grouped = /* @__PURE__ */ new Map();
2354
2363
  for (const row of rows) {
@@ -2646,7 +2655,7 @@ function fieldTitle(f) {
2646
2655
  if (!f) return void 0;
2647
2656
  return f.title ?? f.field;
2648
2657
  }
2649
- function resolveValueRef(ref, data) {
2658
+ function resolveValueRef2(ref, data) {
2650
2659
  if (ref == null) return null;
2651
2660
  if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
2652
2661
  return aggregateValues(data.map(accessor(ref.field)), ref.aggregate ?? "sum");
@@ -2747,9 +2756,9 @@ function analyzeChart(spec) {
2747
2756
  if (rows.length === 0 && !VALUE_TYPES.has(type)) return null;
2748
2757
  if (VALUE_TYPES.has(type)) {
2749
2758
  const s = spec;
2750
- const value = resolveValueRef(s.value, rows);
2759
+ const value = resolveValueRef2(s.value, rows);
2751
2760
  if (value === null) return null;
2752
- const target = resolveValueRef(s.target, rows);
2761
+ const target = resolveValueRef2(s.target, rows);
2753
2762
  const toTarget = target === null ? null : value - target;
2754
2763
  const pctOfTarget = target ? value / target : null;
2755
2764
  return {
@@ -4248,18 +4257,58 @@ function validateSpec(spec) {
4248
4257
  }
4249
4258
  }
4250
4259
  if (type === "kpi") {
4251
- const value = spec.value;
4252
- if (value === void 0) {
4253
- err("value", '"kpi" requires a "value" (a number or { field, aggregate? }).');
4254
- } else if (isObject3(value)) {
4255
- checkField("value.field", value.field);
4256
- } else if (typeof value !== "number") {
4257
- err("value", '"value" must be a number or an object with a "field".');
4260
+ checkValueRef("value", spec.value, "kpi", true);
4261
+ checkValueRef("delta", spec.delta, "delta", false);
4262
+ if (spec.align !== void 0 && !["start", "center", "end"].includes(spec.align)) {
4263
+ err("align", '"align" must be one of: start, center, end.');
4264
+ }
4265
+ if (spec.labelPosition !== void 0 && !["above", "below"].includes(spec.labelPosition)) {
4266
+ err("labelPosition", '"labelPosition" must be one of: above, below.');
4267
+ }
4268
+ if (spec.unit !== void 0 && typeof spec.unit !== "string") {
4269
+ err("unit", '"unit" must be a string.');
4270
+ }
4271
+ if (spec.color !== void 0 && typeof spec.color !== "string") {
4272
+ err("color", '"color" must be a string (a CSS color).');
4273
+ }
4274
+ if (spec.sparkline !== void 0 && typeof spec.sparkline !== "boolean") {
4275
+ if (!isObject3(spec.sparkline)) {
4276
+ err("sparkline", '"sparkline" must be a boolean or an object with { field?, markers? }.');
4277
+ } else {
4278
+ if (spec.sparkline.field !== void 0) checkField("sparkline.field", spec.sparkline.field);
4279
+ if (spec.sparkline.markers !== void 0 && typeof spec.sparkline.markers !== "boolean") {
4280
+ err("sparkline.markers", '"sparkline.markers" must be a boolean.');
4281
+ }
4282
+ }
4283
+ }
4284
+ if (spec.comparisons !== void 0) {
4285
+ if (!Array.isArray(spec.comparisons)) {
4286
+ err("comparisons", '"comparisons" must be an array of { label, delta, amount? }.');
4287
+ } else {
4288
+ spec.comparisons.forEach((c, i) => {
4289
+ if (!isObject3(c)) {
4290
+ err(`comparisons[${i}]`, 'Each comparison must be an object with a "label" and "delta".');
4291
+ return;
4292
+ }
4293
+ if (typeof c.label !== "string" || c.label === "") {
4294
+ err(`comparisons[${i}].label`, 'Each comparison needs a non-empty "label".');
4295
+ }
4296
+ checkValueRef(`comparisons[${i}].delta`, c.delta, "comparison", true);
4297
+ if (c.amount !== void 0) checkValueRef(`comparisons[${i}].amount`, c.amount, "comparison amount", false);
4298
+ if (c.invert !== void 0 && typeof c.invert !== "boolean") {
4299
+ err(`comparisons[${i}].invert`, '"invert" must be a boolean.');
4300
+ }
4301
+ });
4302
+ }
4258
4303
  }
4259
4304
  }
4260
4305
  if (type === "gauge") {
4261
4306
  checkValueRef("value", spec.value, "gauge", true);
4262
4307
  checkValueRef("target", spec.target, "target", false);
4308
+ checkValueRef("compare", spec.compare, "compare", false);
4309
+ if (spec.legend !== void 0 && typeof spec.legend !== "boolean") {
4310
+ err("legend", '"legend" must be a boolean.');
4311
+ }
4263
4312
  const max = spec.max;
4264
4313
  if (max === void 0) {
4265
4314
  err("max", `"gauge" requires a numeric "max" (the scale's full-scale value).`);
@@ -5709,15 +5758,19 @@ function overlayTextToCanvasCmd(o, font) {
5709
5758
  const m = t.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
5710
5759
  return m ? parseFloat(m[1]) * Math.PI / 180 : void 0;
5711
5760
  })();
5761
+ const shiftX100 = t.includes("translateX(-100%)") || t.includes("translate(-100%");
5762
+ const shiftX50 = t.includes("translateX(-50%)") || t.includes("translate(-50%");
5712
5763
  let align = "left";
5713
5764
  let x = o.left;
5714
5765
  const y = o.top;
5715
5766
  if (o.width != null) {
5716
5767
  align = o.align ?? "left";
5717
5768
  x = align === "right" ? o.left + o.width : align === "center" ? o.left + o.width / 2 : o.left;
5718
- } else if (t.includes("translateX(-100%)")) {
5769
+ if (shiftX100) x -= o.width;
5770
+ else if (shiftX50) x -= o.width / 2;
5771
+ } else if (shiftX100) {
5719
5772
  align = "right";
5720
- } else if (t.includes("translateX(-50%)") || t.includes("translate(-50%")) {
5773
+ } else if (shiftX50) {
5721
5774
  align = "center";
5722
5775
  }
5723
5776
  const baseline = t.includes("translateY(-50%)") || t.includes(",-50%") || t.includes(", -50%") ? "middle" : "top";
@@ -6020,6 +6073,7 @@ function resolveSketch(spec) {
6020
6073
 
6021
6074
  // src/layout/frame.ts
6022
6075
  var TICK_SIZE = 6;
6076
+ var NO_PADDING = { top: 0, right: 0, bottom: 0, left: 0 };
6023
6077
  var AXIS_LABEL_GAP = 6;
6024
6078
  var AXIS_TITLE_GAP = 4;
6025
6079
  var LEGEND_SWATCH = 11;
@@ -6638,6 +6692,155 @@ function drawLegend(surface, items, model) {
6638
6692
  }
6639
6693
  }
6640
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
+
6641
6844
  // src/runtime/cartesian.ts
6642
6845
  var CURVES = {
6643
6846
  linear: curveLinear,
@@ -6728,8 +6931,43 @@ function formatNumericTick(value, userFormat) {
6728
6931
  if (userFormat) return formatValue(value, userFormat);
6729
6932
  return formatNumber2(value, ",");
6730
6933
  }
6731
- var DEFAULT_PADDING = { top: 12, right: 16, bottom: 12, left: 12 };
6732
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
+ }
6733
6971
  function legendInteractive(spec) {
6734
6972
  return typeof spec.legend === "object" && spec.legend.interactive === true;
6735
6973
  }
@@ -6894,7 +7132,7 @@ function buildCartesianModel(spec, tokens, opts) {
6894
7132
  color: s.color,
6895
7133
  symbol: spec.type === "scatter" ? "circle" : spec.type === "line" ? "line" : "square"
6896
7134
  }));
6897
- const padding = { ...DEFAULT_PADDING, ...spec.padding };
7135
+ const padding = { ...NO_PADDING, ...spec.padding };
6898
7136
  const titleInput4 = resolveTitle(spec);
6899
7137
  const categoryAxisInput = {
6900
7138
  show: xShow,
@@ -6937,7 +7175,18 @@ function buildCartesianModel(spec, tokens, opts) {
6937
7175
  };
6938
7176
  }) : void 0;
6939
7177
  const plot = frame.plot;
6940
- 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];
6941
7190
  const yScale = yIsLog ? logScale({ domain: yDomain, range: valueRange, base: yBase, clamp: yClamp }) : linearScale({ domain: yDomain, range: valueRange, clamp: yClamp });
6942
7191
  const yPixel = (value) => yScale.map(toNumber(value));
6943
7192
  const valueMin = horizontal ? plot.x : plot.y;
@@ -6948,7 +7197,7 @@ function buildCartesianModel(spec, tokens, opts) {
6948
7197
  type: xType,
6949
7198
  categories,
6950
7199
  domainNum: xDomainNum,
6951
- 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],
6952
7201
  paddingInner: spec.type === "bar" ? 0.2 : spec.type === "box" ? 0.32 : 0.5,
6953
7202
  scaleCfg: enc.x.scale
6954
7203
  });
@@ -7075,21 +7324,32 @@ function maxFinite(values, fallback) {
7075
7324
  return max === -Infinity ? fallback : max;
7076
7325
  }
7077
7326
 
7078
- // src/charts/emphasis.ts
7079
- function rowAlpha(emphasis, row) {
7080
- if (!emphasis) return 1;
7081
- return emphasis.match(row) ? 1 : emphasis.dim;
7327
+ // src/render/metrics.ts
7328
+ function diagonal(plot) {
7329
+ return Math.hypot(plot.width, plot.height);
7082
7330
  }
7083
- function seriesMatches(emphasis, series) {
7084
- if (!emphasis) return true;
7085
- return series.rows.some((row) => emphasis.match(row));
7331
+ function lineStrokeWidth(plot, base = 2) {
7332
+ return clamp(diagonal(plot) / 260, base, 4.5);
7086
7333
  }
7087
- function seriesAlpha(emphasis, series) {
7088
- if (!emphasis) return 1;
7089
- return seriesMatches(emphasis, series) ? 1 : emphasis.dim;
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);
7090
7342
  }
7091
7343
 
7092
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
+ }
7093
7353
  function buildSeriesPoints(model, s) {
7094
7354
  const readX = accessor(model.x.field);
7095
7355
  const readY = accessor(model.y.field ?? "");
@@ -7114,6 +7374,24 @@ function buildSeriesPoints(model, s) {
7114
7374
  gap: () => ({ x: NaN, y: NaN })
7115
7375
  });
7116
7376
  }
7377
+ function matchPointPixels(model, s) {
7378
+ const emphasis = model.emphasis;
7379
+ if (!emphasis) return [];
7380
+ const readX = accessor(model.x.field);
7381
+ const readY = accessor(model.y.field ?? "");
7382
+ const out = [];
7383
+ for (const d of s.rows) {
7384
+ if (!emphasis.match(d)) continue;
7385
+ const px = model.x.pixel(readX(d));
7386
+ if (px == null) continue;
7387
+ const yRaw = readY(d);
7388
+ if (yRaw == null || yRaw === "") continue;
7389
+ const py = model.y.pixel(yRaw);
7390
+ if (Number.isNaN(py)) continue;
7391
+ out.push({ x: px, y: py });
7392
+ }
7393
+ return out;
7394
+ }
7117
7395
  function finiteRuns(points) {
7118
7396
  const runs = [];
7119
7397
  let run = [];
@@ -7128,7 +7406,7 @@ function finiteRuns(points) {
7128
7406
  if (run.length) runs.push(run);
7129
7407
  return runs;
7130
7408
  }
7131
- function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
7409
+ function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints, strokeWidth, markerR, markerSW) {
7132
7410
  if (filled) {
7133
7411
  for (const { series, points, alpha } of allSeries) {
7134
7412
  const prev = ctx.globalAlpha;
@@ -7147,7 +7425,7 @@ function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
7147
7425
  for (const { series, points, alpha } of allSeries) {
7148
7426
  const prev = ctx.globalAlpha;
7149
7427
  if (alpha !== 1) ctx.globalAlpha = alpha;
7150
- pen.trendStroke(points, { stroke: series.color });
7428
+ pen.trendStroke(points, { stroke: series.color, strokeWidth });
7151
7429
  if (alpha !== 1) ctx.globalAlpha = prev;
7152
7430
  }
7153
7431
  if (showPoints) {
@@ -7156,15 +7434,27 @@ function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
7156
7434
  if (alpha !== 1) ctx.globalAlpha = alpha;
7157
7435
  for (const p of points) {
7158
7436
  if (Number.isNaN(p.x) || Number.isNaN(p.y)) continue;
7159
- pen.circle(p.x, p.y, 3.4, {
7437
+ pen.circle(p.x, p.y, markerR, {
7160
7438
  fill: series.color,
7161
7439
  stroke: model.tokens.color.background,
7162
- strokeWidth: 1.5
7440
+ strokeWidth: markerSW
7163
7441
  });
7164
7442
  }
7165
7443
  if (alpha !== 1) ctx.globalAlpha = prev;
7166
7444
  }
7167
7445
  }
7446
+ for (const { series, mode, matchPoints } of allSeries) {
7447
+ if (mode !== "partial") continue;
7448
+ ctx.globalAlpha = 1;
7449
+ for (const p of matchPoints) {
7450
+ pen.circle(p.x, p.y, markerR, {
7451
+ fill: series.color,
7452
+ stroke: model.tokens.color.background,
7453
+ strokeWidth: markerSW
7454
+ });
7455
+ }
7456
+ }
7457
+ ctx.globalAlpha = 1;
7168
7458
  }
7169
7459
  function drawLine(surface, model) {
7170
7460
  const ctx = surface.marks.ctx;
@@ -7173,18 +7463,26 @@ function drawLine(surface, model) {
7173
7463
  const curve = resolveCurve(spec.curve);
7174
7464
  const lineGen = line({ curve });
7175
7465
  const areaGen = spec.area ? area({ curve }) : null;
7176
- const allSeries = model.series.map((s) => ({
7177
- series: s,
7178
- points: buildSeriesPoints(model, s),
7179
- alpha: seriesAlpha(model.emphasis, s)
7180
- }));
7466
+ const allSeries = model.series.map((s) => {
7467
+ const mode = seriesEmphasisMode(model.emphasis, s);
7468
+ return {
7469
+ series: s,
7470
+ points: buildSeriesPoints(model, s),
7471
+ alpha: mode === "all" ? 1 : model.emphasis?.dim ?? 1,
7472
+ mode,
7473
+ matchPoints: matchPointPixels(model, s)
7474
+ };
7475
+ });
7181
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);
7182
7480
  ctx.save();
7183
7481
  ctx.beginPath();
7184
7482
  ctx.rect(plot.x, plot.y, plot.width, plot.height);
7185
7483
  ctx.clip();
7186
7484
  if (model.sketch) {
7187
- 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);
7188
7486
  ctx.restore();
7189
7487
  return;
7190
7488
  }
@@ -7207,7 +7505,7 @@ function drawLine(surface, model) {
7207
7505
  }
7208
7506
  ctx.lineJoin = "round";
7209
7507
  ctx.lineCap = "round";
7210
- ctx.lineWidth = Math.max(1.75, tokens.stroke.thick);
7508
+ ctx.lineWidth = strokeWidth;
7211
7509
  for (const { series, points, alpha } of allSeries) {
7212
7510
  ctx.beginPath();
7213
7511
  lineGen(points, ctx);
@@ -7220,28 +7518,52 @@ function drawLine(surface, model) {
7220
7518
  for (const { series, points, alpha } of allSeries) {
7221
7519
  ctx.fillStyle = series.color;
7222
7520
  ctx.strokeStyle = tokens.color.background;
7223
- ctx.lineWidth = 1.5;
7521
+ ctx.lineWidth = markerSW;
7224
7522
  ctx.globalAlpha = alpha;
7225
7523
  for (const p of points) {
7226
7524
  if (Number.isNaN(p.x) || Number.isNaN(p.y)) continue;
7227
7525
  ctx.beginPath();
7228
- ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
7526
+ ctx.arc(p.x, p.y, markerR, 0, Math.PI * 2);
7229
7527
  ctx.fill();
7230
7528
  ctx.stroke();
7231
7529
  }
7232
7530
  ctx.globalAlpha = 1;
7233
7531
  }
7234
7532
  }
7533
+ for (const { series, mode, matchPoints } of allSeries) {
7534
+ if (mode !== "partial") continue;
7535
+ ctx.fillStyle = series.color;
7536
+ ctx.strokeStyle = tokens.color.background;
7537
+ ctx.lineWidth = markerSW;
7538
+ ctx.globalAlpha = 1;
7539
+ for (const p of matchPoints) {
7540
+ ctx.beginPath();
7541
+ ctx.arc(p.x, p.y, markerR, 0, Math.PI * 2);
7542
+ ctx.fill();
7543
+ ctx.stroke();
7544
+ }
7545
+ }
7546
+ ctx.globalAlpha = 1;
7235
7547
  ctx.restore();
7236
7548
  }
7237
7549
 
7238
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
+ }
7239
7559
  function buildBands(model, stacked) {
7240
7560
  const readX = accessor(model.x.field);
7241
7561
  const readY = accessor(model.y.field ?? "");
7242
7562
  const cumulative = /* @__PURE__ */ new Map();
7243
7563
  const threshold = stacked ? Infinity : Math.max(2, Math.round(model.plot.width));
7244
7564
  return model.series.map((series) => {
7565
+ const mode = seriesEmphasisMode(model.emphasis, series);
7566
+ const matchPoints = [];
7245
7567
  const points = [];
7246
7568
  for (const row of series.rows) {
7247
7569
  const xv = readX(row);
@@ -7254,19 +7576,23 @@ function buildBands(model, stacked) {
7254
7576
  const base = cumulative.get(key) ?? 0;
7255
7577
  const top = base + (missing ? 0 : value);
7256
7578
  cumulative.set(key, top);
7579
+ const y1 = missing ? NaN : model.y.pixel(top);
7257
7580
  points.push({
7258
7581
  x: px == null ? NaN : px,
7259
7582
  y0: model.y.pixel(base),
7260
- y1: missing ? NaN : model.y.pixel(top),
7583
+ y1,
7261
7584
  sortKey: px == null ? Number.POSITIVE_INFINITY : px
7262
7585
  });
7586
+ if (model.emphasis?.match(row) && px != null && !Number.isNaN(y1)) matchPoints.push({ x: px, y: y1 });
7263
7587
  } else {
7588
+ const y1 = missing ? NaN : model.y.pixel(value);
7264
7589
  points.push({
7265
7590
  x: px == null ? NaN : px,
7266
7591
  y0: model.y.baseline,
7267
- y1: missing ? NaN : model.y.pixel(value),
7592
+ y1,
7268
7593
  sortKey: px == null ? Number.POSITIVE_INFINITY : px
7269
7594
  });
7595
+ if (model.emphasis?.match(row) && px != null && !Number.isNaN(y1)) matchPoints.push({ x: px, y: y1 });
7270
7596
  }
7271
7597
  }
7272
7598
  points.sort((a, b) => a.sortKey - b.sortKey);
@@ -7276,10 +7602,10 @@ function buildBands(model, stacked) {
7276
7602
  getY: (p) => p.y1,
7277
7603
  gap: () => ({ x: NaN, y0: NaN, y1: NaN })
7278
7604
  }) : band;
7279
- return { series, points: reduced, alpha: seriesAlpha(model.emphasis, series) };
7605
+ return { series, points: reduced, alpha: mode === "all" ? 1 : model.emphasis?.dim ?? 1, mode, matchPoints };
7280
7606
  });
7281
7607
  }
7282
- function drawAreaSketch(ctx, model, bands, pen, stacked) {
7608
+ function drawAreaSketch(ctx, model, bands, pen, stacked, strokeWidth, markerR, markerSW) {
7283
7609
  for (const { series, points, alpha } of bands) {
7284
7610
  const prev = ctx.globalAlpha;
7285
7611
  if (alpha !== 1) ctx.globalAlpha = alpha;
@@ -7307,10 +7633,22 @@ function drawAreaSketch(ctx, model, bands, pen, stacked) {
7307
7633
  if (alpha !== 1) ctx.globalAlpha = alpha;
7308
7634
  pen.trendStroke(
7309
7635
  points.map((p) => ({ x: p.x, y: p.y1 })),
7310
- { stroke: series.color }
7636
+ { stroke: series.color, strokeWidth }
7311
7637
  );
7312
7638
  if (alpha !== 1) ctx.globalAlpha = prev;
7313
7639
  }
7640
+ for (const { series, mode, matchPoints } of bands) {
7641
+ if (mode !== "partial") continue;
7642
+ ctx.globalAlpha = 1;
7643
+ for (const p of matchPoints) {
7644
+ pen.circle(p.x, p.y, markerR, {
7645
+ fill: series.color,
7646
+ stroke: model.tokens.color.background,
7647
+ strokeWidth: markerSW
7648
+ });
7649
+ }
7650
+ }
7651
+ ctx.globalAlpha = 1;
7314
7652
  }
7315
7653
  function drawArea(surface, model) {
7316
7654
  const ctx = surface.marks.ctx;
@@ -7321,12 +7659,15 @@ function drawArea(surface, model) {
7321
7659
  const lineGen = line({ curve });
7322
7660
  const stacked = model.stacked;
7323
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);
7324
7665
  ctx.save();
7325
7666
  ctx.beginPath();
7326
7667
  ctx.rect(plot.x, plot.y, plot.width, plot.height);
7327
7668
  ctx.clip();
7328
7669
  if (model.sketch) {
7329
- drawAreaSketch(ctx, model, bands, new RoughPen(ctx, model.sketch), stacked);
7670
+ drawAreaSketch(ctx, model, bands, new RoughPen(ctx, model.sketch), stacked, strokeWidth, markerR, markerSW);
7330
7671
  ctx.restore();
7331
7672
  return;
7332
7673
  }
@@ -7348,7 +7689,7 @@ function drawArea(surface, model) {
7348
7689
  }
7349
7690
  ctx.lineJoin = "round";
7350
7691
  ctx.lineCap = "round";
7351
- ctx.lineWidth = Math.max(1.5, tokens.stroke.base);
7692
+ ctx.lineWidth = strokeWidth;
7352
7693
  for (const { series, points, alpha } of bands) {
7353
7694
  ctx.beginPath();
7354
7695
  lineGen(
@@ -7360,6 +7701,20 @@ function drawArea(surface, model) {
7360
7701
  ctx.stroke();
7361
7702
  ctx.globalAlpha = 1;
7362
7703
  }
7704
+ for (const { series, mode, matchPoints } of bands) {
7705
+ if (mode !== "partial") continue;
7706
+ ctx.fillStyle = series.color;
7707
+ ctx.strokeStyle = tokens.color.background;
7708
+ ctx.lineWidth = markerSW;
7709
+ ctx.globalAlpha = 1;
7710
+ for (const p of matchPoints) {
7711
+ ctx.beginPath();
7712
+ ctx.arc(p.x, p.y, markerR, 0, Math.PI * 2);
7713
+ ctx.fill();
7714
+ ctx.stroke();
7715
+ }
7716
+ }
7717
+ ctx.globalAlpha = 1;
7363
7718
  ctx.restore();
7364
7719
  }
7365
7720
 
@@ -7427,6 +7782,8 @@ function drawGroupedBars(ctx, model, radius, pen) {
7427
7782
  const subWidth = seriesCount > 1 ? bandWidth / seriesCount : bandWidth;
7428
7783
  const gap = seriesCount > 1 && subWidth > 3 ? Math.min(1, subWidth * 0.15) : 0;
7429
7784
  const barThick = Math.max(0, subWidth - gap);
7785
+ const t = model.entranceProgress ?? 1;
7786
+ const grow = (pixel) => model.y.baseline + (pixel - model.y.baseline) * t;
7430
7787
  model.series.forEach((series, seriesIndex) => {
7431
7788
  ctx.fillStyle = series.color;
7432
7789
  for (const row of series.rows) {
@@ -7436,7 +7793,7 @@ function drawGroupedBars(ctx, model, radius, pen) {
7436
7793
  const center2 = model.x.pixel(readX(row));
7437
7794
  if (center2 == null) continue;
7438
7795
  const catStart = center2 - bandWidth / 2 + seriesIndex * subWidth + gap / 2;
7439
- const rect = orientedRect(horizontal, catStart, barThick, model.y.baseline, model.y.pixel(value), series.color);
7796
+ const rect = orientedRect(horizontal, catStart, barThick, grow(model.y.baseline), grow(model.y.pixel(value)), series.color);
7440
7797
  if (rect) drawRect(ctx, rect, radius, pen, rowAlpha(model.emphasis, row));
7441
7798
  }
7442
7799
  });
@@ -7450,6 +7807,8 @@ function drawStackedBars(ctx, model, radius, pen) {
7450
7807
  const segments = [];
7451
7808
  const roundedEnds = /* @__PURE__ */ new Map();
7452
7809
  const bandWidth = model.x.bandwidth;
7810
+ const t = model.entranceProgress ?? 1;
7811
+ const grow = (pixel) => model.y.baseline + (pixel - model.y.baseline) * t;
7453
7812
  for (const series of model.series) {
7454
7813
  for (const row of series.rows) {
7455
7814
  const xValue = readX(row);
@@ -7464,7 +7823,7 @@ function drawStackedBars(ctx, model, radius, pen) {
7464
7823
  const base = totals.get(categoryKey2) ?? 0;
7465
7824
  const top = base + value;
7466
7825
  totals.set(categoryKey2, top);
7467
- const rect = orientedRect(horizontal, center2 - bandWidth / 2, bandWidth, model.y.pixel(base), model.y.pixel(top), series.color);
7826
+ const rect = orientedRect(horizontal, center2 - bandWidth / 2, bandWidth, grow(model.y.pixel(base)), grow(model.y.pixel(top)), series.color);
7468
7827
  if (!rect) continue;
7469
7828
  segments.push({ ...rect, stackEndKey, alpha: rowAlpha(model.emphasis, row) });
7470
7829
  roundedEnds.set(stackEndKey, segments.length - 1);
@@ -7494,133 +7853,6 @@ function drawBar(surface, model) {
7494
7853
  ctx.restore();
7495
7854
  }
7496
7855
 
7497
- // src/charts/scatter.ts
7498
- var QUANTIZE_MIN_POINTS = 4e3;
7499
- var QUANTIZE_AREA_DIVISOR = 4;
7500
- function isMissing(value) {
7501
- return value == null || value === "";
7502
- }
7503
- function isInvalidPositionValue(value) {
7504
- return isMissing(value) || typeof value === "number" && Number.isNaN(value) || value instanceof Date && Number.isNaN(value.getTime());
7505
- }
7506
- function fixedRadius() {
7507
- return 3.75;
7508
- }
7509
- function shouldQuantize(count, plot, hasSize) {
7510
- return !hasSize && count > QUANTIZE_MIN_POINTS && count > plot.width * plot.height / QUANTIZE_AREA_DIVISOR;
7511
- }
7512
- function quantizePoints(points) {
7513
- if (points.length === 0) return [];
7514
- let minX = Infinity;
7515
- let maxX = -Infinity;
7516
- let minY = Infinity;
7517
- let maxY = -Infinity;
7518
- for (const p of points) {
7519
- if (p.x < minX) minX = p.x;
7520
- if (p.x > maxX) maxX = p.x;
7521
- if (p.y < minY) minY = p.y;
7522
- if (p.y > maxY) maxY = p.y;
7523
- }
7524
- const seen = /* @__PURE__ */ new Set();
7525
- const keep = [];
7526
- for (const p of points) {
7527
- const extentPoint = p.x === minX || p.x === maxX || p.y === minY || p.y === maxY;
7528
- const ix = Math.round(p.x);
7529
- const iy = Math.round(p.y);
7530
- const key = `${p.seriesKey}|${ix}|${iy}`;
7531
- if (extentPoint || !seen.has(key)) {
7532
- keep.push(p);
7533
- seen.add(key);
7534
- }
7535
- }
7536
- return keep;
7537
- }
7538
- function radiusScale(rows, field, plotSize) {
7539
- const rMin = 3;
7540
- const rMax = clamp(plotSize * 0.05, 10, 26);
7541
- const mid = (rMin + rMax) / 2;
7542
- const domain = extent(rows, field);
7543
- if (!domain) return () => mid;
7544
- const [min, max] = domain;
7545
- if (min === max) return () => mid;
7546
- return (value) => {
7547
- const n = toNumber(value);
7548
- if (Number.isNaN(n)) return rMin;
7549
- const norm = clamp((n - min) / (max - min), 0, 1);
7550
- return rMin + (rMax - rMin) * Math.sqrt(norm);
7551
- };
7552
- }
7553
- function drawScatter(surface, model) {
7554
- const ctx = surface.marks.ctx;
7555
- const spec = model.spec;
7556
- const { plot, tokens } = model;
7557
- const readX = accessor(model.x.field);
7558
- const readY = accessor(model.y.field ?? "");
7559
- const sizeField = spec.encoding.size?.field;
7560
- const allRows = model.series.flatMap((series) => series.rows);
7561
- const readSize = sizeField ? accessor(sizeField) : null;
7562
- const radius = sizeField ? radiusScale(allRows, sizeField, Math.min(plot.width, plot.height)) : fixedRadius;
7563
- const points = [];
7564
- for (const series of model.series) {
7565
- for (const row of series.rows) {
7566
- const xValue = readX(row);
7567
- const yValue = readY(row);
7568
- const yNumber = toNumber(yValue);
7569
- const px = model.x.pixel(xValue);
7570
- if (px == null || isInvalidPositionValue(xValue) || isMissing(yValue) || Number.isNaN(yNumber)) {
7571
- continue;
7572
- }
7573
- const py = model.y.pixel(yValue);
7574
- if (!Number.isFinite(px) || !Number.isFinite(py)) continue;
7575
- points.push({
7576
- x: px,
7577
- y: py,
7578
- r: readSize ? radius(readSize(row)) : radius(),
7579
- color: series.color,
7580
- alpha: rowAlpha(model.emphasis, row),
7581
- seriesKey: series.key
7582
- });
7583
- }
7584
- }
7585
- const originalCount = points.length;
7586
- const drawPoints = shouldQuantize(originalCount, plot, !!sizeField) ? quantizePoints(points) : points;
7587
- model.markStats = { drawn: drawPoints.length, original: originalCount };
7588
- if (sizeField) drawPoints.sort((a, b) => b.r - a.r);
7589
- ctx.save();
7590
- ctx.beginPath();
7591
- ctx.rect(plot.x, plot.y, plot.width, plot.height);
7592
- ctx.clip();
7593
- if (model.sketch) {
7594
- const pen = new RoughPen(ctx, model.sketch);
7595
- for (const point of drawPoints) {
7596
- const prev = ctx.globalAlpha;
7597
- if (point.alpha !== 1) ctx.globalAlpha = point.alpha;
7598
- pen.circle(point.x, point.y, point.r, {
7599
- fill: point.color,
7600
- fillAlpha: 0.7,
7601
- stroke: tokens.color.background,
7602
- strokeWidth: 1
7603
- });
7604
- if (point.alpha !== 1) ctx.globalAlpha = prev;
7605
- }
7606
- ctx.restore();
7607
- return;
7608
- }
7609
- ctx.lineWidth = 1;
7610
- ctx.strokeStyle = tokens.color.background;
7611
- for (const point of drawPoints) {
7612
- ctx.beginPath();
7613
- ctx.arc(point.x, point.y, point.r, 0, Math.PI * 2);
7614
- ctx.fillStyle = point.color;
7615
- ctx.globalAlpha = 0.7 * point.alpha;
7616
- ctx.fill();
7617
- ctx.globalAlpha = point.alpha;
7618
- ctx.stroke();
7619
- }
7620
- ctx.globalAlpha = 1;
7621
- ctx.restore();
7622
- }
7623
-
7624
7856
  // src/charts/box.ts
7625
7857
  function quantileSorted(sorted, p) {
7626
7858
  const n = sorted.length;
@@ -7865,7 +8097,7 @@ function buildBoxInteraction(model) {
7865
8097
  }
7866
8098
 
7867
8099
  // src/charts/chrome.ts
7868
- var CHROME_PAD = { top: 14, right: 16, bottom: 14, left: 16 };
8100
+ var CHROME_PAD = { top: 0, right: 0, bottom: 0, left: 0 };
7869
8101
  function resolveTitle2(title2) {
7870
8102
  if (typeof title2 === "string") return { text: title2, align: "left" };
7871
8103
  if (title2 && typeof title2 === "object") {
@@ -8597,6 +8829,8 @@ function drawPie(surface, spec, tokens, size, context) {
8597
8829
  const colorField = pie.encoding.color.field;
8598
8830
  const emphasis = context?.emphasis ?? null;
8599
8831
  const sliceAlpha = (slice) => emphasis ? emphasis.match({ [colorField]: slice.category }) ? 1 : emphasis.dim : 1;
8832
+ const sweepT = context?.sweepProgress ?? 1;
8833
+ const sweepEnd = TAU3 * sweepT;
8600
8834
  const content = drawTitleBlock(surface, tokens, size, pie.title);
8601
8835
  const slices = buildSlices(pie, tokens.color.palette);
8602
8836
  const legendPosition = resolveLegendPosition(pie, content.width, content.height);
@@ -8654,9 +8888,11 @@ function drawPie(surface, spec, tokens, size, context) {
8654
8888
  if (sketch) {
8655
8889
  const pen = new RoughPen(ctx, sketch);
8656
8890
  for (const slice of slices) {
8891
+ const end = Math.min(slice.endAngle, sweepEnd);
8892
+ if (end <= slice.startAngle) continue;
8657
8893
  const alpha = sliceAlpha(slice);
8658
8894
  if (alpha !== 1) ctx.globalAlpha = alpha;
8659
- pen.wedge(cx, cy, innerRadius, outerRadius, slice.startAngle, slice.endAngle, {
8895
+ pen.wedge(cx, cy, innerRadius, outerRadius, slice.startAngle, end, {
8660
8896
  fill: slice.color,
8661
8897
  stroke: separator
8662
8898
  });
@@ -8665,11 +8901,13 @@ function drawPie(surface, spec, tokens, size, context) {
8665
8901
  ctx.restore();
8666
8902
  } else {
8667
8903
  for (const slice of slices) {
8904
+ const end = Math.min(slice.endAngle, sweepEnd);
8905
+ if (end <= slice.startAngle) continue;
8668
8906
  const wedge = arc({
8669
8907
  innerRadius,
8670
8908
  outerRadius,
8671
8909
  startAngle: slice.startAngle,
8672
- endAngle: slice.endAngle
8910
+ endAngle: end
8673
8911
  });
8674
8912
  ctx.beginPath();
8675
8913
  wedge(ctx, cx, cy);
@@ -8681,7 +8919,7 @@ function drawPie(surface, spec, tokens, size, context) {
8681
8919
  }
8682
8920
  ctx.restore();
8683
8921
  }
8684
- if (labelPlan && labelCfg) {
8922
+ if (sweepT >= 1 && labelPlan && labelCfg) {
8685
8923
  ctx.save();
8686
8924
  ctx.lineWidth = 1;
8687
8925
  ctx.lineJoin = "round";
@@ -8725,7 +8963,7 @@ function drawPie(surface, spec, tokens, size, context) {
8725
8963
  });
8726
8964
  }
8727
8965
  }
8728
- if (innerRadius >= 34) {
8966
+ if (sweepT >= 1 && innerRadius >= 34) {
8729
8967
  addOverlayText(surface, tokens, {
8730
8968
  left: cx,
8731
8969
  top: cy - tokens.font.size.small - 2,
@@ -9010,19 +9248,9 @@ function drawFunnel(surface, spec, tokens, size, context) {
9010
9248
 
9011
9249
  // src/charts/kpi.ts
9012
9250
  var SVG_NS = "http://www.w3.org/2000/svg";
9013
- var SKETCH_BORDER_RADIUS = "255px 15px 225px 15px / 15px 225px 15px 255px";
9014
9251
  function clamp5(value, min, max) {
9015
9252
  return Math.max(min, Math.min(max, value));
9016
9253
  }
9017
- function resolveValue(ref, data) {
9018
- if (ref == null) return null;
9019
- if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
9020
- const read = accessor(ref.field);
9021
- return aggregateValues(
9022
- data.map((d) => read(d)),
9023
- ref.aggregate ?? "sum"
9024
- );
9025
- }
9026
9254
  function valueField(ref) {
9027
9255
  return typeof ref === "number" ? null : ref.field;
9028
9256
  }
@@ -9033,6 +9261,12 @@ function formatDelta(delta, specFormat) {
9033
9261
  if (abs <= 1) return sign2 + formatNumber2(abs, ".1%");
9034
9262
  return sign2 + formatNumber2(abs, ",.0f");
9035
9263
  }
9264
+ function flexAlign(align) {
9265
+ return align === "start" ? "flex-start" : align === "end" ? "flex-end" : "center";
9266
+ }
9267
+ function textAlignOf(align) {
9268
+ return align === "start" ? "left" : align === "end" ? "right" : "center";
9269
+ }
9036
9270
  function cardRectFor(surface, tokens, size, spec) {
9037
9271
  if (spec.title) return drawTitleBlock(surface, tokens, size, spec.title);
9038
9272
  return {
@@ -9042,41 +9276,25 @@ function cardRectFor(surface, tokens, size, spec) {
9042
9276
  height: Math.max(0, size.height - CHROME_PAD.top - CHROME_PAD.bottom)
9043
9277
  };
9044
9278
  }
9045
- function createCard(tokens, rect, hasSparkline, framed = false) {
9046
- const card = document.createElement("div");
9047
- const pad3 = tokens.spacing.lg;
9048
- const sparkReserve = hasSparkline ? Math.min(44, Math.max(28, rect.height * 0.18)) + tokens.spacing.sm : 0;
9049
- card.style.position = "absolute";
9050
- card.style.left = `${rect.x}px`;
9051
- card.style.top = `${rect.y}px`;
9052
- card.style.width = `${rect.width}px`;
9053
- card.style.height = `${rect.height}px`;
9054
- card.style.boxSizing = "border-box";
9055
- card.style.background = framed ? "transparent" : tokens.color.surface;
9056
- card.style.border = framed ? "none" : `1px solid ${tokens.color.border}`;
9057
- card.style.borderRadius = `${tokens.radius.lg}px`;
9058
- card.style.padding = framed ? `0 0 ${sparkReserve}px` : `${pad3}px ${pad3}px ${pad3 + sparkReserve}px`;
9059
- card.style.display = "flex";
9060
- card.style.flexDirection = "column";
9061
- card.style.justifyContent = "center";
9062
- card.style.alignItems = "flex-start";
9063
- card.style.gap = `${tokens.spacing.sm}px`;
9064
- card.style.overflow = "hidden";
9065
- card.style.pointerEvents = "none";
9066
- return card;
9279
+ function valueFontSize(rect, contentWidth, valueText, unit, isRich) {
9280
+ const effectiveLen = valueText.length + (unit ? unit.length * 0.5 + 0.4 : 0);
9281
+ const sizeByCard = Math.min(rect.width, rect.height) * (isRich ? 0.17 : 0.28);
9282
+ const sizeByText = contentWidth / Math.max(1, effectiveLen * 0.58);
9283
+ const lo = isRich ? 22 : 26;
9284
+ const hi = isRich ? 52 : 76;
9285
+ return clamp5(Math.min(sizeByCard, sizeByText), lo, hi);
9067
9286
  }
9068
- function addText2(card, text, style) {
9069
- const el = document.createElement("div");
9070
- el.textContent = text;
9071
- Object.assign(el.style, style);
9072
- card.appendChild(el);
9073
- return el;
9287
+ function unitFontSize(valueSize) {
9288
+ return Math.max(12, valueSize * 0.46);
9074
9289
  }
9075
9290
  function sparklineField(spec) {
9076
9291
  if (!spec.sparkline) return null;
9077
- if (typeof spec.sparkline === "object") return spec.sparkline.field;
9292
+ if (typeof spec.sparkline === "object") return spec.sparkline.field ?? valueField(spec.value);
9078
9293
  return valueField(spec.value);
9079
9294
  }
9295
+ function sparklineMarkers(spec) {
9296
+ return typeof spec.sparkline === "object" ? spec.sparkline.markers !== false : true;
9297
+ }
9080
9298
  function sparklineValues(data, field) {
9081
9299
  const read = accessor(field);
9082
9300
  const values = [];
@@ -9086,41 +9304,165 @@ function sparklineValues(data, field) {
9086
9304
  }
9087
9305
  return values;
9088
9306
  }
9089
- function buildSparkline(tokens, width, height, values) {
9307
+ function kpiSparklineSeries(spec, data) {
9308
+ const field = sparklineField(spec);
9309
+ return field ? sparklineValues(data, field) : [];
9310
+ }
9311
+ function morphSparkline(to, from, progress) {
9312
+ if (progress == null || progress >= 1 || to.length === 0) return to;
9313
+ const t = progress;
9314
+ if (from && from.length === to.length) return to.map((v, i) => from[i] + (v - from[i]) * t);
9315
+ const base = to[0];
9316
+ return to.map((v) => base + (v - base) * t);
9317
+ }
9318
+ function resolveComparison(cmp, data, parentFormat) {
9319
+ const delta = resolveValueRef(cmp.delta, data);
9320
+ if (delta == null) return null;
9321
+ const rawPositive = delta >= 0;
9322
+ const tri = rawPositive ? "\u25B2" : "\u25BC";
9323
+ let text = `${tri} ${formatValue(delta, cmp.format ?? ".0%")}`;
9324
+ if (cmp.amount !== void 0) {
9325
+ const amount = resolveValueRef(cmp.amount, data);
9326
+ if (amount != null) text += ` : ${formatValue(amount, cmp.amountFormat ?? parentFormat ?? ",.0f")}`;
9327
+ }
9328
+ return { label: cmp.label, text, positive: cmp.invert ? !rawPositive : rawPositive };
9329
+ }
9330
+ function layoutKpi(rect, tokens, framed, f) {
9331
+ const font = tokens.font;
9332
+ const pad3 = framed ? 0 : tokens.spacing.lg;
9333
+ const left = rect.x + pad3;
9334
+ const contentWidth = Math.max(1, rect.width - pad3 * 2);
9335
+ const availTop = rect.y + pad3;
9336
+ const availH = Math.max(0, rect.height - pad3 * 2);
9337
+ const labelSize = Math.round(clamp5(f.valueSize * 0.4, font.size.small, font.size.title));
9338
+ const labelH = f.hasLabel ? Math.round(labelSize * 1.35) : 0;
9339
+ const valueH = Math.round(f.valueSize * 1.06);
9340
+ const deltaSize = font.size.large;
9341
+ const deltaH = f.hasDelta ? Math.round(deltaSize * 1.5) : 0;
9342
+ const compSize = font.size.large;
9343
+ const compRowH = Math.round(compSize * 1.55);
9344
+ const gapLabel = Math.round(labelSize * 0.25);
9345
+ const gapDelta = f.hasDelta ? tokens.spacing.xs : 0;
9346
+ const gapSpark = tokens.spacing.sm;
9347
+ const dividerPad = tokens.spacing.sm;
9348
+ const headerH = (f.hasLabel && f.labelAbove ? labelH + gapLabel : 0) + valueH + (f.hasLabel && !f.labelAbove ? gapLabel + labelH : 0) + (f.hasDelta ? gapDelta + deltaH : 0);
9349
+ const footerH = f.compCount > 0 ? dividerPad + 1 + dividerPad + f.compCount * compRowH : 0;
9350
+ let headerTop;
9351
+ let footerTop;
9352
+ let spark = null;
9353
+ if (f.hasSparkline) {
9354
+ headerTop = availTop;
9355
+ footerTop = availTop + availH - footerH;
9356
+ const sparkTop = headerTop + headerH + gapSpark;
9357
+ const sparkBottom = footerTop - (footerH > 0 ? gapSpark : 0);
9358
+ spark = { x: left, y: sparkTop, width: contentWidth, height: Math.max(24, sparkBottom - sparkTop) };
9359
+ } else {
9360
+ const groupH = headerH + (footerH > 0 ? tokens.spacing.md + footerH : 0);
9361
+ headerTop = availTop + Math.max(0, (availH - groupH) / 2);
9362
+ footerTop = headerTop + headerH + (footerH > 0 ? tokens.spacing.md : 0);
9363
+ }
9364
+ let y = headerTop;
9365
+ let labelY = -1;
9366
+ if (f.hasLabel && f.labelAbove) {
9367
+ labelY = y;
9368
+ y += labelH + gapLabel;
9369
+ }
9370
+ const valueY = y;
9371
+ y += valueH;
9372
+ if (f.hasLabel && !f.labelAbove) {
9373
+ y += gapLabel;
9374
+ labelY = y;
9375
+ y += labelH;
9376
+ }
9377
+ let deltaY = -1;
9378
+ if (f.hasDelta) {
9379
+ y += gapDelta;
9380
+ deltaY = y;
9381
+ }
9382
+ let dividerY = -1;
9383
+ let compStartY = -1;
9384
+ if (f.compCount > 0) {
9385
+ dividerY = footerTop + dividerPad;
9386
+ compStartY = dividerY + 1 + dividerPad;
9387
+ }
9388
+ return {
9389
+ left,
9390
+ contentWidth,
9391
+ labelSize,
9392
+ labelY,
9393
+ valueY,
9394
+ valueH,
9395
+ deltaSize,
9396
+ deltaY,
9397
+ spark,
9398
+ compSize,
9399
+ compRowH,
9400
+ dividerY,
9401
+ compStartY
9402
+ };
9403
+ }
9404
+ function sparkMarkers(values, xAt, yAt, r, tokens) {
9405
+ const n = values.length;
9406
+ if (n < 2) return [];
9407
+ let minIdx = 0;
9408
+ let maxIdx = 0;
9409
+ for (let i = 1; i < n; i++) {
9410
+ if (values[i] < values[minIdx]) minIdx = i;
9411
+ if (values[i] > values[maxIdx]) maxIdx = i;
9412
+ }
9413
+ const lastIdx = n - 1;
9414
+ const out = [];
9415
+ if (maxIdx !== lastIdx) out.push({ x: xAt(maxIdx), y: yAt(values[maxIdx]), r, fill: tokens.color.positive });
9416
+ if (minIdx !== lastIdx) out.push({ x: xAt(minIdx), y: yAt(values[minIdx]), r, fill: tokens.color.negative });
9417
+ const lastStroke = lastIdx === maxIdx ? tokens.color.positive : lastIdx === minIdx ? tokens.color.negative : tokens.color.text;
9418
+ out.push({
9419
+ x: xAt(lastIdx),
9420
+ y: yAt(values[lastIdx]),
9421
+ r,
9422
+ fill: tokens.color.surface,
9423
+ stroke: lastStroke,
9424
+ strokeWidth: 1.75
9425
+ });
9426
+ return out;
9427
+ }
9428
+ function sparkScales(values, width, height, markers) {
9429
+ const n = values.length;
9430
+ const r = clamp5(Math.min(height, 40) * 0.12, 2.5, 4);
9431
+ const padX = markers ? r + 1 : 1.75;
9432
+ const padY = markers ? r + 1 : 1.75;
9433
+ const innerW = Math.max(1, width - padX * 2);
9434
+ const innerH = Math.max(1, height - padY * 2);
9435
+ const min = Math.min(...values);
9436
+ const max = Math.max(...values);
9437
+ const span = max - min;
9438
+ const xAt = (i) => n === 1 ? width / 2 : padX + i / (n - 1) * innerW;
9439
+ const yAt = (v) => padY + (1 - (span === 0 ? 0.5 : (v - min) / span)) * innerH;
9440
+ return { xAt, yAt, r };
9441
+ }
9442
+ function buildSparkline(tokens, width, height, values, markers) {
9090
9443
  if (values.length === 0 || width <= 0 || height <= 0) return null;
9091
9444
  const svg = document.createElementNS(SVG_NS, "svg");
9092
9445
  svg.setAttribute("width", String(width));
9093
9446
  svg.setAttribute("height", String(height));
9094
9447
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
9095
- svg.setAttribute("preserveAspectRatio", "none");
9096
9448
  svg.style.display = "block";
9097
9449
  svg.style.overflow = "visible";
9098
9450
  const stroke = 1.75;
9099
- const pad3 = stroke;
9100
- const innerH = Math.max(1, height - pad3 * 2);
9451
+ const line2 = tokens.color.textMuted;
9452
+ const { xAt, yAt, r } = sparkScales(values, width, height, markers);
9101
9453
  const n = values.length;
9102
- const min = Math.min(...values);
9103
- const max = Math.max(...values);
9104
- const span = max - min;
9105
- const xAt = (i) => n === 1 ? width / 2 : i / (n - 1) * width;
9106
- const yAt = (v) => {
9107
- const t = span === 0 ? 0.5 : (v - min) / span;
9108
- return pad3 + (1 - t) * innerH;
9109
- };
9110
9454
  if (n === 1) {
9111
9455
  const dot = document.createElementNS(SVG_NS, "circle");
9112
9456
  dot.setAttribute("cx", String(width / 2));
9113
9457
  dot.setAttribute("cy", String(yAt(values[0])));
9114
- dot.setAttribute("r", "2.25");
9458
+ dot.setAttribute("r", "2.5");
9115
9459
  dot.setAttribute("fill", tokens.color.accent);
9116
9460
  svg.appendChild(dot);
9117
9461
  return svg;
9118
9462
  }
9119
9463
  let lineD = "";
9120
- for (let i = 0; i < n; i++) {
9121
- lineD += `${i === 0 ? "M" : "L"}${xAt(i).toFixed(2)} ${yAt(values[i]).toFixed(2)}`;
9122
- }
9123
- const areaD = `${lineD}L${width.toFixed(2)} ${height}L0 ${height}Z`;
9464
+ for (let i = 0; i < n; i++) lineD += `${i === 0 ? "M" : "L"}${xAt(i).toFixed(2)} ${yAt(values[i]).toFixed(2)}`;
9465
+ const areaD = `${lineD}L${xAt(n - 1).toFixed(2)} ${height}L${xAt(0).toFixed(2)} ${height}Z`;
9124
9466
  const gradId = `graphein-spark-${Math.random().toString(36).slice(2, 9)}`;
9125
9467
  const defs = document.createElementNS(SVG_NS, "defs");
9126
9468
  const grad = document.createElementNS(SVG_NS, "linearGradient");
@@ -9131,11 +9473,11 @@ function buildSparkline(tokens, width, height, values) {
9131
9473
  grad.setAttribute("y2", "1");
9132
9474
  const stopTop = document.createElementNS(SVG_NS, "stop");
9133
9475
  stopTop.setAttribute("offset", "0");
9134
- stopTop.setAttribute("stop-color", tokens.color.accent);
9135
- stopTop.setAttribute("stop-opacity", "0.24");
9476
+ stopTop.setAttribute("stop-color", line2);
9477
+ stopTop.setAttribute("stop-opacity", "0.18");
9136
9478
  const stopBottom = document.createElementNS(SVG_NS, "stop");
9137
9479
  stopBottom.setAttribute("offset", "1");
9138
- stopBottom.setAttribute("stop-color", tokens.color.accent);
9480
+ stopBottom.setAttribute("stop-color", line2);
9139
9481
  stopBottom.setAttribute("stop-opacity", "0");
9140
9482
  grad.appendChild(stopTop);
9141
9483
  grad.appendChild(stopBottom);
@@ -9148,14 +9490,43 @@ function buildSparkline(tokens, width, height, values) {
9148
9490
  const path = document.createElementNS(SVG_NS, "path");
9149
9491
  path.setAttribute("d", lineD);
9150
9492
  path.setAttribute("fill", "none");
9151
- path.setAttribute("stroke", tokens.color.accent);
9493
+ path.setAttribute("stroke", line2);
9152
9494
  path.setAttribute("stroke-width", String(stroke));
9153
9495
  path.setAttribute("stroke-linejoin", "round");
9154
9496
  path.setAttribute("stroke-linecap", "round");
9155
9497
  svg.appendChild(path);
9498
+ if (markers) {
9499
+ for (const m of sparkMarkers(values, xAt, yAt, r, tokens)) {
9500
+ const c = document.createElementNS(SVG_NS, "circle");
9501
+ c.setAttribute("cx", m.x.toFixed(2));
9502
+ c.setAttribute("cy", m.y.toFixed(2));
9503
+ c.setAttribute("r", String(m.r));
9504
+ c.setAttribute("fill", m.fill ?? "none");
9505
+ if (m.stroke) {
9506
+ c.setAttribute("stroke", m.stroke);
9507
+ c.setAttribute("stroke-width", String(m.strokeWidth ?? 1.75));
9508
+ }
9509
+ svg.appendChild(c);
9510
+ }
9511
+ }
9156
9512
  return svg;
9157
9513
  }
9158
- function buildSparklineCanvas(tokens, width, height, values, sketch) {
9514
+ function paintSparkMarkers(ctx, values, xAt, yAt, r, tokens) {
9515
+ for (const m of sparkMarkers(values, xAt, yAt, r, tokens)) {
9516
+ ctx.beginPath();
9517
+ ctx.arc(m.x, m.y, m.r, 0, Math.PI * 2);
9518
+ if (m.fill) {
9519
+ ctx.fillStyle = m.fill;
9520
+ ctx.fill();
9521
+ }
9522
+ if (m.stroke) {
9523
+ ctx.lineWidth = m.strokeWidth ?? 1.75;
9524
+ ctx.strokeStyle = m.stroke;
9525
+ ctx.stroke();
9526
+ }
9527
+ }
9528
+ }
9529
+ function buildSparklineCanvas(tokens, width, height, values, markers, sketch) {
9159
9530
  if (values.length === 0 || width <= 0 || height <= 0) return null;
9160
9531
  const dpr = getDevicePixelRatio();
9161
9532
  const canvas = document.createElement("canvas");
@@ -9167,207 +9538,339 @@ function buildSparklineCanvas(tokens, width, height, values, sketch) {
9167
9538
  const ctx = canvas.getContext("2d");
9168
9539
  if (!ctx) return null;
9169
9540
  ctx.scale(dpr, dpr);
9170
- const stroke = 1.75;
9171
- const pad3 = stroke;
9172
- const innerH = Math.max(1, height - pad3 * 2);
9541
+ const { xAt, yAt, r } = sparkScales(values, width, height, markers);
9173
9542
  const n = values.length;
9174
- const min = Math.min(...values);
9175
- const max = Math.max(...values);
9176
- const span = max - min;
9177
- const xAt = (i) => n === 1 ? width / 2 : i / (n - 1) * width;
9178
- const yAt = (v) => pad3 + (1 - (span === 0 ? 0.5 : (v - min) / span)) * innerH;
9179
9543
  const pen = new RoughPen(ctx, sketch);
9180
9544
  if (n === 1) {
9181
9545
  pen.circle(width / 2, yAt(values[0]), 2.5, { fill: tokens.color.accent });
9182
9546
  return canvas;
9183
9547
  }
9184
9548
  const points = values.map((v, i) => ({ x: xAt(i), y: yAt(v) }));
9185
- pen.trendStroke(points, { stroke: tokens.color.accent, strokeWidth: stroke });
9549
+ pen.trendStroke(points, { stroke: tokens.color.textMuted, strokeWidth: 1.75 });
9550
+ if (markers) paintSparkMarkers(ctx, values, xAt, yAt, r, tokens);
9186
9551
  return canvas;
9187
9552
  }
9188
- function drawKpi(surface, spec, tokens, size, context) {
9189
- const kpi = spec;
9190
- if (surface.headless) {
9191
- drawKpiCanvas(surface, kpi, tokens, size, context);
9553
+ function paintSparklineCanvas(ctx, tokens, box, values, markers) {
9554
+ if (values.length === 0 || box.width <= 0 || box.height <= 0) return;
9555
+ const { xAt: xAt0, yAt: yAt0, r } = sparkScales(values, box.width, box.height, markers);
9556
+ const xAt = (i) => box.x + xAt0(i);
9557
+ const yAt = (v) => box.y + yAt0(v);
9558
+ const n = values.length;
9559
+ ctx.save();
9560
+ if (n === 1) {
9561
+ ctx.beginPath();
9562
+ ctx.arc(xAt(0), yAt(values[0]), 2.5, 0, Math.PI * 2);
9563
+ ctx.fillStyle = tokens.color.accent;
9564
+ ctx.fill();
9565
+ ctx.restore();
9192
9566
  return;
9193
9567
  }
9568
+ ctx.globalAlpha = 0.16;
9569
+ ctx.beginPath();
9570
+ ctx.moveTo(xAt(0), yAt(values[0]));
9571
+ for (let i = 1; i < n; i++) ctx.lineTo(xAt(i), yAt(values[i]));
9572
+ ctx.lineTo(xAt(n - 1), box.y + box.height);
9573
+ ctx.lineTo(xAt(0), box.y + box.height);
9574
+ ctx.closePath();
9575
+ ctx.fillStyle = tokens.color.textMuted;
9576
+ ctx.fill();
9577
+ ctx.globalAlpha = 1;
9578
+ ctx.beginPath();
9579
+ for (let i = 0; i < n; i++) {
9580
+ const px = xAt(i);
9581
+ const py = yAt(values[i]);
9582
+ if (i === 0) ctx.moveTo(px, py);
9583
+ else ctx.lineTo(px, py);
9584
+ }
9585
+ ctx.strokeStyle = tokens.color.textMuted;
9586
+ ctx.lineWidth = 1.75;
9587
+ ctx.lineJoin = "round";
9588
+ ctx.lineCap = "round";
9589
+ ctx.stroke();
9590
+ if (markers) paintSparkMarkers(ctx, values, xAt, yAt, r, tokens);
9591
+ ctx.restore();
9592
+ }
9593
+ function drawKpi(surface, spec, tokens, size, context) {
9594
+ const kpi = spec;
9194
9595
  const data = kpi.data ?? [];
9195
- const value = resolveValue(kpi.value, data);
9196
- const delta = resolveValue(kpi.delta, data);
9596
+ const value = resolveValueRef(kpi.value, data);
9597
+ const delta = resolveValueRef(kpi.delta, data);
9197
9598
  const sparkField = sparklineField(kpi);
9198
9599
  const sparkValues = sparkField ? sparklineValues(data, sparkField) : [];
9199
9600
  const hasSparkline = sparkValues.length > 0;
9200
- const sketch = resolveSketch(spec);
9601
+ const markers = sparklineMarkers(kpi);
9602
+ const comparisons = kpi.comparisons?.map((c) => resolveComparison(c, data, kpi.format)).filter((c) => c != null) ?? [];
9603
+ const hasDelta = delta != null;
9604
+ const isRich = hasDelta || hasSparkline || comparisons.length > 0;
9605
+ const align = kpi.align ?? "center";
9606
+ const labelAbove = kpi.labelPosition === "above" || kpi.labelPosition == null && isRich;
9201
9607
  const framed = context?.framed === true;
9202
9608
  const rect = cardRectFor(surface, tokens, size, kpi);
9203
- const card = createCard(tokens, rect, hasSparkline, framed);
9204
- if (sketch) card.style.borderRadius = SKETCH_BORDER_RADIUS;
9205
- if (kpi.label) {
9206
- addText2(card, kpi.label, {
9609
+ const progress = context?.valueProgress;
9610
+ const from = context?.valueFrom ?? 0;
9611
+ const displayValue = value != null && progress != null && progress < 1 ? from + (value - from) * progress : value;
9612
+ const displaySparkValues = morphSparkline(sparkValues, context?.sparklineFrom, progress);
9613
+ const valueText = value == null ? "\u2014" : formatValue(value, kpi.format);
9614
+ const displayText = displayValue == null ? "\u2014" : formatValue(displayValue, kpi.format);
9615
+ const fromText = context?.valueFrom != null && value != null ? formatValue(context.valueFrom, kpi.format) : valueText;
9616
+ const sizingText = fromText.length > valueText.length ? fromText : valueText;
9617
+ const pad3 = framed ? 0 : tokens.spacing.lg;
9618
+ const contentWidth = Math.max(1, rect.width - pad3 * 2);
9619
+ const valueSize = valueFontSize(rect, contentWidth, sizingText, kpi.unit, isRich);
9620
+ const geom = layoutKpi(rect, tokens, framed, {
9621
+ hasLabel: !!kpi.label,
9622
+ labelAbove,
9623
+ hasDelta,
9624
+ hasSparkline,
9625
+ compCount: comparisons.length,
9626
+ valueSize
9627
+ });
9628
+ const sketch = resolveSketch(spec);
9629
+ if (surface.headless) {
9630
+ drawKpiCanvas(surface, kpi, tokens, {
9631
+ align,
9632
+ valueSize,
9633
+ displayText,
9634
+ delta,
9635
+ comparisons,
9636
+ sparkValues: displaySparkValues,
9637
+ markers,
9638
+ geom
9639
+ });
9640
+ return;
9641
+ }
9642
+ const card = document.createElement("div");
9643
+ card.style.position = "absolute";
9644
+ card.style.left = `${rect.x}px`;
9645
+ card.style.top = `${rect.y}px`;
9646
+ card.style.width = `${rect.width}px`;
9647
+ card.style.height = `${rect.height}px`;
9648
+ card.style.boxSizing = "border-box";
9649
+ card.style.overflow = "hidden";
9650
+ card.style.pointerEvents = "none";
9651
+ const ox = rect.x;
9652
+ const oy = rect.y;
9653
+ const addAt = (el, left, top, width) => {
9654
+ el.style.position = "absolute";
9655
+ el.style.left = `${left - ox}px`;
9656
+ el.style.top = `${top - oy}px`;
9657
+ el.style.width = `${width}px`;
9658
+ card.appendChild(el);
9659
+ };
9660
+ if (kpi.label && geom.labelY >= 0) {
9661
+ const el = document.createElement("div");
9662
+ el.textContent = kpi.label;
9663
+ Object.assign(el.style, {
9207
9664
  color: tokens.color.textMuted,
9208
9665
  fontFamily: tokens.font.family,
9209
- fontSize: `${tokens.font.size.base}px`,
9666
+ fontSize: `${geom.labelSize}px`,
9210
9667
  fontWeight: String(tokens.font.weight.medium),
9211
- letterSpacing: "0.04em",
9212
9668
  lineHeight: "1.25",
9213
- textTransform: "uppercase",
9214
- maxWidth: "100%",
9669
+ textAlign: align === "start" ? "left" : align === "end" ? "right" : "center",
9215
9670
  overflow: "hidden",
9216
9671
  textOverflow: "ellipsis",
9217
9672
  whiteSpace: "nowrap"
9218
9673
  });
9219
- }
9220
- const valueText = value == null ? "\u2014" : formatValue(value, kpi.format);
9221
- const contentWidth = Math.max(1, rect.width - tokens.spacing.lg * 2);
9222
- const sizeByCard = Math.min(rect.width, rect.height) * 0.22;
9223
- const sizeByText = contentWidth / Math.max(1, valueText.length * 0.58);
9224
- const valueSize = clamp5(Math.min(sizeByCard, sizeByText), 28, 64);
9225
- addText2(card, valueText, {
9226
- color: tokens.color.text,
9674
+ addAt(el, geom.left, geom.labelY, geom.contentWidth);
9675
+ }
9676
+ const valueWrap = document.createElement("div");
9677
+ valueWrap.style.display = "flex";
9678
+ valueWrap.style.alignItems = "baseline";
9679
+ valueWrap.style.justifyContent = flexAlign(align);
9680
+ valueWrap.style.gap = "0.12em";
9681
+ valueWrap.style.height = `${geom.valueH}px`;
9682
+ valueWrap.style.overflow = "hidden";
9683
+ const num2 = document.createElement("div");
9684
+ num2.textContent = displayText;
9685
+ Object.assign(num2.style, {
9686
+ color: kpi.color ?? tokens.color.text,
9227
9687
  fontFamily: tokens.font.family,
9228
9688
  fontSize: `${valueSize}px`,
9229
9689
  fontWeight: String(tokens.font.weight.bold),
9230
9690
  letterSpacing: "-0.03em",
9231
9691
  lineHeight: "1.02",
9232
- maxWidth: "100%",
9233
9692
  overflow: "hidden",
9234
9693
  textOverflow: "ellipsis",
9235
9694
  whiteSpace: "nowrap"
9236
9695
  });
9237
- if (delta != null) {
9696
+ valueWrap.appendChild(num2);
9697
+ if (kpi.unit) {
9698
+ const unit = document.createElement("div");
9699
+ unit.textContent = kpi.unit;
9700
+ Object.assign(unit.style, {
9701
+ color: tokens.color.textMuted,
9702
+ fontFamily: tokens.font.family,
9703
+ fontSize: `${unitFontSize(valueSize)}px`,
9704
+ fontWeight: String(tokens.font.weight.medium),
9705
+ whiteSpace: "nowrap",
9706
+ flex: "0 0 auto"
9707
+ });
9708
+ valueWrap.appendChild(unit);
9709
+ }
9710
+ addAt(valueWrap, geom.left, geom.valueY, geom.contentWidth);
9711
+ if (hasDelta && geom.deltaY >= 0 && delta != null) {
9238
9712
  const positive = delta >= 0;
9239
- addText2(card, `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(delta, kpi.format)}`, {
9713
+ const el = document.createElement("div");
9714
+ el.textContent = `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(delta, kpi.format)}`;
9715
+ Object.assign(el.style, {
9240
9716
  color: positive ? tokens.color.positive : tokens.color.negative,
9241
9717
  fontFamily: tokens.font.family,
9242
- fontSize: `${tokens.font.size.large}px`,
9718
+ fontSize: `${geom.deltaSize}px`,
9243
9719
  fontWeight: String(tokens.font.weight.medium),
9244
- lineHeight: "1",
9720
+ lineHeight: "1.2",
9721
+ textAlign: align === "start" ? "left" : align === "end" ? "right" : "center",
9245
9722
  whiteSpace: "nowrap"
9246
9723
  });
9724
+ addAt(el, geom.left, geom.deltaY, geom.contentWidth);
9725
+ }
9726
+ if (geom.spark) {
9727
+ const node = sketch ? buildSparklineCanvas(tokens, geom.spark.width, geom.spark.height, displaySparkValues, markers, sketch) : buildSparkline(tokens, geom.spark.width, geom.spark.height, displaySparkValues, markers);
9728
+ if (node) addAt(node, geom.spark.x, geom.spark.y, geom.spark.width);
9729
+ }
9730
+ if (comparisons.length > 0 && geom.dividerY >= 0) {
9731
+ const divider = document.createElement("div");
9732
+ divider.style.height = "1px";
9733
+ divider.style.background = tokens.color.border;
9734
+ addAt(divider, geom.left, geom.dividerY, geom.contentWidth);
9735
+ comparisons.forEach((c, i) => {
9736
+ const row = document.createElement("div");
9737
+ row.style.display = "flex";
9738
+ row.style.alignItems = "baseline";
9739
+ row.style.gap = "0.5em";
9740
+ row.style.height = `${geom.compRowH}px`;
9741
+ row.style.overflow = "hidden";
9742
+ const label = document.createElement("span");
9743
+ label.textContent = c.label;
9744
+ Object.assign(label.style, {
9745
+ color: tokens.color.text,
9746
+ fontFamily: tokens.font.family,
9747
+ fontSize: `${geom.compSize}px`,
9748
+ fontWeight: String(tokens.font.weight.medium),
9749
+ whiteSpace: "nowrap",
9750
+ flex: "0 0 auto"
9751
+ });
9752
+ const val = document.createElement("span");
9753
+ val.textContent = c.text;
9754
+ Object.assign(val.style, {
9755
+ color: c.positive ? tokens.color.positive : tokens.color.negative,
9756
+ fontFamily: tokens.font.family,
9757
+ fontSize: `${geom.compSize}px`,
9758
+ fontWeight: String(tokens.font.weight.medium),
9759
+ whiteSpace: "nowrap"
9760
+ });
9761
+ row.appendChild(label);
9762
+ row.appendChild(val);
9763
+ addAt(row, geom.left, geom.compStartY + i * geom.compRowH, geom.contentWidth);
9764
+ });
9247
9765
  }
9248
9766
  surface.overlay.appendChild(card);
9249
- if (hasSparkline) {
9250
- const pad3 = framed ? 0 : tokens.spacing.lg;
9251
- const sparkH = clamp5(rect.height * 0.18, 24, 40);
9252
- const sparkW = Math.max(0, rect.width - pad3 * 2);
9253
- const node = sketch ? buildSparklineCanvas(tokens, sparkW, sparkH, sparkValues, sketch) : buildSparkline(tokens, sparkW, sparkH, sparkValues);
9254
- if (node) {
9255
- node.style.position = "absolute";
9256
- node.style.left = `${pad3}px`;
9257
- node.style.bottom = `${pad3}px`;
9258
- card.appendChild(node);
9259
- }
9260
- }
9261
9767
  }
9262
- function drawKpiCanvas(surface, kpi, tokens, size, context) {
9263
- const ctx = surface.marks.ctx;
9264
- const data = kpi.data ?? [];
9265
- const value = resolveValue(kpi.value, data);
9266
- const delta = resolveValue(kpi.delta, data);
9267
- const sparkField = sparklineField(kpi);
9268
- const sparkValues = sparkField ? sparklineValues(data, sparkField) : [];
9269
- const hasSparkline = sparkValues.length > 0;
9270
- const framed = context?.framed === true;
9271
- const rect = cardRectFor(surface, tokens, size, kpi);
9272
- const pad3 = tokens.spacing.lg;
9273
- const sparkReserve = hasSparkline ? Math.min(44, Math.max(28, rect.height * 0.18)) + tokens.spacing.sm : 0;
9274
- if (!framed) {
9275
- ctx.save();
9276
- ctx.beginPath();
9277
- roundedRect(ctx, rect.x, rect.y, rect.width, rect.height, tokens.radius.lg);
9278
- ctx.fillStyle = tokens.color.surface;
9279
- ctx.fill();
9280
- ctx.lineWidth = 1;
9281
- ctx.strokeStyle = tokens.color.border;
9282
- ctx.stroke();
9283
- ctx.restore();
9284
- }
9285
- const left = rect.x + (framed ? 0 : pad3);
9286
- const contentWidth = Math.max(1, rect.width - (framed ? 0 : pad3 * 2));
9287
- const valueText = value == null ? "\u2014" : formatValue(value, kpi.format);
9288
- const sizeByCard = Math.min(rect.width, rect.height) * 0.22;
9289
- const sizeByText = contentWidth / Math.max(1, valueText.length * 0.58);
9290
- const valueSize = clamp5(Math.min(sizeByCard, sizeByText), 28, 64);
9291
- const labelH = kpi.label ? tokens.font.size.base * 1.25 : 0;
9292
- const deltaH = delta != null ? tokens.font.size.large : 0;
9293
- const stackH = labelH + valueSize + deltaH + tokens.spacing.sm * 2;
9294
- const avail = rect.height - sparkReserve;
9295
- let y = rect.y + Math.max(framed ? 0 : pad3, (avail - stackH) / 2);
9296
- if (kpi.label) {
9768
+ function drawKpiCanvas(surface, kpi, tokens, p) {
9769
+ const ctx = surface.marks.ctx;
9770
+ const { geom } = p;
9771
+ const alignX = () => p.align === "start" ? geom.left : p.align === "end" ? geom.left + geom.contentWidth : geom.left + geom.contentWidth / 2;
9772
+ const alignCanvas = textAlignOf(p.align);
9773
+ if (kpi.label && geom.labelY >= 0) {
9297
9774
  paintCanvasText(ctx, {
9298
- x: left,
9299
- y,
9300
- text: kpi.label.toUpperCase(),
9301
- font: fontString(tokens.font.size.base, tokens.font.family, tokens.font.weight.medium),
9775
+ x: alignX(),
9776
+ y: geom.labelY,
9777
+ text: kpi.label,
9778
+ font: fontString(geom.labelSize, tokens.font.family, tokens.font.weight.medium),
9302
9779
  color: tokens.color.textMuted,
9303
- size: tokens.font.size.base,
9780
+ size: geom.labelSize,
9781
+ align: alignCanvas,
9304
9782
  baseline: "top"
9305
9783
  });
9306
- y += labelH + tokens.spacing.sm;
9307
9784
  }
9785
+ const unitSize = kpi.unit ? unitFontSize(p.valueSize) : 0;
9786
+ const numberFont = fontString(p.valueSize, tokens.font.family, tokens.font.weight.bold);
9787
+ const unitFont = kpi.unit ? fontString(unitSize, tokens.font.family, tokens.font.weight.medium) : "";
9788
+ ctx.save();
9789
+ ctx.font = numberFont;
9790
+ const numberW = ctx.measureText(p.displayText).width;
9791
+ let unitW = 0;
9792
+ if (kpi.unit) {
9793
+ ctx.font = unitFont;
9794
+ unitW = ctx.measureText(kpi.unit).width;
9795
+ }
9796
+ ctx.restore();
9797
+ const unitGap = kpi.unit ? p.valueSize * 0.12 : 0;
9798
+ const totalW = numberW + (kpi.unit ? unitGap + unitW : 0);
9799
+ const groupX = p.align === "start" ? geom.left : p.align === "end" ? geom.left + geom.contentWidth - totalW : geom.left + (geom.contentWidth - totalW) / 2;
9800
+ const baselineY = geom.valueY + p.valueSize * 0.82;
9308
9801
  paintCanvasText(ctx, {
9309
- x: left,
9310
- y,
9311
- text: valueText,
9312
- font: fontString(valueSize, tokens.font.family, tokens.font.weight.bold),
9313
- color: tokens.color.text,
9314
- size: valueSize,
9315
- baseline: "top"
9802
+ x: groupX,
9803
+ y: baselineY,
9804
+ text: p.displayText,
9805
+ font: numberFont,
9806
+ color: kpi.color ?? tokens.color.text,
9807
+ size: p.valueSize,
9808
+ align: "left",
9809
+ baseline: "alphabetic"
9316
9810
  });
9317
- y += valueSize + tokens.spacing.sm;
9318
- if (delta != null) {
9319
- const positive = delta >= 0;
9811
+ if (kpi.unit) {
9320
9812
  paintCanvasText(ctx, {
9321
- x: left,
9322
- y,
9323
- text: `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(delta, kpi.format)}`,
9324
- font: fontString(tokens.font.size.large, tokens.font.family, tokens.font.weight.medium),
9813
+ x: groupX + numberW + unitGap,
9814
+ y: baselineY,
9815
+ text: kpi.unit,
9816
+ font: unitFont,
9817
+ color: tokens.color.textMuted,
9818
+ size: unitSize,
9819
+ align: "left",
9820
+ baseline: "alphabetic"
9821
+ });
9822
+ }
9823
+ if (p.delta != null && geom.deltaY >= 0) {
9824
+ const positive = p.delta >= 0;
9825
+ paintCanvasText(ctx, {
9826
+ x: alignX(),
9827
+ y: geom.deltaY,
9828
+ text: `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(p.delta, kpi.format)}`,
9829
+ font: fontString(geom.deltaSize, tokens.font.family, tokens.font.weight.medium),
9325
9830
  color: positive ? tokens.color.positive : tokens.color.negative,
9326
- size: tokens.font.size.large,
9831
+ size: geom.deltaSize,
9832
+ align: alignCanvas,
9327
9833
  baseline: "top"
9328
9834
  });
9329
9835
  }
9330
- if (hasSparkline) {
9331
- const sparkH = clamp5(rect.height * 0.18, 24, 40);
9332
- const inset = framed ? 0 : pad3;
9333
- const sparkW = Math.max(0, rect.width - inset * 2);
9334
- const x0 = rect.x + inset;
9335
- const y1 = rect.y + rect.height - inset;
9336
- paintSparklineCanvas(ctx, tokens, x0, y1 - sparkH, sparkW, sparkH, sparkValues);
9836
+ if (geom.spark && p.sparkValues.length > 0) {
9837
+ paintSparklineCanvas(ctx, tokens, geom.spark, p.sparkValues, p.markers);
9337
9838
  }
9338
- }
9339
- function paintSparklineCanvas(ctx, tokens, x0, y0, width, height, values) {
9340
- if (values.length === 0 || width <= 0 || height <= 0) return;
9341
- const stroke = 1.75;
9342
- const pad3 = stroke;
9343
- const innerH = Math.max(1, height - pad3 * 2);
9344
- const n = values.length;
9345
- const min = Math.min(...values);
9346
- const max = Math.max(...values);
9347
- const span = max - min;
9348
- const xAt = (i) => x0 + (n === 1 ? width / 2 : i / (n - 1) * width);
9349
- const yAt = (v) => y0 + pad3 + (1 - (span === 0 ? 0.5 : (v - min) / span)) * innerH;
9350
- ctx.save();
9351
- if (n === 1) {
9352
- ctx.beginPath();
9353
- ctx.arc(xAt(0), yAt(values[0]), 2.25, 0, Math.PI * 2);
9354
- ctx.fillStyle = tokens.color.accent;
9355
- ctx.fill();
9356
- } else {
9357
- ctx.beginPath();
9358
- for (let i = 0; i < n; i++) {
9359
- const px = xAt(i);
9360
- const py = yAt(values[i]);
9361
- if (i === 0) ctx.moveTo(px, py);
9362
- else ctx.lineTo(px, py);
9363
- }
9364
- ctx.strokeStyle = tokens.color.accent;
9365
- ctx.lineWidth = stroke;
9366
- ctx.lineJoin = "round";
9367
- ctx.lineCap = "round";
9368
- ctx.stroke();
9839
+ if (p.comparisons.length > 0 && geom.dividerY >= 0) {
9840
+ ctx.save();
9841
+ ctx.fillStyle = tokens.color.border;
9842
+ ctx.fillRect(geom.left, geom.dividerY, geom.contentWidth, 1);
9843
+ ctx.restore();
9844
+ const compFont = fontString(geom.compSize, tokens.font.family, tokens.font.weight.medium);
9845
+ const gap = geom.compSize * 0.5;
9846
+ p.comparisons.forEach((c, i) => {
9847
+ const rowY = geom.compStartY + i * geom.compRowH + geom.compRowH / 2;
9848
+ ctx.save();
9849
+ ctx.font = compFont;
9850
+ const labelW = ctx.measureText(c.label).width;
9851
+ ctx.restore();
9852
+ paintCanvasText(ctx, {
9853
+ x: geom.left,
9854
+ y: rowY,
9855
+ text: c.label,
9856
+ font: compFont,
9857
+ color: tokens.color.text,
9858
+ size: geom.compSize,
9859
+ align: "left",
9860
+ baseline: "middle"
9861
+ });
9862
+ paintCanvasText(ctx, {
9863
+ x: geom.left + labelW + gap,
9864
+ y: rowY,
9865
+ text: c.text,
9866
+ font: compFont,
9867
+ color: c.positive ? tokens.color.positive : tokens.color.negative,
9868
+ size: geom.compSize,
9869
+ align: "left",
9870
+ baseline: "middle"
9871
+ });
9872
+ });
9369
9873
  }
9370
- ctx.restore();
9371
9874
  }
9372
9875
 
9373
9876
  // src/charts/sankey.ts
@@ -9496,7 +9999,7 @@ function layout(graph, area2, nodeWidth, pad3) {
9496
9999
  const kx = maxLayer > 0 ? (area2.width - nodeWidth) / maxLayer : 0;
9497
10000
  const maxColumnSize = columns.reduce((m, col) => Math.max(m, col.length), 0);
9498
10001
  const desiredPad = Math.max(0, pad3);
9499
- 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;
9500
10003
  for (const col of columns) {
9501
10004
  for (const n of col) {
9502
10005
  n.x0 = area2.x + n.layer * kx;
@@ -9618,15 +10121,24 @@ function drawSankey(surface, spec, tokens, size) {
9618
10121
  const graph = buildGraph(sankey, tokens.color.palette);
9619
10122
  if (graph.nodes.length === 0 || content.width <= 0 || content.height <= 0) return;
9620
10123
  const labelFont = fontString(f.size.small, f.family, f.weight.medium);
9621
- let maxLabel = 0;
9622
- for (const n of graph.nodes) maxLabel = Math.max(maxLabel, measureText(n.name, labelFont).width);
9623
- 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;
9624
10136
  const nodeWidth = sankey.nodeWidth ?? 16;
9625
10137
  const pad3 = sankey.nodePadding ?? 14;
9626
10138
  const area2 = {
9627
- x: content.x + gutter,
10139
+ x: content.x + leftGutter,
9628
10140
  y: content.y + 4,
9629
- width: Math.max(10, content.width - gutter * 2),
10141
+ width: Math.max(10, content.width - leftGutter - rightGutter),
9630
10142
  height: Math.max(10, content.height - 8)
9631
10143
  };
9632
10144
  layout(graph, area2, nodeWidth, pad3);
@@ -9650,12 +10162,13 @@ function drawSankey(surface, spec, tokens, size) {
9650
10162
  }
9651
10163
  }
9652
10164
  ctx.restore();
9653
- const showValues = sankey.nodeValues !== false;
9654
10165
  const mid = area2.x + area2.width / 2;
9655
10166
  for (const n of graph.nodes) {
9656
- 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;
9657
10170
  const cy = center(n);
9658
- const left = labelRight ? n.x1 + 6 : n.x0 - 6;
10171
+ const left = labelRight ? n.x1 + LABEL_GAP : n.x0 - LABEL_GAP;
9659
10172
  const transform2 = labelRight ? "translateY(-50%)" : "translate(-100%,-50%)";
9660
10173
  addOverlayText(surface, tokens, {
9661
10174
  left,
@@ -9834,18 +10347,24 @@ function drawLegend3(surface, tokens, legend, domain, scheme, format) {
9834
10347
  transform: "translateX(-100%)"
9835
10348
  });
9836
10349
  }
9837
- function drawChoropleth(surface, spec, tokens, size) {
10350
+ function drawChoropleth(surface, spec, tokens, size, context) {
9838
10351
  const choropleth = spec;
10352
+ const emphasis = context?.emphasis ?? null;
9839
10353
  const content = drawTitleBlock(surface, tokens, size, choropleth.title);
9840
10354
  const features = choropleth.geo?.features ?? [];
9841
10355
  if (features.length === 0 || content.width <= 0 || content.height <= 0) return;
9842
10356
  const readKey = accessor(choropleth.encoding.key.field);
9843
10357
  const readColor = accessor(choropleth.encoding.color.field);
9844
10358
  const valueByKey = /* @__PURE__ */ new Map();
10359
+ const rowByKey = /* @__PURE__ */ new Map();
9845
10360
  for (const row of choropleth.data ?? []) {
9846
- const k = toKey(readKey(row));
9847
- const v = toNumber(readColor(row));
9848
- if (k !== "" && Number.isFinite(v)) valueByKey.set(k, v);
10361
+ const datum = row;
10362
+ const k = toKey(readKey(datum));
10363
+ const v = toNumber(readColor(datum));
10364
+ if (k !== "" && Number.isFinite(v)) {
10365
+ valueByKey.set(k, v);
10366
+ rowByKey.set(k, datum);
10367
+ }
9849
10368
  }
9850
10369
  const projection = choropleth.projection ?? "mercator";
9851
10370
  const project = projector(
@@ -9863,6 +10382,7 @@ function drawChoropleth(surface, spec, tokens, size) {
9863
10382
  for (const feature of features) {
9864
10383
  const key = featureKey(feature, choropleth.featureId);
9865
10384
  const value = valueByKey.has(key) ? valueByKey.get(key) : null;
10385
+ const row = rowByKey.get(key);
9866
10386
  if (value != null) {
9867
10387
  if (value < vMin) vMin = value;
9868
10388
  if (value > vMax) vMax = value;
@@ -9885,7 +10405,7 @@ function drawChoropleth(surface, spec, tokens, size) {
9885
10405
  }
9886
10406
  if (rings.length) polys.push(rings);
9887
10407
  }
9888
- projected.push({ key, name: featureName(feature, key), value, polys });
10408
+ projected.push({ key, name: featureName(feature, key), value, row, polys });
9889
10409
  }
9890
10410
  const spanX = maxX - minX;
9891
10411
  const spanY = maxY - minY;
@@ -9932,6 +10452,7 @@ function drawChoropleth(surface, spec, tokens, size) {
9932
10452
  key: pf.key,
9933
10453
  name: pf.name,
9934
10454
  value: pf.value,
10455
+ row: pf.row,
9935
10456
  color: pf.value == null ? noDataFill : rgbaToCss(cscale.map(pf.value)),
9936
10457
  polys,
9937
10458
  cx: count ? cx / count : 0,
@@ -9946,6 +10467,7 @@ function drawChoropleth(surface, spec, tokens, size) {
9946
10467
  ctx.strokeStyle = tokens.color.background;
9947
10468
  ctx.lineWidth = 0.75;
9948
10469
  for (const f of screen) {
10470
+ ctx.globalAlpha = rowAlpha(emphasis, f.row ?? { [choropleth.encoding.key.field]: f.key });
9949
10471
  ctx.fillStyle = f.color;
9950
10472
  for (const polygon of f.polys) {
9951
10473
  ctx.beginPath();
@@ -9966,6 +10488,7 @@ function drawChoropleth(surface, spec, tokens, size) {
9966
10488
  }
9967
10489
  }
9968
10490
  }
10491
+ ctx.globalAlpha = 1;
9969
10492
  }
9970
10493
  ctx.restore();
9971
10494
  if (legendHeight > 0) {
@@ -10120,7 +10643,7 @@ function colorToTone(color) {
10120
10643
  var HEADER_ROW_HEIGHT = 32;
10121
10644
  var MIN_COL_WIDTH = 72;
10122
10645
  var DEFAULT_BODY_ROW_HEIGHT = 30;
10123
- var SKETCH_BORDER_RADIUS2 = "14px 6px 16px 7px / 7px 15px 6px 14px";
10646
+ var SKETCH_BORDER_RADIUS = "14px 6px 16px 7px / 7px 15px 6px 14px";
10124
10647
  function formatCellValue(value, column) {
10125
10648
  return formatValue(value, column.format);
10126
10649
  }
@@ -10160,7 +10683,7 @@ function buildTable(opts) {
10160
10683
  pointerEvents: "auto",
10161
10684
  boxSizing: "border-box",
10162
10685
  border: `1px solid ${opts.sketch ? "transparent" : tokens.color.border}`,
10163
- borderRadius: opts.sketch ? SKETCH_BORDER_RADIUS2 : `${tokens.radius.sm}px`,
10686
+ borderRadius: opts.sketch ? SKETCH_BORDER_RADIUS : `${tokens.radius.sm}px`,
10164
10687
  background: tokens.color.background
10165
10688
  });
10166
10689
  container.replaceChildren();
@@ -13003,7 +13526,6 @@ function scatterInteraction(model) {
13003
13526
  }
13004
13527
 
13005
13528
  // src/runtime/combo.ts
13006
- var DEFAULT_PADDING2 = { top: 12, right: 16, bottom: 12, left: 12 };
13007
13529
  function fmtTick(v, f) {
13008
13530
  return f ? formatValue(v, f) : formatNumber2(v, ",");
13009
13531
  }
@@ -13189,7 +13711,7 @@ function buildComboModel(spec, tokens, opts) {
13189
13711
  const legendPosition = typeof spec.legend === "object" && spec.legend.position || (legendItems.length > 8 ? "right" : "top");
13190
13712
  const xShow = xAxisCfg.show !== false;
13191
13713
  const yShow = spec.axes?.y?.show !== false;
13192
- const padding = { ...DEFAULT_PADDING2, ...spec.padding };
13714
+ const padding = { ...NO_PADDING, ...spec.padding };
13193
13715
  const titleInput4 = typeof spec.title === "string" ? { text: spec.title } : spec.title ? { text: spec.title.text, subtitle: spec.title.subtitle } : void 0;
13194
13716
  const frame = computeFrame({
13195
13717
  width: opts.width,
@@ -13411,7 +13933,6 @@ function renderLayer(surface, lm) {
13411
13933
  }
13412
13934
 
13413
13935
  // src/runtime/histogram.ts
13414
- var DEFAULT_PADDING3 = { top: 12, right: 16, bottom: 12, left: 12 };
13415
13936
  function firstPaletteColor(tokens) {
13416
13937
  return rgbaToCss(ordinalColorScale({ domain: ["0"], palette: tokens.color.palette }).map("0"));
13417
13938
  }
@@ -13463,7 +13984,7 @@ function buildHistogramModel(spec, tokens, opts) {
13463
13984
  const xLabels = xTickValues.map((v) => xFmt ? formatValue(v, xFmt) : formatNumber2(v, ","));
13464
13985
  const yFmt = spec.axes?.y?.format ?? (spec.density ? ".3~f" : void 0);
13465
13986
  const yLabels = yTickValues.map((v) => yFmt ? formatValue(v, yFmt) : formatNumber2(v, ","));
13466
- const padding = { ...DEFAULT_PADDING3, ...spec.padding };
13987
+ const padding = { ...NO_PADDING, ...spec.padding };
13467
13988
  const titleInput4 = typeof spec.title === "string" ? { text: spec.title } : spec.title ? { text: spec.title.text, subtitle: spec.title.subtitle } : void 0;
13468
13989
  const xShow = spec.axes?.x?.show !== false;
13469
13990
  const yShow = spec.axes?.y?.show !== false;
@@ -13669,6 +14190,12 @@ function labelFor(value) {
13669
14190
  const key = toKey(value);
13670
14191
  return key === "" ? "(blank)" : key;
13671
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
+ }
13672
14199
  function buildLeaves(treemap) {
13673
14200
  const readCategory = accessor(treemap.encoding.category.field);
13674
14201
  const readValue = accessor(treemap.encoding.value.field);
@@ -13752,13 +14279,20 @@ function tileProbe(treemap, tile) {
13752
14279
  if (groupField) probe[groupField] = tile.group;
13753
14280
  return probe;
13754
14281
  }
13755
- function drawRect2(ctx, pen, rect, fill, stroke) {
14282
+ function drawRect2(ctx, pen, rect, fill, fillCss, stroke) {
13756
14283
  if (rect.width <= 0 || rect.height <= 0) return;
13757
14284
  if (pen) {
13758
- 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
+ });
13759
14293
  return;
13760
14294
  }
13761
- ctx.fillStyle = fill;
14295
+ ctx.fillStyle = fillCss;
13762
14296
  ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
13763
14297
  ctx.strokeStyle = stroke;
13764
14298
  ctx.lineWidth = 1;
@@ -13828,7 +14362,7 @@ function drawTreemap(surface, spec, tokens, size, context) {
13828
14362
  const drawTile = (tile) => {
13829
14363
  const alpha = emphasis ? emphasis.match(tileProbe(treemap, tile)) ? 1 : emphasis.dim : 1;
13830
14364
  if (alpha !== 1) ctx.globalAlpha = alpha;
13831
- drawRect2(ctx, pen, tile.rect, tile.fillCss, stroke);
14365
+ drawRect2(ctx, pen, tile.rect, tile.fill, tile.fillCss, stroke);
13832
14366
  if (alpha !== 1) ctx.globalAlpha = 1;
13833
14367
  };
13834
14368
  ctx.save();
@@ -13865,11 +14399,13 @@ function drawTreemap(surface, spec, tokens, size, context) {
13865
14399
  ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, Math.max(0, rect.width - 1), Math.max(0, rect.height - 1));
13866
14400
  }
13867
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);
13868
14404
  addOverlayText(surface, tokens, {
13869
14405
  left: rect.x + 5,
13870
14406
  top: rect.y + 4,
13871
- width: Math.max(0, rect.width - 10),
13872
- text: group.label,
14407
+ width: headerW,
14408
+ text: ellipsize(group.label, headerW, headerFont),
13873
14409
  color: tokens.color.text,
13874
14410
  size: tokens.font.size.tiny,
13875
14411
  weight: tokens.font.weight.bold,
@@ -13921,11 +14457,6 @@ function drawTreemap(surface, spec, tokens, size, context) {
13921
14457
  function clamp8(value, min, max) {
13922
14458
  return Math.max(min, Math.min(max, value));
13923
14459
  }
13924
- function resolveValue2(ref, data) {
13925
- if (ref == null) return null;
13926
- if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
13927
- return aggregateValues((data ?? []).map(accessor(ref.field)), ref.aggregate ?? "sum");
13928
- }
13929
14460
  function valueField2(ref) {
13930
14461
  return typeof ref === "number" ? null : ref.field;
13931
14462
  }
@@ -13949,7 +14480,7 @@ function arcPoints(cx, cy, radius, startAngle, endAngle) {
13949
14480
  }
13950
14481
  return points;
13951
14482
  }
13952
- function strokePoints(ctx, pen, points, color, width) {
14483
+ function strokePoints(ctx, pen, points, color, width, cap2 = "butt") {
13953
14484
  if (points.length < 2) return;
13954
14485
  if (pen) {
13955
14486
  pen.polyline(points, { stroke: color, strokeWidth: width });
@@ -13960,29 +14491,12 @@ function strokePoints(ctx, pen, points, color, width) {
13960
14491
  ctx.moveTo(points[0].x, points[0].y);
13961
14492
  for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
13962
14493
  ctx.lineWidth = width;
13963
- ctx.lineCap = "round";
14494
+ ctx.lineCap = cap2;
13964
14495
  ctx.lineJoin = "round";
13965
14496
  ctx.strokeStyle = color;
13966
14497
  ctx.stroke();
13967
14498
  ctx.restore();
13968
14499
  }
13969
- function drawCircle(ctx, pen, cx, cy, radius, fill, stroke) {
13970
- if (pen) {
13971
- pen.circle(cx, cy, radius, { fill, stroke: stroke ?? fill, strokeWidth: 1 });
13972
- return;
13973
- }
13974
- ctx.save();
13975
- ctx.beginPath();
13976
- ctx.arc(cx, cy, radius, 0, Math.PI * 2);
13977
- ctx.fillStyle = fill;
13978
- ctx.fill();
13979
- if (stroke) {
13980
- ctx.strokeStyle = stroke;
13981
- ctx.lineWidth = 1;
13982
- ctx.stroke();
13983
- }
13984
- ctx.restore();
13985
- }
13986
14500
  function drawCenteredMessage(surface, tokens, rect, text) {
13987
14501
  addOverlayText(surface, tokens, {
13988
14502
  left: rect.x,
@@ -13994,86 +14508,157 @@ function drawCenteredMessage(surface, tokens, rect, text) {
13994
14508
  align: "center"
13995
14509
  });
13996
14510
  }
14511
+ var LEGEND_SWATCH3 = 11;
14512
+ function measureLegendWidth(entries, font) {
14513
+ let maxLabel = 0;
14514
+ for (const e of entries) maxLabel = Math.max(maxLabel, measureText(e.label, font).width);
14515
+ return Math.ceil(LEGEND_SWATCH3 + 6 + maxLabel + 4);
14516
+ }
14517
+ function drawGaugeLegend(surface, tokens, area2, entries) {
14518
+ if (entries.length === 0) return;
14519
+ const labelSize = tokens.font.size.small;
14520
+ const rowH = Math.round(labelSize * 1.7);
14521
+ const font = fontString(labelSize, tokens.font.family, tokens.font.weight.normal);
14522
+ const colW = measureLegendWidth(entries, font);
14523
+ const x = area2.x + area2.width - colW;
14524
+ let y = area2.y;
14525
+ if (surface.headless) {
14526
+ const ctx = surface.marks.ctx;
14527
+ for (const e of entries) {
14528
+ const midY = y + rowH / 2;
14529
+ paintLegendSwatch(ctx, x, midY, "square", e.color, LEGEND_SWATCH3);
14530
+ paintCanvasText(ctx, {
14531
+ x: x + LEGEND_SWATCH3 + 6,
14532
+ y: midY,
14533
+ text: e.label,
14534
+ font,
14535
+ color: tokens.color.text,
14536
+ size: labelSize,
14537
+ baseline: "middle"
14538
+ });
14539
+ y += rowH;
14540
+ }
14541
+ return;
14542
+ }
14543
+ for (const e of entries) {
14544
+ const row = document.createElement("div");
14545
+ row.style.position = "absolute";
14546
+ row.style.left = `${x}px`;
14547
+ row.style.top = `${y}px`;
14548
+ row.style.display = "flex";
14549
+ row.style.alignItems = "center";
14550
+ row.style.gap = "6px";
14551
+ row.style.pointerEvents = "none";
14552
+ const swatch = document.createElement("span");
14553
+ swatch.style.flex = "0 0 auto";
14554
+ swatch.style.width = `${LEGEND_SWATCH3}px`;
14555
+ swatch.style.height = `${LEGEND_SWATCH3}px`;
14556
+ swatch.style.borderRadius = "2px";
14557
+ swatch.style.background = e.color;
14558
+ const label = document.createElement("span");
14559
+ label.textContent = e.label;
14560
+ label.style.font = font;
14561
+ label.style.color = tokens.color.text;
14562
+ label.style.whiteSpace = "nowrap";
14563
+ row.appendChild(swatch);
14564
+ row.appendChild(label);
14565
+ surface.overlay.appendChild(row);
14566
+ y += rowH;
14567
+ }
14568
+ }
14569
+ function bandFillColor(gauge, value, tokens, accent) {
14570
+ const bands = gauge.bands;
14571
+ if (!bands?.length) return accent;
14572
+ const palette = tokens.color.palette;
14573
+ for (let i = 0; i < bands.length; i++) {
14574
+ if (value <= bands[i].to) return bands[i].color ?? palette[i % palette.length] ?? accent;
14575
+ }
14576
+ const last = bands.length - 1;
14577
+ return bands[last].color ?? palette[last % palette.length] ?? accent;
14578
+ }
13997
14579
  function drawGauge(surface, spec, tokens, size, context) {
13998
14580
  const gauge = spec;
13999
14581
  const ctx = surface.marks.ctx;
14000
14582
  const content = drawTitleBlock(surface, tokens, size, gauge.title);
14001
14583
  const data = gauge.data ?? [];
14002
- const value = resolveValue2(gauge.value, data);
14003
- const target = resolveValue2(gauge.target, data);
14584
+ const value = resolveValueRef(gauge.value, data);
14585
+ const target = resolveValueRef(gauge.target, data);
14586
+ const compare2 = resolveValueRef(gauge.compare, data);
14004
14587
  const min = gauge.min ?? 0;
14005
14588
  const max = gauge.max;
14006
14589
  if (value == null || !Number.isFinite(min) || !Number.isFinite(max) || max <= min || content.width <= 0 || content.height <= 0) {
14007
14590
  drawCenteredMessage(surface, tokens, content, "No value");
14008
14591
  return;
14009
14592
  }
14593
+ const sketch = resolveSketch(spec);
14594
+ const pen = sketch ? new RoughPen(ctx, sketch) : null;
14595
+ const accent = tokens.color.palette[0] ?? tokens.color.accent;
14596
+ const fill = bandFillColor(gauge, value, tokens, accent);
14597
+ const track = tokens.dark ? withAlpha3(tokens.color.textMuted, 0.3) : tokens.color.border;
14598
+ const compareColor = tokens.color.textMuted;
14599
+ const markColor = tokens.dark ? withAlpha3("#ffffff", 0.92) : withAlpha3(tokens.color.text, 0.82);
14600
+ const targetMark = target != null && target >= min && target <= max ? target : null;
14010
14601
  const valueText = formatValue(value, gauge.format);
14011
14602
  const caption = gauge.label ?? titleText(gauge.title) ?? valueField2(gauge.value) ?? "Value";
14012
- const valueSize = clamp8(Math.min(content.width / Math.max(6, valueText.length * 0.55), tokens.font.size.title * 2.4), 24, 42);
14013
14603
  const captionSize = tokens.font.size.small;
14014
- const readoutReserve = valueSize + captionSize + 24;
14015
- const radius = Math.max(18, Math.min((content.width - 48) / 2, content.height - readoutReserve - 18));
14016
- const cx = content.x + content.width / 2;
14017
- const cy = content.y + Math.max(radius + 8, (content.height - readoutReserve + radius) / 2);
14018
- const trackWidth = clamp8(radius * 0.17, 10, 22);
14019
- const accent = tokens.color.palette[0] ?? tokens.color.accent;
14020
- const track = withAlpha3(tokens.color.border || tokens.color.textMuted, tokens.dark ? 0.75 : 0.9);
14021
- const needleColor = tokens.color.text;
14604
+ const captionReserve = captionSize + 16;
14605
+ const showLegend = gauge.legend ?? compare2 != null;
14606
+ const legendEntries = [];
14607
+ if (showLegend) {
14608
+ legendEntries.push({ label: gauge.valueLabel ?? "Actual", color: fill, symbol: "square" });
14609
+ if (compare2 != null) {
14610
+ legendEntries.push({ label: gauge.compareLabel ?? "Prior period", color: compareColor, symbol: "square" });
14611
+ }
14612
+ legendEntries.push({ label: gauge.trackLabel ?? "Target", color: targetMark != null ? markColor : track, symbol: "square" });
14613
+ }
14614
+ const legendFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
14615
+ const legendW = legendEntries.length ? measureLegendWidth(legendEntries, legendFont) + 12 : 0;
14616
+ const dialW = Math.max(40, content.width - legendW);
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));
14622
+ const cx = content.x + dialW / 2;
14623
+ const blockH = radius + trackWidth / 2 + captionReserve;
14624
+ const topMargin = Math.max(0, (content.height - blockH) / 2);
14625
+ const cy = content.y + topMargin + radius + trackWidth / 2;
14022
14626
  const toAngle = (v) => Math.PI - clamp8((v - min) / (max - min), 0, 1) * Math.PI;
14023
- const valueAngle = toAngle(value);
14024
- const sketch = resolveSketch(spec);
14025
- const pen = sketch ? new RoughPen(ctx, sketch) : null;
14627
+ const sweepT = context?.sweepProgress ?? 1;
14628
+ const from = context?.valueFrom ?? min;
14629
+ const displayValue = from + (value - from) * sweepT;
14630
+ const shownText = formatValue(displayValue, gauge.format);
14631
+ const valueAngle = toAngle(displayValue);
14026
14632
  strokePoints(ctx, pen, arcPoints(cx, cy, radius, Math.PI, 0), track, trackWidth);
14027
- if (gauge.bands?.length) {
14028
- let prev = min;
14029
- gauge.bands.forEach((band, i) => {
14030
- const to = clamp8(band.to, min, max);
14031
- if (to <= prev) return;
14032
- const color = band.color ?? tokens.color.palette[i % tokens.color.palette.length] ?? accent;
14033
- strokePoints(ctx, pen, arcPoints(cx, cy, radius, toAngle(prev), toAngle(to)), color, trackWidth);
14034
- prev = to;
14035
- });
14036
- if (prev < max) {
14037
- strokePoints(ctx, pen, arcPoints(cx, cy, radius, toAngle(prev), 0), withAlpha3(tokens.color.border, 0.6), trackWidth);
14038
- }
14039
- } else {
14040
- strokePoints(ctx, pen, arcPoints(cx, cy, radius, Math.PI, valueAngle), accent, trackWidth);
14041
- }
14042
- const needleEnd = pointOnArc(cx, cy, radius - trackWidth * 0.45, valueAngle);
14043
- strokePoints(ctx, pen, [{ x: cx, y: cy }, needleEnd], needleColor, Math.max(1.5, tokens.stroke.thick));
14044
- drawCircle(ctx, pen, cx, cy, Math.max(3, trackWidth * 0.22), tokens.color.surface, needleColor);
14045
- if (target != null) {
14046
- const a = toAngle(target);
14047
- const outer = pointOnArc(cx, cy, radius + trackWidth * 0.55, a);
14048
- const inner = pointOnArc(cx, cy, radius - trackWidth * 0.7, a);
14049
- strokePoints(ctx, pen, [outer, inner], tokens.color.negative ?? tokens.color.text, Math.max(2, tokens.stroke.thick));
14050
- }
14051
- const leftTick = pointOnArc(cx, cy, radius, Math.PI);
14052
- const rightTick = pointOnArc(cx, cy, radius, 0);
14053
- addOverlayText(surface, tokens, {
14054
- left: leftTick.x - 32,
14055
- top: leftTick.y + 6,
14056
- width: 64,
14057
- text: formatValue(min, gauge.format),
14058
- color: tokens.color.textMuted,
14059
- size: tokens.font.size.tiny,
14060
- align: "center"
14061
- });
14062
- addOverlayText(surface, tokens, {
14063
- left: rightTick.x - 32,
14064
- top: rightTick.y + 6,
14065
- width: 64,
14066
- text: formatValue(max, gauge.format),
14067
- color: tokens.color.textMuted,
14068
- size: tokens.font.size.tiny,
14069
- align: "center"
14070
- });
14071
- const readoutTop = Math.min(content.y + content.height - valueSize - captionSize - 8, cy + 14);
14633
+ if (valueAngle < Math.PI) {
14634
+ strokePoints(ctx, pen, arcPoints(cx, cy, radius, Math.PI, valueAngle), fill, trackWidth);
14635
+ }
14636
+ if (compare2 != null) {
14637
+ const innerWidth = clamp8(trackWidth * 0.42, 6, 22);
14638
+ const innerRadius = Math.max(radius * 0.35, radius - trackWidth / 2 - 2 - innerWidth / 2);
14639
+ const compareFrom = context?.compareFrom ?? min;
14640
+ const displayCompare = compareFrom + (compare2 - compareFrom) * sweepT;
14641
+ const compareAngle = toAngle(displayCompare);
14642
+ if (compareAngle < Math.PI) {
14643
+ strokePoints(ctx, pen, arcPoints(cx, cy, innerRadius, Math.PI, compareAngle), compareColor, innerWidth);
14644
+ }
14645
+ }
14646
+ if (targetMark != null) {
14647
+ const a = toAngle(targetMark);
14648
+ const overhang = clamp8(trackWidth * 0.3, 4, 10);
14649
+ const outer = pointOnArc(cx, cy, radius + trackWidth / 2 + overhang, a);
14650
+ const inner = pointOnArc(cx, cy, radius - trackWidth / 2 - overhang, a);
14651
+ strokePoints(ctx, pen, [outer, inner], markColor, 3, "round");
14652
+ }
14653
+ if (legendEntries.length) {
14654
+ drawGaugeLegend(surface, tokens, { x: content.x, y: content.y, width: content.width, height: content.height }, legendEntries);
14655
+ }
14656
+ const valueSize = clamp8(radius * 0.5, 22, 72);
14072
14657
  addOverlayText(surface, tokens, {
14073
14658
  left: content.x,
14074
- top: readoutTop,
14075
- width: content.width,
14076
- text: valueText,
14659
+ top: cy - radius * 0.32 - valueSize / 2,
14660
+ width: dialW,
14661
+ text: shownText,
14077
14662
  color: tokens.color.text,
14078
14663
  size: valueSize,
14079
14664
  weight: tokens.font.weight.bold,
@@ -14081,20 +14666,20 @@ function drawGauge(surface, spec, tokens, size, context) {
14081
14666
  });
14082
14667
  addOverlayText(surface, tokens, {
14083
14668
  left: content.x,
14084
- top: readoutTop + valueSize + 4,
14085
- width: content.width,
14669
+ top: cy + 8,
14670
+ width: dialW,
14086
14671
  text: caption,
14087
14672
  color: tokens.color.textMuted,
14088
14673
  size: captionSize,
14089
14674
  align: "center"
14090
14675
  });
14091
14676
  const hitTest = (px, py) => {
14092
- const dx = px - cx;
14093
- const dy = py - cy;
14094
- const distance = Math.hypot(dx, dy);
14095
- const withinDial = distance <= radius + trackWidth * 2 && py >= cy - radius - trackWidth * 2 && py <= cy + readoutReserve;
14677
+ const withinDial = px >= cx - radius - trackWidth && px <= cx + radius + trackWidth && py >= cy - radius - trackWidth && py <= cy + captionReserve + 4;
14096
14678
  if (!withinDial) return null;
14097
- const rows = [{ label: "value", value: valueText }];
14679
+ const rows = [{ label: gauge.valueLabel ?? "value", value: valueText }];
14680
+ if (compare2 != null) {
14681
+ rows.push({ label: gauge.compareLabel ?? "prior period", value: formatValue(compare2, gauge.format), muted: true });
14682
+ }
14098
14683
  if (target != null) rows.push({ label: "target", value: formatValue(target, gauge.format), muted: true });
14099
14684
  return {
14100
14685
  key: "value",
@@ -14122,7 +14707,7 @@ function titleText2(title2) {
14122
14707
  function valueField3(ref) {
14123
14708
  return typeof ref === "number" ? null : ref.field;
14124
14709
  }
14125
- function resolveValue3(ref, data) {
14710
+ function resolveValue(ref, data) {
14126
14711
  if (ref == null) return null;
14127
14712
  if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
14128
14713
  return aggregateValues((data ?? []).map(accessor(ref.field)), ref.aggregate ?? "sum");
@@ -14154,8 +14739,8 @@ function drawBullet(surface, spec, tokens, size, context) {
14154
14739
  const ctx = surface.marks.ctx;
14155
14740
  const content = drawTitleBlock(surface, tokens, size, bullet.title);
14156
14741
  const data = bullet.data ?? [];
14157
- const value = resolveValue3(bullet.value, data);
14158
- const target = resolveValue3(bullet.target, data);
14742
+ const value = resolveValue(bullet.value, data);
14743
+ const target = resolveValue(bullet.target, data);
14159
14744
  const caption = bullet.label ?? titleText2(bullet.title) ?? valueField3(bullet.value) ?? "Value";
14160
14745
  const fmt = bullet.format;
14161
14746
  const finiteRanges = (bullet.ranges ?? []).filter((r) => Number.isFinite(r)).sort((a, b) => a - b);
@@ -14176,10 +14761,7 @@ function drawBullet(surface, spec, tokens, size, context) {
14176
14761
  }
14177
14762
  const labelFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.bold);
14178
14763
  const valueFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
14179
- const labelW = Math.min(
14180
- Math.max(72, content.width * 0.18, measureText(caption, labelFont).width + 18),
14181
- Math.max(72, content.width * 0.38)
14182
- );
14764
+ const labelW = clamp9(measureText(caption, labelFont).width + 14, 44, Math.max(72, content.width * 0.4));
14183
14765
  const axisReserve = Math.max(24, tokens.font.size.tiny * 2);
14184
14766
  const availableTrackW = Math.max(0, content.width - labelW);
14185
14767
  const trackW = Math.max(0, availableTrackW - 4);
@@ -14297,7 +14879,6 @@ function drawBullet(surface, spec, tokens, size, context) {
14297
14879
  // src/charts/calendarHeatmap.ts
14298
14880
  var DAY_MS2 = 864e5;
14299
14881
  var GAP2 = 2;
14300
- var MAX_CELL = 16;
14301
14882
  var WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
14302
14883
  var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
14303
14884
  function pad22(n) {
@@ -14320,9 +14901,9 @@ function addDays(date, days) {
14320
14901
  d.setDate(d.getDate() + days);
14321
14902
  return d;
14322
14903
  }
14323
- function drawRoundedCell2(ctx, x, y, size, radius) {
14904
+ function drawRoundedCell2(ctx, x, y, w, h, radius) {
14324
14905
  ctx.beginPath();
14325
- roundedRect(ctx, x, y, size, size, radius);
14906
+ roundedRect(ctx, x, y, w, h, radius);
14326
14907
  ctx.fill();
14327
14908
  }
14328
14909
  function noData(surface, tokens, content) {
@@ -14338,7 +14919,7 @@ function noData(surface, tokens, content) {
14338
14919
  }
14339
14920
  function normalizeDays(days) {
14340
14921
  if (Array.isArray(days)) {
14341
- return days.filter((d) => Number.isFinite(d.value)).map((d) => ({ isoDate: isoDay(d.date), date: startOfLocalDay(d.date), value: d.value }));
14922
+ return days.filter((d) => Number.isFinite(d.value)).map((d) => ({ isoDate: isoDay(d.date), date: startOfLocalDay(d.date), value: d.value, row: d.row }));
14342
14923
  }
14343
14924
  const out = [];
14344
14925
  const dayMap = days;
@@ -14360,19 +14941,20 @@ function buildCalendarGrid(days) {
14360
14941
  const start = addDays(minDay, -minDay.getDay());
14361
14942
  const end = addDays(maxDay, 6 - maxDay.getDay());
14362
14943
  const weeks = Math.floor(diffDays(end, start) / 7) + 1;
14363
- const byValue = new Map(values.map((d) => [d.isoDate, d.value]));
14944
+ const byValue = new Map(values.map((d) => [d.isoDate, { value: d.value, row: d.row }]));
14364
14945
  const cells = [];
14365
14946
  const byIsoDate = /* @__PURE__ */ new Map();
14366
14947
  for (let d = start; diffDays(end, d) >= 0; d = addDays(d, 1)) {
14367
14948
  const isoDate = isoDay(d);
14368
- const value = byValue.get(isoDate);
14949
+ const dayValue = byValue.get(isoDate);
14369
14950
  const cell = {
14370
14951
  isoDate,
14371
14952
  date: d,
14372
14953
  week: Math.floor(diffDays(d, start) / 7),
14373
14954
  weekday: d.getDay(),
14374
- value,
14375
- present: value !== void 0
14955
+ value: dayValue?.value,
14956
+ row: dayValue?.row,
14957
+ present: dayValue !== void 0
14376
14958
  };
14377
14959
  cells.push(cell);
14378
14960
  byIsoDate.set(isoDate, cell);
@@ -14391,9 +14973,9 @@ function collectDays(spec) {
14391
14973
  const isoDate = isoDay(day);
14392
14974
  const current = totals.get(isoDate);
14393
14975
  if (current) current.value += value;
14394
- else totals.set(isoDate, { date: day, value });
14976
+ else totals.set(isoDate, { date: day, value, row });
14395
14977
  }
14396
- return [...totals.entries()].map(([isoDate, { date, value }]) => ({ isoDate, date, value }));
14978
+ return [...totals.entries()].map(([isoDate, { date, value, row }]) => ({ isoDate, date, value, row }));
14397
14979
  }
14398
14980
  function monthAnchors(grid) {
14399
14981
  const seen = /* @__PURE__ */ new Set();
@@ -14417,6 +14999,8 @@ function humanDate(date) {
14417
14999
  function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14418
15000
  const cal = spec;
14419
15001
  const ctx = surface.marks.ctx;
15002
+ const emphasis = context?.emphasis ?? null;
15003
+ const dateField = cal.encoding.date.field;
14420
15004
  const content = drawTitleBlock(surface, tokens, size, cal.title);
14421
15005
  const days = collectDays(cal);
14422
15006
  const grid = buildCalendarGrid(days);
@@ -14430,16 +15014,26 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14430
15014
  const labelHeight = Math.ceil(labelSize * 1.7) + 2;
14431
15015
  const availableW = Math.max(0, content.width - leftGutter);
14432
15016
  const availableH = Math.max(0, content.height - labelHeight);
14433
- const cell = Math.floor(Math.min(MAX_CELL, (availableW - GAP2 * Math.max(0, grid.weeks - 1)) / grid.weeks, (availableH - GAP2 * 6) / 7));
14434
- 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) {
14435
15023
  noData(surface, tokens, content);
14436
15024
  return;
14437
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);
14438
15032
  const gridRect = {
14439
- x: content.x + leftGutter,
14440
- y: content.y + labelHeight,
14441
- width: grid.weeks * cell + Math.max(0, grid.weeks - 1) * GAP2,
14442
- height: 7 * cell + 6 * GAP2
15033
+ x: content.x + leftGutter + hpad,
15034
+ y: content.y + labelHeight + vpad,
15035
+ width: gridW,
15036
+ height: gridH
14443
15037
  };
14444
15038
  const values = days.map((d) => d.value);
14445
15039
  const min = Math.min(...values);
@@ -14448,33 +15042,35 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14448
15042
  const cscale = sequentialColorScale({ domain: [min, max], interpolator: interp });
14449
15043
  const sketch = resolveSketch(spec);
14450
15044
  const pen = sketch ? new RoughPen(ctx, sketch) : null;
14451
- const radius = Math.min(3, tokens.radius.sm, cell / 3);
15045
+ const radius = Math.min(3, tokens.radius.sm, Math.min(cellW, cellH) / 3);
14452
15046
  const laidOut = /* @__PURE__ */ new Map();
14453
15047
  ctx.save();
14454
15048
  for (const c of grid.cells) {
14455
- const x = gridRect.x + c.week * (cell + GAP2);
14456
- const y = gridRect.y + c.weekday * (cell + GAP2);
15049
+ const x = gridRect.x + c.week * stepX;
15050
+ const y = gridRect.y + c.weekday * stepY;
14457
15051
  const fill = c.present && c.value !== void 0 ? rgbaToCss(cscale.map(c.value)) : tokens.color.surface;
14458
- laidOut.set(c.isoDate, { ...c, x, y, size: cell, fill });
15052
+ laidOut.set(c.isoDate, { ...c, x, y, w: cellW, h: cellH, fill });
15053
+ ctx.globalAlpha = c.present ? rowAlpha(emphasis, c.row ?? { [dateField]: c.isoDate }) : 1;
14459
15054
  if (pen) {
14460
- pen.rect(x, y, cell, cell, {
15055
+ pen.rect(x, y, cellW, cellH, {
14461
15056
  fill,
14462
15057
  fillStyle: "solid",
14463
15058
  stroke: c.present ? fill : tokens.color.border
14464
15059
  });
14465
15060
  } else {
14466
15061
  ctx.fillStyle = fill;
14467
- drawRoundedCell2(ctx, x, y, cell, radius);
15062
+ drawRoundedCell2(ctx, x, y, cellW, cellH, radius);
14468
15063
  ctx.strokeStyle = tokens.color.border;
14469
15064
  ctx.lineWidth = 1;
14470
- 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));
14471
15066
  }
15067
+ ctx.globalAlpha = 1;
14472
15068
  }
14473
15069
  ctx.restore();
14474
15070
  for (const m of monthAnchors(grid)) {
14475
15071
  addOverlayText(surface, tokens, {
14476
- left: gridRect.x + m.week * (cell + GAP2),
14477
- top: content.y,
15072
+ left: gridRect.x + m.week * stepX,
15073
+ top: gridRect.y - labelHeight,
14478
15074
  text: m.label,
14479
15075
  color: tokens.color.textMuted,
14480
15076
  size: labelSize
@@ -14483,7 +15079,7 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14483
15079
  for (const weekday of [1, 3, 5]) {
14484
15080
  addOverlayText(surface, tokens, {
14485
15081
  left: content.x,
14486
- top: gridRect.y + weekday * (cell + GAP2) + cell / 2,
15082
+ top: gridRect.y + weekday * stepY + cellH / 2,
14487
15083
  width: Math.max(0, leftGutter - tokens.spacing.xs),
14488
15084
  text: WEEKDAY_LABELS[weekday],
14489
15085
  color: tokens.color.textMuted,
@@ -14495,11 +15091,11 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14495
15091
  const colorLabel = cal.encoding.color.title ?? cal.encoding.color.field;
14496
15092
  const hitCell = (px, py) => {
14497
15093
  if (px < gridRect.x || py < gridRect.y || px > gridRect.x + gridRect.width || py > gridRect.y + gridRect.height) return null;
14498
- const week = Math.floor((px - gridRect.x) / (cell + GAP2));
14499
- const weekday = Math.floor((py - gridRect.y) / (cell + GAP2));
14500
- const x = gridRect.x + week * (cell + GAP2);
14501
- const y = gridRect.y + weekday * (cell + GAP2);
14502
- 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;
14503
15099
  const match = grid.cells.find((c) => c.week === week && c.weekday === weekday);
14504
15100
  if (!match || !match.present) return null;
14505
15101
  return laidOut.get(match.isoDate) ?? null;
@@ -14528,7 +15124,6 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14528
15124
  }
14529
15125
 
14530
15126
  // src/runtime/waterfall.ts
14531
- var DEFAULT_PADDING4 = { top: 12, right: 16, bottom: 12, left: 12 };
14532
15127
  function labelFor2(value) {
14533
15128
  if (value == null) return "";
14534
15129
  if (value instanceof Date) return value.toISOString().slice(0, 10);
@@ -14636,7 +15231,7 @@ function buildWaterfallModel(spec, tokens, opts) {
14636
15231
  const fmt = spec.encoding.value.format ?? spec.axes?.y?.format;
14637
15232
  const yLabels = yTickValues.map((v) => fmt ? formatValue(v, fmt) : formatNumber2(v, ","));
14638
15233
  const categories = bars.map((b) => b.label);
14639
- const padding = { ...DEFAULT_PADDING4, ...spec.padding };
15234
+ const padding = { ...NO_PADDING, ...spec.padding };
14640
15235
  const xShow = spec.axes?.x?.show !== false;
14641
15236
  const yShow = spec.axes?.y?.show !== false;
14642
15237
  const xTitle = spec.axes?.x?.title ?? spec.encoding.stage.title;
@@ -14746,13 +15341,15 @@ function buildWaterfallModel(spec, tokens, opts) {
14746
15341
  }
14747
15342
 
14748
15343
  // src/charts/waterfall.ts
14749
- function drawConnectors(surface, model) {
15344
+ function drawConnectors(surface, model, alphaOf) {
14750
15345
  if (model.connectors.length === 0) return;
14751
15346
  const ctx = surface.marks.ctx;
14752
15347
  const color = model.tokens.color.axis;
14753
15348
  if (model.sketch) {
14754
15349
  const pen = new RoughPen(ctx, model.sketch);
14755
- for (const c of model.connectors) {
15350
+ for (let i = 0; i < model.connectors.length; i += 1) {
15351
+ const c = model.connectors[i];
15352
+ ctx.globalAlpha = alphaOf(model.bars[i]);
14756
15353
  pen.polyline(
14757
15354
  [
14758
15355
  { x: c.x1, y: c.y },
@@ -14760,6 +15357,7 @@ function drawConnectors(surface, model) {
14760
15357
  ],
14761
15358
  { stroke: color }
14762
15359
  );
15360
+ ctx.globalAlpha = 1;
14763
15361
  }
14764
15362
  return;
14765
15363
  }
@@ -14767,23 +15365,28 @@ function drawConnectors(surface, model) {
14767
15365
  ctx.strokeStyle = color;
14768
15366
  ctx.lineWidth = 1;
14769
15367
  ctx.setLineDash([2, 2]);
14770
- ctx.beginPath();
14771
- for (const c of model.connectors) {
15368
+ for (let i = 0; i < model.connectors.length; i += 1) {
15369
+ const c = model.connectors[i];
14772
15370
  const y = Math.round(c.y) + 0.5;
15371
+ ctx.globalAlpha = alphaOf(model.bars[i]);
15372
+ ctx.beginPath();
14773
15373
  ctx.moveTo(c.x1, y);
14774
15374
  ctx.lineTo(c.x2, y);
15375
+ ctx.stroke();
15376
+ ctx.globalAlpha = 1;
14775
15377
  }
14776
- ctx.stroke();
14777
15378
  ctx.restore();
14778
15379
  }
14779
- function drawBars2(surface, model) {
15380
+ function drawBars2(surface, model, alphaOf) {
14780
15381
  const ctx = surface.marks.ctx;
14781
15382
  const pen = model.sketch ? new RoughPen(ctx, model.sketch) : null;
14782
15383
  for (const bar of model.bars) {
14783
15384
  const { rect } = bar;
14784
15385
  if (rect.width <= 0 || rect.height <= 0) continue;
15386
+ ctx.globalAlpha = alphaOf(bar);
14785
15387
  if (pen) {
14786
15388
  pen.rect(rect.x, rect.y, rect.width, rect.height, { stroke: bar.color, fill: bar.color });
15389
+ ctx.globalAlpha = 1;
14787
15390
  continue;
14788
15391
  }
14789
15392
  const r = Math.min(model.cornerRadius, rect.width / 2, rect.height / 2);
@@ -14791,6 +15394,7 @@ function drawBars2(surface, model) {
14791
15394
  roundedRect(ctx, rect.x, rect.y, rect.width, rect.height, [r, r, r, r]);
14792
15395
  ctx.fillStyle = bar.color;
14793
15396
  ctx.fill();
15397
+ ctx.globalAlpha = 1;
14794
15398
  }
14795
15399
  }
14796
15400
  function barLabelText(bar, format) {
@@ -14820,17 +15424,19 @@ function drawLabels(surface, model) {
14820
15424
  });
14821
15425
  }
14822
15426
  }
14823
- function drawWaterfall(surface, spec, tokens, size, _context) {
15427
+ function drawWaterfall(surface, spec, tokens, size, context) {
14824
15428
  const waterfall = spec;
14825
15429
  const model = buildWaterfallModel(waterfall, tokens, size);
15430
+ const emphasis = context?.emphasis ?? null;
15431
+ const stageField = waterfall.encoding.stage.field;
15432
+ const alphaOf = (bar) => bar.stageValue === void 0 ? 1 : rowAlpha(emphasis, { [stageField]: bar.stageValue });
14826
15433
  drawAxesUnderlay(surface, model.base);
14827
- drawConnectors(surface, model);
14828
- drawBars2(surface, model);
15434
+ drawConnectors(surface, model, alphaOf);
15435
+ drawBars2(surface, model, alphaOf);
14829
15436
  drawLabels(surface, model);
14830
15437
  drawOverlay(surface, model.base);
14831
15438
  if (model.bars.length === 0) return;
14832
15439
  const { plot, format } = model;
14833
- const stageField = waterfall.encoding.stage.field;
14834
15440
  const valueLabel2 = waterfall.encoding.value.title ?? waterfall.encoding.value.field;
14835
15441
  const fmtNum = (v) => format ? formatValue(v, format) : formatNumber2(v, ",");
14836
15442
  const hitBar = (px, py) => {
@@ -14866,7 +15472,6 @@ function drawWaterfall(surface, spec, tokens, size, _context) {
14866
15472
  }
14867
15473
 
14868
15474
  // src/runtime/slope.ts
14869
- var DEFAULT_PADDING5 = { top: 16, right: 76, bottom: 26, left: 76 };
14870
15475
  function labelFor3(value) {
14871
15476
  if (value == null) return "";
14872
15477
  if (value instanceof Date) return value.toISOString().slice(0, 10);
@@ -14902,7 +15507,44 @@ function buildSlopeModel(spec, tokens, opts) {
14902
15507
  ];
14903
15508
  const fmt = spec.format ?? spec.encoding.y.format ?? spec.axes?.y?.format;
14904
15509
  const yLabels = yTickValues.map((v) => fmtTick2(v, fmt));
14905
- 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 };
14906
15548
  const xShow = spec.axes?.x?.show !== false;
14907
15549
  const yShow = spec.axes?.y?.show === true;
14908
15550
  const xTitle = spec.axes?.x?.title ?? spec.encoding.x.title;
@@ -14919,20 +15561,6 @@ function buildSlopeModel(spec, tokens, opts) {
14919
15561
  const plot = frame.plot;
14920
15562
  const point = pointScale({ domain: categories, range: [plot.x, plot.x + plot.width], padding: 0.5 });
14921
15563
  const yScale = linearScale({ domain: yDomain, range: [plot.y + plot.height, plot.y] });
14922
- const grouped = /* @__PURE__ */ new Map();
14923
- for (const row of data) {
14924
- const rawSeries = readSeries(row);
14925
- const key = toKey(rawSeries);
14926
- let group = grouped.get(key);
14927
- if (!group) {
14928
- group = { key, label: labelFor3(rawSeries), value: rawSeries, values: /* @__PURE__ */ new Map() };
14929
- grouped.set(key, group);
14930
- }
14931
- const rawX = readX(row);
14932
- const catKey = labelFor3(rawX);
14933
- const value = toNumber(readY(row));
14934
- if (!Number.isNaN(value) && !group.values.has(catKey)) group.values.set(catKey, value);
14935
- }
14936
15564
  const palette = ordinalColorScale({ domain: [...grouped.keys()], palette: tokens.color.palette });
14937
15565
  const series = [...grouped.values()].map((group) => {
14938
15566
  const points = categories.map((catKey) => {
@@ -15001,7 +15629,9 @@ function buildSlopeModel(spec, tokens, opts) {
15001
15629
  series,
15002
15630
  categories,
15003
15631
  format: fmt,
15004
- showLabels: spec.labels !== false,
15632
+ showLabels,
15633
+ leftLabelW,
15634
+ rightLabelW,
15005
15635
  base,
15006
15636
  sketch
15007
15637
  };
@@ -15009,13 +15639,15 @@ function buildSlopeModel(spec, tokens, opts) {
15009
15639
 
15010
15640
  // src/charts/slope.ts
15011
15641
  var HIT_RADIUS2 = 8;
15012
- var LABEL_WIDTH = 68;
15013
15642
  function fmtValue(v, format) {
15014
15643
  return format ? formatValue(v, format) : formatNumber2(v, ",");
15015
15644
  }
15016
- function drawLines(surface, model) {
15645
+ function drawLines(surface, model, alphaOf) {
15017
15646
  const ctx = surface.marks.ctx;
15018
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);
15019
15651
  ctx.save();
15020
15652
  ctx.beginPath();
15021
15653
  ctx.rect(model.plot.x, model.plot.y, model.plot.width, model.plot.height);
@@ -15023,12 +15655,13 @@ function drawLines(surface, model) {
15023
15655
  for (const series of model.series) {
15024
15656
  const points = series.points;
15025
15657
  if (points.length === 0) continue;
15658
+ ctx.globalAlpha = alphaOf(series);
15026
15659
  if (pen && points.length >= 2) {
15027
- pen.polyline(points, { stroke: series.color });
15660
+ pen.polyline(points, { stroke: series.color, strokeWidth: strokeW });
15028
15661
  } else if (points.length >= 2) {
15029
15662
  ctx.beginPath();
15030
15663
  ctx.strokeStyle = series.color;
15031
- ctx.lineWidth = 2;
15664
+ ctx.lineWidth = strokeW;
15032
15665
  ctx.lineJoin = "round";
15033
15666
  ctx.lineCap = "round";
15034
15667
  ctx.moveTo(points[0].x, points[0].y);
@@ -15037,58 +15670,65 @@ function drawLines(surface, model) {
15037
15670
  }
15038
15671
  for (const p of points) {
15039
15672
  if (pen) {
15040
- 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 });
15041
15674
  } else {
15042
15675
  ctx.beginPath();
15043
15676
  ctx.fillStyle = series.color;
15044
15677
  ctx.strokeStyle = model.tokens.color.background;
15045
- ctx.lineWidth = 1.5;
15046
- 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);
15047
15680
  ctx.fill();
15048
15681
  ctx.stroke();
15049
15682
  }
15050
15683
  }
15684
+ ctx.globalAlpha = 1;
15051
15685
  }
15052
15686
  ctx.restore();
15053
15687
  }
15054
- function drawEndLabels(surface, model) {
15688
+ function drawEndLabels(surface, model, alphaOf) {
15055
15689
  if (!model.showLabels) return;
15056
15690
  const { tokens } = model;
15057
15691
  const labelSize = tokens.font.size.tiny;
15058
15692
  const valueSize = tokens.font.size.tiny;
15693
+ const leftW = model.leftLabelW;
15694
+ const rightW = model.rightLabelW;
15059
15695
  for (const series of model.series) {
15060
15696
  if (series.points.length === 0) continue;
15697
+ const opacity = alphaOf(series) === 1 ? 1 : 0.5;
15061
15698
  const first = series.points[0];
15062
15699
  const last = series.points[series.points.length - 1];
15063
15700
  addOverlayText(surface, tokens, {
15064
- left: first.x - 8 - LABEL_WIDTH,
15701
+ left: first.x - 8 - leftW,
15065
15702
  top: first.y - labelSize - 2,
15066
- width: LABEL_WIDTH,
15703
+ width: leftW,
15067
15704
  text: series.label,
15068
15705
  color: series.color,
15069
15706
  size: labelSize,
15070
15707
  weight: tokens.font.weight.bold,
15071
- align: "right"
15708
+ align: "right",
15709
+ opacity
15072
15710
  });
15073
15711
  addOverlayText(surface, tokens, {
15074
- left: first.x - 8 - LABEL_WIDTH,
15712
+ left: first.x - 8 - leftW,
15075
15713
  top: first.y + 1,
15076
- width: LABEL_WIDTH,
15714
+ width: leftW,
15077
15715
  text: fmtValue(first.value, model.format),
15078
15716
  color: tokens.color.textMuted,
15079
15717
  size: valueSize,
15080
- align: "right"
15718
+ align: "right",
15719
+ opacity
15081
15720
  });
15082
15721
  if (last !== first) {
15083
15722
  addOverlayText(surface, tokens, {
15084
15723
  left: last.x + 8,
15085
15724
  top: last.y - valueSize / 2,
15086
- width: LABEL_WIDTH,
15725
+ width: rightW,
15087
15726
  text: fmtValue(last.value, model.format),
15088
15727
  color: series.color,
15089
15728
  size: valueSize,
15090
15729
  weight: tokens.font.weight.medium,
15091
- align: "left"
15730
+ align: "left",
15731
+ opacity
15092
15732
  });
15093
15733
  }
15094
15734
  }
@@ -15106,16 +15746,18 @@ function nearestPoint(model, px, py) {
15106
15746
  }
15107
15747
  return best ? { series: best.series, point: best.point } : null;
15108
15748
  }
15109
- function drawSlope(surface, spec, tokens, size, _context) {
15749
+ function drawSlope(surface, spec, tokens, size, context) {
15110
15750
  const slope = spec;
15111
15751
  const model = buildSlopeModel(slope, tokens, size);
15752
+ const emphasis = context?.emphasis ?? null;
15753
+ const seriesField = slope.encoding.series.field;
15754
+ const alphaOf = (series) => emphasis ? emphasis.match({ [seriesField]: series.value }) ? 1 : emphasis.dim : 1;
15112
15755
  drawAxesUnderlay(surface, model.base);
15113
- drawLines(surface, model);
15756
+ drawLines(surface, model, alphaOf);
15114
15757
  drawOverlay(surface, model.base);
15115
- drawEndLabels(surface, model);
15758
+ drawEndLabels(surface, model, alphaOf);
15116
15759
  if (model.series.length === 0) return;
15117
15760
  const { plot } = model;
15118
- const seriesField = slope.encoding.series.field;
15119
15761
  return {
15120
15762
  region: { x: plot.x, y: plot.y, width: plot.width, height: plot.height },
15121
15763
  hitTest: (px, py) => {
@@ -15254,7 +15896,7 @@ function measureMax2(labels, font) {
15254
15896
  function valueLabel(value, format) {
15255
15897
  return format ? formatValue(value, format) : formatNumber2(value, ",");
15256
15898
  }
15257
- function drawConnector(ctx, pen, x1, x2, y, tokens) {
15899
+ function drawConnector(ctx, pen, x1, x2, y, width, tokens) {
15258
15900
  if (pen) {
15259
15901
  pen.polyline(
15260
15902
  [
@@ -15270,13 +15912,12 @@ function drawConnector(ctx, pen, x1, x2, y, tokens) {
15270
15912
  ctx.moveTo(x1, y);
15271
15913
  ctx.lineTo(x2, y);
15272
15914
  ctx.strokeStyle = tokens.color.axis;
15273
- ctx.lineWidth = 3;
15915
+ ctx.lineWidth = width;
15274
15916
  ctx.lineCap = "round";
15275
15917
  ctx.stroke();
15276
15918
  ctx.restore();
15277
15919
  }
15278
- function drawDot(ctx, pen, x, y, color, tokens) {
15279
- const r = 5;
15920
+ function drawDot(ctx, pen, x, y, r, color, tokens) {
15280
15921
  if (pen) {
15281
15922
  pen.circle(x, y, r + 1.5, { fill: tokens.color.background, stroke: tokens.color.background });
15282
15923
  pen.circle(x, y, r, { fill: color, stroke: tokens.color.background });
@@ -15287,16 +15928,16 @@ function drawDot(ctx, pen, x, y, color, tokens) {
15287
15928
  ctx.arc(x, y, r, 0, Math.PI * 2);
15288
15929
  ctx.fillStyle = color;
15289
15930
  ctx.fill();
15290
- ctx.lineWidth = 1.5;
15931
+ ctx.lineWidth = Math.max(1.5, r * 0.3);
15291
15932
  ctx.strokeStyle = tokens.color.background;
15292
15933
  ctx.stroke();
15293
15934
  ctx.restore();
15294
15935
  }
15295
- function layoutPlot(content, labelWidth, tokens) {
15936
+ function layoutPlot(content, labelWidth, rightReserve, tokens) {
15296
15937
  const leftGutter = Math.ceil(Math.min(content.width * 0.3, Math.max(42, labelWidth + tokens.spacing.sm)));
15297
15938
  const axisHeight = 28;
15298
15939
  const topPad = Math.min(6, CHROME_PAD.top / 2);
15299
- const rightPad = 8;
15940
+ const rightPad = Math.max(8, Math.ceil(rightReserve));
15300
15941
  return {
15301
15942
  leftGutter,
15302
15943
  axisHeight,
@@ -15308,10 +15949,12 @@ function layoutPlot(content, labelWidth, tokens) {
15308
15949
  }
15309
15950
  };
15310
15951
  }
15311
- function drawDumbbell(surface, spec, tokens, size, _context) {
15952
+ function drawDumbbell(surface, spec, tokens, size, context) {
15312
15953
  const dumbbell = spec;
15313
15954
  const model = buildDumbbellModel(dumbbell, tokens);
15314
15955
  const ctx = surface.marks.ctx;
15956
+ const emphasis = context?.emphasis ?? null;
15957
+ const categoryField = dumbbell.encoding.category.field;
15315
15958
  let content = drawTitleBlock(surface, tokens, size, dumbbell.title);
15316
15959
  const legendEntries = model.groups.map((group) => ({ label: group.label, color: group.color, symbol: "circle" }));
15317
15960
  content = drawCategoricalLegend(surface, tokens, content, legendEntries, "top");
@@ -15329,7 +15972,16 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
15329
15972
  }
15330
15973
  const labelFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
15331
15974
  const widestCategory = measureMax2(model.rows.map((row) => row.catLabel), labelFont);
15332
- 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);
15333
15985
  if (plot.width <= 8 || plot.height <= 8) return;
15334
15986
  const xScale = linearScale({ domain: model.valueDomain, range: [plot.x, plot.x + plot.width] });
15335
15987
  const yBand = bandScale({ domain: model.categories, range: [plot.y, plot.y + plot.height], paddingInner: 0.45, paddingOuter: 0.25 });
@@ -15356,6 +16008,9 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
15356
16008
  }
15357
16009
  ctx.restore();
15358
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);
15359
16014
  for (const row of model.rows) {
15360
16015
  const bandTop = yBand.map(row.catKey);
15361
16016
  if (bandTop === void 0) continue;
@@ -15372,8 +16027,10 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
15372
16027
  }).sort((a, b) => a.x - b.x);
15373
16028
  if (pixelDots.length === 0) continue;
15374
16029
  pixelRows.push({ ...row, y: cy, pixelDots });
15375
- drawConnector(ctx, pen, pixelDots[0].x, pixelDots[pixelDots.length - 1].x, cy, tokens);
15376
- for (const dot of pixelDots) drawDot(ctx, pen, dot.x, cy, dot.color, tokens);
16030
+ ctx.globalAlpha = rowAlpha(emphasis, { [categoryField]: row.catRawValue });
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);
16033
+ ctx.globalAlpha = 1;
15377
16034
  addOverlayText(surface, tokens, {
15378
16035
  left: content.x,
15379
16036
  top: cy,
@@ -15389,7 +16046,7 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
15389
16046
  const dot = pixelDots[i];
15390
16047
  const atLeft = i === 0 && pixelDots.length > 1;
15391
16048
  addOverlayText(surface, tokens, {
15392
- left: atLeft ? dot.x - 7 : dot.x + 7,
16049
+ left: atLeft ? dot.x - labelGap : dot.x + labelGap,
15393
16050
  top: cy,
15394
16051
  width: 54,
15395
16052
  text: formatDumbbellValue(dot.value, model.format),
@@ -15401,7 +16058,6 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
15401
16058
  }
15402
16059
  }
15403
16060
  }
15404
- const categoryField = dumbbell.encoding.category.field;
15405
16061
  const nearestDot = (px, py) => {
15406
16062
  let best = null;
15407
16063
  for (const row of pixelRows) {
@@ -15578,10 +16234,13 @@ function labelBox(model, horizontalRule, pos, p) {
15578
16234
  const inset = 6;
15579
16235
  if (horizontalRule) {
15580
16236
  const top = p - 3;
15581
- const transform3 = "translateY(-100%)";
15582
- if (pos === "start") return { left: plot.x + inset, top, width: plot.width - inset, align: "left", transform: transform3 };
15583
- if (pos === "middle") return { left: plot.x, top, width: plot.width, align: "center", transform: transform3 };
15584
- 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%)" };
15585
16244
  }
15586
16245
  const left = p;
15587
16246
  const transform2 = "translateX(-50%)";
@@ -15756,7 +16415,8 @@ function drawTrendlineLabels(surface, model) {
15756
16415
  const y1 = model.y.pixel(g.fit.predict(g.xMax));
15757
16416
  if (x1 == null || !Number.isFinite(x1) || !Number.isFinite(y1)) continue;
15758
16417
  const text = `R\xB2=${g.fit.r2.toFixed(2)}`;
15759
- 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;
15760
16420
  const left = nearRight ? x1 - 6 : x1 + 6;
15761
16421
  const align = nearRight ? "right" : "left";
15762
16422
  const transform2 = nearRight ? "translateX(-100%) translateY(-50%)" : "translateY(-50%)";
@@ -16271,6 +16931,7 @@ function dependentParams(highlight, filter, ownParams = []) {
16271
16931
  var MARK_CONTRAST_MIN = 1.5;
16272
16932
  var TEXT_CONTRAST_MIN = 4.5;
16273
16933
  var MAX_DISTINGUISHABLE_SERIES = 8;
16934
+ var DEFAULT_POINT_MARKER_RADIUS = 3.5;
16274
16935
  var SEVERITY_RANK = { error: 0, warning: 1, info: 2 };
16275
16936
  var VALUE_CHART_TYPES = /* @__PURE__ */ new Set(["kpi", "gauge", "bullet"]);
16276
16937
  var CONTINUOUS_COLOR_TYPES = /* @__PURE__ */ new Set([
@@ -16328,6 +16989,7 @@ function buildRenderReport(input) {
16328
16989
  if (model) {
16329
16990
  checkDegenerateAxis(model, add);
16330
16991
  checkClippedMarks(model, spec, add);
16992
+ checkAnnotationOverflow(model, spec, tokens, add);
16331
16993
  checkAxisLabelOverlap(model, spec, tokens, add);
16332
16994
  checkLegendOverflow(model, spec, add);
16333
16995
  }
@@ -16447,6 +17109,69 @@ function checkClippedMarks(model, spec, add) {
16447
17109
  });
16448
17110
  }
16449
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
+ }
16450
17175
  function checkAxisLabelOverlap(model, spec, tokens, add) {
16451
17176
  const ticks2 = model.xTicks;
16452
17177
  if (ticks2.length < 2) return;
@@ -16534,7 +17259,7 @@ function manualYDomainFix(spec) {
16534
17259
  // src/runtime/facet.ts
16535
17260
  var FACETABLE_TYPES2 = /* @__PURE__ */ new Set(["line", "area", "bar", "scatter"]);
16536
17261
  var FACET_GAP = 16;
16537
- var FACET_PADDING = { top: 12, right: 16, bottom: 12, left: 12 };
17262
+ var FACET_PADDING = { top: 0, right: 0, bottom: 0, left: 0 };
16538
17263
  var SEVERITY_RANK2 = { error: 0, warning: 1, info: 2 };
16539
17264
  function isFaceted(spec) {
16540
17265
  const facet = spec.facet;
@@ -16737,6 +17462,8 @@ var CANVAS_MARK_TYPES = /* @__PURE__ */ new Set([
16737
17462
  "sankey",
16738
17463
  "choropleth"
16739
17464
  ]);
17465
+ var COUNT_UP_TYPES = /* @__PURE__ */ new Set(["kpi"]);
17466
+ var SWEEP_TYPES = /* @__PURE__ */ new Set(["pie", "gauge"]);
16740
17467
  var EMPTY_DATA = [];
16741
17468
  function legendConfigFor(model) {
16742
17469
  const legend = model.spec.legend;
@@ -16832,6 +17559,9 @@ function render(target, spec, options) {
16832
17559
  let interaction;
16833
17560
  let entrance;
16834
17561
  let updateTransition;
17562
+ let lastValue = null;
17563
+ let lastCompare = null;
17564
+ let lastSparkline = null;
16835
17565
  let destroyed = false;
16836
17566
  let lastDrawnW = -1;
16837
17567
  let lastDrawnH = -1;
@@ -16859,7 +17589,7 @@ function render(target, spec, options) {
16859
17589
  }
16860
17590
  }
16861
17591
  const hostListeners = /* @__PURE__ */ new Set();
16862
- const draw = (animateEntrance) => {
17592
+ const draw = (animateEntrance, valueTweenFrom, compareTweenFrom, sparklineTweenFrom) => {
16863
17593
  entrance?.cancel();
16864
17594
  entrance = void 0;
16865
17595
  updateTransition?.cancel();
@@ -16889,6 +17619,11 @@ function render(target, spec, options) {
16889
17619
  let facetRpt;
16890
17620
  let legendSelect = null;
16891
17621
  const type = currentSpec.type;
17622
+ if (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type)) {
17623
+ lastValue = resolveValueRef(effectiveSpec.value, effectiveSpec.data ?? EMPTY_DATA);
17624
+ lastCompare = SWEEP_TYPES.has(type) ? resolveValueRef(effectiveSpec.compare, effectiveSpec.data ?? EMPTY_DATA) : null;
17625
+ lastSparkline = type === "kpi" ? kpiSparklineSeries(effectiveSpec, effectiveSpec.data ?? EMPTY_DATA) : null;
17626
+ }
16892
17627
  const facetLayout = isFaceted(effectiveSpec) ? buildFacetModels(effectiveSpec, tokens, size) : null;
16893
17628
  if (facetLayout) {
16894
17629
  drawFacet(surface, effectiveSpec, facetLayout, tokens);
@@ -16919,7 +17654,8 @@ function render(target, spec, options) {
16919
17654
  tokens,
16920
17655
  currentSpec.background,
16921
17656
  anim,
16922
- finishEntrance
17657
+ finishEntrance,
17658
+ type === "bar" ? "grow" : "wipe"
16923
17659
  );
16924
17660
  }
16925
17661
  } else {
@@ -16937,11 +17673,44 @@ function render(target, spec, options) {
16937
17673
  }
16938
17674
  };
16939
17675
  interactionModel = renderer(surface, effectiveSpec, tokens, size, ctx) ?? null;
17676
+ if (anim.enabled) {
17677
+ if (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type)) {
17678
+ const key = COUNT_UP_TYPES.has(type) ? "valueProgress" : "sweepProgress";
17679
+ entrance = runCustomProgress(anim, finishEntrance, (t) => {
17680
+ surface.overlay.replaceChildren();
17681
+ surface.marks.clear();
17682
+ paintBackground(surface, tokens, currentSpec.background);
17683
+ renderer(surface, effectiveSpec, tokens, size, { ...ctx, [key]: t });
17684
+ });
17685
+ } else {
17686
+ entrance = runFadeEntrance(surface, anim, finishEntrance);
17687
+ }
17688
+ } else if ((valueTweenFrom !== void 0 || sparklineTweenFrom !== void 0) && (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type))) {
17689
+ const tween = resolveUpdate(currentSpec.animation, {
17690
+ reducedMotion: prefersReducedMotion(),
17691
+ disabled: globalThis.__GRAPHEIN_DISABLE_ANIM === true
17692
+ });
17693
+ if (tween.enabled) {
17694
+ const key = COUNT_UP_TYPES.has(type) ? "valueProgress" : "sweepProgress";
17695
+ entrance = runCustomProgress(tween, finishEntrance, (t) => {
17696
+ surface.overlay.replaceChildren();
17697
+ surface.marks.clear();
17698
+ paintBackground(surface, tokens, currentSpec.background);
17699
+ renderer(surface, effectiveSpec, tokens, size, {
17700
+ ...ctx,
17701
+ valueFrom: valueTweenFrom,
17702
+ compareFrom: compareTweenFrom,
17703
+ sparklineFrom: sparklineTweenFrom,
17704
+ [key]: t
17705
+ });
17706
+ });
17707
+ }
17708
+ }
16940
17709
  } else {
16941
17710
  drawPlaceholder(surface, tokens, `\u201C${type}\u201D renderer not yet registered`);
16942
- }
16943
- if (anim.enabled) {
16944
- entrance = runFadeEntrance(surface, anim, finishEntrance);
17711
+ if (anim.enabled) {
17712
+ entrance = runFadeEntrance(surface, anim, finishEntrance);
17713
+ }
16945
17714
  }
16946
17715
  }
16947
17716
  if (!interaction) interaction = new InteractionController(surface, tokens);
@@ -17065,8 +17834,18 @@ function render(target, spec, options) {
17065
17834
  });
17066
17835
  const wantFade = anim.enabled && CANVAS_MARK_TYPES.has(prevType) && CANVAS_MARK_TYPES.has(next.type);
17067
17836
  const before = wantFade ? snapshotLayer(surface.marks) : void 0;
17837
+ const prevValue = lastValue;
17838
+ const prevCompare = lastCompare;
17839
+ const prevSparkline = lastSparkline;
17840
+ const wantValueTween = prevType === next.type && (COUNT_UP_TYPES.has(next.type) || SWEEP_TYPES.has(next.type)) && prevValue != null;
17841
+ const wantSparklineTween = prevType === next.type && next.type === "kpi" && prevSparkline != null && prevSparkline.length > 0;
17068
17842
  currentSpec = next;
17069
- draw(false);
17843
+ draw(
17844
+ false,
17845
+ wantValueTween ? prevValue ?? void 0 : void 0,
17846
+ wantValueTween ? prevCompare ?? void 0 : void 0,
17847
+ wantValueTween || wantSparklineTween ? prevSparkline ?? void 0 : void 0
17848
+ );
17070
17849
  if (before && surface.width === prevW && surface.height === prevH && surface.marks.canvas.width === before.width && surface.marks.canvas.height === before.height) {
17071
17850
  updateTransition = runUpdateCrossfade(surface, before, anim);
17072
17851
  }
@@ -17086,7 +17865,7 @@ function render(target, spec, options) {
17086
17865
  }
17087
17866
  };
17088
17867
  }
17089
- function runCartesianEntrance(surface, model, renderer, tokens, background, anim, onDone) {
17868
+ function runCartesianEntrance(surface, model, renderer, tokens, background, anim, onDone, mode = "wipe") {
17090
17869
  const ctx = surface.marks.ctx;
17091
17870
  const { plot } = model;
17092
17871
  const bg = background ?? tokens.color.background;
@@ -17097,6 +17876,14 @@ function runCartesianEntrance(surface, model, renderer, tokens, background, anim
17097
17876
  ctx.fillRect(0, 0, surface.width, surface.height);
17098
17877
  ctx.restore();
17099
17878
  drawAxesUnderlay(surface, model);
17879
+ if (mode === "grow") {
17880
+ model.entranceProgress = t;
17881
+ renderer(surface, model);
17882
+ model.entranceProgress = void 0;
17883
+ drawTrendlines(surface, model);
17884
+ drawAnnotations(surface, model);
17885
+ return;
17886
+ }
17100
17887
  if (t <= 0) return;
17101
17888
  ctx.save();
17102
17889
  ctx.globalAlpha = Math.min(1, 0.2 + 0.8 * t);
@@ -17119,6 +17906,17 @@ function runCartesianEntrance(surface, model, renderer, tokens, background, anim
17119
17906
  }
17120
17907
  });
17121
17908
  }
17909
+ function runCustomProgress(anim, onDone, paint) {
17910
+ return animate({
17911
+ duration: anim.duration,
17912
+ easing: anim.easing,
17913
+ onUpdate: paint,
17914
+ onComplete: () => {
17915
+ paint(1);
17916
+ onDone();
17917
+ }
17918
+ });
17919
+ }
17122
17920
  function runFadeEntrance(surface, anim, onDone) {
17123
17921
  const root = surface.root;
17124
17922
  const apply = (t) => {
@@ -17270,12 +18068,17 @@ function autoParamName(view) {
17270
18068
  const existing = view.spec.params?.[0]?.name;
17271
18069
  return existing ?? `__sel__${view.id}`;
17272
18070
  }
18071
+ function roleOf(view) {
18072
+ return view.interact ?? "auto";
18073
+ }
17273
18074
  function wireViews(views, interactions) {
17274
18075
  const out = views.map((v) => ({ ...v, spec: v.spec }));
17275
18076
  if (interactions === "none") return out;
17276
18077
  const sources = [];
17277
18078
  out.forEach((v, i) => {
17278
18079
  const type = v.spec.type;
18080
+ const role = roleOf(v);
18081
+ if (role === "none" || role === "target") return;
17279
18082
  if (isSlicerType(type)) {
17280
18083
  const s = v.spec;
17281
18084
  sources.push({
@@ -17306,11 +18109,15 @@ function wireViews(views, interactions) {
17306
18109
  name: src.param,
17307
18110
  select: { type: "point", on: "click", fields: src.fields }
17308
18111
  });
17309
- out[src.view].spec = addHighlight(out[src.view].spec, src.param);
18112
+ if (roleOf(out[src.view]) !== "source") {
18113
+ out[src.view].spec = addHighlight(out[src.view].spec, src.param);
18114
+ }
17310
18115
  }
17311
18116
  out.forEach((v, j) => {
17312
18117
  if (isSlicerType(v.spec.type)) return;
17313
18118
  if (j === src.view) return;
18119
+ const trole = roleOf(v);
18120
+ if (trole === "none" || trole === "source") return;
17314
18121
  out[j].spec = addFilterParam(out[j].spec, src.param);
17315
18122
  });
17316
18123
  }
@@ -17360,6 +18167,41 @@ var DEFAULT_BREAKPOINTS = [
17360
18167
  { maxWidth: 960, cols: 6 }
17361
18168
  ];
17362
18169
  var STRIP_SLICERS = /* @__PURE__ */ new Set(["dropdown", "search", "dateRange", "range"]);
18170
+ var DASHBOARD_CLASS = "graphein-dashboard";
18171
+ var DASHBOARD_CELL_CLASS = "graphein-dashboard-cell";
18172
+ var DASHBOARD_SOURCE_CLASS = "is-source";
18173
+ function dashboardStyles(tokens) {
18174
+ const accent = tokens.color.accent;
18175
+ const surface = tokens.color.surface;
18176
+ return `
18177
+ .${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}[data-framed="true"] {
18178
+ transition: border-color 120ms ease, background-color 120ms ease;
18179
+ }
18180
+ .${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}[data-framed="true"]:hover {
18181
+ border-color: ${accent};
18182
+ background-color: ${surface};
18183
+ }
18184
+ .${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}:focus-visible {
18185
+ outline: 2px solid ${accent};
18186
+ outline-offset: 2px;
18187
+ }
18188
+ .${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}.${DASHBOARD_SOURCE_CLASS} {
18189
+ border-color: ${accent};
18190
+ box-shadow: inset 0 0 0 1px ${accent};
18191
+ }
18192
+ `;
18193
+ }
18194
+ function selectionIsActive(value) {
18195
+ if (value == null) return false;
18196
+ if (Array.isArray(value)) return value.length > 0;
18197
+ if (typeof value !== "object") return true;
18198
+ const v = value;
18199
+ if (v.kind === "point") return Array.isArray(v.tuples) && v.tuples.length > 0;
18200
+ if (v.kind === "set") return Array.isArray(v.values) && v.values.length > 0;
18201
+ if (v.kind === "range") return v.min != null || v.max != null;
18202
+ if (v.kind === "text") return typeof v.query === "string" && v.query.length > 0;
18203
+ return true;
18204
+ }
17363
18205
  function stripCellHeight(view, rowHeight) {
17364
18206
  const titled = !!(view.title || view.subtitle);
17365
18207
  const titlePad = titled ? 30 : 0;
@@ -17502,6 +18344,12 @@ function renderDashboard(target, spec, options) {
17502
18344
  let root;
17503
18345
  let resizeObs;
17504
18346
  let destroyed = false;
18347
+ let sourceCells = /* @__PURE__ */ new Map();
18348
+ const syncSourceCell = (name, value) => {
18349
+ const cell = sourceCells.get(name);
18350
+ if (!cell) return;
18351
+ cell.classList.toggle(DASHBOARD_SOURCE_CLASS, selectionIsActive(value));
18352
+ };
17505
18353
  const build = () => {
17506
18354
  const tokens = resolveTheme(currentSpec.theme);
17507
18355
  const layout2 = resolveDashboardLayout(currentSpec.layout);
@@ -17514,8 +18362,9 @@ function renderDashboard(target, spec, options) {
17514
18362
  store.set(param.name, param.value ?? null);
17515
18363
  }
17516
18364
  }
18365
+ sourceCells = /* @__PURE__ */ new Map();
17517
18366
  root = document.createElement("div");
17518
- root.className = "graphein-dashboard";
18367
+ root.className = DASHBOARD_CLASS;
17519
18368
  Object.assign(root.style, {
17520
18369
  boxSizing: "border-box",
17521
18370
  width: currentSpec.dimensions?.width ? `${currentSpec.dimensions.width}px` : "100%",
@@ -17529,6 +18378,10 @@ function renderDashboard(target, spec, options) {
17529
18378
  flexDirection: "column",
17530
18379
  gap: `${layout2.gap}px`
17531
18380
  });
18381
+ const style = document.createElement("style");
18382
+ style.setAttribute("data-graphein-dashboard-style", "");
18383
+ style.textContent = dashboardStyles(tokens);
18384
+ root.appendChild(style);
17532
18385
  const titleText3 = typeof currentSpec.title === "string" ? currentSpec.title : currentSpec.title?.text;
17533
18386
  if (titleText3 || currentSpec.subtitle) {
17534
18387
  const header = document.createElement("div");
@@ -17588,6 +18441,7 @@ function renderDashboard(target, spec, options) {
17588
18441
  };
17589
18442
  const cellChrome = (cell, view) => {
17590
18443
  const framed = view?.frame ?? true;
18444
+ cell.dataset.framed = framed ? "true" : "false";
17591
18445
  Object.assign(cell.style, {
17592
18446
  minWidth: "0",
17593
18447
  minHeight: "0",
@@ -17599,9 +18453,10 @@ function renderDashboard(target, spec, options) {
17599
18453
  background: view?.background ?? (framed ? tokens.color.surface : "transparent")
17600
18454
  });
17601
18455
  };
17602
- const createCard2 = (view) => {
18456
+ const createCard = (view) => {
17603
18457
  const cell = document.createElement("div");
17604
18458
  cellChrome(cell, view);
18459
+ cell.classList.add(DASHBOARD_CELL_CLASS);
17605
18460
  cell.setAttribute("data-view-id", view.id);
17606
18461
  const hasCardChrome = !!(view.title || view.subtitle || view.accent || view.background || view.frame === false || view.padding === "none");
17607
18462
  if (!hasCardChrome) return { cell, host: cell };
@@ -17628,13 +18483,14 @@ function renderDashboard(target, spec, options) {
17628
18483
  padding: `${tokens.spacing.sm}px ${tokens.spacing.md}px ${tokens.spacing.xs}px ${tokens.spacing.md}px`,
17629
18484
  display: "flex",
17630
18485
  flexDirection: "column",
17631
- gap: "2px"
18486
+ gap: "2px",
18487
+ borderBottom: `1px solid ${tokens.color.border}`
17632
18488
  });
17633
18489
  if (view.title) {
17634
18490
  const title2 = document.createElement("div");
17635
18491
  title2.textContent = view.title;
17636
18492
  Object.assign(title2.style, {
17637
- font: `${tokens.font.weight.bold} ${tokens.font.size.base}px ${tokens.font.family}`,
18493
+ font: `${tokens.font.weight.medium} ${tokens.font.size.base}px ${tokens.font.family}`,
17638
18494
  color: tokens.color.text,
17639
18495
  letterSpacing: "-0.01em"
17640
18496
  });
@@ -17662,6 +18518,24 @@ function renderDashboard(target, spec, options) {
17662
18518
  cell.appendChild(host);
17663
18519
  return { cell, host };
17664
18520
  };
18521
+ const publishingParams = (view) => {
18522
+ const names = /* @__PURE__ */ new Set();
18523
+ for (const param of view.spec.params ?? []) {
18524
+ if (param?.name) names.add(param.name);
18525
+ }
18526
+ if (isSlicerType(view.spec.type)) {
18527
+ const slicer = view.spec;
18528
+ const name = typeof slicer.param === "string" ? slicer.param : typeof slicer.field === "string" ? slicer.field : void 0;
18529
+ if (name) names.add(name);
18530
+ }
18531
+ return [...names];
18532
+ };
18533
+ const registerSourceCell = (view, cell) => {
18534
+ for (const name of publishingParams(view)) {
18535
+ sourceCells.set(name, cell);
18536
+ syncSourceCell(name, store.get(name));
18537
+ }
18538
+ };
17665
18539
  const sectionPlans = resolveDashboardSections(gridViews, layout2);
17666
18540
  const layoutGroups = [];
17667
18541
  const deferredMounts = [];
@@ -17674,7 +18548,8 @@ function renderDashboard(target, spec, options) {
17674
18548
  gap: `${layout2.gap}px`
17675
18549
  });
17676
18550
  for (const view of stripViews) {
17677
- const { cell, host } = createCard2(view);
18551
+ const { cell, host } = createCard(view);
18552
+ registerSourceCell(view, cell);
17678
18553
  Object.assign(cell.style, {
17679
18554
  flex: "1 1 220px",
17680
18555
  minWidth: "200px",
@@ -17720,7 +18595,13 @@ function renderDashboard(target, spec, options) {
17720
18595
  if (plan.section?.title) {
17721
18596
  const title2 = document.createElement("span");
17722
18597
  title2.textContent = plan.section.title;
17723
- Object.assign(title2.style, { fontWeight: String(tokens.font.weight.bold), fontSize: `${tokens.font.size.large}px` });
18598
+ Object.assign(title2.style, {
18599
+ color: tokens.color.textMuted,
18600
+ fontWeight: String(tokens.font.weight.medium),
18601
+ fontSize: `${tokens.font.size.small}px`,
18602
+ letterSpacing: "0.06em",
18603
+ textTransform: "uppercase"
18604
+ });
17724
18605
  titles.appendChild(title2);
17725
18606
  }
17726
18607
  if (plan.section?.subtitle) {
@@ -17772,7 +18653,8 @@ function renderDashboard(target, spec, options) {
17772
18653
  parent.appendChild(grid);
17773
18654
  const cells = placeViews(plan.views, plan.cols, layout2.preset ?? "auto");
17774
18655
  for (const cellInfo of cells) {
17775
- const { cell, host } = createCard2(cellInfo.view);
18656
+ const { cell, host } = createCard(cellInfo.view);
18657
+ registerSourceCell(cellInfo.view, cell);
17776
18658
  grid.appendChild(cell);
17777
18659
  cellInfo.el = cell;
17778
18660
  deferredMounts.push({ el: host, view: cellInfo.view, mounted: false });
@@ -17845,8 +18727,10 @@ function renderDashboard(target, spec, options) {
17845
18727
  instances.length = 0;
17846
18728
  if (root && root.parentNode) root.parentNode.removeChild(root);
17847
18729
  root = void 0;
18730
+ sourceCells = /* @__PURE__ */ new Map();
17848
18731
  };
17849
18732
  const unsubscribe = store.subscribe((name, value) => {
18733
+ syncSourceCell(name, value);
17850
18734
  for (const listener of [...hostListeners]) listener(name, value);
17851
18735
  });
17852
18736
  build();
@@ -18064,6 +18948,6 @@ function renderToContext(target, spec) {
18064
18948
  // src/index.ts
18065
18949
  var VERSION = "0.1.0";
18066
18950
 
18067
- 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, 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 };
18068
18952
  //# sourceMappingURL=index.js.map
18069
18953
  //# sourceMappingURL=index.js.map