Skip to content

Commit fab0cf1

Browse files
authored
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

+75-8
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

+60-6
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

0 commit comments

Comments
 (0)