graphein 0.10.0 → 0.12.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
@@ -6245,6 +6245,10 @@ function yGridEnabled(model) {
6245
6245
  return model.spec.axes?.y?.grid !== false;
6246
6246
  }
6247
6247
  function drawAxesUnderlay(surface, model) {
6248
+ if (model.orientation === "horizontal") {
6249
+ drawHorizontalAxesUnderlay(surface, model);
6250
+ return;
6251
+ }
6248
6252
  const ctx = surface.marks.ctx;
6249
6253
  const { plot, tokens } = model;
6250
6254
  const x0 = plot.x;
@@ -6318,6 +6322,58 @@ function drawAxesUnderlay(surface, model) {
6318
6322
  }
6319
6323
  ctx.restore();
6320
6324
  }
6325
+ function drawHorizontalAxesUnderlay(surface, model) {
6326
+ const ctx = surface.marks.ctx;
6327
+ const { plot, tokens } = model;
6328
+ const x0 = plot.x;
6329
+ const y0 = plot.y;
6330
+ const y1 = plot.y + plot.height;
6331
+ ctx.save();
6332
+ ctx.lineWidth = 1;
6333
+ const pen = model.sketch ? new RoughPen(ctx, model.sketch) : null;
6334
+ const seg = (sx0, sy0, sx1, sy1, stroke) => {
6335
+ pen.polyline([{ x: sx0, y: sy0 }, { x: sx1, y: sy1 }], {
6336
+ stroke,
6337
+ strokeWidth: 1,
6338
+ roughness: model.sketch.roughness * 0.5,
6339
+ bowing: model.sketch.bowing * 0.55
6340
+ });
6341
+ };
6342
+ if (yGridEnabled(model)) {
6343
+ if (pen) {
6344
+ for (const t of model.yTicks) seg(t.pos, y0, t.pos, y1, tokens.color.grid);
6345
+ } else {
6346
+ ctx.strokeStyle = tokens.color.grid;
6347
+ ctx.beginPath();
6348
+ for (const t of model.yTicks) {
6349
+ const x = crisp(t.pos);
6350
+ ctx.moveTo(x, y0);
6351
+ ctx.lineTo(x, y1);
6352
+ }
6353
+ ctx.stroke();
6354
+ }
6355
+ }
6356
+ if (model.spec.axes?.x?.show !== false) {
6357
+ if (pen) {
6358
+ seg(x0, y0, x0, y1, tokens.color.axis);
6359
+ for (const t of model.xTicks) seg(x0 - TICK_SIZE, t.pos, x0, t.pos, tokens.color.axis);
6360
+ } else {
6361
+ ctx.strokeStyle = tokens.color.axis;
6362
+ ctx.beginPath();
6363
+ ctx.moveTo(crisp(x0), y0);
6364
+ ctx.lineTo(crisp(x0), y1);
6365
+ ctx.stroke();
6366
+ ctx.beginPath();
6367
+ for (const t of model.xTicks) {
6368
+ const y = crisp(t.pos);
6369
+ ctx.moveTo(x0 - TICK_SIZE, y);
6370
+ ctx.lineTo(x0, y);
6371
+ }
6372
+ ctx.stroke();
6373
+ }
6374
+ }
6375
+ ctx.restore();
6376
+ }
6321
6377
  function thinXTicks(model, avgCharPx) {
6322
6378
  const ticks2 = model.xTicks;
6323
6379
  if (ticks2.length <= 1) return ticks2;
@@ -6388,9 +6444,12 @@ function drawOverlay(surface, model) {
6388
6444
  const { plot, tokens, frame } = model;
6389
6445
  const f = tokens.font;
6390
6446
  const smallFont = fontString(f.size.small, f.family, f.weight.normal);
6391
- if (model.spec.axes?.y?.labels !== false) {
6447
+ const horizontal = model.orientation === "horizontal";
6448
+ const gutterTicks = horizontal ? model.xTicks : model.yTicks;
6449
+ const gutterLabelsOn = horizontal ? model.spec.axes?.x?.labels !== false : model.spec.axes?.y?.labels !== false;
6450
+ if (gutterLabelsOn) {
6392
6451
  const gutterRight = plot.x - TICK_SIZE - 4;
6393
- for (const t of model.yTicks) {
6452
+ for (const t of gutterTicks) {
6394
6453
  addText(surface, smallFont, {
6395
6454
  left: 0,
6396
6455
  top: t.pos,
@@ -6403,20 +6462,34 @@ function drawOverlay(surface, model) {
6403
6462
  });
6404
6463
  }
6405
6464
  }
6406
- if (model.spec.axes?.x?.labels !== false) {
6465
+ const bottomLabelsOn = horizontal ? model.spec.axes?.y?.labels !== false : model.spec.axes?.x?.labels !== false;
6466
+ if (bottomLabelsOn) {
6407
6467
  const top = plot.y + plot.height + TICK_SIZE + 3;
6408
- const rotated = frame.xLabelAngle > 0;
6409
- for (const p of resolveXLabels(model)) {
6410
- addText(surface, smallFont, {
6411
- left: p.left,
6412
- top,
6413
- text: p.text,
6414
- color: tokens.color.textMuted,
6415
- size: f.size.small,
6416
- align: rotated ? "right" : void 0,
6417
- transform: p.transform,
6418
- transformOrigin: rotated ? "100% 0%" : void 0
6419
- });
6468
+ if (horizontal) {
6469
+ for (const p of placeXLabels(model.yTicks, frame.originX, frame.width, smallFont)) {
6470
+ addText(surface, smallFont, {
6471
+ left: p.left,
6472
+ top,
6473
+ text: p.text,
6474
+ color: tokens.color.textMuted,
6475
+ size: f.size.small,
6476
+ transform: p.transform
6477
+ });
6478
+ }
6479
+ } else {
6480
+ const rotated = frame.xLabelAngle > 0;
6481
+ for (const p of resolveXLabels(model)) {
6482
+ addText(surface, smallFont, {
6483
+ left: p.left,
6484
+ top,
6485
+ text: p.text,
6486
+ color: tokens.color.textMuted,
6487
+ size: f.size.small,
6488
+ align: rotated ? "right" : void 0,
6489
+ transform: p.transform,
6490
+ transformOrigin: rotated ? "100% 0%" : void 0
6491
+ });
6492
+ }
6420
6493
  }
6421
6494
  }
6422
6495
  drawAxisTitles(surface, model);
@@ -6428,6 +6501,32 @@ function drawAxisTitles(surface, model) {
6428
6501
  const f = tokens.font;
6429
6502
  const baseFont = fontString(f.size.base, f.family, f.weight.medium);
6430
6503
  const xTitle = model.spec.axes?.x?.title ?? model.spec.encoding.x.title;
6504
+ const yTitle = model.spec.axes?.y?.title ?? model.spec.encoding.y.title;
6505
+ if (model.orientation === "horizontal") {
6506
+ if (yTitle) {
6507
+ addText(surface, baseFont, {
6508
+ left: plot.x + plot.width / 2,
6509
+ top: frame.originY + frame.height - Math.round(f.size.base * 1.35),
6510
+ text: yTitle,
6511
+ color: tokens.color.text,
6512
+ size: f.size.base,
6513
+ weight: f.weight.medium,
6514
+ transform: "translateX(-50%)"
6515
+ });
6516
+ }
6517
+ if (xTitle) {
6518
+ addText(surface, baseFont, {
6519
+ left: frame.originX + Math.round(f.size.base * 0.4),
6520
+ top: plot.y + plot.height / 2,
6521
+ text: xTitle,
6522
+ color: tokens.color.text,
6523
+ size: f.size.base,
6524
+ weight: f.weight.medium,
6525
+ transform: "translate(-50%, -50%) rotate(-90deg)"
6526
+ });
6527
+ }
6528
+ return;
6529
+ }
6431
6530
  if (xTitle) {
6432
6531
  addText(surface, baseFont, {
6433
6532
  left: plot.x + plot.width / 2,
@@ -6439,7 +6538,6 @@ function drawAxisTitles(surface, model) {
6439
6538
  transform: "translateX(-50%)"
6440
6539
  });
6441
6540
  }
6442
- const yTitle = model.spec.axes?.y?.title ?? model.spec.encoding.y.title;
6443
6541
  if (yTitle) {
6444
6542
  addText(surface, baseFont, {
6445
6543
  left: frame.originX + Math.round(f.size.base * 0.4),
@@ -6642,6 +6740,7 @@ function buildCartesianModel(spec, tokens, opts) {
6642
6740
  const yField = enc.y.field;
6643
6741
  const xType = fieldType(spec, enc.x, "nominal");
6644
6742
  const xKind = xKindFor(spec, xType);
6743
+ const horizontal = spec.type === "bar" && spec.orientation === "horizontal";
6645
6744
  const seriesField = resolveSeriesField(spec);
6646
6745
  const rawSeries = groupBySeries(data, seriesField);
6647
6746
  const seriesKeys = rawSeries.map((s) => s.key);
@@ -6797,6 +6896,19 @@ function buildCartesianModel(spec, tokens, opts) {
6797
6896
  }));
6798
6897
  const padding = { ...DEFAULT_PADDING, ...spec.padding };
6799
6898
  const titleInput4 = resolveTitle(spec);
6899
+ const categoryAxisInput = {
6900
+ show: xShow,
6901
+ labels: xLabels,
6902
+ title: resolveAxisTitle(enc.x, xAxisCfg),
6903
+ edgeAnchored: !categories,
6904
+ labelAngle: xAxisCfg.labelAngle
6905
+ };
6906
+ const valueAxisInput = {
6907
+ show: yShow,
6908
+ labels: yLabels,
6909
+ title: resolveAxisTitle(enc.y, yAxisCfg),
6910
+ edgeAnchored: true
6911
+ };
6800
6912
  const frame = computeFrame({
6801
6913
  width: opts.width,
6802
6914
  height: opts.height,
@@ -6806,8 +6918,8 @@ function buildCartesianModel(spec, tokens, opts) {
6806
6918
  font: tokens.font,
6807
6919
  title: titleInput4,
6808
6920
  legend: legend.show && legendItems.length > 1 ? { items: legendItems, position: legend.position } : void 0,
6809
- xAxis: { show: xShow, labels: xLabels, title: resolveAxisTitle(enc.x, xAxisCfg), edgeAnchored: !categories, labelAngle: xAxisCfg.labelAngle },
6810
- yAxis: { show: yShow, labels: yLabels, title: resolveAxisTitle(enc.y, yAxisCfg) }
6921
+ xAxis: horizontal ? valueAxisInput : categoryAxisInput,
6922
+ yAxis: horizontal ? categoryAxisInput : valueAxisInput
6811
6923
  });
6812
6924
  const legendHits = legendInteractive(spec) && seriesField && frame.legendItems ? frame.legendItems.map((item, i) => {
6813
6925
  const rowH = Math.max(LEGEND_SWATCH2, Math.round(tokens.font.size.small * 1.35));
@@ -6825,16 +6937,18 @@ function buildCartesianModel(spec, tokens, opts) {
6825
6937
  };
6826
6938
  }) : void 0;
6827
6939
  const plot = frame.plot;
6828
- const yRange = [plot.y + plot.height, plot.y];
6829
- const yScale = yIsLog ? logScale({ domain: yDomain, range: yRange, base: yBase, clamp: yClamp }) : linearScale({ domain: yDomain, range: yRange, clamp: yClamp });
6940
+ const valueRange = horizontal ? [plot.x, plot.x + plot.width] : [plot.y + plot.height, plot.y];
6941
+ const yScale = yIsLog ? logScale({ domain: yDomain, range: valueRange, base: yBase, clamp: yClamp }) : linearScale({ domain: yDomain, range: valueRange, clamp: yClamp });
6830
6942
  const yPixel = (value) => yScale.map(toNumber(value));
6831
- const baseline = yIsLog ? plot.y + plot.height : clampPx(yScale.map(0), plot.y, plot.y + plot.height);
6943
+ const valueMin = horizontal ? plot.x : plot.y;
6944
+ const valueMax = horizontal ? plot.x + plot.width : plot.y + plot.height;
6945
+ const baseline = yIsLog ? horizontal ? plot.x : plot.y + plot.height : clampPx(yScale.map(0), valueMin, valueMax);
6832
6946
  const xModel = buildXModel(xKind, {
6833
6947
  field: xField,
6834
6948
  type: xType,
6835
6949
  categories,
6836
6950
  domainNum: xDomainNum,
6837
- range: [plot.x, plot.x + plot.width],
6951
+ range: horizontal ? [plot.y, plot.y + plot.height] : [plot.x, plot.x + plot.width],
6838
6952
  paddingInner: spec.type === "bar" ? 0.2 : spec.type === "box" ? 0.32 : 0.5,
6839
6953
  scaleCfg: enc.x.scale
6840
6954
  });
@@ -6857,6 +6971,8 @@ function buildCartesianModel(spec, tokens, opts) {
6857
6971
  series,
6858
6972
  seriesField,
6859
6973
  stacked,
6974
+ orientation: horizontal ? "horizontal" : "vertical",
6975
+ project: horizontal ? (catPixel, valPixel) => ({ x: valPixel, y: catPixel }) : (catPixel, valPixel) => ({ x: catPixel, y: valPixel }),
6860
6976
  xTicks,
6861
6977
  yTicks,
6862
6978
  colorOf,
@@ -7254,14 +7370,11 @@ function finite2(value) {
7254
7370
  function defaultRadius(model, spec) {
7255
7371
  return Math.max(0, spec.cornerRadius ?? Math.min(4, model.tokens.radius.sm));
7256
7372
  }
7257
- function cornerSideFor(baseY, valueY) {
7258
- if (valueY < baseY) return "top";
7259
- if (valueY > baseY) return "bottom";
7260
- return "none";
7261
- }
7262
7373
  function makeRadii(side, radius) {
7263
7374
  if (side === "top") return [radius, radius, 0, 0];
7264
7375
  if (side === "bottom") return [0, 0, radius, radius];
7376
+ if (side === "right") return [0, radius, radius, 0];
7377
+ if (side === "left") return [radius, 0, 0, radius];
7265
7378
  return 0;
7266
7379
  }
7267
7380
  function drawRect(ctx, rect, radius, pen, alpha = 1) {
@@ -7281,28 +7394,39 @@ function drawRect(ctx, rect, radius, pen, alpha = 1) {
7281
7394
  ctx.fill();
7282
7395
  if (alpha !== 1) ctx.globalAlpha = prevAlpha;
7283
7396
  }
7284
- function barRect(x, width, baseY, valueY, color) {
7285
- if (!finite2(x) || !finite2(width) || !finite2(baseY) || !finite2(valueY)) return null;
7286
- const y = Math.min(baseY, valueY);
7287
- const height = Math.abs(valueY - baseY);
7288
- if (width <= 0 || height <= 0) return null;
7397
+ function orientedRect(horizontal, catStart, catThick, base, value, color) {
7398
+ if (!finite2(catStart) || !finite2(catThick) || !finite2(base) || !finite2(value)) return null;
7399
+ const lo = Math.min(base, value);
7400
+ const len = Math.abs(value - base);
7401
+ if (catThick <= 0 || len <= 0) return null;
7402
+ if (horizontal) {
7403
+ return {
7404
+ x: lo,
7405
+ y: catStart,
7406
+ width: len,
7407
+ height: catThick,
7408
+ color,
7409
+ cornerSide: value > base ? "right" : value < base ? "left" : "none"
7410
+ };
7411
+ }
7289
7412
  return {
7290
- x,
7291
- y,
7292
- width,
7293
- height,
7413
+ x: catStart,
7414
+ y: lo,
7415
+ width: catThick,
7416
+ height: len,
7294
7417
  color,
7295
- cornerSide: cornerSideFor(baseY, valueY)
7418
+ cornerSide: value < base ? "top" : value > base ? "bottom" : "none"
7296
7419
  };
7297
7420
  }
7298
7421
  function drawGroupedBars(ctx, model, radius, pen) {
7299
7422
  const readX = accessor(model.x.field);
7300
7423
  const readY = accessor(model.y.field ?? "");
7424
+ const horizontal = model.orientation === "horizontal";
7301
7425
  const seriesCount = model.series.length;
7302
7426
  const bandWidth = model.x.bandwidth;
7303
7427
  const subWidth = seriesCount > 1 ? bandWidth / seriesCount : bandWidth;
7304
7428
  const gap = seriesCount > 1 && subWidth > 3 ? Math.min(1, subWidth * 0.15) : 0;
7305
- const barWidth = Math.max(0, subWidth - gap);
7429
+ const barThick = Math.max(0, subWidth - gap);
7306
7430
  model.series.forEach((series, seriesIndex) => {
7307
7431
  ctx.fillStyle = series.color;
7308
7432
  for (const row of series.rows) {
@@ -7311,8 +7435,8 @@ function drawGroupedBars(ctx, model, radius, pen) {
7311
7435
  if (raw == null || raw === "" || !finite2(value)) continue;
7312
7436
  const center2 = model.x.pixel(readX(row));
7313
7437
  if (center2 == null) continue;
7314
- const left = center2 - bandWidth / 2 + seriesIndex * subWidth + gap / 2;
7315
- const rect = barRect(left, barWidth, model.y.baseline, model.y.pixel(value), series.color);
7438
+ 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);
7316
7440
  if (rect) drawRect(ctx, rect, radius, pen, rowAlpha(model.emphasis, row));
7317
7441
  }
7318
7442
  });
@@ -7320,6 +7444,7 @@ function drawGroupedBars(ctx, model, radius, pen) {
7320
7444
  function drawStackedBars(ctx, model, radius, pen) {
7321
7445
  const readX = accessor(model.x.field);
7322
7446
  const readY = accessor(model.y.field ?? "");
7447
+ const horizontal = model.orientation === "horizontal";
7323
7448
  const positiveTotals = /* @__PURE__ */ new Map();
7324
7449
  const negativeTotals = /* @__PURE__ */ new Map();
7325
7450
  const segments = [];
@@ -7339,7 +7464,7 @@ function drawStackedBars(ctx, model, radius, pen) {
7339
7464
  const base = totals.get(categoryKey2) ?? 0;
7340
7465
  const top = base + value;
7341
7466
  totals.set(categoryKey2, top);
7342
- const rect = barRect(center2 - bandWidth / 2, bandWidth, model.y.pixel(base), model.y.pixel(top), series.color);
7467
+ const rect = orientedRect(horizontal, center2 - bandWidth / 2, bandWidth, model.y.pixel(base), model.y.pixel(top), series.color);
7343
7468
  if (!rect) continue;
7344
7469
  segments.push({ ...rect, stackEndKey, alpha: rowAlpha(model.emphasis, row) });
7345
7470
  roundedEnds.set(stackEndKey, segments.length - 1);
@@ -10034,7 +10159,7 @@ function buildTable(opts) {
10034
10159
  overflow: "auto",
10035
10160
  pointerEvents: "auto",
10036
10161
  boxSizing: "border-box",
10037
- border: `1px solid ${tokens.color.border}`,
10162
+ border: `1px solid ${opts.sketch ? "transparent" : tokens.color.border}`,
10038
10163
  borderRadius: opts.sketch ? SKETCH_BORDER_RADIUS2 : `${tokens.radius.sm}px`,
10039
10164
  background: tokens.color.background
10040
10165
  });
@@ -10065,14 +10190,23 @@ function paintTableCanvas(opts) {
10065
10190
  const available = Math.max(0, rect.width - 2);
10066
10191
  const widths = computeFittedWidths(opts.columns, available);
10067
10192
  opts.widths = widths;
10193
+ const pen = opts.sketch ? new RoughPen(ctx, opts.sketch) : null;
10194
+ const w = Math.max(0, rect.width);
10195
+ const h = Math.max(0, rect.height);
10068
10196
  ctx.save();
10069
10197
  ctx.beginPath();
10070
- roundedRect(ctx, rect.x, rect.y, Math.max(0, rect.width), Math.max(0, rect.height), tokens.radius.sm);
10071
- ctx.fillStyle = tokens.color.background;
10072
- ctx.fill();
10073
- ctx.lineWidth = 1;
10074
- ctx.strokeStyle = tokens.color.border;
10075
- ctx.stroke();
10198
+ if (pen) {
10199
+ ctx.rect(rect.x, rect.y, w, h);
10200
+ ctx.fillStyle = tokens.color.background;
10201
+ ctx.fill();
10202
+ } else {
10203
+ roundedRect(ctx, rect.x, rect.y, w, h, tokens.radius.sm);
10204
+ ctx.fillStyle = tokens.color.background;
10205
+ ctx.fill();
10206
+ ctx.lineWidth = 1;
10207
+ ctx.strokeStyle = tokens.color.border;
10208
+ ctx.stroke();
10209
+ }
10076
10210
  ctx.clip();
10077
10211
  const headerRows = normalizedHeaderRows(opts);
10078
10212
  const headerHeight = headerRows.length * metrics.headerHeight;
@@ -10084,17 +10218,18 @@ function paintTableCanvas(opts) {
10084
10218
  const clipped = visibleRows < opts.rowCount;
10085
10219
  const cueReserve = clipped && opts.clippedCue !== false && visibleRows > 0 ? metrics.rowHeight : 0;
10086
10220
  const rowsToDraw = Math.max(0, visibleRows - (cueReserve > 0 ? 1 : 0));
10087
- paintHeaderCanvas(opts, widths, headerRows, metrics);
10221
+ paintHeaderCanvas(opts, widths, headerRows, metrics, pen);
10088
10222
  for (let rowIndex = 0; rowIndex < rowsToDraw; rowIndex += 1) {
10089
- paintBodyRowCanvas(opts, widths, rowIndex, bodyTop + rowIndex * metrics.rowHeight, metrics);
10223
+ paintBodyRowCanvas(opts, widths, rowIndex, bodyTop + rowIndex * metrics.rowHeight, metrics, pen);
10090
10224
  }
10091
10225
  if (cueReserve > 0) {
10092
10226
  paintClippedCueCanvas(opts, bodyTop + rowsToDraw * metrics.rowHeight, metrics, opts.rowCount - rowsToDraw);
10093
10227
  }
10094
10228
  if (opts.footerRow) {
10095
- paintFooterCanvas(opts, widths, rect.y + rect.height - metrics.rowHeight, metrics);
10229
+ paintFooterCanvas(opts, widths, rect.y + rect.height - metrics.rowHeight, metrics, pen);
10096
10230
  }
10097
10231
  ctx.restore();
10232
+ if (pen) pen.rect(rect.x, rect.y, w, h, { stroke: tokens.color.border });
10098
10233
  }
10099
10234
  var FILL_EPS = 0.5;
10100
10235
  var cachedScrollbarWidth = null;
@@ -10187,14 +10322,14 @@ function makeHeaderCell(opts, title2, colIndex, align, rowIndex = 0) {
10187
10322
  th.textContent = title2;
10188
10323
  const isSorted = colIndex != null && opts.sortState?.col === colIndex;
10189
10324
  if (isSorted) th.setAttribute("aria-sort", opts.sortState?.dir === "desc" ? "descending" : "ascending");
10190
- setCellBaseStyles(th, opts.tokens, align ?? "left", opts.stickyHeader !== false, metrics.padding);
10325
+ setCellBaseStyles(th, opts.tokens, align ?? "left", opts.stickyHeader !== false, metrics.padding, gridBorderColor(opts));
10191
10326
  setStyles(th, {
10192
10327
  top: `${rowIndex * metrics.headerHeight}px`,
10193
10328
  zIndex: "4",
10194
10329
  background: opts.tokens.color.surface,
10195
10330
  color: opts.tokens.color.text,
10196
10331
  fontWeight: String(opts.tokens.font.weight.bold),
10197
- borderBottom: `1px solid ${opts.tokens.color.border}`,
10332
+ borderBottom: `1px solid ${gridBorderColor(opts)}`,
10198
10333
  userSelect: "none",
10199
10334
  height: `${metrics.headerHeight}px`
10200
10335
  });
@@ -10252,19 +10387,19 @@ function makeBodyRow(opts, rowIndex, rowHeight) {
10252
10387
  const td = document.createElement("td");
10253
10388
  const cell = opts.getCell(rowIndex, colIndex);
10254
10389
  const rowBg = rowBackground(opts, rowIndex, cls);
10255
- setCellBaseStyles(td, opts.tokens, column.align, false, metrics.padding);
10390
+ setCellBaseStyles(td, opts.tokens, column.align, false, metrics.padding, gridBorderColor(opts));
10256
10391
  setStyles(td, {
10257
10392
  position: colIndex < (opts.rowHeaderSpan ?? 0) ? "sticky" : "relative",
10258
10393
  background: rowBg,
10259
10394
  fontWeight: cls === "normal" ? String(opts.tokens.font.weight.normal) : String(opts.tokens.font.weight.bold),
10260
10395
  color: opts.tokens.color.text,
10261
- borderBottom: `1px solid ${opts.tokens.color.border}`,
10396
+ borderBottom: `1px solid ${gridBorderColor(opts)}`,
10262
10397
  height: rowHeight ? `${rowHeight}px` : `${metrics.rowHeight}px`,
10263
10398
  whiteSpace: column.wrap === true ? "normal" : "nowrap"
10264
10399
  });
10265
10400
  const indent = opts.cellIndent?.(rowIndex, colIndex) ?? 0;
10266
10401
  if (indent > 0) td.style.paddingLeft = `${metrics.paddingX + indent}px`;
10267
- applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg);
10402
+ applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg, rowIndex);
10268
10403
  if (td.childNodes.length === 0) {
10269
10404
  td.textContent = formatDisplayValue(cell.value, column);
10270
10405
  applyNegativeStyle(td, cell.value, column);
@@ -10282,7 +10417,7 @@ function buildFooter(opts) {
10282
10417
  opts.columns.forEach((column, colIndex) => {
10283
10418
  const td = document.createElement("td");
10284
10419
  const cell = opts.footerRow?.cells[colIndex] ?? { value: null};
10285
- setCellBaseStyles(td, opts.tokens, column.align, false, metrics.padding);
10420
+ setCellBaseStyles(td, opts.tokens, column.align, false, metrics.padding, gridBorderColor(opts));
10286
10421
  setStyles(td, {
10287
10422
  position: colIndex < (opts.rowHeaderSpan ?? 0) ? "sticky" : "sticky",
10288
10423
  bottom: "0",
@@ -10290,8 +10425,8 @@ function buildFooter(opts) {
10290
10425
  background: opts.tokens.color.surface,
10291
10426
  color: opts.tokens.color.text,
10292
10427
  fontWeight: String(opts.tokens.font.weight.bold),
10293
- borderTop: `1px solid ${opts.tokens.color.border}`,
10294
- borderBottom: `1px solid ${opts.tokens.color.border}`,
10428
+ borderTop: `1px solid ${gridBorderColor(opts)}`,
10429
+ borderBottom: `1px solid ${gridBorderColor(opts)}`,
10295
10430
  height: `${metrics.rowHeight}px`,
10296
10431
  whiteSpace: column.wrap === true ? "normal" : "nowrap"
10297
10432
  });
@@ -10317,7 +10452,7 @@ function makeSpacerRow(opts, height) {
10317
10452
  tr.appendChild(td);
10318
10453
  return tr;
10319
10454
  }
10320
- function applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg) {
10455
+ function applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg, rowIndex = 0) {
10321
10456
  const cf = column.conditionalFormat;
10322
10457
  const raw = cell.raw;
10323
10458
  const domain = opts.conditionalDomains?.[colIndex] ?? null;
@@ -10362,13 +10497,27 @@ function applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg) {
10362
10497
  }
10363
10498
  if (!domain) return;
10364
10499
  const ratio = normalized(numeric, domain);
10365
- const bar = document.createElement("div");
10366
10500
  const diverging2 = cf.negativeColor !== void 0 || cf.baseline === "zero";
10367
10501
  const positive = numeric >= 0;
10368
10502
  const barColor = alphaColor(positive ? cf.color ?? opts.tokens.color.accent : cf.negativeColor ?? opts.tokens.color.negative, 0.85);
10369
10503
  const baseline = diverging2 ? normalized(0, domain) : 0;
10370
10504
  const left = diverging2 ? Math.min(baseline, normalized(numeric, domain)) : 0;
10371
10505
  const width = diverging2 ? Math.abs(normalized(numeric, domain) - baseline) : ratio;
10506
+ const text = document.createElement("span");
10507
+ text.textContent = cf.showValue === false ? "" : formatDisplayValue(cell.value, column);
10508
+ setStyles(text, overlayTextStyles(opts.tokens.color.text));
10509
+ td.style.background = rowBg;
10510
+ if (opts.sketch) {
10511
+ const colWidth = opts.widths?.[colIndex] ?? column.width ?? defaultColumnWidth(column);
10512
+ const rowHeight = densityMetrics(opts.density).rowHeight;
10513
+ const seed = (opts.sketch.seed ^ Math.imul(rowIndex + 1, 374761393) ^ Math.imul(colIndex + 1, 668265263)) >>> 0;
10514
+ const canvas = buildSketchDataBar(opts.sketch, barColor, colWidth, rowHeight, left, width, seed);
10515
+ if (canvas) td.appendChild(canvas);
10516
+ td.appendChild(text);
10517
+ applyNegativeStyle(text, cell.value, column);
10518
+ return;
10519
+ }
10520
+ const bar = document.createElement("div");
10372
10521
  setStyles(bar, {
10373
10522
  position: "absolute",
10374
10523
  left: `calc(${Math.round(left * 1e3) / 10}% + 4px)`,
@@ -10380,14 +10529,38 @@ function applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg) {
10380
10529
  opacity: "0.9",
10381
10530
  pointerEvents: "none"
10382
10531
  });
10383
- const text = document.createElement("span");
10384
- text.textContent = cf.showValue === false ? "" : formatDisplayValue(cell.value, column);
10385
- setStyles(text, overlayTextStyles(opts.tokens.color.text));
10386
- td.style.background = rowBg;
10387
10532
  td.append(bar, text);
10388
10533
  applyNegativeStyle(text, cell.value, column);
10389
10534
  }
10390
- function setCellBaseStyles(cell, tokens, align, sticky, padding = "6px 10px") {
10535
+ function buildSketchDataBar(sketch, color, colWidth, rowHeight, leftRatio, widthRatio, seed) {
10536
+ const avail = Math.max(0, colWidth - 8);
10537
+ const barW = widthRatio * avail;
10538
+ const barH = Math.max(0, rowHeight - 10);
10539
+ if (barW <= 0.5 || barH <= 0) return null;
10540
+ const pad3 = 3;
10541
+ const leftPx = 4 + leftRatio * avail;
10542
+ const dpr = getDevicePixelRatio();
10543
+ const canvas = document.createElement("canvas");
10544
+ const cssW = barW + pad3 * 2;
10545
+ const cssH = barH + pad3 * 2;
10546
+ canvas.width = Math.max(1, Math.round(cssW * dpr));
10547
+ canvas.height = Math.max(1, Math.round(cssH * dpr));
10548
+ setStyles(canvas, {
10549
+ position: "absolute",
10550
+ left: `${leftPx - pad3}px`,
10551
+ top: `${5 - pad3}px`,
10552
+ width: `${cssW}px`,
10553
+ height: `${cssH}px`,
10554
+ pointerEvents: "none"
10555
+ });
10556
+ const ctx = canvas.getContext("2d");
10557
+ if (!ctx) return null;
10558
+ ctx.scale(dpr, dpr);
10559
+ const pen = new RoughPen(ctx, { ...sketch, seed });
10560
+ pen.rect(pad3, pad3, barW, barH, { stroke: color, fill: color, fillAlpha: 0.9, strokeAlpha: 0.9 });
10561
+ return canvas;
10562
+ }
10563
+ function setCellBaseStyles(cell, tokens, align, sticky, padding = "6px 10px", borderColor = tokens.color.border) {
10391
10564
  setStyles(cell, {
10392
10565
  boxSizing: "border-box",
10393
10566
  padding,
@@ -10395,11 +10568,14 @@ function setCellBaseStyles(cell, tokens, align, sticky, padding = "6px 10px") {
10395
10568
  whiteSpace: "nowrap",
10396
10569
  overflow: "hidden",
10397
10570
  textOverflow: "ellipsis",
10398
- borderRight: `1px solid ${tokens.color.border}`,
10571
+ borderRight: `1px solid ${borderColor}`,
10399
10572
  verticalAlign: "middle"
10400
10573
  });
10401
10574
  if (sticky) cell.style.position = "sticky";
10402
10575
  }
10576
+ function gridBorderColor(opts) {
10577
+ return opts.sketch ? "transparent" : opts.tokens.color.border;
10578
+ }
10403
10579
  function renderIconValue(td, icon, color, value, position) {
10404
10580
  const iconSpan = document.createElement("span");
10405
10581
  iconSpan.textContent = icon;
@@ -10453,7 +10629,7 @@ function normalizedHeaderRows(opts) {
10453
10629
  }))
10454
10630
  ];
10455
10631
  }
10456
- function paintHeaderCanvas(opts, widths, rows, metrics) {
10632
+ function paintHeaderCanvas(opts, widths, rows, metrics, pen) {
10457
10633
  rows.forEach((row, rowIndex) => {
10458
10634
  let cursor = 0;
10459
10635
  for (const cell of row) {
@@ -10464,7 +10640,7 @@ function paintHeaderCanvas(opts, widths, rows, metrics) {
10464
10640
  const y = opts.rect.y + rowIndex * metrics.headerHeight;
10465
10641
  const w = sumWidths(widths.slice(start, start + colSpan));
10466
10642
  const h = metrics.headerHeight * rowSpan;
10467
- paintCellBox(opts.ctx, x, y, w, h, opts.tokens.color.surface, opts.tokens.color.border);
10643
+ paintCellBox(opts.ctx, x, y, w, h, opts.tokens.color.surface, opts.tokens.color.border, false, pen);
10468
10644
  paintCellText(opts, {
10469
10645
  x,
10470
10646
  y,
@@ -10480,7 +10656,7 @@ function paintHeaderCanvas(opts, widths, rows, metrics) {
10480
10656
  }
10481
10657
  });
10482
10658
  }
10483
- function paintBodyRowCanvas(opts, widths, rowIndex, y, metrics) {
10659
+ function paintBodyRowCanvas(opts, widths, rowIndex, y, metrics, pen) {
10484
10660
  const cls = opts.rowClass?.(rowIndex) ?? "normal";
10485
10661
  let x = opts.rect.x;
10486
10662
  for (let colIndex = 0; colIndex < opts.columns.length; colIndex += 1) {
@@ -10490,8 +10666,8 @@ function paintBodyRowCanvas(opts, widths, rowIndex, y, metrics) {
10490
10666
  const baseBg = canvasRowBackground(opts, rowIndex, cls);
10491
10667
  const conditional = resolveCanvasConditional(opts, column, colIndex, cell, baseBg);
10492
10668
  const bg = conditional.background ?? baseBg;
10493
- paintCellBox(opts.ctx, x, y, width, metrics.rowHeight, bg, opts.tokens.color.border);
10494
- if (conditional.bar) paintDataBar(opts, x, y, width, metrics.rowHeight, conditional.bar);
10669
+ paintCellBox(opts.ctx, x, y, width, metrics.rowHeight, bg, opts.tokens.color.border, false, pen);
10670
+ if (conditional.bar) paintDataBar(opts, x, y, width, metrics.rowHeight, conditional.bar, pen);
10495
10671
  const indent = opts.cellIndent?.(rowIndex, colIndex) ?? 0;
10496
10672
  const text = conditional.showValue === false ? "" : formatDisplayValue(cell.value, column);
10497
10673
  const color = negativeTextColor(cell.value, column, conditional.color ?? opts.tokens.color.text);
@@ -10513,13 +10689,13 @@ function paintBodyRowCanvas(opts, widths, rowIndex, y, metrics) {
10513
10689
  x += width;
10514
10690
  }
10515
10691
  }
10516
- function paintFooterCanvas(opts, widths, y, metrics) {
10692
+ function paintFooterCanvas(opts, widths, y, metrics, pen) {
10517
10693
  let x = opts.rect.x;
10518
10694
  for (let colIndex = 0; colIndex < opts.columns.length; colIndex += 1) {
10519
10695
  const column = opts.columns[colIndex];
10520
10696
  const width = widths[colIndex] ?? 0;
10521
10697
  const cell = opts.footerRow?.cells[colIndex] ?? { value: null};
10522
- paintCellBox(opts.ctx, x, y, width, metrics.rowHeight, opts.tokens.color.surface, opts.tokens.color.border, true);
10698
+ paintCellBox(opts.ctx, x, y, width, metrics.rowHeight, opts.tokens.color.surface, opts.tokens.color.border, true, pen);
10523
10699
  paintCellText(opts, {
10524
10700
  x,
10525
10701
  y,
@@ -10548,10 +10724,25 @@ function paintClippedCueCanvas(opts, y, metrics, remaining) {
10548
10724
  paddingX: metrics.paddingX
10549
10725
  });
10550
10726
  }
10551
- function paintCellBox(ctx, x, y, width, height, background, border, topBorder = false) {
10727
+ function paintCellBox(ctx, x, y, width, height, background, border, topBorder = false, pen) {
10552
10728
  ctx.save();
10553
10729
  ctx.fillStyle = background;
10554
10730
  ctx.fillRect(x, y, width, height);
10731
+ if (pen) {
10732
+ pen.polyline(
10733
+ [
10734
+ { x: x + width, y },
10735
+ { x: x + width, y: y + height },
10736
+ { x, y: y + height }
10737
+ ],
10738
+ { stroke: border, strokeWidth: 1 }
10739
+ );
10740
+ if (topBorder) {
10741
+ pen.polyline([{ x, y }, { x: x + width, y }], { stroke: border, strokeWidth: 1 });
10742
+ }
10743
+ ctx.restore();
10744
+ return;
10745
+ }
10555
10746
  ctx.strokeStyle = border;
10556
10747
  ctx.lineWidth = 1;
10557
10748
  ctx.beginPath();
@@ -10662,9 +10853,18 @@ function resolveCanvasConditional(opts, column, colIndex, cell, rowBg) {
10662
10853
  const color = alphaColor(positive ? cf.color ?? opts.tokens.color.accent : cf.negativeColor ?? opts.tokens.color.negative, 0.85);
10663
10854
  return { background: rowBg, showValue: cf.showValue !== false, bar: { leftRatio, widthRatio, color } };
10664
10855
  }
10665
- function paintDataBar(opts, x, y, width, height, bar) {
10856
+ function paintDataBar(opts, x, y, width, height, bar, pen) {
10666
10857
  const left = x + 4 + bar.leftRatio * Math.max(0, width - 8);
10667
10858
  const barW = bar.widthRatio * Math.max(0, width - 8);
10859
+ if (pen) {
10860
+ pen.rect(left, y + 5, barW, Math.max(0, height - 10), {
10861
+ stroke: bar.color,
10862
+ fill: bar.color,
10863
+ fillAlpha: 0.9,
10864
+ strokeAlpha: 0.9
10865
+ });
10866
+ return;
10867
+ }
10668
10868
  opts.ctx.save();
10669
10869
  opts.ctx.globalAlpha *= 0.9;
10670
10870
  opts.ctx.beginPath();
@@ -10673,6 +10873,72 @@ function paintDataBar(opts, x, y, width, height, bar) {
10673
10873
  opts.ctx.fill();
10674
10874
  opts.ctx.restore();
10675
10875
  }
10876
+ function createSketchGridCanvas(rect) {
10877
+ const dpr = getDevicePixelRatio();
10878
+ const w = Math.max(0, rect.width);
10879
+ const h = Math.max(0, rect.height);
10880
+ const canvas = document.createElement("canvas");
10881
+ canvas.width = Math.max(1, Math.round(w * dpr));
10882
+ canvas.height = Math.max(1, Math.round(h * dpr));
10883
+ setStyles(canvas, {
10884
+ position: "absolute",
10885
+ left: `${rect.x}px`,
10886
+ top: `${rect.y}px`,
10887
+ width: `${w}px`,
10888
+ height: `${h}px`,
10889
+ pointerEvents: "none"
10890
+ });
10891
+ return { canvas, ctx: canvas.getContext("2d"), dpr };
10892
+ }
10893
+ function paintSketchGridOverlay(ctx, o) {
10894
+ const w = Math.max(0, o.rect.width);
10895
+ const h = Math.max(0, o.rect.height);
10896
+ ctx.save();
10897
+ ctx.setTransform(o.dpr, 0, 0, o.dpr, 0, 0);
10898
+ ctx.clearRect(0, 0, w, h);
10899
+ const metrics = densityMetrics(o.density);
10900
+ const headerHeight = o.headerRowCount * metrics.headerHeight;
10901
+ const footerHeight = o.hasFooter ? metrics.rowHeight : 0;
10902
+ const bodyTop = headerHeight;
10903
+ const bodyBottom = Math.max(bodyTop, h - footerHeight);
10904
+ const border = o.tokens.color.border;
10905
+ const base = o.sketch.seed >>> 0;
10906
+ const stroke = (x1, y1, x2, y2, seed) => {
10907
+ const pen = new RoughPen(ctx, { ...o.sketch, seed: seed >>> 0 });
10908
+ pen.polyline(
10909
+ [
10910
+ { x: x1, y: y1 },
10911
+ { x: x2, y: y2 }
10912
+ ],
10913
+ { stroke: border, strokeWidth: 1 }
10914
+ );
10915
+ };
10916
+ let cursor = 0;
10917
+ for (let i = 0; i < o.columns.length - 1; i += 1) {
10918
+ cursor += o.widths[i] ?? 0;
10919
+ const pinned = i < (o.rowHeaderSpan ?? 0);
10920
+ const x = pinned ? cursor : cursor - o.scrollLeft;
10921
+ if (x <= 0 || x >= w) continue;
10922
+ stroke(x, 0, x, h, base ^ Math.imul(i + 1, 2246822519));
10923
+ }
10924
+ ctx.save();
10925
+ ctx.beginPath();
10926
+ ctx.rect(0, bodyTop, w, Math.max(0, bodyBottom - bodyTop));
10927
+ ctx.clip();
10928
+ const firstRow = Math.max(0, Math.floor(o.scrollTop / metrics.rowHeight) - 1);
10929
+ for (let r = firstRow; r < o.rowCount; r += 1) {
10930
+ const y = bodyTop + (r + 1) * metrics.rowHeight - o.scrollTop;
10931
+ if (y <= bodyTop) continue;
10932
+ if (y >= bodyBottom) break;
10933
+ stroke(0, y, w, y, base ^ Math.imul(r + 1, 3266489917));
10934
+ }
10935
+ ctx.restore();
10936
+ if (headerHeight > 0 && headerHeight < h) stroke(0, headerHeight, w, headerHeight, (base ^ 2654435769) >>> 0);
10937
+ if (footerHeight > 0 && bodyBottom > bodyTop) stroke(0, bodyBottom, w, bodyBottom, (base ^ 2246822507) >>> 0);
10938
+ const framePen = new RoughPen(ctx, { ...o.sketch, seed: base });
10939
+ framePen.rect(1.5, 1.5, Math.max(0, w - 3), Math.max(0, h - 3), { stroke: border, strokeWidth: 1 });
10940
+ ctx.restore();
10941
+ }
10676
10942
  function canvasRowBackground(opts, rowIndex, cls) {
10677
10943
  if (cls !== "normal") return opts.tokens.color.surface;
10678
10944
  if (opts.striped === true && rowIndex % 2 === 1) return subtleStripe(opts.tokens);
@@ -10775,7 +11041,7 @@ function drawTableCanvas(surface, spec, tokens, size) {
10775
11041
  headerRows: buildTableHeaderRows(columns),
10776
11042
  footerRow: buildTotalsRow(sortedRows, columns, read, spec),
10777
11043
  density: spec.density,
10778
- sketch: resolveSketch(spec) != null
11044
+ sketch: resolveSketch(spec)
10779
11045
  });
10780
11046
  }
10781
11047
  function renderTable(surface, spec, tokens, size) {
@@ -10783,7 +11049,7 @@ function renderTable(surface, spec, tokens, size) {
10783
11049
  const data = spec.data ?? [];
10784
11050
  const columns = resolveColumns(data, spec);
10785
11051
  const read = columns.map((column) => accessor(column.key));
10786
- const sketch = resolveSketch(spec) != null;
11052
+ const sketch = resolveSketch(spec);
10787
11053
  let sortState = initialSortState(spec, columns);
10788
11054
  let sortedRows = sortRows(data, columns, read, sortState);
10789
11055
  const domains = columns.map(
@@ -10794,6 +11060,9 @@ function renderTable(surface, spec, tokens, size) {
10794
11060
  );
10795
11061
  const host = document.createElement("div");
10796
11062
  surface.overlay.appendChild(host);
11063
+ const headerRowCount = buildTableHeaderRows(columns)?.length ?? 1;
11064
+ const overlay = sketch ? createSketchGridCanvas(rect) : null;
11065
+ if (overlay) surface.overlay.appendChild(overlay.canvas);
10797
11066
  let frame = 0;
10798
11067
  const renderWindow = () => {
10799
11068
  const scrollTop = host.scrollTop || 0;
@@ -10801,7 +11070,8 @@ function renderTable(surface, spec, tokens, size) {
10801
11070
  const start = Math.max(0, Math.floor(scrollTop / rowHeight) - BUFFER_ROWS);
10802
11071
  const visible = Math.ceil(Math.max(rect.height, host.clientHeight || rect.height) / rowHeight) + BUFFER_ROWS * 2;
10803
11072
  const end = Math.min(sortedRows.length, start + visible);
10804
- buildTable({
11073
+ const footerRow = buildTotalsRow(sortedRows, columns, read, spec);
11074
+ const buildOpts = {
10805
11075
  container: host,
10806
11076
  tokens,
10807
11077
  rect,
@@ -10822,11 +11092,28 @@ function renderTable(surface, spec, tokens, size) {
10822
11092
  sortState: sortState ?? void 0,
10823
11093
  conditionalDomains: domains,
10824
11094
  headerRows: buildTableHeaderRows(columns),
10825
- footerRow: buildTotalsRow(sortedRows, columns, read, spec),
11095
+ footerRow,
10826
11096
  density: spec.density,
10827
11097
  visibleRange: { start, end, rowHeight },
10828
11098
  sketch
10829
- });
11099
+ };
11100
+ buildTable(buildOpts);
11101
+ if (sketch && overlay?.ctx) {
11102
+ paintSketchGridOverlay(overlay.ctx, {
11103
+ sketch,
11104
+ tokens,
11105
+ rect,
11106
+ dpr: overlay.dpr,
11107
+ columns,
11108
+ widths: buildOpts.widths ?? [],
11109
+ rowCount: sortedRows.length,
11110
+ headerRowCount,
11111
+ hasFooter: Boolean(footerRow),
11112
+ density: spec.density,
11113
+ scrollLeft: host.scrollLeft || 0,
11114
+ scrollTop
11115
+ });
11116
+ }
10830
11117
  };
10831
11118
  host.addEventListener("scroll", () => {
10832
11119
  if (frame !== 0) return;
@@ -10983,15 +11270,16 @@ function drawMatrixCanvas(surface, spec, tokens, size) {
10983
11270
  conditionalDomains: prepared.bodyDomains,
10984
11271
  headerRows: prepared.headerRows,
10985
11272
  density: spec.density,
10986
- sketch: resolveSketch(spec) != null
11273
+ sketch: resolveSketch(spec)
10987
11274
  });
10988
11275
  }
10989
11276
  function renderMatrix(surface, spec, tokens, size) {
10990
11277
  const rect = drawTitleBlock(surface, tokens, size, spec.title);
10991
11278
  const prepared = prepareMatrix(spec);
11279
+ const sketch = resolveSketch(spec);
10992
11280
  const host = document.createElement("div");
10993
11281
  surface.overlay.appendChild(host);
10994
- buildTable({
11282
+ const buildOpts = {
10995
11283
  container: host,
10996
11284
  tokens,
10997
11285
  rect,
@@ -11018,8 +11306,45 @@ function renderMatrix(surface, spec, tokens, size) {
11018
11306
  conditionalDomains: prepared.bodyDomains,
11019
11307
  headerRows: prepared.headerRows,
11020
11308
  density: spec.density,
11021
- sketch: resolveSketch(spec) != null
11022
- });
11309
+ sketch
11310
+ };
11311
+ buildTable(buildOpts);
11312
+ if (sketch) {
11313
+ const overlay = createSketchGridCanvas(rect);
11314
+ if (overlay.ctx) {
11315
+ surface.overlay.appendChild(overlay.canvas);
11316
+ const ctx = overlay.ctx;
11317
+ const draw = () => {
11318
+ paintSketchGridOverlay(ctx, {
11319
+ sketch,
11320
+ tokens,
11321
+ rect,
11322
+ dpr: overlay.dpr,
11323
+ columns: prepared.columns,
11324
+ widths: buildOpts.widths ?? [],
11325
+ rowCount: prepared.rows.length,
11326
+ headerRowCount: prepared.headerRows.length,
11327
+ hasFooter: false,
11328
+ rowHeaderSpan: 1,
11329
+ density: spec.density,
11330
+ scrollLeft: host.scrollLeft || 0,
11331
+ scrollTop: host.scrollTop || 0
11332
+ });
11333
+ };
11334
+ draw();
11335
+ let frame = 0;
11336
+ host.addEventListener("scroll", () => {
11337
+ if (frame !== 0) return;
11338
+ frame = typeof requestAnimationFrame === "function" ? requestAnimationFrame(() => {
11339
+ frame = 0;
11340
+ draw();
11341
+ }) : window.setTimeout(() => {
11342
+ frame = 0;
11343
+ draw();
11344
+ }, 16);
11345
+ });
11346
+ }
11347
+ }
11023
11348
  }
11024
11349
  function prepareMatrix(spec) {
11025
11350
  const data = spec.data ?? [];
@@ -12454,6 +12779,7 @@ function indexInteraction(model) {
12454
12779
  const xFormat = model.spec.encoding.x?.format;
12455
12780
  const yFormat = model.spec.encoding.y?.format;
12456
12781
  const band = x.kind === "band";
12782
+ const horizontal = model.orientation === "horizontal";
12457
12783
  const showDots = !model.stacked && !band;
12458
12784
  const stopByKey = /* @__PURE__ */ new Map();
12459
12785
  const lookups = [];
@@ -12488,7 +12814,7 @@ function indexInteraction(model) {
12488
12814
  };
12489
12815
  const hitTest = (px, py) => {
12490
12816
  if (!inside(plot, px, py, 6)) return null;
12491
- const best = nearestStop(px);
12817
+ const best = nearestStop(horizontal ? py : px);
12492
12818
  const rows = [];
12493
12819
  const dots = [];
12494
12820
  let nearestDy = Infinity;
@@ -12516,15 +12842,19 @@ function indexInteraction(model) {
12516
12842
  const anchorY = showDots && nearestIdx >= 0 ? dots[nearestIdx].py : py;
12517
12843
  return {
12518
12844
  key: best.key,
12519
- anchorX: best.px,
12520
- anchorY,
12845
+ anchorX: horizontal ? px : best.px,
12846
+ anchorY: horizontal ? best.px : anchorY,
12521
12847
  content: { title: formatValue(best.value, xFormat), rows },
12522
12848
  draw: (ctx) => {
12523
12849
  ctx.save();
12524
12850
  if (band) {
12525
12851
  const half = x.bandwidth / 2;
12526
12852
  ctx.fillStyle = withAlpha(model.tokens.color.text, 0.06);
12527
- ctx.fillRect(best.px - half, plot.y, x.bandwidth, plot.height);
12853
+ if (horizontal) {
12854
+ ctx.fillRect(plot.x, best.px - half, plot.width, x.bandwidth);
12855
+ } else {
12856
+ ctx.fillRect(best.px - half, plot.y, x.bandwidth, plot.height);
12857
+ }
12528
12858
  } else {
12529
12859
  ctx.strokeStyle = withAlpha(model.tokens.color.text, 0.28);
12530
12860
  ctx.lineWidth = 1;
@@ -12553,7 +12883,7 @@ function indexInteraction(model) {
12553
12883
  };
12554
12884
  const pick = (px, py) => {
12555
12885
  if (!inside(plot, px, py, 6)) return null;
12556
- const best = nearestStop(px);
12886
+ const best = nearestStop(horizontal ? py : px);
12557
12887
  return { kind: "point", fields: [xField], tuples: [[best.value]] };
12558
12888
  };
12559
12889
  return { region: plot, hitTest, pick };
@@ -12920,6 +13250,8 @@ function buildComboModel(spec, tokens, opts) {
12920
13250
  series: [{ key: "", label: name, color, value: name, rows: data }],
12921
13251
  seriesField: void 0,
12922
13252
  stacked: false,
13253
+ orientation: "vertical",
13254
+ project: (c, v) => ({ x: c, y: v }),
12923
13255
  xTicks,
12924
13256
  yTicks: sideTicks,
12925
13257
  colorOf: () => color,
@@ -12947,6 +13279,8 @@ function buildComboModel(spec, tokens, opts) {
12947
13279
  series: [],
12948
13280
  seriesField: void 0,
12949
13281
  stacked: false,
13282
+ orientation: "vertical",
13283
+ project: (c, v) => ({ x: c, y: v }),
12950
13284
  xTicks,
12951
13285
  yTicks: leftTicks,
12952
13286
  colorOf: () => "",
@@ -13184,6 +13518,8 @@ function buildHistogramModel(spec, tokens, opts) {
13184
13518
  series: [],
13185
13519
  seriesField: void 0,
13186
13520
  stacked: false,
13521
+ orientation: "vertical",
13522
+ project: (c, v) => ({ x: c, y: v }),
13187
13523
  xTicks,
13188
13524
  yTicks,
13189
13525
  colorOf: () => color,
@@ -14387,6 +14723,8 @@ function buildWaterfallModel(spec, tokens, opts) {
14387
14723
  series: [],
14388
14724
  seriesField: void 0,
14389
14725
  stacked: false,
14726
+ orientation: "vertical",
14727
+ project: (c, v) => ({ x: c, y: v }),
14390
14728
  xTicks,
14391
14729
  yTicks,
14392
14730
  colorOf: () => incColor,
@@ -14648,6 +14986,8 @@ function buildSlopeModel(spec, tokens, opts) {
14648
14986
  series: [],
14649
14987
  seriesField: void 0,
14650
14988
  stacked: false,
14989
+ orientation: "vertical",
14990
+ project: (c, v) => ({ x: c, y: v }),
14651
14991
  xTicks,
14652
14992
  yTicks,
14653
14993
  colorOf: () => tokens.color.text,
@@ -15122,9 +15462,14 @@ function toPixel(model, axis, value) {
15122
15462
  return px != null && Number.isFinite(px) ? px : void 0;
15123
15463
  }
15124
15464
  function pointPixels(model, ann) {
15125
- const px = toPixel(model, "x", ann.x);
15126
- const py = toPixel(model, "y", ann.y);
15127
- return px === void 0 || py === void 0 ? void 0 : { px, py };
15465
+ const cat = toPixel(model, "x", ann.x);
15466
+ const val = toPixel(model, "y", ann.y);
15467
+ if (cat === void 0 || val === void 0) return void 0;
15468
+ const { x, y } = model.project(cat, val);
15469
+ return { px: x, py: y };
15470
+ }
15471
+ function drawsHorizontalRule(model, axis) {
15472
+ return model.orientation === "horizontal" ? axis === "x" : axis === "y";
15128
15473
  }
15129
15474
  function anchorPixel(model, ann, axis, kind) {
15130
15475
  if (kind === "line") return toPixel(model, axis, ann.value);
@@ -15173,6 +15518,7 @@ function drawAnnotations(surface, model) {
15173
15518
  }
15174
15519
  const axis = ann.axis === "x" ? "x" : "y";
15175
15520
  const color = ann.color ?? tokens.color.textMuted;
15521
+ const ruleHorizontal = drawsHorizontalRule(model, axis);
15176
15522
  if (kindOf(ann) === "line") {
15177
15523
  const p = toPixel(model, axis, ann.value);
15178
15524
  if (p === void 0) continue;
@@ -15181,7 +15527,7 @@ function drawAnnotations(surface, model) {
15181
15527
  ctx.lineWidth = ann.strokeWidth ?? 1.5;
15182
15528
  ctx.setLineDash(ann.strokeDash ?? DEFAULT_DASH);
15183
15529
  ctx.beginPath();
15184
- if (axis === "y") {
15530
+ if (ruleHorizontal) {
15185
15531
  const y = crisp(p);
15186
15532
  ctx.moveTo(plot.x, y);
15187
15533
  ctx.lineTo(x1, y);
@@ -15202,7 +15548,7 @@ function drawAnnotations(surface, model) {
15202
15548
  ctx.save();
15203
15549
  ctx.globalAlpha = ann.fillOpacity ?? DEFAULT_FILL_OPACITY;
15204
15550
  ctx.fillStyle = color;
15205
- if (axis === "y") ctx.fillRect(plot.x, lo, plot.width, hi - lo);
15551
+ if (ruleHorizontal) ctx.fillRect(plot.x, lo, plot.width, hi - lo);
15206
15552
  else ctx.fillRect(lo, plot.y, hi - lo, plot.height);
15207
15553
  ctx.restore();
15208
15554
  ctx.save();
@@ -15211,7 +15557,7 @@ function drawAnnotations(surface, model) {
15211
15557
  ctx.lineWidth = 1;
15212
15558
  ctx.setLineDash(ann.strokeDash ?? [4, 4]);
15213
15559
  ctx.beginPath();
15214
- if (axis === "y") {
15560
+ if (ruleHorizontal) {
15215
15561
  ctx.moveTo(plot.x, crisp(lo));
15216
15562
  ctx.lineTo(x1, crisp(lo));
15217
15563
  ctx.moveTo(plot.x, crisp(hi));
@@ -15227,10 +15573,10 @@ function drawAnnotations(surface, model) {
15227
15573
  }
15228
15574
  ctx.restore();
15229
15575
  }
15230
- function labelBox(model, axis, pos, p) {
15576
+ function labelBox(model, horizontalRule, pos, p) {
15231
15577
  const { plot } = model;
15232
15578
  const inset = 6;
15233
- if (axis === "y") {
15579
+ if (horizontalRule) {
15234
15580
  const top = p - 3;
15235
15581
  const transform3 = "translateY(-100%)";
15236
15582
  if (pos === "start") return { left: plot.x + inset, top, width: plot.width - inset, align: "left", transform: transform3 };
@@ -15266,7 +15612,7 @@ function drawAnnotationLabels(surface, model) {
15266
15612
  const axis = ann.axis === "x" ? "x" : "y";
15267
15613
  const p = anchorPixel(model, ann, axis, kindOf(ann));
15268
15614
  if (p === void 0) continue;
15269
- box = labelBox(model, axis, ann.labelPosition ?? "end", p);
15615
+ box = labelBox(model, drawsHorizontalRule(model, axis), ann.labelPosition ?? "end", p);
15270
15616
  }
15271
15617
  const color = ann.color ?? tokens.color.text;
15272
15618
  const pill = { background: withAlpha4(tokens.color.background, 0.72), radius: 3, padX: 3, padY: 1 };
@@ -16087,7 +16433,8 @@ function checkClippedMarks(model, spec, add) {
16087
16433
  const pHi = model.y.pixel(hi);
16088
16434
  const pLo = model.y.pixel(lo);
16089
16435
  const EPS = 0.75;
16090
- if (pHi < top - EPS || pLo > bottom + EPS) {
16436
+ const clipped = model.orientation === "horizontal" ? pLo < model.plot.x - EPS || pHi > model.plot.x + model.plot.width + EPS : pHi < top - EPS || pLo > bottom + EPS;
16437
+ if (clipped) {
16091
16438
  add({
16092
16439
  code: "marks-clipped",
16093
16440
  severity: "warning",
@@ -16103,7 +16450,8 @@ function checkClippedMarks(model, spec, add) {
16103
16450
  function checkAxisLabelOverlap(model, spec, tokens, add) {
16104
16451
  const ticks2 = model.xTicks;
16105
16452
  if (ticks2.length < 2) return;
16106
- if (model.frame.xLabelAngle > 0) return;
16453
+ const horizontal = model.orientation === "horizontal";
16454
+ if (!horizontal && model.frame.xLabelAngle > 0) return;
16107
16455
  const font = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
16108
16456
  const GAP3 = 4;
16109
16457
  let collisions = 0;
@@ -16111,7 +16459,7 @@ function checkAxisLabelOverlap(model, spec, tokens, add) {
16111
16459
  const prev = ticks2[i - 1];
16112
16460
  const cur = ticks2[i];
16113
16461
  const spacing = Math.abs(cur.pos - prev.pos);
16114
- const half = (measureText(prev.label, font).width + measureText(cur.label, font).width) / 2;
16462
+ const half = horizontal ? tokens.font.size.small : (measureText(prev.label, font).width + measureText(cur.label, font).width) / 2;
16115
16463
  if (half + GAP3 > spacing) collisions++;
16116
16464
  }
16117
16465
  if (collisions > 0) {