Skip to content
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

Localize auto-formatted x-axis date ticks #2261

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

Expand All @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modDateFormat calls out to components/calendars to get the full spectrum of d3-to-world-cal conversions - you've got the ones needed so far, but others will likely show up, and no need to reinvent the wheel with formatWorld.

At one point there was a performance argument for the structure we have here because we could use precompiled formatters... but we lost that benefit with the original date localization PR #2207, so now I think there would be a much more concise way to do this, something like:

calendar = isWorldCalendar(calendar) && calendar;

if(!fmt) {
    if(tr === 'y') fmt = extraFormat.year;
    else if(tr === 'm') fmt = extraFormat.month;
    else if(tr === 'd') {
        fmt = extraFormat.dayMonth + '\n' + extraFormat.year;
    }
    else {
        return modDateFormat(extraFormat.dayMonthYear, x, formatter, calendar) +
            '\n' + formatTime(x, tr);
    }
}

return modDateFormat(fmt, x, formatter, calendar);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I suspected that this conversion table existed somewhere but I didn't know exactly where to look (and code search with date/format/etc returned way too many entries).

I like the new proposal, pretty clear to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just test and it does work indeed on my example (only the last else is incorrect, need to switch date and time).

}
else {
headStr = yearMonthDayFormatWorld(cDate);
headStr = formatWorld(cDate, extraFormat.dayMonthYear);
dateStr = formatTime(x, tr);
}
}
Expand All @@ -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);
}
}
Expand Down
18 changes: 11 additions & 7 deletions src/locale-en.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -32,6 +32,10 @@ module.exports = {
decimal: '.',
thousands: ',',
grouping: [3],
currency: ['$', '']
currency: ['$', ''],
year: '%Y',
month: '%b %Y',
dayMonth: '%b %e',
dayMonthYear: '%b %e, %Y'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%e is padded with a space for 1-digit days - we should switch back to %-d, which has no padding (in fr.js as well). That's responsible for the image test failures.

}
};
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
16 changes: 12 additions & 4 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -342,6 +342,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 +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';

Expand Down