Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fab0cf1

Browse files
authoredApr 11, 2018
Merge pull request #2532 from plotly/fixed-size-shapes
Fixed size shapes
2 parents 2a25820 + eb853ab commit fab0cf1

File tree

10 files changed

+1106
-101
lines changed

10 files changed

+1106
-101
lines changed
 

‎src/components/shapes/attributes.js

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,20 @@ module.exports = {
3535
'Specifies the shape type to be drawn.',
3636

3737
'If *line*, a line is drawn from (`x0`,`y0`) to (`x1`,`y1`)',
38+
'with respect to the axes\' sizing mode.',
3839

3940
'If *circle*, a circle is drawn from',
4041
'((`x0`+`x1`)/2, (`y0`+`y1`)/2))',
4142
'with radius',
4243
'(|(`x0`+`x1`)/2 - `x0`|, |(`y0`+`y1`)/2 -`y0`)|)',
44+
'with respect to the axes\' sizing mode.',
4345

4446
'If *rect*, a rectangle is drawn linking',
4547
'(`x0`,`y0`), (`x1`,`y0`), (`x1`,`y1`), (`x0`,`y1`), (`x0`,`y0`)',
48+
'with respect to the axes\' sizing mode.',
4649

47-
'If *path*, draw a custom SVG path using `path`.'
50+
'If *path*, draw a custom SVG path using `path`.',
51+
'with respect to the axes\' sizing mode.'
4852
].join(' ')
4953
},
5054

