From 157573959479f91d5946b2780a93f576b0a1b70a Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 27 Feb 2019 14:00:42 -0500 Subject: [PATCH 1/3] support locale in hovertemplate --- package-lock.json | 5 ++ package.json | 1 + src/components/fx/hover.js | 2 + src/lib/index.js | 16 ++++--- src/plots/plots.js | 1 + .../mocks/sankey_link_concentration.json | 2 +- test/jasmine/tests/hover_label_test.js | 47 ++++++++++++++++++- test/jasmine/tests/lib_test.js | 40 +++++++++++----- 8 files changed, 93 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83f7ca27609..4ded288e7ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2298,6 +2298,11 @@ "d3-timer": "1" } }, + "d3-format": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz", + "integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ==" + }, "d3-interpolate": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", diff --git a/package.json b/package.json index 0463841fda8..8e2d426f888 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "country-regex": "^1.1.0", "d3": "^3.5.12", "d3-force": "^1.0.6", + "d3-format": "^1.3.2", "d3-interpolate": "1", "d3-sankey-circular": "0.32.0", "delaunay-triangulate": "^1.1.6", diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index badbb374104..f4fbf1219da 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -971,6 +971,7 @@ function createHoverText(hoverData, opts, gd) { } // hovertemplate + var locale = gd._fullLayout._format; 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, + locale, eventData, {meta: fullLayout.meta} ); diff --git a/src/lib/index.js b/src/lib/index.js index 1c0c0eb7900..c36c5bac6d2 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -10,6 +10,7 @@ 'use strict'; var d3 = require('d3'); +var d3format = require('d3-format'); var isNumeric = require('fast-isnumeric'); var numConstants = require('../constants/numerical'); @@ -1031,17 +1032,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} data object containing the 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, locale) { var args = arguments; // Not all that useful, but cache nestedProperty instantiation // just in case it speeds things up *slightly*: @@ -1049,7 +1051,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 +1078,9 @@ lib.hovertemplateString = function(string, labels) { } if(format) { - value = d3.format(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value); + var fmt = d3format; + if(locale) fmt = fmt.formatLocale(locale); + value = fmt.format(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value); } else { if(labels.hasOwnProperty(key + 'Label')) value = labels[key + 'Label']; } diff --git a/src/plots/plots.js b/src/plots/plots.js index cde651366e8..f44bcca99d9 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -372,6 +372,7 @@ plots.supplyDefaults = function(gd, opts) { newFullLayout._d3locale = getFormatter(formatObj, newFullLayout.separators); newFullLayout._extraFormat = getFormatObj(gd, extraFormatKeys); + newFullLayout._format = formatObj; newFullLayout._initialAutoSizeIsDone = true; 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..58cbe30cc4b 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..ce96e13719e 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() { @@ -2239,6 +2241,18 @@ describe('Test lib.js:', function() { } expect(Lib.warn.calls.count()).toBe(10); }); + + describe('support different locale as argument', function() { + var locale = { + decimal: ',', + thousands: ' ', + currency: ['£', ''], + grouping: [3] + }; + it('formats value using d3 mini-language', function() { + expect(Lib.hovertemplateString('a: %{a:$010,.2f}', {}, locale, {a: 1253})).toEqual('a: £001 253,00'); + }); + }); }); describe('relativeAttr()', function() { From 1faaf8b6c3c161e243ed32e2bbf139c678b135ea Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 27 Feb 2019 14:34:26 -0500 Subject: [PATCH 2/3] use existing `fullLayout._d3locale` instead of package `d3-format` --- package-lock.json | 5 ----- package.json | 1 - src/components/fx/hover.js | 4 ++-- src/lib/index.js | 15 +++++++++------ src/plots/plots.js | 1 - test/jasmine/tests/lib_test.js | 12 ------------ 6 files changed, 11 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ded288e7ef..83f7ca27609 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2298,11 +2298,6 @@ "d3-timer": "1" } }, - "d3-format": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz", - "integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ==" - }, "d3-interpolate": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", diff --git a/package.json b/package.json index 8e2d426f888..0463841fda8 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "country-regex": "^1.1.0", "d3": "^3.5.12", "d3-force": "^1.0.6", - "d3-format": "^1.3.2", "d3-interpolate": "1", "d3-sankey-circular": "0.32.0", "delaunay-triangulate": "^1.1.6", diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index f4fbf1219da..7fc51ce2184 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -971,7 +971,7 @@ function createHoverText(hoverData, opts, gd) { } // hovertemplate - var locale = gd._fullLayout._format; + var d3locale = gd._fullLayout._d3locale; var hovertemplate = d.hovertemplate || false; var hovertemplateLabels = d.hovertemplateLabels || d; var eventData = d.eventData[0] || {}; @@ -979,7 +979,7 @@ function createHoverText(hoverData, opts, gd) { text = Lib.hovertemplateString( hovertemplate, hovertemplateLabels, - locale, + d3locale, eventData, {meta: fullLayout.meta} ); diff --git a/src/lib/index.js b/src/lib/index.js index c36c5bac6d2..be6d6707344 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -10,7 +10,6 @@ 'use strict'; var d3 = require('d3'); -var d3format = require('d3-format'); var isNumeric = require('fast-isnumeric'); var numConstants = require('../constants/numerical'); @@ -1036,14 +1035,14 @@ var maximumNumberOfHoverTemplateWarnings = 10; * Lib.hovertemplateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf' * Lib.hovertemplateString('price: %{y:$.2f}', {y: 1}) --> 'price: $1.00' * - * @param {obj} data object containing the locale + * @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, locale) { +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*: @@ -1078,9 +1077,13 @@ lib.hovertemplateString = function(string, labels, locale) { } if(format) { - var fmt = d3format; - if(locale) fmt = fmt.formatLocale(locale); - value = fmt.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/src/plots/plots.js b/src/plots/plots.js index f44bcca99d9..cde651366e8 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -372,7 +372,6 @@ plots.supplyDefaults = function(gd, opts) { newFullLayout._d3locale = getFormatter(formatObj, newFullLayout.separators); newFullLayout._extraFormat = getFormatObj(gd, extraFormatKeys); - newFullLayout._format = formatObj; newFullLayout._initialAutoSizeIsDone = true; diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index ce96e13719e..25b9e0e1278 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -2241,18 +2241,6 @@ describe('Test lib.js:', function() { } expect(Lib.warn.calls.count()).toBe(10); }); - - describe('support different locale as argument', function() { - var locale = { - decimal: ',', - thousands: ' ', - currency: ['£', ''], - grouping: [3] - }; - it('formats value using d3 mini-language', function() { - expect(Lib.hovertemplateString('a: %{a:$010,.2f}', {}, locale, {a: 1253})).toEqual('a: £001 253,00'); - }); - }); }); describe('relativeAttr()', function() { From 770b6097a1e725a29cf2095d882d07dc9f954041 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 27 Feb 2019 17:43:19 -0500 Subject: [PATCH 3/3] replace pound by euro sign in hovertemplate tests --- test/jasmine/tests/hover_label_test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 58cbe30cc4b..4eabcc61a9f 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1731,7 +1731,7 @@ describe('hover info', function() { locales: { 'fr-eu': { format: { - currency: ['£', ''], + currency: ['€', ''], decimal: ',', thousands: ' ', grouping: [3] @@ -1753,7 +1753,7 @@ describe('hover info', function() { expect(hoverTrace.y).toEqual(1); assertHoverLabelContent({ - nums: '£000 001,00', + nums: '€000 001,00', name: 'trace 0', axis: '0,388' });