Skip to content

Commit aa0cd7a

Browse files
committed
property shape.x_shift/y_shift for adjusting the shape coordinates
Only for shapes with reference to (multi-)category axis
1 parent 5d6d457 commit aa0cd7a

File tree

8 files changed

+223
-18
lines changed

8 files changed

+223
-18
lines changed

src/components/shapes/attributes.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,28 @@ module.exports = templatedArray('shape', {
220220
'See `type` and `ysizemode` for more info.'
221221
].join(' ')
222222
},
223-
223+
x_shift: {
224+
valType: 'number',
225+
dflt: 0,
226+
min: -0.5,
227+
max: 0.5,
228+
editType: 'calc',
229+
description: [
230+
'Only relevant if xref is a (multi-)category axes. Shifts x0 and x1 by a fraction of',
231+
'the reference unit.'
232+
].join(' ')
233+
},
234+
y_shift: {
235+
valType: 'number',
236+
dflt: 0,
237+
min: -0.5,
238+
max: 0.5,
239+
editType: 'calc',
240+
description: [
241+
'Only relevant if yref is a (multi-)category axes. Shifts y0 and y1 by a fraction of',
242+
'the reference unit.'
243+
].join(' ')
244+
},
224245
path: {
225246
valType: 'string',
226247
editType: 'calc+arraydraw',

src/components/shapes/calc_autorange.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ module.exports = function calcAutorange(gd) {
2727
var vx1 = shape.xsizemode === 'pixel' ? shape.xanchor : shape.x1;
2828
ax = Axes.getFromId(gd, shape.xref);
2929

30-
bounds = shapeBounds(ax, vx0, vx1, shape.path, constants.paramIsX);
30+
bounds = shapeBounds(ax, vx0, vx1, shape.path, shape.x_shift, constants.paramIsX);
3131
if(bounds) {
3232
shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcXPaddingOptions(shape));
3333
}
@@ -38,7 +38,7 @@ module.exports = function calcAutorange(gd) {
3838
var vy1 = shape.ysizemode === 'pixel' ? shape.yanchor : shape.y1;
3939
ax = Axes.getFromId(gd, shape.yref);
4040

41-
bounds = shapeBounds(ax, vy0, vy1, shape.path, constants.paramIsY);
41+
bounds = shapeBounds(ax, vy0, vy1, shape.path, shape.y_shift, constants.paramIsY);
4242
if(bounds) {
4343
shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcYPaddingOptions(shape));
4444
}
@@ -77,8 +77,13 @@ function calcPaddingOptions(lineWidth, sizeMode, v0, v1, path, isYAxis) {
7777
}
7878
}
7979