@@ -61,7 +65,7 @@ module.exports = {
6165
description: [
6266
'Sets the shape\'s x coordinate axis.',
6367
'If set to an x axis id (e.g. *x* or *x2*), the `x` position',
64-
'refers to an x coordinate',
68+
'refers to an x coordinate.',
6569
'If set to *paper*, the `x` position refers to the distance from',
6670
'the left side of the plotting area in normalized coordinates',
6771
'where *0* (*1*) corresponds to the left (right) side.',
@@ -71,13 +75,43 @@ module.exports = {
7175
'the date to unix time in milliseconds.'
7276
].join(' ')
7377
}),
78+
xsizemode: {
79+
valType: 'enumerated',
80+
values: ['scaled', 'pixel'],
81+
dflt: 'scaled',
82+
role: 'info',
83+
editType: 'calcIfAutorange+arraydraw',
84+
description: [
85+
'Sets the shapes\'s sizing mode along the x axis.',
86+
'If set to *scaled*, `x0`, `x1` and x coordinates within `path` refer to',
87+
'data values on the x axis or a fraction of the plot area\'s width',
88+
'(`xref` set to *paper*).',
89+
'If set to *pixel*, `xanchor` specifies the x position in terms',
90+
'of data or plot fraction but `x0`, `x1` and x coordinates within `path`',
91+
'are pixels relative to `xanchor`. This way, the shape can have',
92+
'a fixed width while maintaining a position relative to data or',
93+
'plot fraction.'
94+
].join(' ')
95+
},
96+
xanchor: {
97+
valType: 'any',
98+
role: 'info',
99+
editType: 'calcIfAutorange+arraydraw',
100+
description: [
101+
'Only relevant in conjunction with `xsizemode` set to *pixel*.',
102+
'Specifies the anchor point on the x axis to which `x0`, `x1`',
103+
'and x coordinates within `path` are relative to.',
104+
'E.g. useful to attach a pixel sized shape to a certain data value.',
105+
'No effect when `xsizemode` not set to *pixel*.'
106+
].join(' ')
107+
},
74108
x0: {
75109
valType: 'any',
76110
role: 'info',
77111
editType: 'calcIfAutorange+arraydraw',
78112
description: [
79113
'Sets the shape\'s starting x position.',
80-
'See `type` for more info.'
114+
'See `type` and `xsizemode` for more info.'
81115
].join(' ')
82116
},
83117
x1: {
@@ -86,7 +120,7 @@ module.exports = {
86120
editType: 'calcIfAutorange+arraydraw',
87121
description: [
88122
'Sets the shape\'s end x position.',
89-
'See `type` for more info.'
123+
'See `type` and `xsizemode` for more info.'
90124
].join(' ')
91125
},
92126

@@ -100,13 +134,43 @@ module.exports = {
100134
'where *0* (*1*) corresponds to the bottom (top).'
101135
].join(' ')
102136
}),
137+
ysizemode: {
138+
valType: 'enumerated',
139+
values: ['scaled', 'pixel'],
140+
dflt: 'scaled',
141+
role: 'info',
142+
editType: 'calcIfAutorange+arraydraw',
143+
description: [
144+
'Sets the shapes\'s sizing mode along the y axis.',
145+
'If set to *scaled*, `y0`, `y1` and y coordinates within `path` refer to',
146+
'data values on the y axis or a fraction of the plot area\'s height',
147+
'(`yref` set to *paper*).',
148+
'If set to *pixel*, `yanchor` specifies the y position in terms',
149+
'of data or plot fraction but `y0`, `y1` and y coordinates within `path`',
150+
'are pixels relative to `yanchor`. This way, the shape can have',
151+
'a fixed height while maintaining a position relative to data or',
152+
'plot fraction.'
153+
].join(' ')
154+
},
155+
yanchor: {
156+
valType: 'any',
157+
role: 'info',
158+
editType: 'calcIfAutorange+arraydraw',
159+
description: [
160+
'Only relevant in conjunction with `ysizemode` set to *pixel*.',
161+
'Specifies the anchor point on the y axis to which `y0`, `y1`',
162+
'and y coordinates within `path` are relative to.',
163+
'E.g. useful to attach a pixel sized shape to a certain data value.',
164+
'No effect when `ysizemode` not set to *pixel*.'
165+
].join(' ')
166+
},
103167
y0: {
104168
valType: 'any',
105169
role: 'info',
106170
editType: 'calcIfAutorange+arraydraw',
107171
description: [
108172
'Sets the shape\'s starting y position.',
109-
'See `type` for more info.'
173+
'See `type` and `ysizemode` for more info.'
110174
].join(' ')
111175
},
112176
y1: {
@@ -115,7 +179,7 @@ module.exports = {
115179
editType: 'calcIfAutorange+arraydraw',
116180
description: [
117181
'Sets the shape\'s end y position.',
118-
'See `type` for more info.'
182+
'See `type` and `ysizemode` for more info.'
119183
].join(' ')
120184
},
121185

@@ -124,8 +188,11 @@ module.exports = {
124188
role: 'info',
125189
editType: 'calcIfAutorange+arraydraw',
126190
description: [
127-
'For `type` *path* - a valid SVG path but with the pixel values',
128-
'replaced by data values. There are a few restrictions / quirks',
191+
'For `type` *path* - a valid SVG path with the pixel values',
192+
'replaced by data values in `xsizemode`/`ysizemode` being *scaled*',
193+
'and taken unmodified as pixels relative to `xanchor` and `yanchor`',
194+
'in case of *pixel* size mode.',
195+
'There are a few restrictions / quirks',
129196
'only absolute instructions, not relative. So the allowed segments',
130197
'are: M, L, H, V, Q, C, T, S, and Z',
131198
'arcs (A) are not allowed because radius rx and ry are relative.',

‎src/components/shapes/calc_autorange.js

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,79 @@ module.exports = function calcAutorange(gd) {
2323
if(!shapeList.length || !gd._fullData.length) return;
2424

2525
for(var i = 0; i < shapeList.length; i++) {
26-
var shape = shapeList[i],
27-
ppad = shape.line.width / 2;
26+
var shape = shapeList[i];
2827

2928
var ax, bounds;
3029

3130
if(shape.xref !== 'paper') {
31+
var vx0 = shape.xsizemode === 'pixel' ? shape.xanchor : shape.x0,
32+
vx1 = shape.xsizemode === 'pixel' ? shape.xanchor : shape.x1;
3233
ax = Axes.getFromId(gd, shape.xref);
33-
bounds = shapeBounds(ax, shape.x0, shape.x1, shape.path, constants.paramIsX);
34-
if(bounds) Axes.expand(ax, bounds, {ppad: ppad});
34+
35+
bounds = shapeBounds(ax, vx0, vx1, shape.path, constants.paramIsX);
36+
37+
if(bounds) Axes.expand(ax, bounds, calcXPaddingOptions(shape));
3538
}
3639

3740
if(shape.yref !== 'paper') {
41+
var vy0 = shape.ysizemode === 'pixel' ? shape.yanchor : shape.y0,
42+
vy1 = shape.ysizemode === 'pixel' ? shape.yanchor : shape.y1;
3843
ax = Axes.getFromId(gd, shape.yref);
39-
bounds = shapeBounds(ax, shape.y0, shape.y1, shape.path, constants.paramIsY);
40-
if(bounds) Axes.expand(ax, bounds, {ppad: ppad});
44+
45+
bounds = shapeBounds(ax, vy0, vy1, shape.path, constants.paramIsY);
46+
if(bounds) Axes.expand(ax, bounds, calcYPaddingOptions(shape));
4147
}
4248
}
4349
};
4450

51+
function calcXPaddingOptions(shape) {
52+
return calcPaddingOptions(shape.line.width, shape.xsizemode, shape.x0, shape.x1, shape.path, false);
53+
}
54+
55+
function calcYPaddingOptions(shape) {
56+
return calcPaddingOptions(shape.line.width, shape.ysizemode, shape.y0, shape.y1, shape.path, true);
57+
}
58+
59+
function calcPaddingOptions(lineWidth, sizeMode, v0, v1, path, isYAxis) {
60+
var ppad = lineWidth / 2,
61+
axisDirectionReverted = isYAxis;
62+
63+
if(sizeMode === 'pixel') {
64+
var coords = path ?
65+
extractPathCoords(path, isYAxis ? constants.paramIsY : constants.paramIsX) :
66+
[v0, v1];
67+
var maxValue = Lib.aggNums(Math.max, null, coords),
68+
minValue = Lib.aggNums(Math.min, null, coords),
69+
beforePad = minValue < 0 ? Math.abs(minValue) + ppad : ppad,
70+
afterPad = maxValue > 0 ? maxValue + ppad : ppad;
71+
72+
return {
73+
ppad: ppad,
74+
ppadplus: axisDirectionReverted ? beforePad : afterPad,
75+
ppadminus: axisDirectionReverted ? afterPad : beforePad
76+
};
77+
} else {
78+
return {ppad: ppad};
79+
}
80+
}
81+
82+
function extractPathCoords(path, paramsToUse) {
83+
var extractedCoordinates = [];
84+
85+
var segments = path.match(constants.segmentRE);
86+
segments.forEach(function(segment) {
87+
var relevantParamIdx = paramsToUse[segment.charAt(0)].drawn;
88+
if(relevantParamIdx === undefined) return;
89+
90+
var params = segment.substr(1).match(constants.paramRE);
91+
if(!params || params.length < relevantParamIdx) return;
92+
93+
extractedCoordinates.push(params[relevantParamIdx]);
94+
});
95+
96+
return extractedCoordinates;
97+
}
98+
4599
function shapeBounds(ax, v0, v1, path, paramsToUse) {
46100
var convertVal = (ax.type === 'category') ? ax.r2c : ax.d2c;
47101

‎src/components/shapes/draw.js

Lines changed: 121 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,11 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
122122
var MINWIDTH = 10,
123123
MINHEIGHT = 10;
124124

125+
var xPixelSized = shapeOptions.xsizemode === 'pixel',
126+
yPixelSized = shapeOptions.ysizemode === 'pixel';
127+
125128
var update;
126-
var x0, y0, x1, y1, astrX0, astrY0, astrX1, astrY1;
129+
var x0, y0, x1, y1, xAnchor, yAnchor, astrX0, astrY0, astrX1, astrY1, astrXAnchor, astrYAnchor;
127130
var n0, s0, w0, e0, astrN, astrS, astrW, astrE, optN, optS, optW, optE;
128131
var pathIn, astrPath;
129132

@@ -135,14 +138,17 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
135138
prepFn: startDrag,
136139
doneFn: endDrag
137140
},
138-
dragBBox = dragOptions.element.getBoundingClientRect(),
139141
dragMode;
140142

141143
dragElement.init(dragOptions);
142144

143145
shapePath.node().onmousemove = updateDragMode;
144146

145147
function updateDragMode(evt) {
148+
// element might not be on screen at time of setup,
149+
// so obtain bounding box here
150+
var dragBBox = dragOptions.element.getBoundingClientRect();
151+
146152
// choose 'move' or 'resize'
147153
// based on initial position of cursor within the drag element
148154
var w = dragBBox.right - dragBBox.left,
@@ -171,15 +177,25 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
171177

172178
// setup update strings and initial values
173179
var astr = 'shapes[' + index + ']';
180+
181+
if(xPixelSized) {
182+
xAnchor = x2p(shapeOptions.xanchor);
183+
astrXAnchor = astr + '.xanchor';
184+
}
185+
if(yPixelSized) {
186+
yAnchor = y2p(shapeOptions.yanchor);
187+
astrYAnchor = astr + '.yanchor';
188+
}
189+
174190
if(shapeOptions.type === 'path') {
175191
pathIn = shapeOptions.path;
176192
astrPath = astr + '.path';
177193
}
178194
else {
179-
x0 = x2p(shapeOptions.x0);
180-
y0 = y2p(shapeOptions.y0);
181-
x1 = x2p(shapeOptions.x1);
182-
y1 = y2p(shapeOptions.y1);
195+
x0 = xPixelSized ? shapeOptions.x0 : x2p(shapeOptions.x0);
196+
y0 = yPixelSized ? shapeOptions.y0 : y2p(shapeOptions.y0);
197+
x1 = xPixelSized ? shapeOptions.x1 : x2p(shapeOptions.x1);
198+
y1 = yPixelSized ? shapeOptions.y1 : y2p(shapeOptions.y1);
183199

184200
astrX0 = astr + '.x0';
185201
astrY0 = astr + '.y0';
@@ -195,7 +211,10 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
195211
w0 = x1; astrW = astr + '.x1'; optW = 'x1';
196212
e0 = x0; astrE = astr + '.x0'; optE = 'x0';
197213
}
198-
if(y0 < y1) {
214+
215+
// For fixed size shapes take opposing direction of y-axis into account.
216+
// Hint: For data sized shapes this is done by the y2p function.
217+
if((!yPixelSized && y0 < y1) || (yPixelSized && y0 > y1)) {
199218
n0 = y0; astrN = astr + '.y0'; optN = 'y0';
200219
s0 = y1; astrS = astr + '.y1'; optS = 'y1';
201220
}
@@ -218,20 +237,41 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
218237

219238
function moveShape(dx, dy) {
220239
if(shapeOptions.type === 'path') {
221-
var moveX = function moveX(x) { return p2x(x2p(x) + dx); };
222-
if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX);
240+
var noOp = function(coord) { return coord; },
241+
moveX = noOp,
242+
moveY = noOp;
243+
244+
if(xPixelSized) {
245+
update[astrXAnchor] = shapeOptions.xanchor = p2x(xAnchor + dx);
246+
} else {
247+
moveX = function moveX(x) { return p2x(x2p(x) + dx); };
248+
if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX);
249+
}
223250

224-
var moveY = function moveY(y) { return p2y(y2p(y) + dy); };
225-
if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY);
251+
if(yPixelSized) {
252+
update[astrYAnchor] = shapeOptions.yanchor = p2y(yAnchor + dy);
253+
} else {
254+
moveY = function moveY(y) { return p2y(y2p(y) + dy); };
255+
if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY);
256+
}
226257

227258
shapeOptions.path = movePath(pathIn, moveX, moveY);
228259
update[astrPath] = shapeOptions.path;
229260
}
230261
else {
231-
update[astrX0] = shapeOptions.x0 = p2x(x0 + dx);
232-
update[astrY0] = shapeOptions.y0 = p2y(y0 + dy);
233-
update[astrX1] = shapeOptions.x1 = p2x(x1 + dx);
234-
update[astrY1] = shapeOptions.y1 = p2y(y1 + dy);
262+
if(xPixelSized) {
263+
update[astrXAnchor] = shapeOptions.xanchor = p2x(xAnchor + dx);
264+
} else {
265+
update[astrX0] = shapeOptions.x0 = p2x(x0 + dx);
266+
update[astrX1] = shapeOptions.x1 = p2x(x1 + dx);
267+
}
268+
269+
if(yPixelSized) {
270+
update[astrYAnchor] = shapeOptions.yanchor = p2y(yAnchor + dy);
271+
} else {
272+
update[astrY0] = shapeOptions.y0 = p2y(y0 + dy);
273+
update[astrY1] = shapeOptions.y1 = p2y(y1 + dy);
274+
}
235275
}
236276

237277
shapePath.attr('d', getPathString(gd, shapeOptions));
@@ -240,11 +280,23 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
240280
function resizeShape(dx, dy) {
241281
if(shapeOptions.type === 'path') {
242282
// TODO: implement path resize
243-
var moveX = function moveX(x) { return p2x(x2p(x) + dx); };
244-
if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX);
283+
var noOp = function(coord) { return coord; },
284+
moveX = noOp,
285+
moveY = noOp;
286+
287+
if(xPixelSized) {
288+
update[astrXAnchor] = shapeOptions.xanchor = p2x(xAnchor + dx);
289+
} else {
290+
moveX = function moveX(x) { return p2x(x2p(x) + dx); };
291+
if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX);
292+
}
245293

246-
var moveY = function moveY(y) { return p2y(y2p(y) + dy); };
247-
if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY);
294+
if(yPixelSized) {
295+
update[astrYAnchor] = shapeOptions.yanchor = p2y(yAnchor + dy);
296+
} else {
297+
moveY = function moveY(y) { return p2y(y2p(y) + dy); };
298+
if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY);
299+
}
248300

