Skip to content

Commit 17928d2

Browse files
committed
auto-margin top and bottom (closes #1859)
fix a bug with inferred labels, which can be empty ("Date", "Year"… are filtered out). observe marginLeft and marginRight when specified in the axis mark (this wasn't tested and had lead to a regression)
1 parent 09ad2b6 commit 17928d2

File tree

7 files changed

+386
-16
lines changed

7 files changed

+386
-16
lines changed

src/dimensions.js

+33-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {isOrdinalScale} from "./scales.js";
44
import {offset} from "./style.js";
55
import {defaultWidth, monospaceWidth} from "./marks/text.js";
66
import {outerDimensions} from "./scales.js";
7+
import {formatAxisLabel} from "./marks/axis.js";
78

89
const marginMedium = 60;
910
const marginLarge = 90;
@@ -21,18 +22,28 @@ export function autoMarginK(
2122
dimensions,
2223
context
2324
) {
25+
const actualLabel = formatAxisLabel(scale, scales[scale], {...options, label});
2426
let {data, facets, channels} = stateByMark.get(mark);
2527
if (mark.initializer) ({channels} = mark.initializer(data, facets, {}, scales, dimensions, context));
26-
const width = mark.monospace ? monospaceWidth : defaultWidth;
27-
const labelPenalty =
28-
(label ?? scales[scale].label ?? "") !== "" &&
29-
(labelAnchor === "center" || (labelAnchor == null && scales[scale].bandwidth));
30-
const l =
31-
max(channels.text.value, (t) => (t ? width(`${t}`) : NaN)) * (mark.monospace ? 0.6 : 1) + (labelPenalty ? 100 : 0);
32-
const m = l >= 500 ? marginLarge : l >= 295 ? marginMedium : null;
28+
if (scale === "y" || scale === "fy") {
29+
const width = mark.monospace ? monospaceWidth : defaultWidth;
30+
const labelPenalty = actualLabel && (labelAnchor === "center" || (labelAnchor == null && scales[scale].bandwidth));
31+
const l =
32+
max(channels.text.value, (t) => (t ? width(`${t}`) : NaN)) * (mark.monospace ? 0.6 : 1) +
33+
(labelPenalty ? 100 : 0);
34+
const m = l >= 500 ? marginLarge : l >= 295 ? marginMedium : null;
35+
return m === null
36+
? options
37+
: scale === "fy"
38+
? {...options, facet: {[margin]: m, ...options.facet}}
39+
: {[margin]: m, ...options};
40+
}
41+
// For the x scale, we bump the margin only if the axis uses multi-line ticks!
42+
const re = new RegExp(/\n/);
43+
const m = actualLabel && channels.text.value.some((d) => re.test(d)) ? 40 : null;
3344
return m === null
3445
? options
35-
: scale === "fy"
46+
: scale === "fx"
3647
? {...options, facet: {[margin]: m, ...options.facet}}
3748
: {[margin]: m, ...options};
3849
}
@@ -53,9 +64,21 @@ export function createDimensions(scales, marks, options = {}) {
5364
// revise the dimensions if necessary.
5465
const autoMargins = [];
5566
for (const m of marks) {
56-
let {marginTop, marginRight, marginBottom, marginLeft, autoMarginRight, autoMarginLeft, frameAnchor} = m;
57-
if (autoMarginLeft && frameAnchor === "left") autoMargins.push(["marginLeft", autoMarginLeft, m]);
67+
let {
68+
marginTop,
69+
marginRight,
70+
marginBottom,
71+
marginLeft,
72+
autoMarginTop,
73+
autoMarginRight,
74+
autoMarginBottom,
75+
autoMarginLeft,
76+
frameAnchor
77+
} = m;
78+
if (autoMarginTop) autoMargins.push(["marginTop", autoMarginTop, m]);
5879
if (autoMarginRight && frameAnchor === "right") autoMargins.push(["marginRight", autoMarginRight, m]);
80+
if (autoMarginBottom) autoMargins.push(["marginBottom", autoMarginBottom, m]);
81+
if (autoMarginLeft && frameAnchor === "left") autoMargins.push(["marginLeft", autoMarginLeft, m]);
5982
if (marginTop > marginTopDefault) marginTopDefault = marginTop;
6083
if (marginRight > marginRightDefault) marginRightDefault = marginRight;
6184
if (marginBottom > marginBottomDefault) marginBottomDefault = marginBottom;

src/marks/axis.js

+20-6
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ function axisKy(
100100
marginRight === undefined &&
101101
anchor === "right" &&
102102
x == null && {scale: k, labelAnchor, label};
103+
marginRight ??= margin === undefined ? (anchor === "right" ? 40 : 0) : margin;
103104
const autoMarginLeft = margin === undefined &&
104105
marginLeft === undefined &&
105106
anchor === "left" &&
106107
x == null && {scale: k, labelAnchor, label};
107-
marginRight = margin === undefined ? (anchor === "right" ? 40 : 0) : margin;
108-
marginLeft = margin === undefined ? (anchor === "left" ? 40 : 0) : margin;
108+
marginLeft ??= margin === undefined ? (anchor === "left" ? 40 : 0) : margin;
109109
return marks(
110110
tickSize && !isNoneish(stroke)
111111
? axisTickKy(k, anchor, data, {
@@ -193,9 +193,9 @@ function axisKx(
193193
tickRotate,
194194
y,
195195
margin,
196-
marginTop = margin === undefined ? (anchor === "top" ? 30 : 0) : margin,
196+
marginTop,
197197
marginRight = margin === undefined ? 20 : margin,
198-
marginBottom = margin === undefined ? (anchor === "bottom" ? 30 : 0) : margin,
198+
marginBottom,
199199
marginLeft = margin === undefined ? 20 : margin,
200200
label,
201201
labelAnchor,
@@ -209,6 +209,16 @@ function axisKx(
209209
tickRotate = number(tickRotate);
210210
if (labelAnchor !== undefined) labelAnchor = keyword(labelAnchor, "labelAnchor", ["center", "left", "right"]);
211211
labelArrow = maybeLabelArrow(labelArrow);
212+
const autoMarginTop = margin === undefined &&
213+
marginTop === undefined &&
214+
anchor === "top" &&
215+
y == null && {scale: k, labelAnchor, label};
216+
marginTop ??= margin === undefined ? (anchor === "top" ? 30 : 0) : margin;
217+
const autoMarginBottom = margin === undefined &&
218+
marginBottom === undefined &&
219+
anchor === "bottom" &&
220+
y == null && {scale: k, labelAnchor, label};
221+
marginBottom ??= margin === undefined ? (anchor === "bottom" ? 30 : 0) : margin;
212222
return marks(
213223
tickSize && !isNoneish(stroke)
214224
? axisTickKx(k, anchor, data, {
@@ -238,6 +248,8 @@ function axisKx(
238248
marginRight,
239249
marginBottom,
240250
marginLeft,
251+
autoMarginTop,
252+
autoMarginBottom,
241253
...options
242254
})
243255
: null,
@@ -627,8 +639,10 @@ function axisMark(mark, k, anchor, ariaLabel, data, options, initialize) {
627639
channels = {};
628640
}
629641
m.ariaLabel = ariaLabel;
630-
m.autoMarginLeft = options.autoMarginLeft;
642+
m.autoMarginTop = options.autoMarginTop;
631643
m.autoMarginRight = options.autoMarginRight;
644+
m.autoMarginBottom = options.autoMarginBottom;
645+
m.autoMarginLeft = options.autoMarginLeft;
632646
if (m.clip === undefined) m.clip = false; // don’t clip axes by default
633647
return m;
634648
}
@@ -709,7 +723,7 @@ function inferScaleOrder(scale) {
709723

710724
// Takes the scale label, and if this is not an ordinal scale and the label was
711725
// inferred from an associated channel, adds an orientation-appropriate arrow.
712-
function formatAxisLabel(k, scale, {anchor, label = scale.label, labelAnchor, labelArrow} = {}) {
726+
export function formatAxisLabel(k, scale, {anchor, label = scale.label, labelAnchor, labelArrow} = {}) {
713727
if (label == null || (label.inferred && hasTemporalDomain(scale) && /^(date|time|year)$/i.test(label))) return;
714728
label = String(label); // coerce to a string after checking if inferred
715729
if (labelArrow === "auto") labelArrow = (!scale.bandwidth || scale.interval) && !/[]/.test(label);

test/output/aaplCloseAxisMargins.svg

+55
Loading

test/output/aaplCloseLabel.svg

+123
Loading

0 commit comments

Comments
 (0)