Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/components/shapes/attributes.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use strict';

var annAttrs = require('../annotations/attributes');
var cartesianConstants = require('../../plots/cartesian/constants');
var fontAttrs = require('../../plots/font_attributes');
var scatterLineAttrs = require('../../traces/scatter/attributes').line;
var dash = require('../drawing/attributes').dash;
var extendFlat = require('../../lib/extend').extendFlat;
var templatedArray = require('../../plot_api/plot_template').templatedArray;
var axisPlaceableObjs = require('../../constants/axis_placeable_objects');
var basePlotAttributes = require('../../plots/attributes');
var annAttrs = require('../annotations/attributes');
const { shapeTexttemplateAttrs, templatefallbackAttrs } = require('../../plots/template_attributes');
var shapeLabelTexttemplateVars = require('./label_texttemplate');

Expand Down Expand Up @@ -115,9 +116,12 @@ module.exports = templatedArray('shape', {
},

xref: extendFlat({}, annAttrs.xref, {
arrayOk: true,
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).',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be explicit how this applies to each shape type - and in particular that it applies to paths

Suggested change
'(e.g., [\'x\', \'x2\'] for a rectangle means `x0` uses the `x` axis and `x1` uses the `x2` axis).',
'e.g., [\'x\', \'x2\'] for a rectangle, line, or circle means `x0` uses the `x` axis and `x1` uses the `x2` axis.',
'Path shapes using an array should have one entry for each x coordinate in the string.',

].join(' ')
}),
xsizemode: {
Expand Down Expand Up @@ -183,9 +187,12 @@ module.exports = templatedArray('shape', {
].join(' ')
},
yref: extendFlat({}, annAttrs.yref, {
arrayOk: true,
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: {
Expand Down
56 changes: 51 additions & 5 deletions src/components/shapes/calc_autorange.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,21 @@ module.exports = function calcAutorange(gd) {
var xRefType = Axes.getRefType(shape.xref);
var yRefType = Axes.getRefType(shape.yref);

// paper and axis domain referenced shapes don't affect autorange
if(shape.xref !== 'paper' && xRefType !== 'domain') {
if(xRefType === 'array') {
calcArrayRefAutorange(gd, shape, 'x');
} else if(shape.xref !== 'paper' && xRefType !== 'domain') {
// paper and axis domain referenced shapes don't affect autorange
ax = Axes.getFromId(gd, shape.xref);

bounds = shapeBounds(ax, shape, constants.paramIsX);
if(bounds) {
shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcXPaddingOptions(shape));
}
}

if(shape.yref !== 'paper' && yRefType !== 'domain') {
if(yRefType === 'array') {
calcArrayRefAutorange(gd, shape, 'y');
} else if(shape.yref !== 'paper' && yRefType !== 'domain') {
ax = Axes.getFromId(gd, shape.yref);

bounds = shapeBounds(ax, shape, constants.paramIsY);
if(bounds) {
shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcYPaddingOptions(shape));
Expand All @@ -42,6 +44,50 @@ module.exports = function calcAutorange(gd) {
}
};

function calcArrayRefAutorange(gd, shape, dim) {
var refs = shape[dim + 'ref'];
var paramsToUse = dim === 'x' ? constants.paramIsX : constants.paramIsY;
var paddingOpts = dim === 'x' ? calcXPaddingOptions(shape) : calcYPaddingOptions(shape);

function addToAxisGroup(ref, val) {
if(ref === 'paper' || Axes.getRefType(ref) === 'domain') return;
if(!axisGroups[ref]) axisGroups[ref] = [];
axisGroups[ref].push(val);
}

// group coordinates by axis reference so we can calculate the extremes for each axis
var axisGroups = {};
if(shape.type === 'path' && shape.path) {
var segments = shape.path.match(constants.segmentRE) || [];
var refIndex = 0;
for(var i = 0; i < segments.length; i++) {
var segment = segments[i];
var command = segment.charAt(0);
var drawnIndex = paramsToUse[command].drawn;

if(drawnIndex === undefined) continue;

var params = segment.slice(1).match(constants.paramRE);
if(params && params.length > drawnIndex) {
addToAxisGroup(refs[refIndex], params[drawnIndex]);
refIndex++;
}
}
} else {
addToAxisGroup(refs[0], shape[dim + '0']);
addToAxisGroup(refs[1], shape[dim + '1']);
}

// For each axis, convert coordinates to data values then calculate extremes
for(var axId in axisGroups) {
var ax = Axes.getFromId(gd, axId);
if(!ax) continue;
var convertVal = (ax.type === 'category' || ax.type === 'multicategory') ? ax.r2c : ax.d2c;
if(ax.type === 'date') convertVal = helpers.decodeDate(convertVal);
shape._extremes[ax._id] = Axes.findExtremes(ax, axisGroups[axId].map(convertVal), paddingOpts);
}
}

function calcXPaddingOptions(shape) {
return calcPaddingOptions(shape.line.width, shape.xsizemode, shape.x0, shape.x1, shape.path, false);
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/shapes/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: 3},
// A: {1: true, 6: true},
Z: {}
},
Expand Down
173 changes: 119 additions & 54 deletions src/components/shapes/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,77 +68,142 @@ 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 };
var ax;
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this want to be the count just for axLetter?

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks to me like this whole loop should be skipped for paths - they don't want x0shift etc, and sizemode='pixel' doesn't make sense with an axis ref array. In fact seems like we should enforce that earlier on, and document it in the sizemode descriptions: if you have an axis reference array, sizemode can only be 'scaled'.

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');
}
Comment on lines +154 to +157
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likewise this shouldn't be done for paths

} 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']);
Expand Down
Loading