Skip to content

Commit b4318ad

Browse files
authoredApr 6, 2017
Merge pull request #1551 from plotly/note-sizing
Annotation size & text alignment
2 parents ce06ea9 + b21154b commit b4318ad

File tree

6 files changed

+183
-32
lines changed

6 files changed

+183
-32
lines changed
 

‎src/components/annotations/annotation_defaults.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
3030
if(!(visible || clickToShow)) return annOut;
3131

3232
coerce('opacity');
33-
coerce('align');
3433
coerce('bgcolor');
3534

3635
var borderColor = coerce('bordercolor'),
@@ -45,6 +44,12 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
4544
coerce('textangle');
4645
Lib.coerceFont(coerce, 'font', fullLayout.font);
4746

47+
coerce('width');
48+
coerce('align');
49+
50+
var h = coerce('height');
51+
if(h) coerce('valign');
52+
4853
// positioning
4954
var axLetters = ['x', 'y'],
5055
arrowPosDflt = [-10, -30],

‎src/components/annotations/attributes.js

+36-4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,27 @@ module.exports = {
4949
font: extendFlat({}, fontAttrs, {
5050
description: 'Sets the annotation text font.'
5151
}),
52+
width: {
53+
valType: 'number',
54+
min: 1,
55+
dflt: null,
56+
role: 'style',
57+
description: [
58+
'Sets an explicit width for the text box. null (default) lets the',
59+
'text set the box width. Wider text will be clipped.',
60+
'There is no automatic wrapping; use <br> to start a new line.'
61+
].join(' ')
62+
},
63+
height: {
64+
valType: 'number',
65+
min: 1,
66+
dflt: null,
67+
role: 'style',
68+
description: [
69+
'Sets an explicit height for the text box. null (default) lets the',
70+
'text set the box height. Taller text will be clipped.'
71+
].join(' ')
72+
},
5273
opacity: {
5374
valType: 'number',
5475
min: 0,
@@ -63,10 +84,21 @@ module.exports = {
6384
dflt: 'center',
6485
role: 'style',
6586
description: [
66-
'Sets the vertical alignment of the `text` with',
67-
'respect to the set `x` and `y` position.',
68-
'Has only an effect if `text` spans more two or more lines',
69-
'(i.e. `text` contains one or more <br> HTML tags).'
87+
'Sets the horizontal alignment of the `text` within the box.',
88+
'Has an effect only if `text` spans more two or more lines',
89+
'(i.e. `text` contains one or more <br> HTML tags) or if an',
90+
'explicit width is set to override the text width.'
91+
].join(' ')
92+
},
93+
valign: {
94+
valType: 'enumerated',
95+
values: ['top', 'middle', 'bottom'],
96+
dflt: 'middle',
97+
role: 'style',
98+
description: [
99+
'Sets the vertical alignment of the `text` within the box.',
100+
'Has an effect only if an explicit height is set to override',
101+
'the text height.'
70102
].join(' ')
71103
},
72104
bgcolor: {

‎src/components/annotations/draw.js

+60-20
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,14 @@ function drawOne(gd, index) {
7272
var optionsIn = (layout.annotations || [])[index],
7373
options = fullLayout.annotations[index];
7474

75+
var annClipID = 'clip' + fullLayout._uid + '_ann' + index;
76+
7577
// this annotation is gone - quit now after deleting it
7678
// TODO: use d3 idioms instead of deleting and redrawing every time
77-
if(!optionsIn || options.visible === false) return;
79+
if(!optionsIn || options.visible === false) {
80+
d3.selectAll('#' + annClipID).remove();
81+
return;
82+
}
7883

7984
var xa = Axes.getFromId(gd, options.xref),
8085
ya = Axes.getFromId(gd, options.yref),
@@ -118,6 +123,18 @@ function drawOne(gd, index) {
118123
.call(Color.stroke, options.bordercolor)
119124
.call(Color.fill, options.bgcolor);
120125

126+
var isSizeConstrained = options.width || options.height;
127+
128+
var annTextClip = fullLayout._defs.select('.clips')
129+
.selectAll('#' + annClipID)
130+
.data(isSizeConstrained ? [0] : []);
131+
132+
annTextClip.enter().append('clipPath')
133+
.classed('annclip', true)
134+
.attr('id', annClipID)
135+
.append('rect');
136+
annTextClip.exit().remove();
137+
121138
var font = options.font;
122139

123140
var annText = annTextGroupInner.append('text')
@@ -144,19 +161,21 @@ function drawOne(gd, index) {
144161
// at the end, even if their position changes
145162
annText.selectAll('tspan.line').attr({y: 0, x: 0});
146163

147-
var mathjaxGroup = annTextGroupInner.select('.annotation-math-group'),
148-
hasMathjax = !mathjaxGroup.empty(),
149-
anntextBB = Drawing.bBox(
150-
(hasMathjax ? mathjaxGroup : annText).node()),
151-
annwidth = anntextBB.width,
152-
annheight = anntextBB.height,
153-
outerwidth = Math.round(annwidth + 2 * borderfull),
154-
outerheight = Math.round(annheight + 2 * borderfull);
164+
var mathjaxGroup = annTextGroupInner.select('.annotation-math-group');
165+
var hasMathjax = !mathjaxGroup.empty();
166+
var anntextBB = Drawing.bBox(
167+
(hasMathjax ? mathjaxGroup : annText).node());
168+
var textWidth = anntextBB.width;
169+
var textHeight = anntextBB.height;
170+
var annWidth = options.width || textWidth;
171+
var annHeight = options.height || textHeight;
172+
var outerWidth = Math.round(annWidth + 2 * borderfull);
173+
var outerHeight = Math.round(annHeight + 2 * borderfull);
155174

156175

157176
// save size in the annotation object for use by autoscale
158-
options._w = annwidth;
159-
options._h = annheight;
177+
options._w = annWidth;
178+
options._h = annHeight;
160179

161180
function shiftFraction(v, anchor) {
162181
if(anchor === 'auto') {
@@ -181,8 +200,8 @@ function drawOne(gd, index) {
181200
ax = Axes.getFromId(gd, axRef),
182201
dimAngle = (textangle + (axLetter === 'x' ? 0 : -90)) * Math.PI / 180,
183202
// note that these two can be either positive or negative
184-
annSizeFromWidth = outerwidth * Math.cos(dimAngle),
185-
annSizeFromHeight = outerheight * Math.sin(dimAngle),
203+
annSizeFromWidth = outerWidth * Math.cos(dimAngle),
204+
annSizeFromHeight = outerHeight * Math.sin(dimAngle),
186205
// but this one is the positive total size
187206
annSize = Math.abs(annSizeFromWidth) + Math.abs(annSizeFromHeight),
188207
anchor = options[axLetter + 'anchor'],
@@ -299,22 +318,43 @@ function drawOne(gd, index) {
299318
return;
300319
}
301320

321+
var xShift = 0;
322+
var yShift = 0;
323+
324+
if(options.align !== 'left') {
325+
xShift = (annWidth - textWidth) * (options.align === 'center' ? 0.5 : 1);
326+
}
327+
if(options.valign !== 'top') {
328+
yShift = (annHeight - textHeight) * (options.valign === 'middle' ? 0.5 : 1);
329+
}
330+
302331
if(hasMathjax) {
303-
mathjaxGroup.select('svg').attr({x: borderfull - 1, y: borderfull});
332+
mathjaxGroup.select('svg').attr({
333+
x: borderfull + xShift - 1,
334+
y: borderfull + yShift
335+
})
336+
.call(Drawing.setClipUrl, isSizeConstrained ? annClipID : null);
304337
}
305338
else {
306-
var texty = borderfull - anntextBB.top,
307-
textx = borderfull - anntextBB.left;
308-
annText.attr({x: textx, y: texty});
339+
var texty = borderfull + yShift - anntextBB.top,
340+
textx = borderfull + xShift - anntextBB.left;
341+
annText.attr({
342+
x: textx,
343+
y: texty
344+
})
345+
.call(Drawing.setClipUrl, isSizeConstrained ? annClipID : null);
309346
annText.selectAll('tspan.line').attr({y: texty, x: textx});
310347
}
311348

349+
annTextClip.select('rect').call(Drawing.setRect, borderfull, borderfull,
350+
annWidth, annHeight);
351+
312352
annTextBG.call(Drawing.setRect, borderwidth / 2, borderwidth / 2,
313-
outerwidth - borderwidth, outerheight - borderwidth);
353+
outerWidth - borderwidth, outerHeight - borderwidth);
314354

315355
annTextGroupInner.call(Drawing.setTranslate,
316-
Math.round(annPosPx.x.text - outerwidth / 2),
317-
Math.round(annPosPx.y.text - outerheight / 2));
356+
Math.round(annPosPx.x.text - outerWidth / 2),
357+
Math.round(annPosPx.y.text - outerHeight / 2));
318358

319359
/*
320360
* rotate text and background
9.82 KB
Loading

‎test/image/mocks/annotations-autorange.json

+33-7
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@
4848
"zeroline":false,
4949
"showline":true
5050
},
51-
"height":300,
51+
"height":360,
5252
"width":800,
53-
"margin":{"r":40,"b":40,"l":40,"t":40},
53+
"margin":{"r":40,"b":100,"l":40,"t":40},
5454
"annotations":[
5555
{"ay":0,"ax":50,"x":1,"y":1.5,"text":"Left"},
5656
{"ay":0,"ax":-50,"x":2,"y":1.5,"text":"Right"},
@@ -60,16 +60,42 @@
6060
"xref":"x2","yref":"y2","text":"From left","y":2,"ax":-17,"ay":0,"x":"2001-01-01",
6161
"xanchor": "right", "yanchor": "top", "textangle": 35, "bordercolor": "#444"
6262
},
63-
{"xref":"x2","yref":"y2","text":"From right","y":2,"x":"2001-03-01","ay":0,"ax":50},
63+
{
64+
"xref":"x2","yref":"y2","text":"From<br>right","y":2,"x":"2001-03-01","ay":0,"ax":50,
65+
"bgcolor": "#eee", "width": 50, "height": 40, "textangle": 70
66+
},
6467
{
6568
"xref":"x2","yref":"y2","text":"From top","y":3,"ax":0,"ay":-27,"x":"2001-02-01",
6669
"xanchor": "left", "yanchor": "bottom", "textangle": -15, "bordercolor": "#444"
6770
},
68-
{"xref":"x2","yref":"y2","text":"From Bottom","y":1,"ax":0,"ay":50,"x":"2001-02-01"},
69-
{"xref":"x3","yref":"y3","text":"Left<br>no<br>arrow","y":1.5,"x":1,"showarrow":false},
71+
{
72+
"xref":"x2","yref":"y2","text":"From Bottom","y":1,"ax":0,"ay":50,"x":"2001-02-01",
73+
"bordercolor": "#444", "borderwidth": 3, "height": 30
74+
},
75+
{
76+
"xref":"x3","yref":"y3","text":"Left<br>no<br>arrow","y":1.5,"x":1,"showarrow":false,
77+
"bordercolor": "#444", "bgcolor": "#eee", "width": 50, "height": 60, "textangle": 10,
78+
"align": "right", "valign": "bottom"
79+
},
7080
{"xref":"x3","yref":"y3","text":"Right<br>no<br>arrow","y":1.5,"x":2,"showarrow":false},
71-
{"xref":"x3","yref":"y3","text":"Bottom<br>no<br>arrow","y":1,"x":1.5,"showarrow":false},
72-
{"xref":"x3","yref":"y3","text":"Top<br>no<br>arrow","y":2,"x":1.5,"showarrow":false}
81+
{
82+
"xref":"x3","yref":"y3","text":"Bottom<br>no<br>arrow","y":1,"x":1.5,"showarrow":false,
83+
"bgcolor": "#eee", "width": 30, "height": 40, "textangle":-10,
84+
"align": "left", "valign": "top"
85+
},
86+
{"xref":"x3","yref":"y3","text":"Top<br>no<br>arrow","y":2,"x":1.5,"showarrow":false},
87+
{
88+
"xref": "paper", "yref": "paper", "text": "On the<br>bottom of the plot",
89+
"x": 0.3, "y": -0.1, "showarrow": false,
90+
"xanchor": "right", "yanchor": "top", "width": 200, "height": 60,
91+
"bgcolor": "#eee", "bordercolor": "#444"
92+
},
93+
{
94+
"xref": "paper", "yref": "paper", "text": "blah blah blah blah<br>blah<br>blah<br>blah<br>blah<br>blah",
95+
"x": 0.3, "y": -0.25, "ax": 100, "ay": 0, "textangle": 40, "borderpad": 4,
96+
"xanchor": "left", "yanchor": "bottom", "align": "left", "valign": "top",
97+
"width": 60, "height": 40, "bgcolor": "#eee", "bordercolor": "#444"
98+
}
7399
]
74100
}
75101
}

‎test/jasmine/tests/annotations_test.js

+48
Original file line numberDiff line numberDiff line change
@@ -1041,3 +1041,51 @@ describe('annotation dragging', function() {
10411041
.then(done);
10421042
});
10431043
});
1044+
1045+
describe('annotation clip paths', function() {
1046+
var gd;
1047+
1048+
beforeEach(function(done) {
1049+
gd = createGraphDiv();
1050+
1051+
// we've already tested autorange with relayout, so fix the geometry
1052+
// completely so we know exactly what we're dealing with
1053+
// plot area is 300x300, and covers data range 100x100
1054+
Plotly.plot(gd, [{x: [0, 100], y: [0, 100]}], {
1055+
annotations: [
1056+
{x: 50, y: 50, text: 'hi', width: 50},
1057+
{x: 20, y: 20, text: 'bye', height: 40},
1058+
{x: 80, y: 80, text: 'why?'}
1059+
]
1060+
})
1061+
.then(done);
1062+
});
1063+
1064+
afterEach(destroyGraphDiv);
1065+
1066+
it('should only make the clippaths it needs and delete others', function(done) {
1067+
expect(d3.select(gd).selectAll('.annclip').size()).toBe(2);
1068+
1069+
Plotly.relayout(gd, {'annotations[0].visible': false})
1070+
.then(function() {
1071+
expect(d3.select(gd).selectAll('.annclip').size()).toBe(1);
1072+
1073+
return Plotly.relayout(gd, {'annotations[2].width': 20});
1074+
})
1075+
.then(function() {
1076+
expect(d3.select(gd).selectAll('.annclip').size()).toBe(2);
1077+
1078+
return Plotly.relayout(gd, {'annotations[1].height': null});
1079+
})
1080+
.then(function() {
1081+
expect(d3.select(gd).selectAll('.annclip').size()).toBe(1);
1082+
1083+
return Plotly.relayout(gd, {'annotations[2]': null});
1084+
})
1085+
.then(function() {
1086+
expect(d3.select(gd).selectAll('.annclip').size()).toBe(0);
1087+
})
1088+
.catch(failTest)
1089+
.then(done);
1090+
});
1091+
});

0 commit comments

Comments
 (0)
Please sign in to comment.