Skip to content

Commit 3a32ac0

Browse files
authored
Merge pull request #3126 from plotly/3007-hovertemplate
support template string on hover
2 parents 2ceb5c3 + a78600a commit 3a32ac0

32 files changed

+615
-30
lines changed

Diff for: src/components/fx/calc.js

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ module.exports = function calc(gd) {
3434

3535
fillFn(trace.hoverinfo, cd, 'hi', makeCoerceHoverInfo(trace));
3636

37+
if(trace.hovertemplate) fillFn(trace.hovertemplate, cd, 'ht');
38+
3739
if(!trace.hoverlabel) continue;
3840

3941
fillFn(trace.hoverlabel.bgcolor, cd, 'hbg');

Diff for: src/components/fx/hover.js

+43-12
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,17 @@ exports.loneHover = function loneHover(hoverItem, opts) {
126126
fontColor: hoverItem.fontColor,
127127

128128
// filler to make createHoverText happy
129-
trace: {
129+
trace: hoverItem.trace || {
130130
index: 0,
131131
hoverinfo: ''
132132
},
133133
xa: {_offset: 0},
134134
ya: {_offset: 0},
135-
index: 0
135+
index: 0,
136+
137+
hovertemplate: hoverItem.hovertemplate || false,
138+
eventData: hoverItem.eventData || false,
139+
hovertemplateLabels: hoverItem.hovertemplateLabels || false,
136140
};
137141

138142
var container3 = d3.select(opts.container);
@@ -146,7 +150,6 @@ exports.loneHover = function loneHover(hoverItem, opts) {
146150
container: container3,
147151
outerContainer: outerContainer3
148152
};
149-
150153
var hoverLabel = createHoverText([pointData], fullOpts, opts.gd);
151154
alignHoverText(hoverLabel, fullOpts.rotateLabels);
152155

@@ -180,13 +183,17 @@ exports.multiHovers = function multiHovers(hoverItems, opts) {
180183
fontColor: hoverItem.fontColor,
181184

182185
// filler to make createHoverText happy
183-
trace: {
186+
trace: hoverItem.trace || {
184187
index: 0,
185188
hoverinfo: ''
186189
},
187190
xa: {_offset: 0},
188191
ya: {_offset: 0},
189-
index: 0
192+
index: 0,
193+
194+
hovertemplate: hoverItem.hovertemplate || false,
195+
eventData: hoverItem.eventData || false,
196+
hovertemplateLabels: hoverItem.hovertemplateLabels || false,
190197
};
191198
});
192199

@@ -662,7 +669,14 @@ function _hover(gd, evt, subplot, noHoverEvent) {
662669
// other people and send it to the event
663670
for(itemnum = 0; itemnum < hoverData.length; itemnum++) {
664671
var pt = hoverData[itemnum];
665-
newhoverdata.push(helpers.makeEventData(pt, pt.trace, pt.cd));
672+
var eventData = helpers.makeEventData(pt, pt.trace, pt.cd);
673+
674+
var ht = false;
675+
if(pt.cd[pt.index] && pt.cd[pt.index].ht) ht = pt.cd[pt.index].ht;
676+
hoverData[itemnum].hovertemplate = ht || pt.trace.hovertemplate || false;
677+
hoverData[itemnum].eventData = [eventData];
678+
679+
newhoverdata.push(eventData);
666680
}
667681

668682
gd._hoverdata = newhoverdata;
@@ -720,6 +734,8 @@ function _hover(gd, evt, subplot, noHoverEvent) {
720734
});
721735
}
722736

