diff --git a/src/components/fx/attributes.js b/src/components/fx/attributes.js index 02ac2484487..90e33a82f1c 100644 --- a/src/components/fx/attributes.js +++ b/src/components/fx/attributes.js @@ -9,49 +9,26 @@ 'use strict'; var fontAttrs = require('../../plots/font_attributes'); +var hoverLabelAttrs = require('./layout_attributes').hoverlabel; +var extendFlat = require('../../lib/extend').extendFlat; module.exports = { hoverlabel: { - bgcolor: { - valType: 'color', - role: 'style', + bgcolor: extendFlat({}, hoverLabelAttrs.bgcolor, { arrayOk: true, - editType: 'none', - description: [ - 'Sets the background color of the hover labels for this trace' - ].join(' ') - }, - bordercolor: { - valType: 'color', - role: 'style', + description: 'Sets the background color of the hover labels for this trace' + }), + bordercolor: extendFlat({}, hoverLabelAttrs.bordercolor, { arrayOk: true, - editType: 'none', - description: [ - 'Sets the border color of the hover labels for this trace.' - ].join(' ') - }, + description: 'Sets the border color of the hover labels for this trace.' + }), font: fontAttrs({ arrayOk: true, editType: 'none', description: 'Sets the font used in hover labels.' }), - namelength: { - valType: 'integer', - min: -1, - dflt: 15, - arrayOk: true, - role: 'style', - editType: 'none', - description: [ - 'Sets the length (in number of characters) of the trace name in', - 'the hover labels for this trace. -1 shows the whole name', - 'regardless of length. 0-3 shows the first 0-3 characters, and', - 'an integer >3 will show the whole name if it is less than that', - 'many characters, but if it is longer, will truncate to', - '`namelength - 3` characters and add an ellipsis.', - 'Note that when `hovertemplate` is set, `namelength` defaults to *-1*.' - ].join(' ') - }, - editType: 'calc' + align: extendFlat({}, hoverLabelAttrs.align, {arrayOk: true}), + namelength: extendFlat({}, hoverLabelAttrs.namelength, {arrayOk: true}), + editType: 'none' } }; diff --git a/src/components/fx/calc.js b/src/components/fx/calc.js index 04e7a58cfb7..4d397764e4f 100644 --- a/src/components/fx/calc.js +++ b/src/components/fx/calc.js @@ -44,6 +44,7 @@ module.exports = function calc(gd) { fillFn(trace.hoverlabel.font.color, cd, 'htc'); fillFn(trace.hoverlabel.font.family, cd, 'htf'); fillFn(trace.hoverlabel.namelength, cd, 'hnl'); + fillFn(trace.hoverlabel.align, cd, 'hta'); } }; diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 203cb6bb674..ea96bdf8398 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -125,6 +125,7 @@ exports.loneHover = function loneHover(hoverItem, opts) { fontSize: hoverItem.fontSize, fontColor: hoverItem.fontColor, nameLength: hoverItem.nameLength, + textAlign: hoverItem.textAlign, // filler to make createHoverText happy trace: hoverItem.trace || { @@ -182,6 +183,7 @@ exports.multiHovers = function multiHovers(hoverItems, opts) { fontSize: hoverItem.fontSize, fontColor: hoverItem.fontColor, nameLength: hoverItem.nameLength, + textAlign: hoverItem.textAlign, // filler to make createHoverText happy trace: hoverItem.trace || { @@ -1281,20 +1283,18 @@ function alignHoverText(hoverLabels, rotateLabels) { // box around it hoverLabels.each(function(d) { var g = d3.select(this); - if(d.del) { - g.remove(); - return; - } + if(d.del) return g.remove(); - var horzSign = d.anchor === 'end' ? -1 : 1; var tx = g.select('text.nums'); - var alignShift = {start: 1, end: -1, middle: 0}[d.anchor]; + var anchor = d.anchor; + var horzSign = anchor === 'end' ? -1 : 1; + var alignShift = {start: 1, end: -1, middle: 0}[anchor]; var txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD); var tx2x = txx + alignShift * (d.txwidth + HOVERTEXTPAD); var offsetX = 0; var offsetY = d.offset; - if(d.anchor === 'middle') { + if(anchor === 'middle') { txx -= d.tx2width / 2; tx2x += d.txwidth / 2 + HOVERTEXTPAD; } @@ -1303,7 +1303,7 @@ function alignHoverText(hoverLabels, rotateLabels) { offsetX = d.offset * YSHIFTX; } - g.select('path').attr('d', d.anchor === 'middle' ? + g.select('path').attr('d', anchor === 'middle' ? // middle aligned: rect centered on data ('M-' + (d.bx / 2 + d.tx2width / 2) + ',' + (offsetY - d.by / 2) + 'h' + d.bx + 'v' + d.by + 'h-' + d.bx + 'Z') : @@ -1316,8 +1316,21 @@ function alignHoverText(hoverLabels, rotateLabels) { 'V' + (offsetY - HOVERARROWSIZE) + 'Z')); - tx.call(svgTextUtils.positionText, - txx + offsetX, offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD); + var posX = txx + offsetX; + var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD; + var textAlign = d.textAlign || 'auto'; + + if(textAlign !== 'auto') { + if(textAlign === 'left' && anchor !== 'start') { + tx.attr('text-anchor', 'start'); + posX = -d.bx - HOVERTEXTPAD; + } else if(textAlign === 'right' && anchor !== 'end') { + tx.attr('text-anchor', 'end'); + posX = d.bx + HOVERTEXTPAD; + } + } + + tx.call(svgTextUtils.positionText, posX, posY); if(d.tx2width) { g.select('text.name') @@ -1364,6 +1377,7 @@ function cleanPoint(d, hovermode) { fill('fontSize', 'hts', 'hoverlabel.font.size'); fill('fontColor', 'htc', 'hoverlabel.font.color'); fill('nameLength', 'hnl', 'hoverlabel.namelength'); + fill('textAlign', 'hta', 'hoverlabel.align'); d.posref = (hovermode === 'y' || (hovermode === 'closest' && trace.orientation === 'h')) ? (d.xa._offset + (d.x0 + d.x1) / 2) : diff --git a/src/components/fx/hoverlabel_defaults.js b/src/components/fx/hoverlabel_defaults.js index f4c42943a20..cc6d868633b 100644 --- a/src/components/fx/hoverlabel_defaults.js +++ b/src/components/fx/hoverlabel_defaults.js @@ -17,4 +17,5 @@ module.exports = function handleHoverLabelDefaults(contIn, contOut, coerce, opts coerce('hoverlabel.bordercolor', opts.bordercolor); coerce('hoverlabel.namelength', opts.namelength); Lib.coerceFont(coerce, 'hoverlabel.font', opts.font); + coerce('hoverlabel.align', opts.align); }; diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js index 219e8d6d5c2..01feb9d21bc 100644 --- a/src/components/fx/layout_attributes.js +++ b/src/components/fx/layout_attributes.js @@ -117,6 +117,17 @@ module.exports = { ].join(' ') }, font: fontAttrs, + align: { + valType: 'enumerated', + values: ['left', 'right', 'auto'], + dflt: 'auto', + role: 'style', + editType: 'none', + description: [ + 'Sets the horizontal alignment of the text content within hover label box.', + 'Has an effect only if the hover label text spans more two or more lines' + ].join(' ') + }, namelength: { valType: 'integer', min: -1, diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 5a25c5cbaa4..475a67eac70 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -687,7 +687,8 @@ proto.draw = function() { fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'), fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'), fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color'), - nameLength: Fx.castHoverOption(trace, ptNumber, 'namelength') + nameLength: Fx.castHoverOption(trace, ptNumber, 'namelength'), + textAlign: Fx.castHoverOption(trace, ptNumber, 'align') }, { container: this.svgContainer, gd: this.graphDiv diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 951f5851bef..972b28ccc71 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -170,6 +170,7 @@ function render(scene) { fontSize: Fx.castHoverOption(traceNow, ptNumber, 'font.size'), fontColor: Fx.castHoverOption(traceNow, ptNumber, 'font.color'), nameLength: Fx.castHoverOption(traceNow, ptNumber, 'namelength'), + textAlign: Fx.castHoverOption(traceNow, ptNumber, 'align'), hovertemplate: Lib.castOption(traceNow, ptNumber, 'hovertemplate'), hovertemplateLabels: Lib.extendFlat({}, pointData, labels), eventData: [pointData] diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index b4601220c70..7dd3929b157 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -380,6 +380,7 @@ function attachFxHandlers(sliceTop, gd, cd) { fontSize: helpers.castOption(hoverFont.size, pt.pts), fontColor: helpers.castOption(hoverFont.color, pt.pts), nameLength: helpers.castOption(hoverLabel.namelength, pt.pts), + textAlign: helpers.castOption(hoverLabel.align, pt.pts), hovertemplate: helpers.castOption(trace2.hovertemplate, pt.pts), hovertemplateLabels: pt, eventData: [eventData(pt, trace2)] diff --git a/src/traces/sankey/plot.js b/src/traces/sankey/plot.js index 769f6942ef7..739378db248 100644 --- a/src/traces/sankey/plot.js +++ b/src/traces/sankey/plot.js @@ -212,6 +212,7 @@ module.exports = function plot(gd, calcData) { fontSize: castHoverOption(obj, 'font.size'), fontColor: castHoverOption(obj, 'font.color'), nameLength: castHoverOption(obj, 'namelength'), + textAlign: castHoverOption(obj, 'align'), idealAlign: d3.event.x < hoverCenter[0] ? 'right' : 'left', hovertemplate: obj.hovertemplate, @@ -301,6 +302,7 @@ module.exports = function plot(gd, calcData) { fontSize: castHoverOption(obj, 'font.size'), fontColor: castHoverOption(obj, 'font.color'), nameLength: castHoverOption(obj, 'namelength'), + textAlign: castHoverOption(obj, 'align'), idealAlign: 'left', hovertemplate: obj.hovertemplate, diff --git a/src/traces/sunburst/plot.js b/src/traces/sunburst/plot.js index c453ed0d3e9..67742e89fc8 100644 --- a/src/traces/sunburst/plot.js +++ b/src/traces/sunburst/plot.js @@ -594,6 +594,7 @@ function attachFxHandlers(sliceTop, gd, cd) { fontSize: _cast('hoverlabel.font.size'), fontColor: _cast('hoverlabel.font.color'), nameLength: _cast('hoverlabel.namelength'), + textAlign: _cast('hoverlabel.align'), hovertemplate: hovertemplate, hovertemplateLabels: hoverPt, eventData: [makeEventData(pt, traceNow)] diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js index fbdf6ad0b97..601d70bb073 100644 --- a/test/jasmine/tests/fx_test.js +++ b/test/jasmine/tests/fx_test.js @@ -171,6 +171,7 @@ describe('Fx defaults', function() { size: 40, color: 'pink' }, + align: 'auto', namelength: 15 }); @@ -182,6 +183,7 @@ describe('Fx defaults', function() { size: 20, color: 'red' }, + align: 'auto', namelength: 15 }); diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 808b84bc882..747bb06f5f1 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -2034,6 +2034,54 @@ describe('hover info', function() { .catch(failTest) .then(done); }); + + it('should honor *hoverlabel.align', function(done) { + var gd = createGraphDiv(); + + function _assert(msg, exp) { + var tx = d3.select('g.hovertext').select('text'); + expect(tx.attr('text-anchor')).toBe(exp.textAnchor, 'text anchor|' + msg); + expect(Number(tx.attr('x'))).toBeWithin(exp.posX, 3, 'x position|' + msg); + } + + Plotly.plot(gd, [{ + y: [1, 2, 1], + text: 'LONG TEXT' + }], { + xaxis: {range: [0, 2]}, + margin: {l: 0, t: 0, b: 0, r: 0}, + hovermode: 'closest', + width: 400, + height: 400 + }) + .then(function() { _hoverNatural(gd, 0, 395); }) + .then(function() { _assert('base left pt', {textAnchor: 'start', posX: 9}); }) + .then(function() { _hoverNatural(gd, 395, 395); }) + .then(function() { _assert('base right pt', {textAnchor: 'end', posX: -9}); }) + .then(function() { + return Plotly.relayout(gd, 'hoverlabel.align', 'left'); + }) + .then(function() { _hoverNatural(gd, 0, 395); }) + .then(function() { _assert('align:left left pt', {textAnchor: 'start', posX: 9}); }) + .then(function() { _hoverNatural(gd, 395, 395); }) + .then(function() { _assert('align:left right pt', {textAnchor: 'start', posX: -84.73}); }) + .then(function() { + return Plotly.restyle(gd, 'hoverlabel.align', 'right'); + }) + .then(function() { _hoverNatural(gd, 0, 395); }) + .then(function() { _assert('align:right left pt', {textAnchor: 'end', posX: 84.73}); }) + .then(function() { _hoverNatural(gd, 395, 395); }) + .then(function() { _assert('align:right right pt', {textAnchor: 'end', posX: -9}); }) + .then(function() { + return Plotly.restyle(gd, 'hoverlabel.align', [['right', 'auto', 'left']]); + }) + .then(function() { _hoverNatural(gd, 0, 395); }) + .then(function() { _assert('arrayOk align:right left pt', {textAnchor: 'end', posX: 84.73}); }) + .then(function() { _hoverNatural(gd, 395, 395); }) + .then(function() { _assert('arrayOk align:left right pt', {textAnchor: 'start', posX: -84.73}); }) + .catch(failTest) + .then(done); + }); }); describe('hover info on stacked subplots', function() { diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js index d8147d8a82e..7f122c28931 100644 --- a/test/jasmine/tests/pie_test.js +++ b/test/jasmine/tests/pie_test.js @@ -1187,6 +1187,61 @@ describe('pie hovering', function() { .catch(failTest) .then(done); }); + + it('should honor *hoverlabel.align*', function(done) { + function _assert(msg, exp) { + var tx = d3.select('g.hovertext').select('text'); + expect(tx.attr('text-anchor')).toBe(exp.textAnchor, 'text anchor|' + msg); + expect(Number(tx.attr('x'))).toBeWithin(exp.posX, 3, 'x position|' + msg); + } + + function _hoverLeft() { + mouseEvent('mouseover', 100, 200); + Lib.clearThrottle(); + } + + function _hoverRight() { + mouseEvent('mouseover', 300, 200); + Lib.clearThrottle(); + } + + Plotly.plot(gd, [{ + type: 'pie', + labels: ['a', 'b'] + }], { + showlegend: false, + margin: {l: 0, t: 0, b: 0, r: 0}, + width: 400, + height: 400 + }) + .then(_hoverLeft) + .then(function() { _assert('base left sector', {textAnchor: 'start', posX: 9}); }) + .then(_hoverRight) + .then(function() { _assert('base right sector', {textAnchor: 'end', posX: -9}); }) + .then(function() { + return Plotly.relayout(gd, 'hoverlabel.align', 'left'); + }) + .then(_hoverLeft) + .then(function() { _assert('align:left left sector', {textAnchor: 'start', posX: 9}); }) + .then(_hoverRight) + .then(function() { _assert('align:left right sector', {textAnchor: 'start', posX: -37.45}); }) + .then(function() { + return Plotly.restyle(gd, 'hoverlabel.align', 'right'); + }) + .then(_hoverLeft) + .then(function() { _assert('align:right left sector', {textAnchor: 'end', posX: 37.45}); }) + .then(_hoverRight) + .then(function() { _assert('align:right right sector', {textAnchor: 'end', posX: -9}); }) + .then(function() { + return Plotly.restyle(gd, 'hoverlabel.align', [['left', 'right']]); + }) + .then(_hoverLeft) + .then(function() { _assert('arrayOk align:right left sector', {textAnchor: 'end', posX: 37.45}); }) + .then(_hoverRight) + .then(function() { _assert('arrayOk align:left right sector', {textAnchor: 'start', posX: -37.45}); }) + .catch(failTest) + .then(done); + }); }); describe('should fit labels within graph div', function() {