Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support template string on hover #3126

Merged
merged 53 commits into from
Nov 15, 2018
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
4c197e8
rough first implementation for hovertemplate
antoinerg Oct 15, 2018
613c8f5
supply hover's eventData inot hovertemplate
antoinerg Oct 16, 2018
edf4795
delete unrelated file from branch
antoinerg Oct 17, 2018
dd62855
update description for hovertemplate
antoinerg Oct 17, 2018
a86327a
fix hovertemplate test not being run
antoinerg Oct 18, 2018
1fde3aa
for now, only offer hoverdata and trace objects to hovertemplate
antoinerg Oct 18, 2018
d263cce
templateString evaluates attributes with a dot in their name
antoinerg Oct 18, 2018
040208f
do not coerce hoverinfo if hovertemplate is defined
antoinerg Nov 5, 2018
3eaa3b9
replace templateString with the more specific hovertemplateString
antoinerg Nov 5, 2018
0084efb
hovertemplate: add warning if variable can't be found
antoinerg Nov 5, 2018
046d367
coerce hovertemplate at trace-level, starting with scatter(gl)
antoinerg Nov 6, 2018
59083ad
add <extra></extra> tag to hovertemplate for secondary labels
antoinerg Nov 6, 2018
266d43a
add URL to d3-format documentation
antoinerg Nov 6, 2018
108b68a
initial hovertemplate support for pie
antoinerg Nov 6, 2018
eb1a94a
pie hovertemplate test %{label}
antoinerg Nov 7, 2018
8214d36
bar support with limited test
antoinerg Nov 7, 2018
bb97abe
add hovertemplate support in histogram
antoinerg Nov 7, 2018
22e3a9d
pie returns default formatted value
antoinerg Nov 8, 2018
377ecba
pass hovertemplate data around in `hoverData` instead of opts
antoinerg Nov 8, 2018
bc6cbab
update Fx.multiHovers to properly massage hoverItem
antoinerg Nov 8, 2018
8c9ba5b
fix lint
antoinerg Nov 8, 2018
c767482
pass `trace` object in Fx.loneHover and Fx.multiHovers for hovertemplate
antoinerg Nov 8, 2018
6d4c03a
extra regex still matches in the presence of newlines
antoinerg Nov 8, 2018
8f440b0
fix jsDocs syntax
antoinerg Nov 12, 2018
0659d20
move regex to outer scope
antoinerg Nov 12, 2018
41ab464
remove old unused commented lines
antoinerg Nov 12, 2018
37e40f9
move regex to outside scope
antoinerg Nov 12, 2018
8e8b75b
move hovertemplate out of global-level plots attributes
antoinerg Nov 12, 2018
2015c57
scatter: do not coerce hovertemplate if hoveron: 'fills'
antoinerg Nov 12, 2018
aef48c0
fix lint
antoinerg Nov 12, 2018
a3058f4
test hovertemplate support for <extra> and pseudo-html
antoinerg Nov 12, 2018
3068d7d
hovertemplate warns user about missing variables up to 10 times
antoinerg Nov 12, 2018
a808883
hovertemplate attribute supports array
antoinerg Nov 12, 2018
522f744
scattergl support for hovertemplate array
antoinerg Nov 12, 2018
c501b44
pie support for hovertemplate array
antoinerg Nov 12, 2018
f89baca
make axes available in eventData to give access to its title
antoinerg Nov 12, 2018
369c9d6
add axis information to eventData only if hovertemplate
antoinerg Nov 12, 2018
8d95f05
axis information is already included in hovertemplate
antoinerg Nov 12, 2018
1e4bd33
pie: test that hovertemplate supports array
antoinerg Nov 13, 2018
61a2da7
list available variables in pie's hovertemplate description
antoinerg Nov 13, 2018
43a4cd9
hovertemplate: do not look into trace object, use fullData instead
antoinerg Nov 13, 2018
70befe3
describe hovertemplate variables for scatter(gl)
antoinerg Nov 13, 2018
b6822e8
describe hovertemplate variables for histogram
antoinerg Nov 13, 2018
59bc981
fix lint
antoinerg Nov 13, 2018
f75f2e0
scatter: test hover event data
antoinerg Nov 13, 2018
0654245
test that event data has correct fields in bar, scatter, histogram
antoinerg Nov 13, 2018
0ecd1c1
bar test: fix hover position to trigger hover events
antoinerg Nov 13, 2018
6f86522
scatter: add `marker.color` to `hovertemplate` available variables
antoinerg Nov 14, 2018
7be804e
one source of truth for event data keys used for doc and test
antoinerg Nov 14, 2018
583eb97
fix lint
antoinerg Nov 14, 2018
14ac99f
scatter: list additionnal variables available in eventData
antoinerg Nov 14, 2018
1caf321
hovertemplate: update desc, do no list attributes that are `arrayOK`
antoinerg Nov 15, 2018
a78600a
fix syntax
antoinerg Nov 15, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
56 changes: 44 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,20 @@ function createHoverText(hoverData, opts, gd) {
text = name;
}

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

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 +1380,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
34 changes: 34 additions & 0 deletions src/components/fx/hovertemplate_attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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 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.',
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 @@ -978,10 +978,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 @@ -993,7 +993,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 @@ -1008,6 +1007,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'];
Copy link
Contributor

