diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index badbb374104..7fc51ce2184 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -971,6 +971,7 @@ 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] || {};
@@ -978,6 +979,7 @@ function createHoverText(hoverData, opts, gd) {
text = Lib.hovertemplateString(
hovertemplate,
hovertemplateLabels,
+ d3locale,
eventData,
{meta: fullLayout.meta}
);
diff --git a/src/lib/index.js b/src/lib/index.js
index 1c0c0eb7900..be6d6707344 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -1031,17 +1031,18 @@ 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*:
@@ -1049,7 +1050,7 @@ lib.hovertemplateString = function(string, labels) {
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];
@@ -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'];
}
diff --git a/test/image/mocks/sankey_link_concentration.json b/test/image/mocks/sankey_link_concentration.json
index 789380539d0..a4b4564fff0 100644
--- a/test/image/mocks/sankey_link_concentration.json
+++ b/test/image/mocks/sankey_link_concentration.json
@@ -63,7 +63,7 @@
}
],
- "hovertemplate": "%{label}
flow.labelConcentration: %{flow.labelConcentration:%0.2f}
flow.concentration: %{flow.concentration:%0.2f}
flow.value: %{flow.value}"
+ "hovertemplate": "%{label}
flow.labelConcentration: %{flow.labelConcentration:0.2%}
flow.concentration: %{flow.concentration:0.2%}
flow.value: %{flow.value}"
}
}],
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index c8178a78fe2..4eabcc61a9f 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -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}trace 0')
@@ -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}trace 0');
+ })
+ .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', 'trace 20 %{y:$.2f}')
diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js
index 00351710619..25b9e0e1278 100644
--- a/test/jasmine/tests/lib_test.js
+++ b/test/jasmine/tests/lib_test.js
@@ -2181,28 +2181,29 @@ 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() {
@@ -2210,23 +2211,24 @@ describe('Test lib.js:', function() {
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() {