80-
function shapeBounds(ax, v0, v1, path, paramsToUse) {
81-
var convertVal = (ax.type === 'category' || ax.type === 'multicategory') ? ax.r2c : ax.d2c;
80+
function shapeBounds(ax, v0, v1, path, shift, paramsToUse) {
81+
var convertVal;
82+
if(ax.type === 'category' || ax.type === 'multicategory') {
83+
convertVal = function(v) { return ax.r2c(v) + shift; };
84+
} else {
85+
convertVal = ax.d2c;
86+
}
8287

8388
if(v0 !== undefined) return [convertVal(v0), convertVal(v1)];
8489
if(!path) return;

src/components/shapes/defaults.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) {
7575
var pos2r;
7676
var r2pos;
7777

78+
coerce('x_shift');
79+
coerce('y_shift');
80+
7881
// xref, yref
7982
var axRef = Axes.coerceRef(shapeIn, shapeOut, gdMock, axLetter, undefined,
8083
'paper');

src/components/shapes/display_labels.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,12 @@ module.exports = function drawLabel(gd, index, options, shapeGroup) {
8989
// Setup conversion functions
9090
var xa = Axes.getFromId(gd, options.xref);
9191
var xRefType = Axes.getRefType(options.xref);
92+
var xShift = options.x_shift;
9293
var ya = Axes.getFromId(gd, options.yref);
9394
var yRefType = Axes.getRefType(options.yref);
94-
var x2p = helpers.getDataToPixel(gd, xa, false, xRefType);
95-
var y2p = helpers.getDataToPixel(gd, ya, true, yRefType);
95+
var yShift = options.y_shift;
96+
var x2p = helpers.getDataToPixel(gd, xa, false, xRefType, xShift);
97+
var y2p = helpers.getDataToPixel(gd, ya, true, yRefType, yShift);
9698
shapex0 = x2p(options.x0);
9799
shapex1 = x2p(options.x1);
98100
shapey0 = y2p(options.y0);

src/components/shapes/helpers.js

Lines changed: 26 additions & 11 deletions
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, isVertical, refType, shift) {
5757
var gs = gd._fullLayout._size;
5858
var dataToPixel;
5959

@@ -66,7 +66,15 @@ 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 = 0;
70+
if(axis.type === 'category' || axis.type === 'multicategory') {
71+
if(isVertical) {
72+
shiftPixels = -1 * ((gs.h - axis.r2p(d2r(0.5, true))) * shift);
73+
} else {
74+
shiftPixels = axis.r2p(d2r(0.5, true)) * shift;
75+
}
76+
}
77+
return axis._offset + axis.r2p(d2r(v, true)) + shiftPixels;
7078
};
7179

7280
if(axis.type === 'date') dataToPixel = exports.decodeDate(dataToPixel);
@@ -179,6 +187,8 @@ exports.getPathString = function(gd, options) {
179187
var ya = Axes.getFromId(gd, options.yref);
180188
var gs = gd._fullLayout._size;
181189
var x2r, x2p, y2r, y2p;
190+
var shiftUnitX = 0;
191+
var shiftUnitY = 0;
182192
var x0, x1, y0, y1;
183193

184194
if(xa) {
@@ -187,6 +197,9 @@ exports.getPathString = function(gd, options) {
187197
} else {
188198
x2r = exports.shapePositionToRange(xa);
189199
x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); };
200+
if(xa.type === 'category' || xa.type === 'multicategory') {
201+
shiftUnitX = xa.r2p(x2r(0.5, true));
202+
}
190203
}
191204
} else {
192205
x2p = function(v) { return gs.l + gs.w * v; };
@@ -198,6 +211,9 @@ exports.getPathString = function(gd, options) {
198211
} else {
199212
y2r = exports.shapePositionToRange(ya);
200213
y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); };
214+
if(ya.type === 'category' || ya.type === 'multicategory') {
215+
shiftUnitY = gs.h - ya.r2p(y2r(0.5, true));
216+
}
201217
}
202218
} else {
203219
y2p = function(v) { return gs.t + gs.h * (1 - v); };
@@ -208,23 +224,22 @@ exports.getPathString = function(gd, options) {
208224
if(ya && ya.type === 'date') y2p = exports.decodeDate(y2p);
209225
return convertPath(options, x2p, y2p);
210226
}
211-
212227
if(options.xsizemode === 'pixel') {
213228
var xAnchorPos = x2p(options.xanchor);
214-
x0 = xAnchorPos + options.x0;
215-
x1 = xAnchorPos + options.x1;
229+
x0 = xAnchorPos + options.x0 + shiftUnitX * options.x_shift;
230+
x1 = xAnchorPos + options.x1 + shiftUnitX * options.x_shift;
216231
} else {
217-
x0 = x2p(options.x0);
218-
x1 = x2p(options.x1);
232+
x0 = x2p(options.x0) + shiftUnitX * options.x_shift;
233+
x1 = x2p(options.x1) + shiftUnitX * options.x_shift;
219234
}
220235

221236
if(options.ysizemode === 'pixel') {
222237
var yAnchorPos = y2p(options.yanchor);
223-
y0 = yAnchorPos - options.y0;
224-
y1 = yAnchorPos - options.y1;
238+
y0 = yAnchorPos - options.y0 - shiftUnitY * options.y_shift;
239+
y1 = yAnchorPos - options.y1 - shiftUnitY * options.y_shift;
225240
} else {
226-
y0 = y2p(options.y0);
227-
y1 = y2p(options.y1);
241+
y0 = y2p(options.y0) - shiftUnitY * options.y_shift;
242+
y1 = y2p(options.y1) - shiftUnitY * options.y_shift;
228243
}
229244

230245
if(type === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
{
2+
"data": [
3+
{
4+
"y": [
5+
["A", "A", "B", "B"], ["C", "D", "C", "D"]
6+
],
7+
"x": [
8+
1, 2, 3, 4
9+
],
10+
"type": "bar",
11+
"marker": {
12+
"color": "rgba(153, 217, 234, 1)"
13+
},
14+
"orientation": "h"
15+
},
16+
{
17+
"y": [
18+
["A", "A", "B", "B"], ["C", "D", "C", "D"]
19+
],
20+
"x": [
21+
4, 3, 2, 1
22+
],
23+
"type": "bar",
24+
"orientation": "h"
25+
}
26+
],
27+
"layout": {
28+
"shapes": [
29+
{
30+
"layer": "above",
31+
"type": "rect",
32+
"label": {
33+
"text": "around A,D",
34+
"textposition": "bottom center",
35+
"textangle": 0
36+
},
37+
"line": {
38+
"color": "black",
39+
"width": 3.0
40+
},
41+
"y0": ["A", "C"],
42+
"y1": ["A", "D"],
43+
"x0": 0,
44+
"x1": 0.25,
45+
"y_shift": 0.5,
46+
"xref": "paper"
47+
},
48+
{
49+
"layer": "above",
50+
"type": "line",
51+
"label": {
52+
"text": "on B,D",
53+
"textposition": "middle",
54+
"textangle": 0
55+
},
56+
"line": {
57+
"color": "blue",
58+
"width": 3.0
59+
},
60+
"y0": ["B", "D"],
61+
"y1": ["B", "D"],
62+
"x0": 0,
63+
"x1": 0.25,
64+
"xref": "paper"
65+
},
66+
{
67+
"layer": "above",
68+
"type": "line",
69+
"label": {
70+
"text": "Before B,D",
71+
"textposition": "middle",
72+
"textangle": 0
73+
},
74+
"line": {
75+
"color": "green",
76+
"width": 3.0
77+
},
78+
"y0": ["B", "D"],
79+
"y1": ["B", "D"],
80+
"x0": 0,
81+
"x1": 0.25,
82+
"y_shift": -0.5,
83+
"xref": "paper"
84+
}
85+
]
86+
}
87+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"data": [
3+
{
4+
"x": [
5+
"A", "B", "C", "D"
6+
],
7+
"y": [
8+
1, 2, 3, 4
9+
],
10+
"type": "bar"
11+
}
12+
],
13+
"layout": {
14+
"width": 600,
15+
"shapes": [
16+
{
17+
"layer": "above",
18+
"type": "line",
19+
"label": {
20+
"text": "right from A",
21+
"textposition": "end",
22+
"textangle": 0
23+
},
24+
"line": {
25+
"color": "yellow",
26+
"width": 3.0
27+
},
28+
"x0": "A",
29+
"x1": "A",
30+
"y0": 0,
31+
"y1": 0.5,
32+
"x_shift": 0.5,
33+
"yref": "paper"
34+
},
35+
{
36+
"layer": "above",
37+
"type": "line",
38+
"label": {
39+
"text": "slightly left from D",
40+
"textposition": "end",
41+
"textangle": 0
42+
},
43+
"line": {
44+
"color": "pink",
45+
"width": 3.0
46+
},
47+
"x0": 3,
48+
"x1": 3,
49+
"y0": 0,
50+
"y1": 0.5,
51+
"x_shift": -0.25,
52+
"yref": "paper"
53+
}
54+
]
55+
}
56+
}

test/plot-schema.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10000,6 +10000,14 @@
1000010000
"legendonly"
1000110001
]
1000210002
},
10003+
"x_shift": {
10004+
"description": "Only relevant if xref is a (multi-)category axes. Shifts x0 and x1 by a fraction of the reference unit.",
10005+
"dflt": 0,
10006+
"editType": "calc",
10007+
"max": 0.5,
10008+
"min": -0.5,
10009+
"valType": "number"
10010+
},
1000310011
"x0": {
1000410012
"description": "Sets the shape's starting x position. See `type` and `xsizemode` for more info.",
1000510013
"editType": "calc+arraydraw",
@@ -10034,6 +10042,14 @@
1003410042
"pixel"
1003510043
]
1003610044
},
10045+
"y_shift": {
10046+
"description": "Only relevant if yref is a (multi-)category axes. Shifts y0 and y1 by a fraction of the reference unit.",
10047+
"dflt": 0,
10048+
"editType": "calc",
10049+
"max": 0.5,
10050+
"min": -0.5,
10051+
"valType": "number"
10052+
},
1003710053
"y0": {
1003810054
"description": "Sets the shape's starting y position. See `type` and `ysizemode` for more info.",
1003910055
"editType": "calc+arraydraw",

0 commit comments

Comments
 (0)