Skip to content

Commit 12e69c8

Browse files
Merge pull request #2243 from plotly/label_automargin
automatic margins for long labels
2 parents 13eb982 + 1df34bd commit 12e69c8

File tree

12 files changed

+339
-42
lines changed

12 files changed

+339
-42
lines changed

src/plot_api/edit_types.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ var layoutOpts = {
3535
valType: 'flaglist',
3636
extras: ['none'],
3737
flags: [
38-
'calc', 'calcIfAutorange', 'plot', 'legend', 'ticks',
38+
'calc', 'calcIfAutorange', 'plot', 'legend', 'ticks', 'margins',
3939
'layoutstyle', 'modebar', 'camera', 'arraydraw'
4040
],
4141
description: [
@@ -47,6 +47,7 @@ var layoutOpts = {
4747
'*plot* calls `Plotly.plot` but without first clearing `gd.calcdata`.',
4848
'*legend* only redraws the legend.',
4949
'*ticks* only redraws axis ticks, labels, and gridlines.',
50+
'*margins* recomputes ticklabel automargins.',
5051
'*layoutstyle* reapplies global and SVG cartesian axis styles.',
5152
'*modebar* just updates the modebar.',
5253
'*camera* just updates the camera settings for gl3d scenes.',

src/plot_api/helpers.js

+9
Original file line numberDiff line numberDiff line change
@@ -561,3 +561,12 @@ exports.clearAxisTypes = function(gd, traces, layoutUpdate) {
561561
}
562562
}
563563
};
564+
565+
exports.clearAxisAutomargins = function(gd) {
566+
var keys = Object.keys(gd._fullLayout._pushmargin);
567+
for(var i = 0; i < keys.length; i++) {
568+
if(keys[i].indexOf('automargin') !== -1) {
569+
delete gd._fullLayout._pushmargin[keys[i]];
570+
}
571+
}
572+
};

src/plot_api/plot_api.js

+3
Original file line numberDiff line numberDiff line change
@@ -1746,6 +1746,7 @@ exports.relayout = function relayout(gd, astr, val) {
17461746

17471747
// clear calcdata if required
17481748
if(flags.calc) gd.calcdata = undefined;
1749+
if(flags.margins) helpers.clearAxisAutomargins(gd);
17491750

17501751
// fill in redraw sequence
17511752

@@ -2186,6 +2187,7 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) {
21862187
// clear calcdata and/or axis types if required
21872188
if(restyleFlags.clearCalc || relayoutFlags.calc) gd.calcdata = undefined;
21882189
if(restyleFlags.clearAxisTypes) helpers.clearAxisTypes(gd, traces, layoutUpdate);
2190+
if(relayoutFlags.margins) helpers.clearAxisAutomargins(gd);
21892191

21902192
// fill in redraw sequence
21912193
var seq = [];
@@ -2313,6 +2315,7 @@ exports.react = function(gd, data, layout, config) {
23132315

23142316
// clear calcdata if required
23152317
if(restyleFlags.calc || relayoutFlags.calc) gd.calcdata = undefined;
2318+
if(relayoutFlags.margins) helpers.clearAxisAutomargins(gd);
23162319

23172320
// Note: what restyle/relayout use impliedEdits and clearAxisTypes for
23182321
// must be handled by the user when using Plotly.react.

src/plots/cartesian/axes.js

+32-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
var d3 = require('d3');
1313
var isNumeric = require('fast-isnumeric');
14+
var Plots = require('../../plots/plots');
1415

1516
var Registry = require('../../registry');
1617
var Lib = require('../../lib');
@@ -2217,10 +2218,40 @@ axes.doTicks = function(gd, axid, skipTitle) {
22172218
}
22182219
}
22192220

2221+
function doAutoMargins() {
2222+
if(!ax.automargin) { return; }
2223+
if(axLetter !== 'x' && axLetter !== 'y') { return; }
2224+
2225+
var s = ax.side[0];
2226+
var push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0};
2227+
2228+
if(axLetter === 'x') {
2229+
push.y = (ax.anchor === 'free' ? ax.position :
2230+
ax._anchorAxis.domain[s === 't' ? 1 : 0]);
2231+
push[s] += ax._boundingBox.height;
2232+
}
2233+
else {
2234+
push.x = (ax.anchor === 'free' ? ax.position :
2235+
ax._anchorAxis.domain[s === 'r' ? 1 : 0]);
2236+
push[s] += ax._boundingBox.width;
2237+
}
2238+
2239+
if(ax.title !== fullLayout._dfltTitle[axLetter]) {
2240+
push[s] += ax.titlefont.size;
2241+
}
2242+
2243+
var pushKey = ax._name + '.automargin';
2244+
var prevPush = fullLayout._pushmargin[pushKey];
2245+
if(!prevPush || prevPush[s].size < push[s]) {
2246+
Plots.autoMargin(gd, pushKey, push);
2247+
}
2248+
}
2249+
22202250
var done = Lib.syncOrAsync([
22212251
allLabelsReady,
22222252
fixLabelOverlaps,
2223-
calcBoundingBox
2253+
calcBoundingBox,
2254+
doAutoMargins
22242255
]);
22252256
if(done && done.then) gd._promises.push(done);
22262257
return done;

src/plots/cartesian/axis_defaults.js

+2
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
8888

8989
if(containerOut.showline || containerOut.ticks) coerce('mirror');
9090

91+
if(options.automargin) coerce('automargin');
92+
9193
return containerOut;
9294
};

