diff --git a/lib/locales/fr.js b/lib/locales/fr.js index fc25903debb..f4a89a884a3 100644 --- a/lib/locales/fr.js +++ b/lib/locales/fr.js @@ -84,6 +84,10 @@ module.exports = { ], date: '%d/%m/%Y', decimal: ',', - thousands: ' ' + thousands: ' ', + year: '%Y', + month: '%b %Y', + dayMonth: '%-d %b', + dayMonthYear: '%-d %b %Y' } }; diff --git a/src/lib/dates.js b/src/lib/dates.js index 4bd3823cc32..8d28045ce53 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -433,18 +433,6 @@ function formatTime(x, tr) { return timeStr; } -// TODO: do these strings need to be localized? -// ie this gives "Dec 13, 2017" but some languages may want eg "13-Dec 2017" -var yearFormatD3 = '%Y'; -var monthFormatD3 = '%b %Y'; -var dayFormatD3 = '%b %-d'; -var yearMonthDayFormatD3 = '%b %-d, %Y'; - -function yearFormatWorld(cDate) { return cDate.formatDate('yyyy'); } -function monthFormatWorld(cDate) { return cDate.formatDate('M yyyy'); } -function dayFormatWorld(cDate) { return cDate.formatDate('M d'); } -function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy'); } - /* * formatDate: turn a date into tick or hover label text. * @@ -462,49 +450,21 @@ function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy'); * the axis may choose to strip things after it when they don't change from * one tick to the next (as it does with automatic formatting) */ -exports.formatDate = function(x, fmt, tr, formatter, calendar) { - var headStr, - dateStr; - +exports.formatDate = function(x, fmt, tr, formatter, calendar, extraFormat) { calendar = isWorldCalendar(calendar) && calendar; - if(fmt) return modDateFormat(fmt, x, formatter, calendar); - - if(calendar) { - try { - var dateJD = Math.floor((x + 0.05) / ONEDAY) + EPOCHJD, - cDate = Registry.getComponentMethod('calendars', 'getCal')(calendar) - .fromJD(dateJD); - - if(tr === 'y') dateStr = yearFormatWorld(cDate); - else if(tr === 'm') dateStr = monthFormatWorld(cDate); - else if(tr === 'd') { - headStr = yearFormatWorld(cDate); - dateStr = dayFormatWorld(cDate); - } - else { - headStr = yearMonthDayFormatWorld(cDate); - dateStr = formatTime(x, tr); - } - } - catch(e) { return 'Invalid'; } - } - else { - var d = new Date(Math.floor(x + 0.05)); - - if(tr === 'y') dateStr = formatter(yearFormatD3)(d); - else if(tr === 'm') dateStr = formatter(monthFormatD3)(d); + if(!fmt) { + if(tr === 'y') fmt = extraFormat.year; + else if(tr === 'm') fmt = extraFormat.month; else if(tr === 'd') { - headStr = formatter(yearFormatD3)(d); - dateStr = formatter(dayFormatD3)(d); + fmt = extraFormat.dayMonth + '\n' + extraFormat.year; } else { - headStr = formatter(yearMonthDayFormatD3)(d); - dateStr = formatTime(x, tr); + return formatTime(x, tr) + '\n' + modDateFormat(extraFormat.dayMonthYear, x, formatter, calendar); } } - return dateStr + (headStr ? '\n' + headStr : ''); + return modDateFormat(fmt, x, formatter, calendar); }; /* diff --git a/src/locale-en.js b/src/locale-en.js index 10e13d92acb..a6805bd5c91 100644 --- a/src/locale-en.js +++ b/src/locale-en.js @@ -32,6 +32,10 @@ module.exports = { decimal: '.', thousands: ',', grouping: [3], - currency: ['$', ''] + currency: ['$', ''], + year: '%Y', + month: '%b %Y', + dayMonth: '%b %-d', + dayMonthYear: '%b %-d, %Y' } }; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index e8d7e354983..07c09c6eafc 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1292,7 +1292,7 @@ function formatDate(ax, out, hover, extraPrecision) { else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 4}[tr]; } - var dateStr = Lib.formatDate(out.x, fmt, tr, ax._dateFormat, ax.calendar), + var dateStr = Lib.formatDate(out.x, fmt, tr, ax._dateFormat, ax.calendar, ax._extraFormat), headStr; var splitIndex = dateStr.indexOf('\n'); diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 9718f64f23a..01db927a7b9 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -457,6 +457,7 @@ module.exports = function setConvert(ax, fullLayout) { var locale = fullLayout._d3locale; if(ax.type === 'date') { ax._dateFormat = locale ? locale.timeFormat.utc : d3.time.format.utc; + ax._extraFormat = fullLayout._extraFormat; } // occasionally we need _numFormat to pass through // even though it won't be needed by this axis diff --git a/src/plots/plots.js b/src/plots/plots.js index bec27b731ab..ec190393265 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -251,6 +251,16 @@ plots.sendDataToCloud = function(gd) { return false; }; +var d3FormatKeys = [ + 'days', 'shortDays', 'months', 'shortMonths', 'periods', + 'dateTime', 'date', 'time', + 'decimal', 'thousands', 'grouping', 'currency' +]; + +var extraFormatKeys = [ + 'year', 'month', 'dayMonth', 'dayMonthYear' +]; + // Fill in default values: // // gd.data, gd.layout: @@ -305,7 +315,7 @@ plots.supplyDefaults = function(gd) { }; newFullLayout._traceWord = _(gd, 'trace'); - var formatObj = getD3FormatObj(gd); + var formatObj = getFormatObj(gd, d3FormatKeys); // first fill in what we can of layout without looking at data // because fullData needs a few things from layout @@ -342,6 +352,7 @@ plots.supplyDefaults = function(gd) { } newFullLayout._d3locale = getFormatter(formatObj, newFullLayout.separators); + newFullLayout._extraFormat = getFormatObj(gd, extraFormatKeys); newFullLayout._initialAutoSizeIsDone = true; @@ -481,21 +492,18 @@ function remapTransformedArrays(cd0, newTrace) { } } -var formatKeys = [ - 'days', 'shortDays', 'months', 'shortMonths', 'periods', - 'dateTime', 'date', 'time', - 'decimal', 'thousands', 'grouping', 'currency' -]; - /** - * getD3FormatObj: use _context to get the d3.locale argument object. + * getFormatObj: use _context to get the format object from locale. + * Used to get d3.locale argument object and extraFormat argument object + * + * Regarding d3.locale argument : * decimal and thousands can be overridden later by layout.separators * grouping and currency are not presently used by our automatic number * formatting system but can be used by custom formats. * * @returns {object} d3.locale format object */ -function getD3FormatObj(gd) { +function getFormatObj(gd, formatKeys) { var locale = gd._context.locale; if(!locale) locale === 'en-US'; diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 9c05bf23584..e9573167693 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1860,7 +1860,12 @@ describe('Test axes', function() { describe('calcTicks and tickText', function() { function mockCalc(ax) { ax.tickfont = {}; - Axes.setConvert(ax, {separators: '.,'}); + Axes.setConvert(ax, {separators: '.,', _extraFormat: { + year: '%Y', + month: '%b %Y', + dayMonth: '%b %-d', + dayMonthYear: '%b %-d, %Y' + }}); return Axes.calcTicks(ax).map(function(v) { return v.text; }); } diff --git a/test/jasmine/tests/lib_date_test.js b/test/jasmine/tests/lib_date_test.js index 7a24ec16f01..2f4c86f9982 100644 --- a/test/jasmine/tests/lib_date_test.js +++ b/test/jasmine/tests/lib_date_test.js @@ -482,7 +482,12 @@ describe('dates', function() { describe('formatDate', function() { function assertFormatRounds(ms, calendar, results) { ['y', 'm', 'd', 'M', 'S', 1, 2, 3, 4].forEach(function(tr, i) { - expect(Lib.formatDate(ms, '', tr, utcFormat, calendar)) + expect(Lib.formatDate(ms, '', tr, utcFormat, calendar, { + year: '%Y', + month: '%b %Y', + dayMonth: '%b %-d', + dayMonthYear: '%b %-d, %Y' + })) .toBe(results[i], calendar); }); } @@ -598,17 +603,23 @@ describe('dates', function() { }); it('should remove extra fractional second zeros', function() { - expect(Lib.formatDate(0.1, '', 4, utcFormat)).toBe('00:00:00.0001\nJan 1, 1970'); - expect(Lib.formatDate(0.1, '', 3, utcFormat)).toBe('00:00:00\nJan 1, 1970'); - expect(Lib.formatDate(0.1, '', 0, utcFormat)).toBe('00:00:00\nJan 1, 1970'); - expect(Lib.formatDate(0.1, '', 'S', utcFormat)).toBe('00:00:00\nJan 1, 1970'); - expect(Lib.formatDate(0.1, '', 3, utcFormat, 'coptic')) + var extraFormat = { + year: '%Y', + month: '%b %Y', + dayMonth: '%b %-d', + dayMonthYear: '%b %-d, %Y' + }; + expect(Lib.formatDate(0.1, '', 4, utcFormat, null, extraFormat)).toBe('00:00:00.0001\nJan 1, 1970'); + expect(Lib.formatDate(0.1, '', 3, utcFormat, null, extraFormat)).toBe('00:00:00\nJan 1, 1970'); + expect(Lib.formatDate(0.1, '', 0, utcFormat, null, extraFormat)).toBe('00:00:00\nJan 1, 1970'); + expect(Lib.formatDate(0.1, '', 'S', utcFormat, null, extraFormat)).toBe('00:00:00\nJan 1, 1970'); + expect(Lib.formatDate(0.1, '', 3, utcFormat, 'coptic', extraFormat)) .toBe('00:00:00\nKoi 23, 1686'); // because the decimal point is explicitly part of the format // string here, we can't remove it OR the very first zero after it. - expect(Lib.formatDate(0.1, '%S.%f', null, utcFormat)).toBe('00.0001'); - expect(Lib.formatDate(0.1, '%S.%3f', null, utcFormat)).toBe('00.0'); + expect(Lib.formatDate(0.1, '%S.%f', null, utcFormat, null, extraFormat)).toBe('00.0001'); + expect(Lib.formatDate(0.1, '%S.%3f', null, utcFormat, null, extraFormat)).toBe('00.0'); }); }); diff --git a/test/jasmine/tests/localize_test.js b/test/jasmine/tests/localize_test.js index 65394e6ef11..440979e4c05 100644 --- a/test/jasmine/tests/localize_test.js +++ b/test/jasmine/tests/localize_test.js @@ -233,4 +233,54 @@ describe('localization', function() { .catch(failTest) .then(done); }); + + it('uses extraFormat to localize the autoFormatted x-axis date tick', function(done) { + plot('test') + .then(function() { + // test format.month + expect(firstXLabel()).toBe('Jan 2001'); + return Plotly.update(gd, {x: [['2001-01-01', '2001-02-01']]}); + }) + .then(function() { + // test format.dayMonth & format.year + expect(firstXLabel()).toBe('Dec 312000'); + + return Plotly.update(gd, {x: [['2001-01-01', '2001-01-02']]}); + }) + .then(function() { + // test format.dayMonthYear + expect(firstXLabel()).toBe('00:00Jan 1, 2001'); + + Plotly.register({ + moduleType: 'locale', + name: 'test', + format: { + year: 'Y%Y', + month: '%Y %b', + dayMonth: '%-d %b', + dayMonthYear: '%-d %b %Y' + } + }); + + return Plotly.update(gd, {x: [['2001-01-01', '2002-01-01']]}); + }) + .then(function() { + // test format.month + expect(firstXLabel()).toBe('2001 Jan'); + + return Plotly.update(gd, {x: [['2001-01-01', '2001-02-01']]}); + }) + .then(function() { + // test format.dayMonth & format.year + expect(firstXLabel()).toBe('31 DecY2000'); + + return Plotly.update(gd, {x: [['2001-01-01', '2001-01-02']]}); + }) + .then(function() { + // test format.dayMonthYear + expect(firstXLabel()).toBe('00:001 Jan 2001'); + }) + .catch(failTest) + .then(done); + }); });