diff --git a/src/lib/index.js b/src/lib/index.js index eb4f17d844c..612dd2fa065 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -541,3 +541,47 @@ lib.objectFromPath = function(path, value) { return obj; }; + +/** + * Converts value to string separated by the provided separators. + * + * @example + * lib.numSeparate(2016, '.,'); + * // returns '2016' + * + * @example + * lib.numSeparate(1234.56, '|,') + * // returns '1,234|56' + * + * @param {string|number} value the value to be converted + * @param {string} separators string of decimal, then thousands separators + * + * @return {string} the value that has been separated + */ +lib.numSeparate = function(value, separators) { + + if(typeof separators !== 'string' || separators.length === 0) { + throw new Error('Separator string required for formatting!'); + } + + if(typeof value === 'number') { + value = String(value); + } + + var thousandsRe = /(\d+)(\d{3})/, + decimalSep = separators.charAt(0), + thouSep = separators.charAt(1); + + var x = value.split('.'), + x1 = x[0], + x2 = x.length > 1 ? decimalSep + x[1] : ''; + + // Years are ignored for thousands separators + if(thouSep && (x.length > 1 || x1.length>4)) { + while(thousandsRe.test(x1)) { + x1 = x1.replace(thousandsRe, '$1' + thouSep + '$2'); + } + } + + return x1 + x2; +}; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 461bb077744..5c61c8b42ec 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1105,7 +1105,7 @@ function numFormat(v, ax, fmtoverride, hover) { if(dp) v = v.substr(0, dp + tickRound).replace(/\.?0+$/, ''); } // insert appropriate decimal point and thousands separator - v = numSeparate(v, ax._gd._fullLayout.separators); + v = Lib.numSeparate(v, ax._gd._fullLayout.separators); } // add exponent @@ -1141,27 +1141,6 @@ function numFormat(v, ax, fmtoverride, hover) { return v; } -// add arbitrary decimal point and thousands separator -var findThousands = /(\d+)(\d{3})/; -function numSeparate(nStr, separators) { - // separators - first char is decimal point, - // next char is thousands separator if there is one - - var dp = separators.charAt(0), - thou = separators.charAt(1), - x = nStr.split('.'), - x1 = x[0], - x2 = x.length > 1 ? dp + x[1] : ''; - // even if there is a thousands separator, don't use it on - // 4-digit integers (like years) - if(thou && (x.length > 1 || x1.length>4)) { - while(findThousands.test(x1)) { - x1 = x1.replace(findThousands, '$1' + thou + '$2'); - } - } - return x1 + x2; -} - axes.subplotMatch = /^x([0-9]*)y([0-9]*)$/; diff --git a/src/traces/pie/calc.js b/src/traces/pie/calc.js index 032f89640d4..be1a2538405 100644 --- a/src/traces/pie/calc.js +++ b/src/traces/pie/calc.js @@ -107,14 +107,15 @@ module.exports = function calc(gd, trace) { hasText = trace.textinfo.indexOf('text') !== -1, hasValue = trace.textinfo.indexOf('value') !== -1, hasPercent = trace.textinfo.indexOf('percent') !== -1, + separators = fullLayout.separators, thisText; for(i = 0; i < cd.length; i++) { pt = cd[i]; thisText = hasLabel ? [pt.label] : []; if(hasText && trace.text[pt.i]) thisText.push(trace.text[pt.i]); - if(hasValue) thisText.push(helpers.formatPieValue(pt.v)); - if(hasPercent) thisText.push(helpers.formatPiePercent(pt.v / vTotal)); + if(hasValue) thisText.push(helpers.formatPieValue(pt.v, separators)); + if(hasPercent) thisText.push(helpers.formatPiePercent(pt.v / vTotal, separators)); pt.text = thisText.join('
'); } } diff --git a/src/traces/pie/helpers.js b/src/traces/pie/helpers.js index 653f6dfad8c..71323340346 100644 --- a/src/traces/pie/helpers.js +++ b/src/traces/pie/helpers.js @@ -8,14 +8,20 @@ 'use strict'; -exports.formatPiePercent = function formatPiePercent(v) { +var Lib = require('../../lib'); + +exports.formatPiePercent = function formatPiePercent(v, separators) { var vRounded = (v * 100).toPrecision(3); - if(vRounded.indexOf('.') !== -1) return vRounded.replace(/[.]?0+$/,'') + '%'; - return vRounded + '%'; + if(vRounded.lastIndexOf('.') !== -1) { + vRounded = vRounded.replace(/[.]?0+$/, ''); + } + return Lib.numSeparate(vRounded, separators) + '%'; }; -exports.formatPieValue = function formatPieValue(v) { +exports.formatPieValue = function formatPieValue(v, separators) { var vRounded = v.toPrecision(10); - if(vRounded.indexOf('.') !== -1) return vRounded.replace(/[.]?0+$/,''); - return vRounded; + if(vRounded.lastIndexOf('.') !== -1) { + vRounded = vRounded.replace(/[.]?0+$/, ''); + } + return Lib.numSeparate(vRounded, separators); }; diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index bea712eb9fb..44e48405376 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -103,13 +103,15 @@ module.exports = function plot(gd, cdpie) { var rInscribed = getInscribedRadiusFraction(pt, cd0), hoverCenterX = cx + pt.pxmid[0] * (1 - rInscribed), hoverCenterY = cy + pt.pxmid[1] * (1 - rInscribed), + separators = fullLayout.separators, thisText = []; + if(hoverinfo.indexOf('label') !== -1) thisText.push(pt.label); if(trace2.text && trace2.text[pt.i] && hoverinfo.indexOf('text') !== -1) { thisText.push(trace2.text[pt.i]); } - if(hoverinfo.indexOf('value') !== -1) thisText.push(helpers.formatPieValue(pt.v)); - if(hoverinfo.indexOf('percent') !== -1) thisText.push(helpers.formatPiePercent(pt.v / cd0.vTotal)); + if(hoverinfo.indexOf('value') !== -1) thisText.push(helpers.formatPieValue(pt.v, separators)); + if(hoverinfo.indexOf('percent') !== -1) thisText.push(helpers.formatPiePercent(pt.v / cd0.vTotal, separators)); Fx.loneHover({ x0: hoverCenterX - rInscribed * cd0.r, diff --git a/test/image/baselines/pie_fonts.png b/test/image/baselines/pie_fonts.png index 1a58140d2f7..ed8b084a10b 100644 Binary files a/test/image/baselines/pie_fonts.png and b/test/image/baselines/pie_fonts.png differ diff --git a/test/image/mocks/pie_fonts.json b/test/image/mocks/pie_fonts.json index 6a88b272e27..2961f3ed035 100644 --- a/test/image/mocks/pie_fonts.json +++ b/test/image/mocks/pie_fonts.json @@ -2,8 +2,8 @@ "data": [ { "values": [ - 1, - 9 + 1.151, + 8.134 ], "text": [ "inherit from
global...", @@ -17,8 +17,8 @@ }, { "values": [ - 1, - 9 + 1.151, + 8.134 ], "text": [ "a font of...", @@ -37,8 +37,8 @@ }, { "values": [ - 1, - 9 + 1.151, + 8.134 ], "text": [ "outside and textfont
both modify...", @@ -58,8 +58,8 @@ }, { "values": [ - 1, - 9 + 1.151, + 8.134 ], "text": [ "independent fonts
outside and...", @@ -89,6 +89,7 @@ "family": "Old Standard TT, serif", "size": 20 }, - "showlegend": true + "showlegend": true, + "separators": "∆|" } } diff --git a/test/jasmine/tests/hover_pie_test.js b/test/jasmine/tests/hover_pie_test.js index 7d96d12daf1..e7472b33951 100644 --- a/test/jasmine/tests/hover_pie_test.js +++ b/test/jasmine/tests/hover_pie_test.js @@ -111,4 +111,57 @@ describe('pie hovering', function() { }, 100); }); }); + + describe('labels', function() { + + var gd, + mockCopy; + + beforeEach(function() { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); + }); + + afterEach(destroyGraphDiv); + + it('should show the default selected values', function(done) { + + var expected = ['4', '5', '33.3%']; + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + + mouseEvent('mouseover', 230, 150); + + var labels = Plotly.d3.selectAll('.hovertext .nums .line'); + + expect(labels[0].length).toBe(3); + + labels.each(function(_, i) { + expect(Plotly.d3.select(this).text()).toBe(expected[i]); + }); + }).then(done); + }); + + it('should show the correct separators for values', function(done) { + + var expected = ['0', '12|345|678@91', '99@9%']; + + mockCopy.layout.separators = '@|'; + mockCopy.data[0].values[0] = 12345678.912; + mockCopy.data[0].values[1] = 10000; + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + + mouseEvent('mouseover', 230, 150); + + var labels = Plotly.d3.selectAll('.hovertext .nums .line'); + + expect(labels[0].length).toBe(3); + + labels.each(function(_, i) { + expect(Plotly.d3.select(this).text()).toBe(expected[i]); + }); + }).then(done); + }); + }); }); diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index d91328e1dc2..b87f45de5a3 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -930,4 +930,35 @@ describe('Test lib.js:', function() { }); }); + + describe('numSeparate', function() { + + it('should work on numbers and strings', function() { + expect(Lib.numSeparate(12345.67, '.,')).toBe('12,345.67'); + expect(Lib.numSeparate('12345.67', '.,')).toBe('12,345.67'); + }); + + it('should ignore years', function() { + expect(Lib.numSeparate(2016, '.,')).toBe('2016'); + }); + + it('should work for multiple thousands', function() { + expect(Lib.numSeparate(1000000000, '.,')).toBe('1,000,000,000'); + }); + + it('should work when there\'s only one separator', function() { + expect(Lib.numSeparate(12.34, '|')).toBe('12|34'); + expect(Lib.numSeparate(1234.56, '|')).toBe('1234|56'); + }); + + it('should throw an error when no separator is provided', function() { + expect(function() { + Lib.numSeparate(1234); + }).toThrowError('Separator string required for formatting!'); + + expect(function() { + Lib.numSeparate(1234, ''); + }).toThrowError('Separator string required for formatting!'); + }); + }); });