diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js index 71a5475aee0..26a4e454b1b 100644 --- a/src/components/shapes/attributes.js +++ b/src/components/shapes/attributes.js @@ -119,12 +119,16 @@ module.exports = templatedArray('shape', { ].join(' ') }, - xref: extendFlat({}, annAttrs.xref, { + xref: { + valType: 'any', + editType: 'calc', description: [ 'Sets the shape\'s x coordinate axis.', - axisPlaceableObjs.axisRefDescription('x', 'left', 'right') + axisPlaceableObjs.axisRefDescription('x', 'left', 'right'), + 'If an array of axis IDs is provided, each `x` value will refer to the corresponding axis', + '(e.g., [\'x\', \'x2\'] for a rectangle means `x0` uses the `x` axis and `x1` uses the `x2` axis).', ].join(' ') - }), + }, xsizemode: { valType: 'enumerated', values: ['scaled', 'pixel'], @@ -193,12 +197,16 @@ module.exports = templatedArray('shape', { 'corresponds to the end of the category.' ].join(' ') }, - yref: extendFlat({}, annAttrs.yref, { + yref: { + valType: 'any', + editType: 'calc', description: [ 'Sets the shape\'s y coordinate axis.', - axisPlaceableObjs.axisRefDescription('y', 'bottom', 'top') + axisPlaceableObjs.axisRefDescription('y', 'bottom', 'top'), + 'If an array of axis IDs is provided, each `y` value will refer to the corresponding axis', + '(e.g., [\'y\', \'y2\'] for a rectangle means `y0` uses the `y` axis and `y1` uses the `y2` axis).', ].join(' ') - }), + }, ysizemode: { valType: 'enumerated', values: ['scaled', 'pixel'], diff --git a/src/components/shapes/calc_autorange.js b/src/components/shapes/calc_autorange.js index 17c6ce23a2f..572ba79df3c 100644 --- a/src/components/shapes/calc_autorange.js +++ b/src/components/shapes/calc_autorange.js @@ -22,7 +22,8 @@ module.exports = function calcAutorange(gd) { var yRefType = Axes.getRefType(shape.yref); // paper and axis domain referenced shapes don't affect autorange - if(shape.xref !== 'paper' && xRefType !== 'domain') { + // TODO: implement autorange calculation for array ref shapes + if(xRefType !== 'array' && shape.xref !== 'paper' && xRefType !== 'domain') { ax = Axes.getFromId(gd, shape.xref); bounds = shapeBounds(ax, shape, constants.paramIsX); @@ -31,7 +32,8 @@ module.exports = function calcAutorange(gd) { } } - if(shape.yref !== 'paper' && yRefType !== 'domain') { + // TODO: implement autorange calculation for array ref shapes + if(yRefType !== 'array' && shape.yref !== 'paper' && yRefType !== 'domain') { ax = Axes.getFromId(gd, shape.yref); bounds = shapeBounds(ax, shape, constants.paramIsY); diff --git a/src/components/shapes/constants.js b/src/components/shapes/constants.js index 5a9deb44470..d0df42c18af 100644 --- a/src/components/shapes/constants.js +++ b/src/components/shapes/constants.js @@ -33,7 +33,7 @@ module.exports = { Q: {1: true, 3: true, drawn: 3}, C: {1: true, 3: true, 5: true, drawn: 5}, T: {1: true, drawn: 1}, - S: {1: true, 3: true, drawn: 5}, + S: {1: true, 3: true, drawn: 4}, // A: {1: true, 6: true}, Z: {} }, diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js index e725b336678..8246345463f 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -65,9 +65,7 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) { var ySizeMode = coerce('ysizemode'); // positioning - var axLetters = ['x', 'y']; - for(var i = 0; i < 2; i++) { - var axLetter = axLetters[i]; + ['x', 'y'].forEach(axLetter => { var attrAnchor = axLetter + 'anchor'; var sizeMode = axLetter === 'x' ? xSizeMode : ySizeMode; var gdMock = {_fullLayout: fullLayout}; @@ -75,68 +73,134 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) { var pos2r; var r2pos; - // xref, yref - var axRef = Axes.coerceRef(shapeIn, shapeOut, gdMock, axLetter, undefined, - 'paper'); - var axRefType = Axes.getRefType(axRef); - - if(axRefType === 'range') { - ax = Axes.getFromId(gdMock, axRef); - ax._shapeIndices.push(shapeOut._index); - r2pos = helpers.rangeToShapePosition(ax); - pos2r = helpers.shapePositionToRange(ax); - if(ax.type === 'category' || ax.type === 'multicategory') { - coerce(axLetter + '0shift'); - coerce(axLetter + '1shift'); - } + // xref, yref - handle both string and array values + var axRef; + var refAttr = axLetter + 'ref'; + var inputRef = shapeIn[refAttr]; + + if(Array.isArray(inputRef) && inputRef.length > 0) { + // Array case: use coerceRefArray for validation + var expectedLen = helpers.countDefiningCoords(shapeType, path); + axRef = Axes.coerceRefArray(shapeIn, shapeOut, gdMock, axLetter, undefined, 'paper', expectedLen); + shapeOut['_' + axLetter + 'refArray'] = true; + + // Need to register the shape with all referenced axes for redrawing purposes + axRef.forEach(function(ref) { + if(Axes.getRefType(ref) === 'range') { + ax = Axes.getFromId(gdMock, ref); + if(ax && ax._shapeIndices.indexOf(shapeOut._index) === -1) { + ax._shapeIndices.push(shapeOut._index); + } + } + }); } else { - pos2r = r2pos = Lib.identity; + // String/undefined case: use coerceRef + axRef = Axes.coerceRef(shapeIn, shapeOut, gdMock, axLetter, undefined, 'paper'); } - // Coerce x0, x1, y0, y1 - if(noPath) { - var dflt0 = 0.25; - var dflt1 = 0.75; - - // hack until V3.0 when log has regular range behavior - make it look like other - // ranges to send to coerce, then put it back after - // this is all to give reasonable default position behavior on log axes, which is - // a pretty unimportant edge case so we could just ignore this. - var attr0 = axLetter + '0'; - var attr1 = axLetter + '1'; - var in0 = shapeIn[attr0]; - var in1 = shapeIn[attr1]; - shapeIn[attr0] = pos2r(shapeIn[attr0], true); - shapeIn[attr1] = pos2r(shapeIn[attr1], true); - - if(sizeMode === 'pixel') { - coerce(attr0, 0); - coerce(attr1, 10); + if(Array.isArray(axRef)) { + var dflts = [0.25, 0.75]; + var pixelDflts = [0, 10]; + + // For each coordinate, coerce the position with their respective axis ref + [0, 1].forEach(function(i) { + var ref = axRef[i]; + var refType = Axes.getRefType(ref); + if(refType === 'range') { + ax = Axes.getFromId(gdMock, ref); + pos2r = helpers.shapePositionToRange(ax); + r2pos = helpers.rangeToShapePosition(ax); + if(ax.type === 'category' || ax.type === 'multicategory') { + coerce(axLetter + i + 'shift'); + } + } else { + pos2r = r2pos = Lib.identity; + } + + if(noPath) { + var attr = axLetter + i; + var inValue = shapeIn[attr]; + shapeIn[attr] = pos2r(shapeIn[attr], true); + + if(sizeMode === 'pixel') { + coerce(attr, pixelDflts[i]); + } else { + Axes.coercePosition(shapeOut, gdMock, coerce, ref, attr, dflts[i]); + } + + shapeOut[attr] = r2pos(shapeOut[attr]); + shapeIn[attr] = inValue; + } + + if(i === 0 && sizeMode === 'pixel') { + var inAnchor = shapeIn[attrAnchor]; + shapeIn[attrAnchor] = pos2r(shapeIn[attrAnchor], true); + Axes.coercePosition(shapeOut, gdMock, coerce, ref, attrAnchor, 0.25); + shapeOut[attrAnchor] = r2pos(shapeOut[attrAnchor]); + shapeIn[attrAnchor] = inAnchor; + } + }); + } else { + var axRefType = Axes.getRefType(axRef); + + if(axRefType === 'range') { + ax = Axes.getFromId(gdMock, axRef); + ax._shapeIndices.push(shapeOut._index); + r2pos = helpers.rangeToShapePosition(ax); + pos2r = helpers.shapePositionToRange(ax); + if(ax.type === 'category' || ax.type === 'multicategory') { + coerce(axLetter + '0shift'); + coerce(axLetter + '1shift'); + } } else { - Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr0, dflt0); - Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr1, dflt1); + pos2r = r2pos = Lib.identity; } - // hack part 2 - shapeOut[attr0] = r2pos(shapeOut[attr0]); - shapeOut[attr1] = r2pos(shapeOut[attr1]); - shapeIn[attr0] = in0; - shapeIn[attr1] = in1; - } + // Coerce x0, x1, y0, y1 + if(noPath) { + var dflt0 = 0.25; + var dflt1 = 0.75; + + // hack until V3.0 when log has regular range behavior - make it look like other + // ranges to send to coerce, then put it back after + // this is all to give reasonable default position behavior on log axes, which is + // a pretty unimportant edge case so we could just ignore this. + var attr0 = axLetter + '0'; + var attr1 = axLetter + '1'; + var in0 = shapeIn[attr0]; + var in1 = shapeIn[attr1]; + shapeIn[attr0] = pos2r(shapeIn[attr0], true); + shapeIn[attr1] = pos2r(shapeIn[attr1], true); + + if(sizeMode === 'pixel') { + coerce(attr0, 0); + coerce(attr1, 10); + } else { + Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr0, dflt0); + Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr1, dflt1); + } + + // hack part 2 + shapeOut[attr0] = r2pos(shapeOut[attr0]); + shapeOut[attr1] = r2pos(shapeOut[attr1]); + shapeIn[attr0] = in0; + shapeIn[attr1] = in1; + } - // Coerce xanchor and yanchor - if(sizeMode === 'pixel') { - // Hack for log axis described above - var inAnchor = shapeIn[attrAnchor]; - shapeIn[attrAnchor] = pos2r(shapeIn[attrAnchor], true); + // Coerce xanchor and yanchor + if(sizeMode === 'pixel') { + // Hack for log axis described above + var inAnchor = shapeIn[attrAnchor]; + shapeIn[attrAnchor] = pos2r(shapeIn[attrAnchor], true); - Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attrAnchor, 0.25); + Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attrAnchor, 0.25); - // Hack part 2 - shapeOut[attrAnchor] = r2pos(shapeOut[attrAnchor]); - shapeIn[attrAnchor] = inAnchor; + // Hack part 2 + shapeOut[attrAnchor] = r2pos(shapeOut[attrAnchor]); + shapeIn[attrAnchor] = inAnchor; + } } - } + }); if(noPath) { Lib.noneOrAll(shapeIn, shapeOut, ['x0', 'x1', 'y0', 'y1']); diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js index ad22a48df8c..b6f669ac747 100644 --- a/src/components/shapes/helpers.js +++ b/src/components/shapes/helpers.js @@ -53,6 +53,23 @@ exports.extractPathCoords = function(path, paramsToUse, isRaw) { return extractedCoordinates; }; +exports.countDefiningCoords = function(shapeType, path) { + // non-path shapes always have 2 defining coordinates + if(shapeType !== 'path') return 2; + if(!path) return 0; + + var segments = path.match(constants.segmentRE); + if(!segments) return 0; + + return segments.reduce((coordCount, segment) => { + // for each path command, check if there is a drawn coordinate + var segmentType = segment.charAt(0); + var hasDrawnX = constants.paramIsX[segmentType].drawn !== undefined; + var hasDrawnY = constants.paramIsY[segmentType].drawn !== undefined; + return coordCount + (hasDrawnX || hasDrawnY ? 1 : 0); + }, 0); +}; + exports.getDataToPixel = function(gd, axis, shift, isVertical, refType) { var gs = gd._fullLayout._size; var dataToPixel; @@ -173,66 +190,104 @@ exports.makeSelectionsOptionsAndPlotinfo = function(gd, index) { exports.getPathString = function(gd, options) { - var type = options.type; + var shapeType = options.type; var xRefType = Axes.getRefType(options.xref); var yRefType = Axes.getRefType(options.yref); - var xa = Axes.getFromId(gd, options.xref); - var ya = Axes.getFromId(gd, options.yref); var gs = gd._fullLayout._size; - var x2r, x2p, y2r, y2p; - var xShiftStart = getPixelShift(xa, options.x0shift); - var xShiftEnd = getPixelShift(xa, options.x1shift); - var yShiftStart = getPixelShift(ya, options.y0shift); - var yShiftEnd = getPixelShift(ya, options.y1shift); + var xa, ya; + var xShiftStart, xShiftEnd, yShiftStart, yShiftEnd; + var x2p, y2p; var x0, x1, y0, y1; - if(xa) { - if(xRefType === 'domain') { - x2p = function(v) { return xa._offset + xa._length * v; }; + function getConverter(axis, refType, shapeType, isVertical) { + var converter; + if(axis) { + if(refType === 'domain') { + if(isVertical) { + converter = function(v) { return axis._offset + axis._length * (1 - v); }; + } else { + converter = function(v) { return axis._offset + axis._length * v; }; + } + } else { + var d2r = exports.shapePositionToRange(axis); + converter = function(v) { return axis._offset + axis.r2p(d2r(v, true)); }; + + if(shapeType === 'path' && axis.type === 'date') converter = exports.decodeDate(converter); + } } else { - x2r = exports.shapePositionToRange(xa); - x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); }; + if(isVertical) { + converter = function(v) { return gs.t + gs.h * (1 - v); }; + } else { + converter = function(v) { return gs.l + gs.w * v; }; + } } - } else { - x2p = function(v) { return gs.l + gs.w * v; }; - } - if(ya) { - if(yRefType === 'domain') { - y2p = function(v) { return ya._offset + ya._length * (1 - v); }; - } else { - y2r = exports.shapePositionToRange(ya); - y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); }; - } - } else { - y2p = function(v) { return gs.t + gs.h * (1 - v); }; + return converter; } - if(type === 'path') { - if(xa && xa.type === 'date') x2p = exports.decodeDate(x2p); - if(ya && ya.type === 'date') y2p = exports.decodeDate(y2p); - return convertPath(options, x2p, y2p); + // Build function(s) to convert data to pixel + if(xRefType === 'array') { + x2p = []; + xa = options.xref.map(function(ref) { return Axes.getFromId(gd, ref); }); + x2p = options.xref.map(function(ref, i) { + return getConverter(xa[i], Axes.getRefType(ref), shapeType, false); + }); + } else { + xa = Axes.getFromId(gd, options.xref); + x2p = getConverter(xa, xRefType, shapeType, false); } - if(options.xsizemode === 'pixel') { - var xAnchorPos = x2p(options.xanchor); - x0 = xAnchorPos + options.x0 + xShiftStart; - x1 = xAnchorPos + options.x1 + xShiftEnd; + if(yRefType === 'array') { + y2p = []; + ya = options.yref.map(function(ref) { return Axes.getFromId(gd, ref); }); + y2p = options.yref.map(function(ref, i) { + return getConverter(ya[i], Axes.getRefType(ref), shapeType, true); + }); } else { - x0 = x2p(options.x0) + xShiftStart; - x1 = x2p(options.x1) + xShiftEnd; + ya = Axes.getFromId(gd, options.yref); + y2p = getConverter(ya, yRefType, shapeType, true); } - if(options.ysizemode === 'pixel') { - var yAnchorPos = y2p(options.yanchor); - y0 = yAnchorPos - options.y0 + yShiftStart; - y1 = yAnchorPos - options.y1 + yShiftEnd; + if(shapeType === 'path') { return convertPath(options, x2p, y2p); } + + // Calculate pixel coordinates for non-path shapes + // Pixel sizemode for array refs is not supported for now + if(xRefType === 'array') { + xShiftStart = getPixelShift(xa[0], options.x0shift); + xShiftEnd = getPixelShift(xa[1], options.x1shift); + x0 = x2p[0](options.x0) + xShiftStart; + x1 = x2p[1](options.x1) + xShiftEnd; } else { - y0 = y2p(options.y0) + yShiftStart; - y1 = y2p(options.y1) + yShiftEnd; + xShiftStart = getPixelShift(xa, options.x0shift); + xShiftEnd = getPixelShift(xa, options.x1shift); + if(options.xsizemode === 'pixel') { + var xAnchorPos = x2p(options.xanchor); + x0 = xAnchorPos + options.x0 + xShiftStart; + x1 = xAnchorPos + options.x1 + xShiftEnd; + } else { + x0 = x2p(options.x0) + xShiftStart; + x1 = x2p(options.x1) + xShiftEnd; + } + } + if(yRefType === 'array') { + yShiftStart = getPixelShift(ya[0], options.y0shift); + yShiftEnd = getPixelShift(ya[1], options.y1shift); + y0 = y2p[0](options.y0) + yShiftStart; + y1 = y2p[1](options.y1) + yShiftEnd; + } else { + yShiftStart = getPixelShift(ya, options.y0shift); + yShiftEnd = getPixelShift(ya, options.y1shift); + if(options.ysizemode === 'pixel') { + var yAnchorPos = y2p(options.yanchor); + y0 = yAnchorPos - options.y0 + yShiftStart; + y1 = yAnchorPos - options.y1 + yShiftEnd; + } else { + y0 = y2p(options.y0) + yShiftStart; + y1 = y2p(options.y1) + yShiftEnd; + } } - if(type === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1; - if(type === 'rect') return 'M' + x0 + ',' + y0 + 'H' + x1 + 'V' + y1 + 'H' + x0 + 'Z'; + if(shapeType === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1; + if(shapeType === 'rect') return 'M' + x0 + ',' + y0 + 'H' + x1 + 'V' + y1 + 'H' + x0 + 'Z'; // circle var cx = (x0 + x1) / 2; @@ -246,13 +301,16 @@ exports.getPathString = function(gd, options) { rArc + ' 0 0,1 ' + rightPt + 'Z'; }; - function convertPath(options, x2p, y2p) { var pathIn = options.path; var xSizemode = options.xsizemode; var ySizemode = options.ysizemode; var xAnchor = options.xanchor; var yAnchor = options.yanchor; + var isArrayXref = Array.isArray(options.xref); + var isArrayYref = Array.isArray(options.yref); + var xVertexIndex = 0; + var yVertexIndex = 0; return pathIn.replace(constants.segmentRE, function(segment) { var paramNumber = 0; @@ -260,14 +318,20 @@ function convertPath(options, x2p, y2p) { var xParams = constants.paramIsX[segmentType]; var yParams = constants.paramIsY[segmentType]; var nParams = constants.numParams[segmentType]; + var hasDrawnX = xParams.drawn !== undefined; + var hasDrawnY = yParams.drawn !== undefined; + + // Use vertex indices for array refs (same converter for all params in segment) + var segmentX2p = (isArrayXref && xSizemode !== 'pixel') ? x2p[xVertexIndex] : x2p; + var segmentY2p = (isArrayYref && ySizemode !== 'pixel') ? y2p[yVertexIndex] : y2p; var paramString = segment.substr(1).replace(constants.paramRE, function(param) { if(xParams[paramNumber]) { - if(xSizemode === 'pixel') param = x2p(xAnchor) + Number(param); - else param = x2p(param); + if(xSizemode === 'pixel') param = segmentX2p(xAnchor) + Number(param); + else param = segmentX2p(param); } else if(yParams[paramNumber]) { - if(ySizemode === 'pixel') param = y2p(yAnchor) - Number(param); - else param = y2p(param); + if(ySizemode === 'pixel') param = segmentY2p(yAnchor) - Number(param); + else param = segmentY2p(param); } paramNumber++; @@ -280,6 +344,9 @@ function convertPath(options, x2p, y2p) { Lib.log('Ignoring extra params in segment ' + segment); } + if(hasDrawnX) xVertexIndex++; + if(hasDrawnY) yVertexIndex++; + return segmentType + paramString; }); } diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 15840ad11a8..0d1777a8d32 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -101,8 +101,8 @@ exports.cleanLayout = function (layout) { if (!Lib.isPlainObject(shape)) continue; - cleanAxRef(shape, 'xref'); - cleanAxRef(shape, 'yref'); + cleanAxRef(shape, 'xref', true); + cleanAxRef(shape, 'yref', true); } var imagesLen = Array.isArray(layout.images) ? layout.images.length : 0; @@ -152,9 +152,13 @@ exports.cleanLayout = function (layout) { return layout; }; -function cleanAxRef(container, attr) { +function cleanAxRef(container, attr, isShape = false) { var valIn = container[attr]; var axLetter = attr.charAt(0); + + // Skip for shapes with array references + if (isShape && Array.isArray(valIn)) return; + if (valIn && valIn !== 'paper') { container[attr] = cleanId(valIn, axLetter, true); } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index bb0cead5689..1fed0f9f331 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -14,6 +14,7 @@ var Drawing = require('../../components/drawing'); var axAttrs = require('./layout_attributes'); var cleanTicks = require('./clean_ticks'); +var cartesianConstants = require('./constants'); var constants = require('../../constants/numerical'); var ONEMAXYEAR = constants.ONEMAXYEAR; @@ -97,8 +98,7 @@ function expandRange(range) { * but can be prefixed, like 'ax' for annotation's arrow x * dflt: the default to coerce to, or blank to use the first axis (falling back on * extraOption if there is no axis) - * extraOption: aside from existing axes with this letter, what non-axis value is allowed? - * Only required if it's different from `dflt` + * extraOption: fallback value, only required if it's different from `dflt` */ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption) { var axLetter = attr.charAt(attr.length - 1); @@ -124,6 +124,49 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption return Lib.coerce(containerIn, containerOut, attrDef, refAttr); }; +/* + * Coerce an array of axis references. Used by shapes for per-coordinate axis references. + * + * attr: the attribute we're generating a reference for. Should end in 'x' or 'y' + * but can be prefixed, like 'ax' for annotation's arrow x + * dflt: the default to coerce to, or blank to use the first axis (falling back on + * extraOption if there is no axis) + * extraOption: aside from existing axes with this letter, what non-axis value is allowed? + * Only required if it's different from `dflt` + */ +axes.coerceRefArray = function(containerIn, containerOut, gd, attr, dflt, extraOption, expectedLen) { + var axLetter = attr.charAt(attr.length - 1); + var axlist = gd._fullLayout._subplots[axLetter + 'axis']; + var refAttr = attr + 'ref'; + var axRef = containerIn[refAttr]; + + // Build the axis list, which we use to validate the axis references + if(!dflt) dflt = axlist[0] || (typeof extraOption === 'string' ? extraOption : extraOption[0]); + axlist = axlist.concat(axlist.map(x => x + ' domain')); + axlist = axlist.concat(extraOption ? extraOption : []); + + // Handle array length mismatch + if(axRef.length > expectedLen) { + // if the array is longer than the expected length, truncate it + Lib.warn('Array attribute ' + refAttr + ' has more entries than expected, truncating to ' + expectedLen); + axRef = axRef.slice(0, expectedLen); + } else if(axRef.length < expectedLen) { + // if the array is shorter than the expected length, extend using the default value + Lib.warn('Array attribute ' + refAttr + ' has fewer entries than expected, extending with default value'); + axRef = axRef.concat(Array(expectedLen - axRef.length).fill(dflt)); + } + + // Check all references, replace with default if invalid + for(var i = 0; i < axRef.length; i++) { + if(!axlist.includes(axRef[i])) { + axRef[i] = dflt; + } + } + + containerOut[refAttr] = axRef; + return axRef; +}; + /* * Get the type of an axis reference. This can be 'range', 'domain', or 'paper'. * This assumes ar is a valid axis reference and returns 'range' if it doesn't @@ -134,6 +177,7 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption */ axes.getRefType = function(ar) { if(ar === undefined) { return ar; } + if(Array.isArray(ar)) { return 'array'; } if(ar === 'paper') { return 'paper'; } if(ar === 'pixel') { return 'pixel'; } if(/( domain)$/.test(ar)) { return 'domain'; } else { return 'range'; }