Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/7965_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add `domainpad` to cartesian axes to reserve space in pixels inside the edges of `domain`, so the gap between subplots stays the same at all figure sizes [[#7965](https://github.com/plotly/plotly.js/pull/7965)], with thanks to @SharadhNaidu for the contribution!
61 changes: 45 additions & 16 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -2429,6 +2429,45 @@ function cleanPoint(d, hovermode) {
return d;
}

/**
* Get the pixel span of a spike with spikemode 'across'.
*
* @param gd - the graph div
* @param ax - the full axis that shows the spike
* @param gs - the plot size, `fullLayout._size`
* @returns [start, end] in pixels: left to right for a y axis spike, top to bottom for an x axis spike
*/
function getAcrossSpikeSpan(gd, ax, gs) {
const isY = ax._id.charAt(0) === 'y';
let start = Infinity;
let end = -Infinity;

for (const counterId of ax._counterAxes || []) {
const counterAx = Axes.getFromId(gd, counterId);
if (!counterAx) continue;

const padStart = counterAx._padStart || 0;
const padEnd = counterAx._padEnd || 0;

if (isY) {
start = Math.min(start, gs.l + counterAx.domain[0] * gs.w + padStart);
end = Math.max(end, gs.l + counterAx.domain[1] * gs.w - padEnd);
} else {
start = Math.min(start, gs.t + (1 - counterAx.domain[1]) * gs.h + padStart);
end = Math.max(end, gs.t + (1 - counterAx.domain[0]) * gs.h - padEnd);
}
}

// a free axis sits outside the plot areas, and the spike must also reach that axis
if (ax.anchor === 'free') {
const position = isY ? gs.l + ax.position * gs.w : gs.t + (1 - ax.position) * gs.h;
start = Math.min(start, position);
end = Math.max(end, position);
}

return [start, end];
}

function createSpikelines(gd, closestPoints, opts) {
var container = opts.container;
var fullLayout = opts.fullLayout;
Expand Down Expand Up @@ -2478,14 +2517,9 @@ function createSpikelines(gd, closestPoints, opts) {
xEndSpike = hLinePointX;
}
if (yMode.indexOf('across') !== -1) {
var xAcross0 = ya._counterDomainMin;
var xAcross1 = ya._counterDomainMax;
if (ya.anchor === 'free') {
xAcross0 = Math.min(xAcross0, ya.position);
xAcross1 = Math.max(xAcross1, ya.position);
}
xBase = gs.l + xAcross0 * gs.w;
xEndSpike = gs.l + xAcross1 * gs.w;
const xSpan = getAcrossSpikeSpan(gd, ya, gs);
xBase = xSpan[0];
xEndSpike = xSpan[1];
}

// Foreground horizontal line (to y-axis)
Expand Down Expand Up @@ -2563,14 +2597,9 @@ function createSpikelines(gd, closestPoints, opts) {
yEndSpike = vLinePointY;
}
if (xMode.indexOf('across') !== -1) {
var yAcross0 = xa._counterDomainMin;
var yAcross1 = xa._counterDomainMax;
if (xa.anchor === 'free') {
yAcross0 = Math.min(yAcross0, xa.position);
yAcross1 = Math.max(yAcross1, xa.position);
}
yBase = gs.t + (1 - yAcross1) * gs.h;
yEndSpike = gs.t + (1 - yAcross0) * gs.h;
const ySpan = getAcrossSpikeSpan(gd, xa, gs);
yBase = ySpan[0];
yEndSpike = ySpan[1];
}

// Foreground vertical line (to x-axis)
Expand Down
7 changes: 4 additions & 3 deletions src/components/rangeslider/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@ module.exports = function(gd) {
// update range slider dimensions

var gs = fullLayout._size;
var domain = axisOpts.domain;

opts._width = gs.w * (domain[1] - domain[0]);
// The slider must line up with the plot area above it. `domainpad` moves the
// plot area but not `domain`, so get the width and position from the axis.
opts._width = axisOpts._length;

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

var y = Math.round(
gs.t + gs.h * (1 - axisOpts._counterDomainMin) +
Expand Down
3 changes: 2 additions & 1 deletion src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4683,7 +4683,8 @@ function swapAxisGroup(gd, xIds, yIds) {
var allAxKeys = Object.keys(axAttrs);

var noSwapAttrs = [
'anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle', 'editType'
'anchor', 'domain', 'domainpad', 'overlaying', 'position', 'side',
'tickangle', 'editType'
];
var numericTypes = ['linear', 'log'];

Expand Down
28 changes: 23 additions & 5 deletions src/plots/cartesian/constraints.js
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ exports.enforce = function enforce(gd) {
var getPadMin = autorange.makePadFn(fullLayout, ax, 0);
var getPadMax = autorange.makePadFn(fullLayout, ax, 1);

updateDomain(ax, factor);
updateDomain(ax, factor, fullLayout);
var m = Math.abs(ax._m);
var extremes = autorange.concatExtremes(gd, ax);
var minArray = extremes.min;
Expand Down Expand Up @@ -596,7 +596,7 @@ exports.enforce = function enforce(gd) {
[rangeMin, rangeMax] : [rangeMax, rangeMin];
}

updateDomain(ax, factor);
updateDomain(ax, factor, fullLayout);
}
}
}
Expand Down Expand Up @@ -633,14 +633,32 @@ exports.clean = function clean(gd, ax) {
}
};

function updateDomain(ax, factor) {
function domainPadFraction(ax, fullLayout) {
const pad = ax.domainpad;
if(!pad) return 0;

const gs = fullLayout._size;
return ax._id.charAt(0) === 'y' ?
((pad.top || 0) + (pad.bottom || 0)) / gs.h :
((pad.left || 0) + (pad.right || 0)) / gs.w;
}

function updateDomain(ax, factor, fullLayout) {
var inputDomain = ax._inputDomain;
var centerFraction = FROM_BL[ax.constraintoward];
var center = inputDomain[0] + (inputDomain[1] - inputDomain[0]) * centerFraction;

let divisor = factor;
const padFraction = domainPadFraction(ax, fullLayout);
if(padFraction > 0) {
const inputSpan = inputDomain[1] - inputDomain[0];
const drawnSpan = inputSpan - padFraction;
if(drawnSpan > 0) divisor = inputSpan / (drawnSpan / factor + padFraction);
}

ax.domain = ax._input.domain = [
center + (inputDomain[0] - center) / factor,
center + (inputDomain[1] - center) / factor
center + (inputDomain[0] - center) / divisor,
center + (inputDomain[1] - center) / divisor
];
ax.setScale();
}
42 changes: 42 additions & 0 deletions src/plots/cartesian/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,48 @@ module.exports = {
editType: 'plot',
description: ['Sets the domain of this axis (in plot fraction).'].join(' ')
},
domainpad: {
left: {
valType: 'number',
min: 0,
dflt: 0,
editType: 'plot',
description: 'Sets the space in pixels to reserve inside the left edge of `domain`. Applies to x axes only.'
},
right: {
valType: 'number',
min: 0,
dflt: 0,
editType: 'plot',
description: 'Sets the space in pixels to reserve inside the right edge of `domain`. Applies to x axes only.'
},
top: {
valType: 'number',
min: 0,
dflt: 0,
editType: 'plot',
description: 'Sets the space in pixels to reserve inside the top edge of `domain`. Applies to y axes only.'
},
bottom: {
valType: 'number',
min: 0,
dflt: 0,
editType: 'plot',
description: 'Sets the space in pixels to reserve inside the bottom edge of `domain`. Applies to y axes only.'
},
editType: 'plot',
description: [
'Sets the space in pixels to reserve inside the edges of `domain`.',
'Because `domain` is a fraction of the plot area, the gap between subplots',
'changes with the figure size. `domainpad` keeps the same size in pixels at all figure sizes,',
'so it can hold content with a fixed size, such as a subplot title.',
'x axes use `left` and `right`. y axes use `top` and `bottom`.',
'If the padding needs more room than the domain has, the two pads shrink by the same factor,',
'and the plot area keeps a length of at least 2 pixels.',
'An axis that sets `overlaying` ignores its own `domainpad` and uses the `domainpad`',
'of the axis it overlays, so that both axes share the same plot area.'
].join(' ')
},
position: {
valType: 'number',
min: 0,
Expand Down
8 changes: 8 additions & 0 deletions src/plots/cartesian/position_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ module.exports = function handlePositionDefaults(containerIn, containerOut, coer
if(domain[0] > domain[1] - 1 / 4096) containerOut.domain = dfltDomain;
Lib.noneOrAll(containerIn.domain, containerOut.domain, dfltDomain);

if(letter === 'x') {
coerce('domainpad.left');
coerce('domainpad.right');
} else {
coerce('domainpad.top');
coerce('domainpad.bottom');
}

// tickmode sync needs an overlaying axis, otherwise
// we should default it to 'auto'
if(containerOut.tickmode === 'sync') {
Expand Down
41 changes: 37 additions & 4 deletions src/plots/cartesian/set_convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ function isValidCategory(v) {
return v !== null && v !== undefined;
}

const MIN_PADDED_LENGTH = 2;

/**
* Get the part of the requested `domainpad` that fits in a domain band.
*
* @param wanted - the total padding of the two edges, in pixels
* @param bandLength - the length of the domain band, in pixels
* @returns the factor from 0 to 1 to multiply each pad by
*/
function padFactor(wanted, bandLength) {
if(wanted <= 0) return 0;
const room = Math.max(0, bandLength - MIN_PADDED_LENGTH);
return wanted > room ? room / wanted : 1;
}

/**
* Define the conversion functions for an axis data is used in 5 ways:
*
Expand Down Expand Up @@ -562,6 +577,7 @@ module.exports = function setConvert(ax, fullLayout) {
if(ax.overlaying) {
var ax2 = axisIds.getFromId({ _fullLayout: fullLayout }, ax.overlaying);
ax.domain = ax2.domain;
ax.domainpad = ax2.domainpad;
}

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

var isY = axLetter === 'y';
// the length in pixels of the domain band, before `domainpad`
const bandLength = (isY ? gs.h : gs.w) * (ax.domain[1] - ax.domain[0]);
const pad = ax.domainpad;
let padStart = 0;
let padEnd = 0;

if(pad) {
padStart = (isY ? pad.top : pad.left) || 0;
padEnd = (isY ? pad.bottom : pad.right) || 0;

const fits = padFactor(padStart + padEnd, bandLength);
padStart *= fits;
padEnd *= fits;
}

ax._length = bandLength - padStart - padEnd;
ax._padStart = padStart;
ax._padEnd = padEnd;

if(isY) {
ax._offset = gs.t + (1 - ax.domain[1]) * gs.h;
ax._length = gs.h * (ax.domain[1] - ax.domain[0]);
ax._offset = gs.t + (1 - ax.domain[1]) * gs.h + padStart;
ax._m = ax._length / (rl0 - rl1);
ax._b = -ax._m * rl1;
} else {
ax._offset = gs.l + ax.domain[0] * gs.w;
ax._length = gs.w * (ax.domain[1] - ax.domain[0]);
ax._offset = gs.l + ax.domain[0] * gs.w + padStart;
ax._m = ax._length / (rl1 - rl0);
ax._b = -ax._m * rl0;
}
Expand Down
1 change: 0 additions & 1 deletion src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,6 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa
// find "full" domain span of counter axes,
// this loop can be costly, so only compute it when required
if(ax._counterAxes.length && (
(ax.spikemode && ax.spikemode.indexOf('across') !== -1) ||
(ax.automargin && ax.mirror && ax.anchor !== 'free') ||
Registry.getComponentMethod('rangeslider', 'isVisible')(ax)
)) {
Expand Down
14 changes: 10 additions & 4 deletions src/traces/scattergl/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,17 @@ function getViewport(fullLayout, xaxis, yaxis, plotGlPixelRatio) {
var t = gs.t * plotGlPixelRatio;
var w = gs.w * plotGlPixelRatio;
var h = gs.h * plotGlPixelRatio;

const padL = (xaxis._padStart || 0) * plotGlPixelRatio;
const padR = (xaxis._padEnd || 0) * plotGlPixelRatio;
const padT = (yaxis._padStart || 0) * plotGlPixelRatio;
const padB = (yaxis._padEnd || 0) * plotGlPixelRatio;

return [
l + xaxis.domain[0] * w,
b + yaxis.domain[0] * h,
(width - r) - (1 - xaxis.domain[1]) * w,
(height - t) - (1 - yaxis.domain[1]) * h
l + xaxis.domain[0] * w + padL,
b + yaxis.domain[0] * h + padB,
(width - r) - (1 - xaxis.domain[1]) * w - padR,
(height - t) - (1 - yaxis.domain[1]) * h - padT
];
}

Expand Down
2 changes: 1 addition & 1 deletion src/traces/splom/base_plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ function makeGridData(gd) {

// ya.l2p assumes top-to-bottom coordinate system (a la SVG),
// we need to compute bottom-to-top offsets and slopes:
var yOffset = gs.b + ya.domain[0] * gs.h;
const yOffset = gs.b + ya.domain[0] * gs.h + (ya._padEnd || 0);
var ym = -ya._m;
var yb = -ym * ya.r2l(ya.range[0], ya.calendar);
var x, y;
Expand Down
10 changes: 6 additions & 4 deletions src/traces/splom/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ function plotOne(gd, cd0) {
viewOpts.ranges = new Array(visibleLength);
viewOpts.domains = new Array(visibleLength);

const padFrac = (px, total) => (px || 0) / total;

for(k = 0; k < visibleDims.length; k++) {
i = visibleDims[k];

Expand All @@ -51,16 +53,16 @@ function plotOne(gd, cd0) {
if(xa) {
rng[0] = xa._rl[0];
rng[2] = xa._rl[1];
dmn[0] = xa.domain[0];
dmn[2] = xa.domain[1];
dmn[0] = xa.domain[0] + padFrac(xa._padStart, gs.w);
dmn[2] = xa.domain[1] - padFrac(xa._padEnd, gs.w);
}

ya = AxisIDs.getFromId(gd, trace._diag[i][1]);
if(ya) {
rng[1] = ya._rl[0];
rng[3] = ya._rl[1];
dmn[1] = ya.domain[0];
dmn[3] = ya.domain[1];
dmn[1] = ya.domain[0] + padFrac(ya._padEnd, gs.h);
dmn[3] = ya.domain[1] - padFrac(ya._padStart, gs.h);
}
}

Expand Down
27 changes: 27 additions & 0 deletions src/types/generated/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12199,6 +12199,33 @@ export interface LayoutAxis {
* @default [0,1]
*/
domain?: [number, number];
/** Sets the space in pixels to reserve inside the edges of `domain`. Because `domain` is a fraction of the plot area, the gap between subplots changes with the figure size. `domainpad` keeps the same size in pixels at all figure sizes, so it can hold content with a fixed size, such as a subplot title. x axes use `left` and `right`. y axes use `top` and `bottom`. If the padding needs more room than the domain has, the two pads shrink by the same factor, and the plot area keeps a length of at least 2 pixels. An axis that sets `overlaying` ignores its own `domainpad` and uses the `domainpad` of the axis it overlays, so that both axes share the same plot area. */
domainpad?: {
/**
* Sets the space in pixels to reserve inside the bottom edge of `domain`. Applies to y axes only.
* @default 0
* Minimum: 0
*/
bottom?: number;
/**
* Sets the space in pixels to reserve inside the left edge of `domain`. Applies to x axes only.
* @default 0
* Minimum: 0
*/
left?: number;
/**
* Sets the space in pixels to reserve inside the right edge of `domain`. Applies to x axes only.
* @default 0
* Minimum: 0
*/
right?: number;
/**
* Sets the space in pixels to reserve inside the top edge of `domain`. Applies to y axes only.
* @default 0
* Minimum: 0
*/
top?: number;
};
/**
* Sets the step in-between ticks on this axis. Use with `tick0`. Must be a positive number, or special strings available to *log* and *date* axes. If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n is the tick number. For example, to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1. To set tick marks at 1, 100, 10000, ... set dtick to 2. To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433. *log* has several special values; *L<f>*, where `f` is a positive number, gives ticks linearly spaced in value (but not position). For example `tick0` = 0.1, `dtick` = *L0.5* will put ticks at 0.1, 0.6, 1.1, 1.6 etc. To show powers of 10 plus small digits between, use *D1* (all digits) or *D2* (only 2 and 5). `tick0` is ignored for *D1* and *D2*. If the axis `type` is *date*, then you must convert the time to milliseconds. For example, to set the interval between ticks to one day, set `dtick` to 86400000.0. *date* also has special values *M<n>* gives ticks spaced by a number of months. `n` must be a positive integer. To set ticks on the 15th of every third month, set `tick0` to *2000-01-15* and `dtick` to *M3*. To set ticks every 4 years, set `dtick` to *M48*
* Setting this also sets: tickmode = "linear"
Expand Down
Binary file added test/image/baselines/axes_domainpad.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading