diff --git a/draftlogs/6356_add.md b/draftlogs/6356_add.md new file mode 100644 index 00000000000..929da457d81 --- /dev/null +++ b/draftlogs/6356_add.md @@ -0,0 +1 @@ +- Add `sync` tickmode option [[#6356](https://github.com/plotly/plotly.js/pull/6356)] \ No newline at end of file diff --git a/src/components/colorbar/attributes.js b/src/components/colorbar/attributes.js index 0c1563bbacc..9811ca15da6 100644 --- a/src/components/colorbar/attributes.js +++ b/src/components/colorbar/attributes.js @@ -129,7 +129,7 @@ module.exports = overrideAll({ description: 'Sets the color of padded area.' }, // tick and title properties named and function exactly as in axes - tickmode: axesAttrs.tickmode, + tickmode: axesAttrs.minor.tickmode, nticks: axesAttrs.nticks, tick0: axesAttrs.tick0, dtick: axesAttrs.dtick, diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 52ea00ddb09..f53c187c8c9 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -951,6 +951,13 @@ axes.calcTicks = function calcTicks(ax, opts) { continue; } + // fill tickVals based on overlaying axis + if(mockAx.tickmode === 'sync') { + tickVals = []; + ticksOut = syncTicks(ax); + continue; + } + // add a tiny bit so we get ticks which may have rounded out var exRng = expandRange(rng); var startTick = exRng[0]; @@ -1203,6 +1210,51 @@ axes.calcTicks = function calcTicks(ax, opts) { return ticksOut; }; +function filterRangeBreaks(ax, ticksOut) { + if(ax.rangebreaks) { + // remove ticks falling inside rangebreaks + ticksOut = ticksOut.filter(function(d) { + return ax.maskBreaks(d.x) !== BADNUM; + }); + } + + return ticksOut; +} + +function syncTicks(ax) { + // get the overlaying axis + var baseAxis = ax._mainAxis; + + var ticksOut = []; + if(baseAxis._vals) { + for(var i = 0; i < baseAxis._vals.length; i++) { + // filter vals with noTick flag + if(baseAxis._vals[i].noTick) { + continue; + } + + // get the position of the every tick + var pos = baseAxis.l2p(baseAxis._vals[i].x); + + // get the tick for the current axis based on position + var vali = ax.p2l(pos); + var obj = axes.tickText(ax, vali); + + // assign minor ticks + if(baseAxis._vals[i].minor) { + obj.minor = true; + obj.text = ''; + } + + ticksOut.push(obj); + } + } + + ticksOut = filterRangeBreaks(ax, ticksOut); + + return ticksOut; +} + function arrayTicks(ax) { var rng = Lib.simpleMap(ax.range, ax.r2l); var exRng = expandRange(rng); @@ -1249,12 +1301,7 @@ function arrayTicks(ax) { } } - if(ax.rangebreaks) { - // remove ticks falling inside rangebreaks - ticksOut = ticksOut.filter(function(d) { - return ax.maskBreaks(d.x) !== BADNUM; - }); - } + ticksOut = filterRangeBreaks(ax, ticksOut); return ticksOut; } @@ -2256,6 +2303,18 @@ axes.draw = function(gd, arg, opts) { return ax.overlaying; }); + // order axes that have dependency to other axes + axList.map(function(axId) { + var ax = axes.getFromId(gd, axId); + + if(ax.tickmode === 'sync' && ax.overlaying) { + var overlayingIndex = axList.findIndex(function(axis) {return axis === ax.overlaying;}); + + if(overlayingIndex >= 0) { + axList.unshift(axList.splice(overlayingIndex, 1).shift()); + } + } + }); var axShifts = {'false': {'left': 0, 'right': 0}}; @@ -3247,6 +3306,11 @@ axes.drawTicks = function(gd, ax, opts) { axes.drawGrid = function(gd, ax, opts) { opts = opts || {}; + if(ax.tickmode === 'sync') { + // for tickmode sync we use the overlaying axis grid + return; + } + var cls = ax._id + 'grid'; var hasMinor = ax.minor && ax.minor.showgrid; diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index dbe068226a7..1e4b889c152 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -727,15 +727,23 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } } + function pushActiveAxIdsSynced(axList, axisType) { + for(i = 0; i < axList.length; i++) { + if(!axList[i].fixedrange && axList[i][axisType]) {activeAxIds.push(axList[i][axisType]._id);} + } + } + if(editX) { pushActiveAxIds(xaxes); pushActiveAxIds(links.xaxes); pushActiveAxIds(matches.xaxes); + pushActiveAxIdsSynced(plotinfo.overlays, 'xaxis'); } if(editY) { pushActiveAxIds(yaxes); pushActiveAxIds(links.yaxes); pushActiveAxIds(matches.yaxes); + pushActiveAxIdsSynced(plotinfo.overlays, 'yaxis'); } updates = {}; diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 66786bc819b..80a39a73be2 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -12,7 +12,7 @@ var constants = require('./constants'); var HOUR = constants.HOUR_PATTERN; var DAY_OF_WEEK = constants.WEEKDAY_PATTERN; -var tickmode = { +var minorTickmode = { valType: 'enumerated', values: ['auto', 'linear', 'array'], editType: 'ticks', @@ -29,6 +29,15 @@ var tickmode = { ].join(' ') }; +var tickmode = extendFlat({}, minorTickmode, { + values: minorTickmode.values.slice().concat(['sync']), + description: [ + minorTickmode.description, + 'If *sync*, the number of ticks will sync with the overlayed axis', + 'set by `overlaying` property.' + ].join(' ') +}); + function makeNticks(minor) { return { valType: 'integer', @@ -947,7 +956,7 @@ module.exports = { }, minor: { - tickmode: tickmode, + tickmode: minorTickmode, nticks: makeNticks('minor'), tick0: tick0, dtick: dtick, diff --git a/src/plots/cartesian/position_defaults.js b/src/plots/cartesian/position_defaults.js index cd693439661..907501830ce 100644 --- a/src/plots/cartesian/position_defaults.js +++ b/src/plots/cartesian/position_defaults.js @@ -83,6 +83,12 @@ module.exports = function handlePositionDefaults(containerIn, containerOut, coer // which applied in the calculation below: if(domain[0] > domain[1] - 1 / 4096) containerOut.domain = dfltDomain; Lib.noneOrAll(containerIn.domain, containerOut.domain, dfltDomain); + + // tickmode sync needs an overlaying axis, otherwise + // we should default it to 'auto' + if(containerOut.tickmode === 'sync') { + containerOut.tickmode = 'auto'; + } } coerce('layer'); diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index e6ffb8f6a5d..92d83983186 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -26,7 +26,7 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe 'auto'; var tickmode = coerce(prefix + 'tickmode', tickmodeDefault); - if(tickmode === 'auto') { + if(tickmode === 'auto' || tickmode === 'sync') { coerce(prefix + 'nticks'); } else if(tickmode === 'linear') { // dtick is usually a positive number, but there are some diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index 64520c93dad..9c2732ad375 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -74,7 +74,7 @@ module.exports = overrideAll({ anim: false }), // ticks - tickmode: axesAttrs.tickmode, + tickmode: axesAttrs.minor.tickmode, nticks: axesAttrs.nticks, tick0: axesAttrs.tick0, dtick: axesAttrs.dtick, diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js index 09c93888c97..41321f262a4 100644 --- a/src/plots/polar/layout_attributes.js +++ b/src/plots/polar/layout_attributes.js @@ -23,7 +23,7 @@ var axisLineGridAttr = overrideAll({ }, 'plot', 'from-root'); var axisTickAttrs = overrideAll({ - tickmode: axesAttrs.tickmode, + tickmode: axesAttrs.minor.tickmode, nticks: axesAttrs.nticks, tick0: axesAttrs.tick0, dtick: axesAttrs.dtick, diff --git a/src/plots/ternary/layout_attributes.js b/src/plots/ternary/layout_attributes.js index 84d4cd5d936..126ebdcea37 100644 --- a/src/plots/ternary/layout_attributes.js +++ b/src/plots/ternary/layout_attributes.js @@ -15,7 +15,7 @@ var ternaryAxesAttrs = { }, color: axesAttrs.color, // ticks - tickmode: axesAttrs.tickmode, + tickmode: axesAttrs.minor.tickmode, nticks: extendFlat({}, axesAttrs.nticks, {dflt: 6, min: 1}), tick0: axesAttrs.tick0, dtick: axesAttrs.dtick, diff --git a/src/traces/indicator/attributes.js b/src/traces/indicator/attributes.js index eee26149754..56b349a945f 100644 --- a/src/traces/indicator/attributes.js +++ b/src/traces/indicator/attributes.js @@ -307,7 +307,7 @@ module.exports = { dflt: true }), // tick and title properties named and function exactly as in axes - tickmode: axesAttrs.tickmode, + tickmode: axesAttrs.minor.tickmode, nticks: axesAttrs.nticks, tick0: axesAttrs.tick0, dtick: axesAttrs.dtick, diff --git a/test/image/baselines/z-new_tickmode_sync.png b/test/image/baselines/z-new_tickmode_sync.png new file mode 100644 index 00000000000..e4db19d05c0 Binary files /dev/null and b/test/image/baselines/z-new_tickmode_sync.png differ diff --git a/test/image/mocks/z-new_tickmode_sync.json b/test/image/mocks/z-new_tickmode_sync.json new file mode 100644 index 00000000000..7c2cbd91ed0 --- /dev/null +++ b/test/image/mocks/z-new_tickmode_sync.json @@ -0,0 +1,99 @@ +{ + "data": [ + { + "name": "Apples", + "type": "bar", + "x": [ + "2000-01", + "2000-02", + "2000-03", + "2000-04", + "2000-05" + ], + "y": [ + 232, + 2506, + 470, + 1864, + -190 + ] + }, + { + "name": "Oranges", + "type": "scatter", + "x": [ + "A", + "B", + "C", + "D", + "E" + ], + "y": [ + 0, + 0.4, + 0.1, + 0.3, + 0.2 + ], + "yaxis": "y2", + "xaxis": "x2" + } + ], + "layout": { + "margin": { + "t": 40, + "r": 70, + "b": 40, + "l": 70 + }, + "width": 700, + "showlegend": false, + "xaxis": { + "showgrid": true, + "ticklen": 10, + "tickwidth": 3, + "ticklabelmode": "period" + }, + "xaxis2": { + "anchor": "y2", + "side": "top", + "overlaying": "x", + "tickmode": "sync", + "ticklen": 10, + "tickwidth": 3 + }, + "yaxis": { + "title": { + "text": "Apples" + }, + "side": "left", + "range": [ + 0, + 2500 + ], + "ticklen": 10, + "tickwidth": 3, + "minor": { + "showgrid": true + } + }, + "yaxis2": { + "title": { + "text": "Oranges" + }, + "side": "right", + "range": [ + 0, + 0.5 + ], + "ticklen": 10, + "tickwidth": 3, + "overlaying": "y", + "tickmode": "sync", + "minor": { + "showgrid": true + }, + "zeroline": false + } + } +} diff --git a/test/plot-schema.json b/test/plot-schema.json index cbe172483ac..1863b863a2a 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -11005,14 +11005,15 @@ "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).", + "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.", "editType": "ticks", "impliedEdits": {}, "valType": "enumerated", "values": [ "auto", "linear", - "array" + "array", + "sync" ] }, "tickprefix": { @@ -12013,14 +12014,15 @@ "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).", + "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.", "editType": "ticks", "impliedEdits": {}, "valType": "enumerated", "values": [ "auto", "linear", - "array" + "array", + "sync" ] }, "tickprefix": {