graphein 0.8.0 → 0.9.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 +15 -0
- package/dist/index.js +45 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1114,6 +1114,13 @@ interface AxisConfig {
|
|
|
1114
1114
|
format?: string;
|
|
1115
1115
|
/** Show tick labels. */
|
|
1116
1116
|
labels?: boolean;
|
|
1117
|
+
/**
|
|
1118
|
+
* Rotation of x-axis tick labels in degrees: `0` horizontal, `45` diagonal,
|
|
1119
|
+
* `90` vertical. Omit for **auto** — categorical (bar/column) labels rotate to
|
|
1120
|
+
* 45° when there isn't room to show them all horizontally, so every category
|
|
1121
|
+
* stays visible instead of being thinned out. (X axis only.)
|
|
1122
|
+
*/
|
|
1123
|
+
labelAngle?: number;
|
|
1117
1124
|
}
|
|
1118
1125
|
interface AxesConfig {
|
|
1119
1126
|
x?: AxisConfig;
|
|
@@ -2877,6 +2884,12 @@ interface AxisInput {
|
|
|
2877
2884
|
* false/omitted for them.
|
|
2878
2885
|
*/
|
|
2879
2886
|
edgeAnchored?: boolean;
|
|
2887
|
+
/**
|
|
2888
|
+
* Explicit tick-label rotation in degrees (0/45/90). When omitted, categorical
|
|
2889
|
+
* (non-edge-anchored) labels auto-rotate to 45° if they wouldn't fit; the
|
|
2890
|
+
* resolved angle is returned on {@link Frame.xLabelAngle}.
|
|
2891
|
+
*/
|
|
2892
|
+
labelAngle?: number;
|
|
2880
2893
|
}
|
|
2881
2894
|
interface FrameInput {
|
|
2882
2895
|
width: number;
|
|
@@ -2909,6 +2922,8 @@ interface Frame {
|
|
|
2909
2922
|
legendRect?: Rect;
|
|
2910
2923
|
legendItems?: PositionedLegendItem[];
|
|
2911
2924
|
legendPosition?: LegendPosition;
|
|
2925
|
+
/** Resolved x tick-label rotation in degrees (0 = horizontal). */
|
|
2926
|
+
xLabelAngle: number;
|
|
2912
2927
|
}
|
|
2913
2928
|
declare const TICK_SIZE = 6;
|
|
2914
2929
|
/** Reserve space for every region and return the plot rect + positioned chrome. */
|
package/dist/index.js
CHANGED
|
@@ -3268,7 +3268,10 @@ function paintCanvasText(ctx, c) {
|
|
|
3268
3268
|
}
|
|
3269
3269
|
function overlayTextToCanvasCmd(o, font) {
|
|
3270
3270
|
const t = o.transform ?? "";
|
|
3271
|
-
const rotate = t.includes("rotate(-90deg)") ? -Math.PI / 2 : t.includes("rotate(90deg)") ? Math.PI / 2 :
|
|
3271
|
+
const rotate = t.includes("rotate(-90deg)") ? -Math.PI / 2 : t.includes("rotate(90deg)") ? Math.PI / 2 : (() => {
|
|
3272
|
+
const m = t.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
|
|
3273
|
+
return m ? parseFloat(m[1]) * Math.PI / 180 : void 0;
|
|
3274
|
+
})();
|
|
3272
3275
|
let align = "left";
|
|
3273
3276
|
let x = o.left;
|
|
3274
3277
|
const y = o.top;
|
|
@@ -5797,6 +5800,15 @@ var LEGEND_ITEM_GAP = 16;
|
|
|
5797
5800
|
var LEGEND_ROW_GAP = 6;
|
|
5798
5801
|
var LEGEND_PAD = 4;
|
|
5799
5802
|
var lineHeight = (size) => Math.round(size * 1.35);
|
|
5803
|
+
function resolveXLabelAngle(axis, available, font) {
|
|
5804
|
+
if (axis.labelAngle != null) return Math.max(0, Math.min(90, axis.labelAngle));
|
|
5805
|
+
const labels = axis.labels;
|
|
5806
|
+
if (axis.edgeAnchored || labels.length < 2 || available <= 0) return 0;
|
|
5807
|
+
const wfont = fontString(font.size.small, font.family, font.weight.normal);
|
|
5808
|
+
const widest = Math.max(...labels.map((l) => measureText(l, wfont).width));
|
|
5809
|
+
const needed = widest + 10;
|
|
5810
|
+
return available / labels.length >= needed ? 0 : 45;
|
|
5811
|
+
}
|
|
5800
5812
|
function legendItemWidth(label, font) {
|
|
5801
5813
|
const w = measureText(label, fontString(font.size.small, font.family, font.weight.normal)).width;
|
|
5802
5814
|
return LEGEND_SWATCH + LEGEND_SWATCH_GAP + Math.ceil(w);
|
|
@@ -5857,7 +5869,8 @@ function computeFrame(input) {
|
|
|
5857
5869
|
height,
|
|
5858
5870
|
originX: ox,
|
|
5859
5871
|
originY: oy,
|
|
5860
|
-
plot: { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) }
|
|
5872
|
+
plot: { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) },
|
|
5873
|
+
xLabelAngle: 0
|
|
5861
5874
|
};
|
|
5862
5875
|
if (input.title?.text) {
|
|
5863
5876
|
const titleH = lineHeight(font.size.title);
|
|
@@ -5922,10 +5935,21 @@ function computeFrame(input) {
|
|
|
5922
5935
|
}
|
|
5923
5936
|
}
|
|
5924
5937
|
if (input.xAxis?.show) {
|
|
5925
|
-
|
|
5938
|
+
const angle = resolveXLabelAngle(input.xAxis, right - left, font);
|
|
5939
|
+
frame.xLabelAngle = angle;
|
|
5940
|
+
const labelH = lineHeight(font.size.small);
|
|
5941
|
+
let labelBand = labelH;
|
|
5942
|
+
if (angle > 0 && input.xAxis.labels.length) {
|
|
5943
|
+
const wfont = fontString(font.size.small, font.family, font.weight.normal);
|
|
5944
|
+
const widest = Math.max(...input.xAxis.labels.map((l) => measureText(l, wfont).width));
|
|
5945
|
+
const cappedW = Math.min(widest, (bottom - top) * 0.4);
|
|
5946
|
+
const rad = angle * Math.PI / 180;
|
|
5947
|
+
labelBand = Math.ceil(cappedW * Math.sin(rad) + labelH * Math.cos(rad));
|
|
5948
|
+
}
|
|
5949
|
+
let gutter = labelBand + TICK_SIZE + AXIS_LABEL_GAP;
|
|
5926
5950
|
if (input.xAxis.title) gutter += lineHeight(font.size.base) + AXIS_TITLE_GAP;
|
|
5927
5951
|
bottom -= gutter;
|
|
5928
|
-
if (input.xAxis.edgeAnchored && input.xAxis.labels.length) {
|
|
5952
|
+
if (input.xAxis.edgeAnchored && angle === 0 && input.xAxis.labels.length) {
|
|
5929
5953
|
const wfont = fontString(font.size.small, font.family, font.weight.normal);
|
|
5930
5954
|
const labels = input.xAxis.labels;
|
|
5931
5955
|
const firstHalf = Math.ceil(measureText(labels[0], wfont).width / 2);
|
|
@@ -5980,6 +6004,7 @@ function addText(surface, font, o) {
|
|
|
5980
6004
|
el.style.textAlign = o.align ?? "left";
|
|
5981
6005
|
}
|
|
5982
6006
|
if (o.transform) el.style.transform = o.transform;
|
|
6007
|
+
if (o.transformOrigin) el.style.transformOrigin = o.transformOrigin;
|
|
5983
6008
|
el.style.pointerEvents = "none";
|
|
5984
6009
|
overlay.appendChild(el);
|
|
5985
6010
|
}
|
|
@@ -6115,8 +6140,18 @@ function placeXLabels(ticks2, originX, frameWidth, font) {
|
|
|
6115
6140
|
}
|
|
6116
6141
|
return boxes.filter((_, i) => keep[i]);
|
|
6117
6142
|
}
|
|
6143
|
+
function placeRotatedXLabels(ticks2, angle) {
|
|
6144
|
+
return ticks2.map((t) => ({
|
|
6145
|
+
text: t.label,
|
|
6146
|
+
pos: t.pos,
|
|
6147
|
+
left: t.pos,
|
|
6148
|
+
transform: `translateX(-100%) rotate(-${angle}deg)`
|
|
6149
|
+
}));
|
|
6150
|
+
}
|
|
6118
6151
|
function resolveXLabels(model) {
|
|
6119
6152
|
const f = model.tokens.font;
|
|
6153
|
+
const angle = model.frame.xLabelAngle;
|
|
6154
|
+
if (angle > 0) return placeRotatedXLabels(model.xTicks, angle);
|
|
6120
6155
|
const smallFont = fontString(f.size.small, f.family, f.weight.normal);
|
|
6121
6156
|
const thinned = thinXTicks(model, f.size.small * 0.58);
|
|
6122
6157
|
return placeXLabels(thinned, model.frame.originX, model.frame.width, smallFont);
|
|
@@ -6142,6 +6177,7 @@ function drawOverlay(surface, model) {
|
|
|
6142
6177
|
}
|
|
6143
6178
|
if (model.spec.axes?.x?.labels !== false) {
|
|
6144
6179
|
const top = plot.y + plot.height + TICK_SIZE + 3;
|
|
6180
|
+
const rotated = frame.xLabelAngle > 0;
|
|
6145
6181
|
for (const p of resolveXLabels(model)) {
|
|
6146
6182
|
addText(surface, smallFont, {
|
|
6147
6183
|
left: p.left,
|
|
@@ -6149,7 +6185,9 @@ function drawOverlay(surface, model) {
|
|
|
6149
6185
|
text: p.text,
|
|
6150
6186
|
color: tokens.color.textMuted,
|
|
6151
6187
|
size: f.size.small,
|
|
6152
|
-
|
|
6188
|
+
align: rotated ? "right" : void 0,
|
|
6189
|
+
transform: p.transform,
|
|
6190
|
+
transformOrigin: rotated ? "100% 0%" : void 0
|
|
6153
6191
|
});
|
|
6154
6192
|
}
|
|
6155
6193
|
}
|
|
@@ -6536,7 +6574,7 @@ function buildCartesianModel(spec, tokens, opts) {
|
|
|
6536
6574
|
font: tokens.font,
|
|
6537
6575
|
title: titleInput4,
|
|
6538
6576
|
legend: legend.show && legendItems.length > 1 ? { items: legendItems, position: legend.position } : void 0,
|
|
6539
|
-
xAxis: { show: xShow, labels: xLabels, title: resolveAxisTitle(enc.x, xAxisCfg), edgeAnchored: !categories },
|
|
6577
|
+
xAxis: { show: xShow, labels: xLabels, title: resolveAxisTitle(enc.x, xAxisCfg), edgeAnchored: !categories, labelAngle: xAxisCfg.labelAngle },
|
|
6540
6578
|
yAxis: { show: yShow, labels: yLabels, title: resolveAxisTitle(enc.y, yAxisCfg) }
|
|
6541
6579
|
});
|
|
6542
6580
|
const plot = frame.plot;
|
|
@@ -15702,6 +15740,7 @@ function checkClippedMarks(model, add) {
|
|
|
15702
15740
|
function checkAxisLabelOverlap(model, tokens, add) {
|
|
15703
15741
|
const ticks2 = model.xTicks;
|
|
15704
15742
|
if (ticks2.length < 2) return;
|
|
15743
|
+
if (model.frame.xLabelAngle > 0) return;
|
|
15705
15744
|
const font = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
|
|
15706
15745
|
const GAP3 = 4;
|
|
15707
15746
|
let collisions = 0;
|