graphein 0.12.0 → 0.13.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";
@@ -7080,13 +7133,13 @@ function rowAlpha(emphasis, row) {
7080
7133
  if (!emphasis) return 1;
7081
7134
  return emphasis.match(row) ? 1 : emphasis.dim;
7082
7135
  }
7083
- function seriesMatches(emphasis, series) {
7084
- if (!emphasis) return true;
7085
- return series.rows.some((row) => emphasis.match(row));
7086
- }
7087
- function seriesAlpha(emphasis, series) {
7088
- if (!emphasis) return 1;
7089
- return seriesMatches(emphasis, series) ? 1 : emphasis.dim;
7136
+ function seriesEmphasisMode(emphasis, series) {
7137
+ if (!emphasis) return "all";
7138
+ let matched = 0;
7139
+ for (const row of series.rows) if (emphasis.match(row)) matched++;
7140
+ if (matched === 0) return "none";
7141
+ if (matched === series.rows.length) return "all";
7142
+ return "partial";
7090
7143
  }
7091
7144
 
7092
7145
  // src/charts/line.ts
@@ -7114,6 +7167,24 @@ function buildSeriesPoints(model, s) {
7114
7167
  gap: () => ({ x: NaN, y: NaN })
7115
7168
  });
7116
7169
  }
7170
+ function matchPointPixels(model, s) {
7171
+ const emphasis = model.emphasis;
7172
+ if (!emphasis) return [];
7173
+ const readX = accessor(model.x.field);
7174
+ const readY = accessor(model.y.field ?? "");
7175
+ const out = [];
7176
+ for (const d of s.rows) {
7177
+ if (!emphasis.match(d)) continue;
7178
+ const px = model.x.pixel(readX(d));
7179
+ if (px == null) continue;
7180
+ const yRaw = readY(d);
7181
+ if (yRaw == null || yRaw === "") continue;
7182
+ const py = model.y.pixel(yRaw);
7183
+ if (Number.isNaN(py)) continue;
7184
+ out.push({ x: px, y: py });
7185
+ }
7186
+ return out;
7187
+ }
7117
7188
  function finiteRuns(points) {
7118
7189
  const runs = [];
7119
7190
  let run = [];
@@ -7165,6 +7236,18 @@ function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
7165
7236
  if (alpha !== 1) ctx.globalAlpha = prev;
7166
7237
  }
7167
7238
  }
7239
+ for (const { series, mode, matchPoints } of allSeries) {
7240
+ if (mode !== "partial") continue;
7241
+ ctx.globalAlpha = 1;
7242
+ for (const p of matchPoints) {
7243
+ pen.circle(p.x, p.y, 3.4, {
7244
+ fill: series.color,
7245
+ stroke: model.tokens.color.background,
7246
+ strokeWidth: 1.5
7247
+ });
7248
+ }
7249
+ }
7250
+ ctx.globalAlpha = 1;
7168
7251
  }
7169
7252
  function drawLine(surface, model) {
7170
7253
  const ctx = surface.marks.ctx;
@@ -7173,11 +7256,16 @@ function drawLine(surface, model) {
7173
7256
  const curve = resolveCurve(spec.curve);
7174
7257
  const lineGen = line({ curve });
7175
7258
  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
- }));
7259
+ const allSeries = model.series.map((s) => {
7260
+ const mode = seriesEmphasisMode(model.emphasis, s);
7261
+ return {
7262
+ series: s,
7263
+ points: buildSeriesPoints(model, s),
7264
+ alpha: mode === "all" ? 1 : model.emphasis?.dim ?? 1,
7265
+ mode,
7266
+ matchPoints: matchPointPixels(model, s)
7267
+ };
7268
+ });
7181
7269
  const showPoints = spec.points || allSeries.some((s) => s.points.filter((p) => !Number.isNaN(p.x)).length === 1);
7182
7270
  ctx.save();
7183
7271
  ctx.beginPath();
@@ -7232,6 +7320,20 @@ function drawLine(surface, model) {
7232
7320
  ctx.globalAlpha = 1;
7233
7321
  }
7234
7322
  }
7323
+ for (const { series, mode, matchPoints } of allSeries) {
7324
+ if (mode !== "partial") continue;
7325
+ ctx.fillStyle = series.color;
7326
+ ctx.strokeStyle = tokens.color.background;
7327
+ ctx.lineWidth = 1.5;
7328
+ ctx.globalAlpha = 1;
7329
+ for (const p of matchPoints) {
7330
+ ctx.beginPath();
7331
+ ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
7332
+ ctx.fill();
7333
+ ctx.stroke();
7334
+ }
7335
+ }
7336
+ ctx.globalAlpha = 1;
7235
7337
  ctx.restore();
7236
7338
  }
7237
7339
 
@@ -7242,6 +7344,8 @@ function buildBands(model, stacked) {
7242
7344
  const cumulative = /* @__PURE__ */ new Map();
7243
7345
  const threshold = stacked ? Infinity : Math.max(2, Math.round(model.plot.width));
7244
7346
  return model.series.map((series) => {
7347
+ const mode = seriesEmphasisMode(model.emphasis, series);
7348
+ const matchPoints = [];
7245
7349
  const points = [];
7246
7350
  for (const row of series.rows) {
7247
7351
  const xv = readX(row);
@@ -7254,19 +7358,23 @@ function buildBands(model, stacked) {
7254
7358
  const base = cumulative.get(key) ?? 0;
7255
7359
  const top = base + (missing ? 0 : value);
7256
7360
  cumulative.set(key, top);
7361
+ const y1 = missing ? NaN : model.y.pixel(top);
7257
7362
  points.push({
7258
7363
  x: px == null ? NaN : px,
7259
7364
  y0: model.y.pixel(base),
7260
- y1: missing ? NaN : model.y.pixel(top),
7365
+ y1,
7261
7366
  sortKey: px == null ? Number.POSITIVE_INFINITY : px
7262
7367
  });
7368
+ if (model.emphasis?.match(row) && px != null && !Number.isNaN(y1)) matchPoints.push({ x: px, y: y1 });
7263
7369
  } else {
7370
+ const y1 = missing ? NaN : model.y.pixel(value);
7264
7371
  points.push({
7265
7372
  x: px == null ? NaN : px,
7266
7373
  y0: model.y.baseline,
7267
- y1: missing ? NaN : model.y.pixel(value),
7374
+ y1,
7268
7375
  sortKey: px == null ? Number.POSITIVE_INFINITY : px
7269
7376
  });
7377
+ if (model.emphasis?.match(row) && px != null && !Number.isNaN(y1)) matchPoints.push({ x: px, y: y1 });
7270
7378
  }
7271
7379
  }
7272
7380
  points.sort((a, b) => a.sortKey - b.sortKey);
@@ -7276,7 +7384,7 @@ function buildBands(model, stacked) {
7276
7384
  getY: (p) => p.y1,
7277
7385
  gap: () => ({ x: NaN, y0: NaN, y1: NaN })
7278
7386
  }) : band;
7279
- return { series, points: reduced, alpha: seriesAlpha(model.emphasis, series) };
7387
+ return { series, points: reduced, alpha: mode === "all" ? 1 : model.emphasis?.dim ?? 1, mode, matchPoints };
7280
7388
  });
7281
7389
  }
7282
7390
  function drawAreaSketch(ctx, model, bands, pen, stacked) {
@@ -7311,6 +7419,18 @@ function drawAreaSketch(ctx, model, bands, pen, stacked) {
7311
7419
  );
7312
7420
  if (alpha !== 1) ctx.globalAlpha = prev;
7313
7421
  }
7422
+ for (const { series, mode, matchPoints } of bands) {
7423
+ if (mode !== "partial") continue;
7424
+ ctx.globalAlpha = 1;
7425
+ for (const p of matchPoints) {
7426
+ pen.circle(p.x, p.y, 3.4, {
7427
+ fill: series.color,
7428
+ stroke: model.tokens.color.background,
7429
+ strokeWidth: 1.5
7430
+ });
7431
+ }
7432
+ }
7433
+ ctx.globalAlpha = 1;
7314
7434
  }
7315
7435
  function drawArea(surface, model) {
7316
7436
  const ctx = surface.marks.ctx;
@@ -7360,6 +7480,20 @@ function drawArea(surface, model) {
7360
7480
  ctx.stroke();
7361
7481
  ctx.globalAlpha = 1;
7362
7482
  }
7483
+ for (const { series, mode, matchPoints } of bands) {
7484
+ if (mode !== "partial") continue;
7485
+ ctx.fillStyle = series.color;
7486
+ ctx.strokeStyle = tokens.color.background;
7487
+ ctx.lineWidth = 1.5;
7488
+ ctx.globalAlpha = 1;
7489
+ for (const p of matchPoints) {
7490
+ ctx.beginPath();
7491
+ ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
7492
+ ctx.fill();
7493
+ ctx.stroke();
7494
+ }
7495
+ }
7496
+ ctx.globalAlpha = 1;
7363
7497
  ctx.restore();
7364
7498
  }
7365
7499
 
