Skip to content

Commit eacc99b

Browse files
committed
use d3.locale formats just like the localization dicts of #2195
1 parent 599dff9 commit eacc99b

File tree

14 files changed

+343
-71
lines changed

14 files changed

+343
-71
lines changed

lib/locale-en-us.js

+3
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@ module.exports = {
1313
name: 'en-US',
1414
dictionary: {
1515
'Click to enter Colorscale title': 'Click to enter Colorscale title'
16+
},
17+
format: {
18+
date: '%m/%d/%Y'
1619
}
1720
};

lib/locale-en.js

+20
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,25 @@ module.exports = {
1313
name: 'en',
1414
dictionary: {
1515
'Click to enter Colorscale title': 'Click to enter Colourscale title'
16+
},
17+
format: {
18+
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
19+
shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
20+
months: [
21+
'January', 'February', 'March', 'April', 'May', 'June',
22+
'July', 'August', 'September', 'October', 'November', 'December'
23+
],
24+
shortMonths: [
25+
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
26+
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
27+
],
28+
periods: ['AM', 'PM'],
29+
dateTime: '%a %b %e %X %Y',
30+
date: '%d/%m/%Y',
31+
time: '%H:%M:%S',
32+
decimal: '.',
33+
thousands: ',',
34+
grouping: [3],
35+
currency: ['$', '']
1636
}
1737
};

src/lib/dates.js

