Skip to content

Commit

Permalink
Merge pull request #3126 from plotly/3007-hovertemplate
Browse files Browse the repository at this point in the history
support template string on hover
  • Loading branch information
antoinerg authored Nov 15, 2018
2 parents 2ceb5c3 + a78600a commit 3a32ac0
Show file tree
Hide file tree
Showing 32 changed files with 615 additions and 30 deletions.
2 changes: 2 additions & 0 deletions src/components/fx/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ module.exports = function calc(gd) {

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

if(trace.hovertemplate) fillFn(trace.hovertemplate, cd, 'ht');

if(!trace.hoverlabel) continue;

fillFn(trace.hoverlabel.bgcolor, cd, 'hbg');
Expand Down
55 changes: 43 additions & 12 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,17 @@ exports.loneHover = function loneHover(hoverItem, opts) {
fontColor: hoverItem.fontColor,

// filler to make createHoverText happy
trace: {
trace: hoverItem.trace || {
index: 0,
hoverinfo: ''
},
xa: {_offset: 0},
ya: {_offset: 0},
index: 0
index: 0,

hovertemplate: hoverItem.hovertemplate || false,
eventData: hoverItem.eventData || false,
hovertemplateLabels: hoverItem.hovertemplateLabels || false,
};

var container3 = d3.select(opts.container);
Expand All @@ -146,7 +150,6 @@ exports.loneHover = function loneHover(hoverItem, opts) {
container: container3,
outerContainer: outerContainer3
};

var hoverLabel = createHoverText([pointData], fullOpts, opts.gd);
alignHoverText(hoverLabel, fullOpts.rotateLabels);

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

// filler to make createHoverText happy
trace: {
trace: hoverItem.trace || {
index: 0,
hoverinfo: ''
},
xa: {_offset: 0},
ya: {_offset: 0},
index: 0
index: 0,

hovertemplate: hoverItem.hovertemplate || false,
eventData: hoverItem.eventData || false,
hovertemplateLabels: hoverItem.hovertemplateLabels || false,
};
});

Expand Down Expand Up @@ -662,7 +669,14 @@ function _hover(gd, evt, subplot, noHoverEvent) {
// other people and send it to the event
for(itemnum = 0; itemnum < hoverData.length; itemnum++) {
var pt = hoverData[itemnum];
newhoverdata.push(helpers.makeEventData(pt, pt.trace, pt.cd));
var eventData = helpers.makeEventData(pt, pt.trace, pt.cd);

var ht = false;
if(pt.cd[pt.index] && pt.cd[pt.index].ht) ht = pt.cd[pt.index].ht;
hoverData[itemnum].hovertemplate = ht || pt.trace.hovertemplate || false;
hoverData[itemnum].eventData = [eventData];

newhoverdata.push(eventData);
}

gd._hoverdata = newhoverdata;
Expand Down Expand Up @@ -720,6 +734,8 @@ function _hover(gd, evt, subplot, noHoverEvent) {
});
}

var EXTRA_STRING_REGEX = /<extra>([\s\S]*)<\/extra>/;

