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 +23 -0
- package/dist/index.js +206 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
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:
|
|
6810
|
-
yAxis:
|
|
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
|
|
6829
|
-
const yScale = yIsLog ? logScale({ domain: yDomain, range:
|
|
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
|
|
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
|
|
7285
|
-
if (!finite2(
|
|
7286
|
-
const
|
|
7287
|
-
const
|
|
7288
|
-
if (
|
|
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:
|
|
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
|
|
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
|
|
7315
|
-
const rect =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
15126
|
-
const
|
|
15127
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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,
|
|
15376
|
+
function labelBox(model, horizontalRule, pos, p) {
|
|
15231
15377
|
const { plot } = model;
|
|
15232
15378
|
const inset = 6;
|
|
15233
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|