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 7 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
14 changes: 11 additions & 3 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ exports.loneHover = function loneHover(hoverItem, opts) {
rotateLabels: false,
bgColor: opts.bgColor || Color.background,
container: container3,
outerContainer: outerContainer3
outerContainer: outerContainer3,
hovertemplate: opts.hovertemplate || false
};

var hoverLabel = createHoverText([pointData], fullOpts, opts.gd);
Expand Down Expand Up @@ -200,7 +201,8 @@ exports.multiHovers = function multiHovers(hoverItems, opts) {
rotateLabels: false,
bgColor: opts.bgColor || Color.background,
container: container3,
outerContainer: outerContainer3
outerContainer: outerContainer3,
hovertemplate: opts.hovertemplate || false
};

var hoverLabel = createHoverText(pointsData, fullOpts, opts.gd);
Expand Down Expand Up @@ -882,7 +884,7 @@ function createHoverText(hoverData, opts, gd) {

// then put the text in, position the pointer to the data,
// and figure out sizes
hoverLabels.each(function(d) {
hoverLabels.each(function(d, curveNumber) {
var g = d3.select(this).attr('transform', '');
var name = '';
var text = '';
Expand Down Expand Up @@ -950,6 +952,12 @@ function createHoverText(hoverData, opts, gd) {
text = name;
}

// hovertemplate
var trace = d.trace, hovertemplate = opts.hovertemplate || trace.hovertemplate || false;
if(hovertemplate) {
text = Lib.templateString(hovertemplate, gd._hoverdata[curveNumber], trace);
Copy link
Contributor Author

@antoinerg antoinerg Oct 18, 2018

Choose a reason for hiding this comment

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

We can pass additional objects as argument to Lib.templateString so the sky is the limit in terms of the number of values we can make available here!

However, as mentioned in point 1, we should probably put useful data into _hoverdata to begin with since it's emitted on hover.

}

// main label
var tx = g.select('text.nums')
.call(Drawing.font,
Expand Down
40 changes: 30 additions & 10 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -978,33 +978,53 @@ 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
* Substitute values from an object into a string and optionally formats them using d3-format
*
* 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 substitution values
* @param {string} input string containing %{...:...} template strings
* @param {obj} data objects containing substitution values
*
* @return {string} templated string
*/

etpinard marked this conversation as resolved.
Show resolved Hide resolved
lib.templateString = function(string, obj) {
lib.templateString = function(string) {
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(dummy, key) {
if(SIMPLE_PROPERTY_REGEX.test(key)) {
return obj[key] || '';
return string.replace(TEMPLATE_STRING_REGEX, function(dummy, key, format) {
var obj, value, i;
for(i = 1; i < args.length; i++) {
obj = args[i];
if(obj.hasOwnProperty(key)) {
value = obj[key];
break;
}

if(!SIMPLE_PROPERTY_REGEX.test(key)) {
// getterCache[key] = getterCache[key] || lib.nestedProperty(obj, key).get;
// value = getterCache[key]();
etpinard marked this conversation as resolved.
Show resolved Hide resolved
value = getterCache[key] || lib.nestedProperty(obj, key).get();
if(value) getterCache[key] = value;
}
if(value !== undefined) break;
}

if(value === undefined) value = '';

if(format) {
value = d3.format(format.replace(/^:/, ''))(value);
etpinard marked this conversation as resolved.
Show resolved Hide resolved
}
getterCache[key] = getterCache[key] || lib.nestedProperty(obj, key).get;
return getterCache[key]() || '';
return value;
});
};

Expand Down
12 changes: 12 additions & 0 deletions src/plots/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ module.exports = {
].join(' ')
},
hoverlabel: fxAttrs.hoverlabel,
hovertemplate: {
etpinard marked this conversation as resolved.
Show resolved Hide resolved
valType: 'string',
role: 'info',
dflt: false,
etpinard marked this conversation as resolved.
Show resolved Hide resolved
editType: 'none',
description: [
etpinard marked this conversation as resolved.
Show resolved Hide resolved
'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}".'
].join(' ')
},
stream: {
token: {
valType: 'string',
Expand Down
2 changes: 2 additions & 0 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,8 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac
Lib.coerceHoverinfo(traceIn, traceOut, layout);
}

coerce('hovertemplate');

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

if(Registry.traceIs(traceOut, 'notLegendIsolatable')) {
Expand Down
30 changes: 30 additions & 0 deletions test/jasmine/tests/hover_label_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,36 @@ describe('hover info', function() {
.toBeWithin(0, 1); // Be robust against floating point arithmetic and subtle future layout changes
});
});

describe('hovertemplate', function() {
var mockCopy = Lib.extendDeep({}, mock);

beforeEach(function(done) {
Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done);
});

it('should format labels according to a template string', function(done) {
var gd = document.getElementById('graph');
Plotly.restyle(gd, 'hovertemplate', '%{y:$.2f}')
.then(function() {
Fx.hover('graph', evt, 'xy');

var hoverTrace = gd._hoverdata[0];

expect(hoverTrace.curveNumber).toEqual(0);
expect(hoverTrace.pointNumber).toEqual(17);
expect(hoverTrace.x).toEqual(0.388);
expect(hoverTrace.y).toEqual(1);

assertHoverLabelContent({
nums: '$1.00',
axis: '0.388'
});
})
.catch(failTest)
.then(done);
});
});
});

describe('hover info on stacked subplots', function() {
Expand Down
23 changes: 23 additions & 0 deletions test/jasmine/tests/lib_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2124,6 +2124,10 @@ describe('Test lib.js:', function() {
expect(Lib.templateString('foo %{bar}', {bar: 'baz'})).toEqual('foo baz');
});

it('evaluates attributes with a dot in their name', function() {
expect(Lib.templateString('%{marker.size}', {'marker.size': 12}, {marker: {size: 14}})).toEqual('12');
});

it('evaluates nested properties', function() {
expect(Lib.templateString('foo %{bar.baz}', {bar: {baz: 'asdf'}})).toEqual('foo asdf');
});
Expand All @@ -2143,6 +2147,25 @@ describe('Test lib.js:', function() {
it('replaces empty key with empty string', function() {
expect(Lib.templateString('foo %{} %{}', {})).toEqual('foo ');
});

it('uses the value from the first object with the specified key', function() {
var obj1 = {a: 'first'}, obj2 = {a: 'second', foo: {bar: 'bar'}};

// Simple key
expect(Lib.templateString('foo %{a}', obj1, obj2)).toEqual('foo first');
expect(Lib.templateString('foo %{a}', obj2, obj1)).toEqual('foo second');

// Nested Keys
expect(Lib.templateString('foo %{foo.bar}', obj1, obj2)).toEqual('foo bar');

// Nested keys with 0
expect(Lib.templateString('y: %{y}', {y: 0}, {y: 1})).toEqual('y: 0');
});

it('formats value using d3 mini-language', function() {
expect(Lib.templateString('a: %{a:.0%}', {a: 0.123})).toEqual('a: 12%');
expect(Lib.templateString('b: %{b:2.2f}', {b: 43})).toEqual('b: 43.00');
});
});

describe('relativeAttr()', function() {
Expand Down