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 = [];