Skip to content

"calendars" component #1230

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 8 commits into from
Dec 7, 2016
11 changes: 11 additions & 0 deletions lib/calendars.js
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

module.exports = require('../src/components/calendars');
5 changes: 5 additions & 0 deletions lib/index.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -54,4 +54,9 @@ Plotly.register([
require('./groupby') require('./groupby')
]); ]);


// components
Plotly.register([
require('./calendars')
]);

module.exports = Plotly; module.exports = Plotly;
249 changes: 249 additions & 0 deletions src/components/calendars/index.js
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,249 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var calendars = require('world-calendars');

var Lib = require('../../lib');
var constants = require('../../constants/numerical');

var EPOCHJD = constants.EPOCHJD;
var ONEDAY = constants.ONEDAY;

var attributes = {
valType: 'enumerated',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

no need for a calendar valType anymore!

values: Object.keys(calendars.calendars),
role: 'info',
dflt: 'gregorian'
};

var handleDefaults = function(contIn, contOut, attr, dflt) {
var attrs = {};
attrs[attr] = attributes;

return Lib.coerce(contIn, contOut, attrs, attr, dflt);
};

var handleTraceDefaults = function(traceIn, traceOut, coords, layout) {
for(var i = 0; i < coords.length; i++) {
handleDefaults(traceIn, traceOut, coords[i] + 'calendar', layout.calendar);
}
};

