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",