Skip to content

Commit 78e7cc6

Browse files
authored
Merge pull request #3753 from plotly/align-hover-text
Align hover label text content with *hoverlabel.align*
2 parents 3523464 + 307287e commit 78e7cc6

File tree

13 files changed

+160
-45
lines changed

13 files changed

+160
-45
lines changed

src/components/fx/attributes.js

+11-34
Original file line numberDiff line numberDiff line change
@@ -9,49 +9,26 @@
99
'use strict';
1010

1111
var fontAttrs = require('../../plots/font_attributes');
12+
var hoverLabelAttrs = require('./layout_attributes').hoverlabel;
13+
var extendFlat = require('../../lib/extend').extendFlat;
1214

1315
module.exports = {
1416
hoverlabel: {
15-
bgcolor: {
16-
valType: 'color',
17-
role: 'style',
17+
bgcolor: extendFlat({}, hoverLabelAttrs.bgcolor, {
1818
arrayOk: true,
19-
editType: 'none',
20-
description: [
21-
'Sets the background color of the hover labels for this trace'
22-
].join(' ')
23-
},
24-
bordercolor: {
25-
valType: 'color',
26-
role: 'style',
19+
description: 'Sets the background color of the hover labels for this trace'
20+
}),
21+
bordercolor: extendFlat({}, hoverLabelAttrs.bordercolor, {
2722
arrayOk: true,
28-
editType: 'none',
29-
description: [
30-
'Sets the border color of the hover labels for this trace.'
31-
].join(' ')
32-
},
23+
description: 'Sets the border color of the hover labels for this trace.'
24+
}),
3325
font: fontAttrs({
3426
arrayOk: true,
3527
editType: 'none',
3628
description: 'Sets the font used in hover labels.'
3729
}),
38-
namelength: {
39-
valType: 'integer',
40-
min: -1,
41-
dflt: 15,
42-
arrayOk: true,
43-
role: 'style',
44-
editType: 'none',
45-
description: [
46-
'Sets the length (in number of characters) of the trace name in',
47-
'the hover labels for this trace. -1 shows the whole name',
48-
'regardless of length. 0-3 shows the first 0-3 characters, and',
49-
'an integer >3 will show the whole name if it is less than that',
50-
'many characters, but if it is longer, will truncate to',
51-
'`namelength - 3` characters and add an ellipsis.',
52-
'Note that when `hovertemplate` is set, `namelength` defaults to *-1*.'
53-
].join(' ')
54-
},
55-
editType: 'calc'
30+
align: extendFlat({}, hoverLabelAttrs.align, {arrayOk: true}),
31+
namelength: extendFlat({}, hoverLabelAttrs.namelength, {arrayOk: true}),
32+
editType: 'none'
5633
}
5734
};

src/components/fx/calc.js

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ module.exports = function calc(gd) {
4444
fillFn(trace.hoverlabel.font.color, cd, 'htc');
4545
fillFn(trace.hoverlabel.font.family, cd, 'htf');
4646
fillFn(trace.hoverlabel.namelength, cd, 'hnl');
47+
fillFn(trace.hoverlabel.align, cd, 'hta');
4748
}
4849
};
4950

src/components/fx/hover.js

