Skip to content

Localize auto-formatted x-axis date ticks #2261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

6 changes: 5 additions & 1 deletion lib/locales/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
};
54 changes: 7 additions & 47 deletions src/lib/dates.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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);
};

/*
Expand Down
6 changes: 5 additions & 1 deletion src/locale-en.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ module.exports = {
decimal: '.',
thousands: ',',
grouping: [3],
currency: ['$', '']
currency: ['$', ''],
year: '%Y',
month: '%b %Y',
dayMonth: '%b %-d',
dayMonthYear: '%b %-d, %Y'
}
};
2 changes: 1 addition & 1 deletion src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
1 change: 1 addition & 0 deletions src/plots/cartesian/set_convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 17 additions & 9 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -342,6 +352,7 @@ plots.supplyDefaults = function(gd) {
}

newFullLayout._d3locale = getFormatter(formatObj, newFullLayout.separators);
newFullLayout._extraFormat = getFormatObj(gd, extraFormatKeys);

newFullLayout._initialAutoSizeIsDone = true;

Expand Down Expand Up @@ -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';

Expand Down
7 changes: 6 additions & 1 deletion test/jasmine/tests/axes_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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; });
}

Expand Down
27 changes: 19 additions & 8 deletions test/jasmine/tests/lib_date_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
Expand Down Expand Up @@ -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');
});

});
Expand Down
50 changes: 50 additions & 0 deletions test/jasmine/tests/localize_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});