Skip to content

World calendars #1220

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Dec 9, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d2a5e4b
simpleMap utility so we don't pass unexpected args
alexcjohnson Dec 2, 2016
6151575
change gl2d_date_axes mock to date string axis range
alexcjohnson Nov 30, 2016
4b9edec
add world calendar support part 1
alexcjohnson Nov 30, 2016
4d8f79a
overhaul set_convert so I can tell where calendars go
alexcjohnson Dec 2, 2016
84a51c2
add world calendar support part 2
alexcjohnson Dec 2, 2016
13cf6b1
world-cals image mock
alexcjohnson Dec 4, 2016
165125b
fix 3d calendar attributes
alexcjohnson Dec 4, 2016
86b31ea
fix gl3d with world calendars
alexcjohnson Dec 5, 2016
78b6646
test world-cals in scattergl & gl2d axes
alexcjohnson Dec 5, 2016
1b563f5
test world calendars with finance charts
alexcjohnson Dec 5, 2016
4e9a632
remove mistaken paste
alexcjohnson Dec 6, 2016
c1c24e8
support world cals in rangesliders
alexcjohnson Dec 6, 2016
a435981
partial support for range selectors on world calendars
alexcjohnson Dec 6, 2016
6653da7
get the right date string replacement for candlestick mock
alexcjohnson Dec 6, 2016
8e1747f
bigger tolerance on updatemenus_test width test
alexcjohnson Dec 6, 2016
00ae2dd
fix and test layout.calendar inheritance
alexcjohnson Dec 6, 2016
3143099
move world-calendar logic in lib/dates.js to new 'calendars' component
etpinard Dec 6, 2016
5984106
coerce calendar attributes in calenders component
etpinard Dec 6, 2016
03ab34f
add requirable 'calendars' module + include it in main bundle
etpinard Dec 6, 2016
db3d18b
fix typo in getComponentMethod call
etpinard Dec 6, 2016
05b2f96
skip over calendar attribute in findArrayAttributes
etpinard Dec 6, 2016
61ecd42
generalise plot schema handling of component attributes
etpinard Dec 6, 2016
68af287
add schema attributes in calendars module
etpinard Dec 6, 2016
cb2c54b
move calendar defaults after early return
etpinard Dec 7, 2016
bc457a9
Merge pull request #1230 from plotly/world-cals-component
etpinard Dec 7, 2016
1444f55
use only the calendars we need from 'world-calendars'
etpinard Dec 7, 2016
08f18ca
Merge pull request #1237 from plotly/world-cals-trimmed
etpinard Dec 7, 2016
dcddcee
support chinese calendar
alexcjohnson Dec 8, 2016
bbb76a4
Merge branch 'master' into world-cals
alexcjohnson Dec 8, 2016
8d8e936
fix some tests for chinese, and robustify dateTime2ms
alexcjohnson Dec 8, 2016
0e05f95
update baseline image with chinese calendar
alexcjohnson Dec 8, 2016
509f287
perf: parseInt for string-leading int extraction
alexcjohnson Dec 9, 2016
7bd501f
prevent non-gregorian month/year todate range selectors
alexcjohnson Dec 9, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/annotations/calc_autorange.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ function annAutorange(gd) {
}