@@ -7427,6 +7561,8 @@ function drawGroupedBars(ctx, model, radius, pen) {
7427
7561
  const subWidth = seriesCount > 1 ? bandWidth / seriesCount : bandWidth;
7428
7562
  const gap = seriesCount > 1 && subWidth > 3 ? Math.min(1, subWidth * 0.15) : 0;
7429
7563
  const barThick = Math.max(0, subWidth - gap);
7564
+ const t = model.entranceProgress ?? 1;
7565
+ const grow = (pixel) => model.y.baseline + (pixel - model.y.baseline) * t;
7430
7566
  model.series.forEach((series, seriesIndex) => {
7431
7567
  ctx.fillStyle = series.color;
7432
7568
  for (const row of series.rows) {
@@ -7436,7 +7572,7 @@ function drawGroupedBars(ctx, model, radius, pen) {
7436
7572
  const center2 = model.x.pixel(readX(row));
7437
7573
  if (center2 == null) continue;
7438
7574
  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);
7575
+ const rect = orientedRect(horizontal, catStart, barThick, grow(model.y.baseline), grow(model.y.pixel(value)), series.color);
7440
7576
  if (rect) drawRect(ctx, rect, radius, pen, rowAlpha(model.emphasis, row));
7441
7577
  }
7442
7578
  });
@@ -7450,6 +7586,8 @@ function drawStackedBars(ctx, model, radius, pen) {
7450
7586
  const segments = [];
7451
7587
  const roundedEnds = /* @__PURE__ */ new Map();
7452
7588
  const bandWidth = model.x.bandwidth;
7589
+ const t = model.entranceProgress ?? 1;
7590
+ const grow = (pixel) => model.y.baseline + (pixel - model.y.baseline) * t;
7453
7591
  for (const series of model.series) {
7454
7592
  for (const row of series.rows) {
7455
7593
  const xValue = readX(row);
@@ -7464,7 +7602,7 @@ function drawStackedBars(ctx, model, radius, pen) {
7464
7602
  const base = totals.get(categoryKey2) ?? 0;
7465
7603
  const top = base + value;
7466
7604
  totals.set(categoryKey2, top);
7467
- const rect = orientedRect(horizontal, center2 - bandWidth / 2, bandWidth, model.y.pixel(base), model.y.pixel(top), series.color);
7605
+ const rect = orientedRect(horizontal, center2 - bandWidth / 2, bandWidth, grow(model.y.pixel(base)), grow(model.y.pixel(top)), series.color);
7468
7606
  if (!rect) continue;
7469
7607
  segments.push({ ...rect, stackEndKey, alpha: rowAlpha(model.emphasis, row) });
7470
7608
  roundedEnds.set(stackEndKey, segments.length - 1);
@@ -8597,6 +8735,8 @@ function drawPie(surface, spec, tokens, size, context) {
8597
8735
  const colorField = pie.encoding.color.field;
8598
8736
  const emphasis = context?.emphasis ?? null;
8599
8737
  const sliceAlpha = (slice) => emphasis ? emphasis.match({ [colorField]: slice.category }) ? 1 : emphasis.dim : 1;
8738
+ const sweepT = context?.sweepProgress ?? 1;
8739
+ const sweepEnd = TAU3 * sweepT;
8600
8740
  const content = drawTitleBlock(surface, tokens, size, pie.title);
8601
8741
  const slices = buildSlices(pie, tokens.color.palette);
8602
8742
  const legendPosition = resolveLegendPosition(pie, content.width, content.height);
@@ -8654,9 +8794,11 @@ function drawPie(surface, spec, tokens, size, context) {
8654
8794
  if (sketch) {
8655
8795
  const pen = new RoughPen(ctx, sketch);
8656
8796
  for (const slice of slices) {
8797
+ const end = Math.min(slice.endAngle, sweepEnd);
8798
+ if (end <= slice.startAngle) continue;
8657
8799
  const alpha = sliceAlpha(slice);
8658
8800
  if (alpha !== 1) ctx.globalAlpha = alpha;
8659
- pen.wedge(cx, cy, innerRadius, outerRadius, slice.startAngle, slice.endAngle, {
8801
+ pen.wedge(cx, cy, innerRadius, outerRadius, slice.startAngle, end, {
8660
8802
  fill: slice.color,
8661
8803
  stroke: separator
8662
8804
  });
@@ -8665,11 +8807,13 @@ function drawPie(surface, spec, tokens, size, context) {
8665
8807
  ctx.restore();
8666
8808
  } else {
8667
8809
  for (const slice of slices) {
8810
+ const end = Math.min(slice.endAngle, sweepEnd);
8811
+ if (end <= slice.startAngle) continue;
8668
8812
  const wedge = arc({
8669
8813
  innerRadius,
8670
8814
  outerRadius,
8671
8815
  startAngle: slice.startAngle,
8672
- endAngle: slice.endAngle
8816
+ endAngle: end
8673
8817
  });
8674
8818
  ctx.beginPath();
8675
8819
  wedge(ctx, cx, cy);
@@ -8681,7 +8825,7 @@ function drawPie(surface, spec, tokens, size, context) {
8681
8825
  }
8682
8826
  ctx.restore();
8683
8827
  }
8684
- if (labelPlan && labelCfg) {
8828
+ if (sweepT >= 1 && labelPlan && labelCfg) {
8685
8829
  ctx.save();
8686
8830
  ctx.lineWidth = 1;
8687
8831
  ctx.lineJoin = "round";
@@ -8725,7 +8869,7 @@ function drawPie(surface, spec, tokens, size, context) {
8725
8869
  });
8726
8870
  }
8727
8871
  }
8728
- if (innerRadius >= 34) {
8872
+ if (sweepT >= 1 && innerRadius >= 34) {
8729
8873
  addOverlayText(surface, tokens, {
8730
8874
  left: cx,
8731
8875
  top: cy - tokens.font.size.small - 2,
@@ -9010,19 +9154,9 @@ function drawFunnel(surface, spec, tokens, size, context) {
9010
9154
 
9011
9155
  // src/charts/kpi.ts
9012
9156
  var SVG_NS = "http://www.w3.org/2000/svg";
9013
- var SKETCH_BORDER_RADIUS = "255px 15px 225px 15px / 15px 225px 15px 255px";
9014
9157
  function clamp5(value, min, max) {
9015
9158
  return Math.max(min, Math.min(max, value));
9016
9159
  }
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
9160
  function valueField(ref) {
9027
9161
  return typeof ref === "number" ? null : ref.field;
9028
9162
  }
@@ -9033,6 +9167,12 @@ function formatDelta(delta, specFormat) {
9033
9167
  if (abs <= 1) return sign2 + formatNumber2(abs, ".1%");
9034
9168
  return sign2 + formatNumber2(abs, ",.0f");
9035
9169
  }
9170
+ function flexAlign(align) {
9171
+ return align === "start" ? "flex-start" : align === "end" ? "flex-end" : "center";
9172
+ }
9173
+ function textAlignOf(align) {
9174
+ return align === "start" ? "left" : align === "end" ? "right" : "center";
9175
+ }
9036
9176
  function cardRectFor(surface, tokens, size, spec) {
9037
9177
  if (spec.title) return drawTitleBlock(surface, tokens, size, spec.title);
9038
9178
  return {
@@ -9042,41 +9182,25 @@ function cardRectFor(surface, tokens, size, spec) {
9042
9182
  height: Math.max(0, size.height - CHROME_PAD.top - CHROME_PAD.bottom)
9043
9183
  };
9044
9184
  }
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;
9185
+ function valueFontSize(rect, contentWidth, valueText, unit, isRich) {
9186
+ const effectiveLen = valueText.length + (unit ? unit.length * 0.5 + 0.4 : 0);
9187
+ const sizeByCard = Math.min(rect.width, rect.height) * (isRich ? 0.17 : 0.28);
9188
+ const sizeByText = contentWidth / Math.max(1, effectiveLen * 0.58);
9189
+ const lo = isRich ? 22 : 26;
9190
+ const hi = isRich ? 52 : 76;
9191
+ return clamp5(Math.min(sizeByCard, sizeByText), lo, hi);
9067
9192
  }
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;
9193
+ function unitFontSize(valueSize) {
9194
+ return Math.max(12, valueSize * 0.46);
9074
9195
  }
9075
9196
  function sparklineField(spec) {
9076
9197
  if (!spec.sparkline) return null;
9077
- if (typeof spec.sparkline === "object") return spec.sparkline.field;
9198
+ if (typeof spec.sparkline === "object") return spec.sparkline.field ?? valueField(spec.value);
9078
9199
  return valueField(spec.value);
9079
9200
  }
9201
+ function sparklineMarkers(spec) {
9202
+ return typeof spec.sparkline === "object" ? spec.sparkline.markers !== false : true;
9203
+ }
9080
9204
  function sparklineValues(data, field) {
9081
9205
  const read = accessor(field);
9082
9206
  const values = [];
@@ -9086,41 +9210,165 @@ function sparklineValues(data, field) {
9086
9210
  }
9087
9211
  return values;
9088
9212
  }
9089
- function buildSparkline(tokens, width, height, values) {
9213
+ function kpiSparklineSeries(spec, data) {
9214
+ const field = sparklineField(spec);
9215
+ return field ? sparklineValues(data, field) : [];
9216
+ }
9217
+ function morphSparkline(to, from, progress) {
9218
+ if (progress == null || progress >= 1 || to.length === 0) return to;
9219
+ const t = progress;
9220
+ if (from && from.length === to.length) return to.map((v, i) => from[i] + (v - from[i]) * t);
9221
+ const base = to[0];
9222
+ return to.map((v) => base + (v - base) * t);
9223
+ }
9224
+ function resolveComparison(cmp, data, parentFormat) {
9225
+ const delta = resolveValueRef(cmp.delta, data);
9226
+ if (delta == null) return null;
9227
+ const rawPositive = delta >= 0;
9228
+ const tri = rawPositive ? "\u25B2" : "\u25BC";
9229
+ let text = `${tri} ${formatValue(delta, cmp.format ?? ".0%")}`;
9230
+ if (cmp.amount !== void 0) {
9231
+ const amount = resolveValueRef(cmp.amount, data);
9232
+ if (amount != null) text += ` : ${formatValue(amount, cmp.amountFormat ?? parentFormat ?? ",.0f")}`;
9233
+ }
9234
+ return { label: cmp.label, text, positive: cmp.invert ? !rawPositive : rawPositive };
9235
+ }
9236
+ function layoutKpi(rect, tokens, framed, f) {
9237
+ const font = tokens.font;
9238
+ const pad3 = framed ? 0 : tokens.spacing.lg;
9239
+ const left = rect.x + pad3;
9240
+ const contentWidth = Math.max(1, rect.width - pad3 * 2);
9241
+ const availTop = rect.y + pad3;
9242
+ const availH = Math.max(0, rect.height - pad3 * 2);
9243
+ const labelSize = Math.round(clamp5(f.valueSize * 0.4, font.size.small, font.size.title));
9244
+ const labelH = f.hasLabel ? Math.round(labelSize * 1.35) : 0;
9245
+ const valueH = Math.round(f.valueSize * 1.06);
9246
+ const deltaSize = font.size.large;
9247
+ const deltaH = f.hasDelta ? Math.round(deltaSize * 1.5) : 0;
9248
+ const compSize = font.size.large;
9249
+ const compRowH = Math.round(compSize * 1.55);
9250
+ const gapLabel = Math.round(labelSize * 0.25);
9251
+ const gapDelta = f.hasDelta ? tokens.spacing.xs : 0;
9252
+ const gapSpark = tokens.spacing.sm;
9253
+ const dividerPad = tokens.spacing.sm;
9254
+ const headerH = (f.hasLabel && f.labelAbove ? labelH + gapLabel : 0) + valueH + (f.hasLabel && !f.labelAbove ? gapLabel + labelH : 0) + (f.hasDelta ? gapDelta + deltaH : 0);
9255
+ const footerH = f.compCount > 0 ? dividerPad + 1 + dividerPad + f.compCount * compRowH : 0;
9256
+ let headerTop;
9257
+ let footerTop;
9258
+ let spark = null;
9259
+ if (f.hasSparkline) {
9260
+ headerTop = availTop;
9261
+ footerTop = availTop + availH - footerH;
9262
+ const sparkTop = headerTop + headerH + gapSpark;
9263
+ const sparkBottom = footerTop - (footerH > 0 ? gapSpark : 0);
9264
+ spark = { x: left, y: sparkTop, width: contentWidth, height: Math.max(24, sparkBottom - sparkTop) };
9265
+ } else {
9266
+ const groupH = headerH + (footerH > 0 ? tokens.spacing.md + footerH : 0);
9267
+ headerTop = availTop + Math.max(0, (availH - groupH) / 2);
9268
+ footerTop = headerTop + headerH + (footerH > 0 ? tokens.spacing.md : 0);
9269
+ }
9270
+ let y = headerTop;
9271
+ let labelY = -1;
9272
+ if (f.hasLabel && f.labelAbove) {
9273
+ labelY = y;
9274
+ y += labelH + gapLabel;
9275
+ }
9276
+ const valueY = y;
9277
+ y += valueH;
9278
+ if (f.hasLabel && !f.labelAbove) {
9279
+ y += gapLabel;
9280
+ labelY = y;
9281
+ y += labelH;
9282
+ }
9283
+ let deltaY = -1;
9284
+ if (f.hasDelta) {
9285
+ y += gapDelta;
9286
+ deltaY = y;
9287
+ }
9288
+ let dividerY = -1;
9289
+ let compStartY = -1;
9290
+ if (f.compCount > 0) {
9291
+ dividerY = footerTop + dividerPad;
9292
+ compStartY = dividerY + 1 + dividerPad;
9293
+ }
9294
+ return {
9295
+ left,
9296
+ contentWidth,
9297
+ labelSize,
9298
+ labelY,
9299
+ valueY,
9300
+ valueH,
9301
+ deltaSize,
9302
+ deltaY,
9303
+ spark,
9304
+ compSize,
9305
+ compRowH,
9306
+ dividerY,
9307
+ compStartY
9308
+ };
9309
+ }
9310
+ function sparkMarkers(values, xAt, yAt, r, tokens) {
9311
+ const n = values.length;
9312
+ if (n < 2) return [];
9313
+ let minIdx = 0;
9314
+ let maxIdx = 0;
9315
+ for (let i = 1; i < n; i++) {
9316
+ if (values[i] < values[minIdx]) minIdx = i;
9317
+ if (values[i] > values[maxIdx]) maxIdx = i;
9318
+ }
9319
+ const lastIdx = n - 1;
9320
+ const out = [];
9321
+ if (maxIdx !== lastIdx) out.push({ x: xAt(maxIdx), y: yAt(values[maxIdx]), r, fill: tokens.color.positive });
9322
+ if (minIdx !== lastIdx) out.push({ x: xAt(minIdx), y: yAt(values[minIdx]), r, fill: tokens.color.negative });
9323
+ const lastStroke = lastIdx === maxIdx ? tokens.color.positive : lastIdx === minIdx ? tokens.color.negative : tokens.color.text;
9324
+ out.push({
9325
+ x: xAt(lastIdx),
9326
+ y: yAt(values[lastIdx]),
9327
+ r,
9328
+ fill: tokens.color.surface,
9329
+ stroke: lastStroke,
9330
+ strokeWidth: 1.75
9331
+ });
9332
+ return out;
9333
+ }
9334
+ function sparkScales(values, width, height, markers) {
9335
+ const n = values.length;
9336
+ const r = clamp5(Math.min(height, 40) * 0.12, 2.5, 4);
9337
+ const padX = markers ? r + 1 : 1.75;
9338
+ const padY = markers ? r + 1 : 1.75;
9339
+ const innerW = Math.max(1, width - padX * 2);
9340
+ const innerH = Math.max(1, height - padY * 2);
9341
+ const min = Math.min(...values);
9342
+ const max = Math.max(...values);
9343
+ const span = max - min;
9344
+ const xAt = (i) => n === 1 ? width / 2 : padX + i / (n - 1) * innerW;
9345
+ const yAt = (v) => padY + (1 - (span === 0 ? 0.5 : (v - min) / span)) * innerH;
9346
+ return { xAt, yAt, r };
9347
+ }
9348
+ function buildSparkline(tokens, width, height, values, markers) {
9090
9349
  if (values.length === 0 || width <= 0 || height <= 0) return null;
9091
9350
  const svg = document.createElementNS(SVG_NS, "svg");
9092
9351
  svg.setAttribute("width", String(width));
9093
9352
  svg.setAttribute("height", String(height));
9094
9353
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
9095
- svg.setAttribute("preserveAspectRatio", "none");
9096
9354
  svg.style.display = "block";
9097
9355
  svg.style.overflow = "visible";
9098
9356
  const stroke = 1.75;
9099
- const pad3 = stroke;
9100
- const innerH = Math.max(1, height - pad3 * 2);
9357
+ const line2 = tokens.color.textMuted;
9358
+ const { xAt, yAt, r } = sparkScales(values, width, height, markers);
9101
9359
  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
9360
  if (n === 1) {
9111
9361
  const dot = document.createElementNS(SVG_NS, "circle");
9112
9362
  dot.setAttribute("cx", String(width / 2));
9113
9363
  dot.setAttribute("cy", String(yAt(values[0])));
9114
- dot.setAttribute("r", "2.25");
9364
+ dot.setAttribute("r", "2.5");
9115
9365
  dot.setAttribute("fill", tokens.color.accent);
9116
9366
  svg.appendChild(dot);
9117
9367
  return svg;
9118
9368
  }
9119
9369
  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`;
9370
+ for (let i = 0; i < n; i++) lineD += `${i === 0 ? "M" : "L"}${xAt(i).toFixed(2)} ${yAt(values[i]).toFixed(2)}`;
9371
+ const areaD = `${lineD}L${xAt(n - 1).toFixed(2)} ${height}L${xAt(0).toFixed(2)} ${height}Z`;
9124
9372
  const gradId = `graphein-spark-${Math.random().toString(36).slice(2, 9)}`;
9125
9373
  const defs = document.createElementNS(SVG_NS, "defs");
9126
9374
  const grad = document.createElementNS(SVG_NS, "linearGradient");
@@ -9131,11 +9379,11 @@ function buildSparkline(tokens, width, height, values) {
9131
9379
  grad.setAttribute("y2", "1");
9132
9380
  const stopTop = document.createElementNS(SVG_NS, "stop");
9133
9381
  stopTop.setAttribute("offset", "0");
9134
- stopTop.setAttribute("stop-color", tokens.color.accent);
9135
- stopTop.setAttribute("stop-opacity", "0.24");
9382
+ stopTop.setAttribute("stop-color", line2);
9383
+ stopTop.setAttribute("stop-opacity", "0.18");
9136
9384
  const stopBottom = document.createElementNS(SVG_NS, "stop");
9137
9385
  stopBottom.setAttribute("offset", "1");
9138
- stopBottom.setAttribute("stop-color", tokens.color.accent);
9386
+ stopBottom.setAttribute("stop-color", line2);
9139
9387
  stopBottom.setAttribute("stop-opacity", "0");
9140
9388
  grad.appendChild(stopTop);
9141
9389
  grad.appendChild(stopBottom);
@@ -9148,14 +9396,43 @@ function buildSparkline(tokens, width, height, values) {
9148
9396
  const path = document.createElementNS(SVG_NS, "path");
9149
9397
  path.setAttribute("d", lineD);
9150
9398
  path.setAttribute("fill", "none");
9151
- path.setAttribute("stroke", tokens.color.accent);
9399
+ path.setAttribute("stroke", line2);
9152
9400
  path.setAttribute("stroke-width", String(stroke));
9153
9401
  path.setAttribute("stroke-linejoin", "round");
9154
9402
  path.setAttribute("stroke-linecap", "round");
9155
9403
  svg.appendChild(path);
9404
+ if (markers) {
9405
+ for (const m of sparkMarkers(values, xAt, yAt, r, tokens)) {
9406
+ const c = document.createElementNS(SVG_NS, "circle");
9407
+ c.setAttribute("cx", m.x.toFixed(2));
9408
+ c.setAttribute("cy", m.y.toFixed(2));
9409
+ c.setAttribute("r", String(m.r));
9410
+ c.setAttribute("fill", m.fill ?? "none");
9411
+ if (m.stroke) {
9412
+ c.setAttribute("stroke", m.stroke);
9413
+ c.setAttribute("stroke-width", String(m.strokeWidth ?? 1.75));
9414
+ }
9415
+ svg.appendChild(c);
9416
+ }
9417
+ }
9156
9418
  return svg;
9157
9419
  }
9158
- function buildSparklineCanvas(tokens, width, height, values, sketch) {
9420
+ function paintSparkMarkers(ctx, values, xAt, yAt, r, tokens) {
9421
+ for (const m of sparkMarkers(values, xAt, yAt, r, tokens)) {
9422
+ ctx.beginPath();
9423
+ ctx.arc(m.x, m.y, m.r, 0, Math.PI * 2);
9424
+ if (m.fill) {
9425
+ ctx.fillStyle = m.fill;
9426
+ ctx.fill();
9427
+ }
9428
+ if (m.stroke) {
9429
+ ctx.lineWidth = m.strokeWidth ?? 1.75;
9430
+ ctx.strokeStyle = m.stroke;
9431
+ ctx.stroke();
9432
+ }
9433
+ }
9434
+ }
9435
+ function buildSparklineCanvas(tokens, width, height, values, markers, sketch) {
9159
9436
  if (values.length === 0 || width <= 0 || height <= 0) return null;
9160
9437
  const dpr = getDevicePixelRatio();
9161
9438
  const canvas = document.createElement("canvas");
@@ -9167,207 +9444,339 @@ function buildSparklineCanvas(tokens, width, height, values, sketch) {
9167
9444
  const ctx = canvas.getContext("2d");
9168
9445
  if (!ctx) return null;
9169
9446
  ctx.scale(dpr, dpr);
9170
- const stroke = 1.75;
9171
- const pad3 = stroke;
9172
- const innerH = Math.max(1, height - pad3 * 2);
9447
+ const { xAt, yAt, r } = sparkScales(values, width, height, markers);
9173
9448
  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
9449
  const pen = new RoughPen(ctx, sketch);
9180
9450
  if (n === 1) {
9181
9451
  pen.circle(width / 2, yAt(values[0]), 2.5, { fill: tokens.color.accent });
9182
9452
  return canvas;
9183
9453
  }
9184
9454
  const points = values.map((v, i) => ({ x: xAt(i), y: yAt(v) }));
9185
- pen.trendStroke(points, { stroke: tokens.color.accent, strokeWidth: stroke });
9455
+ pen.trendStroke(points, { stroke: tokens.color.textMuted, strokeWidth: 1.75 });
9456
+ if (markers) paintSparkMarkers(ctx, values, xAt, yAt, r, tokens);
9186
9457
  return canvas;
9187
9458
  }
9188
- function drawKpi(surface, spec, tokens, size, context) {
9189
- const kpi = spec;
9190
- if (surface.headless) {
9191
- drawKpiCanvas(surface, kpi, tokens, size, context);
9459
+ function paintSparklineCanvas(ctx, tokens, box, values, markers) {
9460
+ if (values.length === 0 || box.width <= 0 || box.height <= 0) return;
9461
+ const { xAt: xAt0, yAt: yAt0, r } = sparkScales(values, box.width, box.height, markers);
9462
+ const xAt = (i) => box.x + xAt0(i);
9463
+ const yAt = (v) => box.y + yAt0(v);
9464
+ const n = values.length;
9465
+ ctx.save();
9466
+ if (n === 1) {
9467
+ ctx.beginPath();
9468
+ ctx.arc(xAt(0), yAt(values[0]), 2.5, 0, Math.PI * 2);
9469
+ ctx.fillStyle = tokens.color.accent;
9470
+ ctx.fill();
9471
+ ctx.restore();
9192
9472
  return;
9193
9473
  }
9474
+ ctx.globalAlpha = 0.16;
9475
+ ctx.beginPath();
9476
+ ctx.moveTo(xAt(0), yAt(values[0]));
9477
+ for (let i = 1; i < n; i++) ctx.lineTo(xAt(i), yAt(values[i]));
9478
+ ctx.lineTo(xAt(n - 1), box.y + box.height);
9479
+ ctx.lineTo(xAt(0), box.y + box.height);
9480
+ ctx.closePath();
9481
+ ctx.fillStyle = tokens.color.textMuted;
9482
+ ctx.fill();
9483
+ ctx.globalAlpha = 1;
9484
+ ctx.beginPath();
9485
+ for (let i = 0; i < n; i++) {
9486
+ const px = xAt(i);
9487
+ const py = yAt(values[i]);
9488
+ if (i === 0) ctx.moveTo(px, py);
9489
+ else ctx.lineTo(px, py);
9490
+ }
9491
+ ctx.strokeStyle = tokens.color.textMuted;
9492
+ ctx.lineWidth = 1.75;
9493
+ ctx.lineJoin = "round";
9494
+ ctx.lineCap = "round";
9495
+ ctx.stroke();
9496
+ if (markers) paintSparkMarkers(ctx, values, xAt, yAt, r, tokens);
9497
+ ctx.restore();
9498
+ }
9499
+ function drawKpi(surface, spec, tokens, size, context) {
9500
+ const kpi = spec;
9194
9501
  const data = kpi.data ?? [];
9195
- const value = resolveValue(kpi.value, data);
9196
- const delta = resolveValue(kpi.delta, data);
9502
+ const value = resolveValueRef(kpi.value, data);
9503
+ const delta = resolveValueRef(kpi.delta, data);
9197
9504
  const sparkField = sparklineField(kpi);
9198
9505
  const sparkValues = sparkField ? sparklineValues(data, sparkField) : [];
9199
9506
  const hasSparkline = sparkValues.length > 0;
9200
- const sketch = resolveSketch(spec);
9507
+ const markers = sparklineMarkers(kpi);
9508
+ const comparisons = kpi.comparisons?.map((c) => resolveComparison(c, data, kpi.format)).filter((c) => c != null) ?? [];
9509
+ const hasDelta = delta != null;
9510
+ const isRich = hasDelta || hasSparkline || comparisons.length > 0;
9511
+ const align = kpi.align ?? "center";
9512
+ const labelAbove = kpi.labelPosition === "above" || kpi.labelPosition == null && isRich;
9201
9513
  const framed = context?.framed === true;
9202
9514
  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, {
9515
+ const progress = context?.valueProgress;
9516
+ const from = context?.valueFrom ?? 0;
9517
+ const displayValue = value != null && progress != null && progress < 1 ? from + (value - from) * progress : value;
9518
+ const displaySparkValues = morphSparkline(sparkValues, context?.sparklineFrom, progress);
9519
+ const valueText = value == null ? "\u2014" : formatValue(value, kpi.format);
9520
+ const displayText = displayValue == null ? "\u2014" : formatValue(displayValue, kpi.format);
9521
+ const fromText = context?.valueFrom != null && value != null ? formatValue(context.valueFrom, kpi.format) : valueText;
9522
+ const sizingText = fromText.length > valueText.length ? fromText : valueText;
9523
+ const pad3 = framed ? 0 : tokens.spacing.lg;
9524
+ const contentWidth = Math.max(1, rect.width - pad3 * 2);
9525
+ const valueSize = valueFontSize(rect, contentWidth, sizingText, kpi.unit, isRich);
9526
+ const geom = layoutKpi(rect, tokens, framed, {
9527
+ hasLabel: !!kpi.label,
9528
+ labelAbove,
9529
+ hasDelta,
9530
+ hasSparkline,
9531
+ compCount: comparisons.length,
9532
+ valueSize
9533
+ });
9534
+ const sketch = resolveSketch(spec);
9535
+ if (surface.headless) {
9536
+ drawKpiCanvas(surface, kpi, tokens, {
9537
+ align,
9538
+ valueSize,
9539
+ displayText,
9540
+ delta,
9541
+ comparisons,
9542
+ sparkValues: displaySparkValues,
9543
+ markers,
9544
+ geom
9545
+ });
9546
+ return;
9547
+ }
9548
+ const card = document.createElement("div");
9549
+ card.style.position = "absolute";
9550
+ card.style.left = `${rect.x}px`;
9551
+ card.style.top = `${rect.y}px`;
9552
+ card.style.width = `${rect.width}px`;
9553
+ card.style.height = `${rect.height}px`;
9554
+ card.style.boxSizing = "border-box";
9555
+ card.style.overflow = "hidden";
9556
+ card.style.pointerEvents = "none";
9557
+ const ox = rect.x;
9558
+ const oy = rect.y;
9559
+ const addAt = (el, left, top, width) => {
9560
+ el.style.position = "absolute";
9561
+ el.style.left = `${left - ox}px`;
9562
+ el.style.top = `${top - oy}px`;
9563
+ el.style.width = `${width}px`;
9564
+ card.appendChild(el);
9565
+ };
9566
+ if (kpi.label && geom.labelY >= 0) {
9567
+ const el = document.createElement("div");
9568
+ el.textContent = kpi.label;
9569
+ Object.assign(el.style, {
9207
9570
  color: tokens.color.textMuted,
9208
9571
  fontFamily: tokens.font.family,
9209
- fontSize: `${tokens.font.size.base}px`,
9572
+ fontSize: `${geom.labelSize}px`,
9210
9573
  fontWeight: String(tokens.font.weight.medium),
9211
- letterSpacing: "0.04em",
9212
9574
  lineHeight: "1.25",
9213
- textTransform: "uppercase",
9214
- maxWidth: "100%",
9575
+ textAlign: align === "start" ? "left" : align === "end" ? "right" : "center",
9215
9576
  overflow: "hidden",
9216
9577
  textOverflow: "ellipsis",
9217
9578
  whiteSpace: "nowrap"
9218
9579
  });
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,
9580
+ addAt(el, geom.left, geom.labelY, geom.contentWidth);
9581
+ }
9582
+ const valueWrap = document.createElement("div");
9583
+ valueWrap.style.display = "flex";
9584
+ valueWrap.style.alignItems = "baseline";
9585
+ valueWrap.style.justifyContent = flexAlign(align);
9586
+ valueWrap.style.gap = "0.12em";
9587
+ valueWrap.style.height = `${geom.valueH}px`;
9588
+ valueWrap.style.overflow = "hidden";
9589
+ const num2 = document.createElement("div");
9590
+ num2.textContent = displayText;
9591
+ Object.assign(num2.style, {
9592
+ color: kpi.color ?? tokens.color.text,
9227
9593
  fontFamily: tokens.font.family,
9228
9594
  fontSize: `${valueSize}px`,
9229
9595
  fontWeight: String(tokens.font.weight.bold),
9230
9596
  letterSpacing: "-0.03em",
9231
9597
  lineHeight: "1.02",
9232
- maxWidth: "100%",
9233
9598
  overflow: "hidden",
9234
9599
  textOverflow: "ellipsis",
9235
9600
  whiteSpace: "nowrap"
9236
9601
  });
9237
- if (delta != null) {
9602
+ valueWrap.appendChild(num2);
9603
+ if (kpi.unit) {
9604
+ const unit = document.createElement("div");
9605
+ unit.textContent = kpi.unit;
9606
+ Object.assign(unit.style, {
9607
+ color: tokens.color.textMuted,
9608
+ fontFamily: tokens.font.family,
9609
+ fontSize: `${unitFontSize(valueSize)}px`,
9610
+ fontWeight: String(tokens.font.weight.medium),
9611
+ whiteSpace: "nowrap",
9612
+ flex: "0 0 auto"
9613
+ });
9614
+ valueWrap.appendChild(unit);
9615
+ }
9616
+ addAt(valueWrap, geom.left, geom.valueY, geom.contentWidth);
9617
+ if (hasDelta && geom.deltaY >= 0 && delta != null) {
9238
9618
  const positive = delta >= 0;
9239
- addText2(card, `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(delta, kpi.format)}`, {
9619
+ const el = document.createElement("div");
9620
+ el.textContent = `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(delta, kpi.format)}`;
9621
+ Object.assign(el.style, {
9240
9622
  color: positive ? tokens.color.positive : tokens.color.negative,
9241
9623
  fontFamily: tokens.font.family,
9242
- fontSize: `${tokens.font.size.large}px`,
9624
+ fontSize: `${geom.deltaSize}px`,
9243
9625
  fontWeight: String(tokens.font.weight.medium),
9244
- lineHeight: "1",
9626
+ lineHeight: "1.2",
9627
+ textAlign: align === "start" ? "left" : align === "end" ? "right" : "center",
9245
9628
  whiteSpace: "nowrap"
9246
9629
  });
9630
+ addAt(el, geom.left, geom.deltaY, geom.contentWidth);
9631
+ }
9632
+ if (geom.spark) {
9633
+ const node = sketch ? buildSparklineCanvas(tokens, geom.spark.width, geom.spark.height, displaySparkValues, markers, sketch) : buildSparkline(tokens, geom.spark.width, geom.spark.height, displaySparkValues, markers);
9634
+ if (node) addAt(node, geom.spark.x, geom.spark.y, geom.spark.width);
9635
+ }
9636
+ if (comparisons.length > 0 && geom.dividerY >= 0) {
9637
+ const divider = document.createElement("div");
9638
+ divider.style.height = "1px";
9639
+ divider.style.background = tokens.color.border;
9640
+ addAt(divider, geom.left, geom.dividerY, geom.contentWidth);
9641
+ comparisons.forEach((c, i) => {
9642
+ const row = document.createElement("div");
9643
+ row.style.display = "flex";
9644
+ row.style.alignItems = "baseline";
9645
+ row.style.gap = "0.5em";
9646
+ row.style.height = `${geom.compRowH}px`;
9647
+ row.style.overflow = "hidden";
9648
+ const label = document.createElement("span");
9649
+ label.textContent = c.label;
9650
+ Object.assign(label.style, {
9651
+ color: tokens.color.text,
9652
+ fontFamily: tokens.font.family,
9653
+ fontSize: `${geom.compSize}px`,
9654
+ fontWeight: String(tokens.font.weight.medium),
9655
+ whiteSpace: "nowrap",
9656
+ flex: "0 0 auto"
9657
+ });
9658
+ const val = document.createElement("span");
9659
+ val.textContent = c.text;
9660
+ Object.assign(val.style, {
9661
+ color: c.positive ? tokens.color.positive : tokens.color.negative,
9662
+ fontFamily: tokens.font.family,
9663
+ fontSize: `${geom.compSize}px`,
9664
+ fontWeight: String(tokens.font.weight.medium),
9665
+ whiteSpace: "nowrap"
9666
+ });
9667
+ row.appendChild(label);
9668
+ row.appendChild(val);
9669
+ addAt(row, geom.left, geom.compStartY + i * geom.compRowH, geom.contentWidth);
9670
+ });
9247
9671
  }
9248
9672
  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
9673
  }
9262
- function drawKpiCanvas(surface, kpi, tokens, size, context) {
9674
+ function drawKpiCanvas(surface, kpi, tokens, p) {
9263
9675
  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) {
9676
+ const { geom } = p;
9677
+ const alignX = () => p.align === "start" ? geom.left : p.align === "end" ? geom.left + geom.contentWidth : geom.left + geom.contentWidth / 2;
9678
+ const alignCanvas = textAlignOf(p.align);
9679
+ if (kpi.label && geom.labelY >= 0) {
9297
9680
  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),
9681
+ x: alignX(),
9682
+ y: geom.labelY,
9683
+ text: kpi.label,
9684
+ font: fontString(geom.labelSize, tokens.font.family, tokens.font.weight.medium),
9302
9685
  color: tokens.color.textMuted,
9303
- size: tokens.font.size.base,
9686
+ size: geom.labelSize,
9687
+ align: alignCanvas,
9304
9688
  baseline: "top"
9305
9689
  });
9306
- y += labelH + tokens.spacing.sm;
9307
9690
  }
9691
+ const unitSize = kpi.unit ? unitFontSize(p.valueSize) : 0;
9692
+ const numberFont = fontString(p.valueSize, tokens.font.family, tokens.font.weight.bold);
9693
+ const unitFont = kpi.unit ? fontString(unitSize, tokens.font.family, tokens.font.weight.medium) : "";
9694
+ ctx.save();
9695
+ ctx.font = numberFont;
9696
+ const numberW = ctx.measureText(p.displayText).width;
9697
+ let unitW = 0;
9698
+ if (kpi.unit) {
9699
+ ctx.font = unitFont;
9700
+ unitW = ctx.measureText(kpi.unit).width;
9701
+ }
9702
+ ctx.restore();
9703
+ const unitGap = kpi.unit ? p.valueSize * 0.12 : 0;
9704
+ const totalW = numberW + (kpi.unit ? unitGap + unitW : 0);
9705
+ const groupX = p.align === "start" ? geom.left : p.align === "end" ? geom.left + geom.contentWidth - totalW : geom.left + (geom.contentWidth - totalW) / 2;
9706
+ const baselineY = geom.valueY + p.valueSize * 0.82;
9308
9707
  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"
9708
+ x: groupX,
9709
+ y: baselineY,
9710
+ text: p.displayText,
9711
+ font: numberFont,
9712
+ color: kpi.color ?? tokens.color.text,
9713
+ size: p.valueSize,
9714
+ align: "left",
9715
+ baseline: "alphabetic"
9316
9716
  });
9317
- y += valueSize + tokens.spacing.sm;
9318
- if (delta != null) {
9319
- const positive = delta >= 0;
9717
+ if (kpi.unit) {
9320
9718
  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),
9719
+ x: groupX + numberW + unitGap,
9720
+ y: baselineY,
9721
+ text: kpi.unit,
9722
+ font: unitFont,
9723
+ color: tokens.color.textMuted,
9724
+ size: unitSize,
9725
+ align: "left",
9726
+ baseline: "alphabetic"
9727
+ });
9728
+ }
9729
+ if (p.delta != null && geom.deltaY >= 0) {
9730
+ const positive = p.delta >= 0;
9731
+ paintCanvasText(ctx, {
9732
+ x: alignX(),
9733
+ y: geom.deltaY,
9734
+ text: `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(p.delta, kpi.format)}`,
9735
+ font: fontString(geom.deltaSize, tokens.font.family, tokens.font.weight.medium),
9325
9736
  color: positive ? tokens.color.positive : tokens.color.negative,
9326
- size: tokens.font.size.large,
9737
+ size: geom.deltaSize,
9738
+ align: alignCanvas,
9327
9739
  baseline: "top"
9328
9740
  });
9329
9741
  }
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);
9742
+ if (geom.spark && p.sparkValues.length > 0) {
9743
+ paintSparklineCanvas(ctx, tokens, geom.spark, p.sparkValues, p.markers);
9337
9744
  }
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();
9745
+ if (p.comparisons.length > 0 && geom.dividerY >= 0) {
9746
+ ctx.save();
9747
+ ctx.fillStyle = tokens.color.border;
9748
+ ctx.fillRect(geom.left, geom.dividerY, geom.contentWidth, 1);
9749
+ ctx.restore();
9750
+ const compFont = fontString(geom.compSize, tokens.font.family, tokens.font.weight.medium);
9751
+ const gap = geom.compSize * 0.5;
9752
+ p.comparisons.forEach((c, i) => {
9753
+ const rowY = geom.compStartY + i * geom.compRowH + geom.compRowH / 2;
9754
+ ctx.save();
9755
+ ctx.font = compFont;
9756
+ const labelW = ctx.measureText(c.label).width;
9757
+ ctx.restore();
9758
+ paintCanvasText(ctx, {
9759
+ x: geom.left,
9760
+ y: rowY,
9761
+ text: c.label,
9762
+ font: compFont,
9763
+ color: tokens.color.text,
9764
+ size: geom.compSize,
9765
+ align: "left",
9766
+ baseline: "middle"
9767
+ });
9768
+ paintCanvasText(ctx, {
9769
+ x: geom.left + labelW + gap,
9770
+ y: rowY,
9771
+ text: c.text,
9772
+ font: compFont,
9773
+ color: c.positive ? tokens.color.positive : tokens.color.negative,
9774
+ size: geom.compSize,
9775
+ align: "left",
9776
+ baseline: "middle"
9777
+ });
9778
+ });
9369
9779
  }
9370
- ctx.restore();
9371
9780
  }
9372
9781
 
9373
9782
  // src/charts/sankey.ts
@@ -9834,18 +10243,24 @@ function drawLegend3(surface, tokens, legend, domain, scheme, format) {
9834
10243
  transform: "translateX(-100%)"
9835
10244
  });
9836
10245
  }
9837
- function drawChoropleth(surface, spec, tokens, size) {
10246
+ function drawChoropleth(surface, spec, tokens, size, context) {
9838
10247
  const choropleth = spec;
10248
+ const emphasis = context?.emphasis ?? null;
9839
10249
  const content = drawTitleBlock(surface, tokens, size, choropleth.title);
9840
10250
  const features = choropleth.geo?.features ?? [];
9841
10251
  if (features.length === 0 || content.width <= 0 || content.height <= 0) return;
9842
10252
  const readKey = accessor(choropleth.encoding.key.field);
9843
10253
  const readColor = accessor(choropleth.encoding.color.field);
9844
10254
  const valueByKey = /* @__PURE__ */ new Map();
10255
+ const rowByKey = /* @__PURE__ */ new Map();
9845
10256
  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);
10257
+ const datum = row;
10258
+ const k = toKey(readKey(datum));
10259
+ const v = toNumber(readColor(datum));
10260
+ if (k !== "" && Number.isFinite(v)) {
10261
+ valueByKey.set(k, v);
10262
+ rowByKey.set(k, datum);
10263
+ }
9849
10264
  }
9850
10265
  const projection = choropleth.projection ?? "mercator";
9851
10266
  const project = projector(
@@ -9863,6 +10278,7 @@ function drawChoropleth(surface, spec, tokens, size) {
9863
10278
  for (const feature of features) {
9864
10279
  const key = featureKey(feature, choropleth.featureId);
9865
10280
  const value = valueByKey.has(key) ? valueByKey.get(key) : null;
10281
+ const row = rowByKey.get(key);
9866
10282
  if (value != null) {
9867
10283
  if (value < vMin) vMin = value;
9868
10284
  if (value > vMax) vMax = value;
@@ -9885,7 +10301,7 @@ function drawChoropleth(surface, spec, tokens, size) {
9885
10301
  }
9886
10302
  if (rings.length) polys.push(rings);
9887
10303
  }
9888
- projected.push({ key, name: featureName(feature, key), value, polys });
10304
+ projected.push({ key, name: featureName(feature, key), value, row, polys });
9889
10305
  }
9890
10306
  const spanX = maxX - minX;
9891
10307
  const spanY = maxY - minY;
@@ -9932,6 +10348,7 @@ function drawChoropleth(surface, spec, tokens, size) {
9932
10348
  key: pf.key,
9933
10349
  name: pf.name,
9934
10350
  value: pf.value,
10351
+ row: pf.row,
9935
10352
  color: pf.value == null ? noDataFill : rgbaToCss(cscale.map(pf.value)),
9936
10353
  polys,
9937
10354
  cx: count ? cx / count : 0,
@@ -9946,6 +10363,7 @@ function drawChoropleth(surface, spec, tokens, size) {
9946
10363
  ctx.strokeStyle = tokens.color.background;
9947
10364
  ctx.lineWidth = 0.75;
9948
10365
  for (const f of screen) {
10366
+ ctx.globalAlpha = rowAlpha(emphasis, f.row ?? { [choropleth.encoding.key.field]: f.key });
9949
10367
  ctx.fillStyle = f.color;
9950
10368
  for (const polygon of f.polys) {
9951
10369
  ctx.beginPath();
@@ -9966,6 +10384,7 @@ function drawChoropleth(surface, spec, tokens, size) {
9966
10384
  }
9967
10385
  }
9968
10386
  }
10387
+ ctx.globalAlpha = 1;
9969
10388
  }
9970
10389
  ctx.restore();
9971
10390
  if (legendHeight > 0) {
@@ -10120,7 +10539,7 @@ function colorToTone(color) {
10120
10539
  var HEADER_ROW_HEIGHT = 32;
10121
10540
  var MIN_COL_WIDTH = 72;
10122
10541
  var DEFAULT_BODY_ROW_HEIGHT = 30;
10123
- var SKETCH_BORDER_RADIUS2 = "14px 6px 16px 7px / 7px 15px 6px 14px";
10542
+ var SKETCH_BORDER_RADIUS = "14px 6px 16px 7px / 7px 15px 6px 14px";
10124
10543
  function formatCellValue(value, column) {
10125
10544
  return formatValue(value, column.format);
10126
10545
  }
@@ -10160,7 +10579,7 @@ function buildTable(opts) {
10160
10579
  pointerEvents: "auto",
10161
10580
  boxSizing: "border-box",
10162
10581
  border: `1px solid ${opts.sketch ? "transparent" : tokens.color.border}`,
10163
- borderRadius: opts.sketch ? SKETCH_BORDER_RADIUS2 : `${tokens.radius.sm}px`,
10582
+ borderRadius: opts.sketch ? SKETCH_BORDER_RADIUS : `${tokens.radius.sm}px`,
10164
10583
  background: tokens.color.background
10165
10584
  });
10166
10585
  container.replaceChildren();
@@ -13921,11 +14340,6 @@ function drawTreemap(surface, spec, tokens, size, context) {
13921
14340
  function clamp8(value, min, max) {
13922
14341
  return Math.max(min, Math.min(max, value));
13923
14342
  }
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
14343
  function valueField2(ref) {
13930
14344
  return typeof ref === "number" ? null : ref.field;
13931
14345
  }
@@ -13949,7 +14363,7 @@ function arcPoints(cx, cy, radius, startAngle, endAngle) {
13949
14363
  }
13950
14364
  return points;
13951
14365
  }
13952
- function strokePoints(ctx, pen, points, color, width) {
14366
+ function strokePoints(ctx, pen, points, color, width, cap2 = "butt") {
13953
14367
  if (points.length < 2) return;
13954
14368
  if (pen) {
13955
14369
  pen.polyline(points, { stroke: color, strokeWidth: width });
@@ -13960,29 +14374,12 @@ function strokePoints(ctx, pen, points, color, width) {
13960
14374
  ctx.moveTo(points[0].x, points[0].y);
13961
14375
  for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
13962
14376
  ctx.lineWidth = width;
13963
- ctx.lineCap = "round";
14377
+ ctx.lineCap = cap2;
13964
14378
  ctx.lineJoin = "round";
13965
14379
  ctx.strokeStyle = color;
13966
14380
  ctx.stroke();
13967
14381
  ctx.restore();
13968
14382
  }
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
14383
  function drawCenteredMessage(surface, tokens, rect, text) {
13987
14384
  addOverlayText(surface, tokens, {
13988
14385
  left: rect.x,
@@ -13994,86 +14391,158 @@ function drawCenteredMessage(surface, tokens, rect, text) {
13994
14391
  align: "center"
13995
14392
  });
13996
14393
  }
14394
+ var LEGEND_SWATCH3 = 11;
14395
+ function measureLegendWidth(entries, font) {
14396
+ let maxLabel = 0;
14397
+ for (const e of entries) maxLabel = Math.max(maxLabel, measureText(e.label, font).width);
14398
+ return Math.ceil(LEGEND_SWATCH3 + 6 + maxLabel + 4);
14399
+ }
14400
+ function drawGaugeLegend(surface, tokens, area2, entries) {
14401
+ if (entries.length === 0) return;
14402
+ const labelSize = tokens.font.size.small;
14403
+ const rowH = Math.round(labelSize * 1.7);
14404
+ const font = fontString(labelSize, tokens.font.family, tokens.font.weight.normal);
14405
+ const colW = measureLegendWidth(entries, font);
14406
+ const x = area2.x + area2.width - colW;
14407
+ let y = area2.y;
14408
+ if (surface.headless) {
14409
+ const ctx = surface.marks.ctx;
14410
+ for (const e of entries) {
14411
+ const midY = y + rowH / 2;
14412
+ paintLegendSwatch(ctx, x, midY, "square", e.color, LEGEND_SWATCH3);
14413
+ paintCanvasText(ctx, {
14414
+ x: x + LEGEND_SWATCH3 + 6,
14415
+ y: midY,
14416
+ text: e.label,
14417
+ font,
14418
+ color: tokens.color.text,
14419
+ size: labelSize,
14420
+ baseline: "middle"
14421
+ });
14422
+ y += rowH;
14423
+ }
14424
+ return;
14425
+ }
14426
+ for (const e of entries) {
14427
+ const row = document.createElement("div");
14428
+ row.style.position = "absolute";
14429
+ row.style.left = `${x}px`;
14430
+ row.style.top = `${y}px`;
14431
+ row.style.display = "flex";
14432
+ row.style.alignItems = "center";
14433
+ row.style.gap = "6px";
14434
+ row.style.pointerEvents = "none";
14435
+ const swatch = document.createElement("span");
14436
+ swatch.style.flex = "0 0 auto";
14437
+ swatch.style.width = `${LEGEND_SWATCH3}px`;
14438
+ swatch.style.height = `${LEGEND_SWATCH3}px`;
14439
+ swatch.style.borderRadius = "2px";
14440
+ swatch.style.background = e.color;
14441
+ const label = document.createElement("span");
14442
+ label.textContent = e.label;
14443
+ label.style.font = font;
14444
+ label.style.color = tokens.color.text;
14445
+ label.style.whiteSpace = "nowrap";
14446
+ row.appendChild(swatch);
14447
+ row.appendChild(label);
14448
+ surface.overlay.appendChild(row);
14449
+ y += rowH;
14450
+ }
14451
+ }
14452
+ function bandFillColor(gauge, value, tokens, accent) {
14453
+ const bands = gauge.bands;
14454
+ if (!bands?.length) return accent;
14455
+ const palette = tokens.color.palette;
14456
+ for (let i = 0; i < bands.length; i++) {
14457
+ if (value <= bands[i].to) return bands[i].color ?? palette[i % palette.length] ?? accent;
14458
+ }
14459
+ const last = bands.length - 1;
14460
+ return bands[last].color ?? palette[last % palette.length] ?? accent;
14461
+ }
13997
14462
  function drawGauge(surface, spec, tokens, size, context) {
13998
14463
  const gauge = spec;
13999
14464
  const ctx = surface.marks.ctx;
14000
14465
  const content = drawTitleBlock(surface, tokens, size, gauge.title);
14001
14466
  const data = gauge.data ?? [];
14002
- const value = resolveValue2(gauge.value, data);
14003
- const target = resolveValue2(gauge.target, data);
14467
+ const value = resolveValueRef(gauge.value, data);
14468
+ const target = resolveValueRef(gauge.target, data);
14469
+ const compare2 = resolveValueRef(gauge.compare, data);
14004
14470
  const min = gauge.min ?? 0;
14005
14471
  const max = gauge.max;
14006
14472
  if (value == null || !Number.isFinite(min) || !Number.isFinite(max) || max <= min || content.width <= 0 || content.height <= 0) {
14007
14473
  drawCenteredMessage(surface, tokens, content, "No value");
14008
14474
  return;
14009
14475
  }
14476
+ const sketch = resolveSketch(spec);
14477
+ const pen = sketch ? new RoughPen(ctx, sketch) : null;
14478
+ const accent = tokens.color.palette[0] ?? tokens.color.accent;
14479
+ const fill = bandFillColor(gauge, value, tokens, accent);
14480
+ const track = tokens.dark ? withAlpha3(tokens.color.textMuted, 0.3) : tokens.color.border;
14481
+ const compareColor = tokens.color.textMuted;
14482
+ const markColor = tokens.dark ? withAlpha3("#ffffff", 0.92) : withAlpha3(tokens.color.text, 0.82);
14483
+ const targetMark = target != null && target >= min && target <= max ? target : null;
14010
14484
  const valueText = formatValue(value, gauge.format);
14011
14485
  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
14486
  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;
14487
+ const captionReserve = captionSize + 16;
14488
+ const showLegend = gauge.legend ?? compare2 != null;
14489
+ const legendEntries = [];
14490
+ if (showLegend) {
14491
+ legendEntries.push({ label: gauge.valueLabel ?? "Actual", color: fill, symbol: "square" });
14492
+ if (compare2 != null) {
14493
+ legendEntries.push({ label: gauge.compareLabel ?? "Prior period", color: compareColor, symbol: "square" });
14494
+ }
14495
+ legendEntries.push({ label: gauge.trackLabel ?? "Target", color: targetMark != null ? markColor : track, symbol: "square" });
14496
+ }
14497
+ const legendFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
14498
+ const legendW = legendEntries.length ? measureLegendWidth(legendEntries, legendFont) + 12 : 0;
14499
+ const dialW = Math.max(40, content.width - legendW);
14500
+ const radius = clamp8(
14501
+ Math.min((dialW - 16) / 2, content.height - captionReserve - 16),
14502
+ 18,
14503
+ Math.min(dialW, content.height * 2)
14504
+ );
14505
+ const trackWidth = clamp8(radius * 0.17, 14, 54);
14506
+ const cx = content.x + dialW / 2;
14507
+ const blockH = radius + trackWidth / 2 + captionReserve;
14508
+ const topMargin = Math.max(0, (content.height - blockH) / 2);
14509
+ const cy = content.y + topMargin + radius + trackWidth / 2;
14022
14510
  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;
14511
+ const sweepT = context?.sweepProgress ?? 1;
14512
+ const from = context?.valueFrom ?? min;
14513
+ const displayValue = from + (value - from) * sweepT;
14514
+ const shownText = formatValue(displayValue, gauge.format);
14515
+ const valueAngle = toAngle(displayValue);
14026
14516
  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);
14517
+ if (valueAngle < Math.PI) {
14518
+ strokePoints(ctx, pen, arcPoints(cx, cy, radius, Math.PI, valueAngle), fill, trackWidth);
14519
+ }
14520
+ if (compare2 != null) {
14521
+ const innerWidth = clamp8(trackWidth * 0.42, 6, 22);
14522
+ const innerRadius = Math.max(radius * 0.35, radius - trackWidth / 2 - 2 - innerWidth / 2);
14523
+ const compareFrom = context?.compareFrom ?? min;
14524
+ const displayCompare = compareFrom + (compare2 - compareFrom) * sweepT;
14525
+ const compareAngle = toAngle(displayCompare);
14526
+ if (compareAngle < Math.PI) {
14527
+ strokePoints(ctx, pen, arcPoints(cx, cy, innerRadius, Math.PI, compareAngle), compareColor, innerWidth);
14528
+ }
14529
+ }
14530
+ if (targetMark != null) {
14531
+ const a = toAngle(targetMark);
14532
+ const overhang = clamp8(trackWidth * 0.3, 4, 10);
14533
+ const outer = pointOnArc(cx, cy, radius + trackWidth / 2 + overhang, a);
14534
+ const inner = pointOnArc(cx, cy, radius - trackWidth / 2 - overhang, a);
14535
+ strokePoints(ctx, pen, [outer, inner], markColor, 3, "round");
14536
+ }
14537
+ if (legendEntries.length) {
14538
+ drawGaugeLegend(surface, tokens, { x: content.x, y: content.y, width: content.width, height: content.height }, legendEntries);
14539
+ }
14540
+ const valueSize = clamp8(radius * 0.5, 22, 72);
14072
14541
  addOverlayText(surface, tokens, {
14073
14542
  left: content.x,
14074
- top: readoutTop,
14075
- width: content.width,
14076
- text: valueText,
14543
+ top: cy - radius * 0.32 - valueSize / 2,
14544
+ width: dialW,
14545
+ text: shownText,
14077
14546
  color: tokens.color.text,
14078
14547
  size: valueSize,
14079
14548
  weight: tokens.font.weight.bold,
@@ -14081,20 +14550,20 @@ function drawGauge(surface, spec, tokens, size, context) {
14081
14550
  });
14082
14551
  addOverlayText(surface, tokens, {
14083
14552
  left: content.x,
14084
- top: readoutTop + valueSize + 4,
14085
- width: content.width,
14553
+ top: cy + 8,
14554
+ width: dialW,
14086
14555
  text: caption,
14087
14556
  color: tokens.color.textMuted,
14088
14557
  size: captionSize,
14089
14558
  align: "center"
14090
14559
  });
14091
14560
  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;
14561
+ const withinDial = px >= cx - radius - trackWidth && px <= cx + radius + trackWidth && py >= cy - radius - trackWidth && py <= cy + captionReserve + 4;
14096
14562
  if (!withinDial) return null;
14097
- const rows = [{ label: "value", value: valueText }];
14563
+ const rows = [{ label: gauge.valueLabel ?? "value", value: valueText }];
14564
+ if (compare2 != null) {
14565
+ rows.push({ label: gauge.compareLabel ?? "prior period", value: formatValue(compare2, gauge.format), muted: true });
14566
+ }
14098
14567
  if (target != null) rows.push({ label: "target", value: formatValue(target, gauge.format), muted: true });
14099
14568
  return {
14100
14569
  key: "value",
@@ -14122,7 +14591,7 @@ function titleText2(title2) {
14122
14591
  function valueField3(ref) {
14123
14592
  return typeof ref === "number" ? null : ref.field;
14124
14593
  }
14125
- function resolveValue3(ref, data) {
14594
+ function resolveValue(ref, data) {
14126
14595
  if (ref == null) return null;
14127
14596
  if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
14128
14597
  return aggregateValues((data ?? []).map(accessor(ref.field)), ref.aggregate ?? "sum");
@@ -14154,8 +14623,8 @@ function drawBullet(surface, spec, tokens, size, context) {
14154
14623
  const ctx = surface.marks.ctx;
14155
14624
  const content = drawTitleBlock(surface, tokens, size, bullet.title);
14156
14625
  const data = bullet.data ?? [];
14157
- const value = resolveValue3(bullet.value, data);
14158
- const target = resolveValue3(bullet.target, data);
14626
+ const value = resolveValue(bullet.value, data);
14627
+ const target = resolveValue(bullet.target, data);
14159
14628
  const caption = bullet.label ?? titleText2(bullet.title) ?? valueField3(bullet.value) ?? "Value";
14160
14629
  const fmt = bullet.format;
14161
14630
  const finiteRanges = (bullet.ranges ?? []).filter((r) => Number.isFinite(r)).sort((a, b) => a - b);
@@ -14338,7 +14807,7 @@ function noData(surface, tokens, content) {
14338
14807
  }
14339
14808
  function normalizeDays(days) {
14340
14809
  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 }));
14810
+ 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
14811
  }
14343
14812
  const out = [];
14344
14813
  const dayMap = days;
@@ -14360,19 +14829,20 @@ function buildCalendarGrid(days) {
14360
14829
  const start = addDays(minDay, -minDay.getDay());
14361
14830
  const end = addDays(maxDay, 6 - maxDay.getDay());
14362
14831
  const weeks = Math.floor(diffDays(end, start) / 7) + 1;
14363
- const byValue = new Map(values.map((d) => [d.isoDate, d.value]));
14832
+ const byValue = new Map(values.map((d) => [d.isoDate, { value: d.value, row: d.row }]));
14364
14833
  const cells = [];
14365
14834
  const byIsoDate = /* @__PURE__ */ new Map();
14366
14835
  for (let d = start; diffDays(end, d) >= 0; d = addDays(d, 1)) {
14367
14836
  const isoDate = isoDay(d);
14368
- const value = byValue.get(isoDate);
14837
+ const dayValue = byValue.get(isoDate);
14369
14838
  const cell = {
14370
14839
  isoDate,
14371
14840
  date: d,
14372
14841
  week: Math.floor(diffDays(d, start) / 7),
14373
14842
  weekday: d.getDay(),
14374
- value,
14375
- present: value !== void 0
14843
+ value: dayValue?.value,
14844
+ row: dayValue?.row,
14845
+ present: dayValue !== void 0
14376
14846
  };
14377
14847
  cells.push(cell);
14378
14848
  byIsoDate.set(isoDate, cell);
@@ -14391,9 +14861,9 @@ function collectDays(spec) {
14391
14861
  const isoDate = isoDay(day);
14392
14862
  const current = totals.get(isoDate);
14393
14863
  if (current) current.value += value;
14394
- else totals.set(isoDate, { date: day, value });
14864
+ else totals.set(isoDate, { date: day, value, row });
14395
14865
  }
14396
- return [...totals.entries()].map(([isoDate, { date, value }]) => ({ isoDate, date, value }));
14866
+ return [...totals.entries()].map(([isoDate, { date, value, row }]) => ({ isoDate, date, value, row }));
14397
14867
  }
14398
14868
  function monthAnchors(grid) {
14399
14869
  const seen = /* @__PURE__ */ new Set();
@@ -14417,6 +14887,8 @@ function humanDate(date) {
14417
14887
  function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14418
14888
  const cal = spec;
14419
14889
  const ctx = surface.marks.ctx;
14890
+ const emphasis = context?.emphasis ?? null;
14891
+ const dateField = cal.encoding.date.field;
14420
14892
  const content = drawTitleBlock(surface, tokens, size, cal.title);
14421
14893
  const days = collectDays(cal);
14422
14894
  const grid = buildCalendarGrid(days);
@@ -14456,6 +14928,7 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14456
14928
  const y = gridRect.y + c.weekday * (cell + GAP2);
14457
14929
  const fill = c.present && c.value !== void 0 ? rgbaToCss(cscale.map(c.value)) : tokens.color.surface;
14458
14930
  laidOut.set(c.isoDate, { ...c, x, y, size: cell, fill });
14931
+ ctx.globalAlpha = c.present ? rowAlpha(emphasis, c.row ?? { [dateField]: c.isoDate }) : 1;
14459
14932
  if (pen) {
14460
14933
  pen.rect(x, y, cell, cell, {
14461
14934
  fill,
@@ -14469,6 +14942,7 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
14469
14942
  ctx.lineWidth = 1;
14470
14943
  ctx.strokeRect(x + 0.5, y + 0.5, Math.max(0, cell - 1), Math.max(0, cell - 1));
14471
14944
  }
14945
+ ctx.globalAlpha = 1;
14472
14946
  }
14473
14947
  ctx.restore();
14474
14948
  for (const m of monthAnchors(grid)) {
@@ -14746,13 +15220,15 @@ function buildWaterfallModel(spec, tokens, opts) {
14746
15220
  }
14747
15221
 
14748
15222
  // src/charts/waterfall.ts
14749
- function drawConnectors(surface, model) {
15223
+ function drawConnectors(surface, model, alphaOf) {
14750
15224
  if (model.connectors.length === 0) return;
14751
15225
  const ctx = surface.marks.ctx;
14752
15226
  const color = model.tokens.color.axis;
14753
15227
  if (model.sketch) {
14754
15228
  const pen = new RoughPen(ctx, model.sketch);
14755
- for (const c of model.connectors) {
15229
+ for (let i = 0; i < model.connectors.length; i += 1) {
15230
+ const c = model.connectors[i];
15231
+ ctx.globalAlpha = alphaOf(model.bars[i]);
14756
15232
  pen.polyline(
14757
15233
  [
14758
15234
  { x: c.x1, y: c.y },
@@ -14760,6 +15236,7 @@ function drawConnectors(surface, model) {
14760
15236
  ],
14761
15237
  { stroke: color }
14762
15238
  );
15239
+ ctx.globalAlpha = 1;
14763
15240
  }
14764
15241
  return;
14765
15242
  }
@@ -14767,23 +15244,28 @@ function drawConnectors(surface, model) {
14767
15244
  ctx.strokeStyle = color;
14768
15245
  ctx.lineWidth = 1;
14769
15246
  ctx.setLineDash([2, 2]);
14770
- ctx.beginPath();
14771
- for (const c of model.connectors) {
15247
+ for (let i = 0; i < model.connectors.length; i += 1) {
15248
+ const c = model.connectors[i];
14772
15249
  const y = Math.round(c.y) + 0.5;
15250
+ ctx.globalAlpha = alphaOf(model.bars[i]);
15251
+ ctx.beginPath();
14773
15252
  ctx.moveTo(c.x1, y);
14774
15253
  ctx.lineTo(c.x2, y);
15254
+ ctx.stroke();
15255
+ ctx.globalAlpha = 1;
14775
15256
  }
14776
- ctx.stroke();
14777
15257
  ctx.restore();
14778
15258
  }
14779
- function drawBars2(surface, model) {
15259
+ function drawBars2(surface, model, alphaOf) {
14780
15260
  const ctx = surface.marks.ctx;
14781
15261
  const pen = model.sketch ? new RoughPen(ctx, model.sketch) : null;
14782
15262
  for (const bar of model.bars) {
14783
15263
  const { rect } = bar;
14784
15264
  if (rect.width <= 0 || rect.height <= 0) continue;
15265
+ ctx.globalAlpha = alphaOf(bar);
14785
15266
  if (pen) {
14786
15267
  pen.rect(rect.x, rect.y, rect.width, rect.height, { stroke: bar.color, fill: bar.color });
15268
+ ctx.globalAlpha = 1;
14787
15269
  continue;
14788
15270
  }
14789
15271
  const r = Math.min(model.cornerRadius, rect.width / 2, rect.height / 2);
@@ -14791,6 +15273,7 @@ function drawBars2(surface, model) {
14791
15273
  roundedRect(ctx, rect.x, rect.y, rect.width, rect.height, [r, r, r, r]);
14792
15274
  ctx.fillStyle = bar.color;
14793
15275
  ctx.fill();
15276
+ ctx.globalAlpha = 1;
14794
15277
  }
14795
15278
  }
14796
15279
  function barLabelText(bar, format) {
@@ -14820,17 +15303,19 @@ function drawLabels(surface, model) {
14820
15303
  });
14821
15304
  }
14822
15305
  }
14823
- function drawWaterfall(surface, spec, tokens, size, _context) {
15306
+ function drawWaterfall(surface, spec, tokens, size, context) {
14824
15307
  const waterfall = spec;
14825
15308
  const model = buildWaterfallModel(waterfall, tokens, size);
15309
+ const emphasis = context?.emphasis ?? null;
15310
+ const stageField = waterfall.encoding.stage.field;
15311
+ const alphaOf = (bar) => bar.stageValue === void 0 ? 1 : rowAlpha(emphasis, { [stageField]: bar.stageValue });
14826
15312
  drawAxesUnderlay(surface, model.base);
14827
- drawConnectors(surface, model);
14828
- drawBars2(surface, model);
15313
+ drawConnectors(surface, model, alphaOf);
15314
+ drawBars2(surface, model, alphaOf);
14829
15315
  drawLabels(surface, model);
14830
15316
  drawOverlay(surface, model.base);
14831
15317
  if (model.bars.length === 0) return;
14832
15318
  const { plot, format } = model;
14833
- const stageField = waterfall.encoding.stage.field;
14834
15319
  const valueLabel2 = waterfall.encoding.value.title ?? waterfall.encoding.value.field;
14835
15320
  const fmtNum = (v) => format ? formatValue(v, format) : formatNumber2(v, ",");
14836
15321
  const hitBar = (px, py) => {
@@ -15013,7 +15498,7 @@ var LABEL_WIDTH = 68;
15013
15498
  function fmtValue(v, format) {
15014
15499
  return format ? formatValue(v, format) : formatNumber2(v, ",");
15015
15500
  }
15016
- function drawLines(surface, model) {
15501
+ function drawLines(surface, model, alphaOf) {
15017
15502
  const ctx = surface.marks.ctx;
15018
15503
  const pen = model.sketch ? new RoughPen(ctx, model.sketch) : null;
15019
15504
  ctx.save();
@@ -15023,6 +15508,7 @@ function drawLines(surface, model) {
15023
15508
  for (const series of model.series) {
15024
15509
  const points = series.points;
15025
15510
  if (points.length === 0) continue;
15511
+ ctx.globalAlpha = alphaOf(series);
15026
15512
  if (pen && points.length >= 2) {
15027
15513
  pen.polyline(points, { stroke: series.color });
15028
15514
  } else if (points.length >= 2) {
@@ -15048,16 +15534,18 @@ function drawLines(surface, model) {
15048
15534
  ctx.stroke();
15049
15535
  }
15050
15536
  }
15537
+ ctx.globalAlpha = 1;
15051
15538
  }
15052
15539
  ctx.restore();
15053
15540
  }
15054
- function drawEndLabels(surface, model) {
15541
+ function drawEndLabels(surface, model, alphaOf) {
15055
15542
  if (!model.showLabels) return;
15056
15543
  const { tokens } = model;
15057
15544
  const labelSize = tokens.font.size.tiny;
15058
15545
  const valueSize = tokens.font.size.tiny;
15059
15546
  for (const series of model.series) {
15060
15547
  if (series.points.length === 0) continue;
15548
+ const opacity = alphaOf(series) === 1 ? 1 : 0.5;
15061
15549
  const first = series.points[0];
15062
15550
  const last = series.points[series.points.length - 1];
15063
15551
  addOverlayText(surface, tokens, {
@@ -15068,7 +15556,8 @@ function drawEndLabels(surface, model) {
15068
15556
  color: series.color,
15069
15557
  size: labelSize,
15070
15558
  weight: tokens.font.weight.bold,
15071
- align: "right"
15559
+ align: "right",
15560
+ opacity
15072
15561
  });
15073
15562
  addOverlayText(surface, tokens, {
15074
15563
  left: first.x - 8 - LABEL_WIDTH,
@@ -15077,7 +15566,8 @@ function drawEndLabels(surface, model) {
15077
15566
  text: fmtValue(first.value, model.format),
15078
15567
  color: tokens.color.textMuted,
15079
15568
  size: valueSize,
15080
- align: "right"
15569
+ align: "right",
15570
+ opacity
15081
15571
  });
15082
15572
  if (last !== first) {
15083
15573
  addOverlayText(surface, tokens, {
@@ -15088,7 +15578,8 @@ function drawEndLabels(surface, model) {
15088
15578
  color: series.color,
15089
15579
  size: valueSize,
15090
15580
  weight: tokens.font.weight.medium,
15091
- align: "left"
15581
+ align: "left",
15582
+ opacity
15092
15583
  });
15093
15584
  }
15094
15585
  }
@@ -15106,16 +15597,18 @@ function nearestPoint(model, px, py) {
15106
15597
  }
15107
15598
  return best ? { series: best.series, point: best.point } : null;
15108
15599
  }
15109
- function drawSlope(surface, spec, tokens, size, _context) {
15600
+ function drawSlope(surface, spec, tokens, size, context) {
15110
15601
  const slope = spec;
15111
15602
  const model = buildSlopeModel(slope, tokens, size);
15603
+ const emphasis = context?.emphasis ?? null;
15604
+ const seriesField = slope.encoding.series.field;
15605
+ const alphaOf = (series) => emphasis ? emphasis.match({ [seriesField]: series.value }) ? 1 : emphasis.dim : 1;
15112
15606
  drawAxesUnderlay(surface, model.base);
15113
- drawLines(surface, model);
15607
+ drawLines(surface, model, alphaOf);
15114
15608
  drawOverlay(surface, model.base);
15115
- drawEndLabels(surface, model);
15609
+ drawEndLabels(surface, model, alphaOf);
15116
15610
  if (model.series.length === 0) return;
15117
15611
  const { plot } = model;
15118
- const seriesField = slope.encoding.series.field;
15119
15612
  return {
15120
15613
  region: { x: plot.x, y: plot.y, width: plot.width, height: plot.height },
15121
15614
  hitTest: (px, py) => {
@@ -15308,10 +15801,12 @@ function layoutPlot(content, labelWidth, tokens) {
15308
15801
  }
15309
15802
  };
15310
15803
  }
15311
- function drawDumbbell(surface, spec, tokens, size, _context) {
15804
+ function drawDumbbell(surface, spec, tokens, size, context) {
15312
15805
  const dumbbell = spec;
15313
15806
  const model = buildDumbbellModel(dumbbell, tokens);
15314
15807
  const ctx = surface.marks.ctx;
15808
+ const emphasis = context?.emphasis ?? null;
15809
+ const categoryField = dumbbell.encoding.category.field;
15315
15810
  let content = drawTitleBlock(surface, tokens, size, dumbbell.title);
15316
15811
  const legendEntries = model.groups.map((group) => ({ label: group.label, color: group.color, symbol: "circle" }));
15317
15812
  content = drawCategoricalLegend(surface, tokens, content, legendEntries, "top");
@@ -15372,8 +15867,10 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
15372
15867
  }).sort((a, b) => a.x - b.x);
15373
15868
  if (pixelDots.length === 0) continue;
15374
15869
  pixelRows.push({ ...row, y: cy, pixelDots });
15870
+ ctx.globalAlpha = rowAlpha(emphasis, { [categoryField]: row.catRawValue });
15375
15871
  drawConnector(ctx, pen, pixelDots[0].x, pixelDots[pixelDots.length - 1].x, cy, tokens);
15376
15872
  for (const dot of pixelDots) drawDot(ctx, pen, dot.x, cy, dot.color, tokens);
15873
+ ctx.globalAlpha = 1;
15377
15874
  addOverlayText(surface, tokens, {
15378
15875
  left: content.x,
15379
15876
  top: cy,
@@ -15401,7 +15898,6 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
15401
15898
  }
15402
15899
  }
15403
15900
  }
15404
- const categoryField = dumbbell.encoding.category.field;
15405
15901
  const nearestDot = (px, py) => {
15406
15902
  let best = null;
15407
15903
  for (const row of pixelRows) {
@@ -16737,6 +17233,8 @@ var CANVAS_MARK_TYPES = /* @__PURE__ */ new Set([
16737
17233
  "sankey",
16738
17234
  "choropleth"
16739
17235
  ]);
17236
+ var COUNT_UP_TYPES = /* @__PURE__ */ new Set(["kpi"]);
17237
+ var SWEEP_TYPES = /* @__PURE__ */ new Set(["pie", "gauge"]);
16740
17238
  var EMPTY_DATA = [];
16741
17239
  function legendConfigFor(model) {
16742
17240
  const legend = model.spec.legend;
@@ -16832,6 +17330,9 @@ function render(target, spec, options) {
16832
17330
  let interaction;
16833
17331
  let entrance;
16834
17332
  let updateTransition;
17333
+ let lastValue = null;
17334
+ let lastCompare = null;
17335
+ let lastSparkline = null;
16835
17336
  let destroyed = false;
16836
17337
  let lastDrawnW = -1;
16837
17338
  let lastDrawnH = -1;
@@ -16859,7 +17360,7 @@ function render(target, spec, options) {
16859
17360
  }
16860
17361
  }
16861
17362
  const hostListeners = /* @__PURE__ */ new Set();
16862
- const draw = (animateEntrance) => {
17363
+ const draw = (animateEntrance, valueTweenFrom, compareTweenFrom, sparklineTweenFrom) => {
16863
17364
  entrance?.cancel();
16864
17365
  entrance = void 0;
16865
17366
  updateTransition?.cancel();
@@ -16889,6 +17390,11 @@ function render(target, spec, options) {
16889
17390
  let facetRpt;
16890
17391
  let legendSelect = null;
16891
17392
  const type = currentSpec.type;
17393
+ if (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type)) {
17394
+ lastValue = resolveValueRef(effectiveSpec.value, effectiveSpec.data ?? EMPTY_DATA);
17395
+ lastCompare = SWEEP_TYPES.has(type) ? resolveValueRef(effectiveSpec.compare, effectiveSpec.data ?? EMPTY_DATA) : null;
17396
+ lastSparkline = type === "kpi" ? kpiSparklineSeries(effectiveSpec, effectiveSpec.data ?? EMPTY_DATA) : null;
17397
+ }
16892
17398
  const facetLayout = isFaceted(effectiveSpec) ? buildFacetModels(effectiveSpec, tokens, size) : null;
16893
17399
  if (facetLayout) {
16894
17400
  drawFacet(surface, effectiveSpec, facetLayout, tokens);
@@ -16919,7 +17425,8 @@ function render(target, spec, options) {
16919
17425
  tokens,
16920
17426
  currentSpec.background,
16921
17427
  anim,
16922
- finishEntrance
17428
+ finishEntrance,
17429
+ type === "bar" ? "grow" : "wipe"
16923
17430
  );
16924
17431
  }
16925
17432
  } else {
@@ -16937,11 +17444,44 @@ function render(target, spec, options) {
16937
17444
  }
16938
17445
  };
16939
17446
  interactionModel = renderer(surface, effectiveSpec, tokens, size, ctx) ?? null;
17447
+ if (anim.enabled) {
17448
+ if (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type)) {
17449
+ const key = COUNT_UP_TYPES.has(type) ? "valueProgress" : "sweepProgress";
17450
+ entrance = runCustomProgress(anim, finishEntrance, (t) => {
17451
+ surface.overlay.replaceChildren();
17452
+ surface.marks.clear();
17453
+ paintBackground(surface, tokens, currentSpec.background);
17454
+ renderer(surface, effectiveSpec, tokens, size, { ...ctx, [key]: t });
17455
+ });
17456
+ } else {
17457
+ entrance = runFadeEntrance(surface, anim, finishEntrance);
17458
+ }
17459
+ } else if ((valueTweenFrom !== void 0 || sparklineTweenFrom !== void 0) && (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type))) {
17460
+ const tween = resolveUpdate(currentSpec.animation, {
17461
+ reducedMotion: prefersReducedMotion(),
17462
+ disabled: globalThis.__GRAPHEIN_DISABLE_ANIM === true
17463
+ });
17464
+ if (tween.enabled) {
17465
+ const key = COUNT_UP_TYPES.has(type) ? "valueProgress" : "sweepProgress";
17466
+ entrance = runCustomProgress(tween, finishEntrance, (t) => {
17467
+ surface.overlay.replaceChildren();
17468
+ surface.marks.clear();
17469
+ paintBackground(surface, tokens, currentSpec.background);
17470
+ renderer(surface, effectiveSpec, tokens, size, {
17471
+ ...ctx,
17472
+ valueFrom: valueTweenFrom,
17473
+ compareFrom: compareTweenFrom,
17474
+ sparklineFrom: sparklineTweenFrom,
17475
+ [key]: t
17476
+ });
17477
+ });
17478
+ }
17479
+ }
16940
17480
  } else {
16941
17481
  drawPlaceholder(surface, tokens, `\u201C${type}\u201D renderer not yet registered`);
16942
- }
16943
- if (anim.enabled) {
16944
- entrance = runFadeEntrance(surface, anim, finishEntrance);
17482
+ if (anim.enabled) {
17483
+ entrance = runFadeEntrance(surface, anim, finishEntrance);
17484
+ }
16945
17485
  }
16946
17486
  }
16947
17487
  if (!interaction) interaction = new InteractionController(surface, tokens);
@@ -17065,8 +17605,18 @@ function render(target, spec, options) {
17065
17605
  });
17066
17606
  const wantFade = anim.enabled && CANVAS_MARK_TYPES.has(prevType) && CANVAS_MARK_TYPES.has(next.type);
17067
17607
  const before = wantFade ? snapshotLayer(surface.marks) : void 0;
17608
+ const prevValue = lastValue;
17609
+ const prevCompare = lastCompare;
17610
+ const prevSparkline = lastSparkline;
17611
+ const wantValueTween = prevType === next.type && (COUNT_UP_TYPES.has(next.type) || SWEEP_TYPES.has(next.type)) && prevValue != null;
17612
+ const wantSparklineTween = prevType === next.type && next.type === "kpi" && prevSparkline != null && prevSparkline.length > 0;
17068
17613
  currentSpec = next;
17069
- draw(false);
17614
+ draw(
17615
+ false,
17616
+ wantValueTween ? prevValue ?? void 0 : void 0,
17617
+ wantValueTween ? prevCompare ?? void 0 : void 0,
17618
+ wantValueTween || wantSparklineTween ? prevSparkline ?? void 0 : void 0
17619
+ );
17070
17620
  if (before && surface.width === prevW && surface.height === prevH && surface.marks.canvas.width === before.width && surface.marks.canvas.height === before.height) {
17071
17621
  updateTransition = runUpdateCrossfade(surface, before, anim);
17072
17622
  }
@@ -17086,7 +17636,7 @@ function render(target, spec, options) {
17086
17636
  }
17087
17637
  };
17088
17638
  }
17089
- function runCartesianEntrance(surface, model, renderer, tokens, background, anim, onDone) {
17639
+ function runCartesianEntrance(surface, model, renderer, tokens, background, anim, onDone, mode = "wipe") {
17090
17640
  const ctx = surface.marks.ctx;
17091
17641
  const { plot } = model;
17092
17642
  const bg = background ?? tokens.color.background;
@@ -17097,6 +17647,14 @@ function runCartesianEntrance(surface, model, renderer, tokens, background, anim
17097
17647
  ctx.fillRect(0, 0, surface.width, surface.height);
17098
17648
  ctx.restore();
17099
17649
  drawAxesUnderlay(surface, model);
17650
+ if (mode === "grow") {
17651
+ model.entranceProgress = t;
17652
+ renderer(surface, model);
17653
+ model.entranceProgress = void 0;
17654
+ drawTrendlines(surface, model);
17655
+ drawAnnotations(surface, model);
17656
+ return;
17657
+ }
17100
17658
  if (t <= 0) return;
17101
17659
  ctx.save();
17102
17660
  ctx.globalAlpha = Math.min(1, 0.2 + 0.8 * t);
@@ -17119,6 +17677,17 @@ function runCartesianEntrance(surface, model, renderer, tokens, background, anim
17119
17677
  }
17120
17678
  });
17121
17679
  }
17680
+ function runCustomProgress(anim, onDone, paint) {
17681
+ return animate({
17682
+ duration: anim.duration,
17683
+ easing: anim.easing,
17684
+ onUpdate: paint,
17685
+ onComplete: () => {
17686
+ paint(1);
17687
+ onDone();
17688
+ }
17689
+ });
17690
+ }
17122
17691
  function runFadeEntrance(surface, anim, onDone) {
17123
17692
  const root = surface.root;
17124
17693
  const apply = (t) => {
@@ -17270,12 +17839,17 @@ function autoParamName(view) {
17270
17839
  const existing = view.spec.params?.[0]?.name;
17271
17840
  return existing ?? `__sel__${view.id}`;
17272
17841
  }
17842
+ function roleOf(view) {
17843
+ return view.interact ?? "auto";
17844
+ }
17273
17845
  function wireViews(views, interactions) {
17274
17846
  const out = views.map((v) => ({ ...v, spec: v.spec }));
17275
17847
  if (interactions === "none") return out;
17276
17848
  const sources = [];
17277
17849
  out.forEach((v, i) => {
17278
17850
  const type = v.spec.type;
17851
+ const role = roleOf(v);
17852
+ if (role === "none" || role === "target") return;
17279
17853
  if (isSlicerType(type)) {
17280
17854
  const s = v.spec;
17281
17855
  sources.push({
@@ -17306,11 +17880,15 @@ function wireViews(views, interactions) {
17306
17880
  name: src.param,
17307
17881
  select: { type: "point", on: "click", fields: src.fields }
17308
17882
  });
17309
- out[src.view].spec = addHighlight(out[src.view].spec, src.param);
17883
+ if (roleOf(out[src.view]) !== "source") {
17884
+ out[src.view].spec = addHighlight(out[src.view].spec, src.param);
17885
+ }
17310
17886
  }
17311
17887
  out.forEach((v, j) => {
17312
17888
  if (isSlicerType(v.spec.type)) return;
17313
17889
  if (j === src.view) return;
17890
+ const trole = roleOf(v);
17891
+ if (trole === "none" || trole === "source") return;
17314
17892
  out[j].spec = addFilterParam(out[j].spec, src.param);
17315
17893
  });
17316
17894
  }
@@ -17360,6 +17938,41 @@ var DEFAULT_BREAKPOINTS = [
17360
17938
  { maxWidth: 960, cols: 6 }
17361
17939
  ];
17362
17940
  var STRIP_SLICERS = /* @__PURE__ */ new Set(["dropdown", "search", "dateRange", "range"]);
17941
+ var DASHBOARD_CLASS = "graphein-dashboard";
17942
+ var DASHBOARD_CELL_CLASS = "graphein-dashboard-cell";
17943
+ var DASHBOARD_SOURCE_CLASS = "is-source";
17944
+ function dashboardStyles(tokens) {
17945
+ const accent = tokens.color.accent;
17946
+ const surface = tokens.color.surface;
17947
+ return `
17948
+ .${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}[data-framed="true"] {
17949
+ transition: border-color 120ms ease, background-color 120ms ease;
17950
+ }
17951
+ .${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}[data-framed="true"]:hover {
17952
+ border-color: ${accent};
17953
+ background-color: ${surface};
17954
+ }
17955
+ .${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}:focus-visible {
17956
+ outline: 2px solid ${accent};
17957
+ outline-offset: 2px;
17958
+ }
17959
+ .${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}.${DASHBOARD_SOURCE_CLASS} {
17960
+ border-color: ${accent};
17961
+ box-shadow: inset 0 0 0 1px ${accent};
17962
+ }
17963
+ `;
17964
+ }
17965
+ function selectionIsActive(value) {
17966
+ if (value == null) return false;
17967
+ if (Array.isArray(value)) return value.length > 0;
17968
+ if (typeof value !== "object") return true;
17969
+ const v = value;
17970
+ if (v.kind === "point") return Array.isArray(v.tuples) && v.tuples.length > 0;
17971
+ if (v.kind === "set") return Array.isArray(v.values) && v.values.length > 0;
17972
+ if (v.kind === "range") return v.min != null || v.max != null;
17973
+ if (v.kind === "text") return typeof v.query === "string" && v.query.length > 0;
17974
+ return true;
17975
+ }
17363
17976
  function stripCellHeight(view, rowHeight) {
17364
17977
  const titled = !!(view.title || view.subtitle);
17365
17978
  const titlePad = titled ? 30 : 0;
@@ -17502,6 +18115,12 @@ function renderDashboard(target, spec, options) {
17502
18115
  let root;
17503
18116
  let resizeObs;
17504
18117
  let destroyed = false;
18118
+ let sourceCells = /* @__PURE__ */ new Map();
18119
+ const syncSourceCell = (name, value) => {
18120
+ const cell = sourceCells.get(name);
18121
+ if (!cell) return;
18122
+ cell.classList.toggle(DASHBOARD_SOURCE_CLASS, selectionIsActive(value));
18123
+ };
17505
18124
  const build = () => {
17506
18125
  const tokens = resolveTheme(currentSpec.theme);
17507
18126
  const layout2 = resolveDashboardLayout(currentSpec.layout);
@@ -17514,8 +18133,9 @@ function renderDashboard(target, spec, options) {
17514
18133
  store.set(param.name, param.value ?? null);
17515
18134
  }
17516
18135
  }
18136
+ sourceCells = /* @__PURE__ */ new Map();
17517
18137
  root = document.createElement("div");
17518
- root.className = "graphein-dashboard";
18138
+ root.className = DASHBOARD_CLASS;
17519
18139
  Object.assign(root.style, {
17520
18140
  boxSizing: "border-box",
17521
18141
  width: currentSpec.dimensions?.width ? `${currentSpec.dimensions.width}px` : "100%",
@@ -17529,6 +18149,10 @@ function renderDashboard(target, spec, options) {
17529
18149
  flexDirection: "column",
17530
18150
  gap: `${layout2.gap}px`
17531
18151
  });
18152
+ const style = document.createElement("style");
18153
+ style.setAttribute("data-graphein-dashboard-style", "");
18154
+ style.textContent = dashboardStyles(tokens);
18155
+ root.appendChild(style);
17532
18156
  const titleText3 = typeof currentSpec.title === "string" ? currentSpec.title : currentSpec.title?.text;
17533
18157
  if (titleText3 || currentSpec.subtitle) {
17534
18158
  const header = document.createElement("div");
@@ -17588,6 +18212,7 @@ function renderDashboard(target, spec, options) {
17588
18212
  };
17589
18213
  const cellChrome = (cell, view) => {
17590
18214
  const framed = view?.frame ?? true;
18215
+ cell.dataset.framed = framed ? "true" : "false";
17591
18216
  Object.assign(cell.style, {
17592
18217
  minWidth: "0",
17593
18218
  minHeight: "0",
@@ -17599,9 +18224,10 @@ function renderDashboard(target, spec, options) {
17599
18224
  background: view?.background ?? (framed ? tokens.color.surface : "transparent")
17600
18225
  });
17601
18226
  };
17602
- const createCard2 = (view) => {
18227
+ const createCard = (view) => {
17603
18228
  const cell = document.createElement("div");
17604
18229
  cellChrome(cell, view);
18230
+ cell.classList.add(DASHBOARD_CELL_CLASS);
17605
18231
  cell.setAttribute("data-view-id", view.id);
17606
18232
  const hasCardChrome = !!(view.title || view.subtitle || view.accent || view.background || view.frame === false || view.padding === "none");
17607
18233
  if (!hasCardChrome) return { cell, host: cell };
@@ -17628,13 +18254,14 @@ function renderDashboard(target, spec, options) {
17628
18254
  padding: `${tokens.spacing.sm}px ${tokens.spacing.md}px ${tokens.spacing.xs}px ${tokens.spacing.md}px`,
17629
18255
  display: "flex",
17630
18256
  flexDirection: "column",
17631
- gap: "2px"
18257
+ gap: "2px",
18258
+ borderBottom: `1px solid ${tokens.color.border}`
17632
18259
  });
17633
18260
  if (view.title) {
17634
18261
  const title2 = document.createElement("div");
17635
18262
  title2.textContent = view.title;
17636
18263
  Object.assign(title2.style, {
17637
- font: `${tokens.font.weight.bold} ${tokens.font.size.base}px ${tokens.font.family}`,
18264
+ font: `${tokens.font.weight.medium} ${tokens.font.size.base}px ${tokens.font.family}`,
17638
18265
  color: tokens.color.text,
17639
18266
  letterSpacing: "-0.01em"
17640
18267
  });
@@ -17662,6 +18289,24 @@ function renderDashboard(target, spec, options) {
17662
18289
  cell.appendChild(host);
17663
18290
  return { cell, host };
17664
18291
  };
18292
+ const publishingParams = (view) => {
18293
+ const names = /* @__PURE__ */ new Set();
18294
+ for (const param of view.spec.params ?? []) {
18295
+ if (param?.name) names.add(param.name);
18296
+ }
18297
+ if (isSlicerType(view.spec.type)) {
18298
+ const slicer = view.spec;
18299
+ const name = typeof slicer.param === "string" ? slicer.param : typeof slicer.field === "string" ? slicer.field : void 0;
18300
+ if (name) names.add(name);
18301
+ }
18302
+ return [...names];
18303
+ };
18304
+ const registerSourceCell = (view, cell) => {
18305
+ for (const name of publishingParams(view)) {
18306
+ sourceCells.set(name, cell);
18307
+ syncSourceCell(name, store.get(name));
18308
+ }
18309
+ };
17665
18310
  const sectionPlans = resolveDashboardSections(gridViews, layout2);
17666
18311
  const layoutGroups = [];
17667
18312
  const deferredMounts = [];
@@ -17674,7 +18319,8 @@ function renderDashboard(target, spec, options) {
17674
18319
  gap: `${layout2.gap}px`
17675
18320
  });
17676
18321
  for (const view of stripViews) {
17677
- const { cell, host } = createCard2(view);
18322
+ const { cell, host } = createCard(view);
18323
+ registerSourceCell(view, cell);
17678
18324
  Object.assign(cell.style, {
17679
18325
  flex: "1 1 220px",
17680
18326
  minWidth: "200px",
@@ -17720,7 +18366,13 @@ function renderDashboard(target, spec, options) {
17720
18366
  if (plan.section?.title) {
17721
18367
  const title2 = document.createElement("span");
17722
18368
  title2.textContent = plan.section.title;
17723
- Object.assign(title2.style, { fontWeight: String(tokens.font.weight.bold), fontSize: `${tokens.font.size.large}px` });
18369
+ Object.assign(title2.style, {
18370
+ color: tokens.color.textMuted,
18371
+ fontWeight: String(tokens.font.weight.medium),
18372
+ fontSize: `${tokens.font.size.small}px`,
18373
+ letterSpacing: "0.06em",
18374
+ textTransform: "uppercase"
18375
+ });
17724
18376
  titles.appendChild(title2);
17725
18377
  }
17726
18378
  if (plan.section?.subtitle) {
@@ -17772,7 +18424,8 @@ function renderDashboard(target, spec, options) {
17772
18424
  parent.appendChild(grid);
17773
18425
  const cells = placeViews(plan.views, plan.cols, layout2.preset ?? "auto");
17774
18426
  for (const cellInfo of cells) {
17775
- const { cell, host } = createCard2(cellInfo.view);
18427
+ const { cell, host } = createCard(cellInfo.view);
18428
+ registerSourceCell(cellInfo.view, cell);
17776
18429
  grid.appendChild(cell);
17777
18430
  cellInfo.el = cell;
17778
18431
  deferredMounts.push({ el: host, view: cellInfo.view, mounted: false });
@@ -17845,8 +18498,10 @@ function renderDashboard(target, spec, options) {
17845
18498
  instances.length = 0;
17846
18499
  if (root && root.parentNode) root.parentNode.removeChild(root);
17847
18500
  root = void 0;
18501
+ sourceCells = /* @__PURE__ */ new Map();
17848
18502
  };
17849
18503
  const unsubscribe = store.subscribe((name, value) => {
18504
+ syncSourceCell(name, value);
17850
18505
  for (const listener of [...hostListeners]) listener(name, value);
17851
18506
  });
17852
18507
  build();
@@ -18064,6 +18719,6 @@ function renderToContext(target, spec) {
18064
18719
  // src/index.ts
18065
18720
  var VERSION = "0.1.0";
18066
18721
 
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 };
18722
+ export { CARTESIAN_TYPES, CHART_TYPES, CanvasLayer, DEFAULT_ENTRANCE_DURATION, DEFAULT_ROUGH_STYLE, DEFAULT_UPDATE_DURATION, DIM_ALPHA, ExpressionError, FACETABLE_TYPES2 as FACETABLE_TYPES, FUNCTION_NAMES, InteractionController, RoughPen, SKETCH_FONT_FAMILY, SKETCH_FONT_NAME, SLICER_TYPES, SR_ONLY_STYLE, Surface, TICK_SIZE, TIME_UNITS, TRANSFORM_KINDS, Tooltip, VERSION, aggregateValues, analyzeChart, animate, applyA11y, applyAggregate, applyBin, applyCalculate, applyFilter, applyFold, applyPatch, applyPick, applyTimeUnit, applyTransform, applyTransforms, arc, area, assertValidSpec, autoInsightAnnotations, backOut, bandScale, bounceOut, buildCartesianInteraction, buildCartesianModel, buildComboModel, buildDataTableFallback, buildFacetModels, buildHistogramModel, buildRenderReport, cartesianInteractionBuilders, cartesianRenderers, categorical, categoricalSchemes, chartTitleText, chartTypeLabel, checkExpression, clamp012 as clamp01, clamp255, compileExpression, compilePredicate, computeBackingSize, computeBins, computeFrame, computeSeriesInsights, contrastRatio, createDiv, createRoughPen, createSelectionStore, cubicIn, cubicInOut, cubicOut, curveCatmullRom, curveLinear, curveMonotoneX, curveStep, curveStepAfter, curveStepBefore, customRenderers, darkTheme, decimate, defaultSpan, dependentParams, diverging, divergingColorScale, divergingSchemes, drawAnnotationLabels, drawAnnotations, drawAxesUnderlay, drawFacet, drawLegend, drawOverlay, drawTitle, drawTrendlineLabels, drawTrendlines, easings, elasticOut, ellipsize, ensureSketchFont, expoInOut, expoOut, facetReport, fillParentStyle, filterRows, fontString, formatDate, formatNumber2 as formatNumber, formatValue, getDevicePixelRatio, groupBy, hashString, inferFieldType, inferFieldTypes, inferValueType, inheritInto, interpolateArray, interpolateNumber, interpolateNumberArray, interpolateObject, interpolateOklab, interpolateRgb, isBrowser, isEmptyValue, isFaceted, isParamClause, isSlicerType, keyFields, legendSwatchWidth, lightTheme, line, linear, linearRegression, linearScale, linearToSrgb, lintSpec, literalToValue, logScale, lttbRun, makeMatcher, matchesValue, measureText, monotoneXTangents, mulberry32, niceDomain, oklabToOklch, oklabToRgb, oklchToOklab, ordinalColorScale, overlayTextToCanvasCmd, paintCanvasText, paintLegendSwatch, parseColor, parseNumberFormat, pivot, placeViews, pointScale, polygonHachureLines, prefersReducedMotion, prettyDate, profileColumns, quadIn, quadInOut, quadOut, rampFromStops, readableTextColor, recommendChart, relativeLuminance, render, renderDashboard, renderToContext, repairReport, repairSpec, resolveCurve, resolveDashboardLayout, resolveDashboardSections, resolveEmphasis, resolveEntrance, resolveFilterValues, resolveInsightOptions, resolveSketch, resolveTheme, resolveUpdate, resolveValueRef, rgbToOklab, rgbaToCss, rotatePoint, roundedRect, sampleArc, sequential, sequentialColorScale, sequentialSchemes, setMeasureContext, setStyle, sinInOut, slicerParamName, smartDate, specFields, srgbToLinear, summarize, summarizeChart, themes, tickIncrement, tickStep, ticks, timeScale, timeTickFormat, timeTicks, toHex, toPointer, tooltipEnabled, truncateTo, validateDashboard, validateSpec, wireViews, withAlpha, withSketchFont };
18068
18723
  //# sourceMappingURL=index.js.map
18069
18724
  //# sourceMappingURL=index.js.map