Skip to content

Commit 33bb9f7

Browse files
committed
add domainpad for pixel spacing inside axis domains
`domain` is a plot fraction, so the gap it leaves between subplots grows and shrinks with the figure while a subplot title does not. domainpad reserves a fixed number of pixels inside the domain edges instead, resolved at draw time. Applied in ax.setScale to _offset and _length rather than to ax.domain, so the domain keeps meaning what the user typed and anything referenced to `<axis> domain` follows the padded area without new drawing code. Along with it: - scattergl, splom and the rangeslider rebuilt the plot rect from `domain` and so did not see the pad; they now read _offset and _length - pads that do not fit are backed off together, since setScale already throws on a negative length - updateDomain scales the padded length rather than the raw span, so a scaleanchor ratio stays exact when the constraint shrinks a padded axis Refs #7835
1 parent 21da158 commit 33bb9f7

10 files changed

Lines changed: 664 additions & 35 deletions

File tree

src/components/rangeslider/draw.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,13 @@ module.exports = function(gd) {
100100
// update range slider dimensions
101101

102102
var gs = fullLayout._size;
103-
var domain = axisOpts.domain;
104103

105-
opts._width = gs.w * (domain[1] - domain[0]);
104+
// the slider has to line up with the plot area above it, so take its width
105+
// and position from the axis rather than working them out from `domain`
106+
// again - `domainpad` moves the axis without touching `domain`
107+
opts._width = axisOpts._length;
106108

107-
var x = Math.round(gs.l + (gs.w * domain[0]));
109+
var x = Math.round(axisOpts._offset);
108110

109111
var y = Math.round(
110112
gs.t + gs.h * (1 - axisOpts._counterDomainMin) +

src/plots/cartesian/constraints.js

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ exports.enforce = function enforce(gd) {
565565
var getPadMin = autorange.makePadFn(fullLayout, ax, 0);
566566
var getPadMax = autorange.makePadFn(fullLayout, ax, 1);
567567

568-
updateDomain(ax, factor);
568+
updateDomain(ax, factor, fullLayout);
569569
var m = Math.abs(ax._m);
570570
var extremes = autorange.concatExtremes(gd, ax);
571571
var minArray = extremes.min;
@@ -596,7 +596,7 @@ exports.enforce = function enforce(gd) {
596596
[rangeMin, rangeMax] : [rangeMax, rangeMin];
597597
}
598598

599-
updateDomain(ax, factor);
599+
updateDomain(ax, factor, fullLayout);
600600
}
601601
}
602602
}
@@ -633,14 +633,41 @@ exports.clean = function clean(gd, ax) {
633633
}
634634
};
635635

