Skip to content

Commit 84a51c2

Browse files
committed
add world calendar support part 2
1 parent 4d8f79a commit 84a51c2

File tree

25 files changed

+431
-218
lines changed

25 files changed

+431
-218
lines changed

src/components/annotations/calc_autorange.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,14 @@ function annAutorange(gd) {
6969
}
7070

7171
if(xa && xa.autorange) {
72-
Axes.expand(xa, [xa.l2c(xa.r2l(ann.x))], {
72+
Axes.expand(xa, [xa.r2c(ann.x)], {
7373
ppadplus: rightSize,
7474
ppadminus: leftSize
7575
});
7676
}
7777

7878
if(ya && ya.autorange) {
79-
Axes.expand(ya, [ya.l2c(ya.r2l(ann.y))], {
79+
Axes.expand(ya, [ya.r2c(ann.y)], {
8080
ppadplus: bottomSize,
8181
ppadminus: topSize
8282
});

src/components/rangeslider/defaults.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,12 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName, counterAxe
3737

3838
// Expand slider range to the axis range
3939
if(containerOut.range && !axOut.autorange) {
40+
// TODO: what if the ranges are reversed?
4041
var outRange = containerOut.range,
41-
axRange = axOut.range,
42-
l2r = axOut.l2r,
43-
r2l = axOut.r2l;
42+
axRange = axOut.range;
4443

45-
outRange[0] = l2r(Math.min(r2l(outRange[0]), r2l(axRange[0])));
46-
outRange[1] = l2r(Math.max(r2l(outRange[1]), r2l(axRange[1])));
44+
outRange[0] = axOut.l2r(Math.min(axOut.r2l(outRange[0]), axOut.r2l(axRange[0])));
45+
outRange[1] = axOut.l2r(Math.max(axOut.r2l(outRange[1]), axOut.r2l(axRange[1])));
4746
} else {
4847
axOut._needsExpand = true;
4948
}

src/lib/dates.js

Lines changed: 250 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,85 @@ function isWorldCalendar(calendar) {
4949
// of the unix epoch. From calendars.instance().newDate(1970, 1, 1).toJD()
5050
var EPOCHJD = 2440587.5;
5151

52+
// each calendar needs its own default canonical tick. I would love to use
53+
// 2000-01-01 (or even 0000-01-01) for them all but they don't necessarily
54+
// all support either of those dates. Instead I'll use the most significant
55+
// number they *do* support, biased toward the present day.
56+
var CANONICAL_TICK = {
57+
gregorian: '2000-01-01',
58+
coptic: '2000-01-01',
59+
discworld: '2000-01-01',
60+
ethiopian: '2000-01-01',
61+
hebrew: '5000-01-01',
62+
islamic: '1000-01-01',
63+
julian: '2000-01-01',
64+
mayan: '5000-01-01',
65+
nanakshahi: '1000-01-01',
66+
nepali: '2000-01-01',
67+
persian: '1000-01-01',
68+
jalali: '1000-01-01',
69+
taiwan: '1000-01-01',
70+
thai: '2000-01-01',
71+
ummalqura: '1400-01-01'
72+
};
73+
// Start on a Sunday - for week ticks
74+
// Discworld and Mayan calendars don't have 7-day weeks anyway so don't change them.
75+
// If anyone really cares we can customize the auto tick spacings for these calendars.
76+
var CANONICAL_SUNDAY = {
77+
gregorian: '2000-01-02',
78+
coptic: '2000-01-03',
79+
discworld: '2000-01-01',
80+
ethiopian: '2000-01-05',
81+
hebrew: '5000-01-01',
82+
islamic: '1000-01-02',
83+
julian: '2000-01-03',
84+
mayan: '5000-01-01',
85+
nanakshahi: '1000-01-05',
86+
nepali: '2000-01-05',
87+
persian: '1000-01-01',
88+
jalali: '1000-01-01',
89+
taiwan: '1000-01-04',
90+
thai: '2000-01-04',
91+
ummalqura: '1400-01-06'
92+
};
93+
94+
var DFLTRANGE = {
95+
gregorian: ['2000-01-01', '2001-01-01'],
96+
coptic: ['1700-01-01', '1701-01-01'],
97+
discworld: ['1800-01-01', '1801-01-01'],
98+
ethiopian: ['2000-01-01', '2001-01-01'],
99+
hebrew: ['5700-01-01', '5701-01-01'],
100+
islamic: ['1400-01-01', '1401-01-01'],
101+
julian: ['2000-01-01', '2001-01-01'],
102+
mayan: ['5200-01-01', '5201-01-01'],
103+
nanakshahi: ['0500-01-01', '0501-01-01'],
104+
nepali: ['2000-01-01', '2001-01-01'],
105+
persian: ['1400-01-01', '1401-01-01'],
106+
jalali: ['1400-01-01', '1401-01-01'],
107+
taiwan: ['0100-01-01', '0101-01-01'],
108+
thai: ['2500-01-01', '2501-01-01'],
109+
ummalqura: ['1400-01-01', '1401-01-01']
110+
};
111+
112+
/*
113+
* dateTick0: get the canonical tick for this calendar
114+
*
115+
* bool sunday is for week ticks, shift it to a Sunday.
116+
*/
117+
exports.dateTick0 = function(calendar, sunday) {
118+
calendar = (isWorldCalendar(calendar) && calendar) || 'gregorian';
119+
if(sunday) return CANONICAL_SUNDAY[calendar];
120+
return CANONICAL_TICK[calendar];
121+
};
122+
123+
/*
124+
* dfltRange: for each calendar, give a valid default range
125+
*/
126+
exports.dfltRange = function(calendar) {
127+
calendar = (isWorldCalendar(calendar) && calendar) || 'gregorian';
128+
return DFLTRANGE[calendar];
129+
};
130+
52131
// is an object a javascript date?
53132
exports.isJSDate = function(v) {
54133
return typeof v === 'object' && v !== null && typeof v.getTime === 'function';
@@ -87,6 +166,10 @@ var MIN_MS, MAX_MS;
87166
* Note that we follow ISO 8601:2004: there *is* a year 0, which
88167
* is 1BC/BCE, and -1===2BC etc.
89168
*
169+
* World calendars: not all of these *have* agreed extensions to this full range,
170+
* if you have another calendar system but want a date range outside its validity,
171+
* you can use a gregorian date string prefixed with 'G' or 'g'.
172+
*
90173
* Where to cut off 2-digit years between 1900s and 2000s?
91174
* from http://support.microsoft.com/kb/244664:
92175
* 1930-2029 (the most retro of all...)
@@ -120,7 +203,19 @@ exports.dateTime2ms = function(s, calendar) {
120203
// otherwise only accept strings and numbers
121204
if(typeof s !== 'string' && typeof s !== 'number') return BADNUM;
122205

123-
var match = String(s).match(DATETIME_REGEXP);
206+
s = String(s);
207+
208+
var isWorld = isWorldCalendar(calendar);
209+
210+
// to handle out-of-range dates in international calendars, accept
211+
// 'G' as a prefix to force the built-in gregorian calendar.
212+
var s0 = s.charAt(0);
213+
if(isWorld && (s0 === 'G' || s0 === 'g')) {
214+
s = s.substr(1);
215+
calendar = '';
216+
}
217+
218+
var match = s.match(DATETIME_REGEXP);
124219
if(!match) return BADNUM;
125220
var y = match[1],
126221
m = Number(match[3] || 1),
@@ -129,11 +224,14 @@ exports.dateTime2ms = function(s, calendar) {
129224
M = Number(match[9] || 0),
130225
S = Number(match[11] || 0);
131226

132-
if(isWorldCalendar(calendar)) {
227+
if(isWorld) {
133228
// disallow 2-digit years for world calendars
134229
if(y.length === 2) return BADNUM;
135230

136-
var cDate = getCal(calendar).newDate(Number(y), m, d);
231+
var cDate;
232+
try { cDate = getCal(calendar).newDate(Number(y), m, d); }
233+
catch(e) { return BADNUM; } // Invalid ... date
234+
137235
if(!cDate) return BADNUM;
138236

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

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

197295
if(isWorldCalendar(calendar)) {
198296
var dateJD = Math.floor(msRounded / ONEDAY) + EPOCHJD,
199297
timeMs = Math.floor(mod(ms, ONEDAY));
200-
dateStr = getCal(calendar).fromJD(dateJD).formatDate('yyyy-mm-dd');
298+
try {
299+
dateStr = getCal(calendar).fromJD(dateJD).formatDate('yyyy-mm-dd');
300+
}
301+
catch(e) {
302+
// invalid date in this calendar - fall back to Gyyyy-mm-dd
303+
dateStr = utcFormat('G%Y-%m-%d')(new Date(msRounded));
304+
}
201305

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

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

@@ -377,7 +481,12 @@ function modDateFormat(fmt, x, calendar) {
377481
fmt = fmt.replace(fracMatch, fracSecs);
378482
}
379483
if(isWorldCalendar(calendar)) {
380-
fmt = worldCalFmt(fmt, x, calendar);
484+
try {
485+
fmt = worldCalFmt(fmt, x, calendar);
486+
}
487+
catch(e) {
488+
return 'Invalid';
489+
}
381490
}
382491
return utcFormat(fmt)(d);
383492
}
@@ -435,19 +544,22 @@ exports.formatDate = function(x, fmt, tr, calendar) {
435544
if(fmt) return modDateFormat(fmt, x, calendar);
436545

437546
if(calendar) {
438-
var dateJD = Math.floor(x + 0.05 / ONEDAY) + EPOCHJD,
439-
cDate = getCal(calendar).fromJD(dateJD);
440-
441-
if(tr === 'y') dateStr = yearFormatWorld(cDate);
442-
else if(tr === 'm') dateStr = monthFormatWorld(cDate);
443-
else if(tr === 'd') {
444-
headStr = yearFormatWorld(cDate);
445-
dateStr = dayFormatWorld(cDate);
446-
}
447-
else {
448-
headStr = yearMonthDayFormatWorld(cDate);
449-
dateStr = formatTime(x, tr);
547+
try {
548+
var dateJD = Math.floor((x + 0.05) / ONEDAY) + EPOCHJD,
549+
cDate = getCal(calendar).fromJD(dateJD);
550+
551+
if(tr === 'y') dateStr = yearFormatWorld(cDate);
552+
else if(tr === 'm') dateStr = monthFormatWorld(cDate);
553+
else if(tr === 'd') {
554+
headStr = yearFormatWorld(cDate);
555+
dateStr = dayFormatWorld(cDate);
556+
}
557+
else {
558+
headStr = yearMonthDayFormatWorld(cDate);
559+
dateStr = formatTime(x, tr);
560+
}
450561
}
562+
catch(e) { return 'Invalid'; }
451563
}
452564
else {
453565
var d = new Date(x);
@@ -466,3 +578,122 @@ exports.formatDate = function(x, fmt, tr, calendar) {
466578

467579
return dateStr + (headStr ? '\n' + headStr : '');
468580
};
581+
582+
/*
583+
* incrementMonth: make a new milliseconds value from the given one,
584+
* having changed the month
585+
*
586+
* special case for world calendars: multiples of 12 are treated as years,
587+
* even for calendar systems that don't have (always or ever) 12 months/year
588+
* TODO: perhaps we need a different code for year increments to support this?
589+
*
590+
* ms (number): the initial millisecond value
591+
* dMonth (int): the (signed) number of months to shift
592+
* calendar (string): the calendar system to use
593+
*
594+
* changing month does not (and CANNOT) always preserve day, since
595+
* months have different lengths. The worst example of this is:
596+
* d = new Date(1970,0,31); d.setMonth(1) -> Feb 31 turns into Mar 3
597+
*
598+
* But we want to be able to iterate over the last day of each month,
599+
* regardless of what its number is.
600+
* So shift 3 days forward, THEN set the new month, then unshift:
601+
* 1/31 -> 2/28 (or 29) -> 3/31 -> 4/30 -> ...
602+
*
603+
* Note that odd behavior still exists if you start from the 26th-28th:
604+
* 1/28 -> 2/28 -> 3/31
605+
* but at least you can't shift any dates into the wrong month,
606+
* and ticks on these days incrementing by month would be very unusual
607+
*/
608+
var THREEDAYS = 3 * ONEDAY;
609+
exports.incrementMonth = function(ms, dMonth, calendar) {
610+
calendar = isWorldCalendar(calendar) && calendar;
611+
612+
// pull time out and operate on pure dates, then add time back at the end
613+
// this gives maximum precision - not that we *normally* care if we're
614+
// incrementing by month, but better to be safe!
615+
var timeMs = mod(ms, ONEDAY);
616+
ms = Math.round(ms - timeMs);
617+
618+
if(calendar) {
619+
try {
620+
var dateJD = Math.round(ms / ONEDAY) + EPOCHJD,
621+
calInstance = getCal(calendar),
622+
cDate = calInstance.fromJD(dateJD);
623+
624+
if(dMonth % 12) calInstance.add(cDate, dMonth, 'm');
625+
else calInstance.add(cDate, dMonth / 12, 'y');
626+
627+
return (cDate.toJD() - EPOCHJD) * ONEDAY + timeMs;
628+
}
629+
catch(e) {
630+
logError('invalid ms ' + ms + ' in calendar ' + calendar);
631+
// then keep going in gregorian even though the result will be 'Invalid'
632+
}
633+
}
634+
635+
var y = new Date(ms + THREEDAYS);
636+
return y.setUTCMonth(y.getUTCMonth() + dMonth) + timeMs - THREEDAYS;
637+
};
638+
639+
/*
640+
* findExactDates: what fraction of data is exact days, months, or years?
641+
*
642+
* data: array of millisecond values
643+
* calendar (string) the calendar to test against
644+
*/
645+
exports.findExactDates = function(data, calendar) {
646+
var exactYears = 0,
647+
exactMonths = 0,
648+
exactDays = 0,
649+
blankCount = 0,
650+
d,
651+
di;
652+
653+
var calInstance = isWorldCalendar(calendar) && getCal(calendar);
654+
655+
for(var i = 0; i < data.length; i++) {
656+
di = data[i];
657+
658+
// not date data at all
659+
if(!isNumeric(di)) {
660+
blankCount ++;
661+
continue;
662+
}
663+
664+
// not an exact date
665+
if(di % ONEDAY) continue;
666+
667+
if(calInstance) {
668+
try {
669+
d = calInstance.fromJD(di / ONEDAY + EPOCHJD);
670+
if(d.day() === 1) {
671+
if(d.month() === 1) exactYears++;
672+
else exactMonths++;
673+
}
674+
else exactDays++;
675+
}
676+
catch(e) {
677+
// invalid date in this calendar - ignore it here.
678+
}
679+
}
680+
else {
681+
d = new Date(di);
682+
if(d.getUTCDate() === 1) {
683+
if(d.getUTCMonth() === 0) exactYears++;
684+
else exactMonths++;
685+
}
686+
else exactDays++;
687+
}
688+
}
689+
exactMonths += exactYears;
690+
exactDays += exactMonths;
691+
692+
var dataCount = data.length - blankCount;
693+
694+
return {
695+
exactYears: exactYears / dataCount,
696+
exactMonths: exactMonths / dataCount,
697+
exactDays: exactDays / dataCount
698+
};
699+
};

src/lib/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ lib.ms2DateTimeLocal = datesModule.ms2DateTimeLocal;
3333
lib.cleanDate = datesModule.cleanDate;
3434
lib.isJSDate = datesModule.isJSDate;
3535
lib.formatDate = datesModule.formatDate;
36+
lib.incrementMonth = datesModule.incrementMonth;
37+
lib.dateTick0 = datesModule.dateTick0;
38+
lib.dfltRange = datesModule.dfltRange;
39+
lib.findExactDates = datesModule.findExactDates;
3640
lib.MIN_MS = datesModule.MIN_MS;
3741
lib.MAX_MS = datesModule.MAX_MS;
3842

0 commit comments

Comments
 (0)