diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js
index ada758b9955..5b653c02884 100644
--- a/src/components/legend/defaults.js
+++ b/src/components/legend/defaults.js
@@ -86,7 +86,7 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
coerce('orientation');
if(containerOut.orientation === 'h') {
var xaxis = layoutIn.xaxis;
- if(xaxis && xaxis.rangeslider && xaxis.rangeslider.visible) {
+ if(Registry.getComponentMethod('rangeslider', 'isVisible')(xaxis)) {
defaultX = 0;
defaultXAnchor = 'left';
defaultY = 1.1;
diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js
index 76ef7a04a78..1a7fe176df6 100644
--- a/src/components/rangeslider/draw.js
+++ b/src/components/rangeslider/draw.js
@@ -19,7 +19,7 @@ var Color = require('../color');
var Titles = require('../titles');
var Cartesian = require('../../plots/cartesian');
-var Axes = require('../../plots/cartesian/axes');
+var axisIDs = require('../../plots/cartesian/axis_ids');
var dragElement = require('../dragelement');
var setCursor = require('../../lib/setcursor');
@@ -27,8 +27,13 @@ var setCursor = require('../../lib/setcursor');
var constants = require('./constants');
module.exports = function(gd) {
- var fullLayout = gd._fullLayout,
- rangeSliderData = makeRangeSliderData(fullLayout);
+ var fullLayout = gd._fullLayout;
+ var rangeSliderData = fullLayout._rangeSliderData;
+ for(var i = 0; i < rangeSliderData.length; i++) {
+ var opts = rangeSliderData[i][constants.name];
+ // fullLayout._uid may not exist when we call makeData
+ opts._clipId = opts._id + '-' + fullLayout._uid;
+ }
/*
*
@@ -55,10 +60,6 @@ module.exports = function(gd) {
.selectAll('g.' + constants.containerClassName)
.data(rangeSliderData, keyFunction);
- rangeSliders.enter().append('g')
- .classed(constants.containerClassName, true)
- .attr('pointer-events', 'all');
-
// remove exiting sliders and their corresponding clip paths
rangeSliders.exit().each(function(axisOpts) {
var opts = axisOpts[constants.name];
@@ -68,12 +69,16 @@ module.exports = function(gd) {
// return early if no range slider is visible
if(rangeSliderData.length === 0) return;
+ rangeSliders.enter().append('g')
+ .classed(constants.containerClassName, true)
+ .attr('pointer-events', 'all');
+
// for all present range sliders
rangeSliders.each(function(axisOpts) {
- var rangeSlider = d3.select(this),
- opts = axisOpts[constants.name],
- oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)],
- oppAxisRangeOpts = opts[Axes.id2name(axisOpts.anchor)];
+ var rangeSlider = d3.select(this);
+ var opts = axisOpts[constants.name];
+ var oppAxisOpts = fullLayout[axisIDs.id2name(axisOpts.anchor)];
+ var oppAxisRangeOpts = opts[axisIDs.id2name(axisOpts.anchor)];
// update range
// Expand slider range to the axis range
@@ -97,19 +102,9 @@ module.exports = function(gd) {
var domain = axisOpts.domain;
var tickHeight = (axisOpts._boundingBox || {}).height || 0;
- var oppBottom = Infinity;
- var subplotData = Axes.getSubplots(gd, axisOpts);
- for(var i = 0; i < subplotData.length; i++) {
- var oppAxis = Axes.getFromId(gd, subplotData[i].substr(subplotData[i].indexOf('y')));
- oppBottom = Math.min(oppBottom, oppAxis.domain[0]);
- }
-
- opts._id = constants.name + axisOpts._id;
- opts._clipId = opts._id + '-' + fullLayout._uid;
+ var oppBottom = opts._oppBottom;
opts._width = graphSize.w * (domain[1] - domain[0]);
- opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness;
- opts._offsetShift = Math.floor(opts.borderwidth / 2);
var x = Math.round(margin.l + (graphSize.w * domain[0]));
@@ -177,36 +172,9 @@ module.exports = function(gd) {
}
});
}
-
- // update margins
- Plots.autoMargin(gd, opts._id, {
- x: domain[0],
- y: oppBottom,
- l: 0,
- r: 0,
- t: 0,
- b: opts._height + margin.b + tickHeight,
- pad: constants.extraPad + opts._offsetShift * 2
- });
});
};
-function makeRangeSliderData(fullLayout) {
- var axes = Axes.list({ _fullLayout: fullLayout }, 'x', true),
- name = constants.name,
- out = [];
-
- if(fullLayout._has('gl2d')) return out;
-
- for(var i = 0; i < axes.length; i++) {
- var ax = axes[i];
-
- if(ax[name] && ax[name].visible) out.push(ax);
- }
-
- return out;
-}
-
function setupDragElement(rangeSlider, gd, axisOpts, opts) {
var slideBox = rangeSlider.select('rect.' + constants.slideBoxClassName).node(),
grabAreaMin = rangeSlider.select('rect.' + constants.grabAreaMinClassName).node(),
@@ -393,11 +361,10 @@ function addClipPath(rangeSlider, gd, axisOpts, opts) {
}
function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
- var subplotData = Axes.getSubplots(gd, axisOpts),
- calcData = gd.calcdata;
+ var calcData = gd.calcdata;
var rangePlots = rangeSlider.selectAll('g.' + constants.rangePlotClassName)
- .data(subplotData, Lib.identity);
+ .data(axisOpts._subplotsWith, Lib.identity);
rangePlots.enter().append('g')
.attr('class', function(id) { return constants.rangePlotClassName + ' ' + id; })
@@ -413,7 +380,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
var plotgroup = d3.select(this),
isMainPlot = (i === 0);
- var oppAxisOpts = Axes.getFromId(gd, id, 'y'),
+ var oppAxisOpts = axisIDs.getFromId(gd, id, 'y'),
oppAxisName = oppAxisOpts._name,
oppAxisRangeOpts = opts[oppAxisName];
@@ -445,6 +412,11 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
var xa = mockFigure._fullLayout.xaxis;
var ya = mockFigure._fullLayout[oppAxisName];
+ xa.clearCalc();
+ xa.setScale();
+ ya.clearCalc();
+ ya.setScale();
+
var plotinfo = {
id: id,
plotgroup: plotgroup,
diff --git a/src/components/rangeslider/helpers.js b/src/components/rangeslider/helpers.js
new file mode 100644
index 00000000000..6009d0d51a1
--- /dev/null
+++ b/src/components/rangeslider/helpers.js
@@ -0,0 +1,67 @@
+/**
+* Copyright 2012-2018, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var axisIDs = require('../../plots/cartesian/axis_ids');
+var constants = require('./constants');
+var name = constants.name;
+
+function isVisible(ax) {
+ var rangeSlider = ax && ax[name];
+ return rangeSlider && rangeSlider.visible;
+}
+exports.isVisible = isVisible;
+
+exports.makeData = function(fullLayout) {
+ var axes = axisIDs.list({ _fullLayout: fullLayout }, 'x', true);
+ var margin = fullLayout.margin;
+ var rangeSliderData = [];
+
+ if(!fullLayout._has('gl2d')) {
+ for(var i = 0; i < axes.length; i++) {
+ var ax = axes[i];
+
+ if(isVisible(ax)) {
+ rangeSliderData.push(ax);
+
+ var opts = ax[name];
+ opts._id = name + ax._id;
+ opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness;
+ opts._offsetShift = Math.floor(opts.borderwidth / 2);
+ }
+ }
+ }
+
+ fullLayout._rangeSliderData = rangeSliderData;
+};
+
+exports.autoMarginOpts = function(gd, ax) {
+ var opts = ax[name];
+
+ var oppBottom = Infinity;
+ var counterAxes = ax._counterAxes;
+ for(var j = 0; j < counterAxes.length; j++) {
+ var counterId = counterAxes[j];
+ var oppAxis = axisIDs.getFromId(gd, counterId);
+ oppBottom = Math.min(oppBottom, oppAxis.domain[0]);
+ }
+ opts._oppBottom = oppBottom;
+
+ var tickHeight = (ax.side === 'bottom' && ax._boundingBox.height) || 0;
+
+ return {
+ x: 0,
+ y: oppBottom,
+ l: 0,
+ r: 0,
+ t: 0,
+ b: opts._height + gd._fullLayout.margin.b + tickHeight,
+ pad: constants.extraPad + opts._offsetShift * 2
+ };
+};
diff --git a/src/components/rangeslider/index.js b/src/components/rangeslider/index.js
index 2983d72c58e..fd3395ff114 100644
--- a/src/components/rangeslider/index.js
+++ b/src/components/rangeslider/index.js
@@ -11,6 +11,7 @@
var Lib = require('../../lib');
var attrs = require('./attributes');
var oppAxisAttrs = require('./oppaxis_attributes');
+var helpers = require('./helpers');
module.exports = {
moduleType: 'component',
@@ -29,5 +30,8 @@ module.exports = {
layoutAttributes: require('./attributes'),
handleDefaults: require('./defaults'),
calcAutorange: require('./calc_autorange'),
- draw: require('./draw')
+ draw: require('./draw'),
+ isVisible: helpers.isVisible,
+ makeData: helpers.makeData,
+ autoMarginOpts: helpers.autoMarginOpts
};
diff --git a/src/components/shapes/calc_autorange.js b/src/components/shapes/calc_autorange.js
index a05a6bc11c2..b1aa79ad935 100644
--- a/src/components/shapes/calc_autorange.js
+++ b/src/components/shapes/calc_autorange.js
@@ -6,7 +6,6 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var Lib = require('../../lib');
@@ -84,7 +83,7 @@ function calcPaddingOptions(lineWidth, sizeMode, v0, v1, path, isYAxis) {
}
function shapeBounds(ax, v0, v1, path, paramsToUse) {
- var convertVal = (ax.type === 'category') ? ax.r2c : ax.d2c;
+ var convertVal = (ax.type === 'category' || ax.type === 'multicategory') ? ax.r2c : ax.d2c;
if(v0 !== undefined) return [convertVal(v0), convertVal(v1)];
if(!path) return;
diff --git a/src/lib/array.js b/src/lib/array.js
index a80f4b3f8ca..cd96886d34e 100644
--- a/src/lib/array.js
+++ b/src/lib/array.js
@@ -132,3 +132,26 @@ exports.concat = function() {
}
return out;
};
+
+exports.maxRowLength = function(z) {
+ return _rowLength(z, Math.max, 0);
+};
+
+exports.minRowLength = function(z) {
+ return _rowLength(z, Math.min, Infinity);
+};
+
+function _rowLength(z, fn, len0) {
+ if(isArrayOrTypedArray(z)) {
+ if(isArrayOrTypedArray(z[0])) {
+ var len = len0;
+ for(var i = 0; i < z.length; i++) {
+ len = fn(len, z[i].length);
+ }
+ return len;
+ } else {
+ return z.length;
+ }
+ }
+ return 0;
+}
diff --git a/src/lib/index.js b/src/lib/index.js
index 2d9de4b1ac1..9c3d46c7841 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -31,6 +31,8 @@ lib.isArrayOrTypedArray = arrayModule.isArrayOrTypedArray;
lib.isArray1D = arrayModule.isArray1D;
lib.ensureArray = arrayModule.ensureArray;
lib.concat = arrayModule.concat;
+lib.maxRowLength = arrayModule.maxRowLength;
+lib.minRowLength = arrayModule.minRowLength;
var modModule = require('./mod');
lib.mod = modModule.mod;
diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index 828797411f2..d95a31ca72e 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -332,8 +332,7 @@ exports.plot = function(gd, data, layout, config) {
return Lib.syncOrAsync([
Registry.getComponentMethod('shapes', 'calcAutorange'),
Registry.getComponentMethod('annotations', 'calcAutorange'),
- doAutoRangeAndConstraints,
- Registry.getComponentMethod('rangeslider', 'calcAutorange')
+ doAutoRangeAndConstraints
], gd);
}
@@ -345,6 +344,11 @@ exports.plot = function(gd, data, layout, config) {
// store initial ranges *after* enforcing constraints, otherwise
// we will never look like we're at the initial ranges
if(graphWasEmpty) Axes.saveRangeInitial(gd);
+
+ // this one is different from shapes/annotations calcAutorange
+ // the others incorporate those components into ax._extremes,
+ // this one actually sets the ranges in rangesliders.
+ Registry.getComponentMethod('rangeslider', 'calcAutorange')(gd);
}
// draw ticks, titles, and calculate axis scaling (._b, ._m)
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index 68874a4eae7..3033462e785 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -55,7 +55,7 @@ function lsInner(gd) {
var gs = fullLayout._size;
var pad = gs.p;
var axList = Axes.list(gd, '', true);
- var i, subplot, plotinfo, xa, ya;
+ var i, subplot, plotinfo, ax, xa, ya;
fullLayout._paperdiv.style({
width: (gd._context.responsive && fullLayout.autosize && !gd._context._hasZeroWidth && !gd.layout.width) ? '100%' : fullLayout.width + 'px',
@@ -91,10 +91,7 @@ function lsInner(gd) {
// some preparation of axis position info
for(i = 0; i < axList.length; i++) {
- var ax = axList[i];
-
- // reset scale in case the margins have changed
- ax.setScale();
+ ax = axList[i];
var counterAx = ax._anchorAxis;
@@ -113,11 +110,6 @@ function lsInner(gd) {
ax._mainMirrorPosition = (ax.mirror && counterAx) ?
getLinePosition(ax, counterAx,
alignmentConstants.OPPOSITE_SIDE[ax.side]) : null;
-
- // Figure out which subplot to draw ticks, labels, & axis lines on
- // do this as a separate loop so we already have all the
- // _mainAxis and _anchorAxis links set
- ax._mainSubplot = findMainSubplot(ax, fullLayout);
}
// figure out which backgrounds we need to draw,
@@ -358,48 +350,6 @@ function lsInner(gd) {
return gd._promises.length && Promise.all(gd._promises);
}
-function findMainSubplot(ax, fullLayout) {
- var subplotList = fullLayout._subplots;
- var ids = subplotList.cartesian.concat(subplotList.gl2d || []);
- var mockGd = {_fullLayout: fullLayout};
-
- var isX = ax._id.charAt(0) === 'x';
- var anchorAx = ax._mainAxis._anchorAxis;
- var mainSubplotID = '';
- var nextBestMainSubplotID = '';
- var anchorID = '';
-
- // First try the main ID with the anchor
- if(anchorAx) {
- anchorID = anchorAx._mainAxis._id;
- mainSubplotID = isX ? (ax._id + anchorID) : (anchorID + ax._id);
- }
-
- // Then look for a subplot with the counteraxis overlaying the anchor
- // If that fails just use the first subplot including this axis
- if(!mainSubplotID || !fullLayout._plots[mainSubplotID]) {
- mainSubplotID = '';
-
- for(var j = 0; j < ids.length; j++) {
- var id = ids[j];
- var yIndex = id.indexOf('y');
- var idPart = isX ? id.substr(0, yIndex) : id.substr(yIndex);
- var counterPart = isX ? id.substr(yIndex) : id.substr(0, yIndex);
-
- if(idPart === ax._id) {
- if(!nextBestMainSubplotID) nextBestMainSubplotID = id;
- var counterAx = Axes.getFromId(mockGd, counterPart);
- if(anchorID && counterAx.overlaying === anchorID) {
- mainSubplotID = id;
- break;
- }
- }
- }
- }
-
- return mainSubplotID || nextBestMainSubplotID;
-}
-
function shouldShowLinesOrTicks(ax, subplot) {
return (ax.ticks || ax.showline) &&
(subplot === ax._mainSubplot || ax.mirror === 'all' || ax.mirror === 'allticks');
@@ -752,6 +702,8 @@ exports.doAutoRangeAndConstraints = function(gd) {
for(var i = 0; i < axList.length; i++) {
var ax = axList[i];
cleanAxisConstraints(gd, ax);
+ // in case margins changed, update scale
+ ax.setScale();
doAutoRange(gd, ax);
}
diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js
index 3fe613d5e7d..c77914096f7 100644
--- a/src/plots/cartesian/autorange.js
+++ b/src/plots/cartesian/autorange.js
@@ -237,10 +237,6 @@ function concatExtremes(gd, ax) {
}
function doAutoRange(gd, ax) {
- if(!ax._length) ax.setScale();
-
- var axIn;
-
if(ax.autorange) {
ax.range = getAutoRange(gd, ax);
@@ -250,7 +246,7 @@ function doAutoRange(gd, ax) {
// doAutoRange will get called on fullLayout,
// but we want to report its results back to layout
- axIn = ax._input;
+ var axIn = ax._input;
// before we edit _input, store preGUI values
var edits = {};
@@ -262,15 +258,16 @@ function doAutoRange(gd, ax) {
axIn.autorange = ax.autorange;
}
- if(ax._anchorAxis && ax._anchorAxis.rangeslider) {
- var axeRangeOpts = ax._anchorAxis.rangeslider[ax._name];
+ var anchorAx = ax._anchorAxis;
+
+ if(anchorAx && anchorAx.rangeslider) {
+ var axeRangeOpts = anchorAx.rangeslider[ax._name];
if(axeRangeOpts) {
if(axeRangeOpts.rangemode === 'auto') {
axeRangeOpts.range = getAutoRange(gd, ax);
}
}
- axIn = ax._anchorAxis._input;
- axIn.rangeslider[ax._name] = Lib.extendFlat({}, axeRangeOpts);
+ anchorAx._input.rangeslider[ax._name] = Lib.extendFlat({}, axeRangeOpts);
}
}
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index f45b50d6318..9adbe25fb55 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -157,6 +157,7 @@ var getDataConversions = axes.getDataConversions = function(gd, trace, target, t
ax.d2c(targetArray[i]);
}
}
+ // TODO what to do for transforms?
} else {
ax = axes.getFromTrace(gd, trace, d2cTarget);
}
@@ -197,7 +198,7 @@ axes.counterLetter = function(id) {
axes.minDtick = function(ax, newDiff, newFirst, allow) {
// doesn't make sense to do forced min dTick on log or category axes,
// and the plot itself may decide to cancel (ie non-grouped bars)
- if(['log', 'category'].indexOf(ax.type) !== -1 || !allow) {
+ if(['log', 'category', 'multicategory'].indexOf(ax.type) !== -1 || !allow) {
ax._minDtick = 0;
}
// undefined means there's nothing there yet
@@ -229,18 +230,15 @@ axes.minDtick = function(ax, newDiff, newFirst, allow) {
// save a copy of the initial axis ranges in fullLayout
// use them in mode bar and dblclick events
axes.saveRangeInitial = function(gd, overwrite) {
- var axList = axes.list(gd, '', true),
- hasOneAxisChanged = false;
+ var axList = axes.list(gd, '', true);
+ var hasOneAxisChanged = false;
for(var i = 0; i < axList.length; i++) {
var ax = axList[i];
-
var isNew = (ax._rangeInitial === undefined);
- var hasChanged = (
- isNew || !(
- ax.range[0] === ax._rangeInitial[0] &&
- ax.range[1] === ax._rangeInitial[1]
- )
+ var hasChanged = isNew || !(
+ ax.range[0] === ax._rangeInitial[0] &&
+ ax.range[1] === ax._rangeInitial[1]
);
if((isNew && ax.autorange === false) || (overwrite && hasChanged)) {
@@ -254,21 +252,16 @@ axes.saveRangeInitial = function(gd, overwrite) {
// save a copy of the initial spike visibility
axes.saveShowSpikeInitial = function(gd, overwrite) {
- var axList = axes.list(gd, '', true),
- hasOneAxisChanged = false,
- allSpikesEnabled = 'on';
+ var axList = axes.list(gd, '', true);
+ var hasOneAxisChanged = false;
+ var allSpikesEnabled = 'on';
for(var i = 0; i < axList.length; i++) {
var ax = axList[i];
-
var isNew = (ax._showSpikeInitial === undefined);
- var hasChanged = (
- isNew || !(
- ax.showspikes === ax._showspikes
- )
- );
+ var hasChanged = isNew || !(ax.showspikes === ax._showspikes);
- if((isNew) || (overwrite && hasChanged)) {
+ if(isNew || (overwrite && hasChanged)) {
ax._showSpikeInitial = ax.showspikes;
hasOneAxisChanged = true;
}
@@ -285,7 +278,7 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar, size) {
var dataMin = Lib.aggNums(Math.min, null, data);
var dataMax = Lib.aggNums(Math.max, null, data);
- if(ax.type === 'category') {
+ if(ax.type === 'category' || ax.type === 'multicategory') {
return {
start: dataMin - 0.5,
end: dataMax + 0.5,
@@ -303,8 +296,7 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar, size) {
type: 'linear',
range: [dataMin, dataMax]
};
- }
- else {
+ } else {
dummyAx = {
type: ax.type,
range: Lib.simpleMap([dataMin, dataMax], ax.c2r, 0, calendar),
@@ -389,10 +381,10 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar, size) {
function autoShiftNumericBins(binStart, data, ax, dataMin, dataMax) {
- var edgecount = 0,
- midcount = 0,
- intcount = 0,
- blankCount = 0;
+ var edgecount = 0;
+ var midcount = 0;
+ var intcount = 0;
+ var blankCount = 0;
function nearEdge(v) {
// is a value within 1% of a bin edge?
@@ -483,14 +475,14 @@ axes.prepTicks = function(ax) {
// calculate max number of (auto) ticks to display based on plot size
if(ax.tickmode === 'auto' || !ax.dtick) {
- var nt = ax.nticks,
- minPx;
+ var nt = ax.nticks;
+ var minPx;
+
if(!nt) {
- if(ax.type === 'category') {
+ if(ax.type === 'category' || ax.type === 'multicategory') {
minPx = ax.tickfont ? (ax.tickfont.size || 12) * 1.2 : 15;
nt = ax._length / minPx;
- }
- else {
+ } else {
minPx = ax._id.charAt(0) === 'y' ? 40 : 80;
nt = Lib.constrain(ax._length / minPx, 4, 9) + 1;
}
@@ -552,7 +544,7 @@ axes.calcTicks = function calcTicks(ax) {
// return the full set of tick vals
var vals = [];
- if(ax.type === 'category') {
+ if(ax.type === 'category' || ax.type === 'multicategory') {
endTick = (axrev) ? Math.max(-0.5, endTick) :
Math.min(ax._categories.length - 0.5, endTick);
}
@@ -596,23 +588,22 @@ axes.calcTicks = function calcTicks(ax) {
};
function arrayTicks(ax) {
- var vals = ax.tickvals,
- text = ax.ticktext,
- ticksOut = new Array(vals.length),
- rng = Lib.simpleMap(ax.range, ax.r2l),
- r0expanded = rng[0] * 1.0001 - rng[1] * 0.0001,
- r1expanded = rng[1] * 1.0001 - rng[0] * 0.0001,
- tickMin = Math.min(r0expanded, r1expanded),
- tickMax = Math.max(r0expanded, r1expanded),
- vali,
- i,
- j = 0;
+ var vals = ax.tickvals;
+ var text = ax.ticktext;
+ var ticksOut = new Array(vals.length);
+ var rng = Lib.simpleMap(ax.range, ax.r2l);
+ var r0expanded = rng[0] * 1.0001 - rng[1] * 0.0001;
+ var r1expanded = rng[1] * 1.0001 - rng[0] * 0.0001;
+ var tickMin = Math.min(r0expanded, r1expanded);
+ var tickMax = Math.max(r0expanded, r1expanded);
+ var j = 0;
// without a text array, just format the given values as any other ticks
// except with more precision to the numbers
if(!Array.isArray(text)) text = [];
// make sure showing ticks doesn't accidentally add new categories
+ // TODO multicategory, if we allow ticktext / tickvals
var tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l;
// array ticks on log axes always show the full number
@@ -621,8 +612,8 @@ function arrayTicks(ax) {
ax.dtick = 'L' + Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1);
}
- for(i = 0; i < vals.length; i++) {
- vali = tickVal2l(vals[i]);
+ for(var i = 0; i < vals.length; i++) {
+ var vali = tickVal2l(vals[i]);
if(vali > tickMin && vali < tickMax) {
if(text[i] === undefined) ticksOut[j] = axes.tickText(ax, vali);
else ticksOut[j] = tickTextObj(ax, vali, String(text[i]));
@@ -635,17 +626,17 @@ function arrayTicks(ax) {
return ticksOut;
}
-var roundBase10 = [2, 5, 10],
- roundBase24 = [1, 2, 3, 6, 12],
- roundBase60 = [1, 2, 5, 10, 15, 30],
- // 2&3 day ticks are weird, but need something btwn 1&7
- roundDays = [1, 2, 3, 7, 14],
- // approx. tick positions for log axes, showing all (1) and just 1, 2, 5 (2)
- // these don't have to be exact, just close enough to round to the right value
- roundLog1 = [-0.046, 0, 0.301, 0.477, 0.602, 0.699, 0.778, 0.845, 0.903, 0.954, 1],
- roundLog2 = [-0.301, 0, 0.301, 0.699, 1],
- // N.B. `thetaunit; 'radians' angular axes must be converted to degrees
- roundAngles = [15, 30, 45, 90, 180];
+var roundBase10 = [2, 5, 10];
+var roundBase24 = [1, 2, 3, 6, 12];
+var roundBase60 = [1, 2, 5, 10, 15, 30];
+// 2&3 day ticks are weird, but need something btwn 1&7
+var roundDays = [1, 2, 3, 7, 14];
+// approx. tick positions for log axes, showing all (1) and just 1, 2, 5 (2)
+// these don't have to be exact, just close enough to round to the right value
+var roundLog1 = [-0.046, 0, 0.301, 0.477, 0.602, 0.699, 0.778, 0.845, 0.903, 0.954, 1];
+var roundLog2 = [-0.301, 0, 0.301, 0.699, 1];
+// N.B. `thetaunit; 'radians' angular axes must be converted to degrees
+var roundAngles = [15, 30, 45, 90, 180];
function roundDTick(roughDTick, base, roundingSet) {
return base * Lib.roundUp(roughDTick / base, roundingSet);
@@ -736,7 +727,7 @@ axes.autoTicks = function(ax, roughDTick) {
ax.dtick = (roughDTick > 0.3) ? 'D2' : 'D1';
}
}
- else if(ax.type === 'category') {
+ else if(ax.type === 'category' || ax.type === 'multicategory') {
ax.tick0 = 0;
ax.dtick = Math.ceil(Math.max(roughDTick, 1));
}
@@ -776,7 +767,7 @@ function autoTickRound(ax) {
dtick = 1;
}
- if(ax.type === 'category') {
+ if(ax.type === 'category' || ax.type === 'multicategory') {
ax._tickround = null;
}
if(ax.type === 'date') {
@@ -868,36 +859,34 @@ axes.tickIncrement = function(x, dtick, axrev, calendar) {
// calculate the first tick on an axis
axes.tickFirst = function(ax) {
- var r2l = ax.r2l || Number,
- rng = Lib.simpleMap(ax.range, r2l),
- axrev = rng[1] < rng[0],
- sRound = axrev ? Math.floor : Math.ceil,
- // add a tiny extra bit to make sure we get ticks
- // that may have been rounded out
- r0 = rng[0] * 1.0001 - rng[1] * 0.0001,
- dtick = ax.dtick,
- tick0 = r2l(ax.tick0);
+ var r2l = ax.r2l || Number;
+ var rng = Lib.simpleMap(ax.range, r2l);
+ var axrev = rng[1] < rng[0];
+ var sRound = axrev ? Math.floor : Math.ceil;
+ // add a tiny extra bit to make sure we get ticks
+ // that may have been rounded out
+ var r0 = rng[0] * 1.0001 - rng[1] * 0.0001;
+ var dtick = ax.dtick;
+ var tick0 = r2l(ax.tick0);
if(isNumeric(dtick)) {
var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0;
// make sure no ticks outside the category list
- if(ax.type === 'category') {
+ if(ax.type === 'category' || ax.type === 'multicategory') {
tmin = Lib.constrain(tmin, 0, ax._categories.length - 1);
}
return tmin;
}
- var tType = dtick.charAt(0),
- dtNum = Number(dtick.substr(1));
+ var tType = dtick.charAt(0);
+ var dtNum = Number(dtick.substr(1));
// Dates: months (or years)
if(tType === 'M') {
- var cnt = 0,
- t0 = tick0,
- t1,
- mult,
- newDTick;
+ var cnt = 0;
+ var t0 = tick0;
+ var t1, mult, newDTick;
// This algorithm should work for *any* nonlinear (but close to linear!)
// tick spacing. Limit to 10 iterations, for gregorian months it's normally <=3.
@@ -939,16 +928,18 @@ axes.tickFirst = function(ax) {
// hover is a (truthy) flag for whether to show numbers with a bit
// more precision for hovertext
axes.tickText = function(ax, x, hover) {
- var out = tickTextObj(ax, x),
- hideexp,
- arrayMode = ax.tickmode === 'array',
- extraPrecision = hover || arrayMode,
- i,
- tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l;
+ var out = tickTextObj(ax, x);
+ var arrayMode = ax.tickmode === 'array';
+ var extraPrecision = hover || arrayMode;
+ var axType = ax.type;
+ // TODO multicategory, if we allow ticktext / tickvals
+ var tickVal2l = axType === 'category' ? ax.d2l_noadd : ax.d2l;
+ var i;
if(arrayMode && Array.isArray(ax.ticktext)) {
- var rng = Lib.simpleMap(ax.range, ax.r2l),
- minDiff = Math.abs(rng[1] - rng[0]) / 10000;
+ var rng = Lib.simpleMap(ax.range, ax.r2l);
+ var minDiff = Math.abs(rng[1] - rng[0]) / 10000;
+
for(i = 0; i < ax.ticktext.length; i++) {
if(Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break;
}
@@ -959,28 +950,25 @@ axes.tickText = function(ax, x, hover) {
}
function isHidden(showAttr) {
- var first_or_last;
-
if(showAttr === undefined) return true;
if(hover) return showAttr === 'none';
- first_or_last = {
+ var firstOrLast = {
first: ax._tmin,
last: ax._tmax
}[showAttr];
- return showAttr !== 'all' && x !== first_or_last;
+ return showAttr !== 'all' && x !== firstOrLast;
}
- if(hover) {
- hideexp = 'never';
- } else {
- hideexp = ax.exponentformat !== 'none' && isHidden(ax.showexponent) ? 'hide' : '';
- }
+ var hideexp = hover ?
+ 'never' :
+ ax.exponentformat !== 'none' && isHidden(ax.showexponent) ? 'hide' : '';
- if(ax.type === 'date') formatDate(ax, out, hover, extraPrecision);
- else if(ax.type === 'log') formatLog(ax, out, hover, extraPrecision, hideexp);
- else if(ax.type === 'category') formatCategory(ax, out);
+ if(axType === 'date') formatDate(ax, out, hover, extraPrecision);
+ else if(axType === 'log') formatLog(ax, out, hover, extraPrecision, hideexp);
+ else if(axType === 'category') formatCategory(ax, out);
+ else if(axType === 'multicategory') formatMultiCategory(ax, out, hover);
else if(isAngular(ax)) formatAngle(ax, out, hover, extraPrecision, hideexp);
else formatLinear(ax, out, hover, extraPrecision, hideexp);
@@ -988,6 +976,20 @@ axes.tickText = function(ax, x, hover) {
if(ax.tickprefix && !isHidden(ax.showtickprefix)) out.text = ax.tickprefix + out.text;
if(ax.ticksuffix && !isHidden(ax.showticksuffix)) out.text += ax.ticksuffix;
+ // Setup ticks and grid lines boundaries
+ // at 1/2 a 'category' to the left/bottom
+ if(ax.tickson === 'boundaries' || ax.showdividers) {
+ var inbounds = function(v) {
+ var p = ax.l2p(v);
+ return p >= 0 && p <= ax._length ? v : null;
+ };
+
+ out.xbnd = [
+ inbounds(out.x - 0.5),
+ inbounds(out.x + ax.dtick - 0.5)
+ ];
+ }
+
return out;
};
@@ -1037,8 +1039,8 @@ function tickTextObj(ax, x, text) {
}
function formatDate(ax, out, hover, extraPrecision) {
- var tr = ax._tickround,
- fmt = (hover && ax.hoverformat) || axes.getTickFormat(ax);
+ var tr = ax._tickround;
+ var fmt = (hover && ax.hoverformat) || axes.getTickFormat(ax);
if(extraPrecision) {
// second or sub-second precision: extra always shows max digits.
@@ -1047,8 +1049,8 @@ function formatDate(ax, out, hover, extraPrecision) {
else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 4}[tr];
}
- var dateStr = Lib.formatDate(out.x, fmt, tr, ax._dateFormat, ax.calendar, ax._extraFormat),
- headStr;
+ var dateStr = Lib.formatDate(out.x, fmt, tr, ax._dateFormat, ax.calendar, ax._extraFormat);
+ var headStr;
var splitIndex = dateStr.indexOf('\n');
if(splitIndex !== -1) {
@@ -1163,19 +1165,21 @@ function formatCategory(ax, out) {
var tt = ax._categories[Math.round(out.x)];
if(tt === undefined) tt = '';
out.text = String(tt);
+}
- // Setup ticks and grid lines boundaries
- // at 1/2 a 'category' to the left/bottom
- if(ax.tickson === 'boundaries') {
- var inbounds = function(v) {
- var p = ax.l2p(v);
- return p >= 0 && p <= ax._length ? v : null;
- };
+function formatMultiCategory(ax, out, hover) {
+ var v = Math.round(out.x);
+ var cats = ax._categories[v] || [];
+ var tt = cats[1] === undefined ? '' : String(cats[1]);
+ var tt2 = cats[0] === undefined ? '' : String(cats[0]);
- out.xbnd = [
- inbounds(out.x - 0.5),
- inbounds(out.x + ax.dtick - 0.5)
- ];
+ if(hover) {
+ // TODO is this what we want?
+ out.text = tt2 + ' - ' + tt;
+ } else {
+ // setup for secondary labels
+ out.text = tt;
+ out.text2 = tt2;
}
}
@@ -1284,14 +1288,13 @@ function beyondSI(exponent) {
}
function numFormat(v, ax, fmtoverride, hover) {
- // negative?
- var isNeg = v < 0,
- // max number of digits past decimal point to show
- tickRound = ax._tickround,
- exponentFormat = fmtoverride || ax.exponentformat || 'B',
- exponent = ax._tickexponent,
- tickformat = axes.getTickFormat(ax),
- separatethousands = ax.separatethousands;
+ var isNeg = v < 0;
+ // max number of digits past decimal point to show
+ var tickRound = ax._tickround;
+ var exponentFormat = fmtoverride || ax.exponentformat || 'B';
+ var exponent = ax._tickexponent;
+ var tickformat = axes.getTickFormat(ax);
+ var separatethousands = ax.separatethousands;
// special case for hover: set exponent just for this value, and
// add a couple more digits of precision over tick labels
@@ -1464,6 +1467,9 @@ axes.getTickFormat = function(ax) {
// as an array of items like 'xy', 'x2y', 'x2y2'...
// sorted by x (x,x2,x3...) then y
// optionally restrict to only subplots containing axis object ax
+//
+// NOTE: this is currently only used OUTSIDE plotly.js (toolpanel, webapp)
+// ideally we get rid of it there (or just copy this there) and remove it here
axes.getSubplots = function(gd, ax) {
var subplotObj = gd._fullLayout._subplots;
var allSubplots = subplotObj.cartesian.concat(subplotObj.gl2d || []);
@@ -1482,6 +1488,8 @@ axes.getSubplots = function(gd, ax) {
};
// find all subplots with axis 'ax'
+// NOTE: this is only used in axes.getSubplots (only used outside plotly.js) and
+// gl2d/convert (where it restricts axis subplots to only those with gl2d)
axes.findSubplotsWithAxis = function(subplots, ax) {
var axMatch = new RegExp(
(ax._id.charAt(0) === 'x') ? ('^' + ax._id + 'y') : (ax._id + '$')
@@ -1576,6 +1584,10 @@ axes.draw = function(gd, arg, opts) {
plotinfo.xaxislayer.selectAll('.' + xa._id + 'tick').remove();
plotinfo.yaxislayer.selectAll('.' + ya._id + 'tick').remove();
+ if(xa.type === 'multicategory') {
+ plotinfo.xaxislayer.selectAll('.' + xa._id + 'tick2').remove();
+ plotinfo.xaxislayer.selectAll('.' + xa._id + 'divider').remove();
+ }
if(plotinfo.gridlayer) plotinfo.gridlayer.selectAll('path').remove();
if(plotinfo.zerolinelayer) plotinfo.zerolinelayer.selectAll('path').remove();
fullLayout._infolayer.select('.g-' + xa._id + 'title').remove();
@@ -1585,7 +1597,7 @@ axes.draw = function(gd, arg, opts) {
var axList = (!arg || arg === 'redraw') ? axes.listIds(gd) : arg;
- Lib.syncOrAsync(axList.map(function(axId) {
+ return Lib.syncOrAsync(axList.map(function(axId) {
return function() {
if(!axId) return;
@@ -1620,44 +1632,46 @@ axes.drawOne = function(gd, ax, opts) {
var axLetter = axId.charAt(0);
var counterLetter = axes.counterLetter(axId);
var mainSubplot = ax._mainSubplot;
+ var mainLinePosition = ax._mainLinePosition;
+ var mainMirrorPosition = ax._mainMirrorPosition;
var mainPlotinfo = fullLayout._plots[mainSubplot];
- var subplotsWithAx = axes.getSubplots(gd, ax);
+ var mainAxLayer = mainPlotinfo[axLetter + 'axislayer'];
+ var subplotsWithAx = ax._subplotsWith;
var vals = ax._vals = axes.calcTicks(ax);
+ // Add a couple of axis properties that should cause us to recreate
+ // elements. Used in d3 data function.
+ var axInfo = [ax.mirror, mainLinePosition, mainMirrorPosition].join('_');
+ for(i = 0; i < vals.length; i++) {
+ vals[i].axInfo = axInfo;
+ }
+
if(!ax.visible) return;
- var transFn = axes.makeTransFn(ax);
+ // stash selections to avoid DOM queries e.g.
+ // - stash tickLabels selection, so that drawTitle can use it to scoot title
+ ax._selections = {};
+ // stash tick angle (including the computed 'auto' values) per tick-label class
+ ax._tickAngles = {};
+ var transFn = axes.makeTransFn(ax);
+ var tickVals;
// We remove zero lines, grid lines, and inside ticks if they're within 1px of the end
// The key case here is removing zero lines when the axis bound is zero
var valsClipped;
- var tickVals;
- var gridVals;
-
- if(ax.tickson === 'boundaries' && vals.length) {
- // valsBoundaries is not used for labels;
- // no need to worry about the other tickTextObj keys
- var valsBoundaries = [];
- var _push = function(d, bndIndex) {
- var xb = d.xbnd[bndIndex];
- if(xb !== null) {
- valsBoundaries.push(Lib.extendFlat({}, d, {x: xb}));
- }
- };
- for(i = 0; i < vals.length; i++) _push(vals[i], 0);
- _push(vals[i - 1], 1);
- valsClipped = axes.clipEnds(ax, valsBoundaries);
- tickVals = ax.ticks === 'inside' ? valsClipped : valsBoundaries;
- gridVals = valsClipped;
+ if(ax.tickson === 'boundaries') {
+ var boundaryVals = getBoundaryVals(ax, vals);
+ valsClipped = axes.clipEnds(ax, boundaryVals);
+ tickVals = ax.ticks === 'inside' ? valsClipped : boundaryVals;
} else {
valsClipped = axes.clipEnds(ax, vals);
tickVals = ax.ticks === 'inside' ? valsClipped : vals;
- gridVals = valsClipped;
}
- ax._valsClipped = valsClipped;
+ var gridVals = ax._gridVals = valsClipped;
+ var dividerVals = getDividerVals(ax, vals);
if(!fullLayout._hasOnlyLargeSploms) {
// keep track of which subplots (by main conteraxis) we've already
@@ -1679,6 +1693,7 @@ axes.drawOne = function(gd, ax, opts) {
axes.drawGrid(gd, ax, {
vals: gridVals,
+ counterAxis: counterAxis,
layer: plotinfo.gridlayer.select('.' + axId),
path: gridPath,
transFn: transFn
@@ -1696,15 +1711,34 @@ axes.drawOne = function(gd, ax, opts) {
var tickSubplots = [];
if(ax.ticks) {
- var mainTickPath = axes.makeTickPath(ax, ax._mainLinePosition, tickSigns[2]);
+ var mainTickPath = axes.makeTickPath(ax, mainLinePosition, tickSigns[2]);
+ var mirrorTickPath;
+ var fullTickPath;
if(ax._anchorAxis && ax.mirror && ax.mirror !== true) {
- mainTickPath += axes.makeTickPath(ax, ax._mainMirrorPosition, tickSigns[3]);
+ mirrorTickPath = axes.makeTickPath(ax, mainMirrorPosition, tickSigns[3]);
+ fullTickPath = mainTickPath + mirrorTickPath;
+ } else {
+ mirrorTickPath = '';
+ fullTickPath = mainTickPath;
+ }
+
+ var tickPath;
+ if(ax.showdividers && ax.ticks === 'outside' && ax.tickson === 'boundaries') {
+ var dividerLookup = {};
+ for(i = 0; i < dividerVals.length; i++) {
+ dividerLookup[dividerVals[i].x] = 1;
+ }
+ tickPath = function(d) {
+ return dividerLookup[d.x] ? mirrorTickPath : fullTickPath;
+ };
+ } else {
+ tickPath = fullTickPath;
}
axes.drawTicks(gd, ax, {
vals: tickVals,
- layer: mainPlotinfo[axLetter + 'axislayer'],
- path: mainTickPath,
+ layer: mainAxLayer,
+ path: tickPath,
transFn: transFn
});
@@ -1727,33 +1761,55 @@ axes.drawOne = function(gd, ax, opts) {
});
}
- var labelFns = axes.makeLabelFns(ax, ax._mainLinePosition);
- // stash tickLabels selection, so that drawTitle can use it
- // to scoot title w/o having to query the axis layer again
- ax._tickLabels = null;
-
var seq = [];
// tick labels - for now just the main labels.
// TODO: mirror labels, esp for subplots
- if(ax._mainLinePosition) {
+
+ seq.push(function() {
+ var labelFns = axes.makeLabelFns(ax, mainLinePosition);
+ return axes.drawLabels(gd, ax, {
+ vals: vals,
+ layer: mainAxLayer,
+ transFn: transFn,
+ labelXFn: labelFns.labelXFn,
+ labelYFn: labelFns.labelYFn,
+ labelAnchorFn: labelFns.labelAnchorFn,
+ });
+ });
+
+ if(ax.type === 'multicategory') {
+ var labelLength = 0;
+ var pad = {x: 2, y: 10}[axLetter];
+
seq.push(function() {
+ labelLength += getLabelLevelSpan(ax, axId + 'tick') + pad;
+ labelLength += ax._tickAngles[axId + 'tick'] ? ax.tickfont.size * LINE_SPACING : 0;
+ var secondaryPosition = mainLinePosition + labelLength * tickSigns[2];
+ var secondaryLabelFns = axes.makeLabelFns(ax, secondaryPosition);
+
return axes.drawLabels(gd, ax, {
- vals: vals,
- layer: mainPlotinfo[axLetter + 'axislayer'],
+ vals: getSecondaryLabelVals(ax, vals),
+ layer: mainAxLayer,
+ cls: axId + 'tick2',
+ repositionOnUpdate: true,
+ secondary: true,
transFn: transFn,
- labelXFn: labelFns.labelXFn,
- labelYFn: labelFns.labelYFn,
- labelAnchorFn: labelFns.labelAnchorFn,
+ labelXFn: secondaryLabelFns.labelXFn,
+ labelYFn: secondaryLabelFns.labelYFn,
+ labelAnchorFn: secondaryLabelFns.labelAnchorFn,
});
});
- }
- if(!opts.skipTitle &&
- !((ax.rangeslider || {}).visible && ax._boundingBox && ax.side === 'bottom')
- ) {
seq.push(function() {
- return axes.drawTitle(gd, ax);
+ labelLength += getLabelLevelSpan(ax, axId + 'tick2');
+
+ return drawDividers(gd, ax, {
+ vals: dividerVals,
+ layer: mainAxLayer,
+ path: axes.makeTickPath(ax, mainLinePosition, tickSigns[2], labelLength),
+ transFn: transFn
+ });
});
}
@@ -1762,10 +1818,10 @@ axes.drawOne = function(gd, ax, opts) {
range[1] = Math.max(range[1], newRange[1]);
}
- seq.push(function calcBoundingBox() {
+ function calcBoundingBox() {
if(ax.showticklabels) {
var gdBB = gd.getBoundingClientRect();
- var bBox = mainPlotinfo[axLetter + 'axislayer'].node().getBoundingClientRect();
+ var bBox = mainAxLayer.node().getBoundingClientRect();
/*
* the way we're going to use this, the positioning that matters
@@ -1842,38 +1898,143 @@ axes.drawOne = function(gd, ax, opts) {
[ax._boundingBox.right, ax._boundingBox.left]);
}
}
- });
+ }
+
+ var hasRangeSlider = Registry.getComponentMethod('rangeslider', 'isVisible')(ax);
- seq.push(function doAutoMargins() {
- var pushKey = ax._name + '.automargin';
+ function doAutoMargins() {
+ var push, rangeSliderPush;
- if(!ax.automargin) {
- Plots.autoMargin(gd, pushKey);
- return;
+ if(hasRangeSlider) {
+ rangeSliderPush = Registry.getComponentMethod('rangeslider', 'autoMarginOpts')(gd, ax);
}
+ Plots.autoMargin(gd, rangeSliderAutoMarginID(ax), rangeSliderPush);
var s = ax.side.charAt(0);
- var push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0};
+ if(ax.automargin && (!hasRangeSlider || s !== 'b')) {
+ push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0};
- if(axLetter === 'x') {
- push.y = (ax.anchor === 'free' ? ax.position :
- ax._anchorAxis.domain[s === 't' ? 1 : 0]);
- push[s] += ax._boundingBox.height;
+ if(axLetter === 'x') {
+ push.y = (ax.anchor === 'free' ? ax.position :
+ ax._anchorAxis.domain[s === 't' ? 1 : 0]);
+ push[s] += ax._boundingBox.height;
+ } else {
+ push.x = (ax.anchor === 'free' ? ax.position :
+ ax._anchorAxis.domain[s === 'r' ? 1 : 0]);
+ push[s] += ax._boundingBox.width;
+ }
+
+ if(ax.title.text !== fullLayout._dfltTitle[axLetter]) {
+ push[s] += ax.title.font.size;
+ }
+ }
+
+ Plots.autoMargin(gd, axAutoMarginID(ax), push);
+ }
+
+ seq.push(calcBoundingBox, doAutoMargins);
+
+ if(!opts.skipTitle &&
+ !(hasRangeSlider && ax._boundingBox && ax.side === 'bottom')
+ ) {
+ seq.push(function() { return drawTitle(gd, ax); });
+ }
+
+ return Lib.syncOrAsync(seq);
+};
+
+function getBoundaryVals(ax, vals) {
+ var out = [];
+ var i;
+
+ // boundaryVals are never used for labels;
+ // no need to worry about the other tickTextObj keys
+ var _push = function(d, bndIndex) {
+ var xb = d.xbnd[bndIndex];
+ if(xb !== null) {
+ out.push(Lib.extendFlat({}, d, {x: xb}));
+ }
+ };
+
+ if(vals.length) {
+ for(i = 0; i < vals.length; i++) {
+ _push(vals[i], 0);
+ }
+ _push(vals[i - 1], 1);
+ }
+
+ return out;
+}
+
+function getSecondaryLabelVals(ax, vals) {
+ var out = [];
+ var lookup = {};
+
+ for(var i = 0; i < vals.length; i++) {
+ var d = vals[i];
+ if(lookup[d.text2]) {
+ lookup[d.text2].push(d.x);
} else {
- push.x = (ax.anchor === 'free' ? ax.position :
- ax._anchorAxis.domain[s === 'r' ? 1 : 0]);
- push[s] += ax._boundingBox.width;
+ lookup[d.text2] = [d.x];
}
+ }
+
+ for(var k in lookup) {
+ out.push(tickTextObj(ax, Lib.interp(lookup[k], 0.5), k));
+ }
+
+ return out;
+}
- if(ax.title.text !== fullLayout._dfltTitle[axLetter]) {
- push[s] += ax.title.font.size;
+function getDividerVals(ax, vals) {
+ var out = [];
+ var i, current;
+
+ // never used for labels;
+ // no need to worry about the other tickTextObj keys
+ var _push = function(d, bndIndex) {
+ var xb = d.xbnd[bndIndex];
+ if(xb !== null) {
+ out.push(Lib.extendFlat({}, d, {x: xb}));
+ }
+ };
+
+ if(ax.showdividers && vals.length) {
+ for(i = 0; i < vals.length; i++) {
+ var d = vals[i];
+ if(d.text2 !== current) {
+ _push(d, 0);
+ }
+ current = d.text2;
}
+ _push(vals[i - 1], 1);
+ }
+
+ return out;
+}
- Plots.autoMargin(gd, pushKey, push);
+function getLabelLevelSpan(ax, cls) {
+ var axLetter = ax._id.charAt(0);
+ var angle = ax._tickAngles[cls] || 0;
+ var rad = Lib.deg2rad(angle);
+ var sinA = Math.sin(rad);
+ var cosA = Math.cos(rad);
+ var maxX = 0;
+ var maxY = 0;
+
+ // N.B. Drawing.bBox does not take into account rotate transforms
+
+ ax._selections[cls].each(function() {
+ var thisLabel = selectTickLabel(this);
+ var bb = Drawing.bBox(thisLabel.node());
+ var w = bb.width;
+ var h = bb.height;
+ maxX = Math.max(maxX, cosA * w, sinA * h);
+ maxY = Math.max(maxY, sinA * w, cosA * h);
});
- return Lib.syncOrAsync(seq);
-};
+ return {x: maxY, y: maxX}[axLetter];
+}
/**
* Which direction do the 'ax.side' values, and free ticks go?
@@ -1926,12 +2087,15 @@ axes.makeTransFn = function(ax) {
* - {number} linewidth
* @param {number} shift along direction of ticklen
* @param {1 or -1} sng tick sign
+ * @param {number (optional)} len tick length
* @return {string}
*/
-axes.makeTickPath = function(ax, shift, sgn) {
+axes.makeTickPath = function(ax, shift, sgn, len) {
+ len = len !== undefined ? len : ax.ticklen;
+
var axLetter = ax._id.charAt(0);
var pad = (ax.linewidth || 1) / 2;
- var len = ax.ticklen;
+
return axLetter === 'x' ?
'M0,' + (shift + pad * sgn) + 'v' + (len * sgn) :
'M' + (shift + pad * sgn) + ',0h' + (len * sgn);
@@ -2016,10 +2180,8 @@ axes.makeLabelFns = function(ax, shift, angle) {
return out;
};
-function makeDataFn(ax) {
- return function(d) {
- return [d.text, d.x, ax.mirror, d.font, d.fontSize, d.fontColor].join('_');
- };
+function tickDataFn(d) {
+ return [d.text, d.x, d.axInfo, d.font, d.fontSize, d.fontColor].join('_');
}
/**
@@ -2044,7 +2206,7 @@ axes.drawTicks = function(gd, ax, opts) {
var cls = ax._id + 'tick';
var ticks = opts.layer.selectAll('path.' + cls)
- .data(ax.ticks ? opts.vals : [], makeDataFn(ax));
+ .data(ax.ticks ? opts.vals : [], tickDataFn);
ticks.exit().remove();
@@ -2074,6 +2236,8 @@ axes.drawTicks = function(gd, ax, opts) {
* @param {object} opts
* - {array of object} vals (calcTicks output-like)
* - {d3 selection} layer
+ * - {object} counterAxis (full axis object corresponding to counter axis)
+ * optional - only required if this axis supports zero lines
* - {string or fn} path
* - {fn} transFn
* - {boolean} crisp (set to false to unset crisp-edge SVG rendering)
@@ -2082,28 +2246,41 @@ axes.drawGrid = function(gd, ax, opts) {
opts = opts || {};
var cls = ax._id + 'grid';
+ var vals = opts.vals;
+ var counterAx = opts.counterAxis;
+ if(ax.showgrid === false) {
+ vals = [];
+ }
+ else if(counterAx && axes.shouldShowZeroLine(gd, ax, counterAx)) {
+ var isArrayMode = ax.tickmode === 'array';
+ for(var i = 0; i < vals.length; i++) {
+ var xi = vals[i].x;
+ if(isArrayMode ? !xi : (Math.abs(xi) < ax.dtick / 100)) {
+ vals = vals.slice(0, i).concat(vals.slice(i + 1));
+ // In array mode you can in principle have multiple
+ // ticks at 0, so test them all. Otherwise once we found
+ // one we can stop.
+ if(isArrayMode) i--;
+ else break;
+ }
+ }
+ }
var grid = opts.layer.selectAll('path.' + cls)
- .data((ax.showgrid === false) ? [] : opts.vals, makeDataFn(ax));
+ .data(vals, tickDataFn);
grid.exit().remove();
grid.enter().append('path')
.classed(cls, 1)
- .classed('crisp', opts.crisp !== false)
- .attr('d', opts.path)
- .each(function(d) {
- if(ax.zeroline && (ax.type === 'linear' || ax.type === '-') &&
- Math.abs(d.x) < ax.dtick / 100) {
- d3.select(this).remove();
- }
- });
+ .classed('crisp', opts.crisp !== false);
- ax._gridWidthCrispRound = Drawing.crispRound(gd, ax.gridwidth, 1);
+ ax._gw = Drawing.crispRound(gd, ax.gridwidth, 1);
grid.attr('transform', opts.transFn)
+ .attr('d', opts.path)
.call(Color.stroke, ax.gridcolor || '#ddd')
- .style('stroke-width', ax._gridWidthCrispRound + 'px');
+ .style('stroke-width', ax._gw + 'px');
if(typeof opts.path === 'function') grid.attr('d', opts.path);
};
@@ -2119,7 +2296,6 @@ axes.drawGrid = function(gd, ax, opts) {
* - {string} zerolinecolor
* - {number (optional)} _gridWidthCrispRound
* @param {object} opts
- * - {array of object} vals (calcTicks output-like)
* - {d3 selection} layer
* - {object} counterAxis (full axis object corresponding to counter axis)
* - {string or fn} path
@@ -2141,7 +2317,6 @@ axes.drawZeroLine = function(gd, ax, opts) {
.classed(cls, 1)
.classed('zl', 1)
.classed('crisp', opts.crisp !== false)
- .attr('d', opts.path)
.each(function() {
// use the fact that only one element can enter to trigger a sort.
// If several zerolines enter at the same time we will sort once per,
@@ -2151,14 +2326,10 @@ axes.drawZeroLine = function(gd, ax, opts) {
});
});
- var strokeWidth = Drawing.crispRound(gd,
- ax.zerolinewidth,
- ax._gridWidthCrispRound || 1
- );
-
zl.attr('transform', opts.transFn)
+ .attr('d', opts.path)
.call(Color.stroke, ax.zerolinecolor || Color.defaultLine)
- .style('stroke-width', strokeWidth + 'px');
+ .style('stroke-width', Drawing.crispRound(gd, ax.zerolinewidth, ax._gw || 1) + 'px');
};
/**
@@ -2169,9 +2340,14 @@ axes.drawZeroLine = function(gd, ax, opts) {
* - {string} _id
* - {boolean} showticklabels
* - {number} tickangle
+ * - {object (optional)} _selections
+ * - {object} (optional)} _tickAngles
* @param {object} opts
* - {array of object} vals (calcTicks output-like)
* - {d3 selection} layer
+ * - {string (optional)} cls (node className)
+ * - {boolean} repositionOnUpdate (set to true to reposition update selection)
+ * - {boolean} secondary
* - {fn} transFn
* - {fn} labelXFn
* - {fn} labelYFn
@@ -2182,14 +2358,16 @@ axes.drawLabels = function(gd, ax, opts) {
var axId = ax._id;
var axLetter = axId.charAt(0);
- var cls = axId + 'tick';
+ var cls = opts.cls || axId + 'tick';
var vals = opts.vals;
var labelXFn = opts.labelXFn;
var labelYFn = opts.labelYFn;
var labelAnchorFn = opts.labelAnchorFn;
+ var tickAngle = opts.secondary ? 0 : ax.tickangle;
+ var lastAngle = (ax._tickAngles || {})[cls];
var tickLabels = opts.layer.selectAll('g.' + cls)
- .data(ax.showticklabels ? vals : [], makeDataFn(ax));
+ .data(ax.showticklabels ? vals : [], tickDataFn);
var labelsReady = [];
@@ -2215,20 +2393,17 @@ axes.drawLabels = function(gd, ax, opts) {
// instead position the label and promise this in
// labelsReady
labelsReady.push(gd._promises.pop().then(function() {
- positionLabels(thisLabel, ax.tickangle);
+ positionLabels(thisLabel, tickAngle);
}));
} else {
// sync label: just position it now.
- positionLabels(thisLabel, ax.tickangle);
+ positionLabels(thisLabel, tickAngle);
}
});
tickLabels.exit().remove();
- ax._tickLabels = tickLabels;
-
- // TODO ??
- if(isAngular(ax)) {
+ if(opts.repositionOnUpdate) {
tickLabels.each(function(d) {
d3.select(this).select('text')
.call(svgTextUtils.positionText, labelXFn(d), labelYFn(d));
@@ -2295,33 +2470,34 @@ axes.drawLabels = function(gd, ax, opts) {
// do this without waiting, using the last calculated angle to
// minimize flicker, then do it again when we know all labels are
// there, putting back the prescribed angle to check for overlaps.
- positionLabels(tickLabels, ax._lastangle || ax.tickangle);
+ positionLabels(tickLabels, lastAngle || tickAngle);
function allLabelsReady() {
return labelsReady.length && Promise.all(labelsReady);
}
function fixLabelOverlaps() {
- positionLabels(tickLabels, ax.tickangle);
+ positionLabels(tickLabels, tickAngle);
+
+ var autoangle = null;
// 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(ax.tickangle) &&
+ if(vals.length && axLetter === 'x' && !isNumeric(tickAngle) &&
(ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D')
) {
+ autoangle = 0;
+
var maxFontSize = 0;
var lbbArray = [];
var i;
tickLabels.each(function(d) {
- var s = d3.select(this);
- var thisLabel = s.select('.text-math-group');
- if(thisLabel.empty()) thisLabel = s.select('text');
-
maxFontSize = Math.max(maxFontSize, d.fontSize);
var x = ax.l2p(d.x);
+ var thisLabel = selectTickLabel(this);
var bb = Drawing.bBox(thisLabel.node());
lbbArray.push({
@@ -2336,12 +2512,12 @@ axes.drawLabels = function(gd, ax, opts) {
});
});
- var autoangle = 0;
-
- if(ax.tickson === 'boundaries') {
+ if((ax.tickson === 'boundaries' || ax.showdividers) && !opts.secondary) {
var gap = 2;
if(ax.ticks) gap += ax.tickwidth / 2;
+ // TODO should secondary labels also fall into this fix-overlap regime?
+
for(i = 0; i < lbbArray.length; i++) {
var xbnd = vals[i].xbnd;
var lbb = lbbArray[i];
@@ -2356,12 +2532,12 @@ axes.drawLabels = function(gd, ax, opts) {
} else {
var vLen = vals.length;
var tickSpacing = Math.abs((vals[vLen - 1].x - vals[0].x) * ax._m) / (vLen - 1);
- var fitBetweenTicks = tickSpacing < maxFontSize * 2.5;
+ var rotate90 = (tickSpacing < maxFontSize * 2.5) || ax.type === 'multicategory';
// 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])) {
- autoangle = fitBetweenTicks ? 90 : 30;
+ autoangle = rotate90 ? 90 : 30;
break;
}
}
@@ -2370,40 +2546,75 @@ axes.drawLabels = function(gd, ax, opts) {
if(autoangle) {
positionLabels(tickLabels, autoangle);
}
- ax._lastangle = autoangle;
+ }
+
+ if(ax._tickAngles) {
+ ax._tickAngles[cls] = autoangle === null ?
+ (isNumeric(tickAngle) ? tickAngle : 0) :
+ autoangle;
}
}
+ if(ax._selections) {
+ ax._selections[cls] = tickLabels;
+ }
+
var done = Lib.syncOrAsync([allLabelsReady, fixLabelOverlaps]);
if(done && done.then) gd._promises.push(done);
return done;
};
-axes.drawTitle = function(gd, ax) {
- var fullLayout = gd._fullLayout;
- var tickLabels = ax._tickLabels;
+/**
+ * Draw axis dividers
+ *
+ * @param {DOM element} gd
+ * @param {object} ax (full) axis object
+ * - {string} _id
+ * - {string} showdividers
+ * - {number} dividerwidth
+ * - {string} dividercolor
+ * @param {object} opts
+ * - {array of object} vals (calcTicks output-like)
+ * - {d3 selection} layer
+ * - {fn} path
+ * - {fn} transFn
+ */
+function drawDividers(gd, ax, opts) {
+ var cls = ax._id + 'divider';
+ var vals = opts.vals;
- var avoid = {
- selection: tickLabels,
- side: ax.side
- };
+ var dividers = opts.layer.selectAll('path.' + cls)
+ .data(vals, tickDataFn);
+
+ dividers.exit().remove();
+
+ dividers.enter().insert('path', ':first-child')
+ .classed(cls, 1)
+ .classed('crisp', 1)
+ .call(Color.stroke, ax.dividercolor)
+ .style('stroke-width', Drawing.crispRound(gd, ax.dividerwidth, 1) + 'px');
+ dividers
+ .attr('transform', opts.transFn)
+ .attr('d', opts.path);
+}
+
+function drawTitle(gd, ax) {
+ var fullLayout = gd._fullLayout;
var axId = ax._id;
var axLetter = axId.charAt(0);
- var offsetBase = 1.5;
var gs = fullLayout._size;
var fontSize = ax.title.font.size;
- var transform, counterAxis, x, y;
-
- if(tickLabels && tickLabels.node() && tickLabels.node().parentNode) {
- var translation = Drawing.getTranslate(tickLabels.node().parentNode);
- avoid.offsetLeft = translation.x;
- avoid.offsetTop = translation.y;
+ var titleStandoff;
+ if(ax.type === 'multicategory') {
+ titleStandoff = ax._boundingBox[{x: 'height', y: 'width'}[axLetter]];
+ } else {
+ var offsetBase = 1.5;
+ titleStandoff = 10 + fontSize * offsetBase + (ax.linewidth ? ax.linewidth - 1 : 0);
}
- var titleStandoff = 10 + fontSize * offsetBase +
- (ax.linewidth ? ax.linewidth - 1 : 0);
+ var transform, counterAxis, x, y;
if(axLetter === 'x') {
counterAxis = (ax.anchor === 'free') ?
@@ -2419,15 +2630,13 @@ axes.drawTitle = function(gd, ax) {
fontSize * (ax.showticklabels ? 1.5 : 0.5);
}
y += counterAxis._offset;
-
- if(!avoid.side) avoid.side = 'bottom';
- }
- else {
+ } else {
counterAxis = (ax.anchor === 'free') ?
{_offset: gs.l + (ax.position || 0) * gs.w, _length: 0} :
axisIds.getFromId(gd, ax.anchor);
y = ax._offset + ax._length / 2;
+
if(ax.side === 'right') {
x = counterAxis._length + titleStandoff +
fontSize * (ax.showticklabels ? 1 : 0.5);
@@ -2437,10 +2646,26 @@ axes.drawTitle = function(gd, ax) {
x += counterAxis._offset;
transform = {rotate: '-90', offset: 0};
- if(!avoid.side) avoid.side = 'left';
}
- Titles.draw(gd, axId + 'title', {
+ var avoid;
+
+ if(ax.type !== 'multicategory') {
+ var tickLabels = ax._selections[ax._id + 'tick'];
+
+ avoid = {
+ selection: tickLabels,
+ side: ax.side
+ };
+
+ if(tickLabels && tickLabels.node() && tickLabels.node().parentNode) {
+ var translation = Drawing.getTranslate(tickLabels.node().parentNode);
+ avoid.offsetLeft = translation.x;
+ avoid.offsetTop = translation.y;
+ }
+ }
+
+ return Titles.draw(gd, axId + 'title', {
propContainer: ax,
propName: ax._name + '.title.text',
placeholder: fullLayout._dfltTitle[axLetter],
@@ -2448,7 +2673,7 @@ axes.drawTitle = function(gd, ax) {
transform: transform,
attributes: {x: x, y: y, 'text-anchor': 'middle'}
});
-};
+}
axes.shouldShowZeroLine = function(gd, ax, counterAxis) {
var rng = Lib.simpleMap(ax.range, ax.r2l);
@@ -2456,7 +2681,7 @@ axes.shouldShowZeroLine = function(gd, ax, counterAxis) {
(rng[0] * rng[1] <= 0) &&
ax.zeroline &&
(ax.type === 'linear' || ax.type === '-') &&
- ax._valsClipped.length &&
+ ax._gridVals.length &&
(
clipEnds(ax, 0) ||
!anyCounterAxLineAtZero(gd, ax, counterAxis, rng) ||
@@ -2544,6 +2769,12 @@ function hasBarsOrFill(gd, ax) {
return false;
}
+function selectTickLabel(gTick) {
+ var s = d3.select(gTick);
+ var mj = s.select('.text-math-group');
+ return mj.empty() ? s.select('text') : mj;
+}
+
/**
* Find all margin pushers for 2D axes and reserve them for later use
* Both label and rangeslider automargin calculations happen later so
@@ -2558,14 +2789,17 @@ axes.allowAutoMargin = function(gd) {
for(var i = 0; i < axList.length; i++) {
var ax = axList[i];
if(ax.automargin) {
- Plots.allowAutoMargin(gd, ax._name + '.automargin');
+ Plots.allowAutoMargin(gd, axAutoMarginID(ax));
}
- if(ax.rangeslider && ax.rangeslider.visible) {
- Plots.allowAutoMargin(gd, 'rangeslider' + ax._id);
+ if(Registry.getComponentMethod('rangeslider', 'isVisible')(ax)) {
+ Plots.allowAutoMargin(gd, rangeSliderAutoMarginID(ax));
}
}
};
+function axAutoMarginID(ax) { return ax._id + '.automargin'; }
+function rangeSliderAutoMarginID(ax) { return ax._id + '.rangeslider'; }
+
// swap all the presentation attributes of the axes showing these traces
axes.swap = function(gd, traces) {
var axGroups = makeAxisGroups(gd, traces);
@@ -2621,11 +2855,10 @@ function mergeAxisGroups(intoSet, fromSet) {
}
function swapAxisGroup(gd, xIds, yIds) {
- var i,
- j,
- xFullAxes = [],
- yFullAxes = [],
- layout = gd.layout;
+ var xFullAxes = [];
+ var yFullAxes = [];
+ var layout = gd.layout;
+ var i, j;
for(i = 0; i < xIds.length; i++) xFullAxes.push(axes.getFromId(gd, xIds[i]));
for(i = 0; i < yIds.length; i++) yFullAxes.push(axes.getFromId(gd, yIds[i]));
@@ -2638,12 +2871,12 @@ function swapAxisGroup(gd, xIds, yIds) {
var numericTypes = ['linear', 'log'];
for(i = 0; i < allAxKeys.length; i++) {
- var keyi = allAxKeys[i],
- xVal = xFullAxes[0][keyi],
- yVal = yFullAxes[0][keyi],
- allEqual = true,
- coerceLinearX = false,
- coerceLinearY = false;
+ var keyi = allAxKeys[i];
+ var xVal = xFullAxes[0][keyi];
+ var yVal = yFullAxes[0][keyi];
+ var allEqual = true;
+ var coerceLinearX = false;
+ var coerceLinearY = false;
if(keyi.charAt(0) === '_' || typeof xVal === 'function' ||
noSwapAttrs.indexOf(keyi) !== -1) {
continue;
@@ -2689,10 +2922,11 @@ function swapAxisAttrs(layout, key, xFullAxes, yFullAxes, dfltTitle) {
// in case the value is the default for either axis,
// look at the first axis in each list and see if
// this key's value is undefined
- var np = Lib.nestedProperty,
- xVal = np(layout[xFullAxes[0]._name], key).get(),
- yVal = np(layout[yFullAxes[0]._name], key).get(),
- i;
+ var np = Lib.nestedProperty;
+ var xVal = np(layout[xFullAxes[0]._name], key).get();
+ var yVal = np(layout[yFullAxes[0]._name], key).get();
+ var i;
+
if(key === 'title') {
// special handling of placeholder titles
if(xVal && xVal.text === dfltTitle.x) {
diff --git a/src/plots/cartesian/axis_autotype.js b/src/plots/cartesian/axis_autotype.js
index c7b90565985..3cb03c9fe4d 100644
--- a/src/plots/cartesian/axis_autotype.js
+++ b/src/plots/cartesian/axis_autotype.js
@@ -14,7 +14,10 @@ var isNumeric = require('fast-isnumeric');
var Lib = require('../../lib');
var BADNUM = require('../../constants/numerical').BADNUM;
-module.exports = function autoType(array, calendar) {
+module.exports = function autoType(array, calendar, opts) {
+ opts = opts || {};
+
+ if(!opts.noMultiCategory && multiCategory(array)) return 'multicategory';
if(moreDates(array, calendar)) return 'date';
if(category(array)) return 'category';
if(linearOK(array)) return 'linear';
@@ -81,3 +84,10 @@ function category(a) {
return curvecats > curvenums * 2;
}
+
+// very-loose requirements for multicategory,
+// trace modules that should never auto-type to multicategory
+// should be declared with 'noMultiCategory'
+function multiCategory(a) {
+ return Lib.isArrayOrTypedArray(a[0]) && Lib.isArrayOrTypedArray(a[1]);
+}
diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js
index 6b8e944b039..79440117ceb 100644
--- a/src/plots/cartesian/axis_defaults.js
+++ b/src/plots/cartesian/axis_defaults.js
@@ -90,9 +90,23 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
if(options.automargin) coerce('automargin');
+ var isMultiCategory = containerOut.type === 'multicategory';
+
if(!options.noTickson &&
- containerOut.type === 'category' && (containerOut.ticks || containerOut.showgrid)) {
- coerce('tickson');
+ (containerOut.type === 'category' || isMultiCategory) &&
+ (containerOut.ticks || containerOut.showgrid)
+ ) {
+ var ticksonDflt;
+ if(isMultiCategory) ticksonDflt = 'boundaries';
+ coerce('tickson', ticksonDflt);
+ }
+
+ if(isMultiCategory) {
+ var showDividers = coerce('showdividers');
+ if(showDividers) {
+ coerce('dividercolor');
+ coerce('dividerwidth');
+ }
}
return containerOut;
diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js
index 70859a2ad0b..36c27ec08b3 100644
--- a/src/plots/cartesian/constraints.js
+++ b/src/plots/cartesian/constraints.js
@@ -139,7 +139,6 @@ exports.enforce = function enforceAxisConstraints(gd) {
var getPad = makePadFn(ax);
updateDomain(ax, factor);
- ax.setScale();
var m = Math.abs(ax._m);
var extremes = concatExtremes(gd, ax);
var minArray = extremes.min;
@@ -206,4 +205,5 @@ function updateDomain(ax, factor) {
center + (inputDomain[0] - center) / factor,
center + (inputDomain[1] - center) / factor
];
+ ax.setScale();
}
diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js
index 6cc75dc0443..8b91a5dab5b 100644
--- a/src/plots/cartesian/dragbox.js
+++ b/src/plots/cartesian/dragbox.js
@@ -516,6 +516,9 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
return;
}
+ // prevent axis drawing from monkeying with margins until we're done
+ gd._fullLayout._replotting = true;
+
if(xActive === 'ew' || yActive === 'ns') {
if(xActive) dragAxList(xaxes, dx);
if(yActive) dragAxList(yaxes, dy);
@@ -726,7 +729,10 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
// accumulated MathJax promises - wait for them before we relayout.
Lib.syncOrAsync([
Plots.previousPromises,
- function() { Registry.call('_guiRelayout', gd, updates); }
+ function() {
+ gd._fullLayout._replotting = false;
+ Registry.call('_guiRelayout', gd, updates);
+ }
], gd);
}
diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js
index 09a11d54a4d..d98b10f7dea 100644
--- a/src/plots/cartesian/layout_attributes.js
+++ b/src/plots/cartesian/layout_attributes.js
@@ -67,7 +67,7 @@ module.exports = {
// '-' means we haven't yet run autotype or couldn't find any data
// it gets turned into linear in gd._fullLayout but not copied back
// to gd.data like the others are.
- values: ['-', 'linear', 'log', 'date', 'category'],
+ values: ['-', 'linear', 'log', 'date', 'category', 'multicategory'],
dflt: '-',
role: 'info',
editType: 'calc',
@@ -323,7 +323,7 @@ module.exports = {
description: [
'Determines where ticks and grid lines are drawn with respect to their',
'corresponding tick labels.',
- 'Only has an effect for axes of `type` *category*.',
+ 'Only has an effect for axes of `type` *category* or *multicategory*.',
'When set to *boundaries*, ticks and grid lines are drawn half a category',
'to the left/bottom of labels.'
].join(' ')
@@ -666,6 +666,40 @@ module.exports = {
editType: 'ticks',
description: 'Sets the width (in px) of the zero line.'
},
+
+ showdividers: {
+ valType: 'boolean',
+ dflt: true,
+ role: 'style',
+ editType: 'ticks',
+ description: [
+ 'Determines whether or not a dividers are drawn',
+ 'between the category levels of this axis.',
+ 'Only has an effect on *multicategory* axes.'
+ ].join(' ')
+ },
+ dividercolor: {
+ valType: 'color',
+ dflt: colorAttrs.defaultLine,
+ role: 'style',
+ editType: 'ticks',
+ description: [
+ 'Sets the color of the dividers',
+ 'Only has an effect on *multicategory* axes.'
+ ].join(' ')
+ },
+ dividerwidth: {
+ valType: 'number',
+ dflt: 1,
+ role: 'style',
+ editType: 'ticks',
+ description: [
+ 'Sets the width (in px) of the dividers',
+ 'Only has an effect on *multicategory* axes.'
+ ].join(' ')
+ },
+ // TODO dividerlen: that would override "to label base" length?
+
// positioning attributes
// anchor: not used directly, just put here for reference
// values are any opposite-letter axis id
diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js
index e0e6e18dc08..6bf2f652fc6 100644
--- a/src/plots/cartesian/layout_defaults.js
+++ b/src/plots/cartesian/layout_defaults.js
@@ -156,6 +156,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
axLayoutOut._traceIndices = traces.map(function(t) { return t._expandedIndex; });
axLayoutOut._annIndices = [];
axLayoutOut._shapeIndices = [];
+ axLayoutOut._subplotsWith = [];
+ axLayoutOut._counterAxes = [];
// set up some private properties
axLayoutOut._name = axLayoutOut._attr = axName;
@@ -239,11 +241,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
var anchoredAxis = layoutOut[id2name(axLayoutOut.anchor)];
- var fixedRangeDflt = (
- anchoredAxis &&
- anchoredAxis.rangeslider &&
- anchoredAxis.rangeslider.visible
- );
+ var fixedRangeDflt = getComponentMethod('rangeslider', 'isVisible')(anchoredAxis);
coerce('fixedrange', fixedRangeDflt);
}
diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js
index e057aba7b20..2f5c25cb04f 100644
--- a/src/plots/cartesian/set_convert.js
+++ b/src/plots/cartesian/set_convert.js
@@ -30,6 +30,10 @@ function fromLog(v) {
return Math.pow(10, v);
}
+function isValidCategory(v) {
+ return v !== null && v !== undefined;
+}
+
/**
* Define the conversion functions for an axis data is used in 5 ways:
*
@@ -123,7 +127,7 @@ module.exports = function setConvert(ax, fullLayout) {
* a disconnect between the array and the index returned
*/
function setCategoryIndex(v) {
- if(v !== null && v !== undefined) {
+ if(isValidCategory(v)) {
if(ax._categoriesMap === undefined) {
ax._categoriesMap = {};
}
@@ -142,14 +146,58 @@ module.exports = function setConvert(ax, fullLayout) {
return BADNUM;
}
+ function setMultiCategoryIndex(arrayIn, len) {
+ var arrayOut = new Array(len);
+ var i;
+
+ // [ [arrayIn[0][i], arrayIn[1][i]], for i .. len ]
+ var tmp = new Array(len);
+ // [ [cnt, {$cat: index}], for j .. arrayIn.length ]
+ var seen = [[0, {}], [0, {}]];
+
+ if(Lib.isArrayOrTypedArray(arrayIn[0]) && Lib.isArrayOrTypedArray(arrayIn[1])) {
+ for(i = 0; i < len; i++) {
+ var v0 = arrayIn[0][i];
+ var v1 = arrayIn[1][i];
+ if(isValidCategory(v0) && isValidCategory(v1)) {
+ tmp[i] = [v0, v1];
+ if(!(v0 in seen[0][1])) {
+ seen[0][1][v0] = seen[0][0]++;
+ }
+ if(!(v1 in seen[1][1])) {
+ seen[1][1][v1] = seen[1][0]++;
+ }
+ }
+ }
+
+ tmp.sort(function(a, b) {
+ var ind0 = seen[0][1];
+ var d = ind0[a[0]] - ind0[b[0]];
+ if(d) return d;
+
+ var ind1 = seen[1][1];
+ return ind1[a[1]] - ind1[b[1]];
+ });
+ }
+
+ for(i = 0; i < len; i++) {
+ arrayOut[i] = setCategoryIndex(tmp[i]);
+ }
+
+ return arrayOut;
+ }
+
function getCategoryIndex(v) {
- // d2l/d2c variant that that won't add categories but will also
- // allow numbers to be mapped to the linearized axis positions
if(ax._categoriesMap) {
- var index = ax._categoriesMap[v];
- if(index !== undefined) return index;
+ return ax._categoriesMap[v];
}
+ }
+ function getCategoryPosition(v) {
+ // d2l/d2c variant that that won't add categories but will also
+ // allow numbers to be mapped to the linearized axis positions
+ var index = getCategoryIndex(v);
+ if(index !== undefined) return index;
if(isNumeric(v)) return +v;
}
@@ -235,15 +283,15 @@ module.exports = function setConvert(ax, fullLayout) {
ax.d2c = ax.d2l = setCategoryIndex;
ax.r2d = ax.c2d = ax.l2d = getCategoryName;
- ax.d2r = ax.d2l_noadd = getCategoryIndex;
+ ax.d2r = ax.d2l_noadd = getCategoryPosition;
ax.r2c = function(v) {
- var index = getCategoryIndex(v);
+ var index = getCategoryPosition(v);
return index !== undefined ? index : ax.fraction2r(0.5);
};
ax.l2r = ax.c2r = ensureNumber;
- ax.r2l = getCategoryIndex;
+ ax.r2l = getCategoryPosition;
ax.d2p = function(v) { return ax.l2p(ax.r2c(v)); };
ax.p2d = function(px) { return getCategoryName(p2l(px)); };
@@ -255,6 +303,34 @@ module.exports = function setConvert(ax, fullLayout) {
return ensureNumber(v);
};
}
+ else if(ax.type === 'multicategory') {
+ // N.B. multicategory axes don't define d2c and d2l,
+ // as 'data-to-calcdata' conversion needs to take into
+ // account all data array items as in ax.makeCalcdata.
+
+ ax.r2d = ax.c2d = ax.l2d = getCategoryName;
+ ax.d2r = ax.d2l_noadd = getCategoryPosition;
+
+ ax.r2c = function(v) {
+ var index = getCategoryPosition(v);
+ return index !== undefined ? index : ax.fraction2r(0.5);
+ };
+
+ ax.r2c_just_indices = getCategoryIndex;
+
+ ax.l2r = ax.c2r = ensureNumber;
+ ax.r2l = getCategoryPosition;
+
+ ax.d2p = function(v) { return ax.l2p(ax.r2c(v)); };
+ ax.p2d = function(px) { return getCategoryName(p2l(px)); };
+ ax.r2p = ax.d2p;
+ ax.p2r = p2l;
+
+ ax.cleanPos = function(v) {
+ if(Array.isArray(v) || (typeof v === 'string' && v !== '')) return v;
+ return ensureNumber(v);
+ };
+ }
// find the range value at the specified (linear) fraction of the axis
ax.fraction2r = function(v) {
@@ -348,11 +424,6 @@ module.exports = function setConvert(ax, fullLayout) {
ax.setScale = function(usePrivateRange) {
var gs = fullLayout._size;
- // TODO cleaner way to handle this case
- if(!ax._categories) ax._categories = [];
- // Add a map to optimize the performance of category collection
- if(!ax._categoriesMap) ax._categoriesMap = {};
-
// make sure we have a domain (pull it in from the axis
// this one is overlaying if necessary)
if(ax.overlaying) {
@@ -407,7 +478,7 @@ module.exports = function setConvert(ax, fullLayout) {
if(axLetter in trace) {
arrayIn = trace[axLetter];
- len = trace._length || arrayIn.length;
+ len = trace._length || Lib.minRowLength(arrayIn);
if(Lib.isTypedArray(arrayIn) && (axType === 'linear' || axType === 'log')) {
if(len === arrayIn.length) {
@@ -417,6 +488,10 @@ module.exports = function setConvert(ax, fullLayout) {
}
}
+ if(axType === 'multicategory') {
+ return setMultiCategoryIndex(arrayIn, len);
+ }
+
arrayOut = new Array(len);
for(i = 0; i < len; i++) {
arrayOut[i] = ax.d2c(arrayIn[i], 0, cal);
diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js
index f5aff20aefa..222f729509f 100644
--- a/src/plots/cartesian/tick_value_defaults.js
+++ b/src/plots/cartesian/tick_value_defaults.js
@@ -6,22 +6,18 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var cleanTicks = require('./clean_ticks');
-
module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType) {
var tickmode;
if(containerIn.tickmode === 'array' &&
(axType === 'log' || axType === 'date')) {
tickmode = containerOut.tickmode = 'auto';
- }
- else {
- var tickmodeDefault =
- Array.isArray(containerIn.tickvals) ? 'array' :
+ } else {
+ var tickmodeDefault = Array.isArray(containerIn.tickvals) ? 'array' :
containerIn.dtick ? 'linear' :
'auto';
tickmode = coerce('tickmode', tickmodeDefault);
@@ -36,8 +32,7 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe
containerIn.dtick, axType);
containerOut.tick0 = cleanTicks.tick0(
containerIn.tick0, axType, containerOut.calendar, dtick);
- }
- else {
+ } else if(axType !== 'multicategory') {
var tickvals = coerce('tickvals');
if(tickvals === undefined) containerOut.tickmode = 'auto';
else coerce('ticktext');
diff --git a/src/plots/cartesian/type_defaults.js b/src/plots/cartesian/type_defaults.js
index 1234f8a24a6..04a050f8938 100644
--- a/src/plots/cartesian/type_defaults.js
+++ b/src/plots/cartesian/type_defaults.js
@@ -8,7 +8,7 @@
'use strict';
-var Registry = require('../../registry');
+var traceIs = require('../../registry').traceIs;
var autoType = require('./axis_autotype');
/*
@@ -57,6 +57,7 @@ function setAutoType(ax, data) {
var calAttr = axLetter + 'calendar';
var calendar = d0[calAttr];
+ var opts = {noMultiCategory: !traceIs(d0, 'cartesian') || traceIs(d0, 'noMultiCategory')};
var i;
// check all boxes on this x axis to see
@@ -67,8 +68,7 @@ function setAutoType(ax, data) {
for(i = 0; i < data.length; i++) {
var trace = data[i];
- if(!Registry.traceIs(trace, 'box-violin') ||
- (trace[axLetter + 'axis'] || axLetter) !== id) continue;
+ if(!traceIs(trace, 'box-violin') || (trace[axLetter + 'axis'] || axLetter) !== id) continue;
if(trace[posLetter] !== undefined) boxPositions.push(trace[posLetter][0]);
else if(trace.name !== undefined) boxPositions.push(trace.name);
@@ -77,7 +77,7 @@ function setAutoType(ax, data) {
if(trace[calAttr] !== calendar) calendar = undefined;
}
- ax.type = autoType(boxPositions, calendar);
+ ax.type = autoType(boxPositions, calendar, opts);
}
else if(d0.type === 'splom') {
var dimensions = d0.dimensions;
@@ -85,13 +85,13 @@ function setAutoType(ax, data) {
for(i = 0; i < dimensions.length; i++) {
var dim = dimensions[i];
if(dim.visible && (diag[i][0] === id || diag[i][1] === id)) {
- ax.type = autoType(dim.values, calendar);
+ ax.type = autoType(dim.values, calendar, opts);
break;
}
}
}
else {
- ax.type = autoType(d0[axLetter] || [d0[axLetter + '0']], calendar);
+ ax.type = autoType(d0[axLetter] || [d0[axLetter + '0']], calendar, opts);
}
}
@@ -122,9 +122,9 @@ function getBoxPosLetter(trace) {
}
function isBoxWithoutPositionCoords(trace, axLetter) {
- var posLetter = getBoxPosLetter(trace),
- isBox = Registry.traceIs(trace, 'box-violin'),
- isCandlestick = Registry.traceIs(trace._fullInput || {}, 'candlestick');
+ var posLetter = getBoxPosLetter(trace);
+ var isBox = traceIs(trace, 'box-violin');
+ var isCandlestick = traceIs(trace._fullInput || {}, 'candlestick');
return (
isBox &&
diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js
index e2c2c9482bc..ef3a2fa4b9a 100644
--- a/src/plots/gl3d/layout/axis_attributes.js
+++ b/src/plots/gl3d/layout/axis_attributes.js
@@ -13,7 +13,6 @@ var axesAttrs = require('../../cartesian/layout_attributes');
var extendFlat = require('../../../lib/extend').extendFlat;
var overrideAll = require('../../../plot_api/edit_types').overrideAll;
-
module.exports = overrideAll({
visible: axesAttrs.visible,
showspikes: {
@@ -73,7 +72,9 @@ module.exports = overrideAll({
categoryorder: axesAttrs.categoryorder,
categoryarray: axesAttrs.categoryarray,
title: axesAttrs.title,
- type: axesAttrs.type,
+ type: extendFlat({}, axesAttrs.type, {
+ values: ['-', 'linear', 'log', 'date', 'category']
+ }),
autorange: axesAttrs.autorange,
rangemode: axesAttrs.rangemode,
range: axesAttrs.range,
diff --git a/src/plots/plots.js b/src/plots/plots.js
index e2a28e87a39..aa52e9e05ef 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -497,15 +497,11 @@ plots.supplyDefaults = function(gd, opts) {
if(uids[uid] === 'old') delete tracePreGUI[uid];
}
- // TODO may return a promise
- plots.doAutoMargin(gd);
+ // set up containers for margin calculations
+ initMargins(newFullLayout);
- // set scale after auto margin routine
- var axList = axisIDs.list(gd);
- for(i = 0; i < axList.length; i++) {
- var ax = axList[i];
- ax.setScale();
- }
+ // collect and do some initial calculations for rangesliders
+ Registry.getComponentMethod('rangeslider', 'makeData')(newFullLayout);
// update object references in calcdata
if(!skipUpdateCalc && oldCalcdata.length === newFullData.length) {
@@ -815,6 +811,12 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa
plotinfo.id = id;
}
+ // add these axis ids to each others' subplot lists
+ xaxis._counterAxes.push(yaxis._id);
+ yaxis._counterAxes.push(xaxis._id);
+ xaxis._subplotsWith.push(id);
+ yaxis._subplotsWith.push(id);
+
// update x and y axis layout object refs
plotinfo.xaxis = xaxis;
plotinfo.yaxis = yaxis;
@@ -842,8 +844,9 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa
// while we're at it, link overlaying axes to their main axes and
// anchored axes to the axes they're anchored to
var axList = axisIDs.list(mockGd, null, true);
+ var ax;
for(i = 0; i < axList.length; i++) {
- var ax = axList[i];
+ ax = axList[i];
var mainAx = null;
if(ax.overlaying) {
@@ -871,8 +874,53 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa
null :
axisIDs.getFromId(mockGd, ax.anchor);
}
+
+ // finally, we can find the main subplot for each axis
+ // (on which the ticks & labels are drawn)
+ for(i = 0; i < axList.length; i++) {
+ ax = axList[i];
+ ax._counterAxes.sort(axisIDs.idSort);
+ ax._subplotsWith.sort(Lib.subplotSort);
+ ax._mainSubplot = findMainSubplot(ax, newFullLayout);
+ }
};
+function findMainSubplot(ax, fullLayout) {
+ var mockGd = {_fullLayout: fullLayout};
+
+ var isX = ax._id.charAt(0) === 'x';
+ var anchorAx = ax._mainAxis._anchorAxis;
+ var mainSubplotID = '';
+ var nextBestMainSubplotID = '';
+ var anchorID = '';
+
+ // First try the main ID with the anchor
+ if(anchorAx) {
+ anchorID = anchorAx._mainAxis._id;
+ mainSubplotID = isX ? (ax._id + anchorID) : (anchorID + ax._id);
+ }
+
+ // Then look for a subplot with the counteraxis overlaying the anchor
+ // If that fails just use the first subplot including this axis
+ if(!mainSubplotID || !fullLayout._plots[mainSubplotID]) {
+ mainSubplotID = '';
+
+ var counterIDs = ax._counterAxes;
+ for(var j = 0; j < counterIDs.length; j++) {
+ var counterPart = counterIDs[j];
+ var id = isX ? (ax._id + counterPart) : (counterPart + ax._id);
+ if(!nextBestMainSubplotID) nextBestMainSubplotID = id;
+ var counterAx = axisIDs.getFromId(mockGd, counterPart);
+ if(anchorID && counterAx.overlaying === anchorID) {
+ mainSubplotID = id;
+ break;
+ }
+ }
+ }
+
+ return mainSubplotID || nextBestMainSubplotID;
+}
+
// This function clears any trace attributes with valType: color and
// no set dflt filed in the plot schema. This is needed because groupby (which
// is the only transform for which this currently applies) supplies parent
@@ -1686,7 +1734,20 @@ plots.allowAutoMargin = function(gd, id) {
gd._fullLayout._pushmarginIds[id] = 1;
};
-function setupAutoMargin(fullLayout) {
+function initMargins(fullLayout) {
+ var margin = fullLayout.margin;
+
+ if(!fullLayout._size) {
+ var gs = fullLayout._size = {
+ l: Math.round(margin.l),
+ r: Math.round(margin.r),
+ t: Math.round(margin.t),
+ b: Math.round(margin.b),
+ p: Math.round(margin.pad)
+ };
+ gs.w = Math.round(fullLayout.width) - gs.l - gs.r;
+ gs.h = Math.round(fullLayout.height) - gs.t - gs.b;
+ }
if(!fullLayout._pushmargin) fullLayout._pushmargin = {};
if(!fullLayout._pushmarginIds) fullLayout._pushmarginIds = {};
}
@@ -1709,8 +1770,6 @@ function setupAutoMargin(fullLayout) {
plots.autoMargin = function(gd, id, o) {
var fullLayout = gd._fullLayout;
- setupAutoMargin(fullLayout);
-
var pushMargin = fullLayout._pushmargin;
var pushMarginIds = fullLayout._pushmarginIds;
@@ -1754,18 +1813,19 @@ plots.autoMargin = function(gd, id, o) {
plots.doAutoMargin = function(gd) {
var fullLayout = gd._fullLayout;
if(!fullLayout._size) fullLayout._size = {};
- setupAutoMargin(fullLayout);
+ initMargins(fullLayout);
- var gs = fullLayout._size,
- oldmargins = JSON.stringify(gs);
+ var gs = fullLayout._size;
+ var oldmargins = JSON.stringify(gs);
+ var margin = fullLayout.margin;
// adjust margins for outside components
// fullLayout.margin is the requested margin,
// fullLayout._size has margins and plotsize after adjustment
- var ml = Math.max(fullLayout.margin.l || 0, 0);
- var mr = Math.max(fullLayout.margin.r || 0, 0);
- var mt = Math.max(fullLayout.margin.t || 0, 0);
- var mb = Math.max(fullLayout.margin.b || 0, 0);
+ var ml = margin.l;
+ var mr = margin.r;
+ var mt = margin.t;
+ var mb = margin.b;
var pushMargin = fullLayout._pushmargin;
var pushMarginIds = fullLayout._pushmarginIds;
@@ -1835,7 +1895,7 @@ plots.doAutoMargin = function(gd) {
gs.r = Math.round(mr);
gs.t = Math.round(mt);
gs.b = Math.round(mb);
- gs.p = Math.round(fullLayout.margin.pad);
+ gs.p = Math.round(margin.pad);
gs.w = Math.round(fullLayout.width) - gs.l - gs.r;
gs.h = Math.round(fullLayout.height) - gs.t - gs.b;
diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js
index 4d878a9fd43..c40dc8d042c 100644
--- a/src/plots/polar/layout_attributes.js
+++ b/src/plots/polar/layout_attributes.js
@@ -57,7 +57,9 @@ var axisTickAttrs = overrideAll({
var radialAxisAttrs = {
visible: extendFlat({}, axesAttrs.visible, {dflt: true}),
- type: axesAttrs.type,
+ type: extendFlat({}, axesAttrs.type, {
+ values: ['-', 'linear', 'log', 'date', 'category']
+ }),
autorange: extendFlat({}, axesAttrs.autorange, {editType: 'plot'}),
rangemode: {
diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js
index 5e56eb258d7..e9d2d8ff8b0 100644
--- a/src/plots/polar/polar.js
+++ b/src/plots/polar/polar.js
@@ -643,6 +643,7 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
Axes.drawLabels(gd, ax, {
vals: vals,
layer: layers['angular-axis'],
+ repositionOnUpdate: true,
transFn: transFn,
labelXFn: labelXFn,
labelYFn: labelYFn,
diff --git a/src/traces/bar/cross_trace_calc.js b/src/traces/bar/cross_trace_calc.js
index e80255413b4..ff300d24ca5 100644
--- a/src/traces/bar/cross_trace_calc.js
+++ b/src/traces/bar/cross_trace_calc.js
@@ -126,9 +126,14 @@ function initBase(gd, pa, sa, calcTraces) {
// time. But included here for completeness.
var scalendar = trace.orientation === 'h' ? trace.xcalendar : trace.ycalendar;
+ // 'base' on categorical axes makes no sense
+ var d2c = sa.type === 'category' || sa.type === 'multicategory' ?
+ function() { return null; } :
+ sa.d2c;
+
if(isArrayOrTypedArray(base)) {
for(j = 0; j < Math.min(base.length, cd.length); j++) {
- b = sa.d2c(base[j], 0, scalendar);
+ b = d2c(base[j], 0, scalendar);
if(isNumeric(b)) {
cd[j].b = +b;
cd[j].hasB = 1;
@@ -139,7 +144,7 @@ function initBase(gd, pa, sa, calcTraces) {
cd[j].b = 0;
}
} else {
- b = sa.d2c(base, 0, scalendar);
+ b = d2c(base, 0, scalendar);
var hasBase = isNumeric(b);
b = hasBase ? b : 0;
for(j = 0; j < cd.length; j++) {
diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js
index 095048f19a7..58d6d55b831 100644
--- a/src/traces/box/calc.js
+++ b/src/traces/box/calc.js
@@ -178,7 +178,10 @@ function getPos(trace, posLetter, posAxis, val, num) {
pos0 = num;
}
- var pos0c = posAxis.d2c(pos0, 0, trace[posLetter + 'calendar']);
+ var pos0c = posAxis.type === 'multicategory' ?
+ posAxis.r2c_just_indices(pos0) :
+ posAxis.d2c(pos0, 0, trace[posLetter + 'calendar']);
+
return val.map(function() { return pos0c; });
}
diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js
index dc75d7f1266..96bbdc8df79 100644
--- a/src/traces/box/defaults.js
+++ b/src/traces/box/defaults.js
@@ -45,16 +45,15 @@ function handleSampleDefaults(traceIn, traceOut, coerce, layout) {
if(y && y.length) {
defaultOrientation = 'v';
if(hasX) {
- len = Math.min(x.length, y.length);
- }
- else {
+ len = Math.min(Lib.minRowLength(x), Lib.minRowLength(y));
+ } else {
coerce('x0');
- len = y.length;
+ len = Lib.minRowLength(y);
}
} else if(hasX) {
defaultOrientation = 'h';
coerce('y0');
- len = x.length;
+ len = Lib.minRowLength(x);
} else {
traceOut.visible = false;
return;
diff --git a/src/traces/carpet/index.js b/src/traces/carpet/index.js
index 2ebbb14a766..236129c9e1a 100644
--- a/src/traces/carpet/index.js
+++ b/src/traces/carpet/index.js
@@ -21,7 +21,7 @@ Carpet.isContainer = true; // so carpet traces get `calc` before other traces
Carpet.moduleType = 'trace';
Carpet.name = 'carpet';
Carpet.basePlotModule = require('../../plots/cartesian');
-Carpet.categories = ['cartesian', 'svg', 'carpet', 'carpetAxis', 'notLegendIsolatable'];
+Carpet.categories = ['cartesian', 'svg', 'carpet', 'carpetAxis', 'notLegendIsolatable', 'noMultiCategory'];
Carpet.meta = {
description: [
'The data describing carpet axis layout is set in `y` and (optionally)',
diff --git a/src/traces/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js
index 1a645d5f261..878c481b8de 100644
--- a/src/traces/contourcarpet/calc.js
+++ b/src/traces/contourcarpet/calc.js
@@ -9,11 +9,10 @@
'use strict';
var colorscaleCalc = require('../../components/colorscale/calc');
-var isArray1D = require('../../lib').isArray1D;
+var Lib = require('../../lib');
var convertColumnData = require('../heatmap/convert_column_xyz');
var clean2dArray = require('../heatmap/clean_2d_array');
-var maxRowLength = require('../heatmap/max_row_length');
var interp2d = require('../heatmap/interp2d');
var findEmpties = require('../heatmap/find_empties');
var makeBoundArray = require('../heatmap/make_bound_array');
@@ -70,7 +69,7 @@ function heatmappishCalc(gd, trace) {
aax._minDtick = 0;
bax._minDtick = 0;
- if(isArray1D(trace.z)) convertColumnData(trace, aax, bax, 'a', 'b', ['z']);
+ if(Lib.isArray1D(trace.z)) convertColumnData(trace, aax, bax, 'a', 'b', ['z']);
a = trace._a = trace._a || trace.a;
b = trace._b = trace._b || trace.b;
@@ -87,7 +86,7 @@ function heatmappishCalc(gd, trace) {
interp2d(z, trace._emptypoints);
// create arrays of brick boundaries, to be used by autorange and heatmap.plot
- var xlen = maxRowLength(z),
+ var xlen = Lib.maxRowLength(z),
xIn = trace.xtype === 'scaled' ? '' : a,
xArray = makeBoundArray(trace, xIn, a0, da, xlen, aax),
yIn = trace.ytype === 'scaled' ? '' : b,
diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js
index 86ebf03aa83..c6c9ad70b1d 100644
--- a/src/traces/heatmap/calc.js
+++ b/src/traces/heatmap/calc.js
@@ -16,7 +16,6 @@ var Axes = require('../../plots/cartesian/axes');
var histogram2dCalc = require('../histogram2d/calc');
var colorscaleCalc = require('../../components/colorscale/calc');
var convertColumnData = require('./convert_column_xyz');
-var maxRowLength = require('./max_row_length');
var clean2dArray = require('./clean_2d_array');
var interp2d = require('./interp2d');
var findEmpties = require('./find_empties');
@@ -116,7 +115,7 @@ module.exports = function calc(gd, trace) {
}
// create arrays of brick boundaries, to be used by autorange and heatmap.plot
- var xlen = maxRowLength(z);
+ var xlen = Lib.maxRowLength(z);
var xIn = trace.xtype === 'scaled' ? '' : x;
var xArray = makeBoundArray(trace, xIn, x0, dx, xlen, xa);
var yIn = trace.ytype === 'scaled' ? '' : y;
diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js
index 57638ace45e..5f7ff06f1f2 100644
--- a/src/traces/heatmap/convert_column_xyz.js
+++ b/src/traces/heatmap/convert_column_xyz.js
@@ -14,43 +14,36 @@ var BADNUM = require('../../constants/numerical').BADNUM;
module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name, arrayVarNames) {
var colLen = trace._length;
- var col1 = trace[var1Name].slice(0, colLen);
- var col2 = trace[var2Name].slice(0, colLen);
+ var col1 = ax1.makeCalcdata(trace, var1Name);
+ var col2 = ax2.makeCalcdata(trace, var2Name);
var textCol = trace.text;
var hasColumnText = (textCol !== undefined && Lib.isArray1D(textCol));
- var col1Calendar = trace[var1Name + 'calendar'];
- var col2Calendar = trace[var2Name + 'calendar'];
+ var i, j;
- var i, j, arrayVar, newArray, arrayVarName;
-
- for(i = 0; i < colLen; i++) {
- col1[i] = ax1.d2c(col1[i], 0, col1Calendar);
- col2[i] = ax2.d2c(col2[i], 0, col2Calendar);
- }
-
- var col1dv = Lib.distinctVals(col1),
- col1vals = col1dv.vals,
- col2dv = Lib.distinctVals(col2),
- col2vals = col2dv.vals,
- newArrays = [];
+ var col1dv = Lib.distinctVals(col1);
+ var col1vals = col1dv.vals;
+ var col2dv = Lib.distinctVals(col2);
+ var col2vals = col2dv.vals;
+ var newArrays = [];
+ var text;
for(i = 0; i < arrayVarNames.length; i++) {
newArrays[i] = Lib.init2dArray(col2vals.length, col1vals.length);
}
- var i1, i2, text;
-
- if(hasColumnText) text = Lib.init2dArray(col2vals.length, col1vals.length);
+ if(hasColumnText) {
+ text = Lib.init2dArray(col2vals.length, col1vals.length);
+ }
for(i = 0; i < colLen; i++) {
if(col1[i] !== BADNUM && col2[i] !== BADNUM) {
- i1 = Lib.findBin(col1[i] + col1dv.minDiff / 2, col1vals);
- i2 = Lib.findBin(col2[i] + col2dv.minDiff / 2, col2vals);
+ var i1 = Lib.findBin(col1[i] + col1dv.minDiff / 2, col1vals);
+ var i2 = Lib.findBin(col2[i] + col2dv.minDiff / 2, col2vals);
for(j = 0; j < arrayVarNames.length; j++) {
- arrayVarName = arrayVarNames[j];
- arrayVar = trace[arrayVarName];
- newArray = newArrays[j];
+ var arrayVarName = arrayVarNames[j];
+ var arrayVar = trace[arrayVarName];
+ var newArray = newArrays[j];
newArray[i2][i1] = arrayVar[i];
}
diff --git a/src/traces/heatmap/find_empties.js b/src/traces/heatmap/find_empties.js
index df5c442119d..27c9244a420 100644
--- a/src/traces/heatmap/find_empties.js
+++ b/src/traces/heatmap/find_empties.js
@@ -8,7 +8,7 @@
'use strict';
-var maxRowLength = require('./max_row_length');
+var maxRowLength = require('../../lib').maxRowLength;
/* Return a list of empty points in 2D array z
* each empty point z[i][j] gives an array [i, j, neighborCount]
diff --git a/src/traces/heatmap/make_bound_array.js b/src/traces/heatmap/make_bound_array.js
index bf0e67b7a9d..ea3ec9a33a3 100644
--- a/src/traces/heatmap/make_bound_array.js
+++ b/src/traces/heatmap/make_bound_array.js
@@ -67,10 +67,15 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks,
var calendar = trace[ax._id.charAt(0) + 'calendar'];
- if(isHist || ax.type === 'category') v0 = ax.r2c(v0In, 0, calendar) || 0;
- else if(isArrayOrTypedArray(arrayIn) && arrayIn.length === 1) v0 = arrayIn[0];
- else if(v0In === undefined) v0 = 0;
- else v0 = ax.d2c(v0In, 0, calendar);
+ if(isHist || ax.type === 'category' || ax.type === 'multicategory') {
+ v0 = ax.r2c(v0In, 0, calendar) || 0;
+ } else if(isArrayOrTypedArray(arrayIn) && arrayIn.length === 1) {
+ v0 = arrayIn[0];
+ } else if(v0In === undefined) {
+ v0 = 0;
+ } else {
+ v0 = ax.d2c(v0In, 0, calendar);
+ }
for(i = (isContour || isGL2D) ? 0 : -0.5; i < numbricks; i++) {
arrayOut.push(v0 + dv * i);
diff --git a/src/traces/heatmap/max_row_length.js b/src/traces/heatmap/max_row_length.js
deleted file mode 100644
index 44e55256ba2..00000000000
--- a/src/traces/heatmap/max_row_length.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
-* Copyright 2012-2018, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
-
-
-'use strict';
-
-module.exports = function maxRowLength(z) {
- var len = 0;
-
- for(var i = 0; i < z.length; i++) {
- len = Math.max(len, z[i].length);
- }
-
- return len;
-};
diff --git a/src/traces/heatmap/plot.js b/src/traces/heatmap/plot.js
index 48130d77238..ef309be4f80 100644
--- a/src/traces/heatmap/plot.js
+++ b/src/traces/heatmap/plot.js
@@ -17,8 +17,6 @@ var Lib = require('../../lib');
var Colorscale = require('../../components/colorscale');
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
-var maxRowLength = require('./max_row_length');
-
module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
var xa = plotinfo.xaxis;
var ya = plotinfo.yaxis;
@@ -38,7 +36,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
// get z dims
var m = z.length;
- var n = maxRowLength(z);
+ var n = Lib.maxRowLength(z);
var xrev = false;
var yrev = false;
diff --git a/src/traces/heatmap/xyz_defaults.js b/src/traces/heatmap/xyz_defaults.js
index c457abfbfb4..6bfafcbdbc1 100644
--- a/src/traces/heatmap/xyz_defaults.js
+++ b/src/traces/heatmap/xyz_defaults.js
@@ -6,7 +6,6 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var isNumeric = require('fast-isnumeric');
@@ -26,10 +25,13 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, x
x = coerce(xName);
y = coerce(yName);
+ var xlen = Lib.minRowLength(x);
+ var ylen = Lib.minRowLength(y);
+
// column z must be accompanied by xName and yName arrays
- if(!(x && x.length && y && y.length)) return 0;
+ if(xlen === 0 || ylen === 0) return 0;
- traceOut._length = Math.min(x.length, y.length, z.length);
+ traceOut._length = Math.min(xlen, ylen, z.length);
}
else {
x = coordDefaults(xName, coerce);
@@ -50,10 +52,8 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, x
};
function coordDefaults(coordStr, coerce) {
- var coord = coerce(coordStr),
- coordType = coord ?
- coerce(coordStr + 'type', 'array') :
- 'scaled';
+ var coord = coerce(coordStr);
+ var coordType = coord ? coerce(coordStr + 'type', 'array') : 'scaled';
if(coordType === 'scaled') {
coerce(coordStr + '0');
diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js
index a03f3cec3ae..8dd1a5f92d3 100644
--- a/src/traces/histogram/calc.js
+++ b/src/traces/histogram/calc.js
@@ -270,7 +270,8 @@ function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) {
// Edge case: single-valued histogram overlaying others
// Use them all together to calculate the bin size for the single-valued one
- if(isOverlay && newBinSpec._dataSpan === 0 && pa.type !== 'category') {
+ if(isOverlay && newBinSpec._dataSpan === 0 &&
+ pa.type !== 'category' && pa.type !== 'multicategory') {
// Several single-valued histograms! Stop infinite recursion,
// just return an extra flag that tells handleSingleValueOverlays
// to sort out this trace too
@@ -327,7 +328,7 @@ function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) {
Lib.aggNums(Math.min, null, pos0);
var dummyAx = {
- type: pa.type === 'category' ? 'linear' : pa.type,
+ type: (pa.type === 'category' || pa.type === 'multicategory') ? 'linear' : pa.type,
r2l: pa.r2l,
dtick: binOpts.size,
tick0: mainStart,
diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js
index 9d5d3715461..96212f84328 100644
--- a/src/traces/histogram/defaults.js
+++ b/src/traces/histogram/defaults.js
@@ -36,7 +36,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
var sampleLetter = orientation === 'v' ? 'x' : 'y';
var aggLetter = orientation === 'v' ? 'y' : 'x';
- var len = (x && y) ? Math.min(x.length && y.length) : (traceOut[sampleLetter] || []).length;
+ var len = (x && y) ?
+ Math.min(Lib.minRowLength(x) && Lib.minRowLength(y)) :
+ Lib.minRowLength(traceOut[sampleLetter] || []);
if(!len) {
traceOut.visible = false;
diff --git a/src/traces/histogram2d/sample_defaults.js b/src/traces/histogram2d/sample_defaults.js
index aca6acf6595..5ad6e547ecf 100644
--- a/src/traces/histogram2d/sample_defaults.js
+++ b/src/traces/histogram2d/sample_defaults.js
@@ -6,24 +6,26 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var Registry = require('../../registry');
+var Lib = require('../../lib');
module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout) {
var x = coerce('x');
var y = coerce('y');
+ var xlen = Lib.minRowLength(x);
+ var ylen = Lib.minRowLength(y);
// we could try to accept x0 and dx, etc...
// but that's a pretty weird use case.
// for now require both x and y explicitly specified.
- if(!(x && x.length && y && y.length)) {
+ if(!xlen || !ylen) {
traceOut.visible = false;
return;
}
- traceOut._length = Math.min(x.length, y.length);
+ traceOut._length = Math.min(xlen, ylen);
var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults');
handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout);
diff --git a/src/traces/ohlc/ohlc_defaults.js b/src/traces/ohlc/ohlc_defaults.js
index b1a6a83e10b..51baf99986f 100644
--- a/src/traces/ohlc/ohlc_defaults.js
+++ b/src/traces/ohlc/ohlc_defaults.js
@@ -6,11 +6,10 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var Registry = require('../../registry');
-
+var Lib = require('../../lib');
module.exports = function handleOHLC(traceIn, traceOut, coerce, layout) {
var x = coerce('x');
@@ -27,9 +26,7 @@ module.exports = function handleOHLC(traceIn, traceOut, coerce, layout) {
if(!(open && high && low && close)) return;
var len = Math.min(open.length, high.length, low.length, close.length);
-
- if(x) len = Math.min(len, x.length);
-
+ if(x) len = Math.min(len, Lib.minRowLength(x));
traceOut._length = len;
return len;
diff --git a/src/traces/scatter/xy_defaults.js b/src/traces/scatter/xy_defaults.js
index 27a987af471..b5ef293ee09 100644
--- a/src/traces/scatter/xy_defaults.js
+++ b/src/traces/scatter/xy_defaults.js
@@ -6,34 +6,32 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
+var Lib = require('../../lib');
var Registry = require('../../registry');
-
module.exports = function handleXYDefaults(traceIn, traceOut, layout, coerce) {
- var len,
- x = coerce('x'),
- y = coerce('y');
+ var x = coerce('x');
+ var y = coerce('y');
+ var len;
var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults');
handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout);
if(x) {
+ var xlen = Lib.minRowLength(x);
if(y) {
- len = Math.min(x.length, y.length);
- }
- else {
- len = x.length;
+ len = Math.min(xlen, Lib.minRowLength(y));
+ } else {
+ len = xlen;
coerce('y0');
coerce('dy');
}
- }
- else {
+ } else {
if(!y) return 0;
- len = traceOut.y.length;
+ len = Lib.minRowLength(y);
coerce('x0');
coerce('dx');
}
diff --git a/src/traces/violin/calc.js b/src/traces/violin/calc.js
index 1994f4235c3..90a91b7ff10 100644
--- a/src/traces/violin/calc.js
+++ b/src/traces/violin/calc.js
@@ -123,7 +123,9 @@ function calcSpan(trace, cdi, valAxis, bandwidth) {
function calcSpanItem(index) {
var s = spanIn[index];
- var sc = valAxis.d2c(s, 0, trace[cdi.valLetter + 'calendar']);
+ var sc = valAxis.type === 'multicategory' ?
+ valAxis.r2c(s) :
+ valAxis.d2c(s, 0, trace[cdi.valLetter + 'calendar']);
return sc === BADNUM ? spanLoose[index] : sc;
}
diff --git a/test/image/baselines/box-violin-multicategory-on-val-axis.png b/test/image/baselines/box-violin-multicategory-on-val-axis.png
new file mode 100644
index 00000000000..a9791da61ea
Binary files /dev/null and b/test/image/baselines/box-violin-multicategory-on-val-axis.png differ
diff --git a/test/image/baselines/box-violin-x0-category-position.png b/test/image/baselines/box-violin-x0-category-position.png
new file mode 100644
index 00000000000..36e4b170345
Binary files /dev/null and b/test/image/baselines/box-violin-x0-category-position.png differ
diff --git a/test/image/baselines/box_grouped-multicategory.png b/test/image/baselines/box_grouped-multicategory.png
new file mode 100644
index 00000000000..395b10cc106
Binary files /dev/null and b/test/image/baselines/box_grouped-multicategory.png differ
diff --git a/test/image/baselines/finance_multicategory.png b/test/image/baselines/finance_multicategory.png
new file mode 100644
index 00000000000..f7bc6c99e8c
Binary files /dev/null and b/test/image/baselines/finance_multicategory.png differ
diff --git a/test/image/baselines/heatmap_multicategory.png b/test/image/baselines/heatmap_multicategory.png
new file mode 100644
index 00000000000..fd9d8ec5c93
Binary files /dev/null and b/test/image/baselines/heatmap_multicategory.png differ
diff --git a/test/image/baselines/multicategory-mirror.png b/test/image/baselines/multicategory-mirror.png
new file mode 100644
index 00000000000..88a03453a83
Binary files /dev/null and b/test/image/baselines/multicategory-mirror.png differ
diff --git a/test/image/baselines/multicategory-y.png b/test/image/baselines/multicategory-y.png
new file mode 100644
index 00000000000..f8986f8a8e6
Binary files /dev/null and b/test/image/baselines/multicategory-y.png differ
diff --git a/test/image/baselines/multicategory.png b/test/image/baselines/multicategory.png
new file mode 100644
index 00000000000..ce31358c8d7
Binary files /dev/null and b/test/image/baselines/multicategory.png differ
diff --git a/test/image/baselines/multicategory2.png b/test/image/baselines/multicategory2.png
new file mode 100644
index 00000000000..ccabc1796dc
Binary files /dev/null and b/test/image/baselines/multicategory2.png differ
diff --git a/test/image/baselines/multicategory_histograms.png b/test/image/baselines/multicategory_histograms.png
new file mode 100644
index 00000000000..a12bd2d0e64
Binary files /dev/null and b/test/image/baselines/multicategory_histograms.png differ
diff --git a/test/image/baselines/range_slider_rangemode.png b/test/image/baselines/range_slider_rangemode.png
index 0915d37b06d..40b03a5eb77 100644
Binary files a/test/image/baselines/range_slider_rangemode.png and b/test/image/baselines/range_slider_rangemode.png differ
diff --git a/test/image/baselines/violin_grouped_horz-multicategory.png b/test/image/baselines/violin_grouped_horz-multicategory.png
new file mode 100644
index 00000000000..b7ca2616b5d
Binary files /dev/null and b/test/image/baselines/violin_grouped_horz-multicategory.png differ
diff --git a/test/image/mocks/box-violin-multicategory-on-val-axis.json b/test/image/mocks/box-violin-multicategory-on-val-axis.json
new file mode 100644
index 00000000000..410563823fc
--- /dev/null
+++ b/test/image/mocks/box-violin-multicategory-on-val-axis.json
@@ -0,0 +1,38 @@
+{
+ "data": [
+ {
+ "type": "violin",
+ "x": [
+ [ "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016",
+ "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017",
+ "2018", "2018", "2018", "2018", "2018", "2018", "2018", "2018", "2018", "2018", "2018", "2018"
+ ],
+ [ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1", "day 2",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 1", "day 1",
+ "day 1", "day 1", "day 1", "day 1", "day 2", "day 2", "day 2",
+ "day 2", "day 2", "day 2", "day 1", "day 1", "day 1", "day 1",
+ "day 1", "day 1", "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"
+ ]
+ ],
+ "span": [0, 5]
+ },
+ {
+ "type": "box",
+ "x": [
+ [ "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016",
+ "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017",
+ "2018", "2018", "2018", "2018", "2018", "2018", "2018", "2018", "2018", "2018", "2018", "2018"
+ ],
+ [ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1", "day 2", "day 2",
+ "day 2", "day 2", "day 2", "day 2", "day 1", "day 1", "day 1", "day 1",
+ "day 1", "day 1", "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1", "day 2", "day 2",
+ "day 2", "day 2", "day 2", "day 2"
+ ]
+ ]
+ }
+ ],
+ "layout": {
+ "showlegend": false
+ }
+}
diff --git a/test/image/mocks/box-violin-x0-category-position.json b/test/image/mocks/box-violin-x0-category-position.json
new file mode 100644
index 00000000000..54d8eebb99a
--- /dev/null
+++ b/test/image/mocks/box-violin-x0-category-position.json
@@ -0,0 +1,99 @@
+{
+ "data": [
+ {
+ "y": [ 0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3, 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2 ],
+ "x": [
+ [ "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016", "2016",
+ "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017", "2017"
+ ],
+ [ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"
+ ]
+ ],
+ "type": "box"
+ },
+ {
+ "y": [ 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2, 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5, 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2 ],
+ "x0": ["2017", "day 1"],
+ "type": "violin"
+ },
+ {
+ "y": [ 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2, 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5, 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2 ],
+ "x0": "2017,day 2",
+ "type": "violin"
+ },
+ {
+ "y": [ 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2, 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5, 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2 ],
+ "x0": "1",
+ "type": "violin",
+ "name": "SHOULD NOT BE VISIBLE"
+ },
+
+ {
+ "y": [ 0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3, 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2 ],
+ "x": [ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1", "day 2",
+ "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"
+ ],
+ "type": "box",
+ "xaxis": "x2",
+ "yaxis": "y2"
+ },
+ {
+ "y": [ 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2, 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5, 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2 ],
+ "x0": "day 2",
+ "type": "violin",
+ "xaxis": "x2",
+ "yaxis": "y2"
+ },
+ {
+ "y": [ 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2, 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5, 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2 ],
+ "x0": 2,
+ "type": "violin",
+ "xaxis": "x2",
+ "yaxis": "y2"
+ }
+ ],
+ "layout": {
+ "grid": {
+ "rows": 2,
+ "columns": 1,
+ "pattern": "independent"
+ },
+ "annotations": [
+ {
+ "xref": "x",
+ "yref": "y",
+ "x": ["2017", "day 1"],
+ "text": "violin at x0:[2017,day 1]",
+ "y": 1.5
+ },
+ {
+ "xref": "x",
+ "yref": "y",
+ "x": "2017,day 2",
+ "text": "violin at x0:\"2017,day 2\"",
+ "y": 1.5,
+ "ax": 20
+ },
+ {
+ "xref": "x2",
+ "yref": "y2",
+ "x": "day 2",
+ "text": "violin at x0:\"day 2\"",
+ "y": 1.5
+ },
+ {
+ "xref": "x2",
+ "yref": "y2",
+ "x": 2,
+ "text": "violin at x0:2",
+ "y": 1.5
+ }
+ ],
+ "showlegend": false
+ }
+}
diff --git a/test/image/mocks/box_grouped-multicategory.json b/test/image/mocks/box_grouped-multicategory.json
new file mode 100644
index 00000000000..c745ca643da
--- /dev/null
+++ b/test/image/mocks/box_grouped-multicategory.json
@@ -0,0 +1,100 @@
+{
+ "data":[
+ {
+ "y":[
+ 0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3,
+ 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2,
+ 0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3
+ ],
+ "x":[
+ ["2016", "2016", "2016", "2016", "2016", "2016",
+ "2016", "2016", "2016", "2016", "2016", "2016",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2018", "2018", "2018", "2018", "2018", "2018",
+ "2018", "2018", "2018", "2018", "2018", "2018"],
+
+ ["day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"]
+ ],
+ "name":"kale",
+ "marker":{
+ "color":"#3D9970"
+ },
+ "type":"box"
+ },
+ {
+ "y":[
+ 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2,
+ 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5,
+ 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2
+ ],
+ "x":[
+ ["2016", "2016", "2016", "2016", "2016", "2016",
+ "2016", "2016", "2016", "2016", "2016", "2016",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2018", "2018", "2018", "2018", "2018", "2018",
+ "2018", "2018", "2018", "2018", "2018", "2018"],
+
+ ["day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"]
+ ],
+ "name":"radishes",
+ "marker":{
+ "color":"#FF4136"
+ },
+ "type":"box"
+ },
+ {
+ "y":[
+ 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5,
+ 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5,
+ 0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3
+ ],
+ "x":[
+ ["2016", "2016", "2016", "2016", "2016", "2016",
+ "2016", "2016", "2016", "2016", "2016", "2016",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2018", "2018", "2018", "2018", "2018", "2018",
+ "2018", "2018", "2018", "2018", "2018", "2018"],
+
+ ["day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"]
+ ],
+ "name":"carrots",
+ "marker":{
+ "color":"#FF851B"
+ },
+ "type":"box"
+ }
+ ],
+ "layout":{
+ "xaxis": {
+ "tickangle": 90
+ },
+ "yaxis":{
+ "zeroline":false,
+ "title":"normalized moisture"
+ },
+ "boxmode":"group",
+ "legend": {
+ "x": 0,
+ "y": 1,
+ "yanchor": "bottom"
+ }
+ }
+}
diff --git a/test/image/mocks/finance_multicategory.json b/test/image/mocks/finance_multicategory.json
new file mode 100644
index 00000000000..ba27d340f5f
--- /dev/null
+++ b/test/image/mocks/finance_multicategory.json
@@ -0,0 +1,41 @@
+{
+ "data": [
+ {
+ "name": "ohlc",
+ "type": "ohlc",
+ "open": [ 10, 11, 12, 13, 12, 13, 14, 15, 16 ],
+ "high": [ 15, 16, 17, 18, 17, 18, 19, 20, 21 ],
+ "low": [ 7, 8, 9, 10, 9, 10, 11, 12, 13 ],
+ "close": [ 9, 10, 12, 13, 13, 12, 14, 14, 17 ],
+ "x": [
+ [ "Group 1", "Group 1", "Group 1", "Group 2", "Group 2", "Group 2", "Group 3", "Group 3", "Group 3" ],
+ [ "a", "b", "c", "a", "b", "c", "a", "b", "c" ]
+ ]
+ },
+ {
+ "name": "candlestick",
+ "type": "candlestick",
+ "open": [ 20, 21, 22, 23, 22, 23, 24, 25, 26 ],
+ "high": [ 25, 26, 27, 28, 27, 28, 29, 30, 31 ],
+ "low": [ 17, 18, 19, 20, 19, 20, 21, 22, 23 ],
+ "close": [ 19, 20, 22, 23, 23, 22, 24, 24, 27 ],
+ "x": [
+ [ "Group 1", "Group 1", "Group 1", "Group 2", "Group 2", "Group 2", "Group 3", "Group 3", "Group 3" ],
+ [ "a", "b", "c", "a", "b", "c", "a", "b", "c" ]
+ ]
+ }
+ ],
+ "layout": {
+ "title": {
+ "text": "Finance traces on multicategory x-axis",
+ "xref": "paper",
+ "x": 0
+ },
+ "legend": {
+ "x": 1,
+ "xanchor": "right",
+ "y": 1,
+ "yanchor": "bottom"
+ }
+ }
+}
diff --git a/test/image/mocks/heatmap_multicategory.json b/test/image/mocks/heatmap_multicategory.json
new file mode 100644
index 00000000000..bcbb22c2c13
--- /dev/null
+++ b/test/image/mocks/heatmap_multicategory.json
@@ -0,0 +1,113 @@
+{
+ "data": [{
+ "type": "heatmap",
+ "name": "w/ 2d z",
+ "x": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "q2", "q3", "q4", "q1", "q2", "q3"]
+ ],
+ "y": [
+ ["Group 1", "Group 1", "Group 1", "Group 2", "Group 2", "Group 2", "Group 3", "Group 3", "Group 3"],
+ ["A", "B", "C", "A", "B", "C", "A", "B", "C"]
+ ],
+ "z": [
+ [ 0.304, 1.465, 2.474, 3.05, 4.38, 5.245, 6.12 ],
+ [ 0.3515, 1.326, 2.18, 3.26, 4.41, 5.25, 6.11 ],
+ [ 0.3994, 1.167, 2.09, 3.306, 4.305, 5.35, 6.00 ],
+ [ 0.297, 1.295, 2.49, 3.428, 4.13, 5.41, 6.38 ],
+ [ 0.4602, 1.2256, 2.3356, 3.0667, 4.498, 5.411, 6.29 ],
+ [ 0.0197, 1.274, 2.407, 3.22, 4.47, 5.44, 6.28 ],
+ [ 0.32, 1.44, 2.303, 3.115, 4.49, 5.25, 6.46 ],
+ [ 0.4446, 1.223, 2.367, 3.253, 4.385, 5.08, 6.19 ],
+ [ 0.1304, 1.046, 2.45, 3.226, 4.34, 5.40, 6.05 ]
+ ],
+ "colorbar": {
+ "x": -0.15,
+ "len": 0.25,
+ "y": 1,
+ "yanchor": "top"
+ }
+ }, {
+ "type": "contour",
+ "name": "w/ 2d z",
+ "x": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "q2", "q3", "q4", "q1", "q2", "q3"]
+ ],
+ "y": [
+ ["Group 1", "Group 1", "Group 1", "Group 2", "Group 2", "Group 2", "Group 3", "Group 3", "Group 3"],
+ ["A", "B", "C", "A", "B", "C", "A", "B", "C"]
+ ],
+ "z":[
+ [0, 10, 0, null, 0, 10, 0],
+ [10, 80, 20, null, 10, 80, 20],
+ [0, 40, 0, null, 0, 40, 0],
+ [0, 10, 0, null, 0, 10, 0],
+ [10, 80, 20, null, 10, 80, 20],
+ [0, 40, 0, null, 0, 40, 0],
+ [0, 10, 0, null, 0, 10, 0],
+ [10, 80, 20, null, 10, 80, 20],
+ [0, 40, 0, null, 0, 40, 0]
+ ],
+ "contours": {
+ "coloring": "lines",
+ "showlabels": true
+ },
+ "reversescale": true,
+ "line": {
+ "width": 4
+ },
+ "colorbar": {
+ "x": -0.15,
+ "len": 0.25,
+ "y": 0.65
+ }
+ }, {
+ "type": "heatmap",
+ "name": "w/ 1d z",
+ "x": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "q2", "q3", "q4", "q1", "q2", "q3"]
+ ],
+ "y": [
+ ["Group 1", "Group 1", "Group 1", "Group 2", "Group 2", "Group 2", "Group 3", "Group 3", "Group 3"],
+ ["A", "B", "C", "A", "B", "C", "A", "B", "C"]
+ ],
+ "z": [1, 2, 3, 6, 5, 4, 11, 12, 13],
+ "opacity": 0.4,
+ "colorscale": "Greens",
+ "colorbar": {
+ "x": -0.15,
+ "len": 0.25,
+ "y": 0.35
+ }
+ }, {
+ "type": "heatmap",
+ "name": "w/ x0|y0",
+ "x0": 2,
+ "y0": 2,
+ "z": [[1, 2], [12, 13]],
+ "opacity": 0.4,
+ "colorscale": "Reds",
+ "colorbar": {
+ "x": -0.15,
+ "len": 0.25,
+ "y": 0,
+ "yanchor": "bottom"
+ }
+ }],
+ "layout": {
+ "title": {
+ "text": "Multi-category heatmap/contour",
+ "x": 0,
+ "xref": "paper"
+ },
+ "xaxis": {
+ "tickson": "boundaries"
+ },
+ "yaxis": {
+ "tickson": "boundaries",
+ "side": "right"
+ }
+ }
+}
diff --git a/test/image/mocks/multicategory-mirror.json b/test/image/mocks/multicategory-mirror.json
new file mode 100644
index 00000000000..dd11116228e
--- /dev/null
+++ b/test/image/mocks/multicategory-mirror.json
@@ -0,0 +1,34 @@
+{
+ "data": [
+ {
+ "type": "bar",
+ "x": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "q2", "q3", "q4", "q1", "q2", "q3" ]
+ ],
+ "y": [1, 2, 3, 1, 3, 2, 3, 1]
+ },
+ {
+ "type": "bar",
+ "x": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "q2", "q3", "q4", "q1", "q2", "q3"]
+ ],
+ "y": [1.12, 2.15, 3.07, 1.48, 2.78, 1.95, 2.54, 0.64]
+ }
+ ],
+ "layout": {
+ "xaxis": {
+ "ticks": "outside",
+ "showline": true,
+ "mirror": "ticks",
+ "zeroline": false
+ },
+ "yaxis": {
+ "showline": true,
+ "ticks": "outside",
+ "mirror": "ticks",
+ "range": [-0.5, 3.5]
+ }
+ }
+}
diff --git a/test/image/mocks/multicategory-y.json b/test/image/mocks/multicategory-y.json
new file mode 100644
index 00000000000..263ab1db6c7
--- /dev/null
+++ b/test/image/mocks/multicategory-y.json
@@ -0,0 +1,77 @@
+{
+ "data": [
+ {
+ "type": "bar",
+ "orientation": "h",
+ "y": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "q2", "q3", "q4", "q1", "q2", "q3" ]
+ ],
+ "x": [1, 2, 3, 1, 3, 2, 3, 1]
+ },
+ {
+ "type": "scatter",
+ "y": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "q2", "q3", "q4", "q1", "q2", "q3"]
+ ],
+ "x": [1.12, 2.15, 3.07, 1.48, 2.78, 1.95, 2.54, 0.64]
+ },
+
+ {
+ "mode": "markers",
+ "marker": {
+ "symbol": "square"
+ },
+ "y": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "looooooooooooooooong", "q3", "q4", "q1", "q2", "q3"]
+ ],
+ "x": [1, 2, 3, 1, 3, 2, 3, 1],
+ "xaxis": "x2",
+ "yaxis": "y2"
+ }
+ ],
+ "layout": {
+ "grid": {
+ "rows": 2,
+ "columns": 1,
+ "pattern": "independent",
+ "ygap": 0.2
+ },
+ "yaxis": {
+ "title": "MULTI-CATEGORY",
+ "tickfont": {"size": 16},
+ "ticks": "outside",
+ "tickson": "boundaries"
+ },
+ "yaxis2": {
+ "title": "MULTI-CATEGORY",
+ "tickfont": {"size": 12},
+ "ticks": "outside",
+ "tickson": "boundaries"
+ },
+ "xaxis": {
+ "domain": [0.05, 1]
+ },
+ "xaxis2": {
+ "domain": [0.3, 1]
+ },
+ "showlegend": false,
+ "hovermode": "y",
+ "width": 600,
+ "height": 700,
+ "annotations": [{
+ "text": "LOOK",
+ "xref": "x", "x": 3,
+ "yref": "y", "y": 6,
+ "ax": 50, "ay": -50
+ }],
+ "shapes": [{
+ "type": "line",
+ "xref": "paper", "x0": 0.05, "x1": 1,
+ "yref": "y", "y0": 7, "y1": 7,
+ "line": {"color": "red"}
+ }]
+ }
+}
diff --git a/test/image/mocks/multicategory.json b/test/image/mocks/multicategory.json
new file mode 100644
index 00000000000..815358e3175
--- /dev/null
+++ b/test/image/mocks/multicategory.json
@@ -0,0 +1,27 @@
+{
+ "data": [
+ {
+ "type": "bar",
+ "x": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "q2", "q3", "q4", "q1", "q2", "q3" ]
+ ],
+ "y": [1, 2, 3, 1, 3, 2, 3, 1]
+ },
+ {
+ "type": "bar",
+ "x": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "q2", "q3", "q4", "q1", "q2", "q3"]
+ ],
+ "y": [1.12, 2.15, 3.07, 1.48, 2.78, 1.95, 2.54, 0.64]
+ }
+ ],
+ "layout": {
+ "xaxis": {
+ "title": "MULTI-CATEGORY",
+ "tickfont": {"size": 16},
+ "ticks": "outside"
+ }
+ }
+}
diff --git a/test/image/mocks/multicategory2.json b/test/image/mocks/multicategory2.json
new file mode 100644
index 00000000000..8f068be2077
--- /dev/null
+++ b/test/image/mocks/multicategory2.json
@@ -0,0 +1,37 @@
+{
+ "data": [
+ {
+ "mode": "markers",
+ "marker": {
+ "symbol": "square"
+ },
+ "x": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "looooooooooooooooong", "q3", "q4", "q1", "q2", "q3"]
+ ],
+ "y": [1, 2, 3, 1, 3, 2, 3, 1]
+ },
+ {
+ "mode": "markers",
+ "marker": {
+ "symbol": "square"
+ },
+ "x": [
+ ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+ ["q1", "looooooooooooooooong", "q3", "q4", "q1", "q2", "q3"]
+ ],
+ "y": [0.63, 2.17, 3.11, 1.07, 3.08, 1.94, 2.55, 0.59]
+ }
+ ],
+ "layout": {
+ "xaxis": {
+ "title": "MULTI-CATEGORY ON TOP",
+ "side": "top",
+ "automargin": true,
+ "tickson": "labels"
+ },
+ "showlegend": false,
+ "width": 400,
+ "height": 800
+ }
+}
diff --git a/test/image/mocks/multicategory_histograms.json b/test/image/mocks/multicategory_histograms.json
new file mode 100644
index 00000000000..114c05e993e
--- /dev/null
+++ b/test/image/mocks/multicategory_histograms.json
@@ -0,0 +1,121 @@
+{
+ "data": [
+ {
+ "type": "histogram2d",
+ "name": "hist2d",
+ "x": [
+ [ 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018 ],
+ [ "a", "a", "a", "a", "a", "a", "b", "b", "c", "c", "c", "c" ]
+ ],
+ "y": [
+ [ 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2018, 2017, 2017 ],
+ [ "a", "a", "a", "a", "a", "a", "b", "b", "c", "c", "c", "c", "b", "c", "a" ]
+ ],
+ "colorscale": "Viridis",
+ "opacity": 0.8,
+ "colorbar": {
+ "x": 0.7,
+ "xanchor": "left",
+ "y": 0.7,
+ "yanchor": "bottom",
+ "len": 0.3,
+ "title": {
+ "side": "right",
+ "text": "hist2d"
+ }
+ }
+ },
+ {
+ "mode": "markers",
+ "x": [
+ [ 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018 ],
+ [ "a", "a", "a", "a", "a", "a", "b", "b", "c", "c", "c", "c" ]
+ ],
+ "y": [
+ [ 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2018, 2017, 2017 ],
+ [ "a", "a", "a", "a", "a", "a", "b", "b", "c", "c", "c", "c", "b", "c", "a" ]
+ ],
+ "marker": {
+ "color": "#d3d3d3",
+ "size": 18,
+ "opacity": 0.3,
+ "line": {"color": "black", "width": 1}
+ }
+ },
+
+ {
+ "type": "histogram2dcontour",
+ "name": "hist2dcontour",
+ "x": [
+ [ 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018 ],
+ [ "a", "a", "a", "a", "a", "a", "b", "b", "c", "c", "c", "c" ]
+ ],
+ "y": [
+ [ 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2018, 2017, 2017 ],
+ [ "a", "a", "a", "a", "a", "a", "b", "b", "c", "c", "c", "c", "b", "c", "a" ]
+ ],
+ "contours": {
+ "coloring": "lines"
+ },
+ "line": {
+ "width": 4
+ },
+ "colorscale": "Viridis",
+ "colorbar": {
+ "x": 1,
+ "xanchor": "right",
+ "y": 0.7,
+ "yanchor": "bottom",
+ "len": 0.3,
+ "title": {
+ "side": "right",
+ "text": "hist2dcontour"
+ }
+ }
+ },
+ {
+ "type": "histogram",
+ "name": "hist-x",
+ "x": [
+ [ 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018 ],
+ [ "a", "a", "a", "a", "a", "a", "b", "b", "c", "c", "c", "c" ]
+ ],
+ "yaxis": "y2",
+ "marker": {
+ "color": "#008080"
+ }
+ },
+ {
+ "type": "histogram",
+ "name": "hist-y",
+ "y": [
+ [ 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2017, 2018, 2018, 2017, 2017 ],
+ [ "a", "a", "a", "a", "a", "a", "b", "b", "c", "c", "c", "c", "b", "c", "a" ]
+ ],
+ "xaxis": "x2",
+ "marker": {
+ "color": "#008080"
+ }
+ }
+ ],
+ "layout": {
+ "title": {
+ "text": "Multi-category histograms",
+ "xref": "paper",
+ "x": 0
+ },
+ "xaxis": {
+ "domain": [0, 0.65]
+ },
+ "yaxis": {
+ "domain": [0, 0.65]
+ },
+ "xaxis2": {
+ "domain": [0.7, 1]
+ },
+ "yaxis2": {
+ "domain": [0.7, 1]
+ },
+ "showlegend": false
+ }
+}
diff --git a/test/image/mocks/violin_grouped_horz-multicategory.json b/test/image/mocks/violin_grouped_horz-multicategory.json
new file mode 100644
index 00000000000..633c665e8d6
--- /dev/null
+++ b/test/image/mocks/violin_grouped_horz-multicategory.json
@@ -0,0 +1,102 @@
+{
+ "data":[
+ {
+ "x":[
+ 0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3,
+ 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2,
+ 0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3
+ ],
+ "y":[
+ ["2016", "2016", "2016", "2016", "2016", "2016",
+ "2016", "2016", "2016", "2016", "2016", "2016",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2018", "2018", "2018", "2018", "2018", "2018",
+ "2018", "2018", "2018", "2018", "2018", "2018"],
+
+ ["day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"]
+ ],
+ "name":"kale",
+ "marker":{
+ "color":"#3D9970"
+ },
+ "orientation": "h",
+ "type":"violin"
+ },
+ {
+ "x":[
+ 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2,
+ 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5,
+ 0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2
+ ],
+ "y":[
+ ["2016", "2016", "2016", "2016", "2016", "2016",
+ "2016", "2016", "2016", "2016", "2016", "2016",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2018", "2018", "2018", "2018", "2018", "2018",
+ "2018", "2018", "2018", "2018", "2018", "2018"],
+
+ ["day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"]
+ ],
+ "name":"radishes",
+ "marker":{
+ "color":"#FF4136"
+ },
+ "orientation": "h",
+ "type":"violin"
+ },
+ {
+ "x":[
+ 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5,
+ 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5,
+ 0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3
+ ],
+ "y":[
+ ["2016", "2016", "2016", "2016", "2016", "2016",
+ "2016", "2016", "2016", "2016", "2016", "2016",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2017", "2017", "2017", "2017", "2017", "2017",
+ "2018", "2018", "2018", "2018", "2018", "2018",
+ "2018", "2018", "2018", "2018", "2018", "2018"],
+
+ ["day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2",
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"]
+ ],
+ "name":"carrots",
+ "marker":{
+ "color":"#FF851B"
+ },
+ "orientation": "h",
+ "type":"violin"
+ }
+ ],
+ "layout":{
+ "yaxis":{
+ "zeroline":false,
+ "title":"normalized moisture"
+ },
+ "violinmode":"group",
+ "legend": {
+ "x": 0,
+ "xanchor": "right",
+ "y": 1,
+ "yanchor": "bottom"
+ },
+ "margin": {"l": 100}
+ }
+}
diff --git a/test/jasmine/assets/mock_lists.js b/test/jasmine/assets/mock_lists.js
index 7b6387ab140..1918a44f77f 100644
--- a/test/jasmine/assets/mock_lists.js
+++ b/test/jasmine/assets/mock_lists.js
@@ -21,6 +21,7 @@ var svgMockList = [
['geo_first', require('@mocks/geo_first.json')],
['layout_image', require('@mocks/layout_image.json')],
['layout-colorway', require('@mocks/layout-colorway.json')],
+ ['multicategory', require('@mocks/multicategory.json')],
['polar_categories', require('@mocks/polar_categories.json')],
['polar_direction', require('@mocks/polar_direction.json')],
['polar_wind-rose', require('@mocks/polar_wind-rose.json')],
diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js
index 39969dd2f57..a4f31b78d11 100644
--- a/test/jasmine/tests/axes_test.js
+++ b/test/jasmine/tests/axes_test.js
@@ -267,6 +267,68 @@ describe('Test axes', function() {
});
checkTypes('date', 'linear');
});
+
+ it('2d coordinate array are considered *multicategory*', function() {
+ supplyWithTrace({
+ x: [
+ [2018, 2018, 2017, 2017],
+ ['a', 'b', 'a', 'b']
+ ],
+ y: [
+ ['a', 'b', 'c'],
+ ['d', 'e', 'f']
+ ]
+ });
+ checkTypes('multicategory', 'multicategory');
+
+ supplyWithTrace({
+ x: [
+ [2018, 2018, 2017, 2017],
+ [2018, 2018, 2017, 2017]
+ ],
+ y: [
+ ['2018', '2018', '2017', '2017'],
+ ['2018', '2018', '2017', '2017']
+ ]
+ });
+ checkTypes('multicategory', 'multicategory');
+
+ supplyWithTrace({
+ x: [
+ new Float32Array([2018, 2018, 2017, 2017]),
+ [2018, 2018, 2017, 2017]
+ ],
+ y: [
+ [2018, 2018, 2017, 2017],
+ new Float64Array([2018, 2018, 2017, 2017])
+ ]
+ });
+ checkTypes('multicategory', 'multicategory');
+
+ supplyWithTrace({
+ x: [
+ [2018, 2018, 2017, 2017]
+ ],
+ y: [
+ null,
+ ['d', 'e', 'f']
+ ]
+ });
+ checkTypes('linear', 'linear');
+
+ supplyWithTrace({
+ type: 'carpet',
+ x: [
+ [2018, 2018, 2017, 2017],
+ ['a', 'b', 'a', 'b']
+ ],
+ y: [
+ ['a', 'b', 'c'],
+ ['d', 'e', 'f']
+ ]
+ });
+ checkTypes('linear', 'linear');
+ });
});
it('should set undefined linewidth/linecolor if linewidth, linecolor or showline is not supplied', function() {
@@ -1347,6 +1409,14 @@ describe('Test axes', function() {
expect(axOut.tickvals).toEqual([2, 4, 6, 8]);
expect(axOut.ticktext).toEqual(['who', 'do', 'we', 'appreciate']);
});
+
+ it('should not coerce ticktext/tickvals on multicategory axes', function() {
+ var axIn = {tickvals: [1, 2, 3], ticktext: ['4', '5', '6']};
+ var axOut = {};
+ mockSupplyDefaults(axIn, axOut, 'multicategory');
+ expect(axOut.tickvals).toBe(undefined);
+ expect(axOut.ticktext).toBe(undefined);
+ });
});
describe('saveRangeInitial', function() {
@@ -2769,6 +2839,112 @@ describe('Test axes', function() {
expect(out).toEqual([946684800000, 978307200000, 1009843200000]);
});
});
+
+ describe('should set up category maps correctly for multicategory axes', function() {
+ it('case 1', function() {
+ var out = _makeCalcdata({
+ x: [['1', '1', '2', '2'], ['a', 'b', 'a', 'b']]
+ }, 'x', 'multicategory');
+
+ expect(out).toEqual([0, 1, 2, 3]);
+ expect(ax._categories).toEqual([['1', 'a'], ['1', 'b'], ['2', 'a'], ['2', 'b']]);
+ expect(ax._categoriesMap).toEqual({'1,a': 0, '1,b': 1, '2,a': 2, '2,b': 3});
+ });
+
+ it('case 2', function() {
+ var out = _makeCalcdata({
+ x: [['1', '2', '1', '2'], ['a', 'a', 'b', 'b']]
+ }, 'x', 'multicategory');
+
+ expect(out).toEqual([0, 1, 2, 3]);
+ expect(ax._categories).toEqual([['1', 'a'], ['1', 'b'], ['2', 'a'], ['2', 'b']]);
+ expect(ax._categoriesMap).toEqual({'1,a': 0, '1,b': 1, '2,a': 2, '2,b': 3});
+ });
+
+ it('case invalid in x[0]', function() {
+ var out = _makeCalcdata({
+ x: [['1', '2', null, '2'], ['a', 'a', 'b', 'b']]
+ }, 'x', 'multicategory');
+
+ expect(out).toEqual([0, 1, 2, BADNUM]);
+ expect(ax._categories).toEqual([['1', 'a'], ['2', 'a'], ['2', 'b']]);
+ expect(ax._categoriesMap).toEqual({'1,a': 0, '2,a': 1, '2,b': 2});
+ });
+
+ it('case invalid in x[1]', function() {
+ var out = _makeCalcdata({
+ x: [['1', '2', '1', '2'], ['a', 'a', null, 'b']]
+ }, 'x', 'multicategory');
+
+ expect(out).toEqual([0, 1, 2, BADNUM]);
+ expect(ax._categories).toEqual([['1', 'a'], ['2', 'a'], ['2', 'b']]);
+ expect(ax._categoriesMap).toEqual({'1,a': 0, '2,a': 1, '2,b': 2});
+ });
+
+ it('case 1D coordinate array', function() {
+ var out = _makeCalcdata({
+ x: ['a', 'b', 'c']
+ }, 'x', 'multicategory');
+
+ expect(out).toEqual([BADNUM, BADNUM, BADNUM]);
+ expect(ax._categories).toEqual([]);
+ expect(ax._categoriesMap).toEqual(undefined);
+ });
+
+ it('case 2D 1-row coordinate array', function() {
+ var out = _makeCalcdata({
+ x: [['a', 'b', 'c']]
+ }, 'x', 'multicategory');
+
+ expect(out).toEqual([BADNUM, BADNUM, BADNUM]);
+ expect(ax._categories).toEqual([]);
+ expect(ax._categoriesMap).toEqual(undefined);
+ });
+
+ it('case 2D with empty x[0] row coordinate array', function() {
+ var out = _makeCalcdata({
+ x: [null, ['a', 'b', 'c']]
+ }, 'x', 'multicategory');
+
+ expect(out).toEqual([BADNUM, BADNUM]);
+ expect(ax._categories).toEqual([]);
+ expect(ax._categoriesMap).toEqual(undefined);
+ });
+
+ it('case with inner typed arrays and set type:multicategory', function() {
+ var out = _makeCalcdata({
+ x: [
+ new Float32Array([1, 2, 1, 2]),
+ new Float32Array([10, 10, 20, 20])
+ ]
+ }, 'x', 'multicategory');
+
+ expect(out).toEqual([0, 1, 2, 3]);
+ expect(ax._categories).toEqual([[1, 10], [1, 20], [2, 10], [2, 20]]);
+ expect(ax._categoriesMap).toEqual({'1,10': 0, '1,20': 1, '2,10': 2, '2,20': 3});
+ });
+ });
+
+ describe('2d coordinate array on non-multicategory axes should return BADNUMs', function() {
+ var axTypes = ['linear', 'log', 'date'];
+
+ axTypes.forEach(function(t) {
+ it('- case ' + t, function() {
+ var out = _makeCalcdata({
+ x: [['1', '1', '2', '2'], ['a', 'b', 'a', 'b']]
+ }, 'x', t);
+ expect(out).toEqual([BADNUM, BADNUM, BADNUM, BADNUM]);
+ });
+ });
+
+ it('- case category', function() {
+ var out = _makeCalcdata({
+ x: [['1', '1', '2', '2'], ['a', 'b', 'a', 'b']]
+ }, 'x', 'category');
+ // picks out length=4
+ expect(out).toEqual([0, 1, undefined, undefined]);
+ });
+ });
});
describe('automargin', function() {
@@ -2800,7 +2976,7 @@ describe('Test axes', function() {
Plotly.plot(gd, data)
.then(function() {
- expect(gd._fullLayout.xaxis._lastangle).toBe(30);
+ expect(gd._fullLayout.xaxis._tickAngles.xtick).toBe(30);
initialSize = previousSize = Lib.extendDeep({}, gd._fullLayout._size);
return Plotly.relayout(gd, {'yaxis.automargin': true});
diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js
index e950af844ee..f2b4911222a 100644
--- a/test/jasmine/tests/bar_test.js
+++ b/test/jasmine/tests/bar_test.js
@@ -878,6 +878,24 @@ describe('Bar.crossTraceCalc (formerly known as setPositions)', function() {
var ya = gd._fullLayout.yaxis;
expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([1.496, 2.027], undefined, '(ya.range)');
});
+
+ it('should ignore *base* on category axes', function() {
+ var gd = mockBarPlot([
+ {x: ['a', 'b', 'c'], base: [0.2, -0.2, 1]},
+ ]);
+
+ expect(gd._fullLayout.xaxis.type).toBe('category');
+ assertPointField(gd.calcdata, 'b', [[0, 0, 0]]);
+ });
+
+ it('should ignore *base* on multicategory axes', function() {
+ var gd = mockBarPlot([
+ {x: [['a', 'a', 'b', 'b'], ['1', '2', '1', '2']], base: 10}
+ ]);
+
+ expect(gd._fullLayout.xaxis.type).toBe('multicategory');
+ assertPointField(gd.calcdata, 'b', [[0, 0, 0, 0]]);
+ });
});
describe('A bar plot', function() {
diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js
index 4843939de02..b124e9cc2fb 100644
--- a/test/jasmine/tests/cartesian_interact_test.js
+++ b/test/jasmine/tests/cartesian_interact_test.js
@@ -673,6 +673,77 @@ describe('axis zoom/pan and main plot zoom', function() {
.catch(failTest)
.then(done);
});
+
+ it('should compute correct multicategory tick label span during drag', function(done) {
+ var fig = Lib.extendDeep({}, require('@mocks/multicategory.json'));
+
+ var dragCoverNode;
+ var p1;
+
+ function _dragStart(draggerClassName, p0, dp) {
+ var node = getDragger('xy', draggerClassName);
+ mouseEvent('mousemove', p0[0], p0[1], {element: node});
+ mouseEvent('mousedown', p0[0], p0[1], {element: node});
+
+ var promise = drag.waitForDragCover().then(function(dcn) {
+ dragCoverNode = dcn;
+ p1 = [p0[0] + dp[0], p0[1] + dp[1]];
+ mouseEvent('mousemove', p1[0], p1[1], {element: dragCoverNode});
+ });
+ return promise;
+ }
+
+ function _assertAndDragEnd(msg, exp) {
+ _assertLabels(msg, exp);
+ mouseEvent('mouseup', p1[0], p1[1], {element: dragCoverNode});
+ return drag.waitForDragCoverRemoval();
+ }
+
+ function _assertLabels(msg, exp) {
+ var tickLabels = d3.select(gd).selectAll('.xtick > text');
+ expect(tickLabels.size()).toBe(exp.angle.length, msg + ' - # of tick labels');
+
+ tickLabels.each(function(_, i) {
+ var t = d3.select(this).attr('transform');
+ var rotate = (t.split('rotate(')[1] || '').split(')')[0];
+ var angle = rotate.split(',')[0];
+ expect(Number(angle)).toBe(exp.angle[i], msg + ' - node ' + i);
+
+ });
+
+ var tickLabels2 = d3.select(gd).selectAll('.xtick2 > text');
+ expect(tickLabels2.size()).toBe(exp.y.length, msg + ' - # of secondary labels');
+
+ tickLabels2.each(function(_, i) {
+ var y = d3.select(this).attr('y');
+ expect(Number(y)).toBeWithin(exp.y[i], 5, msg + ' - node ' + i);
+ });
+ }
+
+ Plotly.plot(gd, fig)
+ .then(function() {
+ _assertLabels('base', {
+ angle: [0, 0, 0, 0, 0, 0, 0],
+ y: [406, 406]
+ });
+ })
+ .then(function() { return _dragStart('edrag', [585, 390], [-340, 0]); })
+ .then(function() {
+ return _assertAndDragEnd('drag to wide-range -> rotates labels', {
+ angle: [90, 90, 90, 90, 90, 90, 90],
+ y: [430, 430]
+ });
+ })
+ .then(function() { return _dragStart('edrag', [585, 390], [100, 0]); })
+ .then(function() {
+ return _assertAndDragEnd('drag to narrow-range -> un-rotates labels', {
+ angle: [0, 0, 0, 0, 0, 0, 0],
+ y: [406, 406]
+ });
+ })
+ .catch(failTest)
+ .then(done);
+ });
});
describe('Event data:', function() {
diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js
index 28e18931514..7152e38429d 100644
--- a/test/jasmine/tests/heatmap_test.js
+++ b/test/jasmine/tests/heatmap_test.js
@@ -164,15 +164,10 @@ describe('heatmap convertColumnXYZ', function() {
'use strict';
var trace;
-
- function makeMockAxis() {
- return {
- d2c: function(v) { return v; }
- };
- }
-
- var xa = makeMockAxis();
- var ya = makeMockAxis();
+ var xa = {type: 'linear'};
+ var ya = {type: 'linear'};
+ setConvert(xa);
+ setConvert(ya);
function checkConverted(trace, x, y, z) {
trace._length = Math.min(trace.x.length, trace.y.length, trace.z.length);
@@ -303,6 +298,13 @@ describe('heatmap calc', function() {
fullTrace._extremes = {};
+ // we used to call ax.setScale during supplyDefaults, and this had a
+ // fallback to provide _categories and _categoriesMap. Now neither of
+ // those is true... anyway the right way to do this though is
+ // ax.clearCalc.
+ fullLayout.xaxis.clearCalc();
+ fullLayout.yaxis.clearCalc();
+
var out = Heatmap.calc(gd, fullTrace)[0];
out._xcategories = fullLayout.xaxis._categories;
out._ycategories = fullLayout.yaxis._categories;
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index b8b3bc16dad..5e0ea9a037b 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -2055,6 +2055,81 @@ describe('hover on fill', function() {
});
});
+describe('Hover on multicategory axes', function() {
+ var gd;
+ var eventData;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+
+ afterEach(destroyGraphDiv);
+
+ function _hover(x, y) {
+ delete gd._hoverdata;
+ Lib.clearThrottle();
+ mouseEvent('mousemove', x, y);
+ }
+
+ it('should work for bar traces', function(done) {
+ Plotly.plot(gd, [{
+ type: 'bar',
+ x: [['2018', '2018', '2019', '2019'], ['a', 'b', 'a', 'b']],
+ y: [1, 2, -1, 3]
+ }], {
+ bargap: 0,
+ width: 400,
+ height: 400
+ })
+ .then(function() {
+ gd.on('plotly_hover', function(d) {
+ eventData = d.points[0];
+ });
+ })
+ .then(function() { _hover(200, 200); })
+ .then(function() {
+ assertHoverLabelContent({ nums: '−1', axis: '2019 - a' });
+ expect(eventData.x).toEqual(['2019', 'a']);
+ })
+ .then(function() {
+ return Plotly.update(gd,
+ {hovertemplate: 'Sample: %{x[1]}
Year: %{x[0]}'},
+ {hovermode: 'closest'}
+ );
+ })
+ .then(function() { _hover(140, 200); })
+ .then(function() {
+ assertHoverLabelContent({ nums: 'Sample: b\nYear: 2018' });
+ expect(eventData.x).toEqual(['2018', 'b']);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should work on heatmap traces', function(done) {
+ var fig = Lib.extendDeep({}, require('@mocks/heatmap_multicategory.json'));
+ fig.data = [fig.data[0]];
+ fig.layout.width = 500;
+ fig.layout.height = 500;
+
+ Plotly.plot(gd, fig)
+ .then(function() {
+ gd.on('plotly_hover', function(d) {
+ eventData = d.points[0];
+ });
+ })
+ .then(function() { _hover(200, 200); })
+ .then(function() {
+ assertHoverLabelContent({
+ nums: 'x: 2017 - q3\ny: Group 3 - A\nz: 2.303'
+ });
+ expect(eventData.x).toEqual(['2017', 'q3']);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+});
+
describe('hover updates', function() {
'use strict';
diff --git a/test/jasmine/tests/plot_api_react_test.js b/test/jasmine/tests/plot_api_react_test.js
index 812eb6f63e5..4dbf48ddc8a 100644
--- a/test/jasmine/tests/plot_api_react_test.js
+++ b/test/jasmine/tests/plot_api_react_test.js
@@ -468,6 +468,51 @@ describe('@noCIdep Plotly.react', function() {
.then(done);
});
+ it('can change from scatter to category scatterpolar and back', function(done) {
+ function scatter() {
+ return {
+ data: [{x: ['a', 'b'], y: [1, 2]}],
+ layout: {width: 400, height: 400, margin: {r: 80, t: 20}}
+ };
+ }
+
+ function scatterpolar() {
+ return {
+ // the bug https://github.com/plotly/plotly.js/issues/3255
+ // required all of this to change:
+ // - type -> scatterpolar
+ // - category theta
+ // - margins changed
+ data: [{type: 'scatterpolar', r: [1, 2, 3], theta: ['a', 'b', 'c']}],
+ layout: {width: 400, height: 400, margin: {r: 80, t: 50}}
+ };
+ }
+
+ function countTraces(scatterTraces, polarTraces) {
+ expect(document.querySelectorAll('.scatter').length)
+ .toBe(scatterTraces + polarTraces);
+ expect(document.querySelectorAll('.xy .scatter').length)
+ .toBe(scatterTraces);
+ expect(document.querySelectorAll('.polar .scatter').length)
+ .toBe(polarTraces);
+ }
+
+ Plotly.newPlot(gd, scatter())
+ .then(function() {
+ countTraces(1, 0);
+ return Plotly.react(gd, scatterpolar());
+ })
+ .then(function() {
+ countTraces(0, 1);
+ return Plotly.react(gd, scatter());
+ })
+ .then(function() {
+ countTraces(1, 0);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
it('can change data in candlesticks multiple times', function(done) {
// test that we've fixed the original issue in
// https://github.com/plotly/plotly.js/issues/2510
diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js
index 49c7c33ed54..11db2d50a96 100644
--- a/test/jasmine/tests/plots_test.js
+++ b/test/jasmine/tests/plots_test.js
@@ -83,9 +83,9 @@ describe('Test Plots', function() {
expect(gd._fullLayout.someFunc).toBe(oldFullLayout.someFunc);
expect(gd._fullLayout.xaxis.c2p)
- .not.toBe(oldFullLayout.xaxis.c2p, '(set during ax.setScale');
+ .not.toBe(oldFullLayout.xaxis.c2p, '(set during setConvert)');
expect(gd._fullLayout.yaxis._m)
- .not.toBe(oldFullLayout.yaxis._m, '(set during ax.setScale');
+ .toBe(oldFullLayout.yaxis._m, '(we don\'t run ax.setScale here)');
});
it('should include the correct reference to user data', function() {
diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js
index 6ba00809e06..a6246a251f4 100644
--- a/test/jasmine/tests/range_slider_test.js
+++ b/test/jasmine/tests/range_slider_test.js
@@ -28,9 +28,7 @@ function getRangeSliderChild(index) {
}
function countRangeSliderClipPaths() {
- return d3.selectAll('defs').selectAll('*').filter(function() {
- return this.id.indexOf('rangeslider') !== -1;
- }).size();
+ return document.querySelectorAll('defs [id*=rangeslider]').length;
}
function testTranslate1D(node, val) {
diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js
index 19dea8d9ffd..6e97b1531b1 100644
--- a/test/jasmine/tests/scatter_test.js
+++ b/test/jasmine/tests/scatter_test.js
@@ -203,6 +203,72 @@ describe('Test scatter', function() {
});
});
+ describe('should find correct coordinate length', function() {
+ function _supply() {
+ supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ }
+
+ it('- x 2d', function() {
+ traceIn = {
+ x: [
+ ['1', '2', '1', '2', '1', '2'],
+ ['a', 'a', 'b', 'b']
+ ],
+ };
+ _supply();
+ expect(traceOut._length).toBe(4);
+ });
+
+ it('- y 2d', function() {
+ traceIn = {
+ y: [
+ ['1', '2', '1', '2', '1', '2'],
+ ['a', 'a', 'b', 'b']
+ ],
+ };
+ _supply();
+ expect(traceOut._length).toBe(4);
+ });
+
+ it('- x 2d / y 1d', function() {
+ traceIn = {
+ x: [
+ ['1', '2', '1', '2', '1', '2'],
+ ['a', 'a', 'b', 'b']
+ ],
+ y: [1, 2, 3, 4, 5, 6]
+ };
+ _supply();
+ expect(traceOut._length).toBe(4);
+ });
+
+ it('- x 1d / y 2d', function() {
+ traceIn = {
+ y: [
+ ['1', '2', '1', '2', '1', '2'],
+ ['a', 'a', 'b', 'b']
+ ],
+ x: [1, 2, 3, 4, 5, 6]
+ };
+ _supply();
+ expect(traceOut._length).toBe(4);
+ });
+
+ it('- x 2d / y 2d', function() {
+ traceIn = {
+ x: [
+ ['1', '2', '1', '2', '1', '2'],
+ ['a', 'a', 'b', 'b', 'c', 'c']
+ ],
+ y: [
+ ['1', '2', '1', '2', '1', '2'],
+ ['a', 'a', 'b', 'b', 'c', 'c', 'd', 'd']
+ ]
+ };
+ _supply();
+ expect(traceOut._length).toBe(6);
+ });
+ });
});
describe('isBubble', function() {
diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js
index 3c4d81911b3..3e09444efd9 100644
--- a/test/jasmine/tests/splom_test.js
+++ b/test/jasmine/tests/splom_test.js
@@ -820,11 +820,11 @@ describe('Test splom interactions:', function() {
function _assert(msg, exp) {
var splomScenes = gd._fullLayout._splomScenes;
- var ids = Object.keys(splomScenes);
+ var ids = gd._fullData.map(function(trace) { return trace.uid; });
for(var i = 0; i < 3; i++) {
var drawFn = splomScenes[ids[i]].draw;
- expect(drawFn).toHaveBeenCalledTimes(exp[i], msg + ' - trace ' + i);
+ expect(drawFn.calls.count()).toBe(exp[i], msg + ' - trace ' + i);
drawFn.calls.reset();
}
}
@@ -869,7 +869,7 @@ describe('Test splom interactions:', function() {
methods.forEach(function(m) { spyOn(Plots, m).and.callThrough(); });
- function assetsFnCall(msg, exp) {
+ function assertFnCall(msg, exp) {
methods.forEach(function(m) {
expect(Plots[m]).toHaveBeenCalledTimes(exp[m], msg);
Plots[m].calls.reset();
@@ -879,7 +879,7 @@ describe('Test splom interactions:', function() {
spyOn(Lib, 'log');
Plotly.plot(gd, fig).then(function() {
- assetsFnCall('base', {
+ assertFnCall('base', {
cleanPlot: 1, // called once from inside Plots.supplyDefaults
supplyDefaults: 1,
doCalcdata: 1
@@ -892,9 +892,9 @@ describe('Test splom interactions:', function() {
return Plotly.relayout(gd, {width: 4810, height: 3656});
})
.then(function() {
- assetsFnCall('after', {
- cleanPlot: 4, // 3 three from supplyDefaults, once in drawFramework
- supplyDefaults: 3, // 1 from relayout, 1 from automargin, 1 in drawFramework
+ assertFnCall('after', {
+ cleanPlot: 3, // 2 from supplyDefaults, once in drawFramework
+ supplyDefaults: 2, // 1 from relayout, 1 in drawFramework
doCalcdata: 1 // once in drawFramework
});
assertDims('after', 4810, 3656);