if(xa && xa.autorange) {
Axes.expand(xa, [xa.l2c(xa.r2l(ann.x))], {
Axes.expand(xa, [xa.r2c(ann.x)], {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

ppadplus: rightSize,
ppadminus: leftSize
});
}

if(ya && ya.autorange) {
Axes.expand(ya, [ya.l2c(ya.r2l(ann.y))], {
Axes.expand(ya, [ya.r2c(ann.y)], {
ppadplus: bottomSize,
ppadminus: topSize
});
Expand Down
9 changes: 4 additions & 5 deletions src/components/rangeslider/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,12 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName, counterAxe

// Expand slider range to the axis range
if(containerOut.range && !axOut.autorange) {
// TODO: what if the ranges are reversed?
var outRange = containerOut.range,
axRange = axOut.range,
l2r = axOut.l2r,
r2l = axOut.r2l;
axRange = axOut.range;

outRange[0] = l2r(Math.min(r2l(outRange[0]), r2l(axRange[0])));
outRange[1] = l2r(Math.max(r2l(outRange[1]), r2l(axRange[1])));
outRange[0] = axOut.l2r(Math.min(axOut.r2l(outRange[0]), axOut.r2l(axRange[0])));
outRange[1] = axOut.l2r(Math.max(axOut.r2l(outRange[1]), axOut.r2l(axRange[1])));
} else {
axOut._needsExpand = true;
}
Expand Down
269 changes: 250 additions & 19 deletions src/lib/dates.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,85 @@ function isWorldCalendar(calendar) {
// of the unix epoch. From calendars.instance().newDate(1970, 1, 1).toJD()
var EPOCHJD = 2440587.5;

// each calendar needs its own default canonical tick. I would love to use
// 2000-01-01 (or even 0000-01-01) for them all but they don't necessarily
// all support either of those dates. Instead I'll use the most significant
// number they *do* support, biased toward the present day.
var CANONICAL_TICK = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't this be better as part of the world-calendar package?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

maybe the world-calendars plotly.js component... the package is a pretty thin wrapper on kbwood/calendars so in principle I'd like it to stay in sync with the original.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good call. I've give this a shot!

gregorian: '2000-01-01',
coptic: '2000-01-01',
discworld: '2000-01-01',
ethiopian: '2000-01-01',
hebrew: '5000-01-01',
islamic: '1000-01-01',
julian: '2000-01-01',
mayan: '5000-01-01',
nanakshahi: '1000-01-01',
nepali: '2000-01-01',
persian: '1000-01-01',
jalali: '1000-01-01',
taiwan: '1000-01-01',
thai: '2000-01-01',
ummalqura: '1400-01-01'
};
// Start on a Sunday - for week ticks
// Discworld and Mayan calendars don't have 7-day weeks anyway so don't change them.
// If anyone really cares we can customize the auto tick spacings for these calendars.
var CANONICAL_SUNDAY = {
gregorian: '2000-01-02',
coptic: '2000-01-03',
discworld: '2000-01-01',
ethiopian: '2000-01-05',
hebrew: '5000-01-01',
islamic: '1000-01-02',
julian: '2000-01-03',
mayan: '5000-01-01',
nanakshahi: '1000-01-05',
nepali: '2000-01-05',
persian: '1000-01-01',
jalali: '1000-01-01',
taiwan: '1000-01-04',
thai: '2000-01-04',
ummalqura: '1400-01-06'
};

var DFLTRANGE = {
gregorian: ['2000-01-01', '2001-01-01'],
coptic: ['1700-01-01', '1701-01-01'],
discworld: ['1800-01-01', '1801-01-01'],
ethiopian: ['2000-01-01', '2001-01-01'],
hebrew: ['5700-01-01', '5701-01-01'],
islamic: ['1400-01-01', '1401-01-01'],
julian: ['2000-01-01', '2001-01-01'],
mayan: ['5200-01-01', '5201-01-01'],
nanakshahi: ['0500-01-01', '0501-01-01'],
nepali: ['2000-01-01', '2001-01-01'],
persian: ['1400-01-01', '1401-01-01'],
jalali: ['1400-01-01', '1401-01-01'],
taiwan: ['0100-01-01', '0101-01-01'],
thai: ['2500-01-01', '2501-01-01'],
ummalqura: ['1400-01-01', '1401-01-01']
};

/*
* dateTick0: get the canonical tick for this calendar
*
* bool sunday is for week ticks, shift it to a Sunday.
*/
exports.dateTick0 = function(calendar, sunday) {
calendar = (isWorldCalendar(calendar) && calendar) || 'gregorian';
if(sunday) return CANONICAL_SUNDAY[calendar];
return CANONICAL_TICK[calendar];
};

/*
* dfltRange: for each calendar, give a valid default range
*/
exports.dfltRange = function(calendar) {
calendar = (isWorldCalendar(calendar) && calendar) || 'gregorian';
return DFLTRANGE[calendar];
};

// is an object a javascript date?
exports.isJSDate = function(v) {
return typeof v === 'object' && v !== null && typeof v.getTime === 'function';
Expand Down Expand Up @@ -87,6 +166,10 @@ var MIN_MS, MAX_MS;
* Note that we follow ISO 8601:2004: there *is* a year 0, which
* is 1BC/BCE, and -1===2BC etc.
*
* World calendars: not all of these *have* agreed extensions to this full range,
* if you have another calendar system but want a date range outside its validity,
* you can use a gregorian date string prefixed with 'G' or 'g'.
*
* Where to cut off 2-digit years between 1900s and 2000s?
* from http://support.microsoft.com/kb/244664:
* 1930-2029 (the most retro of all...)
Expand Down Expand Up @@ -120,7 +203,19 @@ exports.dateTime2ms = function(s, calendar) {
// otherwise only accept strings and numbers
if(typeof s !== 'string' && typeof s !== 'number') return BADNUM;

var match = String(s).match(DATETIME_REGEXP);
s = String(s);

var isWorld = isWorldCalendar(calendar);

// to handle out-of-range dates in international calendars, accept
// 'G' as a prefix to force the built-in gregorian calendar.
var s0 = s.charAt(0);
if(isWorld && (s0 === 'G' || s0 === 'g')) {
s = s.substr(1);
calendar = '';
}

var match = s.match(DATETIME_REGEXP);
if(!match) return BADNUM;
var y = match[1],
m = Number(match[3] || 1),
Expand All @@ -129,11 +224,14 @@ exports.dateTime2ms = function(s, calendar) {
M = Number(match[9] || 0),
S = Number(match[11] || 0);

if(isWorldCalendar(calendar)) {
if(isWorld) {
// disallow 2-digit years for world calendars
if(y.length === 2) return BADNUM;

var cDate = getCal(calendar).newDate(Number(y), m, d);
var cDate;
try { cDate = getCal(calendar).newDate(Number(y), m, d); }
catch(e) { return BADNUM; } // Invalid ... date

if(!cDate) return BADNUM;

return ((cDate.toJD() - EPOCHJD) * ONEDAY) +
Expand Down Expand Up @@ -192,12 +290,18 @@ exports.ms2DateTime = function(ms, r, calendar) {

var msecTenths = Math.floor(mod(ms + 0.05, 1) * 10),
msRounded = Math.round(ms - msecTenths / 10),
dateStr, h, m, s, msec10;
dateStr, h, m, s, msec10, d;

if(isWorldCalendar(calendar)) {
var dateJD = Math.floor(msRounded / ONEDAY) + EPOCHJD,
timeMs = Math.floor(mod(ms, ONEDAY));
dateStr = getCal(calendar).fromJD(dateJD).formatDate('yyyy-mm-dd');
try {
Copy link
Contributor

Choose a reason for hiding this comment

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

Arrgh. try-catch blocks are slow. Oh well, at least only world calendars are affected.

dateStr = getCal(calendar).fromJD(dateJD).formatDate('yyyy-mm-dd');
}
catch(e) {
// invalid date in this calendar - fall back to Gyyyy-mm-dd
dateStr = utcFormat('G%Y-%m-%d')(new Date(msRounded));
}

// yyyy does NOT guarantee 4-digit years. YYYY mostly does, but does
// other things for a few calendars, so we can't trust it. Just pad
Expand All @@ -217,7 +321,7 @@ exports.ms2DateTime = function(ms, r, calendar) {
msec10 = (r < FIVEMIN) ? (timeMs % ONESEC) * 10 + msecTenths : 0;
}
else {
var d = new Date(msRounded);
d = new Date(msRounded);

dateStr = utcFormat('%Y-%m-%d')(d);

Expand Down Expand Up @@ -377,7 +481,12 @@ function modDateFormat(fmt, x, calendar) {
fmt = fmt.replace(fracMatch, fracSecs);
}
if(isWorldCalendar(calendar)) {
fmt = worldCalFmt(fmt, x, calendar);
try {
fmt = worldCalFmt(fmt, x, calendar);
}
catch(e) {
return 'Invalid';
}
}
return utcFormat(fmt)(d);
}
Expand Down Expand Up @@ -435,19 +544,22 @@ exports.formatDate = function(x, fmt, tr, calendar) {
if(fmt) return modDateFormat(fmt, x, calendar);

if(calendar) {
var dateJD = Math.floor(x + 0.05 / ONEDAY) + EPOCHJD,
cDate = 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);
try {
var dateJD = Math.floor((x + 0.05) / ONEDAY) + EPOCHJD,
cDate = 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(x);
Expand All @@ -466,3 +578,122 @@ exports.formatDate = function(x, fmt, tr, calendar) {

return dateStr + (headStr ? '\n' + headStr : '');
};

/*
* incrementMonth: make a new milliseconds value from the given one,
* having changed the month
*
* special case for world calendars: multiples of 12 are treated as years,
* even for calendar systems that don't have (always or ever) 12 months/year
* TODO: perhaps we need a different code for year increments to support this?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@etpinard I meant to flag this one for you - there would be something nice even with regular dates about allowing Y1 and Y5 etc instead of requiring M12 and M60 for 1 and 5 year ticks, even though for Gregorian they're equivalent. And if we allow M12 to mean a year in every calendar now it will be tough to undo later if we decide to add Y1. On the other hand, someone actually wanting 12-month ticks in a calendar that doesn't have 12 months seems ridiculously unlikely.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok. Can you open an issue about it after this PR is merged?

*
* ms (number): the initial millisecond value
* dMonth (int): the (signed) number of months to shift
* calendar (string): the calendar system to use
*
* changing month does not (and CANNOT) always preserve day, since
* months have different lengths. The worst example of this is:
* d = new Date(1970,0,31); d.setMonth(1) -> Feb 31 turns into Mar 3
*
* But we want to be able to iterate over the last day of each month,
* regardless of what its number is.
* So shift 3 days forward, THEN set the new month, then unshift:
* 1/31 -> 2/28 (or 29) -> 3/31 -> 4/30 -> ...
*
* Note that odd behavior still exists if you start from the 26th-28th:
* 1/28 -> 2/28 -> 3/31
* but at least you can't shift any dates into the wrong month,
* and ticks on these days incrementing by month would be very unusual
*/
var THREEDAYS = 3 * ONEDAY;
exports.incrementMonth = function(ms, dMonth, calendar) {
calendar = isWorldCalendar(calendar) && calendar;

// pull time out and operate on pure dates, then add time back at the end
// this gives maximum precision - not that we *normally* care if we're
// incrementing by month, but better to be safe!
var timeMs = mod(ms, ONEDAY);
ms = Math.round(ms - timeMs);

if(calendar) {
try {
var dateJD = Math.round(ms / ONEDAY) + EPOCHJD,
calInstance = getCal(calendar),
cDate = calInstance.fromJD(dateJD);

if(dMonth % 12) calInstance.add(cDate, dMonth, 'm');
else calInstance.add(cDate, dMonth / 12, 'y');

return (cDate.toJD() - EPOCHJD) * ONEDAY + timeMs;
}
catch(e) {
logError('invalid ms ' + ms + ' in calendar ' + calendar);
// then keep going in gregorian even though the result will be 'Invalid'
}
}

var y = new Date(ms + THREEDAYS);
return y.setUTCMonth(y.getUTCMonth() + dMonth) + timeMs - THREEDAYS;
};

/*
* findExactDates: what fraction of data is exact days, months, or years?
*
* data: array of millisecond values
* calendar (string) the calendar to test against
*/
exports.findExactDates = function(data, calendar) {
var exactYears = 0,
exactMonths = 0,
exactDays = 0,
blankCount = 0,
d,
di;

var calInstance = isWorldCalendar(calendar) && getCal(calendar);

for(var i = 0; i < data.length; i++) {
di = data[i];

// not date data at all
if(!isNumeric(di)) {
blankCount ++;
continue;
}

// not an exact date
if(di % ONEDAY) continue;

if(calInstance) {
try {
d = calInstance.fromJD(di / ONEDAY + EPOCHJD);
if(d.day() === 1) {
if(d.month() === 1) exactYears++;
else exactMonths++;
}
else exactDays++;
}
catch(e) {
// invalid date in this calendar - ignore it here.
}
}
else {
d = new Date(di);
if(d.getUTCDate() === 1) {
if(d.getUTCMonth() === 0) exactYears++;
else exactMonths++;
}
else exactDays++;
}
}
exactMonths += exactYears;
exactDays += exactMonths;

var dataCount = data.length - blankCount;

return {
exactYears: exactYears / dataCount,
exactMonths: exactMonths / dataCount,
exactDays: exactDays / dataCount
};
};
4 changes: 4 additions & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ lib.ms2DateTimeLocal = datesModule.ms2DateTimeLocal;
lib.cleanDate = datesModule.cleanDate;
lib.isJSDate = datesModule.isJSDate;
lib.formatDate = datesModule.formatDate;
lib.incrementMonth = datesModule.incrementMonth;
lib.dateTick0 = datesModule.dateTick0;
lib.dfltRange = datesModule.dfltRange;
lib.findExactDates = datesModule.findExactDates;
lib.MIN_MS = datesModule.MIN_MS;
lib.MAX_MS = datesModule.MAX_MS;

Expand Down
Loading