@etpinard etpinard Nov 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very interesting. I've never been a big fan of hasOwnProperty (probably because it's a long word to type out), but look at this:

image

image

using http://jsbench.github.io/#1aebf699d73fb743127903c0b0a8bece

... so maybe we should start switching to hasOwnProperty in hot codepaths 🔥

}
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 @@ -1167,7 +1167,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
2 changes: 2 additions & 0 deletions src/traces/bar/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'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');
Expand Down Expand Up @@ -59,6 +60,7 @@ module.exports = {

text: scatterAttrs.text,
hovertext: scatterAttrs.hovertext,
hovertemplate: hovertemplateAttrs(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should append the hovertemplate descriptions with a list of flags (i.e. event data keys) available.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a related note: I like that for some traces, like pie, we test that event data has the correct keys:

it('should contain the correct fields', function() {
var hoverData,
unhoverData;
gd.on('plotly_hover', function(data) {
hoverData = data;
});
gd.on('plotly_unhover', function(data) {
unhoverData = data;
});
mouseEvent('mouseover', width / 2 - 7, height / 2 - 7);
mouseEvent('mouseout', width / 2 - 7, height / 2 - 7);
expect(hoverData.points.length).toEqual(1);
expect(unhoverData.points.length).toEqual(1);
var fields = [
'curveNumber', 'pointNumber', 'pointNumbers',
'data', 'fullData',
'label', 'color', 'value',
'i', 'v'
];
expect(Object.keys(hoverData.points[0]).sort()).toEqual(fields.sort());
expect(hoverData.points[0].pointNumber).toEqual(3);
expect(Object.keys(unhoverData.points[0]).sort()).toEqual(fields.sort());
expect(unhoverData.points[0].pointNumber).toEqual(3);
});

We could potentially use the same array of field names for the test and for the description making sure we have one source of truth 🤔. This improvement could be done in another PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could potentially use the same array of field names for the test and for the description making sure we have one source of truth

Yeah, good call. We usually put things like that in "constants" files like this one:

https://github.com/plotly/plotly.js/blob/master/src/traces/scatter/constants.js

This improvement could be done in another PR.

That's up to you!

Copy link
Contributor Author

@antoinerg antoinerg Nov 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a new function to check that event data contains a given list of keys: https://github.com/plotly/plotly.js/pull/3126/files#diff-4ddfa80f2c61e112d5587c91a961ab47

If it looks OK, I will dry up the code by listing the extra keys of a given trace into its constants.js file and then use that list to generate the description of hovertemplate attribute 🎉 !

Let me know if that route looks fine to you.


textposition: {
valType: 'enumerated',
Expand Down
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
2 changes: 2 additions & 0 deletions src/traces/histogram/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ module.exports = {
].join(' ')
},

hovertemplate: barAttrs.hovertemplate,

marker: barAttrs.marker,

selected: barAttrs.selected,
Expand Down
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;
};
2 changes: 2 additions & 0 deletions src/traces/pie/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
var colorAttrs = require('../../components/color/attributes');
var fontAttrs = require('../../plots/font_attributes');
var plotAttrs = require('../../plots/attributes');
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
var domainAttrs = require('../../plots/domain').attributes;

var extendFlat = require('../../lib/extend').extendFlat;
Expand Down Expand Up @@ -158,6 +159,7 @@ module.exports = {
hoverinfo: extendFlat({}, plotAttrs.hoverinfo, {
flags: ['label', 'text', 'value', 'percent', 'name']
}),
hovertemplate: hovertemplateAttrs(),
textposition: {
valType: 'enumerated',
role: 'info',
Expand Down
1 change: 1 addition & 0 deletions src/traces/pie/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
var textData = coerce('text');
var textInfo = coerce('textinfo', Array.isArray(textData) ? 'text+percent' : 'percent');
coerce('hovertext');
coerce('hovertemplate');

if(textInfo && textInfo !== 'none') {
var textPosition = coerce('textposition'),
Expand Down
2 changes: 2 additions & 0 deletions src/traces/pie/event_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ module.exports = function eventData(pt, trace) {
label: pt.label,
color: pt.color,
value: pt.v,
percent: pt.percent,
text: pt.text,
etpinard marked this conversation as resolved.
Show resolved Hide resolved

// pt.v (and pt.i below) for backward compatibility
v: pt.v
Expand Down
Loading