Skip to content

Commit

Permalink
Merge pull request #3586 from plotly/fix-3580
Browse files Browse the repository at this point in the history
support locales in hovertemplate
  • Loading branch information
antoinerg authored Feb 27, 2019
2 parents 4b21575 + 770b609 commit 4877a9c
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 21 deletions.
2 changes: 2 additions & 0 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -971,13 +971,15 @@ function createHoverText(hoverData, opts, gd) {
}

// hovertemplate
var d3locale = gd._fullLayout._d3locale;
var hovertemplate = d.hovertemplate || false;
var hovertemplateLabels = d.hovertemplateLabels || d;
var eventData = d.eventData[0] || {};
if(hovertemplate) {
text = Lib.hovertemplateString(
hovertemplate,
hovertemplateLabels,
d3locale,
eventData,
{meta: fullLayout.meta}
);
Expand Down
19 changes: 13 additions & 6 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1031,25 +1031,26 @@ var maximumNumberOfHoverTemplateWarnings = 10;
* 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'
* Lib.hovertemplateString('name: %{trace}', {trace: 'asdf'}) --> 'name: asdf'
* Lib.hovertemplateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf'
* Lib.hovertemplateString('price: %{y:$.2f}', {y: 1}) --> 'price: $1.00'
*
* @param {obj} d3 locale
* @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) {
lib.hovertemplateString = function(string, labels, d3locale) {
var args = arguments;
// Not all that useful, but cache nestedProperty instantiation
// just in case it speeds things up *slightly*:
var getterCache = {};

return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, key, format) {
var obj, value, i;
for(i = 2; i < args.length; i++) {
for(i = 3; i < args.length; i++) {
obj = args[i];
if(obj.hasOwnProperty(key)) {
value = obj[key];
Expand All @@ -1076,7 +1077,13 @@ lib.hovertemplateString = function(string, labels) {
}

if(format) {
value = d3.format(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value);
var fmt;
if(d3locale) {
fmt = d3locale.numberFormat;
} else {
fmt = d3.format;
}
value = fmt(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value);
} else {
if(labels.hasOwnProperty(key + 'Label')) value = labels[key + 'Label'];
}
Expand Down
2 changes: 1 addition & 1 deletion test/image/mocks/sankey_link_concentration.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
}
],

"hovertemplate": "%{label}<br><b>flow.labelConcentration</b>: %{flow.labelConcentration:%0.2f}<br><b>flow.concentration</b>: %{flow.concentration:%0.2f}<br><b>flow.value</b>: %{flow.value}"
"hovertemplate": "%{label}<br><b>flow.labelConcentration</b>: %{flow.labelConcentration:0.2%}<br><b>flow.concentration</b>: %{flow.concentration:0.2%}<br><b>flow.value</b>: %{flow.value}"
}

}],
Expand Down
47 changes: 46 additions & 1 deletion test/jasmine/tests/hover_label_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1688,12 +1688,18 @@ describe('hover info', function() {
});

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

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

afterEach(function() {
Plotly.purge('graph');
destroyGraphDiv();
});

it('should format labels according to a template string', function(done) {
var gd = document.getElementById('graph');
Plotly.restyle(gd, 'hovertemplate', '%{y:$.2f}<extra>trace 0</extra>')
Expand All @@ -1717,6 +1723,45 @@ describe('hover info', function() {
.then(done);
});

it('should format labels according to a template string and locale', function(done) {
var gd = document.getElementById('graph');
mockCopy.layout.separators = undefined;
Plotly.newPlot(gd, mockCopy.data, mockCopy.layout, {
locale: 'fr-eu',
locales: {
'fr-eu': {
format: {
currency: ['€', ''],
decimal: ',',
thousands: ' ',
grouping: [3]
}
}
}
})
.then(function() {
Plotly.restyle(gd, 'hovertemplate', '%{y:$010,.2f}<extra>trace 0</extra>');
})
.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: '€000 001,00',
name: 'trace 0',
axis: '0,388'
});
})
.catch(failTest)
.then(done);
});

it('should format secondary label with extra tag', function(done) {
var gd = document.getElementById('graph');
Plotly.restyle(gd, 'hovertemplate', '<extra>trace 20 %{y:$.2f}</extra>')
Expand Down
28 changes: 15 additions & 13 deletions test/jasmine/tests/lib_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2181,52 +2181,54 @@ describe('Test lib.js:', function() {
});

describe('hovertemplateString', function() {
var locale = false;
it('evaluates attributes', function() {
expect(Lib.hovertemplateString('foo %{bar}', {}, {bar: 'baz'})).toEqual('foo baz');
expect(Lib.hovertemplateString('foo %{bar}', {}, locale, {bar: 'baz'})).toEqual('foo baz');
});

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

it('evaluates nested properties', function() {
expect(Lib.hovertemplateString('foo %{bar.baz}', {}, {bar: {baz: 'asdf'}})).toEqual('foo asdf');
expect(Lib.hovertemplateString('foo %{bar.baz}', {}, locale, {bar: {baz: 'asdf'}})).toEqual('foo asdf');
});

it('evaluates array nested properties', function() {
expect(Lib.hovertemplateString('foo %{bar[0].baz}', {}, {bar: [{baz: 'asdf'}]})).toEqual('foo asdf');
expect(Lib.hovertemplateString('foo %{bar[0].baz}', {}, locale, {bar: [{baz: 'asdf'}]})).toEqual('foo asdf');
});

it('subtitutes multiple matches', function() {
expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, {group: 'asdf', trace: 'jkl;'})).toEqual('foo asdf jkl;');
expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, locale, {group: 'asdf', trace: 'jkl;'})).toEqual('foo asdf jkl;');
});

it('replaces missing matches with template string', function() {
expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, {group: 1})).toEqual('foo 1 %{trace}');
expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, locale, {group: 1})).toEqual('foo 1 %{trace}');
});

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

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

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

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

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

it('looks for default label if no format is provided', function() {
expect(Lib.hovertemplateString('y: %{y}', {yLabel: '0.1'}, {y: 0.123})).toEqual('y: 0.1');
expect(Lib.hovertemplateString('y: %{y}', {yLabel: '0.1'}, locale, {y: 0.123})).toEqual('y: 0.1');
});

it('warns user up to 10 times if a variable cannot be found', function() {
Expand Down

0 comments on commit 4877a9c

Please sign in to comment.