+24-10
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ exports.loneHover = function loneHover(hoverItem, opts) {
125125
fontSize: hoverItem.fontSize,
126126
fontColor: hoverItem.fontColor,
127127
nameLength: hoverItem.nameLength,
128+
textAlign: hoverItem.textAlign,
128129

129130
// filler to make createHoverText happy
130131
trace: hoverItem.trace || {
@@ -182,6 +183,7 @@ exports.multiHovers = function multiHovers(hoverItems, opts) {
182183
fontSize: hoverItem.fontSize,
183184
fontColor: hoverItem.fontColor,
184185
nameLength: hoverItem.nameLength,
186+
textAlign: hoverItem.textAlign,
185187

186188
// filler to make createHoverText happy
187189
trace: hoverItem.trace || {
@@ -1281,20 +1283,18 @@ function alignHoverText(hoverLabels, rotateLabels) {
12811283
// box around it
12821284
hoverLabels.each(function(d) {
12831285
var g = d3.select(this);
1284-
if(d.del) {
1285-
g.remove();
1286-
return;
1287-
}
1286+
if(d.del) return g.remove();
12881287

1289-
var horzSign = d.anchor === 'end' ? -1 : 1;
12901288
var tx = g.select('text.nums');
1291-
var alignShift = {start: 1, end: -1, middle: 0}[d.anchor];
1289+
var anchor = d.anchor;
1290+
var horzSign = anchor === 'end' ? -1 : 1;
1291+
var alignShift = {start: 1, end: -1, middle: 0}[anchor];
12921292
var txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD);
12931293
var tx2x = txx + alignShift * (d.txwidth + HOVERTEXTPAD);
12941294
var offsetX = 0;
12951295
var offsetY = d.offset;
12961296

1297-
if(d.anchor === 'middle') {
1297+
if(anchor === 'middle') {
12981298
txx -= d.tx2width / 2;
12991299
tx2x += d.txwidth / 2 + HOVERTEXTPAD;
13001300
}
@@ -1303,7 +1303,7 @@ function alignHoverText(hoverLabels, rotateLabels) {
13031303
offsetX = d.offset * YSHIFTX;
13041304
}
13051305

1306-
g.select('path').attr('d', d.anchor === 'middle' ?
1306+
g.select('path').attr('d', anchor === 'middle' ?
13071307
// middle aligned: rect centered on data
13081308
('M-' + (d.bx / 2 + d.tx2width / 2) + ',' + (offsetY - d.by / 2) +
13091309
'h' + d.bx + 'v' + d.by + 'h-' + d.bx + 'Z') :
@@ -1316,8 +1316,21 @@ function alignHoverText(hoverLabels, rotateLabels) {
13161316
'V' + (offsetY - HOVERARROWSIZE) +
13171317
'Z'));
13181318

1319-
tx.call(svgTextUtils.positionText,
1320-
txx + offsetX, offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD);
1319+
var posX = txx + offsetX;
1320+
var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD;
1321+
var textAlign = d.textAlign || 'auto';
1322+
1323+
if(textAlign !== 'auto') {
1324+
if(textAlign === 'left' && anchor !== 'start') {
1325+
tx.attr('text-anchor', 'start');
1326+
posX = -d.bx - HOVERTEXTPAD;
1327+
} else if(textAlign === 'right' && anchor !== 'end') {
1328+
tx.attr('text-anchor', 'end');
1329+
posX = d.bx + HOVERTEXTPAD;
1330+
}
1331+
}
1332+
1333+
tx.call(svgTextUtils.positionText, posX, posY);
13211334

13221335
if(d.tx2width) {
13231336
g.select('text.name')
@@ -1364,6 +1377,7 @@ function cleanPoint(d, hovermode) {
13641377
fill('fontSize', 'hts', 'hoverlabel.font.size');
13651378
fill('fontColor', 'htc', 'hoverlabel.font.color');
13661379
fill('nameLength', 'hnl', 'hoverlabel.namelength');
1380+
fill('textAlign', 'hta', 'hoverlabel.align');
13671381

13681382
d.posref = (hovermode === 'y' || (hovermode === 'closest' && trace.orientation === 'h')) ?
13691383
(d.xa._offset + (d.x0 + d.x1) / 2) :

src/components/fx/hoverlabel_defaults.js

+1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ module.exports = function handleHoverLabelDefaults(contIn, contOut, coerce, opts
1717
coerce('hoverlabel.bordercolor', opts.bordercolor);
1818
coerce('hoverlabel.namelength', opts.namelength);
1919
Lib.coerceFont(coerce, 'hoverlabel.font', opts.font);
20+
coerce('hoverlabel.align', opts.align);
2021
};

src/components/fx/layout_attributes.js

+11
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,17 @@ module.exports = {
117117
].join(' ')
118118
},
119119
font: fontAttrs,
120+
align: {
121+
valType: 'enumerated',
122+
values: ['left', 'right', 'auto'],
123+
dflt: 'auto',
124+
role: 'style',
125+
editType: 'none',
126+
description: [
127+
'Sets the horizontal alignment of the text content within hover label box.',
128+
'Has an effect only if the hover label text spans more two or more lines'
129+
].join(' ')
130+
},
120131
namelength: {
121132
valType: 'integer',
122133
min: -1,

src/plots/gl2d/scene2d.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,8 @@ proto.draw = function() {
687687
fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'),
688688
fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'),
689689
fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color'),
690-
nameLength: Fx.castHoverOption(trace, ptNumber, 'namelength')
690+
nameLength: Fx.castHoverOption(trace, ptNumber, 'namelength'),
691+
textAlign: Fx.castHoverOption(trace, ptNumber, 'align')
691692
}, {
692693
container: this.svgContainer,
693694
gd: this.graphDiv

src/plots/gl3d/scene.js

+1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ function render(scene) {
170170
fontSize: Fx.castHoverOption(traceNow, ptNumber, 'font.size'),
171171
fontColor: Fx.castHoverOption(traceNow, ptNumber, 'font.color'),
172172
nameLength: Fx.castHoverOption(traceNow, ptNumber, 'namelength'),
173+
textAlign: Fx.castHoverOption(traceNow, ptNumber, 'align'),
173174
hovertemplate: Lib.castOption(traceNow, ptNumber, 'hovertemplate'),
174175
hovertemplateLabels: Lib.extendFlat({}, pointData, labels),
175176
eventData: [pointData]

src/traces/pie/plot.js

+1
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ function attachFxHandlers(sliceTop, gd, cd) {
380380
fontSize: helpers.castOption(hoverFont.size, pt.pts),
381381
fontColor: helpers.castOption(hoverFont.color, pt.pts),
382382
nameLength: helpers.castOption(hoverLabel.namelength, pt.pts),
383+
textAlign: helpers.castOption(hoverLabel.align, pt.pts),
383384
hovertemplate: helpers.castOption(trace2.hovertemplate, pt.pts),
384385
hovertemplateLabels: pt,
385386
eventData: [eventData(pt, trace2)]

src/traces/sankey/plot.js

+2
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ module.exports = function plot(gd, calcData) {
212212
fontSize: castHoverOption(obj, 'font.size'),
213213
fontColor: castHoverOption(obj, 'font.color'),
214214
nameLength: castHoverOption(obj, 'namelength'),
215+
textAlign: castHoverOption(obj, 'align'),
215216
idealAlign: d3.event.x < hoverCenter[0] ? 'right' : 'left',
216217

217218
hovertemplate: obj.hovertemplate,
@@ -301,6 +302,7 @@ module.exports = function plot(gd, calcData) {
301302
fontSize: castHoverOption(obj, 'font.size'),
302303
fontColor: castHoverOption(obj, 'font.color'),
303304
nameLength: castHoverOption(obj, 'namelength'),
305+
textAlign: castHoverOption(obj, 'align'),
304306
idealAlign: 'left',
305307

306308
hovertemplate: obj.hovertemplate,

src/traces/sunburst/plot.js

+1
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ function attachFxHandlers(sliceTop, gd, cd) {
594594
fontSize: _cast('hoverlabel.font.size'),
595595
fontColor: _cast('hoverlabel.font.color'),
596596
nameLength: _cast('hoverlabel.namelength'),
597+
textAlign: _cast('hoverlabel.align'),
597598
hovertemplate: hovertemplate,
598599
hovertemplateLabels: hoverPt,
599600
eventData: [makeEventData(pt, traceNow)]

test/jasmine/tests/fx_test.js

+2
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ describe('Fx defaults', function() {
171171
size: 40,
172172
color: 'pink'
173173
},
174+
align: 'auto',
174175
namelength: 15
175176
});
176177

@@ -182,6 +183,7 @@ describe('Fx defaults', function() {
182183
size: 20,
183184
color: 'red'
184185
},
186+
align: 'auto',
185187
namelength: 15
186188
});
187189

test/jasmine/tests/hover_label_test.js

+48
Original file line numberDiff line numberDiff line change
@@ -2034,6 +2034,54 @@ describe('hover info', function() {
20342034
.catch(failTest)
20352035
.then(done);
20362036
});
2037+
2038+
it('should honor *hoverlabel.align', function(done) {
2039+
var gd = createGraphDiv();
2040+
2041+
function _assert(msg, exp) {
2042+
var tx = d3.select('g.hovertext').select('text');
2043+
expect(tx.attr('text-anchor')).toBe(exp.textAnchor, 'text anchor|' + msg);
2044+
expect(Number(tx.attr('x'))).toBeWithin(exp.posX, 3, 'x position|' + msg);
2045+
}
2046+
2047+
Plotly.plot(gd, [{
2048+
y: [1, 2, 1],
2049+
text: 'LONG TEXT'
2050+
}], {
2051+
xaxis: {range: [0, 2]},
2052+
margin: {l: 0, t: 0, b: 0, r: 0},
2053+
hovermode: 'closest',
2054+
width: 400,
2055+
height: 400
2056+
})
2057+
.then(function() { _hoverNatural(gd, 0, 395); })
2058+
.then(function() { _assert('base left pt', {textAnchor: 'start', posX: 9}); })
2059+
.then(function() { _hoverNatural(gd, 395, 395); })
2060+
.then(function() { _assert('base right pt', {textAnchor: 'end', posX: -9}); })
2061+
.then(function() {
2062+
return Plotly.relayout(gd, 'hoverlabel.align', 'left');
2063+
})
2064+
.then(function() { _hoverNatural(gd, 0, 395); })
2065+
.then(function() { _assert('align:left left pt', {textAnchor: 'start', posX: 9}); })
2066+
.then(function() { _hoverNatural(gd, 395, 395); })
2067+
.then(function() { _assert('align:left right pt', {textAnchor: 'start', posX: -84.73}); })
2068+
.then(function() {
2069+
return Plotly.restyle(gd, 'hoverlabel.align', 'right');
2070+
})
2071+
.then(function() { _hoverNatural(gd, 0, 395); })
2072+
.then(function() { _assert('align:right left pt', {textAnchor: 'end', posX: 84.73}); })
2073+
.then(function() { _hoverNatural(gd, 395, 395); })
2074+
.then(function() { _assert('align:right right pt', {textAnchor: 'end', posX: -9}); })
2075+
.then(function() {
2076+
return Plotly.restyle(gd, 'hoverlabel.align', [['right', 'auto', 'left']]);
2077+
})
2078+
.then(function() { _hoverNatural(gd, 0, 395); })
2079+
.then(function() { _assert('arrayOk align:right left pt', {textAnchor: 'end', posX: 84.73}); })
2080+
.then(function() { _hoverNatural(gd, 395, 395); })
2081+
.then(function() { _assert('arrayOk align:left right pt', {textAnchor: 'start', posX: -84.73}); })
2082+
.catch(failTest)
2083+
.then(done);
2084+
});
20372085
});
20382086

20392087
describe('hover info on stacked subplots', function() {

test/jasmine/tests/pie_test.js

+55
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,61 @@ describe('pie hovering', function() {
11871187
.catch(failTest)
11881188
.then(done);
11891189
});
1190+
1191+
it('should honor *hoverlabel.align*', function(done) {
1192+
function _assert(msg, exp) {
1193+
var tx = d3.select('g.hovertext').select('text');
1194+
expect(tx.attr('text-anchor')).toBe(exp.textAnchor, 'text anchor|' + msg);
1195+
expect(Number(tx.attr('x'))).toBeWithin(exp.posX, 3, 'x position|' + msg);
1196+
}
1197+
1198+
function _hoverLeft() {
1199+
mouseEvent('mouseover', 100, 200);
1200+
Lib.clearThrottle();
1201+
}
1202+
1203+
function _hoverRight() {
1204+
mouseEvent('mouseover', 300, 200);
1205+
Lib.clearThrottle();
1206+
}
1207+
1208+
Plotly.plot(gd, [{
1209+
type: 'pie',
1210+
labels: ['a', 'b']
1211+
}], {
1212+
showlegend: false,
1213+
margin: {l: 0, t: 0, b: 0, r: 0},
1214+
width: 400,
1215+
height: 400
1216+
})
1217+
.then(_hoverLeft)
1218+
.then(function() { _assert('base left sector', {textAnchor: 'start', posX: 9}); })
1219+
.then(_hoverRight)
1220+
.then(function() { _assert('base right sector', {textAnchor: 'end', posX: -9}); })
1221+
.then(function() {
1222+
return Plotly.relayout(gd, 'hoverlabel.align', 'left');
1223+
})
1224+
.then(_hoverLeft)
1225+
.then(function() { _assert('align:left left sector', {textAnchor: 'start', posX: 9}); })
1226+
.then(_hoverRight)
1227+
.then(function() { _assert('align:left right sector', {textAnchor: 'start', posX: -37.45}); })
1228+
.then(function() {
1229+
return Plotly.restyle(gd, 'hoverlabel.align', 'right');
1230+
})
1231+
.then(_hoverLeft)
1232+
.then(function() { _assert('align:right left sector', {textAnchor: 'end', posX: 37.45}); })
1233+
.then(_hoverRight)
1234+
.then(function() { _assert('align:right right sector', {textAnchor: 'end', posX: -9}); })
1235+
.then(function() {
1236+
return Plotly.restyle(gd, 'hoverlabel.align', [['left', 'right']]);
1237+
})
1238+
.then(_hoverLeft)
1239+
.then(function() { _assert('arrayOk align:right left sector', {textAnchor: 'end', posX: 37.45}); })
1240+
.then(_hoverRight)
1241+
.then(function() { _assert('arrayOk align:left right sector', {textAnchor: 'start', posX: -37.45}); })
1242+
.catch(failTest)
1243+
.then(done);
1244+
});
11901245
});
11911246

11921247
describe('should fit labels within graph div', function() {

0 commit comments

Comments
 (0)