Skip to content

Commit 18e512c

Browse files
authored
Merge pull request #5690 from plotly/other-hovertemplate
Implement (x|y)other hovertemplate to format differing positions in compare and unified modes
2 parents c07a8e0 + a6da451 commit 18e512c

File tree

4 files changed

+156
-33
lines changed

4 files changed

+156
-33
lines changed

src/components/fx/hover.js

+15-8
Original file line numberDiff line numberDiff line change
@@ -1270,14 +1270,17 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) {
12701270
name = plainText(d.name, d.nameLength);
12711271
}
12721272

1273+
var h0 = hovermode.charAt(0);
1274+
var h1 = h0 === 'x' ? 'y' : 'x';
1275+
12731276
if(d.zLabel !== undefined) {
12741277
if(d.xLabel !== undefined) text += 'x: ' + d.xLabel + '<br>';
12751278
if(d.yLabel !== undefined) text += 'y: ' + d.yLabel + '<br>';
12761279
if(d.trace.type !== 'choropleth' && d.trace.type !== 'choroplethmapbox') {
12771280
text += (text ? 'z: ' : '') + d.zLabel;
12781281
}
1279-
} else if(showCommonLabel && d[hovermode.charAt(0) + 'Label'] === t0) {
1280-
text = d[(hovermode.charAt(0) === 'x' ? 'y' : 'x') + 'Label'] || '';
1282+
} else if(showCommonLabel && d[h0 + 'Label'] === t0) {
1283+
text = d[h1 + 'Label'] || '';
12811284
} else if(d.xLabel === undefined) {
12821285
if(d.yLabel !== undefined && d.trace.type !== 'scattercarpet') {
12831286
text = d.yLabel;
@@ -1306,16 +1309,20 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) {
13061309
}
13071310

13081311
// hovertemplate
1309-
var d3locale = fullLayout._d3locale;
13101312
var hovertemplate = d.hovertemplate || false;
1311-
var hovertemplateLabels = d.hovertemplateLabels || d;
1312-
var eventData = d.eventData[0] || {};
13131313
if(hovertemplate) {
1314+
var labels = d.hovertemplateLabels || d;
1315+
1316+
if(d[h0 + 'Label'] !== t0) {
1317+
labels[h0 + 'other'] = labels[h0 + 'Val'];
1318+
labels[h0 + 'otherLabel'] = labels[h0 + 'Label'];
1319+
}
1320+
13141321
text = Lib.hovertemplateString(
13151322
hovertemplate,
1316-
hovertemplateLabels,
1317-
d3locale,
1318-
eventData,
1323+
labels,
1324+
fullLayout._d3locale,
1325+
d.eventData[0] || {},
13191326
d.trace._meta
13201327
);
13211328

src/lib/index.js

+51-14
Original file line numberDiff line numberDiff line change
@@ -1043,21 +1043,50 @@ function templateFormatString(string, labels, d3locale) {
10431043
// just in case it speeds things up *slightly*:
10441044
var getterCache = {};
10451045

1046-
return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, key, format) {
1047-
var obj, value, i;
1048-
for(i = 3; i < args.length; i++) {
1049-
obj = args[i];
1050-
if(!obj) continue;
1051-
if(obj.hasOwnProperty(key)) {
1052-
value = obj[key];
1053-
break;
1054-
}
1046+
return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, rawKey, format) {
1047+
var isOther =
1048+
rawKey === 'xother' ||
1049+
rawKey === 'yother';
1050+
1051+
var isSpaceOther =
1052+
rawKey === '_xother' ||
1053+
rawKey === '_yother';
1054+
1055+
var isSpaceOtherSpace =
1056+
rawKey === '_xother_' ||
1057+
rawKey === '_yother_';
1058+
1059+
var isOtherSpace =
1060+
rawKey === 'xother_' ||
1061+
rawKey === 'yother_';
10551062

1056-
if(!SIMPLE_PROPERTY_REGEX.test(key)) {
1057-
value = getterCache[key] || lib.nestedProperty(obj, key).get();
1058-
if(value) getterCache[key] = value;
1063+
var hasOther = isOther || isSpaceOther || isOtherSpace || isSpaceOtherSpace;
1064+
1065+
var key = rawKey;
1066+
if(isSpaceOther || isSpaceOtherSpace) key = key.substring(1);
1067+
if(isOtherSpace || isSpaceOtherSpace) key = key.substring(0, key.length - 1);
1068+
1069+
var value;
1070+
if(hasOther) {
1071+
value = labels[key];
1072+
if(value === undefined) return '';
1073+
} else {
1074+
var obj, i;
1075+
for(i = 3; i < args.length; i++) {
1076+
obj = args[i];
1077+
if(!obj) continue;
1078+
if(obj.hasOwnProperty(key)) {
1079+
value = obj[key];
1080+
break;
1081+
}
1082+
1083+
if(!SIMPLE_PROPERTY_REGEX.test(key)) {
1084+
value = lib.nestedProperty(obj, key).get();
1085+
value = getterCache[key] || lib.nestedProperty(obj, key).get();
1086+
if(value) getterCache[key] = value;
1087+
}
1088+
if(value !== undefined) break;
10591089
}
1060-
if(value !== undefined) break;
10611090
}
10621091

10631092
if(value === undefined && opts) {
@@ -1087,8 +1116,16 @@ function templateFormatString(string, labels, d3locale) {
10871116
value = lib.formatDate(ms, format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''), false, fmt);
10881117
}
10891118
} else {
1090-
if(labels.hasOwnProperty(key + 'Label')) value = labels[key + 'Label'];
1119+
var keyLabel = key + 'Label';
1120+
if(labels.hasOwnProperty(keyLabel)) value = labels[keyLabel];
10911121
}
1122+
1123+
if(hasOther) {
1124+
value = '(' + value + ')';
1125+
if(isSpaceOther || isSpaceOtherSpace) value = ' ' + value;
1126+
if(isOtherSpace || isSpaceOtherSpace) value = value + ' ';
1127+
}
1128+
10921129
return value;
10931130
});
10941131
}

src/plots/template_attributes.js

+20-11
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@ var docs = require('../constants/docs');
44
var FORMAT_LINK = docs.FORMAT_LINK;
55
var DATE_FORMAT_LINK = docs.DATE_FORMAT_LINK;
66

7-
var templateFormatStringDescription = [
8-
'Variables are inserted using %{variable}, for example "y: %{y}".',
9-
'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".',
10-
FORMAT_LINK,
11-
'for details on the formatting syntax.',
12-
'Dates are formatted using d3-time-format\'s syntax %{variable|d3-time-format}, for example "Day: %{2019-01-01|%A}".',
13-
DATE_FORMAT_LINK,
14-
'for details on the date formatting syntax.'
15-
].join(' ');
7+
function templateFormatStringDescription(opts) {
8+
var supportOther = opts && opts.supportOther;
9+
10+
return [
11+
'Variables are inserted using %{variable},',
12+
'for example "y: %{y}"' + (
13+
supportOther ?
14+
' 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.' :
15+
'.'
16+
),
17+
'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".',
18+
FORMAT_LINK,
19+
'for details on the formatting syntax.',
20+
'Dates are formatted using d3-time-format\'s syntax %{variable|d3-time-format}, for example "Day: %{2019-01-01|%A}".',
21+
DATE_FORMAT_LINK,
22+
'for details on the date formatting syntax.'
23+
].join(' ');
24+
}
1625

1726
function describeVariables(extra) {
1827
var descPart = extra.description ? ' ' + extra.description : '';
@@ -45,7 +54,7 @@ exports.hovertemplateAttrs = function(opts, extra) {
4554
description: [
4655
'Template string used for rendering the information that appear on hover box.',
4756
'Note that this will override `hoverinfo`.',
48-
templateFormatStringDescription,
57+
templateFormatStringDescription({supportOther: true}),
4958
'The variables available in `hovertemplate` are the ones emitted as event data described at this link https://plotly.com/javascript/plotlyjs-events/#event-data.',
5059
'Additionally, every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.',
5160
descPart,
@@ -74,7 +83,7 @@ exports.texttemplateAttrs = function(opts, extra) {
7483
description: [
7584
'Template string used for rendering the information text that appear on points.',
7685
'Note that this will override `textinfo`.',
77-
templateFormatStringDescription,
86+
templateFormatStringDescription(),
7887
'Every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.',
7988
descPart
8089
].join(' ')

test/jasmine/tests/hover_label_test.js

+70
Original file line numberDiff line numberDiff line change
@@ -4831,6 +4831,76 @@ describe('hovermode: (x|y)unified', function() {
48314831
.then(done, done.fail);
48324832
});
48334833

4834+
it('should format differing position using *xother* `hovertemplate` and in respect to `xhoverformat`', function(done) {
4835+
Plotly.newPlot(gd, [{
4836+
type: 'bar',
4837+
hovertemplate: 'y(_x):%{y}%{_xother:.2f}',
4838+
x: [0, 1.001],
4839+
y: [2, 1]
4840+
}, {
4841+
x: [0, 0.749],
4842+
y: [1, 2]
4843+
}, {
4844+
hovertemplate: '(x)y:%{xother}%{y}',
4845+
xhoverformat: '.1f',
4846+
x: [0, 1.251],
4847+
y: [2, 3]
4848+
}, {
4849+
hovertemplate: '(x_)y:%{xother_}%{y}',
4850+
xhoverformat: '.2f',
4851+
x: [0, 1.351],
4852+
y: [3, 4]
4853+
}, {
4854+
hovertemplate: '(_x_)y:%{_xother_}%{y}',
4855+
xhoverformat: '.3f',
4856+
x: [0, 1.451],
4857+
y: [4, 5]
4858+
}], {
4859+
hoverdistance: -1,
4860+
hovermode: 'x unified',
4861+
showlegend: false,
4862+
width: 500,
4863+
height: 500,
4864+
margin: {
4865+
t: 50,
4866+
b: 50,
4867+
l: 50,
4868+
r: 50
4869+
}
4870+
})
4871+
.then(function() {
4872+
_hover(gd, { xpx: 100, ypx: 200 });
4873+
assertLabel({title: '0.000', items: [
4874+
'trace 0 : y(_x):2 (0.00)',
4875+
'trace 1 : (0, 1)',
4876+
'trace 2 : (x)y:(0.0)2',
4877+
'trace 3 : (x_)y:(0.00) 3',
4878+
'trace 4 : (_x_)y:4',
4879+
]});
4880+
})
4881+
.then(function() {
4882+
_hover(gd, { xpx: 250, ypx: 200 });
4883+
assertLabel({title: '0.749', items: [
4884+
'trace 0 : y(_x):1 (1.00)',
4885+
'trace 1 : 2',
4886+
'trace 2 : (x)y:(1.3)3',
4887+
'trace 3 : (x_)y:(1.35) 4',
4888+
'trace 4 : (_x_)y: (1.451) 5',
4889+
]});
4890+
})
4891+
.then(function() {
4892+
_hover(gd, { xpx: 350, ypx: 200 });
4893+
assertLabel({title: '1.35', items: [
4894+
'trace 0 : y(_x):1 (1.00)',
4895+
'trace 1 : (0.749, 2)',
4896+
'trace 2 : (x)y:(1.3)3',
4897+
'trace 3 : (x_)y:4',
4898+
'trace 4 : (_x_)y: (1.451) 5',
4899+
]});
4900+
})
4901+
.then(done, done.fail);
4902+
});
4903+
48344904
it('should display hover for two high-res scatter at different various intervals', function(done) {
48354905
var x1 = [];
48364906
var y1 = [];

0 commit comments

Comments
 (0)