Skip to content

Commit bc457a9

Browse files
authored
Merge pull request #1230 from plotly/world-cals-component
"calendars" component
2 parents 00ae2dd + cb2c54b commit bc457a9

39 files changed

+410
-278
lines changed

lib/calendars.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = require('../src/components/calendars');

lib/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,9 @@ Plotly.register([
5454
require('./groupby')
5555
]);
5656

57+
// components
58+
Plotly.register([
59+
require('./calendars')
60+
]);
61+
5762
module.exports = Plotly;

src/components/calendars/index.js

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var calendars = require('world-calendars');
12+
13+
var Lib = require('../../lib');
14+
var constants = require('../../constants/numerical');
15+
16+
var EPOCHJD = constants.EPOCHJD;
17+
var ONEDAY = constants.ONEDAY;
18+
19+
var attributes = {
20+
valType: 'enumerated',
21+
values: Object.keys(calendars.calendars),
22+
role: 'info',
23+
dflt: 'gregorian'
24+
};
25+
26+
var handleDefaults = function(contIn, contOut, attr, dflt) {
27+
var attrs = {};
28+
attrs[attr] = attributes;
29+
30+
return Lib.coerce(contIn, contOut, attrs, attr, dflt);
31+
};
32+
33+
var handleTraceDefaults = function(traceIn, traceOut, coords, layout) {
34+
for(var i = 0; i < coords.length; i++) {
35+
handleDefaults(traceIn, traceOut, coords[i] + 'calendar', layout.calendar);
36+
}
37+
};
38+
39+
// each calendar needs its own default canonical tick. I would love to use
40+
// 2000-01-01 (or even 0000-01-01) for them all but they don't necessarily
41+
// all support either of those dates. Instead I'll use the most significant
42+
// number they *do* support, biased toward the present day.
43+
var CANONICAL_TICK = {
44+
coptic: '2000-01-01',
45+
discworld: '2000-01-01',
46+
ethiopian: '2000-01-01',
47+
hebrew: '5000-01-01',
48+
islamic: '1000-01-01',
49+
julian: '2000-01-01',
50+
mayan: '5000-01-01',
51+
nanakshahi: '1000-01-01',
52+
nepali: '2000-01-01',
53+
persian: '1000-01-01',
54+
jalali: '1000-01-01',
55+
taiwan: '1000-01-01',
56+
thai: '2000-01-01',
57+
ummalqura: '1400-01-01'
58+
};
59+
60+
// Start on a Sunday - for week ticks
61+
// Discworld and Mayan calendars don't have 7-day weeks anyway so don't change them.
62+
// If anyone really cares we can customize the auto tick spacings for these calendars.
63+
var CANONICAL_SUNDAY = {
64+
coptic: '2000-01-03',
65+
discworld: '2000-01-01',
66+
ethiopian: '2000-01-05',
67+
hebrew: '5000-01-01',
68+
islamic: '1000-01-02',
69+
julian: '2000-01-03',
70+
mayan: '5000-01-01',
71+
nanakshahi: '1000-01-05',
72+
nepali: '2000-01-05',
73+
persian: '1000-01-01',
74+
jalali: '1000-01-01',
75+
taiwan: '1000-01-04',
76+
thai: '2000-01-04',
77+
ummalqura: '1400-01-06'
78+
};
79+
80+
var DFLTRANGE = {
81+
coptic: ['1700-01-01', '1701-01-01'],
82+
discworld: ['1800-01-01', '1801-01-01'],
83+
ethiopian: ['2000-01-01', '2001-01-01'],
84+
hebrew: ['5700-01-01', '5701-01-01'],
85+
islamic: ['1400-01-01', '1401-01-01'],
86+
julian: ['2000-01-01', '2001-01-01'],
87+
mayan: ['5200-01-01', '5201-01-01'],
88+
nanakshahi: ['0500-01-01', '0501-01-01'],
89+
nepali: ['2000-01-01', '2001-01-01'],
90+
persian: ['1400-01-01', '1401-01-01'],
91+
jalali: ['1400-01-01', '1401-01-01'],
92+
taiwan: ['0100-01-01', '0101-01-01'],
93+
thai: ['2500-01-01', '2501-01-01'],
94+
ummalqura: ['1400-01-01', '1401-01-01']
95+
};
96+
97+
/*
98+
* convert d3 templates to world-calendars templates, so our users only need
99+
* to know d3's specifiers. Map space padding to no padding, and unknown fields
100+
* to an ugly placeholder
101+
*/
102+
var UNKNOWN = '##';
103+
var d3ToWorldCalendars = {
104+
'd': {'0': 'dd', '-': 'd'}, // 2-digit or unpadded day of month
105+
'a': {'0': 'D', '-': 'D'}, // short weekday name
106+
'A': {'0': 'DD', '-': 'DD'}, // full weekday name
107+
'j': {'0': 'oo', '-': 'o'}, // 3-digit or unpadded day of the year
108+
'W': {'0': 'ww', '-': 'w'}, // 2-digit or unpadded week of the year (Monday first)
109+
'm': {'0': 'mm', '-': 'm'}, // 2-digit or unpadded month number
110+
'b': {'0': 'M', '-': 'M'}, // short month name
111+
'B': {'0': 'MM', '-': 'MM'}, // full month name
112+
'y': {'0': 'yy', '-': 'yy'}, // 2-digit year (map unpadded to zero-padded)
113+
'Y': {'0': 'yyyy', '-': 'yyyy'}, // 4-digit year (map unpadded to zero-padded)
114+
'U': UNKNOWN, // Sunday-first week of the year
115+
'w': UNKNOWN, // day of the week [0(sunday),6]
116+
// combined format, we replace the date part with the world-calendar version
117+
// and the %X stays there for d3 to handle with time parts
118+
'%c': {'0': 'D M m %X yyyy', '-': 'D M m %X yyyy'},
119+
'%x': {'0': 'mm/dd/yyyy', '-': 'mm/dd/yyyy'}
120+
};
121+
122+
function worldCalFmt(fmt, x, calendar) {
123+
var dateJD = Math.floor(x + 0.05 / ONEDAY) + EPOCHJD,
124+
cDate = getCal(calendar).fromJD(dateJD),
125+
i = 0,
126+
modifier, directive, directiveLen, directiveObj, replacementPart;
127+
while((i = fmt.indexOf('%', i)) !== -1) {
128+
modifier = fmt.charAt(i + 1);
129+
if(modifier === '0' || modifier === '-' || modifier === '_') {
130+
directiveLen = 3;
131+
directive = fmt.charAt(i + 1);
132+
if(modifier === '_') modifier = '-';
133+
}
134+
else {
135+
directive = modifier;
136+
modifier = '0';
137+
directiveLen = 2;
138+
}
139+
directiveObj = d3ToWorldCalendars[directive];
140+
if(!directiveObj) {
141+
i += directiveLen;
142+
}
143+
else {
144+
// code is recognized as a date part but world-calendars doesn't support it
145+
if(directiveObj === UNKNOWN) replacementPart = UNKNOWN;
146+
147+
// format the cDate according to the translated directive
148+
else replacementPart = cDate.formatDate(directiveObj[modifier]);
149+
150+
fmt = fmt.substr(0, i) + replacementPart + fmt.substr(i + directiveLen);
151+
i += replacementPart.length;
152+
}
153+
}
154+
return fmt;
155+
}
156+
157+
// cache world calendars, so we don't have to reinstantiate
158+
// during each date-time conversion
159+
var allCals = {};
160+
function getCal(calendar) {
161+
var calendarObj = allCals[calendar];
162+
if(calendarObj) return calendarObj;
163+
164+
calendarObj = allCals[calendar] = calendars.instance(calendar);
165+
return calendarObj;
166+
}
167+
168+
function makeAttrs(description) {
169+
return Lib.extendFlat({}, attributes, { description: description });
170+
}
171+
172+
function makeTraceAttrsDescription(coord) {
173+
return 'Sets the calendar system to use with `' + coord + '` date data.';
174+
}
175+
176+
var xAttrs = {
177+
xcalendar: makeAttrs(makeTraceAttrsDescription('x'))
178+
};
179+
180+
var xyAttrs = Lib.extendFlat({}, xAttrs, {
181+
ycalendar: makeAttrs(makeTraceAttrsDescription('y'))
182+
});
183+
184+
var xyzAttrs = Lib.extendFlat({}, xyAttrs, {
185+
zcalendar: makeAttrs(makeTraceAttrsDescription('z'))
186+
});
187+
188+
var axisAttrs = makeAttrs([
189+
'Sets the calendar system to use for `range` and `tick0`',
190+
'if this is a date axis. This does not set the calendar for',
191+
'interpreting data on this axis, that\'s specified in the trace',
192+
'or via the global `layout.calendar`'
193+
].join(' '));
194+
195+
module.exports = {
196+
moduleType: 'component',
197+
name: 'calendars',
198+
199+
schema: {
200+
traces: {
201+
scatter: xyAttrs,
202+
bar: xyAttrs,
203+
heatmap: xyAttrs,
204+
contour: xyAttrs,
205+
histogram: xyAttrs,
206+
histogram2d: xyAttrs,
207+
histogram2dcontour: xyAttrs,
208+
scatter3d: xyzAttrs,
209+
surface: xyzAttrs,
210+
mesh3d: xyzAttrs,
211+
scattergl: xyAttrs,
212+
ohlc: xAttrs,
213+
candlestick: xAttrs
214+
},
215+
layout: {
216+
calendar: makeAttrs([
217+
'Sets the default calendar system to use for interpreting and',
218+
'displaying dates throughout the plot.'
219+
].join(' ')),
220+
'xaxis.calendar': axisAttrs,
221+
'yaxis.calendar': axisAttrs,
222+
'scene.xaxis.calendar': axisAttrs,
223+
'scene.yaxis.calendar': axisAttrs,
224+
'scene.zaxis.calendar': axisAttrs
225+
},
226+
transforms: {
227+
filter: {
228+
calendar: makeAttrs([
229+
'Sets the calendar system to use for `value`, if it is a date.',
230+
'Note that this is not necessarily the same calendar as is used',
231+
'for the target data; that is set by its own calendar attribute,',
232+
'ie `trace.x` uses `trace.xcalendar` etc.'
233+
].join(' '))
234+
}
235+
}
236+
},
237+
238+
layoutAttributes: attributes,
239+
240+
handleDefaults: handleDefaults,
241+
handleTraceDefaults: handleTraceDefaults,
242+
243+
CANONICAL_SUNDAY: CANONICAL_SUNDAY,
244+
CANONICAL_TICK: CANONICAL_TICK,
245+
DFLTRANGE: DFLTRANGE,
246+
247+
getCal: getCal,
248+
worldCalFmt: worldCalFmt
249+
};

