diff --git a/draftlogs/6790_add.md b/draftlogs/6790_add.md
new file mode 100644
index 00000000000..3c4a539095e
--- /dev/null
+++ b/draftlogs/6790_add.md
@@ -0,0 +1 @@
+- Add `autotickangles` to cartesian and radial axes [[#6790](https://github.com/plotly/plotly.js/pull/6790)], with thanks to @my-tien for the contribution!
diff --git a/src/components/colorbar/defaults.js b/src/components/colorbar/defaults.js
index c791db0e4a2..3b92c9e2836 100644
--- a/src/components/colorbar/defaults.js
+++ b/src/components/colorbar/defaults.js
@@ -109,7 +109,11 @@ module.exports = function colorbarDefaults(containerIn, containerOut, layout) {
handleTickValueDefaults(colorbarIn, colorbarOut, coerce, 'linear');
var font = layout.font;
- var opts = {outerTicks: false, font: font};
+ var opts = {
+ noAutotickangles: true,
+ outerTicks: false,
+ font: font
+ };
if(ticklabelposition.indexOf('inside') !== -1) {
opts.bgColor = 'black'; // could we instead use the average of colors in the scale?
}
diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js
index 7b01b971113..794ff699918 100644
--- a/src/components/colorbar/draw.js
+++ b/src/components/colorbar/draw.js
@@ -1001,6 +1001,7 @@ function mockColorBarAxis(gd, opts, zrange) {
var axisOptions = {
letter: letter,
font: fullLayout.font,
+ noAutotickangles: letter === 'y',
noHover: true,
noTickson: true,
noTicklabelmode: true,
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index dfdb0e5166d..8e13fe800d7 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -3472,13 +3472,13 @@ axes.drawLabels = function(gd, ax, opts) {
var fullLayout = gd._fullLayout;
var axId = ax._id;
- var axLetter = axId.charAt(0);
var cls = opts.cls || axId + 'tick';
var vals = opts.vals.filter(function(d) { return d.text; });
var labelFns = opts.labelFns;
var tickAngle = opts.secondary ? 0 : ax.tickangle;
+
var prevAngle = (ax._prevTickAngles || {})[cls];
var tickLabels = opts.layer.selectAll('g.' + cls)
@@ -3719,21 +3719,22 @@ axes.drawLabels = function(gd, ax, opts) {
// check for auto-angling if x labels overlap
// don't auto-angle at all for log axes with
// base and digit format
- if(vals.length && axLetter === 'x' && !isNumeric(tickAngle) &&
+ if(vals.length && ax.autotickangles &&
(ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D')
) {
- autoangle = 0;
+ autoangle = ax.autotickangles[0];
var maxFontSize = 0;
var lbbArray = [];
var i;
-
+ var maxLines = 1;
tickLabels.each(function(d) {
maxFontSize = Math.max(maxFontSize, d.fontSize);
var x = ax.l2p(d.x);
var thisLabel = selectTickLabel(this);
var bb = Drawing.bBox(thisLabel.node());
+ maxLines = Math.max(maxLines, svgTextUtils.lineCount(thisLabel));
lbbArray.push({
// ignore about y, just deal with x overlaps
@@ -3780,12 +3781,31 @@ axes.drawLabels = function(gd, ax, opts) {
var pad = !isAligned ? 0 :
(ax.tickwidth || 0) + 2 * TEXTPAD;
- var rotate90 = (tickSpacing < maxFontSize * 2.5) || ax.type === 'multicategory' || ax._name === 'realaxis';
+ // autotickangles
+ var adjacent = tickSpacing;
+ var opposite = maxFontSize * 1.25 * maxLines;
+ var hypotenuse = Math.sqrt(Math.pow(adjacent, 2) + Math.pow(opposite, 2));
+ var maxCos = adjacent / hypotenuse;
+ var autoTickAnglesRadians = ax.autotickangles.map(
+ function(degrees) { return degrees * Math.PI / 180; }
+ );
+ var angleRadians = autoTickAnglesRadians.find(
+ function(angle) { return Math.abs(Math.cos(angle)) <= maxCos; }
+ );
+ if(angleRadians === undefined) {
+ // no angle with smaller cosine than maxCos, just pick the angle with smallest cosine
+ angleRadians = autoTickAnglesRadians.reduce(
+ function(currentMax, nextAngle) {
+ return Math.abs(Math.cos(currentMax)) < Math.abs(Math.cos(nextAngle)) ? currentMax : nextAngle;
+ }
+ , autoTickAnglesRadians[0]
+ );
+ }
+ var newAngle = angleRadians * (180 / Math.PI /* to degrees */);
- // any overlap at all - set 30 degrees or 90 degrees
for(i = 0; i < lbbArray.length - 1; i++) {
if(Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1], pad)) {
- autoangle = rotate90 ? 90 : 30;
+ autoangle = newAngle;
break;
}
}
@@ -3807,7 +3827,7 @@ axes.drawLabels = function(gd, ax, opts) {
// by rotating 90 degrees, do not attempt to re-fix its label overlaps
// as this can lead to infinite redraw loops!
if(ax.automargin && fullLayout._redrawFromAutoMarginCount && prevAngle === 90) {
- autoangle = 90;
+ autoangle = prevAngle;
seq.push(function() {
positionLabels(tickLabels, prevAngle);
});
diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js
index bfa540f082c..2532e0c75cd 100644
--- a/src/plots/cartesian/layout_attributes.js
+++ b/src/plots/cartesian/layout_attributes.js
@@ -809,6 +809,20 @@ module.exports = {
'vertically.'
].join(' ')
},
+ autotickangles: {
+ valType: 'info_array',
+ freeLength: true,
+ items: {
+ valType: 'angle'
+ },
+ dflt: [0, 30, 90],
+ editType: 'ticks',
+ description: [
+ 'When `tickangle` is set to *auto*, it will be set to the first',
+ 'angle in this array that is large enough to prevent label',
+ 'overlap.'
+ ].join(' ')
+ },
tickprefix: {
valType: 'string',
dflt: '',
diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js
index 97f332a8430..203eb51f266 100644
--- a/src/plots/cartesian/layout_defaults.js
+++ b/src/plots/cartesian/layout_defaults.js
@@ -242,7 +242,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
visibleDflt: visibleDflt,
reverseDflt: reverseDflt,
autotypenumbersDflt: autotypenumbersDflt,
- splomStash: ((layoutOut._splomAxes || {})[axLetter] || {})[axId]
+ splomStash: ((layoutOut._splomAxes || {})[axLetter] || {})[axId],
+ noAutotickangles: axLetter === 'y'
};
coerce('uirevision', layoutOut.uirevision);
diff --git a/src/plots/cartesian/tick_label_defaults.js b/src/plots/cartesian/tick_label_defaults.js
index 35e87f2047b..71b1cde55a0 100644
--- a/src/plots/cartesian/tick_label_defaults.js
+++ b/src/plots/cartesian/tick_label_defaults.js
@@ -40,7 +40,12 @@ module.exports = function handleTickLabelDefaults(containerIn, containerOut, coe
coerce('ticklabelstep');
}
- if(!options.noAng) coerce('tickangle');
+ if(!options.noAng) {
+ var tickAngle = coerce('tickangle');
+ if(!options.noAutotickangles && tickAngle === 'auto') {
+ coerce('autotickangles');
+ }
+ }
if(axType !== 'category') {
var tickFormat = coerce('tickformat');
diff --git a/src/plots/gl3d/layout/axis_defaults.js b/src/plots/gl3d/layout/axis_defaults.js
index fff3e6ec2a1..7bbc87249d7 100644
--- a/src/plots/gl3d/layout/axis_defaults.js
+++ b/src/plots/gl3d/layout/axis_defaults.js
@@ -41,6 +41,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) {
letter: axName[0],
data: options.data,
showGrid: true,
+ noAutotickangles: true,
noTickson: true,
noTicklabelmode: true,
noTicklabelstep: true,
diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js
index eeec131dac6..cef166556dd 100644
--- a/src/plots/polar/layout_attributes.js
+++ b/src/plots/polar/layout_attributes.js
@@ -105,6 +105,8 @@ var radialAxisAttrs = {
].join(' ')
},
+ autotickangles: axesAttrs.autotickangles,
+
side: {
valType: 'enumerated',
// TODO add 'center' for `showline: false` radial axes
diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js
index 25dd745851d..70cef789c1b 100644
--- a/src/plots/polar/layout_defaults.js
+++ b/src/plots/polar/layout_defaults.js
@@ -167,7 +167,8 @@ function handleDefaults(contIn, contOut, coerce, opts) {
color: dfltFontColor,
size: dfltFontSize,
family: dfltFontFamily
- }
+ },
+ noAutotickangles: axName === 'angularaxis'
});
handleTickMarkDefaults(axIn, axOut, coerceAxis, {outerTicks: true});
diff --git a/src/plots/smith/layout_defaults.js b/src/plots/smith/layout_defaults.js
index 70529ebc8e7..1c5091a3f81 100644
--- a/src/plots/smith/layout_defaults.js
+++ b/src/plots/smith/layout_defaults.js
@@ -83,6 +83,7 @@ function handleDefaults(contIn, contOut, coerce, opts) {
}
handleTickLabelDefaults(axIn, axOut, coerceAxis, axOut.type, {
+ noAutotickangles: true,
noTicklabelstep: true,
noAng: !isRealAxis,
noExp: true,
diff --git a/src/plots/ternary/layout_defaults.js b/src/plots/ternary/layout_defaults.js
index 7994b04da00..cd0d77c379f 100644
--- a/src/plots/ternary/layout_defaults.js
+++ b/src/plots/ternary/layout_defaults.js
@@ -92,7 +92,7 @@ function handleAxisDefaults(containerIn, containerOut, options, ternaryLayoutOut
handleTickValueDefaults(containerIn, containerOut, coerce, 'linear');
handlePrefixSuffixDefaults(containerIn, containerOut, coerce, 'linear');
- handleTickLabelDefaults(containerIn, containerOut, coerce, 'linear');
+ handleTickLabelDefaults(containerIn, containerOut, coerce, 'linear', { noAutotickangles: true });
handleTickMarkDefaults(containerIn, containerOut, coerce,
{ outerTicks: true });
diff --git a/src/traces/carpet/ab_defaults.js b/src/traces/carpet/ab_defaults.js
index fbc5fa55ade..58fe6e2baf6 100644
--- a/src/traces/carpet/ab_defaults.js
+++ b/src/traces/carpet/ab_defaults.js
@@ -30,6 +30,7 @@ function mimickAxisDefaults(traceIn, traceOut, fullLayout, dfltColor) {
var axOut = Template.newContainer(traceOut, axName);
var defaultOptions = {
+ noAutotickangles: true,
noTicklabelstep: true,
tickfont: 'x',
id: axLetter + 'axis',
diff --git a/src/traces/indicator/defaults.js b/src/traces/indicator/defaults.js
index 77457986b99..6ccb013e7a9 100644
--- a/src/traces/indicator/defaults.js
+++ b/src/traces/indicator/defaults.js
@@ -129,7 +129,10 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
coerceGaugeAxis('visible');
traceOut._range = coerceGaugeAxis('range', traceOut._range);
- var opts = {outerTicks: true};
+ var opts = {
+ noAutotickangles: true,
+ outerTicks: true
+ };
handleTickValueDefaults(axisIn, axisOut, coerceGaugeAxis, 'linear');
handlePrefixSuffixDefaults(axisIn, axisOut, coerceGaugeAxis, 'linear', opts);
handleTickLabelDefaults(axisIn, axisOut, coerceGaugeAxis, 'linear', opts);
diff --git a/src/traces/indicator/plot.js b/src/traces/indicator/plot.js
index 563aa2cb065..a2d5af64e2b 100644
--- a/src/traces/indicator/plot.js
+++ b/src/traces/indicator/plot.js
@@ -823,6 +823,7 @@ function mockAxis(gd, opts, zrange) {
var axisOptions = {
letter: 'x',
font: fullLayout.font,
+ noAutotickangles: true,
noHover: true,
noTickson: true
};
diff --git a/test/image/baselines/10.png b/test/image/baselines/10.png
index 9ac419f7083..0a2f8398405 100644
Binary files a/test/image/baselines/10.png and b/test/image/baselines/10.png differ
diff --git a/test/image/baselines/automargin-zoom.png b/test/image/baselines/automargin-zoom.png
index 2bd98d4dba3..4fb44a68f7a 100644
Binary files a/test/image/baselines/automargin-zoom.png and b/test/image/baselines/automargin-zoom.png differ
diff --git a/test/image/baselines/domain_ref_axis_types.png b/test/image/baselines/domain_ref_axis_types.png
index 6c5fffe674f..1749c5a7046 100644
Binary files a/test/image/baselines/domain_ref_axis_types.png and b/test/image/baselines/domain_ref_axis_types.png differ
diff --git a/test/image/baselines/finance_subplots_categories.png b/test/image/baselines/finance_subplots_categories.png
index 07046ceff93..bc7364cffa5 100644
Binary files a/test/image/baselines/finance_subplots_categories.png and b/test/image/baselines/finance_subplots_categories.png differ
diff --git a/test/image/baselines/grid_subplot_types.png b/test/image/baselines/grid_subplot_types.png
index 41deca2b1a3..26c6136a62f 100644
Binary files a/test/image/baselines/grid_subplot_types.png and b/test/image/baselines/grid_subplot_types.png differ
diff --git a/test/image/baselines/period_positioning6.png b/test/image/baselines/period_positioning6.png
index 2b840a02a90..79f9a5436df 100644
Binary files a/test/image/baselines/period_positioning6.png and b/test/image/baselines/period_positioning6.png differ
diff --git a/test/image/baselines/polar_polygon-grids.png b/test/image/baselines/polar_polygon-grids.png
index a6422ec91b7..d9d59a24b85 100644
Binary files a/test/image/baselines/polar_polygon-grids.png and b/test/image/baselines/polar_polygon-grids.png differ
diff --git a/test/image/baselines/tick-increment.png b/test/image/baselines/tick-increment.png
index a8cceed43c7..8778a6c50c5 100644
Binary files a/test/image/baselines/tick-increment.png and b/test/image/baselines/tick-increment.png differ
diff --git a/test/image/baselines/ticklabelposition-5.png b/test/image/baselines/ticklabelposition-5.png
index b899794805a..2f3fd915324 100644
Binary files a/test/image/baselines/ticklabelposition-5.png and b/test/image/baselines/ticklabelposition-5.png differ
diff --git a/test/image/baselines/waterfall_and_bar.png b/test/image/baselines/waterfall_and_bar.png
index e2713e94ed3..25050359038 100644
Binary files a/test/image/baselines/waterfall_and_bar.png and b/test/image/baselines/waterfall_and_bar.png differ
diff --git a/test/image/mocks/10.json b/test/image/mocks/10.json
index cffa8d0a475..d6e8a351923 100644
--- a/test/image/mocks/10.json
+++ b/test/image/mocks/10.json
@@ -13,7 +13,7 @@
-0.255461150807681,
-0.25597595662203515
],
- "name": "Trial 1",
+ "name": "Trial 1 Very Long
multiline label",
"boxpoints": "all",
"pointpos": -1.5,
"jitter": 0,
@@ -45,7 +45,7 @@
2.2237747316232417,
2.0456528234898133
],
- "name": "Trial 2",
+ "name": "Trial 2 Very Long
multiline label",
"boxpoints": "all",
"pointpos": -1.5,
"jitter": 0,
@@ -77,7 +77,7 @@
1.114484992405051,
0.577777449231605
],
- "name": "Trial 3",
+ "name": "Trial 3 Very Long
multiline label",
"boxpoints": "all",
"pointpos": -1.5,
"jitter": 0,
@@ -109,7 +109,7 @@
3.01289053296517,
2.8335761244537614
],
- "name": "Trial 4",
+ "name": "Trial 4 Very Long
multiline label",
"boxpoints": "all",
"pointpos": -1.5,
"jitter": 0,
@@ -141,7 +141,7 @@
5.687207835036456,
5.718713550485276
],
- "name": "Trial 5",
+ "name": "Trial 5 Very Long
multiline label",
"boxpoints": "all",
"pointpos": -1.5,
"jitter": 0,
@@ -173,7 +173,7 @@
6.146408206163261,
6.726224574612897
],
- "name": "Trial 6",
+ "name": "Trial 6 Very Long
multiline label",
"boxpoints": "all",
"pointpos": -1.5,
"jitter": 0,
@@ -194,12 +194,12 @@
},
{
"x": [
- "Trial 1",
- "Trial 2",
- "Trial 3",
- "Trial 4",
- "Trial 5",
- "Trial 6"
+ "Trial 1 Very Long
multiline label",
+ "Trial 2 Very Long
multiline label",
+ "Trial 3 Very Long
multiline label",
+ "Trial 4 Very Long
multiline label",
+ "Trial 5 Very Long
multiline label",
+ "Trial 6 Very Long
multiline label"
],
"y": [
-0.16783142774745008,
@@ -262,7 +262,8 @@
"showticklabels": true,
"tick0": 0,
"dtick": 1,
- "tickangle": 0,
+ "tickangle": "auto",
+ "autotickangles": [5, -25],
"anchor": "y",
"autorange": true
},
diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js
index d128072a307..9e380f464d0 100644
--- a/test/jasmine/tests/axes_test.js
+++ b/test/jasmine/tests/axes_test.js
@@ -4268,16 +4268,22 @@ describe('Test axes', function() {
var op = parts[0];
var method = {
- '=': 'toBe',
+ '=': 'toBeCloseTo',
'~=': 'toBeWithin',
grew: 'toBeGreaterThan',
shrunk: 'toBeLessThan',
- initial: 'toBe'
+ initial: 'toBeCloseTo'
}[op];
var val = op === 'initial' ? initialSize[k] : previousSize[k];
var msgk = msg + ' ' + k + (parts[1] ? ' |' + parts[1] : '');
- var args = op === '~=' ? [val, 1.1, msgk] : [val, msgk, ''];
+ var args = [val];
+ if(op === '~=') {
+ args.push(1.1);
+ } else if(method === 'toBeCloseTo') {
+ args.push(3);
+ }
+ args.push(msgk);
expect(actual[k])[method](args[0], args[1], args[2]);
}
@@ -4313,7 +4319,7 @@ describe('Test axes', function() {
width: 600, height: 600
})
.then(function() {
- expect(gd._fullLayout.xaxis._tickAngles.xtick).toBe(30);
+ expect(gd._fullLayout.xaxis._tickAngles.xtick).toBeCloseTo(30, 3);
var gs = gd._fullLayout._size;
initialSize = Lib.extendDeep({}, gs);
@@ -4485,13 +4491,22 @@ describe('Test axes', function() {
var op = parts[0];
var method = {
- '=': 'toBe',
+ '=': 'toBeCloseTo',
+ '~=': 'toBeWithin',
grew: 'toBeGreaterThan',
+ shrunk: 'toBeLessThan',
+ initial: 'toBeCloseTo'
}[op];
var val = initialSize[k];
var msgk = msg + ' ' + k + (parts[1] ? ' |' + parts[1] : '');
- var args = op === '~=' ? [val, 1.1, msgk] : [val, msgk, ''];
+ var args = [val];
+ if(op === '~=') {
+ args.push(1.1);
+ } else if(method === 'toBeCloseTo') {
+ args.push(3);
+ }
+ args.push(msgk);
expect(actual[k])[method](args[0], args[1], args[2]);
}
@@ -4526,7 +4541,7 @@ describe('Test axes', function() {
width: 600, height: 600
})
.then(function() {
- expect(gd._fullLayout.xaxis._tickAngles.xtick).toBe(30);
+ expect(gd._fullLayout.xaxis._tickAngles.xtick).toBeCloseTo(30, 3);
var gs = gd._fullLayout._size;
initialSize = Lib.extendDeep({}, gs);
diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js
index fd8aec63255..11afee51827 100644
--- a/test/jasmine/tests/plots_test.js
+++ b/test/jasmine/tests/plots_test.js
@@ -1360,7 +1360,10 @@ describe('Test Plots with automargin and minreducedwidth/height', function() {
assert('height', '100');
})
.then(function() {
- return Plotly.relayout(gd, 'minreducedwidth', 100);
+ // force tickangle to 90 so when we increase the width the x axis labels
+ // don't revert to 30 degrees, giving us a larger height
+ // this is a cool effect, but not what we're testing here!
+ return Plotly.relayout(gd, {minreducedwidth: 100, 'xaxis.tickangle': 90});
})
.then(function() {
assert('width', '100');
diff --git a/test/plot-schema.json b/test/plot-schema.json
index 4970a9f5752..614b8e38af5 100644
--- a/test/plot-schema.json
+++ b/test/plot-schema.json
@@ -4464,6 +4464,20 @@
},
"role": "object"
},
+ "autotickangles": {
+ "description": "When `tickangle` is set to *auto*, it will be set to the first angle in this array that is large enough to prevent label overlap.",
+ "dflt": [
+ 0,
+ 30,
+ 90
+ ],
+ "editType": "ticks",
+ "freeLength": true,
+ "items": {
+ "valType": "angle"
+ },
+ "valType": "info_array"
+ },
"autotypenumbers": {
"description": "Using *strict* a numeric string in trace data is not converted to a number. Using *convert types* a numeric string in trace data may be treated as a number during automatic axis `type` detection. Defaults to layout.autotypenumbers.",
"dflt": "convert types",
@@ -10699,6 +10713,20 @@
},
"role": "object"
},
+ "autotickangles": {
+ "description": "When `tickangle` is set to *auto*, it will be set to the first angle in this array that is large enough to prevent label overlap.",
+ "dflt": [
+ 0,
+ 30,
+ 90
+ ],
+ "editType": "ticks",
+ "freeLength": true,
+ "items": {
+ "valType": "angle"
+ },
+ "valType": "info_array"
+ },
"autotypenumbers": {
"description": "Using *strict* a numeric string in trace data is not converted to a number. Using *convert types* a numeric string in trace data may be treated as a number during automatic axis `type` detection. Defaults to layout.autotypenumbers.",
"dflt": "convert types",
@@ -12039,6 +12067,20 @@
"editType": "plot",
"valType": "boolean"
},
+ "autotickangles": {
+ "description": "When `tickangle` is set to *auto*, it will be set to the first angle in this array that is large enough to prevent label overlap.",
+ "dflt": [
+ 0,
+ 30,
+ 90
+ ],
+ "editType": "ticks",
+ "freeLength": true,
+ "items": {
+ "valType": "angle"
+ },
+ "valType": "info_array"
+ },
"autotypenumbers": {
"description": "Using *strict* a numeric string in trace data is not converted to a number. Using *convert types* a numeric string in trace data may be treated as a number during automatic axis `type` detection. Defaults to layout.autotypenumbers.",
"dflt": "convert types",