Merged
6 changes: 5 additions & 1 deletion lib/locales/fr.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -342,6 +352,7 @@ plots.supplyDefaults = function(gd) {
}

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

newFullLayout._initialAutoSizeIsDone = true;

Expand DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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);
});
});
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
6 changes: 5 additions & 1 deletion lib/locales/fr.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -342,6 +352,7 @@ plots.supplyDefaults = function(gd) {
}

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

newFullLayout._initialAutoSizeIsDone = true;

Expand DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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);
});
});
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
6 changes: 5 additions & 1 deletion lib/locales/fr.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -342,6 +352,7 @@ plots.supplyDefaults = function(gd) {
}

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

newFullLayout._initialAutoSizeIsDone = true;

Expand DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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);
});
});
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
6 changes: 5 additions & 1 deletion lib/locales/fr.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -342,6 +352,7 @@ plots.supplyDefaults = function(gd) {
}

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

newFullLayout._initialAutoSizeIsDone = true;

Expand DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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);
});
});
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
6 changes: 5 additions & 1 deletion lib/locales/fr.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -342,6 +352,7 @@ plots.supplyDefaults = function(gd) {
}

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

newFullLayout._initialAutoSizeIsDone = true;

Expand DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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);
});
});
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
6 changes: 5 additions & 1 deletion lib/locales/fr.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -342,6 +352,7 @@ plots.supplyDefaults = function(gd) {
}

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

newFullLayout._initialAutoSizeIsDone = true;

Expand DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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);
});
});
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
6 changes: 5 additions & 1 deletion lib/locales/fr.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -342,6 +352,7 @@ plots.supplyDefaults = function(gd) {
}

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

newFullLayout._initialAutoSizeIsDone = true;

Expand DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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);
});
});
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
6 changes: 5 additions & 1 deletion lib/locales/fr.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -342,6 +352,7 @@ plots.supplyDefaults = function(gd) {
}

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

newFullLayout._initialAutoSizeIsDone = true;

Expand DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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);
});
});