graphein 0.10.0 → 0.11.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.d.ts CHANGED
@@ -1371,6 +1371,12 @@ interface BarSpec extends BaseSpec {
1371
1371
  x: FieldDef;
1372
1372
  y: FieldDef;
1373
1373
  };
1374
+ /**
1375
+ * Bar direction. `'vertical'` (default) draws columns growing up from the
1376
+ * x-axis; `'horizontal'` draws bars growing rightward from the y-axis, with the
1377
+ * `x` categories listed down the left — ideal when category names are long.
1378
+ * The `encoding` is unchanged either way (`x` = category, `y` = value).
1379
+ */
1374
1380
  orientation?: 'vertical' | 'horizontal';
1375
1381
  /** Stack series (default false). */
1376
1382
  stack?: boolean;
@@ -3100,6 +3106,23 @@ interface CartesianModel {
3100
3106
  series: ResolvedSeries[];
3101
3107
  seriesField?: string;
3102
3108
  stacked: boolean;
3109
+ /**
3110
+ * Bar orientation. Only `bar` ever sets `'horizontal'`; every other chart is
3111
+ * `'vertical'`. In horizontal mode the `x` (category/band) axis runs vertically
3112
+ * and the `y` (value) axis runs horizontally — so `x.pixel`/`x.bandwidth` are
3113
+ * vertical extents and `y.pixel`/`y.baseline` are horizontal ones. Consumers map
3114
+ * an (category-axis, value-axis) pixel pair to screen coords via {@link project}.
3115
+ */
3116
+ orientation: 'vertical' | 'horizontal';
3117
+ /**
3118
+ * Map a (category-axis pixel, value-axis pixel) pair to screen `{x, y}`,
3119
+ * accounting for orientation. Vertical is the identity; horizontal swaps the
3120
+ * pair (value → screen x, category → screen y).
3121
+ */
3122
+ project(catPixel: number, valPixel: number): {
3123
+ x: number;
3124
+ y: number;
3125
+ };
3103
3126
  xTicks: Tick[];
3104
3127
  yTicks: Tick[];
3105
3128
  colorOf(key: string): string;
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);
@@ -12454,6 +12579,7 @@ function indexInteraction(model) {
12454
12579
  const xFormat = model.spec.encoding.x?.format;
12455
12580
  const yFormat = model.spec.encoding.y?.format;
12456
12581
  const band = x.kind === "band";
12582
+ const horizontal = model.orientation === "horizontal";
12457
12583
  const showDots = !model.stacked && !band;
12458
12584
  const stopByKey = /* @__PURE__ */ new Map();
12459
12585
  const lookups = [];
@@ -12488,7 +12614,7 @@ function indexInteraction(model) {
12488
12614
  };
12489
12615
  const hitTest = (px, py) => {
12490
12616
  if (!inside(plot, px, py, 6)) return null;
12491
- const best = nearestStop(px);
12617
+ const best = nearestStop(horizontal ? py : px);
12492
12618
  const rows = [];
12493
12619
  const dots = [];
12494
12620
  let nearestDy = Infinity;
@@ -12516,15 +12642,19 @@ function indexInteraction(model) {
12516
12642
  const anchorY = showDots && nearestIdx >= 0 ? dots[nearestIdx].py : py;
12517
12643
  return {
12518
12644
  key: best.key,
12519
- anchorX: best.px,
12520
- anchorY,
12645
+ anchorX: horizontal ? px : best.px,
12646
+ anchorY: horizontal ? best.px : anchorY,
12521
12647
  content: { title: formatValue(best.value, xFormat), rows },
12522
12648
  draw: (ctx) => {
12523
12649
  ctx.save();
12524
12650
  if (band) {
12525
12651
  const half = x.bandwidth / 2;
12526
12652
  ctx.fillStyle = withAlpha(model.tokens.color.text, 0.06);
12527
- ctx.fillRect(best.px - half, plot.y, x.bandwidth, plot.height);
12653
+ if (horizontal) {
12654
+ ctx.fillRect(plot.x, best.px - half, plot.width, x.bandwidth);
12655
+ } else {
12656
+ ctx.fillRect(best.px - half, plot.y, x.bandwidth, plot.height);
12657
+ }
12528
12658
  } else {
12529
12659
  ctx.strokeStyle = withAlpha(model.tokens.color.text, 0.28);
12530
12660
  ctx.lineWidth = 1;
@@ -12553,7 +12683,7 @@ function indexInteraction(model) {
12553
12683
  };
12554
12684
  const pick = (px, py) => {
12555
12685
  if (!inside(plot, px, py, 6)) return null;
12556
- const best = nearestStop(px);
12686
+ const best = nearestStop(horizontal ? py : px);
12557
12687
  return { kind: "point", fields: [xField], tuples: [[best.value]] };
12558
12688
  };
12559
12689
  return { region: plot, hitTest, pick };
@@ -12920,6 +13050,8 @@ function buildComboModel(spec, tokens, opts) {
12920
13050
  series: [{ key: "", label: name, color, value: name, rows: data }],
12921
13051
  seriesField: void 0,
12922
13052
  stacked: false,
13053
+ orientation: "vertical",
13054
+ project: (c, v) => ({ x: c, y: v }),
12923
13055
  xTicks,
12924
13056
  yTicks: sideTicks,
12925
13057
  colorOf: () => color,
@@ -12947,6 +13079,8 @@ function buildComboModel(spec, tokens, opts) {
12947
13079
  series: [],
12948
13080
  seriesField: void 0,
12949
13081
  stacked: false,
13082
+ orientation: "vertical",
13083
+ project: (c, v) => ({ x: c, y: v }),
12950
13084
  xTicks,
12951
13085
  yTicks: leftTicks,
12952
13086
  colorOf: () => "",
@@ -13184,6 +13318,8 @@ function buildHistogramModel(spec, tokens, opts) {
13184
13318
  series: [],
13185
13319
  seriesField: void 0,
13186
13320
  stacked: false,
13321
+ orientation: "vertical",
13322
+ project: (c, v) => ({ x: c, y: v }),
13187
13323
  xTicks,
13188
13324
  yTicks,
13189
13325
  colorOf: () => color,
@@ -14387,6 +14523,8 @@ function buildWaterfallModel(spec, tokens, opts) {
14387
14523
  series: [],
14388
14524
  seriesField: void 0,
14389
14525
  stacked: false,
14526
+ orientation: "vertical",
14527
+ project: (c, v) => ({ x: c, y: v }),
14390
14528
  xTicks,
14391
14529
  yTicks,
14392
14530
  colorOf: () => incColor,
@@ -14648,6 +14786,8 @@ function buildSlopeModel(spec, tokens, opts) {
14648
14786
  series: [],
14649
14787
  seriesField: void 0,
14650
14788
  stacked: false,
14789
+ orientation: "vertical",
14790
+ project: (c, v) => ({ x: c, y: v }),
14651
14791
  xTicks,
14652
14792
  yTicks,
14653
14793
  colorOf: () => tokens.color.text,
@@ -15122,9 +15262,14 @@ function toPixel(model, axis, value) {
15122
15262
  return px != null && Number.isFinite(px) ? px : void 0;
15123
15263
  }
15124
15264
  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 };
15265
+ const cat = toPixel(model, "x", ann.x);
15266
+ const val = toPixel(model, "y", ann.y);
15267
+ if (cat === void 0 || val === void 0) return void 0;
15268
+ const { x, y } = model.project(cat, val);
15269
+ return { px: x, py: y };
15270
+ }
15271
+ function drawsHorizontalRule(model, axis) {
15272
+ return model.orientation === "horizontal" ? axis === "x" : axis === "y";
15128
15273
  }
15129
15274
  function anchorPixel(model, ann, axis, kind) {
15130
15275
  if (kind === "line") return toPixel(model, axis, ann.value);
@@ -15173,6 +15318,7 @@ function drawAnnotations(surface, model) {
15173
15318
  }
15174
15319
  const axis = ann.axis === "x" ? "x" : "y";
15175
15320
  const color = ann.color ?? tokens.color.textMuted;
15321
+ const ruleHorizontal = drawsHorizontalRule(model, axis);
15176
15322
  if (kindOf(ann) === "line") {
15177
15323
  const p = toPixel(model, axis, ann.value);
15178
15324
  if (p === void 0) continue;
@@ -15181,7 +15327,7 @@ function drawAnnotations(surface, model) {
15181
15327
  ctx.lineWidth = ann.strokeWidth ?? 1.5;
15182
15328
  ctx.setLineDash(ann.strokeDash ?? DEFAULT_DASH);
15183
15329
  ctx.beginPath();
15184
- if (axis === "y") {
15330
+ if (ruleHorizontal) {
15185
15331
  const y = crisp(p);
15186
15332
  ctx.moveTo(plot.x, y);
15187
15333
  ctx.lineTo(x1, y);
@@ -15202,7 +15348,7 @@ function drawAnnotations(surface, model) {
15202
15348
  ctx.save();
15203
15349
  ctx.globalAlpha = ann.fillOpacity ?? DEFAULT_FILL_OPACITY;
15204
15350
  ctx.fillStyle = color;
15205
- if (axis === "y") ctx.fillRect(plot.x, lo, plot.width, hi - lo);
15351
+ if (ruleHorizontal) ctx.fillRect(plot.x, lo, plot.width, hi - lo);
15206
15352
  else ctx.fillRect(lo, plot.y, hi - lo, plot.height);
15207
15353
  ctx.restore();
15208
15354
  ctx.save();
@@ -15211,7 +15357,7 @@ function drawAnnotations(surface, model) {
15211
15357
  ctx.lineWidth = 1;
15212
15358
  ctx.setLineDash(ann.strokeDash ?? [4, 4]);
15213
15359
  ctx.beginPath();
15214
- if (axis === "y") {
15360
+ if (ruleHorizontal) {
15215
15361
  ctx.moveTo(plot.x, crisp(lo));
15216
15362
  ctx.lineTo(x1, crisp(lo));
15217
15363
  ctx.moveTo(plot.x, crisp(hi));
@@ -15227,10 +15373,10 @@ function drawAnnotations(surface, model) {
15227
15373
  }
15228
15374
  ctx.restore();
15229
15375
  }
15230
- function labelBox(model, axis, pos, p) {
15376
+ function labelBox(model, horizontalRule, pos, p) {
15231
15377
  const { plot } = model;
15232
15378
  const inset = 6;
15233
- if (axis === "y") {
15379
+ if (horizontalRule) {
15234
15380
  const top = p - 3;
15235
15381
  const transform3 = "translateY(-100%)";
15236
15382
  if (pos === "start") return { left: plot.x + inset, top, width: plot.width - inset, align: "left", transform: transform3 };
@@ -15266,7 +15412,7 @@ function drawAnnotationLabels(surface, model) {
15266
15412
  const axis = ann.axis === "x" ? "x" : "y";
15267
15413
  const p = anchorPixel(model, ann, axis, kindOf(ann));
15268
15414
  if (p === void 0) continue;
15269
- box = labelBox(model, axis, ann.labelPosition ?? "end", p);
15415
+ box = labelBox(model, drawsHorizontalRule(model, axis), ann.labelPosition ?? "end", p);
15270
15416
  }
15271
15417
  const color = ann.color ?? tokens.color.text;
15272
15418
  const pill = { background: withAlpha4(tokens.color.background, 0.72), radius: 3, padX: 3, padY: 1 };
@@ -16087,7 +16233,8 @@ function checkClippedMarks(model, spec, add) {
16087
16233
  const pHi = model.y.pixel(hi);
16088
16234
  const pLo = model.y.pixel(lo);
16089
16235
  const EPS = 0.75;
16090
- if (pHi < top - EPS || pLo > bottom + EPS) {
16236
+ const clipped = model.orientation === "horizontal" ? pLo < model.plot.x - EPS || pHi > model.plot.x + model.plot.width + EPS : pHi < top - EPS || pLo > bottom + EPS;
16237
+ if (clipped) {
16091
16238
  add({
16092
16239
  code: "marks-clipped",
16093
16240
  severity: "warning",
@@ -16103,7 +16250,8 @@ function checkClippedMarks(model, spec, add) {
16103
16250
  function checkAxisLabelOverlap(model, spec, tokens, add) {
16104
16251
  const ticks2 = model.xTicks;
16105
16252
  if (ticks2.length < 2) return;
16106
- if (model.frame.xLabelAngle > 0) return;
16253
+ const horizontal = model.orientation === "horizontal";
16254
+ if (!horizontal && model.frame.xLabelAngle > 0) return;
16107
16255
  const font = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
16108
16256
  const GAP3 = 4;
16109
16257
  let collisions = 0;
@@ -16111,7 +16259,7 @@ function checkAxisLabelOverlap(model, spec, tokens, add) {
16111
16259
  const prev = ticks2[i - 1];
16112
16260
  const cur = ticks2[i];
16113
16261
  const spacing = Math.abs(cur.pos - prev.pos);
16114
- const half = (measureText(prev.label, font).width + measureText(cur.label, font).width) / 2;
16262
+ const half = horizontal ? tokens.font.size.small : (measureText(prev.label, font).width + measureText(cur.label, font).width) / 2;
16115
16263
  if (half + GAP3 > spacing) collisions++;
16116
16264
  }
16117
16265
  if (collisions > 0) {