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() {