function createHoverText(hoverData, opts, gd) {
var hovermode = opts.hovermode;
var rotateLabels = opts.rotateLabels;
Expand Down Expand Up @@ -763,11 +779,13 @@ function createHoverText(hoverData, opts, gd) {
if(allHaveZ && hoverData[i].zLabel === undefined) allHaveZ = false;

traceHoverinfo = hoverData[i].hoverinfo || hoverData[i].trace.hoverinfo;
var parts = Array.isArray(traceHoverinfo) ? traceHoverinfo : traceHoverinfo.split('+');
if(parts.indexOf('all') === -1 &&
parts.indexOf(hovermode) === -1) {
showCommonLabel = false;
break;
if(traceHoverinfo) {
var parts = Array.isArray(traceHoverinfo) ? traceHoverinfo : traceHoverinfo.split('+');
if(parts.indexOf('all') === -1 &&
parts.indexOf(hovermode) === -1) {
showCommonLabel = false;
break;
}
}
}

Expand Down Expand Up @@ -950,6 +968,19 @@ function createHoverText(hoverData, opts, gd) {
text = name;
}

// hovertemplate
var hovertemplate = d.hovertemplate || false;
var hovertemplateLabels = d.hovertemplateLabels || d;
var eventData = d.eventData[0] || {};
if(hovertemplate) {
text = Lib.hovertemplateString(hovertemplate, hovertemplateLabels, eventData);

text = text.replace(EXTRA_STRING_REGEX, function(match, extra) {
name = extra; // Assign name for secondary text label
return ''; // Remove from main text label
});
}

// main label
var tx = g.select('text.nums')
.call(Drawing.font,
Expand Down Expand Up @@ -1348,7 +1379,7 @@ function cleanPoint(d, hovermode) {

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

if(infomode !== 'all') {
if(infomode && infomode !== 'all') {
infomode = Array.isArray(infomode) ? infomode : infomode.split('+');
if(infomode.indexOf('x') === -1) d.xLabel = undefined;
if(infomode.indexOf('y') === -1) d.yLabel = undefined;
Expand Down
49 changes: 49 additions & 0 deletions src/components/fx/hovertemplate_attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright 2012-2018, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

module.exports = function(opts, extra) {
opts = opts || {};
extra = extra || {};

var descPart = extra.description ? ' ' + extra.description : '';
var keys = extra.keys || [];
if(keys.length > 0) {
var quotedKeys = [];
for(var i = 0; i < keys.length; i++) {
quotedKeys[i] = '`' + keys[i] + '`';
}
descPart = descPart + 'Finally, this trace also supports ';
if(keys.length === 1) {
descPart = 'variable ' + quotedKeys[0];
} else {
descPart = 'variables ' + quotedKeys.slice(0, -1).join(', ') + ' and ' + quotedKeys.slice(-1) + '.';
}
}

var hovertemplate = {
valType: 'string',
role: 'info',
dflt: '',
arrayOk: true,
editType: 'none',
description: [
'Template string used for rendering the information that appear on hover box.',
'Note that this will override `hoverinfo`.',
'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}".',
'See https://github.com/d3/d3-format/blob/master/README.md#locale_format for details on the formatting syntax.',
'The variables available in `hovertemplate` are the ones emitted as event data described at this link https://plot.ly/javascript/plotlyjs-events/#event-data.',
'Additionally, every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.',
descPart
].join(' ')
};

return hovertemplate;
};
66 changes: 63 additions & 3 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -979,10 +979,10 @@ lib.numSeparate = function(value, separators, separatethousands) {
return x1 + x2;
};

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

/*
/**
* Substitute values from an object into a string
*
* Examples:
Expand All @@ -994,7 +994,6 @@ var SIMPLE_PROPERTY_REGEX = /^\w*$/;
*
* @return {string} templated string
*/

lib.templateString = function(string, obj) {
// Not all that useful, but cache nestedProperty instantiation
// just in case it speeds things up *slightly*:
Expand All @@ -1009,6 +1008,67 @@ lib.templateString = function(string, obj) {
});
};

var TEMPLATE_STRING_FORMAT_SEPARATOR = /^:/;
var numberOfHoverTemplateWarnings = 0;
var maximumNumberOfHoverTemplateWarnings = 10;
/**
* Substitute values from an object into a string and optionally formats them using d3-format,
* or fallback to associated labels.
*
* Examples:
* Lib.templateString('name: %{trace}', {trace: 'asdf'}) --> 'name: asdf'
* Lib.templateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf'
* Lib.templateString('price: %{y:$.2f}', {y: 1}) --> 'price: $1.00'
*
* @param {string} input string containing %{...:...} template strings
* @param {obj} data object containing fallback text when no formatting is specified, ex.: {yLabel: 'formattedYValue'}
* @param {obj} data objects containing substitution values
*
* @return {string} templated string
*/
lib.hovertemplateString = function(string, labels) {
var args = arguments;
// Not all that useful, but cache nestedProperty instantiation
// just in case it speeds things up *slightly*:
var getterCache = {};

return string.replace(TEMPLATE_STRING_REGEX, function(match, key, format) {
var obj, value, i;
for(i = 2; i < args.length; i++) {
obj = args[i];
if(obj.hasOwnProperty(key)) {
value = obj[key];
break;
}

if(!SIMPLE_PROPERTY_REGEX.test(key)) {
value = getterCache[key] || lib.nestedProperty(obj, key).get();
if(value) getterCache[key] = value;
}
if(value !== undefined) break;
}

if(value === undefined) {
if(numberOfHoverTemplateWarnings < maximumNumberOfHoverTemplateWarnings) {
lib.warn('Variable \'' + key + '\' in hovertemplate could not be found!');
value = match;
}

if(numberOfHoverTemplateWarnings === maximumNumberOfHoverTemplateWarnings) {
lib.warn('Too many hovertemplate warnings - additional warnings will be suppressed');
}
numberOfHoverTemplateWarnings++;
}

if(format) {
value = d3.format(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value);
} else {
if(labels.hasOwnProperty(key + 'Label')) value = labels[key + 'Label'];
}
return value;
});
};

/*
* alphanumeric string sort, tailored for subplot IDs like scene2, scene10, x10y13 etc
*/
Expand Down
2 changes: 1 addition & 1 deletion src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -1195,7 +1195,7 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac

if(_module) {
_module.supplyDefaults(traceIn, traceOut, defaultColor, layout);
Lib.coerceHoverinfo(traceIn, traceOut, layout);
if(!traceOut.hovertemplate) Lib.coerceHoverinfo(traceIn, traceOut, layout);
}

if(!Registry.traceIs(traceOut, 'noOpacity')) coerce('opacity');
Expand Down
5 changes: 5 additions & 0 deletions src/traces/bar/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
'use strict';

var scatterAttrs = require('../scatter/attributes');
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
var colorAttributes = require('../../components/colorscale/attributes');
var colorbarAttrs = require('../../components/colorbar/attributes');
var fontAttrs = require('../../plots/font_attributes');
var constants = require('./constants.js');

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

Expand Down Expand Up @@ -60,6 +62,9 @@ module.exports = {

text: scatterAttrs.text,
hovertext: scatterAttrs.hovertext,
hovertemplate: hovertemplateAttrs({}, {
keys: constants.eventDataKeys
}),

textposition: {
valType: 'enumerated',
Expand Down
14 changes: 14 additions & 0 deletions src/traces/bar/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright 2012-2018, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/


'use strict';

module.exports = {
eventDataKeys: []
};
1 change: 1 addition & 0 deletions src/traces/bar/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout

coerce('text');
coerce('hovertext');
coerce('hovertemplate');

var textPosition = coerce('textposition');

Expand Down
1 change: 1 addition & 0 deletions src/traces/bar/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ function hoverPoints(pointData, xval, yval, hovermode) {
fillHoverText(di, trace, pointData);
Registry.getComponentMethod('errorbars', 'hoverInfo')(di, trace, pointData);

pointData.hovertemplate = trace.hovertemplate;
return [pointData];
}

Expand Down
6 changes: 6 additions & 0 deletions src/traces/histogram/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
'use strict';

var barAttrs = require('../bar/attributes');
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
var makeBinAttrs = require('./bin_attributes');
var constants = require('./constants');

module.exports = {
x: {
Expand Down Expand Up @@ -185,6 +187,10 @@ module.exports = {
].join(' ')
},

hovertemplate: hovertemplateAttrs({}, {
keys: constants.eventDataKeys
}),

marker: barAttrs.marker,

selected: barAttrs.selected,
Expand Down
14 changes: 14 additions & 0 deletions src/traces/histogram/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright 2012-2018, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/


'use strict';

module.exports = {
eventDataKeys: ['binNumber']
};
2 changes: 2 additions & 0 deletions src/traces/histogram/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
// autobin(x|y) are only included here to appease Plotly.validate
coerce('autobin' + sampleLetter);

coerce('hovertemplate');

handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout);

// override defaultColor for error bars with defaultLine
Expand Down
2 changes: 2 additions & 0 deletions src/traces/histogram/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
pointData[posLetter + 'Label'] = hoverLabelText(pointData[posLetter + 'a'], di.ph0, di.ph1);
}

if(trace.hovermplate) pointData.hovertemplate = trace.hovertemplate;

return pts;
};
Loading

0 comments on commit 3a32ac0

Please sign in to comment.