diff --git a/draftlogs/7684_change.md b/draftlogs/7684_change.md new file mode 100644 index 00000000000..93797426c49 --- /dev/null +++ b/draftlogs/7684_change.md @@ -0,0 +1 @@ + - Set default layout.axis.tickmode to 'sync' when axis is overlaying [[#7684](https://github.com/plotly/plotly.js/pull/7684)] \ No newline at end of file diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 08bfda6f367..d99867a7e50 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1113,6 +1113,9 @@ axes.calcTicks = function calcTicks(ax, opts) { var obj = { value: x }; if(major) { + // mark first major tick, for showexponent/showtickprefix/showticksuffix 'first' + if(x === x0) obj.first = true; + if(isDLog && (x !== (x | 0))) { obj.simpleLabel = true; } @@ -1255,9 +1258,10 @@ axes.calcTicks = function calcTicks(ax, opts) { tickVals.pop(); } - // save the last tick as well as first, so we can - // show the exponent only on the last one + // save the last tick as well as first ax._tmax = (tickVals[tickVals.length - 1] || {}).value; + // mark the last major tick, for showexponent/showtickprefix/showticksuffix 'last' + if(tickVals.length) tickVals[tickVals.length - 1].last = true; // for showing the rest of a date when the main tick label is only the // latter part: ax._prevDateHead holds what we showed most recently. @@ -1279,7 +1283,8 @@ axes.calcTicks = function calcTicks(ax, opts) { ax, tickVal.value, false, // hover - tickVal.simpleLabel // noSuffixPrefix + tickVal.simpleLabel, // noSuffixPrefix + {first: tickVal.first, last: tickVal.last} // positionFlags ); var p = tickVal.periodX; if(p !== undefined) { @@ -1351,6 +1356,16 @@ function syncTicks(ax) { var ticksOut = []; if(baseAxis._vals) { + // find the indices of the first and last labelled (major, non-noTick) base ticks, + // for showexponent/showtickprefix/showticksuffix 'first'/'last' + var firstMajorIdx = -1; + var lastMajorIdx = -1; + for(var j = 0; j < baseAxis._vals.length; j++) { + if(baseAxis._vals[j].noTick || baseAxis._vals[j].minor) continue; + if(firstMajorIdx === -1) firstMajorIdx = j; + lastMajorIdx = j; + } + for(var i = 0; i < baseAxis._vals.length; i++) { // filter vals with noTick flag if(baseAxis._vals[i].noTick) { @@ -1362,7 +1377,10 @@ function syncTicks(ax) { // get the tick for the current axis based on position var vali = ax.p2l(pos); - var obj = axes.tickText(ax, vali); + var obj = axes.tickText(ax, vali, false, undefined, { + first: i === firstMajorIdx, + last: i === lastMajorIdx, + }); // assign minor ticks if(baseAxis._vals[i].minor) { @@ -1408,10 +1426,27 @@ function arrayTicks(ax, majorOnly) { // except with more precision to the numbers if(!Lib.isArrayOrTypedArray(text)) text = []; + // indices of the first and last in-range major ticks, + // showexponent/showtickprefix/showticksuffix 'first'/'last' + var firstIdx = -1; + var lastIdx = -1; + if(!isMinor) { + for(var k = 0; k < vals.length; k++) { + var valk = tickVal2l(vals[k]); + if(valk > tickMin && valk < tickMax) { + if(firstIdx === -1) firstIdx = k; + lastIdx = k; + } + } + } + for(var i = 0; i < vals.length; i++) { var vali = tickVal2l(vals[i]); if(vali > tickMin && vali < tickMax) { - var obj = axes.tickText(ax, vali, false, String(text[i])); + var obj = axes.tickText(ax, vali, false, String(text[i]), { + first: i === firstIdx, + last: i === lastIdx, + }); if(isMinor) { obj.minor = true; obj.text = ''; @@ -1725,13 +1760,23 @@ axes.tickFirst = function(ax, opts) { } else throw 'unrecognized dtick ' + String(dtick); }; -// draw the text for one tick. -// px,py are the location on gd.paper -// prefix is there so the x axis ticks can be dropped a line -// ax is the axis layout, x is the tick value -// hover is a (truthy) flag for whether to show numbers with a bit -// more precision for hovertext -axes.tickText = function(ax, x, hover, noSuffixPrefix) { +/** + * Compute the text and metadata for one tick. + * + * @param {object} ax: the axis layout object + * @param {number} x: the tick value + * @param {boolean} hover: whether tick being computed for hovertext (as opposed to axis) + * @param {boolean} noSuffixPrefix: whether to skip adding tickprefix and ticksuffix + * @param {object} positionFlags: optional flags describing where this tick sits on the + * axis, used by the showexponent/showtickprefix/showticksuffix 'first'/'last' options: + * - first (boolean): whether this is the first (labelled, major) tick on the axis + * - last (boolean): whether this is the last (labelled, major) tick on the axis + * @return {object} the tick object, including its formatted `text` + */ +axes.tickText = function(ax, x, hover, noSuffixPrefix, positionFlags) { + var first = !!positionFlags?.first; + var last = !!positionFlags?.last; + var out = tickTextObj(ax, x); var arrayMode = ax.tickmode === 'array'; var extraPrecision = hover || arrayMode; @@ -1765,13 +1810,10 @@ axes.tickText = function(ax, x, hover, noSuffixPrefix) { function isHidden(showAttr) { if(showAttr === undefined) return true; if(hover) return showAttr === 'none'; - - var firstOrLast = { - first: ax._tmin, - last: ax._tmax - }[showAttr]; - - return showAttr !== 'all' && x !== firstOrLast; + if(showAttr === 'all') return false; + if(showAttr === 'first') return !first; + if(showAttr === 'last') return !last; + return true; // fallback for the hover is false and showAttr==='none' or another value } var hideexp = hover ? diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 0aa0caceee2..e66ec74628d 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -732,7 +732,10 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { for(i = 0; i < axList.length; i++) { var axListI = axList[i]; var axListIType = axListI[axisType]; - if(!axListI.fixedrange && axListIType.tickmode === 'sync') activeAxIds.push(axListIType._id); + var axId = axListIType._id; + if(!axListI.fixedrange && axListIType.tickmode === 'sync' && !activeAxIds.includes(axId)) { + activeAxIds.push(axId); + } } } diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index d1ce0158ace..d7c207fd21f 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -35,7 +35,8 @@ var tickmode = extendFlat({}, minorTickmode, { description: [ minorTickmode.description, 'If *sync*, the number of ticks will sync with the overlayed axis', - 'set by `overlaying` property.' + 'set by `overlaying` property. When no other tick info is provided,', + 'overlaying (non-categorical) axes default to *sync*, while other axes default to *auto*.', ].join(' ') }); diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index 68b9207ee62..faa2d36f03b 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -5,8 +5,7 @@ var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; var isTypedArraySpec = require('../../lib/array').isTypedArraySpec; var decodeTypedArraySpec = require('../../lib/array').decodeTypedArraySpec; -module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType, opts) { - if(!opts) opts = {}; +module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType, opts = {}) { var isMinor = opts.isMinor; var cIn = isMinor ? containerIn.minor || {} : containerIn; var cOut = isMinor ? containerOut.minor : containerOut; @@ -14,35 +13,40 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe function readInput(attr) { var v = cIn[attr]; - if(isTypedArraySpec(v)) v = decodeTypedArraySpec(v); + if (isTypedArraySpec(v)) v = decodeTypedArraySpec(v); - return ( - v !== undefined - ) ? v : (cOut._template || {})[attr]; + return v !== undefined ? v : (cOut._template || {})[attr]; } var _tick0 = readInput('tick0'); var _dtick = readInput('dtick'); var _tickvals = readInput('tickvals'); + var _overlaying = readInput('overlaying'); + var _categorical = axType === 'category' || axType === 'multicategory'; - var tickmodeDefault = isArrayOrTypedArray(_tickvals) ? 'array' : - _dtick ? 'linear' : - 'auto'; + var tickmodeDefault; + if (isArrayOrTypedArray(_tickvals)) { + tickmodeDefault = 'array'; + } else if (_dtick) { + tickmodeDefault = 'linear'; + } else if (_overlaying && !_categorical) { + tickmodeDefault = 'sync'; + } else { + tickmodeDefault = 'auto'; + } var tickmode = coerce(prefix + 'tickmode', tickmodeDefault); - if(tickmode === 'auto' || tickmode === 'sync') { + if (tickmode === 'auto' || tickmode === 'sync') { coerce(prefix + 'nticks'); - } else if(tickmode === 'linear') { + } else if (tickmode === 'linear') { // dtick is usually a positive number, but there are some // special strings available for log or date axes // tick0 also has special logic - var dtick = cOut.dtick = cleanTicks.dtick( - _dtick, axType); - cOut.tick0 = cleanTicks.tick0( - _tick0, axType, containerOut.calendar, dtick); - } else if(axType !== 'multicategory') { + var dtick = (cOut.dtick = cleanTicks.dtick(_dtick, axType)); + cOut.tick0 = cleanTicks.tick0(_tick0, axType, containerOut.calendar, dtick); + } else if (axType !== 'multicategory') { var tickvals = coerce(prefix + 'tickvals'); - if(tickvals === undefined) cOut.tickmode = 'auto'; - else if(!isMinor) coerce('ticktext'); + if (tickvals === undefined) cOut.tickmode = 'auto'; + else if (!isMinor) coerce('ticktext'); } }; diff --git a/src/traces/carpet/calc_labels.js b/src/traces/carpet/calc_labels.js index 8b7d5a47045..ce75f71fdfc 100644 --- a/src/traces/carpet/calc_labels.js +++ b/src/traces/carpet/calc_labels.js @@ -13,7 +13,10 @@ module.exports = function calcLabels(trace, axis) { gridline = gridlines[i]; if(['start', 'both'].indexOf(axis.showticklabels) !== -1) { - tobj = Axes.tickText(axis, gridline.value); + tobj = Axes.tickText(axis, gridline.value, false, false, { + first: i === 0, + last: i === gridlines.length - 1, + }); extendFlat(tobj, { prefix: prefix, @@ -32,7 +35,10 @@ module.exports = function calcLabels(trace, axis) { } if(['end', 'both'].indexOf(axis.showticklabels) !== -1) { - tobj = Axes.tickText(axis, gridline.value); + tobj = Axes.tickText(axis, gridline.value, false, false, { + first: i === 0, + last: i === gridlines.length - 1, + }); extendFlat(tobj, { endAnchor: false, diff --git a/src/types/generated/schema.d.ts b/src/types/generated/schema.d.ts index 1e05da351d0..ccfbd77a70a 100644 --- a/src/types/generated/schema.d.ts +++ b/src/types/generated/schema.d.ts @@ -12876,7 +12876,7 @@ export interface LayoutAxis { * Minimum: 0 */ ticklen?: number; - /** Sets the tick mode for this axis. If *auto*, the number of ticks is set via `nticks`. If *linear*, the placement of the ticks is determined by a starting position `tick0` and a tick step `dtick` (*linear* is the default value if `tick0` and `dtick` are provided). If *array*, the placement of the ticks is set via `tickvals` and the tick text is `ticktext`. (*array* is the default value if `tickvals` is provided). If *sync*, the number of ticks will sync with the overlayed axis set by `overlaying` property. */ + /** Sets the tick mode for this axis. If *auto*, the number of ticks is set via `nticks`. If *linear*, the placement of the ticks is determined by a starting position `tick0` and a tick step `dtick` (*linear* is the default value if `tick0` and `dtick` are provided). If *array*, the placement of the ticks is set via `tickvals` and the tick text is `ticktext`. (*array* is the default value if `tickvals` is provided). If *sync*, the number of ticks will sync with the overlayed axis set by `overlaying` property. When no other tick info is provided, overlaying (non-categorical) axes default to *sync*, while other axes default to *auto*. */ tickmode?: 'auto' | 'linear' | 'array' | 'sync'; /** Sets a tick label prefix. */ tickprefix?: string; diff --git a/test/image/baselines/20.png b/test/image/baselines/20.png index 230a6baf8b7..374551b3b33 100644 Binary files a/test/image/baselines/20.png and b/test/image/baselines/20.png differ diff --git a/test/image/baselines/autorange-tozero-rangemode.png b/test/image/baselines/autorange-tozero-rangemode.png index ebc57f4d8d2..138d26a311b 100644 Binary files a/test/image/baselines/autorange-tozero-rangemode.png and b/test/image/baselines/autorange-tozero-rangemode.png differ diff --git a/test/image/baselines/candlestick_double-y-axis.png b/test/image/baselines/candlestick_double-y-axis.png index 077f3b8fab0..fd0c83a5331 100644 Binary files a/test/image/baselines/candlestick_double-y-axis.png and b/test/image/baselines/candlestick_double-y-axis.png differ diff --git a/test/image/baselines/candlestick_rangeslider_thai.png b/test/image/baselines/candlestick_rangeslider_thai.png index f40e1cdd0ef..5057aaa7197 100644 Binary files a/test/image/baselines/candlestick_rangeslider_thai.png and b/test/image/baselines/candlestick_rangeslider_thai.png differ diff --git a/test/image/baselines/legend_scroll_beyond_plotarea.png b/test/image/baselines/legend_scroll_beyond_plotarea.png index 677421dd0ff..51ecafd947b 100644 Binary files a/test/image/baselines/legend_scroll_beyond_plotarea.png and b/test/image/baselines/legend_scroll_beyond_plotarea.png differ diff --git a/test/image/baselines/legend_visibility.png b/test/image/baselines/legend_visibility.png index e80069a9c02..bbdb2156d96 100644 Binary files a/test/image/baselines/legend_visibility.png and b/test/image/baselines/legend_visibility.png differ diff --git a/test/image/baselines/mult-yaxes-subplots-stacked.png b/test/image/baselines/mult-yaxes-subplots-stacked.png index 5aac4b389cd..855dcde1bf3 100644 Binary files a/test/image/baselines/mult-yaxes-subplots-stacked.png and b/test/image/baselines/mult-yaxes-subplots-stacked.png differ diff --git a/test/image/baselines/multicategory_series.png b/test/image/baselines/multicategory_series.png index dfbe24abe2e..7e4b0c8edf0 100644 Binary files a/test/image/baselines/multicategory_series.png and b/test/image/baselines/multicategory_series.png differ diff --git a/test/image/baselines/range_slider_legend_left.png b/test/image/baselines/range_slider_legend_left.png index 9eb5c4457ff..d35f8c99b28 100644 Binary files a/test/image/baselines/range_slider_legend_left.png and b/test/image/baselines/range_slider_legend_left.png differ diff --git a/test/image/baselines/yaxis-over-yaxis2.png b/test/image/baselines/yaxis-over-yaxis2.png index 175f1c39adb..6d3398ef7aa 100644 Binary files a/test/image/baselines/yaxis-over-yaxis2.png and b/test/image/baselines/yaxis-over-yaxis2.png differ diff --git a/test/image/baselines/zerolinelayer_above.png b/test/image/baselines/zerolinelayer_above.png index 10fca3a7e8c..6e4490680da 100644 Binary files a/test/image/baselines/zerolinelayer_above.png and b/test/image/baselines/zerolinelayer_above.png differ diff --git a/test/image/baselines/zerolinelayer_below.png b/test/image/baselines/zerolinelayer_below.png index d011848fa65..9e43516c183 100644 Binary files a/test/image/baselines/zerolinelayer_below.png and b/test/image/baselines/zerolinelayer_below.png differ diff --git a/test/image/mocks/shapes_layer_below_traces.json b/test/image/mocks/shapes_layer_below_traces.json index 9b221568538..049b66b9c4c 100644 --- a/test/image/mocks/shapes_layer_below_traces.json +++ b/test/image/mocks/shapes_layer_below_traces.json @@ -103,7 +103,8 @@ "yaxis2": { "gridwidth": 2, "side": "right", - "overlaying": "y" + "overlaying": "y", + "tickmode": "auto" } } } diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 3d1e962312f..5a9457f9ad2 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -8416,4 +8416,33 @@ describe('test tickmode calculator', function() { }).then(done, done.fail); }); }); + + describe('sync', function() { + it('shows the exponent on a synced overlaying axis with showexponent *first*/*last*', function(done) { + Plotly.newPlot(gd, { + data: [ + {y: [0, 6]}, + {y: [0, 60000], yaxis: 'y2'} + ], + layout: { + yaxis2: { + overlaying: 'y', + exponentformat: 'SI', + showexponent: 'last' + } + } + }).then(function() { + var ax = gd._fullLayout.yaxis2; + expect(ax.tickmode).toBe('sync'); + + var labels = ax._vals + .filter(function(d) { return !d.minor; }) + .map(function(d) { return d.text; }); + + // the multiplier (e.g. 'k') must still appear on the labelled tick + expect(labels.some(function(t) { return /k$/.test(t); })) + .toBe(true, 'expected an SI prefix in: ' + JSON.stringify(labels)); + }).then(done, done.fail); + }); + }); }); diff --git a/test/plot-schema.json b/test/plot-schema.json index 86964d4f5e9..5ae14291f41 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -14867,7 +14867,7 @@ "valType": "number" }, "tickmode": { - "description": "Sets the tick mode for this axis. If *auto*, the number of ticks is set via `nticks`. If *linear*, the placement of the ticks is determined by a starting position `tick0` and a tick step `dtick` (*linear* is the default value if `tick0` and `dtick` are provided). If *array*, the placement of the ticks is set via `tickvals` and the tick text is `ticktext`. (*array* is the default value if `tickvals` is provided). If *sync*, the number of ticks will sync with the overlayed axis set by `overlaying` property.", + "description": "Sets the tick mode for this axis. If *auto*, the number of ticks is set via `nticks`. If *linear*, the placement of the ticks is determined by a starting position `tick0` and a tick step `dtick` (*linear* is the default value if `tick0` and `dtick` are provided). If *array*, the placement of the ticks is set via `tickvals` and the tick text is `ticktext`. (*array* is the default value if `tickvals` is provided). If *sync*, the number of ticks will sync with the overlayed axis set by `overlaying` property. When no other tick info is provided, overlaying (non-categorical) axes default to *sync*, while other axes default to *auto*.", "editType": "ticks", "impliedEdits": {}, "valType": "enumerated", @@ -16146,7 +16146,7 @@ "valType": "number" }, "tickmode": { - "description": "Sets the tick mode for this axis. If *auto*, the number of ticks is set via `nticks`. If *linear*, the placement of the ticks is determined by a starting position `tick0` and a tick step `dtick` (*linear* is the default value if `tick0` and `dtick` are provided). If *array*, the placement of the ticks is set via `tickvals` and the tick text is `ticktext`. (*array* is the default value if `tickvals` is provided). If *sync*, the number of ticks will sync with the overlayed axis set by `overlaying` property.", + "description": "Sets the tick mode for this axis. If *auto*, the number of ticks is set via `nticks`. If *linear*, the placement of the ticks is determined by a starting position `tick0` and a tick step `dtick` (*linear* is the default value if `tick0` and `dtick` are provided). If *array*, the placement of the ticks is set via `tickvals` and the tick text is `ticktext`. (*array* is the default value if `tickvals` is provided). If *sync*, the number of ticks will sync with the overlayed axis set by `overlaying` property. When no other tick info is provided, overlaying (non-categorical) axes default to *sync*, while other axes default to *auto*.", "editType": "ticks", "impliedEdits": {}, "valType": "enumerated",