diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 57541162afa..4cc6d19e802 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1270,14 +1270,17 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) { name = plainText(d.name, d.nameLength); } + var h0 = hovermode.charAt(0); + var h1 = h0 === 'x' ? 'y' : 'x'; + if(d.zLabel !== undefined) { if(d.xLabel !== undefined) text += 'x: ' + d.xLabel + '
'; if(d.yLabel !== undefined) text += 'y: ' + d.yLabel + '
'; if(d.trace.type !== 'choropleth' && d.trace.type !== 'choroplethmapbox') { text += (text ? 'z: ' : '') + d.zLabel; } - } else if(showCommonLabel && d[hovermode.charAt(0) + 'Label'] === t0) { - text = d[(hovermode.charAt(0) === 'x' ? 'y' : 'x') + 'Label'] || ''; + } else if(showCommonLabel && d[h0 + 'Label'] === t0) { + text = d[h1 + 'Label'] || ''; } else if(d.xLabel === undefined) { if(d.yLabel !== undefined && d.trace.type !== 'scattercarpet') { text = d.yLabel; @@ -1306,16 +1309,20 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) { } // hovertemplate - var d3locale = fullLayout._d3locale; var hovertemplate = d.hovertemplate || false; - var hovertemplateLabels = d.hovertemplateLabels || d; - var eventData = d.eventData[0] || {}; if(hovertemplate) { + var labels = d.hovertemplateLabels || d; + + if(d[h0 + 'Label'] !== t0) { + labels[h0 + 'other'] = labels[h0 + 'Val']; + labels[h0 + 'otherLabel'] = labels[h0 + 'Label']; + } + text = Lib.hovertemplateString( hovertemplate, - hovertemplateLabels, - d3locale, - eventData, + labels, + fullLayout._d3locale, + d.eventData[0] || {}, d.trace._meta ); diff --git a/src/lib/index.js b/src/lib/index.js index e696a2068ab..3aa60885db9 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -1043,21 +1043,50 @@ function templateFormatString(string, labels, d3locale) { // just in case it speeds things up *slightly*: var getterCache = {}; - return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, key, format) { - var obj, value, i; - for(i = 3; i < args.length; i++) { - obj = args[i]; - if(!obj) continue; - if(obj.hasOwnProperty(key)) { - value = obj[key]; - break; - } + return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, rawKey, format) { + var isOther = + rawKey === 'xother' || + rawKey === 'yother'; + + var isSpaceOther = + rawKey === '_xother' || + rawKey === '_yother'; + + var isSpaceOtherSpace = + rawKey === '_xother_' || + rawKey === '_yother_'; + + var isOtherSpace = + rawKey === 'xother_' || + rawKey === 'yother_'; - if(!SIMPLE_PROPERTY_REGEX.test(key)) { - value = getterCache[key] || lib.nestedProperty(obj, key).get(); - if(value) getterCache[key] = value; + var hasOther = isOther || isSpaceOther || isOtherSpace || isSpaceOtherSpace; + + var key = rawKey; + if(isSpaceOther || isSpaceOtherSpace) key = key.substring(1); + if(isOtherSpace || isSpaceOtherSpace) key = key.substring(0, key.length - 1); + + var value; + if(hasOther) { + value = labels[key]; + if(value === undefined) return ''; + } else { + var obj, i; + for(i = 3; i < args.length; i++) { + obj = args[i]; + if(!obj) continue; + if(obj.hasOwnProperty(key)) { + value = obj[key]; + break; + } + + if(!SIMPLE_PROPERTY_REGEX.test(key)) { + value = lib.nestedProperty(obj, key).get(); + value = getterCache[key] || lib.nestedProperty(obj, key).get(); + if(value) getterCache[key] = value; + } + if(value !== undefined) break; } - if(value !== undefined) break; } if(value === undefined && opts) { @@ -1087,8 +1116,16 @@ function templateFormatString(string, labels, d3locale) { value = lib.formatDate(ms, format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''), false, fmt); } } else { - if(labels.hasOwnProperty(key + 'Label')) value = labels[key + 'Label']; + var keyLabel = key + 'Label'; + if(labels.hasOwnProperty(keyLabel)) value = labels[keyLabel]; } + + if(hasOther) { + value = '(' + value + ')'; + if(isSpaceOther || isSpaceOtherSpace) value = ' ' + value; + if(isOtherSpace || isSpaceOtherSpace) value = value + ' '; + } + return value; }); } diff --git a/src/plots/template_attributes.js b/src/plots/template_attributes.js index 70a0e536d87..1fb3596110e 100644 --- a/src/plots/template_attributes.js +++ b/src/plots/template_attributes.js @@ -4,15 +4,24 @@ var docs = require('../constants/docs'); var FORMAT_LINK = docs.FORMAT_LINK; var DATE_FORMAT_LINK = docs.DATE_FORMAT_LINK; -var templateFormatStringDescription = [ - 'Variables are inserted using %{variable}, for example "y: %{y}".', - 'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".', - FORMAT_LINK, - 'for details on the formatting syntax.', - 'Dates are formatted using d3-time-format\'s syntax %{variable|d3-time-format}, for example "Day: %{2019-01-01|%A}".', - DATE_FORMAT_LINK, - 'for details on the date formatting syntax.' -].join(' '); +function templateFormatStringDescription(opts) { + var supportOther = opts && opts.supportOther; + + return [ + 'Variables are inserted using %{variable},', + 'for example "y: %{y}"' + ( + supportOther ? + ' as well as %{xother}, {%_xother}, {%_xother_}, {%xother_}. When showing info for several points, *xother* will be added to those with different x positions from the first point. An underscore before or after *(x|y)other* will add a space on that side, only when this field is shown.' : + '.' + ), + 'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".', + FORMAT_LINK, + 'for details on the formatting syntax.', + 'Dates are formatted using d3-time-format\'s syntax %{variable|d3-time-format}, for example "Day: %{2019-01-01|%A}".', + DATE_FORMAT_LINK, + 'for details on the date formatting syntax.' + ].join(' '); +} function describeVariables(extra) { var descPart = extra.description ? ' ' + extra.description : ''; @@ -45,7 +54,7 @@ exports.hovertemplateAttrs = function(opts, extra) { description: [ 'Template string used for rendering the information that appear on hover box.', 'Note that this will override `hoverinfo`.', - templateFormatStringDescription, + templateFormatStringDescription({supportOther: true}), 'The variables available in `hovertemplate` are the ones emitted as event data described at this link https://plotly.com/javascript/plotlyjs-events/#event-data.', 'Additionally, every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.', descPart, @@ -74,7 +83,7 @@ exports.texttemplateAttrs = function(opts, extra) { description: [ 'Template string used for rendering the information text that appear on points.', 'Note that this will override `textinfo`.', - templateFormatStringDescription, + templateFormatStringDescription(), 'Every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.', descPart ].join(' ') diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 9ea0ac24305..f804dc6e367 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -4831,6 +4831,76 @@ describe('hovermode: (x|y)unified', function() { .then(done, done.fail); }); + it('should format differing position using *xother* `hovertemplate` and in respect to `xhoverformat`', function(done) { + Plotly.newPlot(gd, [{ + type: 'bar', + hovertemplate: 'y(_x):%{y}%{_xother:.2f}', + x: [0, 1.001], + y: [2, 1] + }, { + x: [0, 0.749], + y: [1, 2] + }, { + hovertemplate: '(x)y:%{xother}%{y}', + xhoverformat: '.1f', + x: [0, 1.251], + y: [2, 3] + }, { + hovertemplate: '(x_)y:%{xother_}%{y}', + xhoverformat: '.2f', + x: [0, 1.351], + y: [3, 4] + }, { + hovertemplate: '(_x_)y:%{_xother_}%{y}', + xhoverformat: '.3f', + x: [0, 1.451], + y: [4, 5] + }], { + hoverdistance: -1, + hovermode: 'x unified', + showlegend: false, + width: 500, + height: 500, + margin: { + t: 50, + b: 50, + l: 50, + r: 50 + } + }) + .then(function() { + _hover(gd, { xpx: 100, ypx: 200 }); + assertLabel({title: '0.000', items: [ + 'trace 0 : y(_x):2 (0.00)', + 'trace 1 : (0, 1)', + 'trace 2 : (x)y:(0.0)2', + 'trace 3 : (x_)y:(0.00) 3', + 'trace 4 : (_x_)y:4', + ]}); + }) + .then(function() { + _hover(gd, { xpx: 250, ypx: 200 }); + assertLabel({title: '0.749', items: [ + 'trace 0 : y(_x):1 (1.00)', + 'trace 1 : 2', + 'trace 2 : (x)y:(1.3)3', + 'trace 3 : (x_)y:(1.35) 4', + 'trace 4 : (_x_)y: (1.451) 5', + ]}); + }) + .then(function() { + _hover(gd, { xpx: 350, ypx: 200 }); + assertLabel({title: '1.35', items: [ + 'trace 0 : y(_x):1 (1.00)', + 'trace 1 : (0.749, 2)', + 'trace 2 : (x)y:(1.3)3', + 'trace 3 : (x_)y:4', + 'trace 4 : (_x_)y: (1.451) 5', + ]}); + }) + .then(done, done.fail); + }); + it('should display hover for two high-res scatter at different various intervals', function(done) { var x1 = []; var y1 = [];