From fc86bdb7cb00e2db46feb5c5bb411b527f810b59 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Thu, 18 Jan 2018 14:35:50 +0100 Subject: [PATCH 1/8] Localize auto-formatted x-axis date ticks --- lib/locales/fr.js | 6 +++- src/lib/dates.js | 54 ++++++++++++++++++------------ src/locale-en.js | 18 ++++++---- src/plots/cartesian/axes.js | 2 +- src/plots/cartesian/set_convert.js | 1 + src/plots/plots.js | 16 ++++++--- 6 files changed, 62 insertions(+), 35 deletions(-) diff --git a/lib/locales/fr.js b/lib/locales/fr.js index 1d8bf37a70c..f98c32953f8 100644 --- a/lib/locales/fr.js +++ b/lib/locales/fr.js @@ -23,6 +23,10 @@ module.exports = { 'Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc' ], - date: '%d/%m/%Y' + date: '%d/%m/%Y', + year: '%Y', + month: '%b %Y', + dayMonth: '%e %b', + dayMonthYear: '%e %b %Y' } }; diff --git a/src/lib/dates.js b/src/lib/dates.js index 4bd3823cc32..ece991a4989 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -433,17 +433,27 @@ 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'); } +/* + * formatWorld: format a calendar date using the d3 format syntax. + * + * cDate: the date to format + * d3Format: the d3 format + * + * returns the formatted date + */ +function formatWorld(cDate, d3Format) { + var d3ToC = [ + {d3: '%Y', c: 'yyyy'}, + {d3: '%b', c: 'M'}, + {d3: '%-d', c: 'd'}, + {d3: '%e', c: 'd'} + ]; + var calendarFormat = d3Format; + for (var i = 0; i < d3ToC.length; i++) { + calendarFormat = calendarFormat.replace(new RegExp(d3ToC[i].d3,"g"), d3ToC[i].c); + } + return cDate.formatDate(calendarFormat); +} /* * formatDate: turn a date into tick or hover label text. @@ -462,7 +472,7 @@ 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) { +exports.formatDate = function(x, fmt, tr, formatter, calendar, extraFormat) { var headStr, dateStr; @@ -476,14 +486,14 @@ exports.formatDate = function(x, fmt, tr, formatter, calendar) { cDate = Registry.getComponentMethod('calendars', 'getCal')(calendar) .fromJD(dateJD); - if(tr === 'y') dateStr = yearFormatWorld(cDate); - else if(tr === 'm') dateStr = monthFormatWorld(cDate); + if(tr === 'y') dateStr = formatWorld(cDate, extraFormat.year); + else if(tr === 'm') dateStr = formatWorld(cDate, extraFormat.month); else if(tr === 'd') { - headStr = yearFormatWorld(cDate); - dateStr = dayFormatWorld(cDate); + headStr = formatWorld(cDate, extraFormat.year); + dateStr = formatWorld(cDate, extraFormat.dayMonth); } else { - headStr = yearMonthDayFormatWorld(cDate); + headStr = formatWorld(cDate, extraFormat.dayMonthYear); dateStr = formatTime(x, tr); } } @@ -492,14 +502,14 @@ exports.formatDate = function(x, fmt, tr, formatter, calendar) { 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(tr === 'y') dateStr = formatter(extraFormat.year)(d); + else if(tr === 'm') dateStr = formatter(extraFormat.month)(d); else if(tr === 'd') { - headStr = formatter(yearFormatD3)(d); - dateStr = formatter(dayFormatD3)(d); + headStr = formatter(extraFormat.year)(d); + dateStr = formatter(extraFormat.dayMonth)(d); } else { - headStr = formatter(yearMonthDayFormatD3)(d); + headStr = formatter(extraFormat.dayMonthYear)(d); dateStr = formatTime(x, tr); } } diff --git a/src/locale-en.js b/src/locale-en.js index 10e13d92acb..f5743a4ec90 100644 --- a/src/locale-en.js +++ b/src/locale-en.js @@ -1,10 +1,10 @@ /** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ + * Copyright 2012-2018, Plotly, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ 'use strict'; @@ -32,6 +32,10 @@ module.exports = { decimal: '.', thousands: ',', grouping: [3], - currency: ['$', ''] + currency: ['$', ''], + year: '%Y', + month: '%b %Y', + dayMonth: '%b %e', + dayMonthYear: '%b %e, %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..7f6b3fc14e4 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -305,7 +305,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 +342,7 @@ plots.supplyDefaults = function(gd) { } newFullLayout._d3locale = getFormatter(formatObj, newFullLayout.separators); + newFullLayout._extraFormat = getFormatObj(gd, extraFormatKeys); newFullLayout._initialAutoSizeIsDone = true; @@ -481,21 +482,28 @@ function remapTransformedArrays(cd0, newTrace) { } } -var formatKeys = [ +var d3FormatKeys = [ 'days', 'shortDays', 'months', 'shortMonths', 'periods', 'dateTime', 'date', 'time', 'decimal', 'thousands', 'grouping', 'currency' ]; +var extraFormatKeys = [ + 'year', 'month', 'dayMonth', 'dayMonthYear' +]; + /** - * 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'; From aebb07c7898891c1866d49815b04528566b3ff32 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Thu, 18 Jan 2018 18:49:10 +0100 Subject: [PATCH 2/8] changes from @alexcjohnson --- src/lib/dates.js | 62 +++++------------------------------------------- 1 file changed, 6 insertions(+), 56 deletions(-) diff --git a/src/lib/dates.js b/src/lib/dates.js index ece991a4989..8d28045ce53 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -433,28 +433,6 @@ function formatTime(x, tr) { return timeStr; } -/* - * formatWorld: format a calendar date using the d3 format syntax. - * - * cDate: the date to format - * d3Format: the d3 format - * - * returns the formatted date - */ -function formatWorld(cDate, d3Format) { - var d3ToC = [ - {d3: '%Y', c: 'yyyy'}, - {d3: '%b', c: 'M'}, - {d3: '%-d', c: 'd'}, - {d3: '%e', c: 'd'} - ]; - var calendarFormat = d3Format; - for (var i = 0; i < d3ToC.length; i++) { - calendarFormat = calendarFormat.replace(new RegExp(d3ToC[i].d3,"g"), d3ToC[i].c); - } - return cDate.formatDate(calendarFormat); -} - /* * formatDate: turn a date into tick or hover label text. * @@ -473,48 +451,20 @@ function formatWorld(cDate, d3Format) { * one tick to the next (as it does with automatic formatting) */ exports.formatDate = function(x, fmt, tr, formatter, calendar, extraFormat) { - var headStr, - dateStr; - 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 = formatWorld(cDate, extraFormat.year); - else if(tr === 'm') dateStr = formatWorld(cDate, extraFormat.month); - else if(tr === 'd') { - headStr = formatWorld(cDate, extraFormat.year); - dateStr = formatWorld(cDate, extraFormat.dayMonth); - } - else { - headStr = formatWorld(cDate, extraFormat.dayMonthYear); - dateStr = formatTime(x, tr); - } - } - catch(e) { return 'Invalid'; } - } - else { - var d = new Date(Math.floor(x + 0.05)); - - if(tr === 'y') dateStr = formatter(extraFormat.year)(d); - else if(tr === 'm') dateStr = formatter(extraFormat.month)(d); + if(!fmt) { + if(tr === 'y') fmt = extraFormat.year; + else if(tr === 'm') fmt = extraFormat.month; else if(tr === 'd') { - headStr = formatter(extraFormat.year)(d); - dateStr = formatter(extraFormat.dayMonth)(d); + fmt = extraFormat.dayMonth + '\n' + extraFormat.year; } else { - headStr = formatter(extraFormat.dayMonthYear)(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); }; /* From 8e257defee376e52cd3475548e8942df34d00aaa Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Thu, 18 Jan 2018 19:17:57 +0100 Subject: [PATCH 3/8] Switch back to %-d to avoid space padding --- lib/locales/fr.js | 3 ++- src/locale-en.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/locales/fr.js b/lib/locales/fr.js index 1c7e0df37da..f4a89a884a3 100644 --- a/lib/locales/fr.js +++ b/lib/locales/fr.js @@ -87,6 +87,7 @@ module.exports = { thousands: ' ', year: '%Y', month: '%b %Y', - dayMonth: '%e %b' + dayMonth: '%-d %b', + dayMonthYear: '%-d %b %Y' } }; diff --git a/src/locale-en.js b/src/locale-en.js index f5743a4ec90..70b56abaebc 100644 --- a/src/locale-en.js +++ b/src/locale-en.js @@ -35,7 +35,7 @@ module.exports = { currency: ['$', ''], year: '%Y', month: '%b %Y', - dayMonth: '%b %e', - dayMonthYear: '%b %e, %Y' + dayMonth: '%b %-d', + dayMonthYear: '%b %-d, %Y' } }; From e6250a0065714a49bc1ee0d030bd0ba6a3d9afaf Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Mon, 22 Jan 2018 10:08:51 +0100 Subject: [PATCH 4/8] Fix axes_test (missing _extraFormats member in fullLayout parameter passed to setConvert) --- test/jasmine/tests/axes_test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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; }); } From d79d00a1aa2931c99ad506931fab51a6b5012f00 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Mon, 22 Jan 2018 10:19:19 +0100 Subject: [PATCH 5/8] Fix lib_date_test (missing extraFormats parameter passed to formatDate) --- test/jasmine/tests/lib_date_test.js | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) 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'); }); }); From 14a14d897705bb56756dc7d5c6bad950c5c58cbc Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Mon, 22 Jan 2018 10:40:42 +0100 Subject: [PATCH 6/8] Move up formatKeys array to respect syntax declaration order --- src/plots/plots.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 7f6b3fc14e4..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: @@ -482,16 +492,6 @@ function remapTransformedArrays(cd0, newTrace) { } } -var d3FormatKeys = [ - 'days', 'shortDays', 'months', 'shortMonths', 'periods', - 'dateTime', 'date', 'time', - 'decimal', 'thousands', 'grouping', 'currency' -]; - -var extraFormatKeys = [ - 'year', 'month', 'dayMonth', 'dayMonthYear' -]; - /** * getFormatObj: use _context to get the format object from locale. * Used to get d3.locale argument object and extraFormat argument object From f90efdca5b340234223a31c26294570a0423db60 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Mon, 22 Jan 2018 11:06:58 +0100 Subject: [PATCH 7/8] Fix locale-en.js header syntax --- src/locale-en.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/locale-en.js b/src/locale-en.js index 70b56abaebc..a6805bd5c91 100644 --- a/src/locale-en.js +++ b/src/locale-en.js @@ -1,10 +1,10 @@ /** - * Copyright 2012-2018, Plotly, Inc. - * All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ 'use strict'; From 58acd275fe83a2249b2624e645f70ce736703fae Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Mon, 22 Jan 2018 11:45:22 +0100 Subject: [PATCH 8/8] Add new test to localize_test to handle new locale format parameters --- test/jasmine/tests/localize_test.js | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) 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); + }); });