636-
function updateDomain(ax, factor) {
636+
// `domainpad` in domain fractions rather than pixels, which is the unit
637+
// everything around the constraint solve is expressed in.
638+
function domainPadFraction(ax, fullLayout) {
639+
var pad = ax.domainpad;
640+
if(!pad) return 0;
641+
642+
var gs = fullLayout._size;
643+
return ax._id.charAt(0) === 'y' ?
644+
((pad.top || 0) + (pad.bottom || 0)) / gs.h :
645+
((pad.left || 0) + (pad.right || 0)) / gs.w;
646+
}
647+
648+
function updateDomain(ax, factor, fullLayout) {
637649
var inputDomain = ax._inputDomain;
638-
var centerFraction = FROM_BL[ax.constraintoward];
639-
var center = inputDomain[0] + (inputDomain[1] - inputDomain[0]) * centerFraction;
650+
var inputSpan = inputDomain[1] - inputDomain[0];
651+
var center = inputDomain[0] + inputSpan * FROM_BL[ax.constraintoward];
652+
653+
// We have to divide the axis' drawn length by `factor`. With no padding that
654+
// length is the domain span, so dividing the span does it. `domainpad` breaks
655+
// the equivalence by taking a fixed number of pixels off the ends: the drawn
656+
// length is (span - padding), and only that part scales. Divide it and add the
657+
// padding back. If the padding covers the whole domain there is nothing left to
658+
// scale, so fall through to the plain behaviour and let setScale clamp it.
659+
var padFraction = domainPadFraction(ax, fullLayout);
660+
var drawnSpan = inputSpan - padFraction;
661+
var newSpan = drawnSpan > 0 ?
662+
drawnSpan / factor + padFraction :
663+
inputSpan / factor;
664+
665+
// grow or shrink about whichever edge or centre `constraintoward` asked for
666+
var scale = newSpan / inputSpan;
640667

641668
ax.domain = ax._input.domain = [
642-
center + (inputDomain[0] - center) / factor,
643-
center + (inputDomain[1] - center) / factor
669+
center + (inputDomain[0] - center) * scale,
670+
center + (inputDomain[1] - center) * scale
644671
];
645672
ax.setScale();
646673
}

src/plots/cartesian/layout_attributes.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,47 @@ module.exports = {
12051205
'Sets the domain of this axis (in plot fraction).'
12061206
].join(' ')
12071207
},
1208+
domainpad: {
1209+
left: {
1210+
valType: 'number',
1211+
min: 0,
1212+
dflt: 0,
1213+
editType: 'plot',
1214+
description: 'Pixels of space to reserve inside the left edge of the domain. Ignored on y axes.'
1215+
},
1216+
right: {
1217+
valType: 'number',
1218+
min: 0,
1219+
dflt: 0,
1220+
editType: 'plot',
1221+
description: 'Pixels of space to reserve inside the right edge of the domain. Ignored on y axes.'
1222+
},
1223+
top: {
1224+
valType: 'number',
1225+
min: 0,
1226+
dflt: 0,
1227+
editType: 'plot',
1228+
description: 'Pixels of space to reserve inside the top edge of the domain. Ignored on x axes.'
1229+
},
1230+
bottom: {
1231+
valType: 'number',
1232+
min: 0,
1233+
dflt: 0,
1234+
editType: 'plot',
1235+
description: 'Pixels of space to reserve inside the bottom edge of the domain. Ignored on x axes.'
1236+
},
1237+
editType: 'plot',
1238+
description: [
1239+
'Reserves space inside the edges of `domain`, in pixels.',
1240+
'Because `domain` is a plot fraction, the space it leaves between subplots',
1241+
'grows and shrinks with the figure. `domainpad` stays the same size at any',
1242+
'figure height or width, which is what you want for anything sized in pixels',
1243+
'such as a subplot title.',
1244+
'x axes use `left` and `right`, y axes use `top` and `bottom`.',
1245+
'If the padding asks for more room than the domain has, it is scaled down',
1246+
'so the subplot keeps a usable size.'
1247+
].join(' ')
1248+
},
12081249
position: {
12091250
valType: 'number',
12101251
min: 0,

src/plots/cartesian/position_defaults.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ module.exports = function handlePositionDefaults(containerIn, containerOut, coer
8484
if(domain[0] > domain[1] - 1 / 4096) containerOut.domain = dfltDomain;
8585
Lib.noneOrAll(containerIn.domain, containerOut.domain, dfltDomain);
8686

87+
// domainpad reserves pixels inside the domain edges. Only the two sides that
88+
// point along this axis mean anything, so we skip the other two rather than
89+
// let people set a value that silently does nothing.
90+
if(letter === 'x') {
91+
coerce('domainpad.left');
92+
coerce('domainpad.right');
93+
} else {
94+
coerce('domainpad.top');
95+
coerce('domainpad.bottom');
96+
}
97+
8798
// tickmode sync needs an overlaying axis, otherwise
8899
// we should default it to 'auto'
89100
if(containerOut.tickmode === 'sync') {

src/plots/cartesian/set_convert.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,27 @@ function isValidCategory(v) {
3535
return v !== null && v !== undefined;
3636
}
3737

38+
// Smallest plot area `domainpad` may leave behind, matching the floor
39+
// `doAutoMargin` keeps for margins. Zero would not do: the guard at the end of
40+
// setScale only rejects lengths below zero, so an exactly-zero length slips
41+
// through with a slope of 0 and the subplot collapses without saying why.
42+
var MIN_PADDED_LENGTH = 2;
43+
44+
/*
45+
* What fraction of the requested `domainpad` actually fits.
46+
*
47+
* `domain` is a plot fraction, so the band it covers shrinks with the figure while
48+
* the padding does not. Make the figure small enough and the two pads together ask
49+
* for more room than the band has. Back both off by the same factor rather than
50+
* hand setScale a negative length, which throws - the same way `doAutoMargin`
51+
* shrinks margins that no longer fit.
52+
*/
53+
function padFactor(wanted, bandLength) {
54+
if(wanted <= 0) return 0;
55+
var room = Math.max(0, bandLength - MIN_PADDED_LENGTH);
56+
return wanted > room ? room / wanted : 1;
57+
}
58+
3859
/**
3960
* Define the conversion functions for an axis data is used in 5 ways:
4061
*
@@ -562,6 +583,9 @@ module.exports = function setConvert(ax, fullLayout) {
562583
if(ax.overlaying) {
563584
var ax2 = axisIds.getFromId({ _fullLayout: fullLayout }, ax.overlaying);
564585
ax.domain = ax2.domain;
586+
// an overlaying axis has to sit on exactly the same plot area as the axis
587+
// underneath it, so it takes that axis' padding along with its domain
588+
ax.domainpad = ax2.domainpad;
565589
}
566590

567591
// While transitions are occurring, we get a double-transform
@@ -576,14 +600,31 @@ module.exports = function setConvert(ax, fullLayout) {
576600
var rl1 = ax.r2l(ax[rangeAttr][1], calendar);
577601

578602
var isY = axLetter === 'y';
603+
// the band `domain` covers, before domainpad takes its share of it
604+
var bandLength = (isY ? gs.h : gs.w) * (ax.domain[1] - ax.domain[0]);
605+
var pad = ax.domainpad;
606+
// padStart is the edge _offset is measured from, the top for y and the left
607+
// for x, so it is the one that pushes the plot area inwards
608+
var padStart = 0;
609+
var padEnd = 0;
610+
611+
if(pad) {
612+
padStart = (isY ? pad.top : pad.left) || 0;
613+
padEnd = (isY ? pad.bottom : pad.right) || 0;
614+
615+
var fits = padFactor(padStart + padEnd, bandLength);
616+
padStart *= fits;
617+
padEnd *= fits;
618+
}
619+
620+
ax._length = bandLength - padStart - padEnd;
621+
579622
if(isY) {
580-
ax._offset = gs.t + (1 - ax.domain[1]) * gs.h;
581-
ax._length = gs.h * (ax.domain[1] - ax.domain[0]);
623+
ax._offset = gs.t + (1 - ax.domain[1]) * gs.h + padStart;
582624
ax._m = ax._length / (rl0 - rl1);
583625
ax._b = -ax._m * rl1;
584626
} else {
585-
ax._offset = gs.l + ax.domain[0] * gs.w;
586-
ax._length = gs.w * (ax.domain[1] - ax.domain[0]);
627+
ax._offset = gs.l + ax.domain[0] * gs.w + padStart;
587628
ax._m = ax._length / (rl1 - rl0);
588629
ax._b = -ax._m * rl0;
589630
}

src/traces/scattergl/plot.js

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,22 @@ var styleTextSelection = require('./edit_style').styleTextSelection;
1717
var reglPrecompiled = {};
1818

1919
function getViewport(fullLayout, xaxis, yaxis, plotGlPixelRatio) {
20-
var gs = fullLayout._size;
21-
var width = fullLayout.width * plotGlPixelRatio;
22-
var height = fullLayout.height * plotGlPixelRatio;
23-
24-
var l = gs.l * plotGlPixelRatio;
25-
var b = gs.b * plotGlPixelRatio;
26-
var r = gs.r * plotGlPixelRatio;
27-
var t = gs.t * plotGlPixelRatio;
28-
var w = gs.w * plotGlPixelRatio;
29-
var h = gs.h * plotGlPixelRatio;
20+
// This is the same rectangle the svg side draws into, only measured up from the
21+
// bottom of the figure instead of down from the top. Read it off _offset and
22+
// _length rather than working it out from `domain` again, so that anything which
23+
// moves the plot area by a pixel amount - `domainpad`, say - lands here as well.
24+
var height = fullLayout.height;
25+
26+
var left = xaxis._offset;
27+
var right = xaxis._offset + xaxis._length;
28+
var bottom = height - (yaxis._offset + yaxis._length);
29+
var top = height - yaxis._offset;
30+
3031
return [
31-
l + xaxis.domain[0] * w,
32-
b + yaxis.domain[0] * h,
33-
(width - r) - (1 - xaxis.domain[1]) * w,
34-
(height - t) - (1 - yaxis.domain[1]) * h
32+
left * plotGlPixelRatio,
33+
bottom * plotGlPixelRatio,
34+
right * plotGlPixelRatio,
35+
top * plotGlPixelRatio
3536
];
3637
}
3738

src/traces/splom/base_plot.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ function updateGrid(gd) {
9090
function makeGridData(gd) {
9191
var plotGlPixelRatio = gd._context.plotGlPixelRatio;
9292
var fullLayout = gd._fullLayout;
93-
var gs = fullLayout._size;
9493
var fullView = [
9594
0, 0,
9695
fullLayout.width * plotGlPixelRatio,
@@ -135,8 +134,10 @@ function makeGridData(gd) {
135134
var yLength = ya._length;
136135

137136
// ya.l2p assumes top-to-bottom coordinate system (a la SVG),
138-
// we need to compute bottom-to-top offsets and slopes:
139-
var yOffset = gs.b + ya.domain[0] * gs.h;
137+
// we need to compute bottom-to-top offsets and slopes.
138+
// Flip the axis' own bottom edge rather than rebuilding it from `domain`,
139+
// so pixel adjustments to the plot area such as `domainpad` come along:
140+
var yOffset = fullLayout.height - (ya._offset + ya._length);
140141
var ym = -ya._m;
141142
var yb = -ym * ya.r2l(ya.range[0], ya.calendar);
142143
var x, y;

src/traces/splom/plot.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ function plotOne(gd, cd0) {
4141
viewOpts.ranges = new Array(visibleLength);
4242
viewOpts.domains = new Array(visibleLength);
4343

44+
// regl-splom places each cell as a fraction of the viewport below, which is the
45+
// whole plot area. Derive those fractions from where the axes actually ended up
46+
// rather than from `domain`, otherwise anything that shifts the plot area in
47+
// pixels - `domainpad` - would move the axes but leave the points behind.
48+
// regl counts y up from the bottom, so the y pair comes back reversed.
49+
function xFraction(px) { return (px - gs.l) / gs.w; }
50+
function yFraction(px) { return (fullLayout.height - px - gs.b) / gs.h; }
51+
4452
for(k = 0; k < visibleDims.length; k++) {
4553
i = visibleDims[k];
4654

@@ -51,16 +59,16 @@ function plotOne(gd, cd0) {
5159
if(xa) {
5260
rng[0] = xa._rl[0];
5361
rng[2] = xa._rl[1];
54-
dmn[0] = xa.domain[0];
55-
dmn[2] = xa.domain[1];
62+
dmn[0] = xFraction(xa._offset);
63+
dmn[2] = xFraction(xa._offset + xa._length);
5664
}
5765

5866
ya = AxisIDs.getFromId(gd, trace._diag[i][1]);
5967
if(ya) {
6068
rng[1] = ya._rl[0];
6169
rng[3] = ya._rl[1];
62-
dmn[1] = ya.domain[0];
63-
dmn[3] = ya.domain[1];
70+
dmn[1] = yFraction(ya._offset + ya._length);
71+
dmn[3] = yFraction(ya._offset);
6472
}
6573
}
6674

0 commit comments

Comments
 (0)