Skip to content

Commit e2653aa

Browse files
authored
Merge pull request #7005 from my-tien/shape_shift
property x0shift, x1shift, y0shift, y1shift for adjusting the shape coordinates
2 parents ec283ee + 9ac970a commit e2653aa

15 files changed

+391
-56
lines changed

Diff for: draftlogs/7005_add.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add property x0shift, x1shift, y0shift, y1shift for shapes referencing (multi-)category axes, with thanks to @my-tien for the contribution! [[#7005](https://github.com/plotly/plotly.js/pull/7005)]

Diff for: src/components/shapes/attributes.js

+48-2
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,30 @@ module.exports = templatedArray('shape', {
169169
'See `type` and `xsizemode` for more info.'
170170
].join(' ')
171171
},
172-
172+
x0shift: {
173+
valType: 'number',
174+
dflt: 0,
175+
min: -1,
176+
max: 1,
177+
editType: 'calc',
178+
description: [
179+
'Shifts `x0` away from the center of the category when `xref` is a *category* or',
180+
'*multicategory* axis. -0.5 corresponds to the start of the category and 0.5',
181+
'corresponds to the end of the category.'
182+
].join(' ')
183+
},
184+
x1shift: {
185+
valType: 'number',
186+
dflt: 0,
187+
min: -1,
188+
max: 1,
189+
editType: 'calc',
190+
description: [
191+
'Shifts `x1` away from the center of the category when `xref` is a *category* or',
192+
'*multicategory* axis. -0.5 corresponds to the start of the category and 0.5',
193+
'corresponds to the end of the category.'
194+
].join(' ')
195+
},
173196
yref: extendFlat({}, annAttrs.yref, {
174197
description: [
175198
'Sets the shape\'s y coordinate axis.',
@@ -220,7 +243,30 @@ module.exports = templatedArray('shape', {
220243
'See `type` and `ysizemode` for more info.'
221244
].join(' ')
222245
},
223-
246+
y0shift: {
247+
valType: 'number',
248+
dflt: 0,
249+
min: -1,
250+
max: 1,
251+
editType: 'calc',
252+
description: [
253+
'Shifts `y0` away from the center of the category when `yref` is a *category* or',
254+
'*multicategory* axis. -0.5 corresponds to the start of the category and 0.5',
255+
'corresponds to the end of the category.'
256+
].join(' ')
257+
},
258+
y1shift: {
259+
valType: 'number',
260+
dflt: 0,
261+
min: -1,
262+
max: 1,
263+
editType: 'calc',
264+
description: [
265+
'Shifts `y1` away from the center of the category when `yref` is a *category* or',
266+
'*multicategory* axis. -0.5 corresponds to the start of the category and 0.5',
267+
'corresponds to the end of the category.'
268+
].join(' ')
269+
},
224270
path: {
225271
valType: 'string',
226272
editType: 'calc+arraydraw',

Diff for: src/components/shapes/calc_autorange.js

+27-11
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,18 @@ module.exports = function calcAutorange(gd) {
2323

2424
// paper and axis domain referenced shapes don't affect autorange
2525
if(shape.xref !== 'paper' && xRefType !== 'domain') {
26-
var vx0 = shape.xsizemode === 'pixel' ? shape.xanchor : shape.x0;
27-
var vx1 = shape.xsizemode === 'pixel' ? shape.xanchor : shape.x1;
2826
ax = Axes.getFromId(gd, shape.xref);
2927

30-
bounds = shapeBounds(ax, vx0, vx1, shape.path, constants.paramIsX);
28+
bounds = shapeBounds(ax, shape, constants.paramIsX);
3129
if(bounds) {
3230
shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcXPaddingOptions(shape));
3331
}
3432
}
3533

3634
if(shape.yref !== 'paper' && yRefType !== 'domain') {
37-
var vy0 = shape.ysizemode === 'pixel' ? shape.yanchor : shape.y0;
38-
var vy1 = shape.ysizemode === 'pixel' ? shape.yanchor : shape.y1;
3935
ax = Axes.getFromId(gd, shape.yref);
4036

41-
bounds = shapeBounds(ax, vy0, vy1, shape.path, constants.paramIsY);
37+
bounds = shapeBounds(ax, shape, constants.paramIsY);
4238
if(bounds) {
4339
shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcYPaddingOptions(shape));
4440
}
@@ -77,15 +73,35 @@ function calcPaddingOptions(lineWidth, sizeMode, v0, v1, path, isYAxis) {
7773
}
7874
}
7975

80-
function shapeBounds(ax, v0, v1, path, paramsToUse) {
81-
var convertVal = (ax.type === 'category' || ax.type === 'multicategory') ? ax.r2c : ax.d2c;
76+
function shapeBounds(ax, shape, paramsToUse) {
77+
var dim = ax._id.charAt(0) === 'x' ? 'x' : 'y';
78+
var isCategory = ax.type === 'category' || ax.type === 'multicategory';
79+
var v0;
80+
var v1;
81+
var shiftStart = 0;
82+
var shiftEnd = 0;
83+
84+
var convertVal = isCategory ? ax.r2c : ax.d2c;
85+
86+
var isSizeModeScale = shape[dim + 'sizemode'] === 'scaled';
87+
if(isSizeModeScale) {
88+
v0 = shape[dim + '0'];
89+
v1 = shape[dim + '1'];
90+
if(isCategory) {
91+
shiftStart = shape[dim + '0shift'];
92+
shiftEnd = shape[dim + '1shift'];
93+
}
94+
} else {
95+
v0 = shape[dim + 'anchor'];
96+
v1 = shape[dim + 'anchor'];
97+
}
8298

83-
if(v0 !== undefined) return [convertVal(v0), convertVal(v1)];
84-
if(!path) return;
99+
if(v0 !== undefined) return [convertVal(v0) + shiftStart, convertVal(v1) + shiftEnd];
100+
if(!shape.path) return;
85101

86102
var min = Infinity;
87103
var max = -Infinity;
88-
var segments = path.match(constants.segmentRE);
104+
var segments = shape.path.match(constants.segmentRE);
89105
var i;
90106
var segment;
91107
var drawnParam;

Diff for: src/components/shapes/defaults.js

+4
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) {
8585
ax._shapeIndices.push(shapeOut._index);
8686
r2pos = helpers.rangeToShapePosition(ax);
8787
pos2r = helpers.shapePositionToRange(ax);
88+
if(ax.type === 'category' || ax.type === 'multicategory') {
89+
coerce(axLetter + '0shift');
90+
coerce(axLetter + '1shift');
91+
}
8892
} else {
8993
pos2r = r2pos = Lib.identity;
9094
}

Diff for: src/components/shapes/display_labels.js

+16-6
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,25 @@ module.exports = function drawLabel(gd, index, options, shapeGroup) {
8888
// and convert them to pixel coordinates
8989
// Setup conversion functions
9090
var xa = Axes.getFromId(gd, options.xref);
91+
var xShiftStart = options.x0shift;
92+
var xShiftEnd = options.x1shift;
9193
var xRefType = Axes.getRefType(options.xref);
9294
var ya = Axes.getFromId(gd, options.yref);
95+
var yShiftStart = options.y0shift;
96+
var yShiftEnd = options.y1shift;
9397
var yRefType = Axes.getRefType(options.yref);
94-
var x2p = helpers.getDataToPixel(gd, xa, false, xRefType);
95-
var y2p = helpers.getDataToPixel(gd, ya, true, yRefType);
96-
shapex0 = x2p(options.x0);
97-
shapex1 = x2p(options.x1);
98-
shapey0 = y2p(options.y0);
99-
shapey1 = y2p(options.y1);
98+
var x2p = function(v, shift) {
99+
var dataToPixel = helpers.getDataToPixel(gd, xa, shift, false, xRefType);
100+
return dataToPixel(v);
101+
};
102+
var y2p = function(v, shift) {
103+
var dataToPixel = helpers.getDataToPixel(gd, ya, shift, true, yRefType);
104+
return dataToPixel(v);
105+
};
106+
shapex0 = x2p(options.x0, xShiftStart);
107+
shapex1 = x2p(options.x1, xShiftEnd);
108+
shapey0 = y2p(options.y0, yShiftStart);
109+
shapey1 = y2p(options.y1, yShiftEnd);
100110
}
101111

102112
// Handle `auto` angle

Diff for: src/components/shapes/draw.js

+16-6
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,18 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer, editHe
227227
var xRefType = Axes.getRefType(shapeOptions.xref);
228228
var ya = Axes.getFromId(gd, shapeOptions.yref);
229229
var yRefType = Axes.getRefType(shapeOptions.yref);
230-
var x2p = helpers.getDataToPixel(gd, xa, false, xRefType);
231-
var y2p = helpers.getDataToPixel(gd, ya, true, yRefType);
230+
var shiftXStart = shapeOptions.x0shift;
231+
var shiftXEnd = shapeOptions.x1shift;
232+
var shiftYStart = shapeOptions.y0shift;
233+
var shiftYEnd = shapeOptions.y1shift;
234+
var x2p = function(v, shift) {
235+
var dataToPixel = helpers.getDataToPixel(gd, xa, shift, false, xRefType);
236+
return dataToPixel(v);
237+
};
238+
var y2p = function(v, shift) {
239+
var dataToPixel = helpers.getDataToPixel(gd, ya, shift, true, yRefType);
240+
return dataToPixel(v);
241+
};
232242
var p2x = helpers.getPixelToData(gd, xa, false, xRefType);
233243
var p2y = helpers.getPixelToData(gd, ya, true, yRefType);
234244

@@ -279,8 +289,8 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer, editHe
279289
g.append('circle')
280290
.attr({
281291
'data-line-point': 'start-point',
282-
cx: xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x0 : x2p(shapeOptions.x0),
283-
cy: yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y0 : y2p(shapeOptions.y0),
292+
cx: xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x0 : x2p(shapeOptions.x0, shiftXStart),
293+
cy: yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y0 : y2p(shapeOptions.y0, shiftYStart),
284294
r: circleRadius
285295
})
286296
.style(circleStyle)
@@ -289,8 +299,8 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer, editHe
289299
g.append('circle')
290300
.attr({
291301
'data-line-point': 'end-point',
292-
cx: xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x1 : x2p(shapeOptions.x1),
293-
cy: yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y1 : y2p(shapeOptions.y1),
302+
cx: xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x1 : x2p(shapeOptions.x1, shiftXEnd),
303+
cy: yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y1 : y2p(shapeOptions.y1, shiftYEnd),
294304
r: circleRadius
295305
})
296306
.style(circleStyle)

Diff for: src/components/shapes/draw_newshape/newshapes.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@ function newShapes(outlines, dragOptions) {
8484
case 'line':
8585
case 'rect':
8686
case 'circle':
87-
modifyItem('x0', afterEdit.x0);
88-
modifyItem('x1', afterEdit.x1);
89-
modifyItem('y0', afterEdit.y0);
90-
modifyItem('y1', afterEdit.y1);
87+
modifyItem('x0', afterEdit.x0 - (beforeEdit.x0shift || 0));
88+
modifyItem('x1', afterEdit.x1 - (beforeEdit.x1shift || 0));
89+
modifyItem('y0', afterEdit.y0 - (beforeEdit.y0shift || 0));
90+
modifyItem('y1', afterEdit.y1 - (beforeEdit.y1shift || 0));
9191
break;
9292

9393
case 'path':

Diff for: src/components/shapes/helpers.js

+24-11
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ exports.extractPathCoords = function(path, paramsToUse, isRaw) {
5353
return extractedCoordinates;
5454
};
5555

56-
exports.getDataToPixel = function(gd, axis, isVertical, refType) {
56+
exports.getDataToPixel = function(gd, axis, shift, isVertical, refType) {
5757
var gs = gd._fullLayout._size;
5858
var dataToPixel;
5959

@@ -66,7 +66,8 @@ exports.getDataToPixel = function(gd, axis, isVertical, refType) {
6666
var d2r = exports.shapePositionToRange(axis);
6767

6868
dataToPixel = function(v) {
69-
return axis._offset + axis.r2p(d2r(v, true));
69+
var shiftPixels = getPixelShift(axis, shift);
70+
return axis._offset + axis.r2p(d2r(v, true)) + shiftPixels;
7071
};
7172

7273
if(axis.type === 'date') dataToPixel = exports.decodeDate(dataToPixel);
@@ -179,6 +180,10 @@ exports.getPathString = function(gd, options) {
179180
var ya = Axes.getFromId(gd, options.yref);
180181
var gs = gd._fullLayout._size;
181182
var x2r, x2p, y2r, y2p;
183+
var xShiftStart = getPixelShift(xa, options.x0shift);
184+
var xShiftEnd = getPixelShift(xa, options.x1shift);
185+
var yShiftStart = getPixelShift(ya, options.y0shift);
186+
var yShiftEnd = getPixelShift(ya, options.y1shift);
182187
var x0, x1, y0, y1;
183188

184189
if(xa) {
@@ -208,23 +213,22 @@ exports.getPathString = function(gd, options) {
208213
if(ya && ya.type === 'date') y2p = exports.decodeDate(y2p);
209214
return convertPath(options, x2p, y2p);
210215
}
211-
212216
if(options.xsizemode === 'pixel') {
213217
var xAnchorPos = x2p(options.xanchor);
214-
x0 = xAnchorPos + options.x0;
215-
x1 = xAnchorPos + options.x1;
218+
x0 = xAnchorPos + options.x0 + xShiftStart;
219+
x1 = xAnchorPos + options.x1 + xShiftEnd;
216220
} else {
217-
x0 = x2p(options.x0);
218-
x1 = x2p(options.x1);
221+
x0 = x2p(options.x0) + xShiftStart;
222+
x1 = x2p(options.x1) + xShiftEnd;
219223
}
220224

221225
if(options.ysizemode === 'pixel') {
222226
var yAnchorPos = y2p(options.yanchor);
223-
y0 = yAnchorPos - options.y0;
224-
y1 = yAnchorPos - options.y1;
227+
y0 = yAnchorPos - options.y0 + yShiftStart;
228+
y1 = yAnchorPos - options.y1 + yShiftEnd;
225229
} else {
226-
y0 = y2p(options.y0);
227-
y1 = y2p(options.y1);
230+
y0 = y2p(options.y0) + yShiftStart;
231+
y1 = y2p(options.y1) + yShiftEnd;
228232
}
229233

230234
if(type === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1;
@@ -279,3 +283,12 @@ function convertPath(options, x2p, y2p) {
279283
return segmentType + paramString;
280284
});
281285
}
286+
287+
function getPixelShift(axis, shift) {
288+
shift = shift || 0;
289+
var shiftPixels = 0;
290+
if(shift && axis && (axis.type === 'category' || axis.type === 'multicategory')) {
291+
shiftPixels = (axis.r2p(1) - axis.r2p(0)) * shift;
292+
}
293+
return shiftPixels;
294+
}

Diff for: src/components/shapes/label_texttemplate.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@ function x1Fn(shape) { return shape.x1; }
1616
function y0Fn(shape) { return shape.y0; }
1717
function y1Fn(shape) { return shape.y1; }
1818

19+
function x0shiftFn(shape) { return shape.x0shift || 0; }
20+
function x1shiftFn(shape) { return shape.x1shift || 0; }
21+
function y0shiftFn(shape) { return shape.y0shift || 0; }
22+
function y1shiftFn(shape) { return shape.y1shift || 0; }
23+
1924
function dxFn(shape, xa) {
20-
return d2l(shape.x1, xa) - d2l(shape.x0, xa);
25+
return d2l(shape.x1, xa) + x1shiftFn(shape) - d2l(shape.x0, xa) - x0shiftFn(shape);
2126
}
2227

2328
function dyFn(shape, xa, ya) {
24-
return d2l(shape.y1, ya) - d2l(shape.y0, ya);
29+
return d2l(shape.y1, ya) + y1shiftFn(shape) - d2l(shape.y0, ya) - y0shiftFn(shape);
2530
}
2631

2732
function widthFn(shape, xa) {
@@ -41,11 +46,11 @@ function lengthFn(shape, xa, ya) {
4146
}
4247

4348
function xcenterFn(shape, xa) {
44-
return l2d((d2l(shape.x1, xa) + d2l(shape.x0, xa)) / 2, xa);
49+
return l2d((d2l(shape.x1, xa) + x1shiftFn(shape) + d2l(shape.x0, xa) + x0shiftFn(shape)) / 2, xa);
4550
}
4651

4752
function ycenterFn(shape, xa, ya) {
48-
return l2d((d2l(shape.y1, ya) + d2l(shape.y0, ya)) / 2, ya);
53+
return l2d((d2l(shape.y1, ya) + y1shiftFn(shape) + d2l(shape.y0, ya) + y0shiftFn(shape)) / 2, ya);
4954
}
5055

5156
function slopeFn(shape, xa, ya) {

Diff for: test/image/baselines/zzz_shape_shift_horizontal.png

20.3 KB
Loading

Diff for: test/image/baselines/zzz_shape_shift_vertical.png

24.3 KB
Loading

0 commit comments

Comments
 (0)