737+
var EXTRA_STRING_REGEX = /<extra>([\s\S]*)<\/extra>/;
738+
723739
function createHoverText(hoverData, opts, gd) {
724740
var hovermode = opts.hovermode;
725741
var rotateLabels = opts.rotateLabels;
@@ -763,11 +779,13 @@ function createHoverText(hoverData, opts, gd) {
763779
if(allHaveZ && hoverData[i].zLabel === undefined) allHaveZ = false;
764780

765781
traceHoverinfo = hoverData[i].hoverinfo || hoverData[i].trace.hoverinfo;
766-
var parts = Array.isArray(traceHoverinfo) ? traceHoverinfo : traceHoverinfo.split('+');
767-
if(parts.indexOf('all') === -1 &&
768-
parts.indexOf(hovermode) === -1) {
769-
showCommonLabel = false;
770-
break;
782+
if(traceHoverinfo) {
783+
var parts = Array.isArray(traceHoverinfo) ? traceHoverinfo : traceHoverinfo.split('+');
784+
if(parts.indexOf('all') === -1 &&
785+
parts.indexOf(hovermode) === -1) {
786+
showCommonLabel = false;
787+
break;
788+
}
771789
}
772790
}
773791

@@ -950,6 +968,19 @@ function createHoverText(hoverData, opts, gd) {
950968
text = name;
951969
}
952970

971+
// hovertemplate
972+
var hovertemplate = d.hovertemplate || false;
973+
var hovertemplateLabels = d.hovertemplateLabels || d;
974+
var eventData = d.eventData[0] || {};
975+
if(hovertemplate) {
976+
text = Lib.hovertemplateString(hovertemplate, hovertemplateLabels, eventData);
977+
978+
text = text.replace(EXTRA_STRING_REGEX, function(match, extra) {
979+
name = extra; // Assign name for secondary text label
980+
return ''; // Remove from main text label
981+
});
982+
}
983+
953984
// main label
954985
var tx = g.select('text.nums')
955986
.call(Drawing.font,
@@ -1348,7 +1379,7 @@ function cleanPoint(d, hovermode) {
13481379

13491380
var infomode = d.hoverinfo || d.trace.hoverinfo;
13501381

1351-
if(infomode !== 'all') {
1382+
if(infomode && infomode !== 'all') {
13521383
infomode = Array.isArray(infomode) ? infomode : infomode.split('+');
13531384
if(infomode.indexOf('x') === -1) d.xLabel = undefined;
13541385
if(infomode.indexOf('y') === -1) d.yLabel = undefined;

Diff for: src/components/fx/hovertemplate_attributes.js

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Copyright 2012-2018, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = function(opts, extra) {
12+
opts = opts || {};
13+
extra = extra || {};
14+
15+
var descPart = extra.description ? ' ' + extra.description : '';
16+
var keys = extra.keys || [];
17+
if(keys.length > 0) {
18+
var quotedKeys = [];
19+
for(var i = 0; i < keys.length; i++) {
20+
quotedKeys[i] = '`' + keys[i] + '`';
21+
}
22+
descPart = descPart + 'Finally, this trace also supports ';
23+
if(keys.length === 1) {
24+
descPart = 'variable ' + quotedKeys[0];
25+
} else {
26+
descPart = 'variables ' + quotedKeys.slice(0, -1).join(', ') + ' and ' + quotedKeys.slice(-1) + '.';
27+
}
28+
}
29+
30+
var hovertemplate = {
31+
valType: 'string',
32+
role: 'info',
33+
dflt: '',
34+
arrayOk: true,
35+
editType: 'none',
36+
description: [
37+
'Template string used for rendering the information that appear on hover box.',
38+
'Note that this will override `hoverinfo`.',
39+
'Variables are inserted using %{variable}, for example "y: %{y}".',
40+
'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".',
41+
'See https://github.com/d3/d3-format/blob/master/README.md#locale_format for details on the formatting syntax.',
42+
'The variables available in `hovertemplate` are the ones emitted as event data described at this link https://plot.ly/javascript/plotlyjs-events/#event-data.',
43+
'Additionally, every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.',
44+
descPart
45+
].join(' ')
46+
};
47+
48+
return hovertemplate;
49+
};

Diff for: src/lib/index.js

+63-3
Original file line numberDiff line numberDiff line change
@@ -979,10 +979,10 @@ lib.numSeparate = function(value, separators, separatethousands) {
979979
return x1 + x2;
980980
};
981981

982-
var TEMPLATE_STRING_REGEX = /%{([^\s%{}]*)}/g;
982+
var TEMPLATE_STRING_REGEX = /%{([^\s%{}:]*)(:[^}]*)?}/g;
983983
var SIMPLE_PROPERTY_REGEX = /^\w*$/;
984984

985-
/*
985+
/**
986986
* Substitute values from an object into a string
987987
*
988988
* Examples:
@@ -994,7 +994,6 @@ var SIMPLE_PROPERTY_REGEX = /^\w*$/;
994994
*
995995
* @return {string} templated string
996996
*/
997-
998997
lib.templateString = function(string, obj) {
999998
// Not all that useful, but cache nestedProperty instantiation
1000999
// just in case it speeds things up *slightly*:
@@ -1009,6 +1008,67 @@ lib.templateString = function(string, obj) {
10091008
});
10101009
};
10111010

1011+
var TEMPLATE_STRING_FORMAT_SEPARATOR = /^:/;
1012+
var numberOfHoverTemplateWarnings = 0;
1013+
var maximumNumberOfHoverTemplateWarnings = 10;
1014+
/**
1015+
* Substitute values from an object into a string and optionally formats them using d3-format,
1016+
* or fallback to associated labels.
1017+
*
1018+
* Examples:
1019+
* Lib.templateString('name: %{trace}', {trace: 'asdf'}) --> 'name: asdf'
1020+
* Lib.templateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf'
1021+
* Lib.templateString('price: %{y:$.2f}', {y: 1}) --> 'price: $1.00'
1022+
*
1023+
* @param {string} input string containing %{...:...} template strings
1024+
* @param {obj} data object containing fallback text when no formatting is specified, ex.: {yLabel: 'formattedYValue'}
1025+
* @param {obj} data objects containing substitution values
1026+
*
1027+
* @return {string} templated string
1028+
*/
1029+
lib.hovertemplateString = function(string, labels) {
1030+
var args = arguments;
1031+
// Not all that useful, but cache nestedProperty instantiation
1032+
// just in case it speeds things up *slightly*:
1033+
var getterCache = {};
1034+
1035+
return string.replace(TEMPLATE_STRING_REGEX, function(match, key, format) {
1036+
var obj, value, i;
1037+
for(i = 2; i < args.length; i++) {
1038+
obj = args[i];
1039+
if(obj.hasOwnProperty(key)) {
1040+
value = obj[key];
1041+
break;
1042+
}
1043+
1044+
if(!SIMPLE_PROPERTY_REGEX.test(key)) {
1045+
value = getterCache[key] || lib.nestedProperty(obj, key).get();
1046+
if(value) getterCache[key] = value;
1047+
}
1048+
if(value !== undefined) break;
1049+
}
1050+
1051+
if(value === undefined) {
1052+
if(numberOfHoverTemplateWarnings < maximumNumberOfHoverTemplateWarnings) {
1053+
lib.warn('Variable \'' + key + '\' in hovertemplate could not be found!');
1054+
value = match;
1055+
}
1056+
1057+
if(numberOfHoverTemplateWarnings === maximumNumberOfHoverTemplateWarnings) {
1058+
lib.warn('Too many hovertemplate warnings - additional warnings will be suppressed');
1059+
}
1060+
numberOfHoverTemplateWarnings++;
1061+
}
1062+
1063+
if(format) {
1064+
value = d3.format(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value);
1065+
} else {
1066+
if(labels.hasOwnProperty(key + 'Label')) value = labels[key + 'Label'];
1067+
}
1068+
return value;
1069+
});
1070+
};
1071+
10121072
/*
10131073
* alphanumeric string sort, tailored for subplot IDs like scene2, scene10, x10y13 etc
10141074
*/

Diff for: src/plots/plots.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1195,7 +1195,7 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac
11951195

11961196
if(_module) {
11971197
_module.supplyDefaults(traceIn, traceOut, defaultColor, layout);
1198-
Lib.coerceHoverinfo(traceIn, traceOut, layout);
1198+
if(!traceOut.hovertemplate) Lib.coerceHoverinfo(traceIn, traceOut, layout);
11991199
}
12001200

12011201
if(!Registry.traceIs(traceOut, 'noOpacity')) coerce('opacity');

Diff for: src/traces/bar/attributes.js

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

1111
var scatterAttrs = require('../scatter/attributes');
12+
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
1213
var colorAttributes = require('../../components/colorscale/attributes');
1314
var colorbarAttrs = require('../../components/colorbar/attributes');
1415
var fontAttrs = require('../../plots/font_attributes');
16+
var constants = require('./constants.js');
1517

1618
var extendFlat = require('../../lib/extend').extendFlat;
1719

@@ -60,6 +62,9 @@ module.exports = {
6062

6163
text: scatterAttrs.text,
6264
hovertext: scatterAttrs.hovertext,
65+
hovertemplate: hovertemplateAttrs({}, {
66+
keys: constants.eventDataKeys
67+
}),
6368

6469
textposition: {
6570
valType: 'enumerated',

Diff for: src/traces/bar/constants.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Copyright 2012-2018, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
module.exports = {
13+
eventDataKeys: []
14+
};

Diff for: src/traces/bar/defaults.js

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
3737

3838
coerce('text');
3939
coerce('hovertext');
40+
coerce('hovertemplate');
4041

4142
var textPosition = coerce('textposition');
4243

Diff for: src/traces/bar/hover.js

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ function hoverPoints(pointData, xval, yval, hovermode) {
137137
fillHoverText(di, trace, pointData);
138138
Registry.getComponentMethod('errorbars', 'hoverInfo')(di, trace, pointData);
139139

140+
pointData.hovertemplate = trace.hovertemplate;
140141
return [pointData];
141142
}
142143

Diff for: src/traces/histogram/attributes.js

+6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
'use strict';
1010

1111
var barAttrs = require('../bar/attributes');
12+
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
1213
var makeBinAttrs = require('./bin_attributes');
14+
var constants = require('./constants');
1315

1416
module.exports = {
1517
x: {
@@ -185,6 +187,10 @@ module.exports = {
185187
].join(' ')
186188
},
187189

190+
hovertemplate: hovertemplateAttrs({}, {
191+
keys: constants.eventDataKeys
192+
}),
193+
188194
marker: barAttrs.marker,
189195

190196
selected: barAttrs.selected,

Diff for: src/traces/histogram/constants.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Copyright 2012-2018, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
module.exports = {
13+
eventDataKeys: ['binNumber']
14+
};

Diff for: src/traces/histogram/defaults.js

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
5656
// autobin(x|y) are only included here to appease Plotly.validate
5757
coerce('autobin' + sampleLetter);
5858

59+
coerce('hovertemplate');
60+
5961
handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout);
6062

6163
// override defaultColor for error bars with defaultLine

Diff for: src/traces/histogram/hover.js

+2
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
2727
pointData[posLetter + 'Label'] = hoverLabelText(pointData[posLetter + 'a'], di.ph0, di.ph1);
2828
}
2929

30+
if(trace.hovermplate) pointData.hovertemplate = trace.hovertemplate;
31+
3032
return pts;
3133
};

0 commit comments

Comments
 (0)