249301
shapeOptions.path = movePath(pathIn, moveX, moveY);
250302
update[astrPath] = shapeOptions.path;
@@ -255,14 +307,21 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
255307
newW = (~dragMode.indexOf('w')) ? w0 + dx : w0,
256308
newE = (~dragMode.indexOf('e')) ? e0 + dx : e0;
257309

258-
if(newS - newN > MINHEIGHT) {
259-
update[astrN] = shapeOptions[optN] = p2y(newN);
260-
update[astrS] = shapeOptions[optS] = p2y(newS);
310+
// Do things in opposing direction for y-axis.
311+
// Hint: for data-sized shapes the reversal of axis direction is done in p2y.
312+
if(~dragMode.indexOf('n') && yPixelSized) newN = n0 - dy;
313+
if(~dragMode.indexOf('s') && yPixelSized) newS = s0 - dy;
314+
315+
// Update shape eventually. Again, be aware of the
316+
// opposing direction of the y-axis of fixed size shapes.
317+
if((!yPixelSized && newS - newN > MINHEIGHT) ||
318+
(yPixelSized && newN - newS > MINHEIGHT)) {
319+
update[astrN] = shapeOptions[optN] = yPixelSized ? newN : p2y(newN);
320+
update[astrS] = shapeOptions[optS] = yPixelSized ? newS : p2y(newS);
261321
}
262-
263322
if(newE - newW > MINWIDTH) {
264-
update[astrW] = shapeOptions[optW] = p2x(newW);
265-
update[astrE] = shapeOptions[optE] = p2x(newE);
323+
update[astrW] = shapeOptions[optW] = xPixelSized ? newW : p2x(newW);
324+
update[astrE] = shapeOptions[optE] = xPixelSized ? newE : p2x(newE);
266325
}
267326
}
268327