// 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 = {
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 = {
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 = {
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']
};

/*
* convert d3 templates to world-calendars templates, so our users only need
* to know d3's specifiers. Map space padding to no padding, and unknown fields
* to an ugly placeholder
*/
var UNKNOWN = '##';
var d3ToWorldCalendars = {
'd': {'0': 'dd', '-': 'd'}, // 2-digit or unpadded day of month
'a': {'0': 'D', '-': 'D'}, // short weekday name
'A': {'0': 'DD', '-': 'DD'}, // full weekday name
'j': {'0': 'oo', '-': 'o'}, // 3-digit or unpadded day of the year
'W': {'0': 'ww', '-': 'w'}, // 2-digit or unpadded week of the year (Monday first)
'm': {'0': 'mm', '-': 'm'}, // 2-digit or unpadded month number
'b': {'0': 'M', '-': 'M'}, // short month name
'B': {'0': 'MM', '-': 'MM'}, // full month name
'y': {'0': 'yy', '-': 'yy'}, // 2-digit year (map unpadded to zero-padded)
'Y': {'0': 'yyyy', '-': 'yyyy'}, // 4-digit year (map unpadded to zero-padded)
'U': UNKNOWN, // Sunday-first week of the year
'w': UNKNOWN, // day of the week [0(sunday),6]
// combined format, we replace the date part with the world-calendar version
// and the %X stays there for d3 to handle with time parts
'%c': {'0': 'D M m %X yyyy', '-': 'D M m %X yyyy'},
'%x': {'0': 'mm/dd/yyyy', '-': 'mm/dd/yyyy'}
};

function worldCalFmt(fmt, x, calendar) {
var dateJD = Math.floor(x + 0.05 / ONEDAY) + EPOCHJD,
cDate = getCal(calendar).fromJD(dateJD),
i = 0,
modifier, directive, directiveLen, directiveObj, replacementPart;
while((i = fmt.indexOf('%', i)) !== -1) {
modifier = fmt.charAt(i + 1);
if(modifier === '0' || modifier === '-' || modifier === '_') {
directiveLen = 3;
directive = fmt.charAt(i + 1);
if(modifier === '_') modifier = '-';
}
else {
directive = modifier;
modifier = '0';
directiveLen = 2;
}
directiveObj = d3ToWorldCalendars[directive];
if(!directiveObj) {
i += directiveLen;
}
else {
// code is recognized as a date part but world-calendars doesn't support it
if(directiveObj === UNKNOWN) replacementPart = UNKNOWN;

// format the cDate according to the translated directive
else replacementPart = cDate.formatDate(directiveObj[modifier]);

fmt = fmt.substr(0, i) + replacementPart + fmt.substr(i + directiveLen);
i += replacementPart.length;
}
}
return fmt;
}

// cache world calendars, so we don't have to reinstantiate
// during each date-time conversion
var allCals = {};
function getCal(calendar) {
var calendarObj = allCals[calendar];
if(calendarObj) return calendarObj;

calendarObj = allCals[calendar] = calendars.instance(calendar);
return calendarObj;
}

function makeAttrs(description) {
return Lib.extendFlat({}, attributes, { description: description });
}

function makeTraceAttrsDescription(coord) {
return 'Sets the calendar system to use with `' + coord + '` date data.';
}

var xAttrs = {
xcalendar: makeAttrs(makeTraceAttrsDescription('x'))
};

var xyAttrs = Lib.extendFlat({}, xAttrs, {
ycalendar: makeAttrs(makeTraceAttrsDescription('y'))
});

var xyzAttrs = Lib.extendFlat({}, xyAttrs, {
zcalendar: makeAttrs(makeTraceAttrsDescription('z'))
});

var axisAttrs = makeAttrs([
'Sets the calendar system to use for `range` and `tick0`',
'if this is a date axis. This does not set the calendar for',
'interpreting data on this axis, that\'s specified in the trace',
'or via the global `layout.calendar`'
].join(' '));

module.exports = {
moduleType: 'component',
name: 'calendars',

schema: {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

some fancy (slightly hacky) declaration to make sure that calendar attributes will show up on https://community.plot.ly/

Copy link
Collaborator

Choose a reason for hiding this comment

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

how will we know to keep this up to date?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

how will we know to keep this up to date?

good question. There's got to be a way to test this.

traces: {
scatter: xyAttrs,
bar: xyAttrs,
heatmap: xyAttrs,
contour: xyAttrs,
histogram: xyAttrs,
histogram2d: xyAttrs,
histogram2dcontour: xyAttrs,
scatter3d: xyzAttrs,
surface: xyzAttrs,
mesh3d: xyzAttrs,
scattergl: xyAttrs,
ohlc: xAttrs,
candlestick: xAttrs
},
layout: {
calendar: makeAttrs([
'Sets the default calendar system to use for interpreting and',
'displaying dates throughout the plot.'
].join(' ')),
'xaxis.calendar': axisAttrs,
'yaxis.calendar': axisAttrs,
'scene.xaxis.calendar': axisAttrs,
'scene.yaxis.calendar': axisAttrs,
'scene.zaxis.calendar': axisAttrs
},
transforms: {
filter: {
calendar: makeAttrs([
'Sets the calendar system to use for `value`, if it is a date.',
'Note that this is not necessarily the same calendar as is used',
'for the target data; that is set by its own calendar attribute,',
'ie `trace.x` uses `trace.xcalendar` etc.'
].join(' '))
}
}
},

layoutAttributes: attributes,

handleDefaults: handleDefaults,
handleTraceDefaults: handleTraceDefaults,

CANONICAL_SUNDAY: CANONICAL_SUNDAY,
CANONICAL_TICK: CANONICAL_TICK,
DFLTRANGE: DFLTRANGE,

getCal: getCal,
worldCalFmt: worldCalFmt
};
7 changes: 6 additions & 1 deletion src/components/rangeselector/index.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ module.exports = {
moduleType: 'component', moduleType: 'component',
name: 'rangeselector', name: 'rangeselector',


layoutNodes: ['xaxis.'], schema: {
layout: {
'xaxis.rangeselector': require('./attributes')
}
},

layoutAttributes: require('./attributes'), layoutAttributes: require('./attributes'),
handleDefaults: require('./defaults'), handleDefaults: require('./defaults'),


Expand Down
7 changes: 6 additions & 1 deletion src/components/rangeslider/index.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ module.exports = {
moduleType: 'component', moduleType: 'component',
name: 'rangeslider', name: 'rangeslider',


layoutNodes: ['xaxis.'], schema: {
layout: {
'xaxis.rangeslider': require('./attributes')
}
},

layoutAttributes: require('./attributes'), layoutAttributes: require('./attributes'),
handleDefaults: require('./defaults'), handleDefaults: require('./defaults'),


Expand Down
8 changes: 7 additions & 1 deletion src/constants/numerical.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -36,5 +36,11 @@ module.exports = {
ONEDAY: 86400000, ONEDAY: 86400000,
ONEHOUR: 3600000, ONEHOUR: 3600000,
ONEMIN: 60000, ONEMIN: 60000,
ONESEC: 1000 ONESEC: 1000,

/*
* For fast conversion btwn world calendars and epoch ms, the Julian Day Number
* of the unix epoch. From calendars.instance().newDate(1970, 1, 1).toJD()
*/
EPOCHJD: 2440587.5
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not super happy about this, but I had to find a way to make EPOCHJD requirable in both lib/dates.js and components/calendars/ in a non-circular way

Copy link
Collaborator

Choose a reason for hiding this comment

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

If the whole contents of the if(calendar) blocks went into the component, you wouldn't need EPOCHJD in dates at all, would you?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. This will lead to some return object destructuring though. I'll see how bad it gets.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I want to go through with this.

Moving all the logic from the if(calendar) blocks to the calendars component would involved passing oddly named variables to new routines and return sets of values and even mutating them in findExactDates.

I understand that if would be nice to put all the world-calendar logic in one place, but it would make the code less readable, unless we start duplicating some dates.js logic in the calendars component.

The current implementation is both DRY and readable - which gets my vote.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sure, lets leave it as is.

}; };
15 changes: 0 additions & 15 deletions src/lib/coerce.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@


'use strict'; 'use strict';


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


Expand Down Expand Up @@ -268,20 +267,6 @@ exports.valObjects = {


return true; return true;
} }
},
calendar: {
description: [
'A string, one of the calendar systems available',
'in the `world-calendars` package, to be used in evaluating',
'or displaying date data. Defaults to built-in (Gregorian) dates.',
'available calendars:', '*' + calendarList.join('*, *') + '*'
].join(' '),
requiredOpts: [],
otherOpts: ['dflt'],
coerceFunction: function(v, propOut, dflt) {
if(v && calendarList.indexOf(v) !== -1) propOut.set(v);
else propOut.set(dflt);
}
} }
}; };


Expand Down
Loading