src/components/rangeselector/index.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ module.exports = {
1212
moduleType: 'component',
1313
name: 'rangeselector',
1414

15-
layoutNodes: ['xaxis.'],
15+
schema: {
16+
layout: {
17+
'xaxis.rangeselector': require('./attributes')
18+
}
19+
},
20+
1621
layoutAttributes: require('./attributes'),
1722
handleDefaults: require('./defaults'),
1823

src/components/rangeslider/index.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ module.exports = {
1212
moduleType: 'component',
1313
name: 'rangeslider',
1414

15-
layoutNodes: ['xaxis.'],
15+
schema: {
16+
layout: {
17+
'xaxis.rangeslider': require('./attributes')
18+
}
19+
},
20+
1621
layoutAttributes: require('./attributes'),
1722
handleDefaults: require('./defaults'),
1823

src/constants/numerical.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,11 @@ module.exports = {
3636
ONEDAY: 86400000,
3737
ONEHOUR: 3600000,
3838
ONEMIN: 60000,
39-
ONESEC: 1000
39+
ONESEC: 1000,
40+
41+
/*
42+
* For fast conversion btwn world calendars and epoch ms, the Julian Day Number
43+
* of the unix epoch. From calendars.instance().newDate(1970, 1, 1).toJD()
44+
*/
45+
EPOCHJD: 2440587.5
4046
};

src/lib/coerce.js

-15
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
'use strict';
1111

12-
var calendarList = Object.keys(require('world-calendars').calendars);
1312
var isNumeric = require('fast-isnumeric');
1413
var tinycolor = require('tinycolor2');
1514

@@ -268,20 +267,6 @@ exports.valObjects = {
268267

269268
return true;
270269
}
271-
},
272-
calendar: {
273-
description: [
274-
'A string, one of the calendar systems available',
275-
'in the `world-calendars` package, to be used in evaluating',
276-
'or displaying date data. Defaults to built-in (Gregorian) dates.',
277-
'available calendars:', '*' + calendarList.join('*, *') + '*'
278-
].join(' '),
279-
requiredOpts: [],
280-
otherOpts: ['dflt'],
281-
coerceFunction: function(v, propOut, dflt) {
282-
if(v && calendarList.indexOf(v) !== -1) propOut.set(v);
283-
else propOut.set(dflt);
284-
}
285270
}
286271
};
287272

0 commit comments

Comments
 (0)