+17-13
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ exports.cleanDate = function(v, dflt, calendar) {
367367
* %{n}f where n is the max number of digits of fractional seconds
368368
*/
369369
var fracMatch = /%\d?f/g;
370-
function modDateFormat(fmt, x, calendar) {
370+
function modDateFormat(fmt, x, formatter, calendar) {
371371

372372
fmt = fmt.replace(fracMatch, function(match) {
373373
var digits = Math.min(+(match.charAt(1)) || 6, 6),
@@ -387,7 +387,7 @@ function modDateFormat(fmt, x, calendar) {
387387
return 'Invalid';
388388
}
389389
}
390-
return utcFormat(fmt)(d);
390+
return formatter(fmt)(d);
391391
}
392392

393393
/*
@@ -433,10 +433,12 @@ function formatTime(x, tr) {
433433
return timeStr;
434434
}
435435

436-
var yearFormat = utcFormat('%Y'),
437-
monthFormat = utcFormat('%b %Y'),
438-
dayFormat = utcFormat('%b %-d'),
439-
yearMonthDayFormat = utcFormat('%b %-d, %Y');
436+
// TODO: do these strings need to be localized?
437+
// ie this gives "Dec 13, 2017" but some languages may want eg "13-Dec 2017"
438+
var yearFormatD3 = '%Y';
439+
var monthFormatD3 = '%b %Y';
440+
var dayFormatD3 = '%b %-d';
441+
var yearMonthDayFormatD3 = '%b %-d, %Y';
440442

441443
function yearFormatWorld(cDate) { return cDate.formatDate('yyyy'); }
442444
function monthFormatWorld(cDate) { return cDate.formatDate('M yyyy'); }
@@ -450,6 +452,8 @@ function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy');
450452
* fmt: optional, an explicit format string (d3 format, even for world calendars)
451453
* tr: tickround ('y', 'm', 'd', 'M', 'S', or # digits)
452454
* used if no explicit fmt is provided
455+
* formatter: locale-aware d3 date formatter for standard gregorian calendars
456+
* should be the result of exports.getD3DateFormat(gd)
453457
* calendar: optional string, the world calendar system to use
454458
*
455459
* returns the date/time as a string, potentially with the leading portion
@@ -458,13 +462,13 @@ function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy');
458462
* the axis may choose to strip things after it when they don't change from
459463
* one tick to the next (as it does with automatic formatting)
460464
*/
461-
exports.formatDate = function(x, fmt, tr, calendar) {
465+
exports.formatDate = function(x, fmt, tr, formatter, calendar) {
462466
var headStr,
463467
dateStr;
464468

465469
calendar = isWorldCalendar(calendar) && calendar;
466470

467-
if(fmt) return modDateFormat(fmt, x, calendar);
471+
if(fmt) return modDateFormat(fmt, x, formatter, calendar);
468472

469473
if(calendar) {
470474
try {
@@ -488,14 +492,14 @@ exports.formatDate = function(x, fmt, tr, calendar) {
488492
else {
489493
var d = new Date(Math.floor(x + 0.05));
490494

491-
if(tr === 'y') dateStr = yearFormat(d);
492-
else if(tr === 'm') dateStr = monthFormat(d);
495+
if(tr === 'y') dateStr = formatter(yearFormatD3)(d);
496+
else if(tr === 'm') dateStr = formatter(monthFormatD3)(d);
493497
else if(tr === 'd') {
494-
headStr = yearFormat(d);
495-
dateStr = dayFormat(d);
498+
headStr = formatter(yearFormatD3)(d);
499+
dateStr = formatter(dayFormatD3)(d);
496500
}
497501
else {
498-
headStr = yearMonthDayFormat(d);
502+
headStr = formatter(yearMonthDayFormatD3)(d);
499503
dateStr = formatTime(x, tr);
500504
}
501505
}

src/plot_api/plot_config.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,20 @@ module.exports = {
137137

138138
// Localization dictionaries
139139
// Dictionaries can be provided either here (specific to one chart) or globally
140-
// by registering them as modules.
140+
// by registering them as modules (which contain dateFormat specs as well).
141141
// Here `dictionaries` should be an object of objects
142142
// {'da': {'Reset axes': 'Nulstil aksler', ...}, ...}
143143
// When looking for a translation we look at these dictionaries first, then
144144
// the ones registered as modules. If those fail, we strip off any
145145
// regionalization ('en-US' -> 'en') and try each again
146-
dictionaries: {}
146+
dictionaries: {},
147+
148+
// Localization specs for dates and numbers
149+
// Each localization should be an object with keys matching most of d3.locale,
150+
// see https://github.com/d3/d3-3.x-api-reference/blob/master/Localization.md
151+
// {'da': {months: [...], shortMonths: [...], ...}, ...}
152+
// Unlike d3.locale, every key is optional, we will fall back on English ('en').
153+
// Currently `grouping` and `currency` are ignored for our automatic number
154+
// formatting, but can be used in custom formats.
155+
formats: {}
147156
};

src/plots/cartesian/axes.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1273,7 +1273,7 @@ function formatDate(ax, out, hover, extraPrecision) {
12731273
else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 4}[tr];
12741274
}
12751275

1276-
var dateStr = Lib.formatDate(out.x, fmt, tr, ax.calendar),
1276+
var dateStr = Lib.formatDate(out.x, fmt, tr, ax._dateFormat, ax.calendar),
12771277
headStr;
12781278

12791279
var splitIndex = dateStr.indexOf('\n');
@@ -1451,7 +1451,7 @@ function numFormat(v, ax, fmtoverride, hover) {
14511451
if(ax.hoverformat) tickformat = ax.hoverformat;
14521452
}
14531453

1454-
if(tickformat) return d3.format(tickformat)(v).replace(/-/g, MINUS_SIGN);
1454+
if(tickformat) return ax._numFormat(tickformat)(v).replace(/-/g, MINUS_SIGN);
14551455

14561456
// 'epsilon' - rounding increment
14571457
var e = Math.pow(10, -tickRound) / 2;

src/plots/cartesian/set_convert.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -447,9 +447,19 @@ module.exports = function setConvert(ax, fullLayout) {
447447
ax._min = [];
448448
ax._max = [];
449449

450-
// copy ref to fullLayout.separators so that
450+
// Fropagate localization into the axis so that
451451
// methods in Axes can use it w/o having to pass fullLayout
452+
// Default (non-d3) number formatting uses separators directly
453+
// dates and d3-formatted numbers use the d3 locale
454+
// Fall back on default format for dummy axes that don't care about formatting
455+
var locale = fullLayout._d3locale;
456+
if(ax.type === 'date') {
457+
ax._dateFormat = locale ? locale.timeFormat.utc : d3.time.format.utc;
458+
}
459+
// occasionally we need _numFormat to pass through
460+
// even though it won't be needed by this axis
452461
ax._separators = fullLayout.separators;
462+
ax._numFormat = locale ? locale.numberFormat : d3.format;
453463

454464
// and for bar charts and box plots: reset forced minimum tick spacing
455465
delete ax._minDtick;

src/plots/layout_attributes.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,12 @@ module.exports = {
147147
separators: {
148148
valType: 'string',
149149
role: 'style',
150-
dflt: '.,',
151150
editType: 'plot',
152151
description: [
153152
'Sets the decimal and thousand separators.',
154-
'For example, *. * puts a \'.\' before decimals and',
155-
'a space between thousands.'
153+
'For example, *. * puts a \'.\' before decimals and a space',
154+
'between thousands. In English locales, dflt is *.,* but',
155+
'other locales may alter this default.'
156156
].join(' ')
157157
},
158158
hidesources: {

src/plots/plots.js

+85-4
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,8 @@ plots.supplyDefaults = function(gd) {
438438
};
439439
newFullLayout._traceWord = _(gd, 'trace');
440440

441+
var formatObj = getD3FormatObj(gd);
442+
441443
// first fill in what we can of layout without looking at data
442444
// because fullData needs a few things from layout
443445

@@ -447,15 +449,15 @@ plots.supplyDefaults = function(gd) {
447449
var oldWidth = oldFullLayout.width,
448450
oldHeight = oldFullLayout.height;
449451

450-
plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout);
452+
plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout, formatObj);
451453

452454
if(!newLayout.width) newFullLayout.width = oldWidth;
453455
if(!newLayout.height) newFullLayout.height = oldHeight;
454456
}
455457
else {
456458

457459
// coerce the updated layout and autosize if needed
458-
plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout);
460+
plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout, formatObj);
459461

460462
var missingWidthOrHeight = (!newLayout.width || !newLayout.height),
461463
autosize = newFullLayout.autosize,
@@ -472,6 +474,8 @@ plots.supplyDefaults = function(gd) {
472474
}
473475
}
474476

477+
newFullLayout._d3locale = getFormatter(formatObj, newFullLayout.separators);
478+
475479
newFullLayout._initialAutoSizeIsDone = true;
476480

477481
// keep track of how many traces are inputted
@@ -563,6 +567,83 @@ function remapTransformedArrays(cd0, newTrace) {
563567
}
564568
}
565569

570+
var formatKeys = [
571+
'days', 'shortDays', 'months', 'shortMonths', 'periods',
572+
'dateTime', 'date', 'time',
573+
'decimal', 'thousands', 'grouping', 'currency'
574+
];
575+
576+
/**
577+
* getD3FormatObj: use _context to get the d3.locale argument object.
578+
* decimal and thousands can be overridden later by layout.separators
579+
* grouping and currency are not presently used by our automatic number
580+
* formatting system but can be used by custom formats.
581+
*
582+
* @returns {object} d3.locale format object
583+
*/
584+
function getD3FormatObj(gd) {
585+
var locale = gd._context.locale;
586+
if(!locale) locale === 'en-US';
587+
588+
var formatDone = false;
589+
var formatObj = {};
590+
591+
function includeFormat(newFormat) {
592+
var formatFinished = true;
593+
for(var i = 0; i < formatKeys.length; i++) {
594+
var formatKey = formatKeys[i];
595+
if(!formatObj[formatKey]) {
596+
if(newFormat[formatKey]) {
597+
formatObj[formatKey] = newFormat[formatKey];
598+
}
599+
else formatFinished = false;
600+
}
601+
}
602+
if(formatFinished) formatDone = true;
603+
}
604+
605+
// same as localize, look for format parts in each format spec in the chain
606+
for(var i = 0; i < 2; i++) {
607+
var formats = gd._context.formats;
608+
for(var j = 0; j < 2; j++) {
609+
var formatj = formats[locale];
610+
if(formatj) {
611+
includeFormat(formatj);
612+
if(formatDone) break;
613+
}
614+
formats = Registry.formatRegistry;
615+
}
616+
617+
var baseLocale = locale.split('-')[0];
618+
if(formatDone || baseLocale === locale) break;
619+
locale = baseLocale;
620+
}
621+
622+
// lastly pick out defaults from english (non-US, as DMY is so much more common)
623+
if(!formatDone) includeFormat(Registry.formatRegistry.en);
624+
625+
return formatObj;
626+
}
627+
628+
/**
629+
* getFormatter: combine the final separators with the locale formatting object
630+
* we pulled earlier to generate number and time formatters
631+
* TODO: remove separators in v2, only use locale, so we don't need this step?
632+
*
633+
* @param {object} formatObj: d3.locale format object
634+
* @param {string} separators: length-2 string to override decimal and thousands
635+
* separators in number formatting
636+
*
637+
* @returns {object} {numberFormat, timeFormat} d3 formatter factory functions
638+
* for numbers and time
639+
*/
640+
function getFormatter(formatObj, separators) {
641+
formatObj.decimal = separators.charAt(0);
642+
formatObj.thousands = separators.charAt(1);
643+
644+
return d3.locale(formatObj);
645+
}
646+
566647
// Create storage for all of the data related to frames and transitions:
567648
plots.createTransitionData = function(gd) {
568649
// Set up the default keyframe if it doesn't exist:
@@ -1144,7 +1225,7 @@ function applyTransforms(fullTrace, fullData, layout, fullLayout) {
11441225
return dataOut;
11451226
}
11461227

1147-
plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) {
1228+
plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) {
11481229
function coerce(attr, dflt) {
11491230
return Lib.coerce(layoutIn, layoutOut, plots.layoutAttributes, attr, dflt);
11501231
}
@@ -1183,7 +1264,7 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) {
11831264

11841265
coerce('paper_bgcolor');
11851266

1186-
coerce('separators');
1267+
coerce('separators', formatObj.decimal + formatObj.thousands);
11871268
coerce('hidesources');
11881269

11891270
coerce('colorway');

src/registry.js

+19-3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ exports.layoutArrayContainers = [];
2929
exports.layoutArrayRegexes = [];
3030
exports.traceLayoutAttributes = {};
3131
exports.localeRegistry = {};
32+
exports.formatRegistry = {};
3233

3334
/**
3435
* register a module as the handler for a trace type
@@ -328,23 +329,38 @@ function getTraceType(traceType) {
328329
* the dictionary mapping input strings to localized strings
329330
* generally the keys should be the literal input strings, but
330331
* if default translations are provided you can use any string as a key.
332+
* @param {object} module.format
333+
* a `d3.locale` format specifier for this locale
334+
* any omitted keys we'll fall back on en-US
331335
*/
332336
exports.registerLocale = function(_module) {
333337
var locale = _module.name;
334338
var baseLocale = locale.split('-')[0];
335339

336340
var newDict = _module.dictionary;
341+
var newFormat = _module.format;
342+
var hasDict = newDict && Object.keys(newDict).length;
343+
var hasFormat = newFormat && Object.keys(newFormat).length;
337344

338345
var locales = exports.localeRegistry;
339346

347+
var formats = exports.formatRegistry;
348+
340349
// Should we use this dict for the base locale?
341350
// In case we're overwriting a previous dict for this locale, check
342351
// whether the base matches the full locale dict now. If we're not
343352
// overwriting, locales[locale] is undefined so this just checks if
344353
// baseLocale already had a dict or not.
345-
if(baseLocale !== locale && locales[baseLocale] === locales[locale]) {
346-
locales[baseLocale] = newDict;
354+
// Same logic for dateFormats
355+
if(baseLocale !== locale) {
356+
if(hasDict && locales[baseLocale] === locales[locale]) {
357+
locales[baseLocale] = newDict;
358+
}
359+
if(hasFormat && formats[baseLocale] === formats[locale]) {
360+
formats[baseLocale] = newFormat;
361+
}
347362
}
348363

349-
locales[locale] = newDict;
364+
if(hasDict) locales[locale] = newDict;
365+
if(hasFormat) formats[locale] = newFormat;
350366
};

src/traces/heatmap/hover.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay
2929
zmask = cd0.zmask,
3030
range = [trace.zmin, trace.zmax],
3131
zhoverformat = trace.zhoverformat,
32-
_separators = trace._separators,
3332
x2 = x,
3433
y2 = y,
3534
xl,
@@ -109,7 +108,8 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay
109108
type: 'linear',
110109
range: range,
111110
hoverformat: zhoverformat,
112-
_separators: _separators
111+
_separators: xa._separators,
112+
_numFormat: xa._numFormat
113113
};
114114
var zLabelObj = Axes.tickText(dummyAx, zVal, 'hover');
115115
zLabel = zLabelObj.text;

0 commit comments

Comments
 (0)