Skip to content

Commit 4877a9c

Browse files
authored
Merge pull request #3586 from plotly/fix-3580
support locales in hovertemplate
2 parents 4b21575 + 770b609 commit 4877a9c

File tree

5 files changed

+77
-21
lines changed

5 files changed

+77
-21
lines changed

src/components/fx/hover.js

+2
Original file line numberDiff line numberDiff line change
@@ -971,13 +971,15 @@ function createHoverText(hoverData, opts, gd) {
971971
}
972972

973973
// hovertemplate
974+
var d3locale = gd._fullLayout._d3locale;
974975
var hovertemplate = d.hovertemplate || false;
975976
var hovertemplateLabels = d.hovertemplateLabels || d;
976977
var eventData = d.eventData[0] || {};
977978
if(hovertemplate) {
978979
text = Lib.hovertemplateString(
979980
hovertemplate,
980981
hovertemplateLabels,
982+
d3locale,
981983
eventData,
982984
{meta: fullLayout.meta}
983985
);

src/lib/index.js

+13-6
Original file line numberDiff line numberDiff line change
@@ -1031,25 +1031,26 @@ var maximumNumberOfHoverTemplateWarnings = 10;
10311031
* or fallback to associated labels.
10321032
*
10331033
* Examples:
1034-
* Lib.templateString('name: %{trace}', {trace: 'asdf'}) --> 'name: asdf'
1035-
* Lib.templateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf'
1036-
* Lib.templateString('price: %{y:$.2f}', {y: 1}) --> 'price: $1.00'
1034+
* Lib.hovertemplateString('name: %{trace}', {trace: 'asdf'}) --> 'name: asdf'
1035+
* Lib.hovertemplateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf'
1036+
* Lib.hovertemplateString('price: %{y:$.2f}', {y: 1}) --> 'price: $1.00'
10371037
*
1038+
* @param {obj} d3 locale
10381039
* @param {string} input string containing %{...:...} template strings
10391040
* @param {obj} data object containing fallback text when no formatting is specified, ex.: {yLabel: 'formattedYValue'}
10401041
* @param {obj} data objects containing substitution values
10411042
*
10421043
* @return {string} templated string
10431044
*/
1044-
lib.hovertemplateString = function(string, labels) {
1045+
lib.hovertemplateString = function(string, labels, d3locale) {
10451046
var args = arguments;
10461047
// Not all that useful, but cache nestedProperty instantiation
10471048
// just in case it speeds things up *slightly*:
10481049
var getterCache = {};
10491050

10501051
return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, key, format) {
10511052
var obj, value, i;
1052-
for(i = 2; i < args.length; i++) {
1053+
for(i = 3; i < args.length; i++) {
10531054
obj = args[i];
10541055
if(obj.hasOwnProperty(key)) {
10551056
value = obj[key];
@@ -1076,7 +1077,13 @@ lib.hovertemplateString = function(string, labels) {
10761077
}
10771078

10781079
if(format) {
1079-
value = d3.format(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value);
1080+
var fmt;
1081+
if(d3locale) {
1082+
fmt = d3locale.numberFormat;
1083+
} else {
1084+
fmt = d3.format;
1085+
}
1086+
value = fmt(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value);
10801087
} else {
10811088
if(labels.hasOwnProperty(key + 'Label')) value = labels[key + 'Label'];
10821089
}

test/image/mocks/sankey_link_concentration.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
}
6464
],
6565

66-
"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}"
66+
"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}"
6767
}
6868

6969
}],

test/jasmine/tests/hover_label_test.js

+46-1
Original file line numberDiff line numberDiff line change
@@ -1688,12 +1688,18 @@ describe('hover info', function() {
16881688
});
16891689

16901690
describe('hovertemplate', function() {
1691-
var mockCopy = Lib.extendDeep({}, mock);
1691+
var mockCopy;
16921692

16931693
beforeEach(function(done) {
1694+
mockCopy = Lib.extendDeep({}, mock);
16941695
Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done);
16951696
});
16961697

1698+
afterEach(function() {
1699+
Plotly.purge('graph');
1700+
destroyGraphDiv();
1701+
});
1702+
16971703
it('should format labels according to a template string', function(done) {
16981704
var gd = document.getElementById('graph');
16991705
Plotly.restyle(gd, 'hovertemplate', '%{y:$.2f}<extra>trace 0</extra>')
@@ -1717,6 +1723,45 @@ describe('hover info', function() {
17171723
.then(done);
17181724
});
17191725

1726+
it('should format labels according to a template string and locale', function(done) {
1727+
var gd = document.getElementById('graph');
1728+
mockCopy.layout.separators = undefined;
1729+
Plotly.newPlot(gd, mockCopy.data, mockCopy.layout, {
1730+
locale: 'fr-eu',
1731+
locales: {
1732+
'fr-eu': {
1733+
format: {
1734+
currency: ['€', ''],
1735+
decimal: ',',
1736+
thousands: ' ',
1737+
grouping: [3]
1738+
}
1739+
}
1740+
}
1741+
})
1742+
.then(function() {
1743+
Plotly.restyle(gd, 'hovertemplate', '%{y:$010,.2f}<extra>trace 0</extra>');
1744+
})
1745+
.then(function() {
1746+
Fx.hover('graph', evt, 'xy');
1747+
1748+
var hoverTrace = gd._hoverdata[0];
1749+
1750+
expect(hoverTrace.curveNumber).toEqual(0);
1751+
expect(hoverTrace.pointNumber).toEqual(17);
1752+
expect(hoverTrace.x).toEqual(0.388);
1753+
expect(hoverTrace.y).toEqual(1);
1754+
1755+
assertHoverLabelContent({
1756+
nums: '€000 001,00',
1757+
name: 'trace 0',
1758+
axis: '0,388'
1759+
});
1760+
})
1761+
.catch(failTest)
1762+
.then(done);
1763+
});
1764+
17201765
it('should format secondary label with extra tag', function(done) {
17211766
var gd = document.getElementById('graph');
17221767
Plotly.restyle(gd, 'hovertemplate', '<extra>trace 20 %{y:$.2f}</extra>')

test/jasmine/tests/lib_test.js

+15-13
Original file line numberDiff line numberDiff line change
@@ -2181,52 +2181,54 @@ describe('Test lib.js:', function() {
21812181
});
21822182

21832183
describe('hovertemplateString', function() {
2184+
var locale = false;
21842185
it('evaluates attributes', function() {
2185-
expect(Lib.hovertemplateString('foo %{bar}', {}, {bar: 'baz'})).toEqual('foo baz');
2186+
expect(Lib.hovertemplateString('foo %{bar}', {}, locale, {bar: 'baz'})).toEqual('foo baz');
21862187
});
21872188

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

21922193
it('evaluates nested properties', function() {
2193-
expect(Lib.hovertemplateString('foo %{bar.baz}', {}, {bar: {baz: 'asdf'}})).toEqual('foo asdf');
2194+
expect(Lib.hovertemplateString('foo %{bar.baz}', {}, locale, {bar: {baz: 'asdf'}})).toEqual('foo asdf');
21942195
});
21952196

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

22002201
it('subtitutes multiple matches', function() {
2201-
expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, {group: 'asdf', trace: 'jkl;'})).toEqual('foo asdf jkl;');
2202+
expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, locale, {group: 'asdf', trace: 'jkl;'})).toEqual('foo asdf jkl;');
22022203
});
22032204

22042205
it('replaces missing matches with template string', function() {
2205-
expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, {group: 1})).toEqual('foo 1 %{trace}');
2206+
expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, locale, {group: 1})).toEqual('foo 1 %{trace}');
22062207
});
22072208

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

22122213
// Simple key
2213-
expect(Lib.hovertemplateString('foo %{a}', {}, obj1, obj2)).toEqual('foo first');
2214-
expect(Lib.hovertemplateString('foo %{a}', {}, obj2, obj1)).toEqual('foo second');
2214+
expect(Lib.hovertemplateString('foo %{a}', {}, locale, obj1, obj2)).toEqual('foo first');
2215+
expect(Lib.hovertemplateString('foo %{a}', {}, locale, obj2, obj1)).toEqual('foo second');
22152216

22162217
// Nested Keys
2217-
expect(Lib.hovertemplateString('foo %{foo.bar}', {}, obj1, obj2)).toEqual('foo bar');
2218+
expect(Lib.hovertemplateString('foo %{foo.bar}', {}, locale, obj1, obj2)).toEqual('foo bar');
22182219

22192220
// Nested keys with 0
2220-
expect(Lib.hovertemplateString('y: %{y}', {}, {y: 0}, {y: 1})).toEqual('y: 0');
2221+
expect(Lib.hovertemplateString('y: %{y}', {}, locale, {y: 0}, {y: 1})).toEqual('y: 0');
22212222
});
22222223

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

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

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

0 commit comments

Comments
 (0)