@@ -275,10 +334,8 @@ function getPathString(gd, options) {
275334
xa = Axes.getFromId(gd, options.xref),
276335
ya = Axes.getFromId(gd, options.yref),
277336
gs = gd._fullLayout._size,
278-
x2r,
279-
x2p,
280-
y2r,
281-
y2p;
337+
x2r, x2p, y2r, y2p,
338+
x0, x1, y0, y1;
282339

283340
if(xa) {
284341
x2r = helpers.shapePositionToRange(xa);
@@ -299,13 +356,28 @@ function getPathString(gd, options) {
299356
if(type === 'path') {
300357
if(xa && xa.type === 'date') x2p = helpers.decodeDate(x2p);
301358
if(ya && ya.type === 'date') y2p = helpers.decodeDate(y2p);
302-
return convertPath(options.path, x2p, y2p);
359+
return convertPath(options, x2p, y2p);
303360
}
304361

305-
var x0 = x2p(options.x0),
306-
x1 = x2p(options.x1),
307-
y0 = y2p(options.y0),
362+
if(options.xsizemode === 'pixel') {
363+
var xAnchorPos = x2p(options.xanchor);
364+
x0 = xAnchorPos + options.x0;
365+
x1 = xAnchorPos + options.x1;
366+
}
367+
else {
368+
x0 = x2p(options.x0);
369+
x1 = x2p(options.x1);
370+
}
371+
372+
if(options.ysizemode === 'pixel') {
373+
var yAnchorPos = y2p(options.yanchor);
374+
y0 = yAnchorPos - options.y0;
375+
y1 = yAnchorPos - options.y1;
376+
}
377+
else {
378+
y0 = y2p(options.y0);
308379
y1 = y2p(options.y1);
380+
}
309381

310382
if(type === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1;
311383
if(type === 'rect') return 'M' + x0 + ',' + y0 + 'H' + x1 + 'V' + y1 + 'H' + x0 + 'Z';
@@ -322,8 +394,14 @@ function getPathString(gd, options) {
322394
}
323395

324396

325-
function convertPath(pathIn, x2p, y2p) {
326-
// convert an SVG path string from data units to pixels
397+
function convertPath(options, x2p, y2p) {
398+
var pathIn = options.path,
399+
xSizemode = options.xsizemode,
400+
ySizemode = options.ysizemode,
401+
xAnchor = options.xanchor,
402+
yAnchor = options.yanchor;
403+
404+
327405
return pathIn.replace(constants.segmentRE, function(segment) {
328406
var paramNumber = 0,
329407
segmentType = segment.charAt(0),
@@ -332,8 +410,14 @@ function convertPath(pathIn, x2p, y2p) {
332410
nParams = constants.numParams[segmentType];
333411

334412
var paramString = segment.substr(1).replace(constants.paramRE, function(param) {
335-
if(xParams[paramNumber]) param = x2p(param);
336-
else if(yParams[paramNumber]) param = y2p(param);
413+
if(xParams[paramNumber]) {
414+
if(xSizemode === 'pixel') param = x2p(xAnchor) + Number(param);
415+
else param = x2p(param);
416+
}
417+
else if(yParams[paramNumber]) {
418+
if(ySizemode === 'pixel') param = y2p(yAnchor) - Number(param);
419+
else param = y2p(param);
420+
}
337421
paramNumber++;
338422

339423
if(paramNumber > nParams) param = 'X';

‎src/components/shapes/shape_defaults.js

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,32 +36,37 @@ module.exports = function handleShapeDefaults(shapeIn, shapeOut, fullLayout, opt
3636
coerce('line.dash');
3737

3838
var dfltType = shapeIn.path ? 'path' : 'rect',
39-
shapeType = coerce('type', dfltType);
39+
shapeType = coerce('type', dfltType),
40+
xSizeMode = coerce('xsizemode'),
41+
ySizeMode = coerce('ysizemode');
4042

4143
// positioning
4244
var axLetters = ['x', 'y'];
4345
for(var i = 0; i < 2; i++) {
4446
var axLetter = axLetters[i],
45-
gdMock = {_fullLayout: fullLayout};
47+
attrAnchor = axLetter + 'anchor',
48+
sizeMode = axLetter === 'x' ? xSizeMode : ySizeMode,
49+
gdMock = {_fullLayout: fullLayout},
50+
ax,
51+
pos2r,
52+
r2pos;
4653

4754
// xref, yref
4855
var axRef = Axes.coerceRef(shapeIn, shapeOut, gdMock, axLetter, '', 'paper');
4956

57+
if(axRef !== 'paper') {
58+
ax = Axes.getFromId(gdMock, axRef);
59+
r2pos = helpers.rangeToShapePosition(ax);
60+
pos2r = helpers.shapePositionToRange(ax);
61+
}
62+
else {
63+
pos2r = r2pos = Lib.identity;
64+
}
65+
66+
// Coerce x0, x1, y0, y1
5067
if(shapeType !== 'path') {
5168
var dflt0 = 0.25,
52-
dflt1 = 0.75,
53-
ax,
54-
pos2r,
55-
r2pos;
56-
57-
if(axRef !== 'paper') {
58-
ax = Axes.getFromId(gdMock, axRef);
59-
r2pos = helpers.rangeToShapePosition(ax);
60-
pos2r = helpers.shapePositionToRange(ax);
61-
}
62-
else {
63-
pos2r = r2pos = Lib.identity;
64-
}
69+
dflt1 = 0.75;
6570

6671
// hack until V2.0 when log has regular range behavior - make it look like other
6772
// ranges to send to coerce, then put it back after
@@ -74,16 +79,33 @@ module.exports = function handleShapeDefaults(shapeIn, shapeOut, fullLayout, opt
7479
shapeIn[attr0] = pos2r(shapeIn[attr0], true);
7580
shapeIn[attr1] = pos2r(shapeIn[attr1], true);
7681

77-
// x0, x1 (and y0, y1)
78-
Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr0, dflt0);
79-
Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr1, dflt1);
82+
if(sizeMode === 'pixel') {
83+
coerce(attr0, 0);
84+
coerce(attr1, 10);
85+
} else {
86+
Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr0, dflt0);
87+
Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr1, dflt1);
88+
}
8089

8190
// hack part 2
8291
shapeOut[attr0] = r2pos(shapeOut[attr0]);
8392
shapeOut[attr1] = r2pos(shapeOut[attr1]);
8493
shapeIn[attr0] = in0;
8594
shapeIn[attr1] = in1;
8695
}
96+
97+
// Coerce xanchor and yanchor
98+
if(sizeMode === 'pixel') {
99+
// Hack for log axis described above
100+
var inAnchor = shapeIn[attrAnchor];
101+
shapeIn[attrAnchor] = pos2r(shapeIn[attrAnchor], true);
102+
103+
Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attrAnchor, 0.25);
104+
105+
// Hack part 2
106+
shapeOut[attrAnchor] = r2pos(shapeOut[attrAnchor]);
107+
shapeIn[attrAnchor] = inAnchor;
108+
}
87109
}
88110

89111
if(shapeType === 'path') {

‎src/plots/cartesian/autorange.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ function needsAutorange(ax) {
219219
* Note that `expand` is called during `calc`, when we don't yet know the axis
220220
* length; all the inputs should be based solely on the trace data, nothing
221221
* about the axis layout.
222+
* Note that `ppad` and `vpad` as well as their asymmetric variants refer to
223+
* the before and after padding of the passed `data` array, not to the whole axis.
222224
*
223225
* @param {object} ax: the axis being expanded. The result will be more entries
224226
* in ax._min and ax._max if necessary to include the new data
85.2 KB
Loading
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
{
2+
"data": [
3+
{
4+
"x": [1,4],
5+
"y": [1,4]
6+
}, {
7+
"x": [1,3,4],
8+
"y": ["Giraffes","Apes","Zebras"],
9+
"xaxis": "x2",
10+
"yaxis": "y2"
11+
}, {
12+
"x": [
13+
"2018-01-01 00:00:00",
14+
"2018-02-01 00:00:00",
15+
"2018-03-01 00:00:00"
16+
],
17+
"y": [1,5,7],
18+
"xaxis": "x3",
19+
"yaxis": "y3"
20+
}, {
21+
"x": [1,4],
22+
"y": [1,5],
23+
"xaxis": "x4",
24+
"yaxis": "y4"
25+
}
26+
],
27+
"layout": {
28+
"title": "Fixed size shapes",
29+
"autosize": false,
30+
"width": 1000,
31+
"height": 800,
32+
"xaxis": {
33+
"domain": [0,0.30],
34+
"anchor": "y",
35+
"title": "rect, circle, line"
36+
},
37+
"yaxis": {
38+
"domain": [0.55,1],
39+
"type": "log",
40+
"anchor": "x"
41+
},
42+
"xaxis2": {
43+
"domain":[0.33,0.66],
44+
"anchor": "y2",
45+
"title": "triangle as path"
46+
},
47+
"yaxis2": {
48+
"domain": [0.55,1],
49+
"type": "categorical",
50+
"anchor": "x2"
51+
},
52+
"xaxis3": {
53+
"domain":[0.70,1],
54+
"anchor": "y3",
55+
"title": "drag / resize for auto-range"
56+
},
57+
"yaxis3": {
58+
"domain": [0.55,1],
59+
"type": "linear",
60+
"anchor": "x3"
61+
},
62+
"xaxis4": {
63+
"domain": [0,0.30],
64+
"anchor": "y4",
65+
"title": "xy pos & x size paper, y size pixel"
66+
},
67+
"yaxis4": {
68+
"domain": [0,0.45],
69+
"type": "linear",
70+
"anchor": "x4"
71+
},
72+
"xaxis5": {
73+
"domain":[0.33,0.66],
74+
"anchor": "y5",
75+
"title": "rects w/ mixed size modes"
76+
},
77+
"yaxis5": {
78+
"domain": [0,0.45],
79+
"type": "linear",
80+
"anchor": "x5"
81+
},
82+
"xaxis6": {
83+
"domain":[0.70,1],
84+
"anchor": "y6",
85+
"title": "paths w/ mixed size modes"
86+
},
87+
"yaxis6": {
88+
"domain": [0,0.45],
89+
"type": "linear",
90+
"anchor": "x6"
91+
},
92+
"shapes": [
93+
{
94+
"type": "circle",
95+
"xref": "x",
96+
"xsizemode": "pixel",
97+
"yref": "y",
98+
"ysizemode": "pixel",
99+
"fillcolor": "rgba(96, 171, 50,0.7)",
100+
"xanchor": 8,
101+
"yanchor": 8,
102+
"x0": -20,
103+
"x1": 20,
104+
"y0": 20,
105+
"y1": -20,
106+
"line": {
107+
"color": "rgba(96, 171, 50, 1)"
108+
}
109+
},
110+
{
111+
"type": "rect",
112+
"xref": "x",
113+
"xsizemode": "pixel",
114+
"yref": "y",
115+
"ysizemode": "pixel",
116+
"fillcolor": "rgba(96, 171, 50,0.7)",
117+
"xanchor": 7,
118+
"yanchor": 8,
119+
"x0": -20,
120+
"x1": 20,
121+
"y0": 20,
122+
"y1": -20,
123+
"line": {
124+
"color": "rgba(96, 171, 50, 1)"
125+
}
126+
},
127+
{
128+
"type": "line",
129+
"xref": "x",
130+
"xsizemode": "pixel",
131+
"yref": "y",
132+
"ysizemode": "pixel",
133+
"fillcolor": "rgba(96, 171, 50,0.7)",
134+
"xanchor": 6,
135+
"yanchor": 2,
136+
"x0": -20,
137+
"x1": 60,
138+
"y0": 0,
139+
"y1": 40,
140+
"line": {
141+
"color": "rgba(96, 171, 50, 1)"
142+
}
143+
},
144+
{
145+
"type": "path",
146+
"xref": "x2",
147+
"xsizemode": "pixel",
148+
"yref": "y2",
149+
"ysizemode": "pixel",
150+
"fillcolor": "rgba(96, 171, 50,0.7)",
151+
"xanchor": -1,
152+
"yanchor": 1,
153+
"path": "M0,0 L30,0 L15,15 Z",
154+
"line": {
155+
"color": "rgba(96, 171, 50, 1)"
156+
}
157+
},
158+
{
159+
"type": "rect",
160+
"xref": "x3",
161+
"xsizemode": "pixel",
162+
"yref": "y3",
163+
"ysizemode": "pixel",
164+
"fillcolor": "rgba(96, 171, 50,0.7)",
165+
"xanchor": "2018-02-15 00:00:00",
166+
"yanchor": 2,
167+
"x0": 3,
168+
"x1": 53,
169+
"y0": 20,
170+
"y1": -20,
171+
"line": {
172+
"color": "rgba(96, 171, 50, 1)"
173+
}
174+
},
175+
{
176+
"type": "rect",
177+
"xref": "paper",
178+
"xsizemode": "scaled",
179+
"yref": "paper",
180+
"ysizemode": "pixel",
181+
"fillcolor": "rgba(96, 171, 50,0.7)",
182+
"xanchor": 0,
183+
"yanchor": 0,
184+
"x0": 0,
185+
"x1": 0.3,
186+
"y0": 10,
187+
"y1": -10,
188+
"line": {
189+
"color": "rgba(96, 171, 50, 1)"
190+
}
191+
},
192+
{
193+
"type": "rect",
194+
"xref": "x5",
195+
"xsizemode": "scaled",
196+
"yref": "y5",
197+
"ysizemode": "pixel",
198+
"fillcolor": "rgba(96, 171, 50,0.7)",
199+
"xanchor": 0,
200+
"yanchor": 10,
201+
"x0": 0,
202+
"x1": 10,
203+
"y0": 0,
204+
"y1": -30,
205+
"line": {
206+
"color": "rgba(96, 171, 50, 1)"
207+
}
208+
},
209+
{
210+
"type": "rect",
211+
"xref": "x5",
212+
"xsizemode": "pixel",
213+
"yref": "y5",
214+
"ysizemode": "scaled",
215+
"fillcolor": "rgba(96, 171, 50,0.7)",
216+
"xanchor": 3,
217+
"yanchor": 3,
218+
"x0": 0,
219+
"x1": 30,
220+
"y0": 0,
221+
"y1": 10,
222+
"line": {
223+
"color": "rgba(96, 171, 50, 1)"
224+
}
225+
},
226+
{
227+
"type": "path",
228+
"xref": "x6",
229+
"xsizemode": "pixel",
230+
"yref": "y6",
231+
"ysizemode": "scaled",
232+
"fillcolor": "rgba(96, 171, 50,0.7)",
233+
"xanchor": 10,
234+
"yanchor": 10,
235+
"path": "M0,0 L30,0 L15,4 Z",
236+
"line": {
237+
"color": "rgba(96, 171, 50, 1)"
238+
}
239+
},
240+
{
241+
"type": "path",
242+
"xref": "x6",
243+
"xsizemode": "scaled",
244+
"yref": "y6",
245+
"ysizemode": "pixel",
246+
"fillcolor": "rgba(96, 171, 50,0.7)",
247+
"xanchor": 10,
248+
"yanchor": 10,
249+
"path": "M0,0 L10,0 L5,25 Z",
250+
"line": {
251+
"color": "rgba(96, 171, 50, 1)"
252+
}
253+
}
254+
]
255+
},
256+
"config": {
257+
"editable": "true"
258+
}
259+
}

‎test/jasmine/assets/custom_assertions.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,29 @@ exports.checkTicks = function(axLetter, vals, msg) {
190190
expect(d3.select(this).text()).toBe(vals[i], msg + ': ' + i);
191191
});
192192
};
193+
194+
exports.assertElemRightTo = function(elem, refElem, msg) {
195+
var elemBB = elem.getBoundingClientRect();
196+
var refElemBB = refElem.getBoundingClientRect();
197+
expect(elemBB.left >= refElemBB.right).toBe(true, msg);
198+
};
199+
200+
201+
exports.assertElemTopsAligned = function(elem1, elem2, msg) {
202+
var elem1BB = elem1.getBoundingClientRect();
203+
var elem2BB = elem2.getBoundingClientRect();
204+
205+
// Hint: toBeWithin tolerance is exclusive, hence a
206+
// diff of exactly 1 would fail the test
207+
var tolerance = 1.1;
208+
expect(elem1BB.top - elem2BB.top).toBeWithin(0, tolerance, msg);
209+
};
210+
211+
exports.assertElemInside = function(elem, container, msg) {
212+
var elemBB = elem.getBoundingClientRect();
213+
var contBB = container.getBoundingClientRect();
214+
expect(contBB.left < elemBB.left &&
215+
contBB.right > elemBB.right &&
216+
contBB.top < elemBB.top &&
217+
contBB.bottom > elemBB.bottom).toBe(true, msg);
218+
};

‎test/jasmine/tests/hover_label_test.js

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ var fail = require('../assets/fail_test');
1717
var customAssertions = require('../assets/custom_assertions');
1818
var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle;
1919
var assertHoverLabelContent = customAssertions.assertHoverLabelContent;
20+
var assertElemRightTo = customAssertions.assertElemRightTo;
21+
var assertElemTopsAligned = customAssertions.assertElemTopsAligned;
22+
var assertElemInside = customAssertions.assertElemInside;
2023

2124
describe('hover info', function() {
2225
'use strict';
@@ -1136,15 +1139,6 @@ describe('hover info', function() {
11361139
msgPrefixFmt + 'Primary text inside box');
11371140
assertElemInside(nodes.secondaryText, nodes.secondaryBox,
11381141
msgPrefixFmt + 'Secondary text inside box');
1139-
1140-
function assertElemInside(elem, container, msg) {
1141-
var elemBB = elem.getBoundingClientRect();
1142-
var contBB = container.getBoundingClientRect();
1143-
expect(contBB.left < elemBB.left &&
1144-
contBB.right > elemBB.right &&
1145-
contBB.top < elemBB.top &&
1146-
contBB.bottom > elemBB.bottom).toBe(true, msg);
1147-
}
11481142
}
11491143

11501144
function assertSecondaryRightToPrimaryBox(nodes, msgPrefix) {
@@ -1154,22 +1148,6 @@ describe('hover info', function() {
11541148
msgPrefixFmt + 'Secondary box right to primary box');
11551149
assertElemTopsAligned(nodes.secondaryBox, nodes.primaryBox,
11561150
msgPrefixFmt + 'Top edges of primary and secondary boxes aligned');
1157-
1158-
function assertElemRightTo(elem, refElem, msg) {
1159-
var elemBB = elem.getBoundingClientRect();
1160-
var refElemBB = refElem.getBoundingClientRect();
1161-
expect(elemBB.left >= refElemBB.right).toBe(true, msg);
1162-
}
1163-
1164-
function assertElemTopsAligned(elem1, elem2, msg) {
1165-
var elem1BB = elem1.getBoundingClientRect();
1166-
var elem2BB = elem2.getBoundingClientRect();
1167-
1168-
// Hint: toBeWithin tolerance is exclusive, hence a
1169-
// diff of exactly 1 would fail the test
1170-
var tolerance = 1.1;
1171-
expect(elem1BB.top - elem2BB.top).toBeWithin(0, tolerance, msg);
1172-
}
11731151
}
11741152

11751153
describe('centered', function() {

‎test/jasmine/tests/shapes_test.js

Lines changed: 520 additions & 7 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.