src/plots/cartesian/layout_attributes.js

+45-35
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ module.exports = {
4242
title: {
4343
valType: 'string',
4444
role: 'info',
45-
editType: 'ticks',
45+
editType: 'ticks+margins',
4646
description: 'Sets the title of this axis.'
4747
},
4848
titlefont: fontAttrs({
49-
editType: 'ticks',
49+
editType: 'ticks+margins',
5050
description: [
5151
'Sets this axis\' title font.'
5252
].join(' ')
@@ -100,10 +100,10 @@ module.exports = {
100100
valType: 'info_array',
101101
role: 'info',
102102
items: [
103-
{valType: 'any', editType: 'plot', impliedEdits: {'^autorange': false}},
104-
{valType: 'any', editType: 'plot', impliedEdits: {'^autorange': false}}
103+
{valType: 'any', editType: 'plot+margins', impliedEdits: {'^autorange': false}},
104+
{valType: 'any', editType: 'plot+margins', impliedEdits: {'^autorange': false}}
105105
],
106-
editType: 'plot',
106+
editType: 'plot+margins',
107107
impliedEdits: {'autorange': false},
108108
description: [
109109
'Sets the range of this axis.',
@@ -198,7 +198,7 @@ module.exports = {
198198
valType: 'enumerated',
199199
values: ['auto', 'linear', 'array'],
200200
role: 'info',
201-
editType: 'ticks',
201+
editType: 'ticks+margins',
202202
impliedEdits: {tick0: undefined, dtick: undefined},
203203
description: [
204204
'Sets the tick mode for this axis.',
@@ -216,7 +216,7 @@ module.exports = {
216216
min: 0,
217217
dflt: 0,
218218
role: 'style',
219-
editType: 'ticks',
219+
editType: 'ticks+margins',
220220
description: [
221221
'Specifies the maximum number of ticks for the particular axis.',
222222
'The actual number of ticks will be chosen automatically to be',
@@ -227,7 +227,7 @@ module.exports = {
227227
tick0: {
228228
valType: 'any',
229229
role: 'style',
230-
editType: 'ticks',
230+
editType: 'ticks+margins',
231231
impliedEdits: {tickmode: 'linear'},
232232
description: [
233233
'Sets the placement of the first tick on this axis.',
@@ -243,7 +243,7 @@ module.exports = {
243243
dtick: {
244244
valType: 'any',
245245
role: 'style',
246-
editType: 'ticks',
246+
editType: 'ticks+margins',
247247
impliedEdits: {tickmode: 'linear'},
248248
description: [
249249
'Sets the step in-between ticks on this axis. Use with `tick0`.',
@@ -269,7 +269,7 @@ module.exports = {
269269
},
270270
tickvals: {
271271
valType: 'data_array',
272-
editType: 'ticks',
272+
editType: 'ticks+margins',
273273
description: [
274274
'Sets the values at which ticks on this axis appear.',
275275
'Only has an effect if `tickmode` is set to *array*.',
@@ -278,7 +278,7 @@ module.exports = {
278278
},
279279
ticktext: {
280280
valType: 'data_array',
281-
editType: 'ticks',
281+
editType: 'ticks+margins',
282282
description: [
283283
'Sets the text displayed at the ticks position via `tickvals`.',
284284
'Only has an effect if `tickmode` is set to *array*.',
@@ -289,7 +289,7 @@ module.exports = {
289289
valType: 'enumerated',
290290
values: ['outside', 'inside', ''],
291291
role: 'style',
292-
editType: 'ticks',
292+
editType: 'ticks+margins',
293293
description: [
294294
'Determines whether ticks are drawn or not.',
295295
'If **, this axis\' ticks are not drawn.',
@@ -341,9 +341,19 @@ module.exports = {
341341
valType: 'boolean',
342342
dflt: true,
343343
role: 'style',
344-
editType: 'ticks',
344+
editType: 'ticks+margins',
345345
description: 'Determines whether or not the tick labels are drawn.'
346346
},
347+
automargin: {
348+
valType: 'boolean',
349+
dflt: false,
350+
role: 'style',
351+
editType: 'ticks+margins',
352+
description: [
353+
'Determines whether long tick labels automatically grow the figure',
354+
'margins.'
355+
].join(' ')
356+
},
347357
showspikes: {
348358
valType: 'boolean',
349359
dflt: false,
@@ -396,14 +406,14 @@ module.exports = {
396406
description: 'Determines whether spikelines are stuck to the cursor or to the closest datapoints.'
397407
},
398408
tickfont: fontAttrs({
399-
editType: 'ticks',
409+
editType: 'ticks+margins',
400410
description: 'Sets the tick font.'
401411
}),
402412
tickangle: {
403413
valType: 'angle',
404414
dflt: 'auto',
405415
role: 'style',
406-
editType: 'ticks',
416+
editType: 'ticks+margins',
407417
description: [
408418
'Sets the angle of the tick labels with respect to the horizontal.',
409419
'For example, a `tickangle` of -90 draws the tick labels',
@@ -414,15 +424,15 @@ module.exports = {
414424
valType: 'string',
415425
dflt: '',
416426
role: 'style',
417-
editType: 'ticks',
427+
editType: 'ticks+margins',
418428
description: 'Sets a tick label prefix.'
419429
},
420430
showtickprefix: {
421431
valType: 'enumerated',
422432
values: ['all', 'first', 'last', 'none'],
423433
dflt: 'all',
424434
role: 'style',
425-
editType: 'ticks',
435+
editType: 'ticks+margins',
426436
description: [
427437
'If *all*, all tick labels are displayed with a prefix.',
428438
'If *first*, only the first tick is displayed with a prefix.',
@@ -434,23 +444,23 @@ module.exports = {
434444
valType: 'string',
435445
dflt: '',
436446
role: 'style',
437-
editType: 'ticks',
447+
editType: 'ticks+margins',
438448
description: 'Sets a tick label suffix.'
439449
},
440450
showticksuffix: {
441451
valType: 'enumerated',
442452
values: ['all', 'first', 'last', 'none'],
443453
dflt: 'all',
444454
role: 'style',
445-
editType: 'ticks',
455+
editType: 'ticks+margins',
446456
description: 'Same as `showtickprefix` but for tick suffixes.'
447457
},
448458
showexponent: {
449459
valType: 'enumerated',
450460
values: ['all', 'first', 'last', 'none'],
451461
dflt: 'all',
452462
role: 'style',
453-
editType: 'ticks',
463+
editType: 'ticks+margins',
454464
description: [
455465
'If *all*, all exponents are shown besides their significands.',
456466
'If *first*, only the exponent of the first tick is shown.',
@@ -463,7 +473,7 @@ module.exports = {
463473
values: ['none', 'e', 'E', 'power', 'SI', 'B'],
464474
dflt: 'B',
465475
role: 'style',
466-
editType: 'ticks',
476+
editType: 'ticks+margins',
467477
description: [
468478
'Determines a formatting rule for the tick exponents.',
469479
'For example, consider the number 1,000,000,000.',
@@ -479,7 +489,7 @@ module.exports = {
479489
valType: 'boolean',
480490
dflt: false,
481491
role: 'style',
482-
editType: 'ticks',
492+
editType: 'ticks+margins',
483493
description: [
484494
'If "true", even 4-digit integers are separated'
485495
].join(' ')
@@ -488,7 +498,7 @@ module.exports = {
488498
valType: 'string',
489499
dflt: '',
490500
role: 'style',
491-
editType: 'ticks',
501+
editType: 'ticks+margins',
492502
description: [
493503
'Sets the tick label formatting rule using d3 formatting mini-languages',
494504
'which are very similar to those in Python. For numbers, see:',
@@ -507,10 +517,10 @@ module.exports = {
507517
valType: 'info_array',
508518
role: 'info',
509519
items: [
510-
{valType: 'any', editType: 'ticks'},
511-
{valType: 'any', editType: 'ticks'}
520+
{valType: 'any', editType: 'ticks+margins'},
521+
{valType: 'any', editType: 'ticks+margins'}
512522
],
513-
editType: 'ticks',
523+
editType: 'ticks+margins',
514524
description: [
515525
'range [*min*, *max*], where *min*, *max* - dtick values',
516526
'which describe some zoom level, it is possible to omit *min*',
@@ -521,12 +531,12 @@ module.exports = {
521531
valType: 'string',
522532
dflt: '',
523533
role: 'style',
524-
editType: 'ticks',
534+
editType: 'ticks+margins',
525535
description: [
526536
'string - dtickformat for described zoom level, the same as *tickformat*'
527537
].join(' ')
528538
},
529-
editType: 'ticks'
539+
editType: 'ticks+margins'
530540
},
531541
hoverformat: {
532542
valType: 'string',
@@ -628,7 +638,7 @@ module.exports = {
628638
constants.idRegex.y.toString()
629639
],
630640
role: 'info',
631-
editType: 'plot',
641+
editType: 'plot+margins',
632642
description: [
633643
'If set to an opposite-letter axis id (e.g. `x2`, `y`), this axis is bound to',
634644
'the corresponding opposite-letter axis.',
@@ -641,7 +651,7 @@ module.exports = {
641651
valType: 'enumerated',
642652
values: ['top', 'bottom', 'left', 'right'],
643653
role: 'info',
644-
editType: 'plot',
654+
editType: 'plot+margins',
645655
description: [
646656
'Determines whether a x (y) axis is positioned',
647657
'at the *bottom* (*left*) or *top* (*right*)',
@@ -685,11 +695,11 @@ module.exports = {
685695
valType: 'info_array',
686696
role: 'info',
687697
items: [
688-
{valType: 'number', min: 0, max: 1, editType: 'plot'},
689-
{valType: 'number', min: 0, max: 1, editType: 'plot'}
698+
{valType: 'number', min: 0, max: 1, editType: 'plot+margins'},
699+
{valType: 'number', min: 0, max: 1, editType: 'plot+margins'}
690700
],
691701
dflt: [0, 1],
692-
editType: 'plot',
702+
editType: 'plot+margins',
693703
description: [
694704
'Sets the domain of this axis (in plot fraction).'
695705
].join(' ')
@@ -700,7 +710,7 @@ module.exports = {
700710
max: 1,
701711
dflt: 0,
702712
role: 'style',
703-
editType: 'plot',
713+
editType: 'plot+margins',
704714
description: [
705715
'Sets the position of this axis in the plotting space',
706716
'(in normalized coordinates).',
@@ -744,7 +754,7 @@ module.exports = {
744754
autotick: {
745755
valType: 'boolean',
746756
role: 'info',
747-
editType: 'ticks',
757+
editType: 'ticks+margins',
748758
description: [
749759
'Obsolete.',
750760
'Set `tickmode` to *auto* for old `autotick` *true* behavior.',

0 commit comments

Comments
 (0)