Skip to content

Commit 6607870

Browse files
authored
Merge pull request #6608 from plotly/shape-label-while-drawing
Render shape label while drawing
2 parents 6d4cd86 + 4d860a2 commit 6607870

File tree

5 files changed

+358
-308
lines changed

5 files changed

+358
-308
lines changed

src/components/selections/select.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ var newShapeHelpers = require('../shapes/draw_newshape/helpers');
2525
var handleEllipse = newShapeHelpers.handleEllipse;
2626
var readPaths = newShapeHelpers.readPaths;
2727

28-
var newShapes = require('../shapes/draw_newshape/newshapes');
28+
var newShapes = require('../shapes/draw_newshape/newshapes').newShapes;
2929

3030
var newSelections = require('./draw_newselection/newselections');
3131
var activateLastSelection = require('./draw').activateLastSelection;
@@ -112,6 +112,10 @@ function prepSelect(evt, startX, startY, dragOptions, mode) {
112112
fullLayout.newshape :
113113
fullLayout.newselection;
114114

115+
if(isDrawMode) {
116+
dragOptions.hasText = newStyle.label.text || newStyle.label.texttemplate;
117+
}
118+
115119
var fillC = (isDrawMode && !isOpenMode) ? newStyle.fillcolor : 'rgba(0,0,0,0)';
116120

117121
var strokeC = newStyle.line.color || (
@@ -146,6 +150,16 @@ function prepSelect(evt, startX, startY, dragOptions, mode) {
146150
.attr('transform', transform)
147151
.attr('d', 'M0,0Z');
148152

153+
// create & style group for text label
154+
if(isDrawMode && dragOptions.hasText) {
155+
var shapeGroup = zoomLayer.select('.label-temp');
156+
if(shapeGroup.empty()) {
157+
shapeGroup = zoomLayer.append('g')
158+
.classed('label-temp', true)
159+
.classed('select-outline', true)
160+
.style({ opacity: 0.8 });
161+
}
162+
}
149163

150164
var throttleID = fullLayout._uid + constants.SELECTID;
151165
var selection = [];
+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
'use strict';
2+
3+
var Lib = require('../../lib');
4+
var Axes = require('../../plots/cartesian/axes');
5+
var svgTextUtils = require('../../lib/svg_text_utils');
6+
7+
var Drawing = require('../drawing');
8+
9+
var readPaths = require('./draw_newshape/helpers').readPaths;
10+
var helpers = require('./helpers');
11+
var getPathString = helpers.getPathString;
12+
var shapeLabelTexttemplateVars = require('./label_texttemplate');
13+
14+
var FROM_TL = require('../../constants/alignment').FROM_TL;
15+
16+
17+
module.exports = function drawLabel(gd, index, options, shapeGroup) {
18+
// Remove existing label
19+
shapeGroup.selectAll('.shape-label').remove();
20+
21+
// If no label text or texttemplate, return
22+
if(!(options.label.text || options.label.texttemplate)) return;
23+
24+
// Text template overrides text
25+
var text;
26+
if(options.label.texttemplate) {
27+
var templateValues = {};
28+
if(options.type !== 'path') {
29+
var _xa = Axes.getFromId(gd, options.xref);
30+
var _ya = Axes.getFromId(gd, options.yref);
31+
for(var key in shapeLabelTexttemplateVars) {
32+
var val = shapeLabelTexttemplateVars[key](options, _xa, _ya);
33+
if(val !== undefined) templateValues[key] = val;
34+
}
35+
}
36+
text = Lib.texttemplateStringForShapes(options.label.texttemplate,
37+
{},
38+
gd._fullLayout._d3locale,
39+
templateValues);
40+
} else {
41+
text = options.label.text;
42+
}
43+
44+
var labelGroupAttrs = {
45+
'data-index': index,
46+
};
47+
var font = options.label.font;
48+
49+
var labelTextAttrs = {
50+
'data-notex': 1
51+
};
52+
53+
var labelGroup = shapeGroup.append('g')
54+
.attr(labelGroupAttrs)
55+
.classed('shape-label', true);
56+
var labelText = labelGroup.append('text')
57+
.attr(labelTextAttrs)
58+
.classed('shape-label-text', true)
59+
.text(text);
60+
61+
// Get x and y bounds of shape
62+
var shapex0, shapex1, shapey0, shapey1;
63+
if(options.path) {
64+
// If shape is defined as a path, get the
65+
// min and max bounds across all polygons in path
66+
var d = getPathString(gd, options);
67+
var polygons = readPaths(d, gd);
68+
shapex0 = Infinity;
69+
shapey0 = Infinity;
70+
shapex1 = -Infinity;
71+
shapey1 = -Infinity;
72+
for(var i = 0; i < polygons.length; i++) {
73+
for(var j = 0; j < polygons[i].length; j++) {
74+
var p = polygons[i][j];
75+
for(var k = 1; k < p.length; k += 2) {
76+
var _x = p[k];
77+
var _y = p[k + 1];
78+
79+
shapex0 = Math.min(shapex0, _x);
80+
shapex1 = Math.max(shapex1, _x);
81+
shapey0 = Math.min(shapey0, _y);
82+
shapey1 = Math.max(shapey1, _y);
83+
}
84+
}
85+
}
86+
} else {
87+
// Otherwise, we use the x and y bounds defined in the shape options
88+
// and convert them to pixel coordinates
89+
// Setup conversion functions
90+
var xa = Axes.getFromId(gd, options.xref);
91+
var xRefType = Axes.getRefType(options.xref);
92+
var ya = Axes.getFromId(gd, options.yref);
93+
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);
100+
}
101+
102+
// Handle `auto` angle
103+
var textangle = options.label.textangle;
104+
if(textangle === 'auto') {
105+
if(options.type === 'line') {
106+
// Auto angle for line is same angle as line
107+
textangle = calcTextAngle(shapex0, shapey0, shapex1, shapey1);
108+
} else {
109+
// Auto angle for all other shapes is 0
110+
textangle = 0;
111+
}
112+
}
113+
114+
// Do an initial render so we can get the text bounding box height
115+
labelText.call(function(s) {
116+
s.call(Drawing.font, font).attr({});
117+
svgTextUtils.convertToTspans(s, gd);
118+
return s;
119+
});
120+
var textBB = Drawing.bBox(labelText.node());
121+
122+
// Calculate correct (x,y) for text
123+
// We also determine true xanchor since xanchor depends on position when set to 'auto'
124+
var textPos = calcTextPosition(shapex0, shapey0, shapex1, shapey1, options, textangle, textBB);
125+
var textx = textPos.textx;
126+
var texty = textPos.texty;
127+
var xanchor = textPos.xanchor;
128+
129+
// Update (x,y) position, xanchor, and angle
130+
labelText.attr({
131+
'text-anchor': {
132+
left: 'start',
133+
center: 'middle',
134+
right: 'end'
135+
}[xanchor],
136+
y: texty,
137+
x: textx,
138+
transform: 'rotate(' + textangle + ',' + textx + ',' + texty + ')'
139+
}).call(svgTextUtils.positionText, textx, texty);
140+
};
141+
142+
function calcTextAngle(shapex0, shapey0, shapex1, shapey1) {
143+
var dy, dx;
144+
dx = Math.abs(shapex1 - shapex0);
145+
if(shapex1 >= shapex0) {
146+
dy = shapey0 - shapey1;
147+
} else {
148+
dy = shapey1 - shapey0;
149+
}
150+
return -180 / Math.PI * Math.atan2(dy, dx);
151+
}
152+
153+
function calcTextPosition(shapex0, shapey0, shapex1, shapey1, shapeOptions, actualTextAngle, textBB) {
154+
var textPosition = shapeOptions.label.textposition;
155+
var textAngle = shapeOptions.label.textangle;
156+
var textPadding = shapeOptions.label.padding;
157+
var shapeType = shapeOptions.type;
158+
var textAngleRad = Math.PI / 180 * actualTextAngle;
159+
var sinA = Math.sin(textAngleRad);
160+
var cosA = Math.cos(textAngleRad);
161+
var xanchor = shapeOptions.label.xanchor;
162+
var yanchor = shapeOptions.label.yanchor;
163+
164+
var textx, texty, paddingX, paddingY;
165+
166+
// Text position functions differently for lines vs. other shapes
167+
if(shapeType === 'line') {
168+
// Set base position for start vs. center vs. end of line (default is 'center')
169+
if(textPosition === 'start') {
170+
textx = shapex0;
171+
texty = shapey0;
172+
} else if(textPosition === 'end') {
173+
textx = shapex1;
174+
texty = shapey1;
175+
} else { // Default: center
176+
textx = (shapex0 + shapex1) / 2;
177+
texty = (shapey0 + shapey1) / 2;
178+
}
179+
180+
// Set xanchor if xanchor is 'auto'
181+
if(xanchor === 'auto') {
182+
if(textPosition === 'start') {
183+
if(textAngle === 'auto') {
184+
if(shapex1 > shapex0) xanchor = 'left';
185+
else if(shapex1 < shapex0) xanchor = 'right';
186+
else xanchor = 'center';
187+
} else {
188+
if(shapex1 > shapex0) xanchor = 'right';
189+
else if(shapex1 < shapex0) xanchor = 'left';
190+
else xanchor = 'center';
191+
}
192+
} else if(textPosition === 'end') {
193+
if(textAngle === 'auto') {
194+
if(shapex1 > shapex0) xanchor = 'right';
195+
else if(shapex1 < shapex0) xanchor = 'left';
196+
else xanchor = 'center';
197+
} else {
198+
if(shapex1 > shapex0) xanchor = 'left';
199+
else if(shapex1 < shapex0) xanchor = 'right';
200+
else xanchor = 'center';
201+
}
202+
} else {
203+
xanchor = 'center';
204+
}
205+
}
206+
207+
// Special case for padding when angle is 'auto' for lines
208+
// Padding should be treated as an orthogonal offset in this case
209+
// Otherwise, padding is just a simple x and y offset
210+
var paddingConstantsX = { left: 1, center: 0, right: -1 };
211+
var paddingConstantsY = { bottom: -1, middle: 0, top: 1 };
212+
if(textAngle === 'auto') {
213+
// Set direction to apply padding (based on `yanchor` only)
214+
var paddingDirection = paddingConstantsY[yanchor];
215+
paddingX = -textPadding * sinA * paddingDirection;
216+
paddingY = textPadding * cosA * paddingDirection;
217+
} else {
218+
// Set direction to apply padding (based on `xanchor` and `yanchor`)
219+
var paddingDirectionX = paddingConstantsX[xanchor];
220+
var paddingDirectionY = paddingConstantsY[yanchor];
221+
paddingX = textPadding * paddingDirectionX;
222+
paddingY = textPadding * paddingDirectionY;
223+
}
224+
textx = textx + paddingX;
225+
texty = texty + paddingY;
226+
} else {
227+
// Text position for shapes that are not lines
228+
// calc horizontal position
229+
// Horizontal needs a little extra padding to look balanced
230+
paddingX = textPadding + 3;
231+
if(textPosition.indexOf('right') !== -1) {
232+
textx = Math.max(shapex0, shapex1) - paddingX;
233+
if(xanchor === 'auto') xanchor = 'right';
234+
} else if(textPosition.indexOf('left') !== -1) {
235+
textx = Math.min(shapex0, shapex1) + paddingX;
236+
if(xanchor === 'auto') xanchor = 'left';
237+
} else { // Default: center
238+
textx = (shapex0 + shapex1) / 2;
239+
if(xanchor === 'auto') xanchor = 'center';
240+
}
241+
242+
// calc vertical position
243+
if(textPosition.indexOf('top') !== -1) {
244+
texty = Math.min(shapey0, shapey1);
245+
} else if(textPosition.indexOf('bottom') !== -1) {
246+
texty = Math.max(shapey0, shapey1);
247+
} else {
248+
texty = (shapey0 + shapey1) / 2;
249+
}
250+
// Apply padding
251+
paddingY = textPadding;
252+
if(yanchor === 'bottom') {
253+
texty = texty - paddingY;
254+
} else if(yanchor === 'top') {
255+
texty = texty + paddingY;
256+
}
257+
}
258+
259+
// Shift vertical (& horizontal) position according to `yanchor`
260+
var shiftFraction = FROM_TL[yanchor];
261+
// Adjust so that text is anchored at top of first line rather than at baseline of first line
262+
var baselineAdjust = shapeOptions.label.font.size;
263+
var textHeight = textBB.height;
264+
var xshift = (textHeight * shiftFraction - baselineAdjust) * sinA;
265+
var yshift = -(textHeight * shiftFraction - baselineAdjust) * cosA;
266+
267+
return { textx: textx + xshift, texty: texty + yshift, xanchor: xanchor };
268+
}

src/components/shapes/display_outlines.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ var helpers = require('./draw_newshape/helpers');
2424
var pointsOnRectangle = helpers.pointsOnRectangle;
2525
var pointsOnEllipse = helpers.pointsOnEllipse;
2626
var writePaths = helpers.writePaths;
27-
var newShapes = require('./draw_newshape/newshapes');
27+
var newShapes = require('./draw_newshape/newshapes').newShapes;
28+
var createShapeObj = require('./draw_newshape/newshapes').createShapeObj;
2829
var newSelections = require('../selections/draw_newselection/newselections');
30+
var drawLabel = require('./display_labels');
31+
2932

3033
module.exports = function displayOutlines(polygons, outlines, dragOptions, nCalls) {
3134
if(!nCalls) nCalls = 0;
@@ -95,6 +98,13 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall
9598
addGroupControllers();
9699
}
97100

101+
// draw label
102+
if(isDrawMode && dragOptions.hasText) {
103+
var shapeGroup = zoomLayer.select('.label-temp');
104+
var shapeOptions = createShapeObj(outlines, dragOptions, dragOptions.dragmode);
105+
drawLabel(gd, 'label-temp', shapeOptions, shapeGroup);
106+
}
107+
98108
function startDragVertex(evt) {
99109
indexI = +evt.srcElement.getAttribute('data-i');
100110
indexJ = +evt.srcElement.getAttribute('data-j');

0 commit comments

Comments
 (0)