Skip to content

Commit 08f3f04

Browse files
authored
Merge pull request #5275 from plotly/ticklabelposition
Implement ticklabelposition for cartesian subplots and colorbars
2 parents 5700db2 + 40c09b7 commit 08f3f04

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1764
-113
lines changed

src/components/colorbar/attributes.js

+13
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,19 @@ module.exports = overrideAll({
157157
tickvals: axesAttrs.tickvals,
158158
ticktext: axesAttrs.ticktext,
159159
ticks: extendFlat({}, axesAttrs.ticks, {dflt: ''}),
160+
ticklabelposition: {
161+
valType: 'enumerated',
162+
values: [
163+
'outside', 'inside',
164+
'outside top', 'inside top',
165+
'outside bottom', 'inside bottom'
166+
],
167+
dflt: 'outside',
168+
role: 'info',
169+
description: [
170+
'Determines where tick labels are drawn.'
171+
].join(' ')
172+
},
160173
ticklen: axesAttrs.ticklen,
161174
tickwidth: axesAttrs.tickwidth,
162175
tickcolor: axesAttrs.tickcolor,

src/components/colorbar/defaults.js

+4
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,14 @@ module.exports = function colorbarDefaults(containerIn, containerOut, layout) {
5151
coerce('bordercolor');
5252
coerce('borderwidth');
5353
coerce('bgcolor');
54+
var ticklabelposition = coerce('ticklabelposition');
5455

5556
handleTickValueDefaults(colorbarIn, colorbarOut, coerce, 'linear');
5657

5758
var opts = {outerTicks: false, font: layout.font};
59+
if(ticklabelposition.indexOf('inside') !== -1) {
60+
opts.bgColor = 'black'; // could we instead use the average of colors in the scale?
61+
}
5862
handleTickLabelDefaults(colorbarIn, colorbarOut, coerce, 'linear', opts);
5963
handleTickMarkDefaults(colorbarIn, colorbarOut, coerce, 'linear', opts);
6064

src/components/colorbar/draw.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -462,20 +462,19 @@ function drawColorBar(g, opts, gd) {
462462
(opts.outlinewidth || 0) / 2 - (opts.ticks === 'outside' ? 1 : 0);
463463

464464
var vals = Axes.calcTicks(ax);
465-
var transFn = Axes.makeTransFn(ax);
466465
var tickSign = Axes.getTickSigns(ax)[2];
467466

468467
Axes.drawTicks(gd, ax, {
469468
vals: ax.ticks === 'inside' ? Axes.clipEnds(ax, vals) : vals,
470469
layer: axLayer,
471470
path: Axes.makeTickPath(ax, shift, tickSign),
472-
transFn: transFn
471+
transFn: Axes.makeTransTickFn(ax)
473472
});
474473

475474
return Axes.drawLabels(gd, ax, {
476475
vals: vals,
477476
layer: axLayer,
478-
transFn: transFn,
477+
transFn: Axes.makeTransTickLabelFn(ax),
479478
labelFns: Axes.makeLabelFns(ax, shift)
480479
});
481480
}
@@ -485,7 +484,11 @@ function drawColorBar(g, opts, gd) {
485484
// TODO: why are we redrawing multiple times now with this?
486485
// I guess autoMargin doesn't like being post-promise?
487486
function positionCB() {
488-
var innerWidth = thickPx + opts.outlinewidth / 2 + Drawing.bBox(axLayer.node()).width;
487+
var innerWidth = thickPx + opts.outlinewidth / 2;
488+
if(ax.ticklabelposition.indexOf('inside') === -1) {
489+
innerWidth += Drawing.bBox(axLayer.node()).width;
490+
}
491+
489492
titleEl = titleCont.select('text');
490493

491494
if(titleEl.node() && !titleEl.classed(cn.jsPlaceholder)) {
@@ -681,6 +684,7 @@ function mockColorBarAxis(gd, opts, zrange) {
681684
tickwidth: opts.tickwidth,
682685
tickcolor: opts.tickcolor,
683686
showticklabels: opts.showticklabels,
687+
ticklabelposition: opts.ticklabelposition,
684688
tickfont: opts.tickfont,
685689
tickangle: opts.tickangle,
686690
tickformat: opts.tickformat,

src/plot_api/plot_api.js

+27-3
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,18 @@ function plot(gd, data, layout, config) {
367367
if(hasCartesian) seq.push(positionAndAutorange);
368368

369369
seq.push(subroutines.layoutStyles);
370-
if(hasCartesian) seq.push(drawAxes);
370+
if(hasCartesian) {
371+
seq.push(
372+
drawAxes,
373+
function insideTickLabelsAutorange(gd) {
374+
if(gd._fullLayout._insideTickLabelsAutorange) {
375+
relayout(gd, gd._fullLayout._insideTickLabelsAutorange).then(function() {
376+
gd._fullLayout._insideTickLabelsAutorange = undefined;
377+
});
378+
}
379+
}
380+
);
381+
}
371382

372383
seq.push(
373384
subroutines.drawData,
@@ -381,9 +392,16 @@ function plot(gd, data, layout, config) {
381392
// calculated. Would be much better to separate margin calculations from
382393
// component drawing - see https://github.com/plotly/plotly.js/issues/2704
383394
Plots.doAutoMargin,
395+
saveRangeInitialForInsideTickLabels,
384396
Plots.previousPromises
385397
);
386398

399+
function saveRangeInitialForInsideTickLabels(gd) {
400+
if(gd._fullLayout._insideTickLabelsAutorange) {
401+
if(graphWasEmpty) Axes.saveRangeInitial(gd, true);
402+
}
403+
}
404+
387405
// even if everything we did was synchronous, return a promise
388406
// so that the caller doesn't care which route we took
389407
var plotDone = Lib.syncOrAsync(seq, gd);
@@ -1961,6 +1979,12 @@ function addAxRangeSequence(seq, rangesAltered) {
19611979
var ax = Axes.getFromId(gd, id);
19621980
axIds.push(id);
19631981

1982+
if((ax.ticklabelposition || '').indexOf('inside') !== -1) {
1983+
if(ax._anchorAxis) {
1984+
axIds.push(ax._anchorAxis._id);
1985+
}
1986+
}
1987+
19641988
if(ax._matchGroup) {
19651989
for(var id2 in ax._matchGroup) {
19661990
if(!rangesAltered[id2]) {
@@ -2055,7 +2079,7 @@ function _relayout(gd, aobj) {
20552079
// we're editing the (auto)range of, so we can tell the others constrained
20562080
// to scale with them that it's OK for them to shrink
20572081
var rangesAltered = {};
2058-
var axId, ax;
2082+
var ax;
20592083

20602084
function recordAlteredAxis(pleafPlus) {
20612085
var axId = Axes.name2id(pleafPlus.split('.')[0]);
@@ -2283,7 +2307,7 @@ function _relayout(gd, aobj) {
22832307
}
22842308

22852309
// figure out if we need to recalculate axis constraints
2286-
for(axId in rangesAltered) {
2310+
for(var axId in rangesAltered) {
22872311
ax = Axes.getFromId(gd, axId);
22882312
var group = ax && ax._constraintGroup;
22892313
if(group) {

src/plot_api/subroutines.js

+1
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,7 @@ exports.doAutoRangeAndConstraints = function(gd) {
674674

675675
for(var i = 0; i < axList.length; i++) {
676676
ax = axList[i];
677+
677678
if(!autoRangeDone[ax._id]) {
678679
autoRangeDone[ax._id] = 1;
679680
cleanAxisConstraints(gd, ax);

src/plots/cartesian/autorange.js

+132-26
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ function getAutoRange(gd, ax) {
5656
var i, j;
5757
var newRange = [];
5858

59-
var getPad = makePadFn(ax);
59+
var getPadMin = makePadFn(ax, 0);
60+
var getPadMax = makePadFn(ax, 1);
6061
var extremes = concatExtremes(gd, ax);
6162
var minArray = extremes.min;
6263
var maxArray = extremes.max;
@@ -97,29 +98,16 @@ function getAutoRange(gd, ax) {
9798
// don't allow padding to reduce the data to < 10% of the length
9899
var minSpan = axLen / 10;
99100

100-
// find axis rangebreaks in [v0,v1] and compute its length in value space
101-
var calcBreaksLength = function(v0, v1) {
102-
var lBreaks = 0;
103-
if(ax.rangebreaks) {
104-
var rangebreaksOut = ax.locateBreaks(v0, v1);
105-
for(var i = 0; i < rangebreaksOut.length; i++) {
106-
var brk = rangebreaksOut[i];
107-
lBreaks += brk.max - brk.min;
108-
}
109-
}
110-
return lBreaks;
111-
};
112-
113101
var mbest = 0;
114102
var minpt, maxpt, minbest, maxbest, dp, dv;
115103

116104
for(i = 0; i < minArray.length; i++) {
117105
minpt = minArray[i];
118106
for(j = 0; j < maxArray.length; j++) {
119107
maxpt = maxArray[j];
120-
dv = maxpt.val - minpt.val - calcBreaksLength(minpt.val, maxpt.val);
108+
dv = maxpt.val - minpt.val - calcBreaksLength(ax, minpt.val, maxpt.val);
121109
if(dv > 0) {
122-
dp = axLen - getPad(minpt) - getPad(maxpt);
110+
dp = axLen - getPadMin(minpt) - getPadMax(maxpt);
123111
if(dp > minSpan) {
124112
if(dv / dp > mbest) {
125113
minbest = minpt;
@@ -137,8 +125,8 @@ function getAutoRange(gd, ax) {
137125
}
138126
}
139127

140-
function getMaxPad(prev, pt) {
141-
return Math.max(prev, getPad(pt));
128+
function maximumPad(prev, pt) {
129+
return Math.max(prev, getPadMax(pt));
142130
}
143131

144132
if(minmin === maxmax) {
@@ -152,7 +140,7 @@ function getAutoRange(gd, ax) {
152140
// 'tozero' pins 0 to the low end, so follow that.
153141
newRange = [0, 1];
154142
} else {
155-
var maxPad = (minmin > 0 ? maxArray : minArray).reduce(getMaxPad, 0);
143+
var maxPad = (minmin > 0 ? maxArray : minArray).reduce(maximumPad, 0);
156144
// we're pushing a single value away from the edge due to its
157145
// padding, with the other end clamped at zero
158146
// 0.5 means don't push it farther than the center.
@@ -173,7 +161,7 @@ function getAutoRange(gd, ax) {
173161
maxbest = {val: 0, pad: 0};
174162
}
175163
} else if(nonNegative) {
176-
if(minbest.val - mbest * getPad(minbest) < 0) {
164+
if(minbest.val - mbest * getPadMin(minbest) < 0) {
177165
minbest = {val: 0, pad: 0};
178166
}
179167
if(maxbest.val <= 0) {
@@ -182,12 +170,12 @@ function getAutoRange(gd, ax) {
182170
}
183171

184172
// in case it changed again...
185-
mbest = (maxbest.val - minbest.val - calcBreaksLength(minpt.val, maxpt.val)) /
186-
(axLen - getPad(minbest) - getPad(maxbest));
173+
mbest = (maxbest.val - minbest.val - calcBreaksLength(ax, minpt.val, maxpt.val)) /
174+
(axLen - getPadMin(minbest) - getPadMax(maxbest));
187175

188176
newRange = [
189-
minbest.val - mbest * getPad(minbest),
190-
maxbest.val + mbest * getPad(maxbest)
177+
minbest.val - mbest * getPadMin(minbest),
178+
maxbest.val + mbest * getPadMax(maxbest)
191179
];
192180
}
193181

@@ -197,13 +185,41 @@ function getAutoRange(gd, ax) {
197185
return Lib.simpleMap(newRange, ax.l2r || Number);
198186
}
199187

188+
// find axis rangebreaks in [v0,v1] and compute its length in value space
189+
function calcBreaksLength(ax, v0, v1) {
190+
var lBreaks = 0;
191+
if(ax.rangebreaks) {
192+
var rangebreaksOut = ax.locateBreaks(v0, v1);
193+
for(var i = 0; i < rangebreaksOut.length; i++) {
194+
var brk = rangebreaksOut[i];
195+
lBreaks += brk.max - brk.min;
196+
}
197+
}
198+
return lBreaks;
199+
}
200+
200201
/*
201202
* calculate the pixel padding for ax._min and ax._max entries with
202203
* optional extrapad as 5% of the total axis length
203204
*/
204-
function makePadFn(ax) {
205+
function makePadFn(ax, max) {
205206
// 5% padding for points that specify extrapad: true
206-
var extrappad = ax._length / 20;
207+
var extrappad = 0.05 * ax._length;
208+
209+
if(
210+
(ax.ticklabelposition || '').indexOf('inside') !== -1 ||
211+
((ax._anchorAxis || {}).ticklabelposition || '').indexOf('inside') !== -1
212+
) {
213+
var axReverse = ax.autorange === 'reversed';
214+
if(!axReverse) {
215+
var rng = Lib.simpleMap(ax.range, ax.r2l);
216+
axReverse = rng[1] < rng[0];
217+
}
218+
if(axReverse) max = !max;
219+
}
220+
221+
extrappad = adjustPadForInsideLabelsOnAnchorAxis(extrappad, ax, max);
222+
extrappad = adjustPadForInsideLabelsOnThisAxis(extrappad, ax, max);
207223

208224
// domain-constrained axes: base extrappad on the unconstrained
209225
// domain so it's consistent as the domain changes
@@ -215,6 +231,96 @@ function makePadFn(ax) {
215231
return function getPad(pt) { return pt.pad + (pt.extrapad ? extrappad : 0); };
216232
}
217233

234+
var TEXTPAD = 3;
235+
236+
function adjustPadForInsideLabelsOnThisAxis(extrappad, ax, max) {
237+
var ticklabelposition = ax.ticklabelposition || '';
238+
var has = function(str) {
239+
return ticklabelposition.indexOf(str) !== -1;
240+
};
241+
242+
if(!has('inside')) return extrappad;
243+
var isTop = has('top');
244+
var isLeft = has('left');
245+
var isRight = has('right');
246+
var isBottom = has('bottom');
247+
var isAligned = isBottom || isLeft || isTop || isRight;
248+
249+
if(
250+
(max && (isLeft || isBottom)) ||
251+
(!max && (isRight || isTop))
252+
) {
253+
return extrappad;
254+
}
255+
256+
// increase padding to make more room for inside tick labels of the axis
257+
var fontSize = ax.tickfont ? ax.tickfont.size : 12;
258+
var isX = ax._id.charAt(0) === 'x';
259+
var morePad = (isX ? 1.2 : 0.6) * fontSize;
260+
261+
if(isAligned) {
262+
morePad *= 2;
263+
morePad += (ax.tickwidth || 0) / 2;
264+
}
265+
266+
morePad += TEXTPAD;
267+
268+
extrappad = Math.max(extrappad, morePad);
269+
270+
return extrappad;
271+
}
272+
273+
function adjustPadForInsideLabelsOnAnchorAxis(extrappad, ax, max) {
274+
var anchorAxis = (ax._anchorAxis || {});
275+
if((anchorAxis.ticklabelposition || '').indexOf('inside') !== -1) {
276+
// increase padding to make more room for inside tick labels of the counter axis
277+
if((
278+
!max && (
279+
anchorAxis.side === 'left' ||
280+
anchorAxis.side === 'bottom'
281+
)
282+
) || (
283+
max && (
284+
anchorAxis.side === 'top' ||
285+
anchorAxis.side === 'right'
286+
)
287+
)) {
288+
var isX = ax._id.charAt(0) === 'x';
289+
290+
var morePad = 0;
291+
if(anchorAxis._vals) {
292+
var rad = Lib.deg2rad(anchorAxis._tickAngles[anchorAxis._id + 'tick'] || 0);
293+
var cosA = Math.abs(Math.cos(rad));
294+
var sinA = Math.abs(Math.sin(rad));
295+
296+
// use bounding boxes
297+
anchorAxis._vals.forEach(function(t) {
298+
if(t.bb) {
299+
var w = t.bb.width;
300+
var h = t.bb.height;
301+
302+
morePad = Math.max(morePad, isX ?
303+
Math.max(w * cosA, h * sinA) :
304+
Math.max(h * cosA, w * sinA)
305+
);
306+
307+
// add extra pad around label
308+
morePad += 3;
309+
}
310+
});
311+
}
312+
313+
if(anchorAxis.ticks === 'inside' && anchorAxis.ticklabelposition === 'inside') {
314+
morePad += anchorAxis.ticklen || 0;
315+
}
316+
317+
extrappad = Math.max(extrappad, morePad);
318+
}
319+
}
320+
321+
return extrappad;
322+
}
323+
218324
function concatExtremes(gd, ax, noMatch) {
219325
var axId = ax._id;
220326
var fullData = gd._fullData;

0 commit comments

Comments
 (0)