diff --git a/lib/calendars.js b/lib/calendars.js new file mode 100644 index 00000000000..93eb53e3ba6 --- /dev/null +++ b/lib/calendars.js @@ -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'); diff --git a/lib/index.js b/lib/index.js index ea309cde21a..2f4c821fb27 100644 --- a/lib/index.js +++ b/lib/index.js @@ -54,4 +54,9 @@ Plotly.register([ require('./groupby') ]); +// components +Plotly.register([ + require('./calendars') +]); + module.exports = Plotly; diff --git a/package.json b/package.json index 17515ba7a89..bb9325e5d71 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,8 @@ "superscript-text": "^1.0.0", "tinycolor2": "^1.3.0", "topojson-client": "^2.1.0", - "webgl-context": "^2.2.0" + "webgl-context": "^2.2.0", + "world-calendars": "^1.0.0" }, "devDependencies": { "brfs": "^1.4.3", diff --git a/src/components/annotations/calc_autorange.js b/src/components/annotations/calc_autorange.js index a5969ae181b..9046863bf81 100644 --- a/src/components/annotations/calc_autorange.js +++ b/src/components/annotations/calc_autorange.js @@ -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)], { 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 }); diff --git a/src/components/calendars/calendars.js b/src/components/calendars/calendars.js new file mode 100644 index 00000000000..02dfbfefdc1 --- /dev/null +++ b/src/components/calendars/calendars.js @@ -0,0 +1,31 @@ +/** +* 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'; + +// a trimmed down version of: +// https://github.com/alexcjohnson/world-calendars/blob/master/dist/index.js + +module.exports = require('world-calendars/dist/main'); + +require('world-calendars/dist/plus'); + +require('world-calendars/dist/calendars/chinese'); +require('world-calendars/dist/calendars/coptic'); +require('world-calendars/dist/calendars/discworld'); +require('world-calendars/dist/calendars/ethiopian'); +require('world-calendars/dist/calendars/hebrew'); +require('world-calendars/dist/calendars/islamic'); +require('world-calendars/dist/calendars/julian'); +require('world-calendars/dist/calendars/mayan'); +require('world-calendars/dist/calendars/nanakshahi'); +require('world-calendars/dist/calendars/nepali'); +require('world-calendars/dist/calendars/persian'); +require('world-calendars/dist/calendars/taiwan'); +require('world-calendars/dist/calendars/thai'); +require('world-calendars/dist/calendars/ummalqura'); diff --git a/src/components/calendars/index.js b/src/components/calendars/index.js new file mode 100644 index 00000000000..076411f4739 --- /dev/null +++ b/src/components/calendars/index.js @@ -0,0 +1,253 @@ +/** +* 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('./calendars'); + +var Lib = require('../../lib'); +var constants = require('../../constants/numerical'); + +var EPOCHJD = constants.EPOCHJD; +var ONEDAY = constants.ONEDAY; + +var attributes = { + valType: 'enumerated', + 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 = { + chinese: '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 but we're going to give them +// 7-day week ticks so start on our Sundays. +// If anyone really cares we can customize the auto tick spacings for these calendars. +var CANONICAL_SUNDAY = { + chinese: '2000-01-02', + coptic: '2000-01-03', + discworld: '2000-01-03', + 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 = { + chinese: ['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'] +}; + +/* + * 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: { + 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 +}; diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index a6c28680724..e31b43873f3 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -174,7 +174,8 @@ module.exports = function draw(gd, id) { axisOptions = { letter: 'y', font: fullLayout.font, - noHover: true + noHover: true, + calendar: fullLayout.calendar // not really necessary (yet?) }; // Coerce w.r.t. Axes layoutAttributes: diff --git a/src/components/rangeselector/button_attributes.js b/src/components/rangeselector/button_attributes.js index 5ebfd7a7faa..db0f69cb017 100644 --- a/src/components/rangeselector/button_attributes.js +++ b/src/components/rangeselector/button_attributes.js @@ -33,7 +33,9 @@ module.exports = { '*step* milliseconds back.', 'For example, with `step` set to *year* and `count` set to *1*', 'the range update shifts the start of the range back to', - 'January 01 of the current year.' + 'January 01 of the current year.', + 'Month and year *todate* are currently available only', + 'for the built-in (Gregorian) calendar.' ].join(' ') }, count: { diff --git a/src/components/rangeselector/defaults.js b/src/components/rangeselector/defaults.js index bb568c2479c..4f9d4efd18a 100644 --- a/src/components/rangeselector/defaults.js +++ b/src/components/rangeselector/defaults.js @@ -16,7 +16,7 @@ var buttonAttrs = require('./button_attributes'); var constants = require('./constants'); -module.exports = function handleDefaults(containerIn, containerOut, layout, counterAxes) { +module.exports = function handleDefaults(containerIn, containerOut, layout, counterAxes, calendar) { var selectorIn = containerIn.rangeselector || {}, selectorOut = containerOut.rangeselector = {}; @@ -24,7 +24,7 @@ module.exports = function handleDefaults(containerIn, containerOut, layout, coun return Lib.coerce(selectorIn, selectorOut, attributes, attr, dflt); } - var buttons = buttonsDefaults(selectorIn, selectorOut); + var buttons = buttonsDefaults(selectorIn, selectorOut, calendar); var visible = coerce('visible', buttons.length > 0); if(!visible) return; @@ -45,7 +45,7 @@ module.exports = function handleDefaults(containerIn, containerOut, layout, coun coerce('borderwidth'); }; -function buttonsDefaults(containerIn, containerOut) { +function buttonsDefaults(containerIn, containerOut, calendar) { var buttonsIn = containerIn.buttons || [], buttonsOut = containerOut.buttons = []; @@ -63,7 +63,13 @@ function buttonsDefaults(containerIn, containerOut) { var step = coerce('step'); if(step !== 'all') { - coerce('stepmode'); + if(calendar && calendar !== 'gregorian' && (step === 'month' || step === 'year')) { + buttonOut.stepmode = 'backward'; + } + else { + coerce('stepmode'); + } + coerce('count'); } diff --git a/src/components/rangeselector/get_update_object.js b/src/components/rangeselector/get_update_object.js index 71978d0a310..e01624b7c61 100644 --- a/src/components/rangeselector/get_update_object.js +++ b/src/components/rangeselector/get_update_object.js @@ -11,9 +11,6 @@ var d3 = require('d3'); -var Lib = require('../../lib'); - - module.exports = function getUpdateObject(axisLayout, buttonLayout) { var axName = axisLayout._name; var update = {}; @@ -33,7 +30,7 @@ module.exports = function getUpdateObject(axisLayout, buttonLayout) { function getXRange(axisLayout, buttonLayout) { var currentRange = axisLayout.range; - var base = new Date(Lib.dateTime2ms(currentRange[1])); + var base = new Date(axisLayout.r2l(currentRange[1])); var step = buttonLayout.step, count = buttonLayout.count; @@ -42,13 +39,13 @@ function getXRange(axisLayout, buttonLayout) { switch(buttonLayout.stepmode) { case 'backward': - range0 = Lib.ms2DateTime(+d3.time[step].utc.offset(base, -count)); + range0 = axisLayout.l2r(+d3.time[step].utc.offset(base, -count)); break; case 'todate': var base2 = d3.time[step].utc.offset(base, -count); - range0 = Lib.ms2DateTime(+d3.time[step].utc.ceil(base2)); + range0 = axisLayout.l2r(+d3.time[step].utc.ceil(base2)); break; } diff --git a/src/components/rangeselector/index.js b/src/components/rangeselector/index.js index 600a7c8e608..baad450c296 100644 --- a/src/components/rangeselector/index.js +++ b/src/components/rangeselector/index.js @@ -12,7 +12,12 @@ module.exports = { moduleType: 'component', name: 'rangeselector', - layoutNodes: ['xaxis.'], + schema: { + layout: { + 'xaxis.rangeselector': require('./attributes') + } + }, + layoutAttributes: require('./attributes'), handleDefaults: require('./defaults'), diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 01c07091e01..ab448bb022a 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -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; } diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 2a92d8d3091..e1f361acfe0 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -342,7 +342,8 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { xaxis: { type: axisOpts.type, domain: [0, 1], - range: opts.range.slice() + range: opts.range.slice(), + calendar: axisOpts.calendar }, width: opts._width, height: opts._height, @@ -352,7 +353,8 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { domain: [0, 1], - range: oppAxisOpts.range.slice() + range: oppAxisOpts.range.slice(), + calendar: oppAxisOpts.calendar }; Plots.supplyDefaults(mockFigure); diff --git a/src/components/rangeslider/index.js b/src/components/rangeslider/index.js index 7b652dc43c3..cd929887fc3 100644 --- a/src/components/rangeslider/index.js +++ b/src/components/rangeslider/index.js @@ -12,7 +12,12 @@ module.exports = { moduleType: 'component', name: 'rangeslider', - layoutNodes: ['xaxis.'], + schema: { + layout: { + 'xaxis.rangeslider': require('./attributes') + } + }, + layoutAttributes: require('./attributes'), handleDefaults: require('./defaults'), diff --git a/src/constants/numerical.js b/src/constants/numerical.js index d10ffa42a51..85155e8d16c 100644 --- a/src/constants/numerical.js +++ b/src/constants/numerical.js @@ -36,5 +36,11 @@ module.exports = { ONEDAY: 86400000, ONEHOUR: 3600000, 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 }; diff --git a/src/lib/dates.js b/src/lib/dates.js index a486bc797ee..503acd6ef48 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -10,8 +10,10 @@ 'use strict'; var d3 = require('d3'); +var isNumeric = require('fast-isnumeric'); var logError = require('./loggers').error; +var mod = require('./mod'); var constants = require('../constants/numerical'); var BADNUM = constants.BADNUM; @@ -19,12 +21,55 @@ var ONEDAY = constants.ONEDAY; var ONEHOUR = constants.ONEHOUR; var ONEMIN = constants.ONEMIN; var ONESEC = constants.ONESEC; +var EPOCHJD = constants.EPOCHJD; -var DATETIME_REGEXP = /^\s*(-?\d\d\d\d|\d\d)(-(0?[1-9]|1[012])(-([0-3]?\d)([ Tt]([01]?\d|2[0-3])(:([0-5]\d)(:([0-5]\d(\.\d+)?))?(Z|z|[+\-]\d\d:?\d\d)?)?)?)?)?\s*$/m; +var Registry = require('../registry'); + +var utcFormat = d3.time.format.utc; + +var DATETIME_REGEXP = /^\s*(-?\d\d\d\d|\d\d)(-(\d?\d)(-(\d?\d)([ Tt]([01]?\d|2[0-3])(:([0-5]\d)(:([0-5]\d(\.\d+)?))?(Z|z|[+\-]\d\d:?\d\d)?)?)?)?)?\s*$/m; +// special regex for chinese calendars to support yyyy-mmi-dd etc for intercalary months +var DATETIME_REGEXP_CN = /^\s*(-?\d\d\d\d|\d\d)(-(\d?\di?)(-(\d?\d)([ Tt]([01]?\d|2[0-3])(:([0-5]\d)(:([0-5]\d(\.\d+)?))?(Z|z|[+\-]\d\d:?\d\d)?)?)?)?)?\s*$/m; // for 2-digit years, the first year we map them onto var YFIRST = new Date().getFullYear() - 70; +function isWorldCalendar(calendar) { + return ( + calendar && + Registry.componentsRegistry.calendars && + typeof calendar === 'string' && calendar !== 'gregorian' + ); +} + +/* + * dateTick0: get the canonical tick for this calendar + * + * bool sunday is for week ticks, shift it to a Sunday. + */ +exports.dateTick0 = function(calendar, sunday) { + if(isWorldCalendar(calendar)) { + return sunday ? + Registry.getComponentMethod('calendars', 'CANONICAL_SUNDAY')[calendar] : + Registry.getComponentMethod('calendars', 'CANONICAL_TICK')[calendar]; + } + else { + return sunday ? '2000-01-02' : '2000-01-01'; + } +}; + +/* + * dfltRange: for each calendar, give a valid default range + */ +exports.dfltRange = function(calendar) { + if(isWorldCalendar(calendar)) { + return Registry.getComponentMethod('calendars', 'DFLTRANGE')[calendar]; + } + else { + return ['2000-01-01', '2001-01-01']; + } +}; + // is an object a javascript date? exports.isJSDate = function(v) { return typeof v === 'object' && v !== null && typeof v.getTime === 'function'; @@ -38,6 +83,7 @@ var MIN_MS, MAX_MS; /** * dateTime2ms - turn a date object or string s into milliseconds * (relative to 1970-01-01, per javascript standard) + * optional calendar (string) to use a non-gregorian calendar * * Returns BADNUM if it doesn't find a date * @@ -62,6 +108,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...) @@ -83,8 +133,7 @@ var MIN_MS, MAX_MS; * currently (2016) this range is: * 1946-2045 */ - -exports.dateTime2ms = function(s) { +exports.dateTime2ms = function(s, calendar) { // first check if s is a date object if(exports.isJSDate(s)) { // Convert to the UTC milliseconds that give the same @@ -96,24 +145,70 @@ exports.dateTime2ms = function(s) { // 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 isChinese = isWorld && calendar.substr(0, 7) === 'chinese'; + + var match = s.match(isChinese ? DATETIME_REGEXP_CN : DATETIME_REGEXP); if(!match) return BADNUM; var y = match[1], - m = Number(match[3] || 1), + m = match[3] || '1', d = Number(match[5] || 1), H = Number(match[7] || 0), M = Number(match[9] || 0), S = Number(match[11] || 0); + + if(isWorld) { + // disallow 2-digit years for world calendars + if(y.length === 2) return BADNUM; + y = Number(y); + + var cDate; + try { + var calInstance = Registry.getComponentMethod('calendars', 'getCal')(calendar); + if(isChinese) { + var isIntercalary = m.charAt(m.length - 1) === 'i'; + m = parseInt(m, 10); + cDate = calInstance.newDate(y, calInstance.toMonthIndex(y, m, isIntercalary), d); + } + else { + cDate = calInstance.newDate(y, Number(m), d); + } + } + catch(e) { return BADNUM; } // Invalid ... date + + if(!cDate) return BADNUM; + + return ((cDate.toJD() - EPOCHJD) * ONEDAY) + + (H * ONEHOUR) + (M * ONEMIN) + (S * ONESEC); + } + if(y.length === 2) { y = (Number(y) + 2000 - YFIRST) % 100 + YFIRST; } else y = Number(y); + // new Date uses months from 0; subtract 1 here just so we + // don't have to do it again during the validity test below + m -= 1; + // javascript takes new Date(0..99,m,d) to mean 1900-1999, so // to support years 0-99 we need to use setFullYear explicitly - var date = new Date(Date.UTC(2000, m - 1, d, H, M)); + // Note that 2000 is a leap year. + var date = new Date(Date.UTC(2000, m, d, H, M)); date.setUTCFullYear(y); + if(date.getUTCMonth() !== m) return BADNUM; if(date.getUTCDate() !== d) return BADNUM; return date.getTime() + S * ONESEC; @@ -123,8 +218,8 @@ MIN_MS = exports.MIN_MS = exports.dateTime2ms('-9999'); MAX_MS = exports.MAX_MS = exports.dateTime2ms('9999-12-31 23:59:59.9999'); // is string s a date? (see above) -exports.isDateTime = function(s) { - return (exports.dateTime2ms(s) !== BADNUM); +exports.isDateTime = function(s, calendar) { + return (exports.dateTime2ms(s, calendar) !== BADNUM); }; // pad a number with zeroes, to given # of digits before the decimal point @@ -143,21 +238,57 @@ function lpad(val, digits) { var NINETYDAYS = 90 * ONEDAY; var THREEHOURS = 3 * ONEHOUR; var FIVEMIN = 5 * ONEMIN; -exports.ms2DateTime = function(ms, r) { +exports.ms2DateTime = function(ms, r, calendar) { if(typeof ms !== 'number' || !(ms >= MIN_MS && ms <= MAX_MS)) return BADNUM; if(!r) r = 0; - var msecTenths = Math.round(((ms % 1) + 1) * 10) % 10, - d = new Date(Math.round(ms - msecTenths / 10)), - dateStr = d3.time.format.utc('%Y-%m-%d')(d), + var msecTenths = Math.floor(mod(ms + 0.05, 1) * 10), + msRounded = Math.round(ms - msecTenths / 10), + dateStr, h, m, s, msec10, d; + + if(isWorldCalendar(calendar)) { + var dateJD = Math.floor(msRounded / ONEDAY) + EPOCHJD, + timeMs = Math.floor(mod(ms, ONEDAY)); + try { + dateStr = Registry.getComponentMethod('calendars', '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 + // it manually (after the '-' if there is one) + if(dateStr.charAt(0) === '-') { + while(dateStr.length < 11) dateStr = '-0' + dateStr.substr(1); + } + else { + while(dateStr.length < 10) dateStr = '0' + dateStr; + } + + // TODO: if this is faster, we could use this block for extracting + // the time components of regular gregorian too + h = (r < NINETYDAYS) ? Math.floor(timeMs / ONEHOUR) : 0; + m = (r < NINETYDAYS) ? Math.floor((timeMs % ONEHOUR) / ONEMIN) : 0; + s = (r < THREEHOURS) ? Math.floor((timeMs % ONEMIN) / ONESEC) : 0; + msec10 = (r < FIVEMIN) ? (timeMs % ONESEC) * 10 + msecTenths : 0; + } + else { + d = new Date(msRounded); + + dateStr = utcFormat('%Y-%m-%d')(d); + // <90 days: add hours and minutes - never *only* add hours - h = (r < NINETYDAYS) ? d.getUTCHours() : 0, - m = (r < NINETYDAYS) ? d.getUTCMinutes() : 0, + h = (r < NINETYDAYS) ? d.getUTCHours() : 0; + m = (r < NINETYDAYS) ? d.getUTCMinutes() : 0; // <3 hours: add seconds - s = (r < THREEHOURS) ? d.getUTCSeconds() : 0, + s = (r < THREEHOURS) ? d.getUTCSeconds() : 0; // <5 minutes: add ms (plus one extra digit, this is msec*10) msec10 = (r < FIVEMIN) ? d.getUTCMilliseconds() * 10 + msecTenths : 0; + } return includeTime(dateStr, h, m, s, msec10); }; @@ -171,7 +302,7 @@ exports.ms2DateTime = function(ms, r) { exports.ms2DateTimeLocal = function(ms) { if(!(ms >= MIN_MS + ONEDAY && ms <= MAX_MS - ONEDAY)) return BADNUM; - var msecTenths = Math.round(((ms % 1) + 1) * 10) % 10, + var msecTenths = Math.floor(mod(ms + 0.05, 1) * 10), d = new Date(Math.round(ms - msecTenths / 10)), dateStr = d3.time.format('%Y-%m-%d')(d), h = d.getHours(), @@ -204,17 +335,265 @@ function includeTime(dateStr, h, m, s, msec10) { // normalize date format to date string, in case it starts as // a Date object or milliseconds // optional dflt is the return value if cleaning fails -exports.cleanDate = function(v, dflt) { +exports.cleanDate = function(v, dflt, calendar) { if(exports.isJSDate(v) || typeof v === 'number') { + // do not allow milliseconds (old) or jsdate objects (inherently + // described as gregorian dates) with world calendars + if(isWorldCalendar(calendar)) { + logError('JS Dates and milliseconds are incompatible with world calendars', v); + return dflt; + } + // NOTE: if someone puts in a year as a number rather than a string, // this will mistakenly convert it thinking it's milliseconds from 1970 // that is: '2012' -> Jan. 1, 2012, but 2012 -> 2012 epoch milliseconds v = exports.ms2DateTimeLocal(+v); if(!v && dflt !== undefined) return dflt; } - else if(!exports.isDateTime(v)) { + else if(!exports.isDateTime(v, calendar)) { logError('unrecognized date', v); return dflt; } return v; }; + +/* + * Date formatting for ticks and hovertext + */ + +/* + * modDateFormat: Support world calendars, and add one item to + * d3's vocabulary: + * %{n}f where n is the max number of digits of fractional seconds + */ +var fracMatch = /%(\d?)f/g; +function modDateFormat(fmt, x, calendar) { + var fm = fmt.match(fracMatch), + d = new Date(x); + if(fm) { + var digits = Math.min(+fm[1] || 6, 6), + fracSecs = String((x / 1000 % 1) + 2.0000005) + .substr(2, digits).replace(/0+$/, '') || '0'; + fmt = fmt.replace(fracMatch, fracSecs); + } + if(isWorldCalendar(calendar)) { + try { + fmt = Registry.getComponentMethod('calendars', 'worldCalFmt')(fmt, x, calendar); + } + catch(e) { + return 'Invalid'; + } + } + return utcFormat(fmt)(d); +} + +/* + * formatTime: create a time string from: + * x: milliseconds + * tr: tickround ('M', 'S', or # digits) + * only supports UTC times (where every day is 24 hours and 0 is at midnight) + */ +function formatTime(x, tr) { + var timePart = mod(x, ONEDAY); + + var timeStr = lpad(Math.floor(timePart / ONEHOUR), 2) + ':' + + lpad(mod(Math.floor(timePart / ONEMIN), 60), 2); + + if(tr !== 'M') { + if(!isNumeric(tr)) tr = 0; // should only be 'S' + timeStr += ':' + String(100 + d3.round(mod(x / ONESEC, 60), tr)).substr(1); + } + return timeStr; +} + +var yearFormat = utcFormat('%Y'), + monthFormat = utcFormat('%b %Y'), + dayFormat = utcFormat('%b %-d'), + yearMonthDayFormat = utcFormat('%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. + * + * x: milliseconds, the value to convert + * fmt: optional, an explicit format string (d3 format, even for world calendars) + * tr: tickround ('y', 'm', 'd', 'M', 'S', or # digits) + * used if no explicit fmt is provided + * calendar: optional string, the world calendar system to use + * + * returns the date/time as a string, potentially with the leading portion + * on a separate line (after '\n') + * Note that this means if you provide an explicit format which includes '\n' + * 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, calendar) { + var headStr, + dateStr; + + calendar = isWorldCalendar(calendar) && calendar; + + if(fmt) return modDateFormat(fmt, x, 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(x); + + if(tr === 'y') dateStr = yearFormat(d); + else if(tr === 'm') dateStr = monthFormat(d); + else if(tr === 'd') { + headStr = yearFormat(d); + dateStr = dayFormat(d); + } + else { + headStr = yearMonthDayFormat(d); + dateStr = formatTime(x, tr); + } + } + + 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? + * + * 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 = Registry.getComponentMethod('calendars', '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) && + Registry.getComponentMethod('calendars', '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 + }; +}; diff --git a/src/lib/index.js b/src/lib/index.js index 023896b0e80..5a66ac4c01b 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -16,6 +16,7 @@ var lib = module.exports = {}; lib.nestedProperty = require('./nested_property'); lib.isPlainObject = require('./is_plain_object'); lib.isArray = require('./is_array'); +lib.mod = require('./mod'); var coerceModule = require('./coerce'); lib.valObjects = coerceModule.valObjects; @@ -31,6 +32,11 @@ lib.ms2DateTime = datesModule.ms2DateTime; 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; @@ -135,6 +141,22 @@ lib.identity = function(d) { return d; }; // minor convenience helper lib.noop = function() {}; +/* + * simpleMap: alternative to Array.map that only + * passes on the element and up to 2 extra args you + * provide (but not the array index or the whole array) + * + * array: the array to map it to + * func: the function to apply + * x1, x2: optional extra args + */ +lib.simpleMap = function(array, func, x1, x2) { + var len = array.length, + out = new Array(len); + for(var i = 0; i < len; i++) out[i] = func(array[i], x1, x2); + return out; +}; + // random string generator lib.randstr = function randstr(existing, bits, base) { /* diff --git a/src/lib/mod.js b/src/lib/mod.js new file mode 100644 index 00000000000..0ce9323b28f --- /dev/null +++ b/src/lib/mod.js @@ -0,0 +1,18 @@ +/** +* 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'; + +/** + * sanitized modulus function that always returns in the range [0, d) + * rather than (-d, 0] if v is negative + */ +module.exports = function mod(v, d) { + var out = v % d; + return out < 0 ? out + d : out; +}; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 8d21c672bdd..2026234ae8d 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -312,7 +312,7 @@ Plotly.plot = function(gd, data, layout, config) { // show annotations and shapes Registry.getComponentMethod('shapes', 'draw')(gd); - Registry.getComponentMethod('annoations', 'draw')(gd); + Registry.getComponentMethod('annotations', 'draw')(gd); // source links Plots.addLinks(gd); @@ -1295,7 +1295,8 @@ function _restyle(gd, aobj, _traces) { 'domain.x[0]', 'domain.x[1]', 'domain.y[0]', 'domain.y[1]', 'tilt', 'tiltaxis', 'depth', 'direction', 'rotation', 'pull', 'line.showscale', 'line.cauto', 'line.autocolorscale', 'line.reversescale', - 'marker.line.showscale', 'marker.line.cauto', 'marker.line.autocolorscale', 'marker.line.reversescale' + 'marker.line.showscale', 'marker.line.cauto', 'marker.line.autocolorscale', 'marker.line.reversescale', + 'xcalendar', 'ycalendar' ]; for(i = 0; i < traces.length; i++) { @@ -2000,6 +2001,7 @@ function _relayout(gd, aobj) { p.parts[1] === 'rangemode' || p.parts[1] === 'type' || p.parts[1] === 'domain' || + ai.indexOf('calendar') !== -1 || ai.match(/^(bar|box|font)/)) { flags.docalc = true; } diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index c5a55ef34ee..1be1b01ba24 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -141,7 +141,7 @@ exports.findArrayAttributes = function(trace) { function callback(attr, attrName, attrs, level) { stack = stack.slice(0, level).concat([attrName]); - var splittableAttr = attr.valType === 'data_array' || attr.arrayOk === true; + var splittableAttr = attr && (attr.valType === 'data_array' || attr.arrayOk === true); if(!splittableAttr) return; var astr = toAttrString(stack); @@ -164,6 +164,7 @@ exports.findArrayAttributes = function(trace) { var transform = transforms[i]; stack = ['transforms[' + i + ']']; + exports.crawl(transform._module.attributes, callback, 1); } } @@ -211,6 +212,17 @@ function getTraceAttributes(type) { extendDeep(attributes, basePlotModule.attributes); } + // add registered components trace attributes + Object.keys(Registry.componentsRegistry).forEach(function(k) { + var _module = Registry.componentsRegistry[k]; + + if(_module.schema && _module.schema.traces && _module.schema.traces[type]) { + Object.keys(_module.schema.traces[type]).forEach(function(v) { + insertAttrs(attributes, _module.schema.traces[type][v], v); + }); + } + }); + // 'type' gets overwritten by baseAttributes; reset it here attributes.type = type; @@ -256,19 +268,19 @@ function getLayoutAttributes() { // polar layout attributes layoutAttributes = assignPolarLayoutAttrs(layoutAttributes); - // add registered components layout attribute + // add registered components layout attributes Object.keys(Registry.componentsRegistry).forEach(function(k) { var _module = Registry.componentsRegistry[k]; if(!_module.layoutAttributes) return; - if(Array.isArray(_module.layoutNodes)) { - _module.layoutNodes.forEach(function(v) { - handleRegisteredComponent(layoutAttributes, _module, v + _module.name); + if(_module.schema && _module.schema.layout) { + Object.keys(_module.schema.layout).forEach(function(v) { + insertAttrs(layoutAttributes, _module.schema.layout[v], v); }); } else { - handleRegisteredComponent(layoutAttributes, _module, _module.name); + insertAttrs(layoutAttributes, _module.layoutAttributes, _module.name); } }); @@ -279,9 +291,21 @@ function getLayoutAttributes() { function getTransformAttributes(type) { var _module = Registry.transformsRegistry[type]; + var attributes = extendDeep({}, _module.attributes); + + // add registered components transform attributes + Object.keys(Registry.componentsRegistry).forEach(function(k) { + var _module = Registry.componentsRegistry[k]; + + if(_module.schema && _module.schema.transforms && _module.schema.transforms[type]) { + Object.keys(_module.schema.transforms[type]).forEach(function(v) { + insertAttrs(attributes, _module.schema.transforms[type][v], v); + }); + } + }); return { - attributes: formatAttributes(_module.attributes) + attributes: formatAttributes(attributes) }; } @@ -365,9 +389,8 @@ function handleBasePlotModule(layoutAttributes, _module, astr) { np.set(attrs); } -function handleRegisteredComponent(layoutAttributes, _module, astr) { - var np = Lib.nestedProperty(layoutAttributes, astr), - attrs = extendDeep(np.get() || {}, _module.layoutAttributes); +function insertAttrs(baseAttrs, newAttrs, astr) { + var np = Lib.nestedProperty(baseAttrs, astr); - np.set(attrs); + np.set(extendDeep(np.get() || {}, newAttrs)); } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 9f9b7da42d6..44556f88eb3 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -27,6 +27,7 @@ var ONEDAY = constants.ONEDAY; var ONEHOUR = constants.ONEHOUR; var ONEMIN = constants.ONEMIN; var ONESEC = constants.ONESEC; +var BADNUM = constants.BADNUM; var axes = module.exports = {}; @@ -121,7 +122,7 @@ axes.coercePosition = function(containerOut, gd, coerce, axRef, attr, dflt) { } } else if(ax.type === 'date') { - containerOut[attr] = Lib.cleanDate(pos); + containerOut[attr] = Lib.cleanDate(pos, BADNUM, ax.calendar); return; } } @@ -220,7 +221,7 @@ axes.getAutoRange = function(ax) { axReverse = false; if(ax.range) { - var rng = ax.range.map(ax.r2l); + var rng = Lib.simpleMap(ax.range, ax.r2l); axReverse = rng[1] < rng[0]; } @@ -313,7 +314,7 @@ axes.getAutoRange = function(ax) { // maintain reversal if(axReverse) newRange.reverse(); - return newRange.map(ax.l2r || Number); + return Lib.simpleMap(newRange, ax.l2r || Number); }; axes.doAutoRange = function(ax) { @@ -485,10 +486,12 @@ axes.expand = function(ax, data, options) { }; -axes.autoBin = function(data, ax, nbins, is2d) { +axes.autoBin = function(data, ax, nbins, is2d, calendar) { var dataMin = Lib.aggNums(Math.min, null, data), dataMax = Lib.aggNums(Math.max, null, data); + if(!calendar) calendar = ax.calendar; + if(ax.type === 'category') { return { start: dataMin - 0.5, @@ -518,23 +521,21 @@ axes.autoBin = function(data, ax, nbins, is2d) { if(ax.type === 'log') { dummyAx = { type: 'linear', - range: [dataMin, dataMax], - r2l: Number + range: [dataMin, dataMax] }; } else { dummyAx = { type: ax.type, - // conversion below would be ax.c2r but that's only different from l2r - // for log, and this is the only place (so far?) we would want c2r. - range: [dataMin, dataMax].map(ax.l2r), - r2l: ax.r2l + range: Lib.simpleMap([dataMin, dataMax], ax.c2r, 0, calendar), + calendar: calendar }; } + axes.setConvert(dummyAx); axes.autoTicks(dummyAx, size0); var binStart = axes.tickIncrement( - axes.tickFirst(dummyAx), dummyAx.dtick, 'reverse'), + axes.tickFirst(dummyAx), dummyAx.dtick, 'reverse', calendar), binEnd; // check for too many data points right at the edges of bins @@ -553,20 +554,20 @@ axes.autoBin = function(data, ax, nbins, is2d) { // we bin it on a linear axis (which one could argue against, but that's // a separate issue) if(dummyAx.dtick.charAt(0) === 'M') { - binStart = autoShiftMonthBins(binStart, data, dummyAx.dtick, dataMin); + binStart = autoShiftMonthBins(binStart, data, dummyAx.dtick, dataMin, calendar); } // calculate the endpoint for nonlinear ticks - you have to // just increment until you're done binEnd = binStart; while(binEnd <= dataMax) { - binEnd = axes.tickIncrement(binEnd, dummyAx.dtick); + binEnd = axes.tickIncrement(binEnd, dummyAx.dtick, false, calendar); } } return { - start: ax.c2r(binStart), - end: ax.c2r(binEnd), + start: ax.c2r(binStart, 0, calendar), + end: ax.c2r(binEnd, 0, calendar), size: dummyAx.dtick }; }; @@ -618,59 +619,22 @@ function autoShiftNumericBins(binStart, data, ax, dataMin, dataMax) { } -function autoShiftMonthBins(binStart, data, dtick, dataMin) { - var exactYears = 0, - exactMonths = 0, - exactDays = 0, - blankCount = 0, - dataCount, - di, - d, - year, - month; - - for(var i = 0; i < data.length; i++) { - di = data[i]; - if(!isNumeric(di)) { - blankCount ++; - continue; - } - d = new Date(di), - year = d.getUTCFullYear(); - if(di === Date.UTC(year, 0, 1)) { - exactYears ++; - } - else { - month = d.getUTCMonth(); - if(di === Date.UTC(year, month, 1)) { - exactMonths ++; - } - else if(di === Date.UTC(year, month, d.getUTCDate())) { - exactDays ++; - } - } - } - - dataCount = data.length - blankCount; - - // include bigger exact dates in the smaller ones - exactMonths += exactYears; - exactDays += exactMonths; - - // unmber of data points that needs to be an exact value +function autoShiftMonthBins(binStart, data, dtick, dataMin, calendar) { + var stats = Lib.findExactDates(data, calendar); + // number of data points that needs to be an exact value // to shift that increment to (near) the bin center - var threshold = 0.8 * dataCount; + var threshold = 0.8; - if(exactDays > threshold) { + if(stats.exactDays > threshold) { var numMonths = Number(dtick.substr(1)); - if((exactYears > threshold) && (numMonths % 12 === 0)) { + if((stats.exactYears > threshold) && (numMonths % 12 === 0)) { // The exact middle of a non-leap-year is 1.5 days into July // so if we start the bins here, all but leap years will // get hover-labeled as exact years. binStart = axes.tickIncrement(binStart, 'M6', 'reverse') + ONEDAY * 1.5; } - else if(exactMonths > threshold) { + else if(stats.exactMonths > threshold) { // Months are not as clean, but if we shift half the *longest* // month (31/2 days) then 31-day months will get labeled exactly // and shorter months will get labeled with the correct month @@ -700,7 +664,7 @@ function autoShiftMonthBins(binStart, data, dtick, dataMin) { // in any case, set tickround to # of digits to round tick labels to, // or codes to this effect for log and date scales axes.calcTicks = function calcTicks(ax) { - var rng = ax.range.map(ax.r2l); + var rng = Lib.simpleMap(ax.range, ax.r2l); // calculate max number of (auto) ticks to display based on plot size if(ax.tickmode === 'auto' || !ax.dtick) { @@ -757,7 +721,7 @@ axes.calcTicks = function calcTicks(ax) { } for(var x = ax._tmin; (axrev) ? (x >= endtick) : (x <= endtick); - x = axes.tickIncrement(x, ax.dtick, axrev)) { + x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar)) { vals.push(x); // prevent infinite loops @@ -787,7 +751,7 @@ function arrayTicks(ax) { var vals = ax.tickvals, text = ax.ticktext, ticksOut = new Array(vals.length), - rng = ax.range.map(ax.r2l), + rng = Lib.simpleMap(ax.range, ax.r2l), r0expanded = rng[0] * 1.0001 - rng[1] * 0.0001, r1expanded = rng[1] * 1.0001 - rng[0] * 0.0001, tickMin = Math.min(r0expanded, r1expanded), @@ -857,7 +821,7 @@ axes.autoTicks = function(ax, roughDTick) { var base; if(ax.type === 'date') { - ax.tick0 = '2000-01-01'; + ax.tick0 = Lib.dateTick0(ax.calendar); // the criteria below are all based on the rough spacing we calculate // being > half of the final unit - so precalculate twice the rough val var roughX2 = 2 * roughDTick; @@ -876,7 +840,7 @@ axes.autoTicks = function(ax, roughDTick) { // get week ticks on sunday // this will also move the base tick off 2000-01-01 if dtick is // 2 or 3 days... but that's a weird enough case that we'll ignore it. - ax.tick0 = '2000-01-02'; + ax.tick0 = Lib.dateTick0(ax.calendar, true); } else if(roughX2 > ONEHOUR) { ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24); @@ -895,7 +859,7 @@ axes.autoTicks = function(ax, roughDTick) { } else if(ax.type === 'log') { ax.tick0 = 0; - var rng = ax.range.map(ax.r2l); + var rng = Lib.simpleMap(ax.range, ax.r2l); if(roughDTick > 0.7) { // only show powers of 10 @@ -960,9 +924,9 @@ function autoTickRound(ax) { // If tick0 is unusual, give tickround a bit more information // not necessarily *all* the information in tick0 though, if it's really odd // minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19 - // take off a leading minus (year < 0 so length is consistent) - var tick0ms = Lib.dateTime2ms(ax.tick0), - tick0str = Lib.ms2DateTime(tick0ms).replace(/^-/, ''), + // take off a leading minus (year < 0) and i (intercalary month) so length is consistent + var tick0ms = ax.r2l(ax.tick0), + tick0str = ax.l2r(tick0ms).replace(/(^-|i)/g, ''), tick0len = tick0str.length; if(String(dtick).charAt(0) === 'M') { @@ -975,9 +939,10 @@ function autoTickRound(ax) { else if((dtick >= ONEMIN && tick0len <= 16) || (dtick >= ONEHOUR)) ax._tickround = 'M'; else if((dtick >= ONESEC && tick0len <= 19) || (dtick >= ONEMIN)) ax._tickround = 'S'; else { + // tickround is a number of digits of fractional seconds // of any two adjacent ticks, at least one will have the maximum fractional digits // of all possible ticks - so take the max. length of tick0 and the next one - var tick1len = Lib.ms2DateTime(tick0ms + dtick).replace(/^-/, '').length; + var tick1len = ax.l2r(tick0ms + dtick).replace(/^-/, '').length; ax._tickround = Math.max(tick0len, tick1len) - 20; } } @@ -1008,36 +973,18 @@ function autoTickRound(ax) { // for pure powers of 10 // numeric ticks always have constant differences, other datetime ticks // can all be calculated as constant number of milliseconds -var THREEDAYS = 3 * ONEDAY; -axes.tickIncrement = function(x, dtick, axrev) { +axes.tickIncrement = function(x, dtick, axrev, calendar) { var axSign = axrev ? -1 : 1; - // includes all dates smaller than month, and pure 10^n in log + // includes linear, all dates smaller than month, and pure 10^n in log if(isNumeric(dtick)) return x + axSign * dtick; + // everything else is a string, one character plus a number var tType = dtick.charAt(0), dtSigned = axSign * Number(dtick.substr(1)); - // Dates: months (or years) - if(tType === 'M') { - /* - * set(UTC)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 y = new Date(x + THREEDAYS); - return y.setUTCMonth(y.getUTCMonth() + dtSigned) - THREEDAYS; - } + // Dates: months (or years - see Lib.incrementMonth) + if(tType === 'M') return Lib.incrementMonth(x, dtSigned, calendar); // Log scales: Linear, Digits else if(tType === 'L') return Math.log(Math.pow(10, x) + dtSigned) / Math.LN10; @@ -1047,7 +994,7 @@ axes.tickIncrement = function(x, dtick, axrev) { else if(tType === 'D') { var tickset = (dtick === 'D2') ? roundLog2 : roundLog1, x2 = x + axSign * 0.01, - frac = Lib.roundUp(mod(x2, 1), tickset, axrev); + frac = Lib.roundUp(Lib.mod(x2, 1), tickset, axrev); return Math.floor(x2) + Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10; @@ -1058,7 +1005,7 @@ axes.tickIncrement = function(x, dtick, axrev) { // calculate the first tick on an axis axes.tickFirst = function(ax) { var r2l = ax.r2l || Number, - rng = ax.range.map(r2l), + rng = Lib.simpleMap(ax.range, r2l), axrev = rng[1] < rng[0], sRound = axrev ? Math.floor : Math.ceil, // add a tiny extra bit to make sure we get ticks @@ -1078,24 +1025,32 @@ axes.tickFirst = function(ax) { } var tType = dtick.charAt(0), - dtNum = Number(dtick.substr(1)), - t0, - mdif, - t1; + dtNum = Number(dtick.substr(1)); // Dates: months (or years) if(tType === 'M') { - t0 = new Date(tick0); - r0 = new Date(r0); - mdif = (r0.getUTCFullYear() - t0.getUTCFullYear()) * 12 + - r0.getUTCMonth() - t0.getUTCMonth(); - t1 = t0.setUTCMonth(t0.getUTCMonth() + - (Math.round(mdif / dtNum) + (axrev ? 1 : -1)) * dtNum); - - while(axrev ? t1 > r0 : t1 < r0) { - t1 = axes.tickIncrement(t1, dtick, axrev); + var cnt = 0, + t0 = tick0, + t1, + mult, + newDTick; + + // This algorithm should work for *any* nonlinear (but close to linear!) + // tick spacing. Limit to 10 iterations, for gregorian months it's normally <=3. + while(cnt < 10) { + t1 = axes.tickIncrement(t0, dtick, axrev, ax.calendar); + if((t1 - r0) * (t0 - r0) <= 0) { + // t1 and t0 are on opposite sides of r0! we've succeeded! + if(axrev) return Math.min(t0, t1); + return Math.max(t0, t1); + } + mult = (r0 - ((t0 + t1) / 2)) / (t1 - t0); + newDTick = tType + ((Math.abs(Math.round(mult)) || 1) * dtNum); + t0 = axes.tickIncrement(t0, newDTick, mult < 0 ? !axrev : axrev, ax.calendar); + cnt++; } - return t1; + Lib.error('tickFirst did not converge', ax); + return t0; } // Log scales: Linear, Digits @@ -1105,7 +1060,7 @@ axes.tickFirst = function(ax) { } else if(tType === 'D') { var tickset = (dtick === 'D2') ? roundLog2 : roundLog1, - frac = Lib.roundUp(mod(r0, 1), tickset, axrev); + frac = Lib.roundUp(Lib.mod(r0, 1), tickset, axrev); return Math.floor(r0) + Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10; @@ -1113,32 +1068,6 @@ axes.tickFirst = function(ax) { else throw 'unrecognized dtick ' + String(dtick); }; -var utcFormat = d3.time.format.utc, - yearFormat = utcFormat('%Y'), - monthFormat = utcFormat('%b %Y'), - dayFormat = utcFormat('%b %-d'), - yearMonthDayFormat = utcFormat('%b %-d, %Y'), - minuteFormat = utcFormat('%H:%M'), - secondFormat = utcFormat(':%S'); - -// add one item to d3's vocabulary: -// %{n}f where n is the max number of digits -// of fractional seconds -var fracMatch = /%(\d?)f/g; -function modDateFormat(fmt, x) { - var fm = fmt.match(fracMatch), - d = new Date(x); - if(fm) { - var digits = Math.min(+fm[1] || 6, 6), - fracSecs = String((x / 1000 % 1) + 2.0000005) - .substr(2, digits).replace(/0+$/, '') || '0'; - return utcFormat(fmt.replace(fracMatch, fracSecs))(d); - } - else { - return utcFormat(fmt)(d); - } -} - // draw the text for one tick. // px,py are the location on gd.paper // prefix is there so the x axis ticks can be dropped a line @@ -1154,7 +1083,7 @@ axes.tickText = function(ax, x, hover) { tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l; if(arrayMode && Array.isArray(ax.ticktext)) { - var rng = ax.range.map(ax.r2l), + var rng = Lib.simpleMap(ax.range, ax.r2l), minDiff = Math.abs(rng[1] - rng[0]) / 10000; for(i = 0; i < ax.ticktext.length; i++) { if(Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break; @@ -1208,76 +1137,59 @@ function tickTextObj(ax, x, text) { } function formatDate(ax, out, hover, extraPrecision) { - var x = out.x, - tr = ax._tickround, - d = new Date(x), - // headPart completes the full date info, to be included - // with only the first tick or if any info before what's - // shown has changed - headPart, - tt; - if(hover && ax.hoverformat) { - tt = modDateFormat(ax.hoverformat, x); - } - else if(ax.tickformat) { - tt = modDateFormat(ax.tickformat, x); - // TODO: potentially hunt for ways to automatically add more - // precision to the hover text? + var tr = ax._tickround, + fmt = (hover && ax.hoverformat) || ax.tickformat; + + if(extraPrecision) { + // second or sub-second precision: extra always shows max digits. + // for other fields, extra precision just adds one field. + if(isNumeric(tr)) tr = 4; + else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 4}[tr]; } - else { - if(extraPrecision) { - if(isNumeric(tr)) tr += 2; - else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 2}[tr]; - } - if(tr === 'y') tt = yearFormat(d); - else if(tr === 'm') tt = monthFormat(d); - else { - if(tr === 'd') { - headPart = yearFormat(d); - tt = dayFormat(d); - } - else { - headPart = yearMonthDayFormat(d); - - tt = minuteFormat(d); - if(tr !== 'M') { - tt += secondFormat(d); - if(tr !== 'S') { - tt += numFormat(d3.round(mod(x / 1000, 1), 4), ax, 'none', hover) - .substr(1); - } - } - } - } + var dateStr = Lib.formatDate(out.x, fmt, tr, ax.calendar), + headStr; + + var splitIndex = dateStr.indexOf('\n'); + if(splitIndex !== -1) { + headStr = dateStr.substr(splitIndex + 1); + dateStr = dateStr.substr(0, splitIndex); } - if(hover || ax.tickmode === 'array') { - // we get extra precision in array mode or hover, - // but it may be useless, strip it off - if(tt === '00:00:00' || tt === '00:00') { - tt = headPart; - headPart = ''; + + if(extraPrecision) { + // if extraPrecision led to trailing zeros, strip them off + // actually, this can lead to removing even more zeros than + // in the original rounding, but that's fine because in these + // contexts uniformity is not so important (if there's even + // anything to be uniform with!) + + // can we remove the whole time part? + if(dateStr === '00:00:00' || dateStr === '00:00') { + dateStr = headStr; + headStr = ''; } - else if(tt.length === 8) { + else if(dateStr.length === 8) { // strip off seconds if they're zero (zero fractional seconds // are already omitted) - tt = tt.replace(/:00$/, ''); + // but we never remove minutes and leave just hours + dateStr = dateStr.replace(/:00$/, ''); } } - if(headPart) { + if(headStr) { if(hover) { // hover puts it all on one line, so headPart works best up front // except for year headPart: turn this into "Jan 1, 2000" etc. - if(tr === 'd') tt += ', ' + headPart; - else tt = headPart + (tt ? ', ' + tt : ''); + if(tr === 'd') dateStr += ', ' + headStr; + else dateStr = headStr + (dateStr ? ', ' + dateStr : ''); } - else if(!ax._inCalcTicks || (headPart !== ax._prevDateHead)) { - tt += '
' + headPart; - ax._prevDateHead = headPart; + else if(!ax._inCalcTicks || (headStr !== ax._prevDateHead)) { + dateStr += '
' + headStr; + ax._prevDateHead = headStr; } } - out.text = tt; + + out.text = dateStr; } function formatLog(ax, out, hover, extraPrecision, hideexp) { @@ -1288,7 +1200,7 @@ function formatLog(ax, out, hover, extraPrecision, hideexp) { if(ax.tickformat || (typeof dtick === 'string' && dtick.charAt(0) === 'L')) { out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision); } - else if(isNumeric(dtick) || ((dtick.charAt(0) === 'D') && (mod(x + 0.01, 1) < 0.1))) { + else if(isNumeric(dtick) || ((dtick.charAt(0) === 'D') && (Lib.mod(x + 0.01, 1) < 0.1))) { if(['e', 'E', 'power'].indexOf(ax.exponentformat) !== -1) { var p = Math.round(x); if(p === 0) out.text = 1; @@ -1306,7 +1218,7 @@ function formatLog(ax, out, hover, extraPrecision, hideexp) { } } else if(dtick.charAt(0) === 'D') { - out.text = String(Math.round(Math.pow(10, mod(x, 1)))); + out.text = String(Math.round(Math.pow(10, Lib.mod(x, 1)))); out.fontSize *= 0.75; } else throw 'unrecognized dtick ' + String(dtick); @@ -1659,7 +1571,7 @@ axes.doTicks = function(gd, axid, skipTitle) { var axDone = axes.doTicks(gd, ax._id); if(axid === 'redraw') { ax._r = ax.range.slice(); - ax._rl = ax._r.map(ax.r2l); + ax._rl = Lib.simpleMap(ax._r, ax.r2l); } return axDone; }; @@ -2066,7 +1978,7 @@ axes.doTicks = function(gd, axid, skipTitle) { break; } } - var rng = ax.range.map(ax.r2l), + var rng = Lib.simpleMap(ax.range, ax.r2l), showZl = (rng[0] * rng[1] <= 0) && ax.zeroline && (ax.type === 'linear' || ax.type === '-') && gridvals.length && (hasBarsOrFill || clipEnds({x: 0}) || !ax.showline); @@ -2286,7 +2198,3 @@ function swapAxisAttrs(layout, key, xFullAxes, yFullAxes) { np(layout, yFullAxes[i]._name + '.' + key).set(xVal); } } - -// mod - version of modulus that always restricts to [0,divisor) -// rather than built-in % which gives a negative value for negative v -function mod(v, d) { return ((v % d) + d) % d; } diff --git a/src/plots/cartesian/axis_autotype.js b/src/plots/cartesian/axis_autotype.js index 00139887d28..7dec6b2fd52 100644 --- a/src/plots/cartesian/axis_autotype.js +++ b/src/plots/cartesian/axis_autotype.js @@ -14,8 +14,8 @@ var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); var BADNUM = require('../../constants/numerical').BADNUM; -module.exports = function autoType(array) { - if(moreDates(array)) return 'date'; +module.exports = function autoType(array, calendar) { + if(moreDates(array, calendar)) return 'date'; if(category(array)) return 'category'; if(linearOK(array)) return 'linear'; else return '-'; @@ -38,7 +38,7 @@ function linearOK(array) { // 2- or 4-digit integers can be both, so require twice as many // dates as non-dates, to exclude cases with mostly 2 & 4 digit // numbers and a few dates -function moreDates(a) { +function moreDates(a, calendar) { var dcnt = 0, ncnt = 0, // test at most 1000 points, evenly spaced @@ -47,7 +47,7 @@ function moreDates(a) { for(var i = 0; i < a.length; i += inc) { ai = a[Math.round(i)]; - if(Lib.isDateTime(ai)) dcnt += 1; + if(Lib.isDateTime(ai, calendar)) dcnt += 1; if(isNumeric(ai)) ncnt += 1; } diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 0bfdbde1ff2..a0ab9aed0b8 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -74,6 +74,11 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, } } + if(axType === 'date') { + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults'); + handleCalendarDefaults(containerIn, containerOut, 'calendar', options.calendar); + } + setConvert(containerOut); var dfltColor = coerce('color'); @@ -166,6 +171,9 @@ function setAutoType(ax, data) { return; } + var calAttr = axLetter + 'calendar', + calendar = d0[calAttr]; + // check all boxes on this x axis to see // if they're dates, numbers, or categories if(isBoxWithoutPositionCoords(d0, axLetter)) { @@ -181,12 +189,14 @@ function setAutoType(ax, data) { if(trace[posLetter] !== undefined) boxPositions.push(trace[posLetter][0]); else if(trace.name !== undefined) boxPositions.push(trace.name); else boxPositions.push('text'); + + if(trace[calAttr] !== calendar) calendar = undefined; } - ax.type = autoType(boxPositions); + ax.type = autoType(boxPositions, calendar); } else { - ax.type = autoType(d0[axLetter] || [d0[axLetter + '0']]); + ax.type = autoType(d0[axLetter] || [d0[axLetter + '0']], calendar); } } diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index 765241182a6..dc7d444c477 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -66,8 +66,7 @@ module.exports = { // delay before a redraw (relayout) after smooth panning and zooming REDRAWDELAY: 50, - // last resort axis ranges for x, y, and date axes if we have no data + // last resort axis ranges for x and y axes if we have no data DFLTRANGEX: [-1, 6], - DFLTRANGEY: [-1, 4], - DFLTRANGEDATE: ['2000-01-01', '2001-01-01'], + DFLTRANGEY: [-1, 4] }; diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 2927becef5a..43c9e7a1610 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -424,7 +424,7 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { function zoomWheelOneAxis(ax, centerFraction, zoom) { if(ax.fixedrange) return; - var axRange = ax.range.map(ax.r2l), + var axRange = Lib.simpleMap(ax.range, ax.r2l), v0 = axRange[0] + (axRange[1] - axRange[0]) * centerFraction; function doZoom(v) { return ax.l2r(v0 + (v - v0) * zoom); } ax.range = axRange.map(doZoom); diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 10408f47ac7..57c65b99b13 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -666,6 +666,10 @@ function cleanPoint(d, hovermode) { if(d.xLabelVal === 0) d.xLabel = '0'; else d.xLabel = '-' + xLabelObj.text; } + // TODO: should we do something special if the axis calendar and + // the data calendar are different? Somehow display both dates with + // their system names? Right now it will just display in the axis calendar + // but users could add the other one as text. else d.xLabel = xLabelObj.text; d.xVal = d.xa.c2d(d.xLabelVal); } @@ -678,6 +682,7 @@ function cleanPoint(d, hovermode) { if(d.yLabelVal === 0) d.yLabel = '0'; else d.yLabel = '-' + yLabelObj.text; } + // TODO: see above TODO else d.yLabel = yLabelObj.text; d.yVal = d.ya.c2d(d.yLabelVal); } diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index a3078d94897..625cb9e171f 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -126,7 +126,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { showGrid: !noGrids[axName], name: axName, data: fullData, - bgColor: bgColor + bgColor: bgColor, + calendar: layoutOut.calendar }, positioningOptions = { letter: axLetter, @@ -140,7 +141,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { return Lib.coerce(axLayoutIn, axLayoutOut, layoutAttributes, attr, dflt); } - handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions); + handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut); handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, positioningOptions); layoutOut[axName] = axLayoutOut; @@ -166,7 +167,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { rangeSliderDefaults(layoutIn, layoutOut, axName, counterAxes); if(axLetter === 'x' && axLayoutOut.type === 'date') { - rangeSelectorDefaults(axLayoutIn, axLayoutOut, layoutOut, counterAxes); + rangeSelectorDefaults(axLayoutIn, axLayoutOut, layoutOut, counterAxes, + axLayoutOut.calendar); } }); }; diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 377c638ecf5..7202411e980 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -13,6 +13,10 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); +var cleanNumber = Lib.cleanNumber; +var ms2DateTime = Lib.ms2DateTime; +var dateTime2ms = Lib.dateTime2ms; + var numConstants = require('../../constants/numerical'); var FP_SAFE = numConstants.FP_SAFE; var BADNUM = numConstants.BADNUM; @@ -20,6 +24,16 @@ var BADNUM = numConstants.BADNUM; var constants = require('./constants'); var axisIds = require('./axis_ids'); +function fromLog(v) { + return Math.pow(10, v); +} + +function num(v) { + if(!isNumeric(v)) return BADNUM; + v = Number(v); + if(v < -FP_SAFE || v > FP_SAFE) return BADNUM; + return isNumeric(v) ? Number(v) : BADNUM; +} /** * Define the conversion functions for an axis data is used in 5 ways: @@ -41,8 +55,9 @@ var axisIds = require('./axis_ids'); * shapes will work the same way as ranges, tick0, and annotations * so they can use this conversion too. * - * Creates/updates these conversion functions, as well as cleaner functions: - * ax.d2d and ax.clean2r + * Creates/updates these conversion functions, and a few more utilities + * like cleanRange, and makeCalcdata + * * also clears the autorange bounds ._min and ._max * and the autotick constraints ._minDtick, ._forceTick0 */ @@ -67,24 +82,170 @@ module.exports = function setConvert(ax) { else return BADNUM; } - function fromLog(v) { - return Math.pow(10, v); + /* + * wrapped dateTime2ms that: + * - accepts ms numbers for backward compatibility + * - inserts a dummy arg so calendar is the 3rd arg (see notes below). + * - defaults to ax.calendar + */ + function dt2ms(v, _, calendar) { + // NOTE: Changed this behavior: previously we took any numeric value + // to be a ms, even if it was a string that could be a bare year. + // Now we convert it as a date if at all possible, and only try + // as (local) ms if that fails. + var ms = dateTime2ms(v, calendar || ax.calendar); + if(ms === BADNUM) { + if(isNumeric(v)) ms = dateTime2ms(new Date(+v)); + else return BADNUM; + } + return ms; + } + + // wrapped ms2DateTime to insert default ax.calendar + function ms2dt(v, r, calendar) { + return ms2DateTime(v, r, calendar || ax.calendar); + } + + function getCategoryName(v) { + return ax._categories[Math.round(v)]; + } + + /* + * setCategoryIndex: return the index of category v, + * inserting it in the list if it's not already there + * + * this will enter the categories in the order it + * encounters them, ie all the categories from the + * first data set, then all the ones from the second + * that aren't in the first etc. + * + * it is assumed that this function is being invoked in the + * already sorted category order; otherwise there would be + * a disconnect between the array and the index returned + */ + function setCategoryIndex(v) { + if(v !== null && v !== undefined) { + var c = ax._categories.indexOf(v); + if(c === -1) { + ax._categories.push(v); + return ax._categories.length - 1; + } + return c; + } + return BADNUM; + } + + function getCategoryIndex(v) { + // d2l/d2c variant that that won't add categories but will also + // allow numbers to be mapped to the linearized axis positions + var index = ax._categories.indexOf(v); + if(index !== -1) return index; + if(typeof v === 'number') return v; } - function num(v) { + function l2p(v) { if(!isNumeric(v)) return BADNUM; - v = Number(v); - if(v < -FP_SAFE || v > FP_SAFE) return BADNUM; - return isNumeric(v) ? Number(v) : BADNUM; + + // include 2 fractional digits on pixel, for PDF zooming etc + return d3.round(ax._b + ax._m * v, 2); } + function p2l(px) { return (px - ax._b) / ax._m; } + + // conversions among c/l/p are fairly simple - do them together for all axis types ax.c2l = (ax.type === 'log') ? toLog : num; ax.l2c = (ax.type === 'log') ? fromLog : num; - ax.l2d = function(v) { return ax.c2d(ax.l2c(v)); }; - ax.p2d = function(v) { return ax.l2d(ax.p2l(v)); }; + + ax.l2p = l2p; + ax.p2l = p2l; + + ax.c2p = (ax.type === 'log') ? function(v, clip) { return l2p(toLog(v, clip)); } : l2p; + ax.p2c = (ax.type === 'log') ? function(px) { return fromLog(p2l(px)); } : p2l; + + /* + * now type-specific conversions for **ALL** other combinations + * they're all written out, instead of being combinations of each other, for + * both clarity and speed. + */ + if(['linear', '-'].indexOf(ax.type) !== -1) { + // all are data vals, but d and r need cleaning + ax.d2r = ax.r2d = ax.d2c = ax.r2c = ax.d2l = ax.r2l = cleanNumber; + ax.c2d = ax.c2r = ax.l2d = ax.l2r = num; + + ax.d2p = ax.r2p = function(v) { return l2p(cleanNumber(v)); }; + ax.p2d = ax.p2r = p2l; + } + else if(ax.type === 'log') { + // d and c are data vals, r and l are logged (but d and r need cleaning) + ax.d2r = ax.d2l = function(v, clip) { return toLog(cleanNumber(v), clip); }; + ax.r2d = ax.r2c = function(v) { return fromLog(cleanNumber(v)); }; + + ax.d2c = ax.r2l = cleanNumber; + ax.c2d = ax.l2r = num; + + ax.c2r = toLog; + ax.l2d = fromLog; + + ax.d2p = function(v, clip) { return l2p(ax.d2r(v, clip)); }; + ax.p2d = function(px) { return fromLog(p2l(px)); }; + + ax.r2p = function(v) { return l2p(cleanNumber(v)); }; + ax.p2r = p2l; + } + else if(ax.type === 'date') { + // r and d are date strings, l and c are ms + + /* + * Any of these functions with r and d on either side, calendar is the + * **3rd** argument. log has reserved the second argument. + * + * Unless you need the special behavior of the second arg (ms2DateTime + * uses this to limit precision, toLog uses true to clip negatives + * to offscreen low rather than undefined), it's safe to pass 0. + */ + ax.d2r = ax.r2d = Lib.identity; + + ax.d2c = ax.r2c = ax.d2l = ax.r2l = dt2ms; + ax.c2d = ax.c2r = ax.l2d = ax.l2r = ms2dt; + + ax.d2p = ax.r2p = function(v, _, calendar) { return l2p(dt2ms(v, 0, calendar)); }; + ax.p2d = ax.p2r = function(px, r, calendar) { return ms2dt(p2l(px), r, calendar); }; + } + else if(ax.type === 'category') { + // d is categories; r, c, and l are indices + // TODO: should r accept category names too? + // ie r2c and r2l would be getCategoryIndex (and r2p would change) + + ax.d2r = ax.d2c = ax.d2l = setCategoryIndex; + ax.r2d = ax.c2d = ax.l2d = getCategoryName; + + // special d2l variant that won't add categories + ax.d2l_noadd = getCategoryIndex; + + ax.r2l = ax.l2r = ax.r2c = ax.c2r = num; + + ax.d2p = function(v) { return l2p(getCategoryIndex(v)); }; + ax.p2d = function(px) { return getCategoryName(p2l(px)); }; + ax.r2p = l2p; + ax.p2r = p2l; + } + + // find the range value at the specified (linear) fraction of the axis + ax.fraction2r = function(v) { + var rl0 = ax.r2l(ax.range[0]), + rl1 = ax.r2l(ax.range[1]); + return ax.l2r(rl0 + v * (rl1 - rl0)); + }; + + // find the fraction of the range at the specified range value + ax.r2fraction = function(v) { + var rl0 = ax.r2l(ax.range[0]), + rl1 = ax.r2l(ax.range[1]); + return (ax.r2l(v) - rl0) / (rl1 - rl0); + }; /* - * fn to make sure range is a couplet of valid & distinct values + * cleanRange: make sure range is a couplet of valid & distinct values * keep numbers away from the limits of floating point numbers, * and dates away from the ends of our date system (+/- 9999 years) * @@ -97,7 +258,7 @@ module.exports = function setConvert(ax) { axLetter = (ax._id || 'x').charAt(0), i, dflt; - if(ax.type === 'date') dflt = constants.DFLTRANGEDATE; + if(ax.type === 'date') dflt = Lib.dfltRange(ax.calendar); else if(axLetter === 'y') dflt = constants.DFLTRANGEY; else dflt = constants.DFLTRANGEX; @@ -112,20 +273,17 @@ module.exports = function setConvert(ax) { if(ax.type === 'date') { // check if milliseconds or js date objects are provided for range // and convert to date strings - range[0] = Lib.cleanDate(range[0]); - range[1] = Lib.cleanDate(range[1]); + range[0] = Lib.cleanDate(range[0], BADNUM, ax.calendar); + range[1] = Lib.cleanDate(range[1], BADNUM, ax.calendar); } for(i = 0; i < 2; i++) { if(ax.type === 'date') { - if(!Lib.isDateTime(range[i])) { + if(!Lib.isDateTime(range[i], ax.calendar)) { ax[rangeAttr] = dflt; break; } - if(range[i] < Lib.MIN_MS) range[i] = Lib.MIN_MS; - if(range[i] > Lib.MAX_MS) range[i] = Lib.MAX_MS; - if(ax.r2l(range[0]) === ax.r2l(range[1])) { // split by +/- 1 second var linCenter = Lib.constrain(ax.r2l(range[0]), @@ -159,20 +317,6 @@ module.exports = function setConvert(ax) { } }; - // find the range value at the specified (linear) fraction of the axis - ax.fraction2r = function(v) { - var rl0 = ax.r2l(ax.range[0]), - rl1 = ax.r2l(ax.range[1]); - return ax.l2r(rl0 + v * (rl1 - rl0)); - }; - - // find the fraction of the range at the specified range value - ax.r2fraction = function(v) { - var rl0 = ax.r2l(ax.range[0]), - rl1 = ax.r2l(ax.range[1]); - return (ax.r2l(v) - rl0) / (rl1 - rl0); - }; - // set scaling to pixels ax.setScale = function(usePrivateRange) { var gs = ax._gd._fullLayout._size, @@ -192,11 +336,12 @@ module.exports = function setConvert(ax) { // issue if we transform the drawn layer *and* use the new axis range to // draw the data. This allows us to construct setConvert using the pre- // interaction values of the range: - var rangeAttr = (usePrivateRange && ax._r) ? '_r' : 'range'; + var rangeAttr = (usePrivateRange && ax._r) ? '_r' : 'range', + calendar = ax.calendar; ax.cleanRange(rangeAttr); - var rl0 = ax.r2l(ax[rangeAttr][0]), - rl1 = ax.r2l(ax[rangeAttr][1]); + var rl0 = ax.r2l(ax[rangeAttr][0], calendar), + rl1 = ax.r2l(ax[rangeAttr][1], calendar); if(axLetter === 'y') { ax._offset = gs.t + (1 - ax.domain[1]) * gs.h; @@ -220,114 +365,6 @@ module.exports = function setConvert(ax) { } }; - ax.l2p = function(v) { - if(!isNumeric(v)) return BADNUM; - - // include 2 fractional digits on pixel, for PDF zooming etc - return d3.round(ax._b + ax._m * v, 2); - }; - - ax.p2l = function(px) { return (px - ax._b) / ax._m; }; - - ax.c2p = function(v, clip) { return ax.l2p(ax.c2l(v, clip)); }; - ax.p2c = function(px) { return ax.l2c(ax.p2l(px)); }; - - // clip doesn't do anything here yet, but in v2.0 when log axes get - // refactored it will... so including it now so we don't forget. - ax.r2p = function(v, clip) { return ax.l2p(ax.r2l(v, clip)); }; - ax.p2r = function(px) { return ax.l2r(ax.p2l(px)); }; - - ax.r2c = function(v) { return ax.l2c(ax.r2l(v)); }; - ax.c2r = function(v) { return ax.l2r(ax.c2l(v)); }; - - if(['linear', 'log', '-'].indexOf(ax.type) !== -1) { - ax.c2d = num; - ax.d2c = Lib.cleanNumber; - if(ax.type === 'log') { - ax.d2l = function(v, clip) { - return ax.c2l(ax.d2c(v), clip); - }; - ax.d2r = ax.d2l; - ax.r2d = ax.l2d; - } - else { - ax.d2l = Lib.cleanNumber; - ax.d2r = Lib.cleanNumber; - ax.r2d = num; - } - ax.r2l = num; - ax.l2r = num; - } - else if(ax.type === 'date') { - ax.c2d = Lib.ms2DateTime; - - ax.d2c = function(v) { - // NOTE: Changed this behavior: previously we took any numeric value - // to be a ms, even if it was a string that could be a bare year. - // Now we convert it as a date if at all possible, and only try - // as (local) ms if that fails. - var ms = Lib.dateTime2ms(v); - if(ms === BADNUM) { - if(isNumeric(v)) ms = Lib.dateTime2ms(new Date(v)); - else return BADNUM; - } - return Lib.constrain(ms, Lib.MIN_MS, Lib.MAX_MS); - }; - - ax.d2l = ax.d2c; - ax.r2l = ax.d2c; - ax.l2r = ax.c2d; - ax.d2r = Lib.identity; - ax.r2d = Lib.identity; - ax.cleanr = function(v) { - /* - * If v is already a date string this is a noop, but running it - * through d2c and back validates the value: - * normalizes Date objects, milliseconds, and out-of-bounds dates - * so we always end up with either a clean date string or BADNUM - */ - return ax.c2d(ax.d2c(v)); - }; - } - else if(ax.type === 'category') { - - ax.c2d = function(v) { - return ax._categories[Math.round(v)]; - }; - - ax.d2c = function(v) { - // create the category list - // this will enter the categories in the order it - // encounters them, ie all the categories from the - // first data set, then all the ones from the second - // that aren't in the first etc. - // it is assumed that this function is being invoked in the - // already sorted category order; otherwise there would be - // a disconnect between the array and the index returned - - if(v !== null && v !== undefined && ax._categories.indexOf(v) === -1) { - ax._categories.push(v); - } - - var c = ax._categories.indexOf(v); - return c === -1 ? BADNUM : c; - }; - - ax.d2l_noadd = function(v) { - // d2c variant that that won't add categories but will also - // allow numbers to be mapped to the linearized axis positions - var index = ax._categories.indexOf(v); - if(index !== -1) return index; - if(typeof v === 'number') return v; - }; - - ax.d2l = ax.d2c; - ax.r2l = num; - ax.l2r = num; - ax.d2r = ax.d2c; - ax.r2d = ax.c2d; - } - // makeCalcdata: takes an x or y array and converts it // to a position on the axis object "ax" // inputs: @@ -340,15 +377,19 @@ module.exports = function setConvert(ax) { ax.makeCalcdata = function(trace, axLetter) { var arrayIn, arrayOut, i; + var cal = ax.type === 'date' && trace[axLetter + 'calendar']; + if(axLetter in trace) { arrayIn = trace[axLetter]; arrayOut = new Array(arrayIn.length); - for(i = 0; i < arrayIn.length; i++) arrayOut[i] = ax.d2c(arrayIn[i]); + for(i = 0; i < arrayIn.length; i++) { + arrayOut[i] = ax.d2c(arrayIn[i], 0, cal); + } } else { var v0 = ((axLetter + '0') in trace) ? - ax.d2c(trace[axLetter + '0']) : 0, + ax.d2c(trace[axLetter + '0'], 0, cal) : 0, dv = (trace['d' + axLetter]) ? Number(trace['d' + axLetter]) : 1; diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index f441cb5e055..0c8790eb346 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -61,7 +61,7 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe // tick0 can have different valType for different axis types, so // validate that now. Also for dates, change milliseconds to date strings - var tick0Dflt = (axType === 'date') ? '2000-01-01' : 0; + var tick0Dflt = (axType === 'date') ? Lib.dateTick0(containerOut.calendar) : 0; var tick0 = coerce('tick0', tick0Dflt); if(axType === 'date') { containerOut.tick0 = Lib.cleanDate(tick0, tick0Dflt); diff --git a/src/plots/gl3d/layout/axis_defaults.js b/src/plots/gl3d/layout/axis_defaults.js index df6857ec47a..981c7b042a3 100644 --- a/src/plots/gl3d/layout/axis_defaults.js +++ b/src/plots/gl3d/layout/axis_defaults.js @@ -46,7 +46,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) { letter: axName[0], data: options.data, showGrid: true, - bgColor: options.bgColor + bgColor: options.bgColor, + calendar: options.calendar }); coerce('gridcolor', colorMix(containerOut.color, options.bgColor, gridLightness).toRgbString()); diff --git a/src/plots/gl3d/layout/defaults.js b/src/plots/gl3d/layout/defaults.js index 291d8dcd572..a8aa30dd0de 100644 --- a/src/plots/gl3d/layout/defaults.js +++ b/src/plots/gl3d/layout/defaults.js @@ -41,7 +41,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { font: layoutOut.font, fullData: fullData, getDfltFromLayout: getDfltFromLayout, - paper_bgcolor: layoutOut.paper_bgcolor + paper_bgcolor: layoutOut.paper_bgcolor, + calendar: layoutOut.calendar }); }; @@ -97,7 +98,8 @@ function handleGl3dDefaults(sceneLayoutIn, sceneLayoutOut, coerce, opts) { font: opts.font, scene: opts.id, data: opts.fullData, - bgColor: bgColorCombined + bgColor: bgColorCombined, + calendar: opts.calendar }); coerce('dragmode', opts.getDfltFromLayout('dragmode')); diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 28750ec3208..02188822a9c 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -292,12 +292,12 @@ proto.recoverContext = function() { var axisProperties = [ 'xaxis', 'yaxis', 'zaxis' ]; -function coordinateBound(axis, coord, d, bounds) { +function coordinateBound(axis, coord, d, bounds, calendar) { var x; for(var i = 0; i < coord.length; ++i) { if(Array.isArray(coord[i])) { for(var j = 0; j < coord[i].length; ++j) { - x = axis.d2l(coord[i][j]); + x = axis.d2l(coord[i][j], 0, calendar); if(!isNaN(x) && isFinite(x)) { bounds[0][d] = Math.min(bounds[0][d], x); bounds[1][d] = Math.max(bounds[1][d], x); @@ -305,7 +305,7 @@ function coordinateBound(axis, coord, d, bounds) { } } else { - x = axis.d2l(coord[i]); + x = axis.d2l(coord[i], 0, calendar); if(!isNaN(x) && isFinite(x)) { bounds[0][d] = Math.min(bounds[0][d], x); bounds[1][d] = Math.max(bounds[1][d], x); @@ -316,9 +316,9 @@ function coordinateBound(axis, coord, d, bounds) { function computeTraceBounds(scene, trace, bounds) { var sceneLayout = scene.fullSceneLayout; - coordinateBound(sceneLayout.xaxis, trace.x, 0, bounds); - coordinateBound(sceneLayout.yaxis, trace.y, 1, bounds); - coordinateBound(sceneLayout.zaxis, trace.z, 2, bounds); + coordinateBound(sceneLayout.xaxis, trace.x, 0, bounds, trace.xcalendar); + coordinateBound(sceneLayout.yaxis, trace.y, 1, bounds, trace.ycalendar); + coordinateBound(sceneLayout.zaxis, trace.z, 2, bounds, trace.zcalendar); } proto.plot = function(sceneData, fullLayout, layout) { diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index f98ffc9606f..235ff39250c 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -170,7 +170,6 @@ module.exports = { role: 'info', description: 'Determines whether or not a legend is drawn.' }, - dragmode: { valType: 'enumerated', role: 'info', diff --git a/src/plots/plots.js b/src/plots/plots.js index 06f2e91aff5..e670463ce37 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -941,6 +941,9 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { coerce('separators'); coerce('hidesources'); coerce('smith'); + + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults'); + handleCalendarDefaults(layoutIn, layoutOut, 'calendar'); }; plots.plotAutoSize = function plotAutoSize(gd, layout, fullLayout) { diff --git a/src/traces/bar/calc.js b/src/traces/bar/calc.js index 22c2a1a9871..52f45c4b69a 100644 --- a/src/traces/bar/calc.js +++ b/src/traces/bar/calc.js @@ -25,17 +25,24 @@ module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'), ya = Axes.getFromId(gd, trace.yaxis || 'y'), orientation = trace.orientation || ((trace.x && !trace.y) ? 'h' : 'v'), - sa, pos, size, i; + sa, pos, size, i, scalendar; if(orientation === 'h') { sa = xa; size = xa.makeCalcdata(trace, 'x'); pos = ya.makeCalcdata(trace, 'y'); + + // not sure if it really makes sense to have dates for bar size data... + // ideally if we want to make gantt charts or something we'd treat + // the actual size (trace.x or y) as time delta but base as absolute + // time. But included here for completeness. + scalendar = trace.xcalendar; } else { sa = ya; size = ya.makeCalcdata(trace, 'y'); pos = xa.makeCalcdata(trace, 'x'); + scalendar = trace.ycalendar; } // create the "calculated data" to plot @@ -60,7 +67,7 @@ module.exports = function calc(gd, trace) { if(Array.isArray(base)) { for(i = 0; i < Math.min(base.length, cd.length); i++) { - b = sa.d2c(base[i]); + b = sa.d2c(base[i], 0, scalendar); cd[i].b = (isNumeric(b)) ? b : 0; } for(; i < cd.length; i++) { @@ -68,7 +75,7 @@ module.exports = function calc(gd, trace) { } } else { - b = sa.d2c(base); + b = sa.d2c(base, 0, scalendar); b = (isNumeric(b)) ? b : 0; for(i = 0; i < cd.length; i++) { cd[i].b = b; diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index e907b40ad06..ada3fe7116b 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -25,7 +25,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var coerceFont = Lib.coerceFont; - var len = handleXYDefaults(traceIn, traceOut, coerce); + var len = handleXYDefaults(traceIn, traceOut, layout, coerce); if(!len) { traceOut.visible = false; return; diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index c01f7914850..41d7170f7bf 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -47,6 +47,8 @@ module.exports = { 'See overview for more info.' ].join(' ') }, + xcalendar: scatterAttrs.xcalendar, + ycalendar: scatterAttrs.ycalendar, whiskerwidth: { valType: 'number', min: 0, diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index cafd8eb16c7..c8bffeeff5e 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -62,7 +62,7 @@ module.exports = function calc(gd, trace) { pos0 = trace.name; } else pos0 = gd.numboxes; - pos0 = posAxis.d2c(pos0); + pos0 = posAxis.d2c(pos0, 0, trace[posLetter + 'calendar']); pos = val.map(function() { return pos0; }); } return pos; diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index 61d9653d8a9..9f33075d88b 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -9,11 +9,12 @@ 'use strict'; var Lib = require('../../lib'); +var Registry = require('../../registry'); var Color = require('../../components/color'); var attributes = require('./attributes'); -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor) { +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } @@ -33,6 +34,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor) { return; } + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); + handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); + coerce('orientation', defaultOrientation); coerce('line.color', (traceIn.marker || {}).color || defaultColor); diff --git a/src/traces/candlestick/defaults.js b/src/traces/candlestick/defaults.js index c6b4538ea53..6668cd15cd0 100644 --- a/src/traces/candlestick/defaults.js +++ b/src/traces/candlestick/defaults.js @@ -15,14 +15,14 @@ var handleDirectionDefaults = require('../ohlc/direction_defaults'); var helpers = require('../ohlc/helpers'); var attributes = require('./attributes'); -module.exports = function supplyDefaults(traceIn, traceOut) { +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { helpers.pushDummyTransformOpts(traceIn, traceOut); function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var len = handleOHLC(traceIn, traceOut, coerce); + var len = handleOHLC(traceIn, traceOut, coerce, layout); if(len === 0) { traceOut.visible = false; return; diff --git a/src/traces/candlestick/transform.js b/src/traces/candlestick/transform.js index 6d32870e3ce..f89dd7a46a4 100644 --- a/src/traces/candlestick/transform.js +++ b/src/traces/candlestick/transform.js @@ -70,6 +70,7 @@ function makeTrace(traceIn, state, direction) { // to make autotype catch date axes soon!! x: traceIn.x || [0], + xcalendar: traceIn.xcalendar, // concat low and high to get correct autorange y: [].concat(traceIn.low).concat(traceIn.high), diff --git a/src/traces/contour/defaults.js b/src/traces/contour/defaults.js index e88b43cbe7f..0f4107e5796 100644 --- a/src/traces/contour/defaults.js +++ b/src/traces/contour/defaults.js @@ -22,7 +22,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var len = handleXYZDefaults(traceIn, traceOut, coerce); + var len = handleXYZDefaults(traceIn, traceOut, coerce, layout); if(!len) { traceOut.visible = false; return; diff --git a/src/traces/heatmap/attributes.js b/src/traces/heatmap/attributes.js index da86b34d004..2ec094818f9 100644 --- a/src/traces/heatmap/attributes.js +++ b/src/traces/heatmap/attributes.js @@ -25,6 +25,7 @@ module.exports = extendFlat({}, { y: scatterAttrs.y, y0: scatterAttrs.y0, dy: scatterAttrs.dy, + text: { valType: 'data_array', description: 'Sets the text elements associated with each z value.' diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index 1428406404b..ae307000e5b 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -127,9 +127,13 @@ module.exports = function calc(gd, trace) { colorscaleCalc(trace, z, '', 'z'); if(isContour && trace.contours && trace.contours.coloring === 'heatmap') { - var hmType = trace.type === 'contour' ? 'heatmap' : 'histogram2d'; - cd0.xfill = makeBoundArray(hmType, xIn, x0, dx, xlen, xa); - cd0.yfill = makeBoundArray(hmType, yIn, y0, dy, z.length, ya); + var dummyTrace = { + type: trace.type === 'contour' ? 'heatmap' : 'histogram2d', + xcalendar: trace.xcalendar, + ycalendar: trace.ycalendar + }; + cd0.xfill = makeBoundArray(dummyTrace, xIn, x0, dx, xlen, xa); + cd0.yfill = makeBoundArray(dummyTrace, yIn, y0, dy, z.length, ya); } return [cd0]; diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index 1f18b6a5474..1798b4e93b8 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -18,7 +18,9 @@ module.exports = function convertColumnXYZ(trace, xa, ya) { zCol = trace.z, textCol = trace.text, colLen = Math.min(xCol.length, yCol.length, zCol.length), - hasColumnText = (textCol !== undefined && !Array.isArray(textCol[0])); + hasColumnText = (textCol !== undefined && !Array.isArray(textCol[0])), + xcalendar = trace.xcalendar, + ycalendar = trace.ycalendar; var i; @@ -26,8 +28,8 @@ module.exports = function convertColumnXYZ(trace, xa, ya) { if(colLen < yCol.length) yCol = yCol.slice(0, colLen); for(i = 0; i < colLen; i++) { - xCol[i] = xa.d2c(xCol[i]); - yCol[i] = ya.d2c(yCol[i]); + xCol[i] = xa.d2c(xCol[i], 0, xcalendar); + yCol[i] = ya.d2c(yCol[i], 0, ycalendar); } var xColdv = Lib.distinctVals(xCol), diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js index f41624cf57e..cc4c33aae0e 100644 --- a/src/traces/heatmap/defaults.js +++ b/src/traces/heatmap/defaults.js @@ -22,7 +22,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var len = handleXYZDefaults(traceIn, traceOut, coerce); + var len = handleXYZDefaults(traceIn, traceOut, coerce, layout); if(!len) { traceOut.visible = false; return; diff --git a/src/traces/heatmap/make_bound_array.js b/src/traces/heatmap/make_bound_array.js index 42f09489a24..6640f03a4bd 100644 --- a/src/traces/heatmap/make_bound_array.js +++ b/src/traces/heatmap/make_bound_array.js @@ -64,10 +64,12 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, else { dv = dvIn || 1; - if(isHist || ax.type === 'category') v0 = ax.r2c(v0In) || 0; + var calendar = trace[ax._id.charAt(0) + 'calendar']; + + if(isHist || ax.type === 'category') v0 = ax.r2c(v0In, 0, calendar) || 0; else if(Array.isArray(arrayIn) && arrayIn.length === 1) v0 = arrayIn[0]; else if(v0In === undefined) v0 = 0; - else v0 = ax.d2c(v0In); + else v0 = ax.d2c(v0In, 0, calendar); for(i = (isContour || isGL2D) ? 0 : -0.5; i < numbricks; i++) { arrayOut.push(v0 + dv * i); diff --git a/src/traces/heatmap/xyz_defaults.js b/src/traces/heatmap/xyz_defaults.js index abc9d03f08f..f07805e88f5 100644 --- a/src/traces/heatmap/xyz_defaults.js +++ b/src/traces/heatmap/xyz_defaults.js @@ -11,10 +11,11 @@ var isNumeric = require('fast-isnumeric'); +var Registry = require('../../registry'); var hasColumns = require('./has_columns'); -module.exports = function handleXYZDefaults(traceIn, traceOut, coerce) { +module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout) { var z = coerce('z'); var x, y; @@ -37,6 +38,9 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce) { coerce('transpose'); } + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); + handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); + return traceOut.z.length; }; diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 5248979368a..c4333d979b3 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -32,7 +32,8 @@ module.exports = function calc(gd, trace) { pa = Axes.getFromId(gd, trace.orientation === 'h' ? (trace.yaxis || 'y') : (trace.xaxis || 'x')), maindata = trace.orientation === 'h' ? 'y' : 'x', - counterdata = {x: 'y', y: 'x'}[maindata]; + counterdata = {x: 'y', y: 'x'}[maindata], + calendar = trace[maindata + 'calendar']; cleanBins(trace, pa, maindata); @@ -40,7 +41,7 @@ module.exports = function calc(gd, trace) { var pos0 = pa.makeCalcdata(trace, maindata); // calculate the bins if((trace['autobin' + maindata] !== false) || !(maindata + 'bins' in trace)) { - trace[maindata + 'bins'] = Axes.autoBin(pos0, pa, trace['nbins' + maindata]); + trace[maindata + 'bins'] = Axes.autoBin(pos0, pa, trace['nbins' + maindata], false, calendar); // copy bin info back to the source data. trace._input[maindata + 'bins'] = trace[maindata + 'bins']; @@ -64,6 +65,7 @@ module.exports = function calc(gd, trace) { binfunc = binFunctions.count, normfunc = normFunctions[norm], doavg = false, + pr2c = function(v) { return pa.r2c(v, 0, calendar); }, rawCounterData; if(Array.isArray(trace[counterdata]) && func !== 'count') { @@ -74,13 +76,13 @@ module.exports = function calc(gd, trace) { // create the bins (and any extra arrays needed) // assume more than 5000 bins is an error, so we don't crash the browser - i = pa.r2c(binspec.start); + i = pr2c(binspec.start); // decrease end a little in case of rounding errors - binend = pa.r2c(binspec.end) + (i - Axes.tickIncrement(i, binspec.size)) / 1e6; + binend = pr2c(binspec.end) + (i - Axes.tickIncrement(i, binspec.size, false, calendar)) / 1e6; while(i < binend && pos.length < 5000) { - i2 = Axes.tickIncrement(i, binspec.size); + i2 = Axes.tickIncrement(i, binspec.size, false, calendar); pos.push((i + i2) / 2); size.push(sizeinit); // nonuniform bins (like months) we need to search, @@ -96,8 +98,8 @@ module.exports = function calc(gd, trace) { // we already have this, but uniform with start/end/size they're still strings. if(!nonuniformBins && pa.type === 'date') { bins = { - start: pa.r2c(bins.start), - end: pa.r2c(bins.end), + start: pr2c(bins.start), + end: pr2c(bins.end), size: bins.size }; } diff --git a/src/traces/histogram/clean_bins.js b/src/traces/histogram/clean_bins.js index d6c4d00e5d7..d23e6383a3e 100644 --- a/src/traces/histogram/clean_bins.js +++ b/src/traces/histogram/clean_bins.js @@ -10,7 +10,9 @@ 'use strict'; var isNumeric = require('fast-isnumeric'); var cleanDate = require('../../lib').cleanDate; -var ONEDAY = require('../../constants/numerical').ONEDAY; +var constants = require('../../constants/numerical'); +var ONEDAY = constants.ONEDAY; +var BADNUM = constants.BADNUM; /* * cleanBins: validate attributes autobin[xy] and [xy]bins.(start, end, size) @@ -29,7 +31,7 @@ module.exports = function cleanBins(trace, ax, binDirection) { if(!bins) bins = trace[binAttr] = {}; var cleanBound = (axType === 'date') ? - function(v) { return (v || v === 0) ? cleanDate(v) : null; } : + function(v) { return (v || v === 0) ? cleanDate(v, BADNUM, bins.calendar) : null; } : function(v) { return isNumeric(v) ? Number(v) : null; }; bins.start = cleanBound(bins.start); diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index a415ff00797..c90508f8620 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -9,6 +9,7 @@ 'use strict'; +var Registry = require('../../registry'); var Lib = require('../../lib'); var Color = require('../../components/color'); @@ -36,6 +37,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return; } + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); + handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); + var hasAggregationData = traceOut[orientation === 'h' ? 'x' : 'y']; if(hasAggregationData) coerce('histfunc'); diff --git a/src/traces/histogram2d/calc.js b/src/traces/histogram2d/calc.js index 1f524e34ee0..e9550ec9bef 100644 --- a/src/traces/histogram2d/calc.js +++ b/src/traces/histogram2d/calc.js @@ -23,6 +23,12 @@ module.exports = function calc(gd, trace) { x = trace.x ? xa.makeCalcdata(trace, 'x') : [], ya = Axes.getFromId(gd, trace.yaxis || 'y'), y = trace.y ? ya.makeCalcdata(trace, 'y') : [], + xcalendar = trace.xcalendar, + ycalendar = trace.ycalendar, + xr2c = function(v) { return xa.r2c(v, 0, xcalendar); }, + yr2c = function(v) { return ya.r2c(v, 0, ycalendar); }, + xc2r = function(v) { return xa.c2r(v, 0, xcalendar); }, + yc2r = function(v) { return ya.c2r(v, 0, ycalendar); }, x0, dx, y0, @@ -40,22 +46,26 @@ module.exports = function calc(gd, trace) { // calculate the bins if(trace.autobinx || !('xbins' in trace)) { - trace.xbins = Axes.autoBin(x, xa, trace.nbinsx, '2d'); + trace.xbins = Axes.autoBin(x, xa, trace.nbinsx, '2d', xcalendar); if(trace.type === 'histogram2dcontour') { // the "true" last argument reverses the tick direction (which we can't // just do with a minus sign because of month bins) - trace.xbins.start = xa.c2r(Axes.tickIncrement(xa.r2c(trace.xbins.start), trace.xbins.size, true)); - trace.xbins.end = xa.c2r(Axes.tickIncrement(xa.r2c(trace.xbins.end), trace.xbins.size)); + trace.xbins.start = xc2r(Axes.tickIncrement( + xr2c(trace.xbins.start), trace.xbins.size, true, xcalendar)); + trace.xbins.end = xc2r(Axes.tickIncrement( + xr2c(trace.xbins.end), trace.xbins.size, false, xcalendar)); } // copy bin info back to the source data. trace._input.xbins = trace.xbins; } if(trace.autobiny || !('ybins' in trace)) { - trace.ybins = Axes.autoBin(y, ya, trace.nbinsy, '2d'); + trace.ybins = Axes.autoBin(y, ya, trace.nbinsy, '2d', ycalendar); if(trace.type === 'histogram2dcontour') { - trace.ybins.start = ya.c2r(Axes.tickIncrement(ya.r2c(trace.ybins.start), trace.ybins.size, true)); - trace.ybins.end = ya.c2r(Axes.tickIncrement(ya.r2c(trace.ybins.end), trace.ybins.size)); + trace.ybins.start = yc2r(Axes.tickIncrement( + yr2c(trace.ybins.start), trace.ybins.size, true, ycalendar)); + trace.ybins.end = yc2r(Axes.tickIncrement( + yr2c(trace.ybins.end), trace.ybins.size, false, ycalendar)); } trace._input.ybins = trace.ybins; } @@ -99,11 +109,11 @@ module.exports = function calc(gd, trace) { // decrease end a little in case of rounding errors var binspec = trace.xbins, - binStart = xa.r2c(binspec.start), - binEnd = xa.r2c(binspec.end) + - (binStart - Axes.tickIncrement(binStart, binspec.size)) / 1e6; + binStart = xr2c(binspec.start), + binEnd = xr2c(binspec.end) + + (binStart - Axes.tickIncrement(binStart, binspec.size, false, xcalendar)) / 1e6; - for(i = binStart; i < binEnd; i = Axes.tickIncrement(i, binspec.size)) { + for(i = binStart; i < binEnd; i = Axes.tickIncrement(i, binspec.size, false, xcalendar)) { onecol.push(sizeinit); if(nonuniformBinsX) xbins.push(i); if(doavg) zerocol.push(0); @@ -112,16 +122,16 @@ module.exports = function calc(gd, trace) { var nx = onecol.length; x0 = trace.xbins.start; - var x0c = xa.r2c(x0); + var x0c = xr2c(x0); dx = (i - x0c) / nx; - x0 = xa.c2r(x0c + dx / 2); + x0 = xc2r(x0c + dx / 2); binspec = trace.ybins; - binStart = ya.r2c(binspec.start); - binEnd = ya.r2c(binspec.end) + - (binStart - Axes.tickIncrement(binStart, binspec.size)) / 1e6; + binStart = yr2c(binspec.start); + binEnd = yr2c(binspec.end) + + (binStart - Axes.tickIncrement(binStart, binspec.size, false, ycalendar)) / 1e6; - for(i = binStart; i < binEnd; i = Axes.tickIncrement(i, binspec.size)) { + for(i = binStart; i < binEnd; i = Axes.tickIncrement(i, binspec.size, false, ycalendar)) { z.push(onecol.concat()); if(nonuniformBinsY) ybins.push(i); if(doavg) counts.push(zerocol.concat()); @@ -130,9 +140,9 @@ module.exports = function calc(gd, trace) { var ny = z.length; y0 = trace.ybins.start; - var y0c = ya.r2c(y0); + var y0c = yr2c(y0); dy = (i - y0c) / ny; - y0 = ya.c2r(y0c + dy / 2); + y0 = yc2r(y0c + dy / 2); if(densitynorm) { xinc = onecol.map(function(v, i) { @@ -149,15 +159,15 @@ module.exports = function calc(gd, trace) { // we already have this, but uniform with start/end/size they're still strings. if(!nonuniformBinsX && xa.type === 'date') { xbins = { - start: xa.r2c(xbins.start), - end: xa.r2c(xbins.end), + start: xr2c(xbins.start), + end: xr2c(xbins.end), size: xbins.size }; } if(!nonuniformBinsY && ya.type === 'date') { ybins = { - start: ya.r2c(ybins.start), - end: ya.r2c(ybins.end), + start: yr2c(ybins.start), + end: yr2c(ybins.end), size: ybins.size }; } diff --git a/src/traces/histogram2d/defaults.js b/src/traces/histogram2d/defaults.js index 563c59563d1..8d9d1582fec 100644 --- a/src/traces/histogram2d/defaults.js +++ b/src/traces/histogram2d/defaults.js @@ -16,12 +16,12 @@ var colorscaleDefaults = require('../../components/colorscale/defaults'); var attributes = require('./attributes'); -module.exports = function supplyDefaults(traceIn, traceOut, layout) { +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - handleSampleDefaults(traceIn, traceOut, coerce); + handleSampleDefaults(traceIn, traceOut, coerce, layout); var zsmooth = coerce('zsmooth'); if(zsmooth === false) { diff --git a/src/traces/histogram2d/sample_defaults.js b/src/traces/histogram2d/sample_defaults.js index b6197d490f7..37070082c48 100644 --- a/src/traces/histogram2d/sample_defaults.js +++ b/src/traces/histogram2d/sample_defaults.js @@ -9,10 +9,11 @@ 'use strict'; +var Registry = require('../../registry'); var handleBinDefaults = require('../histogram/bin_defaults'); -module.exports = function handleSampleDefaults(traceIn, traceOut, coerce) { +module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout) { var x = coerce('x'), y = coerce('y'); @@ -24,6 +25,9 @@ module.exports = function handleSampleDefaults(traceIn, traceOut, coerce) { return; } + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); + handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); + // if marker.color is an array, we can use it in aggregation instead of z var hasAggregationData = coerce('z') || coerce('marker.color'); diff --git a/src/traces/histogram2dcontour/defaults.js b/src/traces/histogram2dcontour/defaults.js index 18331ac99ff..be0d9f457f8 100644 --- a/src/traces/histogram2dcontour/defaults.js +++ b/src/traces/histogram2dcontour/defaults.js @@ -21,7 +21,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - handleSampleDefaults(traceIn, traceOut, coerce); + handleSampleDefaults(traceIn, traceOut, coerce, layout); var contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'), contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'), diff --git a/src/traces/mesh3d/convert.js b/src/traces/mesh3d/convert.js index 9652842315a..7435526fc9f 100644 --- a/src/traces/mesh3d/convert.js +++ b/src/traces/mesh3d/convert.js @@ -75,16 +75,16 @@ proto.update = function(data) { this.data = data; // Unpack position data - function toDataCoords(axis, coord, scale) { + function toDataCoords(axis, coord, scale, calendar) { return coord.map(function(x) { - return axis.d2l(x) * scale; + return axis.d2l(x, 0, calendar) * scale; }); } var positions = zip3( - toDataCoords(layout.xaxis, data.x, scene.dataScale[0]), - toDataCoords(layout.yaxis, data.y, scene.dataScale[1]), - toDataCoords(layout.zaxis, data.z, scene.dataScale[2])); + toDataCoords(layout.xaxis, data.x, scene.dataScale[0], data.xcalendar), + toDataCoords(layout.yaxis, data.y, scene.dataScale[1], data.ycalendar), + toDataCoords(layout.zaxis, data.z, scene.dataScale[2], data.zcalendar)); var cells; if(data.i && data.j && data.k) { diff --git a/src/traces/mesh3d/defaults.js b/src/traces/mesh3d/defaults.js index 54feaaad32f..f366a2dab0b 100644 --- a/src/traces/mesh3d/defaults.js +++ b/src/traces/mesh3d/defaults.js @@ -9,6 +9,7 @@ 'use strict'; +var Registry = require('../../registry'); var Lib = require('../../lib'); var colorbarDefaults = require('../../components/colorbar/defaults'); var attributes = require('./attributes'); @@ -48,6 +49,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout }); } + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); + handleCalendarDefaults(traceIn, traceOut, ['x', 'y', 'z'], layout); + // Coerce remaining properties [ 'lighting.ambient', diff --git a/src/traces/ohlc/defaults.js b/src/traces/ohlc/defaults.js index 791535de251..cf1a32ed970 100644 --- a/src/traces/ohlc/defaults.js +++ b/src/traces/ohlc/defaults.js @@ -15,14 +15,14 @@ var handleDirectionDefaults = require('./direction_defaults'); var attributes = require('./attributes'); var helpers = require('./helpers'); -module.exports = function supplyDefaults(traceIn, traceOut) { +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { helpers.pushDummyTransformOpts(traceIn, traceOut); function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var len = handleOHLC(traceIn, traceOut, coerce); + var len = handleOHLC(traceIn, traceOut, coerce, layout); if(len === 0) { traceOut.visible = false; return; diff --git a/src/traces/ohlc/ohlc_defaults.js b/src/traces/ohlc/ohlc_defaults.js index 33e335f4188..0fe5abdfda7 100644 --- a/src/traces/ohlc/ohlc_defaults.js +++ b/src/traces/ohlc/ohlc_defaults.js @@ -9,7 +9,10 @@ 'use strict'; -module.exports = function handleOHLC(traceIn, traceOut, coerce) { +var Registry = require('../../registry'); + + +module.exports = function handleOHLC(traceIn, traceOut, coerce, layout) { var len; var x = coerce('x'), @@ -18,6 +21,9 @@ module.exports = function handleOHLC(traceIn, traceOut, coerce) { low = coerce('low'), close = coerce('close'); + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); + handleCalendarDefaults(traceIn, traceOut, ['x'], layout); + len = Math.min(open.length, high.length, low.length, close.length); if(x) { diff --git a/src/traces/ohlc/transform.js b/src/traces/ohlc/transform.js index 93ea567cea9..c1e90ddd00f 100644 --- a/src/traces/ohlc/transform.js +++ b/src/traces/ohlc/transform.js @@ -73,6 +73,7 @@ function makeTrace(traceIn, state, direction) { // to make autotype catch date axes soon!! x: traceIn.x || [0], + xcalendar: traceIn.xcalendar, // concat low and high to get correct autorange y: [].concat(traceIn.low).concat(traceIn.high), @@ -138,12 +139,13 @@ exports.calcTransform = function calcTransform(gd, trace, opts) { if(trace._fullInput.x) { appendX = function(i) { var xi = trace.x[i], - xcalc = xa.d2c(xi); + xcalendar = trace.xcalendar, + xcalc = xa.d2c(xi, 0, xcalendar); x.push( - xa.c2d(xcalc - tickWidth), + xa.c2d(xcalc - tickWidth, 0, xcalendar), xi, xi, xi, xi, - xa.c2d(xcalc + tickWidth), + xa.c2d(xcalc + tickWidth, 0, xcalendar), null); }; } @@ -236,7 +238,8 @@ function convertTickWidth(gd, xa, trace) { // - handle trace of length 1 separately. if(_trace.x && _trace.x.length > 1) { - var _minDiff = Lib.distinctVals(_trace.x.map(xa.d2c)).minDiff; + var xcalc = Lib.simpleMap(_trace.x, xa.d2c, 0, trace.xcalendar), + _minDiff = Lib.distinctVals(xcalc).minDiff; minDiff = Math.min(minDiff, _minDiff); } } diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js index 115ef4c757e..c5fc7259dc8 100644 --- a/src/traces/scatter/defaults.js +++ b/src/traces/scatter/defaults.js @@ -28,7 +28,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var len = handleXYDefaults(traceIn, traceOut, coerce), + var len = handleXYDefaults(traceIn, traceOut, layout, coerce), // TODO: default mode by orphan points... defaultMode = len < constants.PTS_LINESONLY ? 'lines+markers' : 'lines'; if(!len) { diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index 3cd4663ea40..a026a3dd2fa 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -496,8 +496,8 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition function selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll) { var xa = plotinfo.xaxis, ya = plotinfo.yaxis, - xr = d3.extent(xa.range.map(xa.r2l).map(xa.l2c)), - yr = d3.extent(ya.range.map(ya.r2l).map(ya.l2c)); + xr = d3.extent(Lib.simpleMap(xa.range, xa.r2c)), + yr = d3.extent(Lib.simpleMap(ya.range, ya.r2c)); var trace = cdscatter[0].trace; if(!subTypes.hasMarkers(trace)) return; diff --git a/src/traces/scatter/xy_defaults.js b/src/traces/scatter/xy_defaults.js index d85a0aea2b1..7e6ff7409f7 100644 --- a/src/traces/scatter/xy_defaults.js +++ b/src/traces/scatter/xy_defaults.js @@ -9,12 +9,17 @@ 'use strict'; +var Registry = require('../../registry'); -module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { + +module.exports = function handleXYDefaults(traceIn, traceOut, layout, coerce) { var len, x = coerce('x'), y = coerce('y'); + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); + handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); + if(x) { if(y) { len = Math.min(x.length, y.length); diff --git a/src/traces/scatter3d/attributes.js b/src/traces/scatter3d/attributes.js index 5208068b5de..836c4511cb4 100644 --- a/src/traces/scatter3d/attributes.js +++ b/src/traces/scatter3d/attributes.js @@ -65,6 +65,7 @@ module.exports = { valType: 'data_array', description: 'Sets the z coordinates.' }, + text: extendFlat({}, scatterAttrs.text, { description: [ 'Sets text elements associated with each (x,y,z) triplet.', diff --git a/src/traces/scatter3d/convert.js b/src/traces/scatter3d/convert.js index c58aa8a2674..b16a08d667c 100644 --- a/src/traces/scatter3d/convert.js +++ b/src/traces/scatter3d/convert.js @@ -177,14 +177,17 @@ function convertPlotlyOptions(scene, data) { yc, y = data.y || [], zc, z = data.z || [], len = x.length, + xcalendar = data.xcalendar, + ycalendar = data.ycalendar, + zcalendar = data.zcalendar, text; // Convert points for(i = 0; i < len; i++) { // sanitize numbers and apply transforms based on axes.type - xc = xaxis.d2l(x[i]) * scaleFactor[0]; - yc = yaxis.d2l(y[i]) * scaleFactor[1]; - zc = zaxis.d2l(z[i]) * scaleFactor[2]; + xc = xaxis.d2l(x[i], 0, xcalendar) * scaleFactor[0]; + yc = yaxis.d2l(y[i], 0, ycalendar) * scaleFactor[1]; + zc = zaxis.d2l(z[i], 0, zcalendar) * scaleFactor[2]; points[i] = [xc, yc, zc]; } diff --git a/src/traces/scatter3d/defaults.js b/src/traces/scatter3d/defaults.js index 9dcf04788f8..4bd5185b682 100644 --- a/src/traces/scatter3d/defaults.js +++ b/src/traces/scatter3d/defaults.js @@ -9,6 +9,7 @@ 'use strict'; +var Registry = require('../../registry'); var Lib = require('../../lib'); var subTypes = require('../scatter/subtypes'); @@ -26,7 +27,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var len = handleXYZDefaults(traceIn, traceOut, coerce); + var len = handleXYZDefaults(traceIn, traceOut, coerce, layout); if(!len) { traceOut.visible = false; return; @@ -66,12 +67,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'z'}); }; -function handleXYZDefaults(traceIn, traceOut, coerce) { +function handleXYZDefaults(traceIn, traceOut, coerce, layout) { var len = 0, x = coerce('x'), y = coerce('y'), z = coerce('z'); + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); + handleCalendarDefaults(traceIn, traceOut, ['x', 'y', 'z'], layout); + if(x && y && z) { len = Math.min(x.length, y.length, z.length); if(len < x.length) traceOut.x = x.slice(0, len); diff --git a/src/traces/scattergl/attributes.js b/src/traces/scattergl/attributes.js index 55c8b2681a8..c5bb9c12bc5 100644 --- a/src/traces/scattergl/attributes.js +++ b/src/traces/scattergl/attributes.js @@ -27,6 +27,7 @@ module.exports = { y: scatterAttrs.y, y0: scatterAttrs.y0, dy: scatterAttrs.dy, + text: extendFlat({}, scatterAttrs.text, { description: [ 'Sets text elements associated with each (x,y) pair to appear on hover.', diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index 60725bb1aa8..a5e07c095c5 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -298,8 +298,10 @@ proto.updateFast = function(options) { var xx, yy; + var xcalendar = options.xcalendar; + var fastType = allFastTypesLikely(x); - var isDateTime = !fastType && autoType(x) === 'date'; + var isDateTime = !fastType && autoType(x, xcalendar) === 'date'; // TODO add 'very fast' mode that bypasses this loop // TODO bypass this on modebar +/- zoom @@ -312,7 +314,7 @@ proto.updateFast = function(options) { if(isNumeric(yy)) { if(!fastType) { - xx = Lib.dateTime2ms(xx); + xx = Lib.dateTime2ms(xx, xcalendar); } idToIndex[pId++] = i; @@ -397,12 +399,8 @@ proto.updateFancy = function(options) { ptrX = 0, ptrY = 0; - var getX = (xaxis.type === 'log') ? - function(x) { return xaxis.d2l(x); } : - function(x) { return x; }; - var getY = (yaxis.type === 'log') ? - function(y) { return yaxis.d2l(y); } : - function(y) { return y; }; + var getX = (xaxis.type === 'log') ? xaxis.d2l : function(x) { return x; }; + var getY = (yaxis.type === 'log') ? yaxis.d2l : function(y) { return y; }; var i, j, xx, yy, ex0, ex1, ey0, ey1; diff --git a/src/traces/scattergl/defaults.js b/src/traces/scattergl/defaults.js index b1a8effb015..3d15c0d0a3e 100644 --- a/src/traces/scattergl/defaults.js +++ b/src/traces/scattergl/defaults.js @@ -27,7 +27,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var len = handleXYDefaults(traceIn, traceOut, coerce); + var len = handleXYDefaults(traceIn, traceOut, layout, coerce); if(!len) { traceOut.visible = false; return; diff --git a/src/traces/surface/attributes.js b/src/traces/surface/attributes.js index 7bd693a1894..47a0eaf725d 100644 --- a/src/traces/surface/attributes.js +++ b/src/traces/surface/attributes.js @@ -109,6 +109,7 @@ module.exports = { valType: 'data_array', description: 'Sets the y coordinates.' }, + text: { valType: 'data_array', description: 'Sets the text elements associated with each z value.' diff --git a/src/traces/surface/convert.js b/src/traces/surface/convert.js index 2b3fbbda9ee..a430cd4bef1 100644 --- a/src/traces/surface/convert.js +++ b/src/traces/surface/convert.js @@ -61,9 +61,9 @@ proto.handlePick = function(selection) { var sceneLayout = this.scene.fullSceneLayout; selection.dataCoordinate = [ - sceneLayout.xaxis.d2l(traceCoordinate[0]) * this.scene.dataScale[0], - sceneLayout.yaxis.d2l(traceCoordinate[1]) * this.scene.dataScale[1], - sceneLayout.zaxis.d2l(traceCoordinate[2]) * this.scene.dataScale[2] + sceneLayout.xaxis.d2l(traceCoordinate[0], 0, this.data.xcalendar) * this.scene.dataScale[0], + sceneLayout.yaxis.d2l(traceCoordinate[1], 0, this.data.ycalendar) * this.scene.dataScale[1], + sceneLayout.zaxis.d2l(traceCoordinate[2], 0, this.data.zcalendar) * this.scene.dataScale[2] ]; var text = this.data.text; @@ -213,31 +213,36 @@ proto.update = function(data) { * and that the sub-array entries correspond to a x-coords, * which is the transpose of 'gl-surface-plot'. */ + + var xcalendar = data.xcalendar, + ycalendar = data.ycalendar, + zcalendar = data.zcalendar; + fill(coords[2], function(row, col) { - return zaxis.d2l(z[col][row]) * scaleFactor[2]; + return zaxis.d2l(z[col][row], 0, zcalendar) * scaleFactor[2]; }); // coords x if(Array.isArray(x[0])) { fill(xc, function(row, col) { - return xaxis.d2l(x[col][row]) * scaleFactor[0]; + return xaxis.d2l(x[col][row], 0, xcalendar) * scaleFactor[0]; }); } else { // ticks x fill(xc, function(row) { - return xaxis.d2l(x[row]) * scaleFactor[0]; + return xaxis.d2l(x[row], 0, xcalendar) * scaleFactor[0]; }); } // coords y if(Array.isArray(y[0])) { fill(yc, function(row, col) { - return yaxis.d2l(y[col][row]) * scaleFactor[1]; + return yaxis.d2l(y[col][row], 0, ycalendar) * scaleFactor[1]; }); } else { // ticks y fill(yc, function(row, col) { - return yaxis.d2l(y[col]) * scaleFactor[1]; + return yaxis.d2l(y[col], 0, ycalendar) * scaleFactor[1]; }); } diff --git a/src/traces/surface/defaults.js b/src/traces/surface/defaults.js index a03da51d3a5..3e88b83674a 100644 --- a/src/traces/surface/defaults.js +++ b/src/traces/surface/defaults.js @@ -9,6 +9,7 @@ 'use strict'; +var Registry = require('../../registry'); var Lib = require('../../lib'); var colorscaleDefaults = require('../../components/colorscale/defaults'); @@ -34,6 +35,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('x'); coerce('y'); + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); + handleCalendarDefaults(traceIn, traceOut, ['x', 'y', 'z'], layout); + if(!Array.isArray(traceOut.x)) { // build a linearly scaled x traceOut.x = []; diff --git a/src/transforms/filter.js b/src/transforms/filter.js index b1c6d704b2e..1467620d7cf 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -9,6 +9,7 @@ 'use strict'; var Lib = require('../lib'); +var Registry = require('../registry'); var PlotSchema = require('../plot_api/plot_schema'); var axisIds = require('../plots/cartesian/axis_ids'); var autoType = require('../plots/cartesian/axis_autotype'); @@ -116,6 +117,9 @@ exports.supplyDefaults = function(transformIn) { coerce('operation'); coerce('value'); coerce('target'); + + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults'); + handleCalendarDefaults(transformIn, transformOut, 'calendar', null); } return transformOut; @@ -130,8 +134,9 @@ exports.calcTransform = function(gd, trace, opts) { if(!len) return; - var dataToCoord = getDataToCoordFunc(gd, trace, target), - filterFunc = getFilterFunc(opts, dataToCoord), + var targetCalendar = Lib.nestedProperty(trace, target + 'calendar').get(), + dataToCoord = getDataToCoordFunc(gd, trace, target), + filterFunc = getFilterFunc(opts, dataToCoord, targetCalendar), arrayAttrs = PlotSchema.findArrayAttributes(trace), originalArrays = {}; @@ -188,9 +193,11 @@ function getDataToCoordFunc(gd, trace, target) { setConvert(ax); - // build up ax._categories (usually done during ax.makeCalcdata() - for(var i = 0; i < target.length; i++) { - ax.d2c(target[i]); + if(ax.type === 'category') { + // build up ax._categories (usually done during ax.makeCalcdata() + for(var i = 0; i < target.length; i++) { + ax.d2c(target[i]); + } } } else { @@ -210,7 +217,7 @@ function getDataToCoordFunc(gd, trace, target) { return function(v) { return +v; }; } -function getFilterFunc(opts, d2c) { +function getFilterFunc(opts, d2c, targetCalendar) { var operation = opts.operation, value = opts.value, hasArrayValue = Array.isArray(value); @@ -219,93 +226,96 @@ function getFilterFunc(opts, d2c) { return array.indexOf(operation) !== -1; } + var d2cValue = function(v) { return d2c(v, 0, opts.calendar); }, + d2cTarget = function(v) { return d2c(v, 0, targetCalendar); }; + var coercedValue; if(isOperationIn(INEQUALITY_OPS)) { - coercedValue = hasArrayValue ? d2c(value[0]) : d2c(value); + coercedValue = hasArrayValue ? d2cValue(value[0]) : d2cValue(value); } else if(isOperationIn(INTERVAL_OPS)) { coercedValue = hasArrayValue ? - [d2c(value[0]), d2c(value[1])] : - [d2c(value), d2c(value)]; + [d2cValue(value[0]), d2cValue(value[1])] : + [d2cValue(value), d2cValue(value)]; } else if(isOperationIn(SET_OPS)) { - coercedValue = hasArrayValue ? value.map(d2c) : [d2c(value)]; + coercedValue = hasArrayValue ? value.map(d2cValue) : [d2cValue(value)]; } switch(operation) { case '=': - return function(v) { return d2c(v) === coercedValue; }; + return function(v) { return d2cTarget(v) === coercedValue; }; case '<': - return function(v) { return d2c(v) < coercedValue; }; + return function(v) { return d2cTarget(v) < coercedValue; }; case '<=': - return function(v) { return d2c(v) <= coercedValue; }; + return function(v) { return d2cTarget(v) <= coercedValue; }; case '>': - return function(v) { return d2c(v) > coercedValue; }; + return function(v) { return d2cTarget(v) > coercedValue; }; case '>=': - return function(v) { return d2c(v) >= coercedValue; }; + return function(v) { return d2cTarget(v) >= coercedValue; }; case '[]': return function(v) { - var cv = d2c(v); + var cv = d2cTarget(v); return cv >= coercedValue[0] && cv <= coercedValue[1]; }; case '()': return function(v) { - var cv = d2c(v); + var cv = d2cTarget(v); return cv > coercedValue[0] && cv < coercedValue[1]; }; case '[)': return function(v) { - var cv = d2c(v); + var cv = d2cTarget(v); return cv >= coercedValue[0] && cv < coercedValue[1]; }; case '(]': return function(v) { - var cv = d2c(v); + var cv = d2cTarget(v); return cv > coercedValue[0] && cv <= coercedValue[1]; }; case '][': return function(v) { - var cv = d2c(v); + var cv = d2cTarget(v); return cv <= coercedValue[0] || cv >= coercedValue[1]; }; case ')(': return function(v) { - var cv = d2c(v); + var cv = d2cTarget(v); return cv < coercedValue[0] || cv > coercedValue[1]; }; case '](': return function(v) { - var cv = d2c(v); + var cv = d2cTarget(v); return cv <= coercedValue[0] || cv > coercedValue[1]; }; case ')[': return function(v) { - var cv = d2c(v); + var cv = d2cTarget(v); return cv < coercedValue[0] || cv >= coercedValue[1]; }; case '{}': return function(v) { - return coercedValue.indexOf(d2c(v)) !== -1; + return coercedValue.indexOf(d2cTarget(v)) !== -1; }; case '}{': return function(v) { - return coercedValue.indexOf(d2c(v)) === -1; + return coercedValue.indexOf(d2cTarget(v)) === -1; }; } } diff --git a/test/image/baselines/candlestick_rangeslider_thai.png b/test/image/baselines/candlestick_rangeslider_thai.png new file mode 100644 index 00000000000..60880772a07 Binary files /dev/null and b/test/image/baselines/candlestick_rangeslider_thai.png differ diff --git a/test/image/baselines/finance_style.png b/test/image/baselines/finance_style.png index 6bf7d6fa708..c9902e3eb3f 100644 Binary files a/test/image/baselines/finance_style.png and b/test/image/baselines/finance_style.png differ diff --git a/test/image/baselines/gl2d_date_axes.png b/test/image/baselines/gl2d_date_axes.png index bc2b828d88a..96fd04f20ab 100644 Binary files a/test/image/baselines/gl2d_date_axes.png and b/test/image/baselines/gl2d_date_axes.png differ diff --git a/test/image/baselines/gl3d_scatter-date.png b/test/image/baselines/gl3d_scatter-date.png deleted file mode 100644 index 2a60ee8b50e..00000000000 Binary files a/test/image/baselines/gl3d_scatter-date.png and /dev/null differ diff --git a/test/image/baselines/gl3d_world-cals.png b/test/image/baselines/gl3d_world-cals.png new file mode 100644 index 00000000000..ffbc6fc552e Binary files /dev/null and b/test/image/baselines/gl3d_world-cals.png differ diff --git a/test/image/baselines/world-cals.png b/test/image/baselines/world-cals.png new file mode 100644 index 00000000000..a5263a73199 Binary files /dev/null and b/test/image/baselines/world-cals.png differ diff --git a/test/image/mocks/candlestick_double-y-axis.json b/test/image/mocks/candlestick_double-y-axis.json index 3d94eaa9e0f..ecb8a71b398 100644 --- a/test/image/mocks/candlestick_double-y-axis.json +++ b/test/image/mocks/candlestick_double-y-axis.json @@ -1947,8 +1947,8 @@ "layout": { "xaxis": { "range": [ - 1452453861917, - 1471240462978 + "2016-01-10 19:24:21.917", + "2016-08-15 05:54:22.978" ] }, "yaxis": { diff --git a/test/image/mocks/candlestick_rangeslider_thai.json b/test/image/mocks/candlestick_rangeslider_thai.json new file mode 100644 index 00000000000..e9d13a688e9 --- /dev/null +++ b/test/image/mocks/candlestick_rangeslider_thai.json @@ -0,0 +1,1975 @@ +{ + "data": [ + { + "type": "candlestick", + "name": "ABC", + "x": [ + "2016-09-30", + "2016-09-29", + "2016-09-28", + "2016-09-27", + "2016-09-26", + "2016-09-23", + "2016-09-22", + "2016-09-21", + "2016-09-20", + "2016-09-19", + "2016-09-16", + "2016-09-15", + "2016-09-14", + "2016-09-13", + "2016-09-12", + "2016-09-09", + "2016-09-08", + "2016-09-07", + "2016-09-06", + "2016-09-02", + "2016-09-01", + "2016-08-31", + "2016-08-30", + "2016-08-29", + "2016-08-26", + "2016-08-25", + "2016-08-24", + "2016-08-23", + "2016-08-22", + "2016-08-19", + "2016-08-18", + "2016-08-17", + "2016-08-16", + "2016-08-15", + "2016-08-12", + "2016-08-11", + "2016-08-10", + "2016-08-09", + "2016-08-08", + "2016-08-05", + "2016-08-04", + "2016-08-03", + "2016-08-02", + "2016-08-01", + "2016-07-29", + "2016-07-28", + "2016-07-27", + "2016-07-26", + "2016-07-25", + "2016-07-22", + "2016-07-21", + "2016-07-20", + "2016-07-19", + "2016-07-18", + "2016-07-15", + "2016-07-14", + "2016-07-13", + "2016-07-12", + "2016-07-11", + "2016-07-08", + "2016-07-07", + "2016-07-06", + "2016-07-05", + "2016-07-01", + "2016-06-30", + "2016-06-29", + "2016-06-28", + "2016-06-27", + "2016-06-24", + "2016-06-23", + "2016-06-22", + "2016-06-21", + "2016-06-20", + "2016-06-17", + "2016-06-16", + "2016-06-15", + "2016-06-14", + "2016-06-13", + "2016-06-10", + "2016-06-09", + "2016-06-08", + "2016-06-07", + "2016-06-06", + "2016-06-03", + "2016-06-02", + "2016-06-01", + "2016-05-31", + "2016-05-27", + "2016-05-26", + "2016-05-25", + "2016-05-24", + "2016-05-23", + "2016-05-20", + "2016-05-19", + "2016-05-18", + "2016-05-17", + "2016-05-16", + "2016-05-13", + "2016-05-12", + "2016-05-11", + "2016-05-10", + "2016-05-09", + "2016-05-06", + "2016-05-05", + "2016-05-04", + "2016-05-03", + "2016-05-02", + "2016-04-29", + "2016-04-28", + "2016-04-27", + "2016-04-26", + "2016-04-25", + "2016-04-22", + "2016-04-21", + "2016-04-20", + "2016-04-19", + "2016-04-18", + "2016-04-15", + "2016-04-14", + "2016-04-13", + "2016-04-12", + "2016-04-11", + "2016-04-08", + "2016-04-07", + "2016-04-06", + "2016-04-05", + "2016-04-04", + "2016-04-01", + "2016-03-31", + "2016-03-30", + "2016-03-29", + "2016-03-28", + "2016-03-24", + "2016-03-23", + "2016-03-22", + "2016-03-21", + "2016-03-18", + "2016-03-17", + "2016-03-16", + "2016-03-15", + "2016-03-14", + "2016-03-11", + "2016-03-10", + "2016-03-09", + "2016-03-08", + "2016-03-07", + "2016-03-04", + "2016-03-03", + "2016-03-02", + "2016-03-01", + "2016-02-29", + "2016-02-26", + "2016-02-25", + "2016-02-24", + "2016-02-23", + "2016-02-22", + "2016-02-19", + "2016-02-18", + "2016-02-17", + "2016-02-16", + "2016-02-12", + "2016-02-11", + "2016-02-10", + "2016-02-09", + "2016-02-08", + "2016-02-05", + "2016-02-04", + "2016-02-03", + "2016-02-02", + "2016-02-01", + "2016-01-29", + "2016-01-28", + "2016-01-27", + "2016-01-26", + "2016-01-25", + "2016-01-22", + "2016-01-21", + "2016-01-20", + "2016-01-19", + "2016-01-15", + "2016-01-14", + "2016-01-13", + "2016-01-12", + "2016-01-11", + "2016-01-08", + "2016-01-07", + "2016-01-06", + "2016-01-05", + "2016-01-04" + ], + "open": [ + "776.330017", + "781.440002", + "777.849976", + "775.50", + "782.73999", + "786.590027", + "780.00", + "772.659973", + "769.00", + "772.419983", + "769.75", + "762.890015", + "759.609985", + "764.47998", + "755.130005", + "770.099976", + "778.590027", + "780.00", + "773.450012", + "773.01001", + "769.25", + "767.01001", + "769.330017", + "768.73999", + "769.00", + "767.00", + "770.580017", + "775.47998", + "773.27002", + "775.00", + "780.01001", + "777.320007", + "780.299988", + "783.75", + "781.50", + "785.00", + "783.75", + "781.099976", + "782.00", + "773.780029", + "772.219971", + "767.179993", + "768.690002", + "761.090027", + "772.710022", + "747.039978", + "738.280029", + "739.039978", + "740.669983", + "741.859985", + "740.359985", + "737.330017", + "729.890015", + "722.710022", + "725.72998", + "721.580017", + "723.619995", + "719.119995", + "708.049988", + "699.50", + "698.080017", + "689.97998", + "696.059998", + "692.200012", + "685.469971", + "683.00", + "678.969971", + "671.00", + "675.169983", + "697.450012", + "699.059998", + "698.400024", + "698.77002", + "708.650024", + "714.909973", + "719.00", + "716.47998", + "716.51001", + "719.469971", + "722.869995", + "723.960022", + "719.840027", + "724.909973", + "729.27002", + "732.50", + "734.530029", + "731.73999", + "724.01001", + "722.869995", + "720.76001", + "706.859985", + "706.530029", + "701.619995", + "702.359985", + "703.669983", + "715.98999", + "709.130005", + "711.929993", + "717.059998", + "723.409973", + "716.75", + "712.00", + "698.380005", + "697.700012", + "690.48999", + "696.869995", + "697.630005", + "690.700012", + "708.26001", + "707.289978", + "725.419983", + "716.099976", + "726.299988", + "755.380005", + "758.00", + "769.51001", + "760.460022", + "753.97998", + "754.01001", + "749.159973", + "738.00", + "743.02002", + "743.969971", + "745.369995", + "735.77002", + "738.00", + "750.059998", + "738.599976", + "749.25", + "750.099976", + "734.590027", + "736.789978", + "732.01001", + "742.359985", + "737.460022", + "736.50", + "741.859985", + "736.450012", + "726.369995", + "726.919983", + "726.809998", + "720.00", + "708.119995", + "698.469971", + "688.590027", + "706.900024", + "714.98999", + "718.679993", + "719.00", + "703.619995", + "700.320007", + "708.580017", + "700.01001", + "688.919983", + "701.450012", + "707.450012", + "695.030029", + "710.00", + "698.090027", + "692.97998", + "690.26001", + "675.00", + "686.859985", + "672.320007", + "667.849976", + "703.869995", + "722.809998", + "770.219971", + "784.50", + "750.460022", + "731.530029", + "722.219971", + "713.669983", + "713.849976", + "723.580017", + "723.599976", + "702.179993", + "688.609985", + "703.299988", + "692.289978", + "705.380005", + "730.849976", + "721.679993", + "716.609985", + "731.450012", + "730.309998", + "730.00", + "746.450012", + "743.00" + ], + "high": [ + "780.940002", + "785.799988", + "781.809998", + "785.98999", + "782.73999", + "788.929993", + "789.849976", + "777.159973", + "773.330017", + "774.00", + "769.75", + "773.799988", + "767.679993", + "766.219971", + "770.289978", + "773.244995", + "780.349976", + "782.72998", + "782.00", + "773.919983", + "771.02002", + "769.090027", + "774.466003", + "774.98999", + "776.080017", + "771.890015", + "774.50", + "776.440002", + "774.539978", + "777.099976", + "782.859985", + "780.809998", + "780.97998", + "787.48999", + "783.39502", + "789.75", + "786.812012", + "788.940002", + "782.630005", + "783.039978", + "774.070007", + "773.210022", + "775.840027", + "780.429993", + "778.549988", + "748.650024", + "744.460022", + "741.690002", + "742.609985", + "743.23999", + "741.690002", + "742.130005", + "736.98999", + "736.130005", + "725.73999", + "722.210022", + "724.00", + "722.940002", + "716.51001", + "705.710022", + "698.200012", + "701.679993", + "696.940002", + "700.650024", + "692.320007", + "687.429016", + "680.330017", + "672.299988", + "689.400024", + "701.950012", + "700.859985", + "702.77002", + "702.47998", + "708.820007", + "716.650024", + "722.97998", + "722.469971", + "725.440002", + "725.890015", + "729.539978", + "728.570007", + "721.97998", + "724.909973", + "729.48999", + "733.02002", + "737.210022", + "739.72998", + "733.935974", + "728.330017", + "727.51001", + "720.969971", + "711.478027", + "714.580017", + "706.00", + "711.599976", + "721.52002", + "718.47998", + "716.661987", + "719.25", + "724.47998", + "723.50", + "718.710022", + "711.859985", + "702.320007", + "699.75", + "697.840027", + "700.640015", + "697.619995", + "714.169983", + "708.97998", + "725.765991", + "723.929993", + "736.119995", + "760.450012", + "758.132019", + "769.900024", + "768.049988", + "761.00", + "757.309998", + "754.380005", + "743.830017", + "745.00", + "745.450012", + "747.00", + "746.23999", + "742.799988", + "752.799988", + "750.340027", + "750.849976", + "757.880005", + "747.25", + "738.98999", + "737.747009", + "745.719971", + "745.00", + "742.50", + "742.00", + "743.070007", + "737.469971", + "732.289978", + "735.50", + "726.919983", + "716.440002", + "705.679993", + "703.789978", + "708.091003", + "716.48999", + "719.450012", + "720.00", + "718.809998", + "710.890015", + "713.429993", + "705.97998", + "700.00", + "708.400024", + "713.23999", + "703.080994", + "712.349976", + "709.75", + "698.00", + "693.75", + "689.349976", + "701.309998", + "699.900024", + "684.030029", + "703.98999", + "727.00", + "774.50", + "789.869995", + "757.859985", + "744.98999", + "733.690002", + "718.234985", + "718.280029", + "729.679993", + "728.130005", + "719.190002", + "706.849976", + "709.97998", + "706.73999", + "721.924988", + "734.73999", + "728.75", + "718.85498", + "733.22998", + "738.50", + "747.179993", + "752.00", + "744.059998" + ], + "low": [ + "774.090027", + "774.231995", + "774.969971", + "774.307983", + "773.070007", + "784.150024", + "778.440002", + "768.301025", + "768.530029", + "764.440979", + "764.659973", + "759.960022", + "759.109985", + "755.799988", + "754.00", + "759.659973", + "773.580017", + "776.200012", + "771.00", + "768.409973", + "764.299988", + "765.380005", + "766.840027", + "766.61499", + "765.849976", + "763.184998", + "767.070007", + "771.784973", + "770.049988", + "773.130005", + "777.00", + "773.530029", + "773.44397", + "780.109985", + "780.400024", + "782.969971", + "782.778015", + "780.570007", + "778.091003", + "772.340027", + "768.794983", + "766.820007", + "767.849976", + "761.090027", + "766.77002", + "739.299988", + "737.00", + "734.27002", + "737.50", + "736.559998", + "735.830994", + "737.099976", + "729.00", + "721.190002", + "719.054993", + "718.030029", + "716.849976", + "715.909973", + "707.23999", + "696.434998", + "688.215027", + "689.090027", + "688.880005", + "692.130005", + "683.650024", + "681.409973", + "673.00", + "663.283997", + "673.450012", + "687.00", + "693.08197", + "692.01001", + "693.409973", + "688.452026", + "703.26001", + "717.309998", + "713.119995", + "716.51001", + "716.429993", + "722.335999", + "720.580017", + "716.549988", + "714.609985", + "720.559998", + "724.169983", + "730.659973", + "731.26001", + "724.00", + "720.280029", + "719.705017", + "706.859985", + "704.179993", + "700.52002", + "696.799988", + "700.630005", + "704.109985", + "705.650024", + "709.26001", + "709.00", + "712.799988", + "715.719971", + "710.00", + "698.106995", + "695.719971", + "689.01001", + "692.00", + "691.00", + "689.00", + "689.549988", + "692.36499", + "703.026001", + "715.590027", + "713.609985", + "749.549988", + "750.01001", + "749.330017", + "757.299988", + "752.69397", + "752.705017", + "744.260986", + "731.01001", + "736.049988", + "735.549988", + "736.280029", + "735.559998", + "735.369995", + "742.429993", + "737.00", + "740.940002", + "748.73999", + "728.76001", + "732.50", + "731.00", + "736.150024", + "737.460022", + "733.515991", + "731.830017", + "736.00", + "724.51001", + "724.77002", + "725.150024", + "717.125", + "703.359985", + "694.00", + "685.340027", + "686.900024", + "706.02002", + "706.02002", + "712.00", + "699.77002", + "697.679993", + "700.859985", + "690.585022", + "680.780029", + "693.580017", + "702.51001", + "694.049988", + "696.030029", + "691.380005", + "685.049988", + "678.599976", + "668.867981", + "682.130005", + "668.77002", + "663.059998", + "680.150024", + "701.859985", + "720.50", + "764.650024", + "743.27002", + "726.799988", + "712.349976", + "694.390015", + "706.47998", + "710.01001", + "720.120972", + "694.460022", + "673.26001", + "693.409973", + "685.369995", + "689.099976", + "698.609985", + "717.317017", + "703.539978", + "713.00", + "719.059998", + "728.919983", + "738.640015", + "731.257996" + ], + "close": [ + "777.289978", + "775.01001", + "781.559998", + "783.01001", + "774.210022", + "786.900024", + "787.210022", + "776.219971", + "771.409973", + "765.700012", + "768.880005", + "771.76001", + "762.48999", + "759.690002", + "769.02002", + "759.659973", + "775.320007", + "780.349976", + "780.080017", + "771.460022", + "768.780029", + "767.049988", + "769.090027", + "772.150024", + "769.539978", + "769.409973", + "769.640015", + "772.080017", + "772.150024", + "775.419983", + "777.50", + "779.909973", + "777.140015", + "782.440002", + "783.219971", + "784.849976", + "784.679993", + "784.26001", + "781.76001", + "782.219971", + "771.609985", + "773.179993", + "771.070007", + "772.880005", + "768.789978", + "745.909973", + "741.77002", + "738.419983", + "739.77002", + "742.73999", + "738.630005", + "741.190002", + "736.960022", + "733.780029", + "719.849976", + "720.950012", + "716.97998", + "720.640015", + "715.090027", + "705.630005", + "695.359985", + "697.77002", + "694.950012", + "699.210022", + "692.099976", + "684.109985", + "680.039978", + "668.26001", + "675.219971", + "701.869995", + "697.460022", + "695.940002", + "693.710022", + "691.719971", + "710.359985", + "718.919983", + "718.27002", + "718.359985", + "719.409973", + "728.580017", + "728.280029", + "716.650024", + "716.549988", + "722.340027", + "730.400024", + "734.150024", + "735.719971", + "732.659973", + "724.119995", + "725.27002", + "720.090027", + "704.23999", + "709.73999", + "700.320007", + "706.630005", + "706.22998", + "716.48999", + "710.830017", + "713.309998", + "715.289978", + "723.179993", + "712.900024", + "711.119995", + "701.429993", + "695.700012", + "692.359985", + "698.210022", + "693.01001", + "691.02002", + "705.840027", + "708.140015", + "723.150024", + "718.77002", + "759.140015", + "752.669983", + "753.929993", + "766.609985", + "759.00", + "753.200012", + "751.719971", + "743.090027", + "736.099976", + "739.150024", + "740.280029", + "745.690002", + "737.799988", + "745.289978", + "749.909973", + "744.950012", + "750.530029", + "744.77002", + "733.530029", + "735.299988", + "738.059998", + "740.75", + "742.090027", + "737.599976", + "737.780029", + "736.090027", + "728.330017", + "730.48999", + "726.820007", + "712.820007", + "705.23999", + "693.969971", + "695.159973", + "710.890015", + "712.419983", + "718.849976", + "718.809998", + "697.77002", + "705.070007", + "705.75", + "699.559998", + "695.849976", + "706.460022", + "700.909973", + "697.349976", + "708.400024", + "691.00", + "682.400024", + "683.109985", + "684.119995", + "678.109985", + "682.73999", + "683.570007", + "708.01001", + "726.950012", + "764.650024", + "752.00", + "742.950012", + "730.960022", + "699.98999", + "713.039978", + "711.669983", + "725.25", + "706.590027", + "698.450012", + "701.789978", + "694.450012", + "714.719971", + "700.559998", + "726.070007", + "716.030029", + "714.469971", + "726.390015", + "743.619995", + "742.580017", + "741.840027" + ], + "increasing": { + "line": { + "color": "#4dac26" + }, + "name": "ABC" + }, + "decreasing": { + "line": { + "color": "#d01c8b" + }, + "showlegend": false + } + }, + { + "type": "candlestick", + "name": "XYZ", + "x": [ + "2016-09-30", + "2016-09-29", + "2016-09-28", + "2016-09-27", + "2016-09-26", + "2016-09-23", + "2016-09-22", + "2016-09-21", + "2016-09-20", + "2016-09-19", + "2016-09-16", + "2016-09-15", + "2016-09-14", + "2016-09-13", + "2016-09-12", + "2016-09-09", + "2016-09-08", + "2016-09-07", + "2016-09-06", + "2016-09-02", + "2016-09-01", + "2016-08-31", + "2016-08-30", + "2016-08-29", + "2016-08-26", + "2016-08-25", + "2016-08-24", + "2016-08-23", + "2016-08-22", + "2016-08-19", + "2016-08-18", + "2016-08-17", + "2016-08-16", + "2016-08-15", + "2016-08-12", + "2016-08-11", + "2016-08-10", + "2016-08-09", + "2016-08-08", + "2016-08-05", + "2016-08-04", + "2016-08-03", + "2016-08-02", + "2016-08-01", + "2016-07-29", + "2016-07-28", + "2016-07-27", + "2016-07-26", + "2016-07-25", + "2016-07-22", + "2016-07-21", + "2016-07-20", + "2016-07-19", + "2016-07-18", + "2016-07-15", + "2016-07-14", + "2016-07-13", + "2016-07-12", + "2016-07-11", + "2016-07-08", + "2016-07-07", + "2016-07-06", + "2016-07-05", + "2016-07-01", + "2016-06-30", + "2016-06-29", + "2016-06-28", + "2016-06-27", + "2016-06-24", + "2016-06-23", + "2016-06-22", + "2016-06-21", + "2016-06-20", + "2016-06-17", + "2016-06-16", + "2016-06-15", + "2016-06-14", + "2016-06-13", + "2016-06-10", + "2016-06-09", + "2016-06-08", + "2016-06-07", + "2016-06-06", + "2016-06-03", + "2016-06-02", + "2016-06-01", + "2016-05-31", + "2016-05-27", + "2016-05-26", + "2016-05-25", + "2016-05-24", + "2016-05-23", + "2016-05-20", + "2016-05-19", + "2016-05-18", + "2016-05-17", + "2016-05-16", + "2016-05-13", + "2016-05-12", + "2016-05-11", + "2016-05-10", + "2016-05-09", + "2016-05-06", + "2016-05-05", + "2016-05-04", + "2016-05-03", + "2016-05-02", + "2016-04-29", + "2016-04-28", + "2016-04-27", + "2016-04-26", + "2016-04-25", + "2016-04-22", + "2016-04-21", + "2016-04-20", + "2016-04-19", + "2016-04-18", + "2016-04-15", + "2016-04-14", + "2016-04-13", + "2016-04-12", + "2016-04-11", + "2016-04-08", + "2016-04-07", + "2016-04-06", + "2016-04-05", + "2016-04-04", + "2016-04-01", + "2016-03-31", + "2016-03-30", + "2016-03-29", + "2016-03-28", + "2016-03-24", + "2016-03-23", + "2016-03-22", + "2016-03-21", + "2016-03-18", + "2016-03-17", + "2016-03-16", + "2016-03-15", + "2016-03-14", + "2016-03-11", + "2016-03-10", + "2016-03-09", + "2016-03-08", + "2016-03-07", + "2016-03-04", + "2016-03-03", + "2016-03-02", + "2016-03-01", + "2016-02-29", + "2016-02-26", + "2016-02-25", + "2016-02-24", + "2016-02-23", + "2016-02-22", + "2016-02-19", + "2016-02-18", + "2016-02-17", + "2016-02-16", + "2016-02-12", + "2016-02-11", + "2016-02-10", + "2016-02-09", + "2016-02-08", + "2016-02-05", + "2016-02-04", + "2016-02-03", + "2016-02-02", + "2016-02-01", + "2016-01-29", + "2016-01-28", + "2016-01-27", + "2016-01-26", + "2016-01-25", + "2016-01-22", + "2016-01-21", + "2016-01-20", + "2016-01-19", + "2016-01-15", + "2016-01-14", + "2016-01-13", + "2016-01-12", + "2016-01-11", + "2016-01-08", + "2016-01-07", + "2016-01-06", + "2016-01-05", + "2016-01-04" + ], + "open": [ + "112.459999", + "113.160004", + "113.690002", + "113.00", + "111.639999", + "114.419998", + "114.349998", + "113.849998", + "113.050003", + "115.190002", + "115.120003", + "113.860001", + "108.730003", + "107.510002", + "102.650002", + "104.639999", + "107.25", + "107.830002", + "107.900002", + "107.699997", + "106.139999", + "105.660004", + "105.800003", + "106.620003", + "107.410004", + "107.389999", + "108.57", + "108.589996", + "108.860001", + "108.769997", + "109.230003", + "109.099998", + "109.629997", + "108.139999", + "107.779999", + "108.519997", + "108.709999", + "108.230003", + "107.519997", + "106.269997", + "105.580002", + "104.809998", + "106.050003", + "104.410004", + "104.190002", + "102.830002", + "104.269997", + "96.82", + "98.25", + "99.260002", + "99.830002", + "100.00", + "99.559998", + "98.699997", + "98.919998", + "97.389999", + "97.410004", + "97.169998", + "96.75", + "96.489998", + "95.699997", + "94.599998", + "95.389999", + "95.489998", + "94.440002", + "93.970001", + "92.900002", + "93.00", + "92.910004", + "95.940002", + "96.25", + "94.940002", + "96.00", + "96.620003", + "96.449997", + "97.82", + "97.32", + "98.690002", + "98.529999", + "98.50", + "99.019997", + "99.25", + "97.989998", + "97.790001", + "97.599998", + "99.019997", + "99.599998", + "99.440002", + "99.68", + "98.669998", + "97.220001", + "95.870003", + "94.639999", + "94.639999", + "94.160004", + "94.550003", + "92.389999", + "90.00", + "92.720001", + "93.480003", + "93.330002", + "93.00", + "93.370003", + "94.00", + "95.199997", + "94.199997", + "93.970001", + "93.989998", + "97.610001", + "96.00", + "103.910004", + "105.00", + "105.010002", + "106.93", + "106.639999", + "107.879997", + "108.889999", + "112.110001", + "111.620003", + "110.800003", + "109.339996", + "108.970001", + "108.910004", + "109.949997", + "110.230003", + "109.510002", + "110.419998", + "108.779999", + "109.720001", + "108.650002", + "104.889999", + "106.00", + "105.470001", + "106.480003", + "105.25", + "105.93", + "106.339996", + "105.519997", + "104.610001", + "103.959999", + "101.910004", + "102.239998", + "101.410004", + "101.309998", + "100.779999", + "102.389999", + "102.370003", + "100.580002", + "100.510002", + "97.650002", + "96.860001", + "97.199997", + "96.050003", + "93.980003", + "96.400002", + "96.309998", + "96.00", + "98.839996", + "96.669998", + "95.019997", + "94.190002", + "93.790001", + "95.919998", + "94.290001", + "93.129997", + "96.519997", + "95.860001", + "95.00", + "95.419998", + "96.470001", + "94.790001", + "93.790001", + "96.040001", + "99.93", + "101.519997", + "98.629997", + "97.059998", + "95.099998", + "98.410004", + "96.199997", + "97.959999", + "100.32", + "100.550003", + "98.970001", + "98.550003", + "98.68", + "100.559998", + "105.75", + "102.610001" + ], + "high": [ + "113.370003", + "113.800003", + "114.639999", + "113.18", + "113.389999", + "114.790001", + "114.940002", + "113.989998", + "114.120003", + "116.18", + "116.129997", + "115.730003", + "113.029999", + "108.790001", + "105.720001", + "105.720001", + "107.269997", + "108.760002", + "108.300003", + "108.00", + "106.800003", + "106.57", + "106.50", + "107.440002", + "107.949997", + "107.879997", + "108.75", + "109.32", + "109.099998", + "109.690002", + "109.599998", + "109.370003", + "110.230003", + "109.540001", + "108.440002", + "108.93", + "108.900002", + "108.940002", + "108.370003", + "107.650002", + "106.00", + "105.839996", + "106.07", + "106.150002", + "104.550003", + "104.449997", + "104.349998", + "97.970001", + "98.839996", + "99.300003", + "101.00", + "100.459999", + "100.00", + "100.129997", + "99.300003", + "98.989998", + "97.669998", + "97.699997", + "97.650002", + "96.889999", + "96.50", + "95.660004", + "95.400002", + "96.470001", + "95.769997", + "94.550003", + "93.660004", + "93.050003", + "94.660004", + "96.290001", + "96.889999", + "96.349998", + "96.57", + "96.650002", + "97.75", + "98.410004", + "98.480003", + "99.120003", + "99.349998", + "99.989998", + "99.559998", + "99.870003", + "101.889999", + "98.269997", + "97.839996", + "99.540001", + "100.400002", + "100.470001", + "100.730003", + "99.739998", + "98.089996", + "97.190002", + "95.43", + "94.639999", + "95.209999", + "94.699997", + "94.389999", + "91.669998", + "92.779999", + "93.57", + "93.57", + "93.769997", + "93.449997", + "94.07", + "95.900002", + "95.739998", + "94.080002", + "94.720001", + "97.879997", + "98.709999", + "105.300003", + "105.650002", + "106.480003", + "106.93", + "108.089996", + "108.00", + "108.949997", + "112.300003", + "112.389999", + "112.339996", + "110.50", + "110.610001", + "109.769997", + "110.419998", + "110.980003", + "110.730003", + "112.190002", + "110.00", + "109.900002", + "110.419998", + "107.790001", + "106.190002", + "106.25", + "107.07", + "107.290001", + "107.650002", + "106.50", + "106.470001", + "106.309998", + "105.18", + "102.910004", + "102.279999", + "102.239998", + "101.580002", + "101.760002", + "102.830002", + "103.75", + "101.709999", + "100.889999", + "100.769997", + "98.230003", + "98.019997", + "96.760002", + "96.379997", + "96.50", + "96.900002", + "96.760002", + "98.889999", + "98.209999", + "96.849998", + "94.50", + "94.720001", + "96.349998", + "95.940002", + "95.699997", + "96.919998", + "97.330002", + "96.839996", + "96.040001", + "96.709999", + "97.339996", + "94.519997", + "96.629997", + "100.879997", + "101.529999", + "101.459999", + "97.879997", + "98.190002", + "98.650002", + "97.709999", + "100.480003", + "101.190002", + "100.690002", + "99.059998", + "99.110001", + "100.129997", + "102.370003", + "105.849998", + "105.370003" + ], + "low": [ + "111.800003", + "111.800003", + "113.43", + "112.339996", + "111.550003", + "111.550003", + "114.00", + "112.440002", + "112.510002", + "113.25", + "114.040001", + "113.489998", + "108.599998", + "107.239998", + "102.529999", + "103.129997", + "105.239998", + "107.07", + "107.510002", + "106.82", + "105.620003", + "105.639999", + "105.50", + "106.290001", + "106.309998", + "106.68", + "107.68", + "108.529999", + "107.849998", + "108.360001", + "109.019997", + "108.339996", + "109.209999", + "108.080002", + "107.779999", + "107.849998", + "107.760002", + "108.010002", + "107.160004", + "106.18", + "105.279999", + "104.769997", + "104.00", + "104.410004", + "103.68", + "102.82", + "102.75", + "96.419998", + "96.919998", + "98.309998", + "99.129997", + "99.739998", + "99.339996", + "98.599998", + "98.50", + "97.32", + "96.839996", + "97.120003", + "96.730003", + "96.050003", + "95.620003", + "94.370003", + "94.459999", + "95.330002", + "94.300003", + "93.629997", + "92.139999", + "91.50", + "92.650002", + "95.25", + "95.349998", + "94.68", + "95.029999", + "95.300003", + "96.07", + "97.029999", + "96.75", + "97.099998", + "98.480003", + "98.459999", + "98.68", + "98.959999", + "97.550003", + "97.449997", + "96.629997", + "98.330002", + "98.82", + "99.25", + "98.639999", + "98.110001", + "96.839996", + "95.669998", + "94.519997", + "93.57", + "93.889999", + "93.010002", + "91.650002", + "90.00", + "89.470001", + "92.459999", + "92.110001", + "92.589996", + "91.849998", + "92.68", + "93.82", + "93.68", + "92.400002", + "92.510002", + "94.25", + "95.68", + "103.910004", + "104.510002", + "104.620003", + "105.519997", + "106.059998", + "106.230003", + "106.940002", + "109.730003", + "111.330002", + "110.800003", + "108.660004", + "108.830002", + "108.169998", + "108.120003", + "109.199997", + "109.419998", + "110.269997", + "108.199997", + "108.879997", + "108.599998", + "104.879997", + "105.059998", + "104.889999", + "105.900002", + "105.209999", + "105.139999", + "105.190002", + "104.959999", + "104.589996", + "103.849998", + "101.779999", + "101.50", + "100.150002", + "100.269997", + "100.400002", + "100.959999", + "101.370003", + "100.449997", + "99.639999", + "97.419998", + "96.650002", + "96.580002", + "95.25", + "93.32", + "94.550003", + "95.919998", + "95.800003", + "96.089996", + "96.150002", + "94.610001", + "93.010002", + "92.589996", + "94.099998", + "93.93", + "93.040001", + "93.690002", + "95.190002", + "94.080002", + "94.279999", + "95.400002", + "94.349998", + "92.389999", + "93.339996", + "98.07", + "99.209999", + "98.370003", + "94.940002", + "93.419998", + "95.50", + "95.360001", + "95.739998", + "97.300003", + "98.839996", + "97.339996", + "96.760002", + "96.43", + "99.870003", + "102.410004", + "102.00" + ], + "close": [ + "113.050003", + "112.18", + "113.949997", + "113.089996", + "112.879997", + "112.709999", + "114.620003", + "113.550003", + "113.57", + "113.580002", + "114.919998", + "115.57", + "111.769997", + "107.949997", + "105.440002", + "103.129997", + "105.519997", + "108.360001", + "107.699997", + "107.730003", + "106.730003", + "106.099998", + "106.00", + "106.82", + "106.940002", + "107.57", + "108.029999", + "108.849998", + "108.510002", + "109.360001", + "109.080002", + "109.220001", + "109.379997", + "109.480003", + "108.18", + "107.93", + "108.00", + "108.809998", + "108.370003", + "107.480003", + "105.870003", + "105.790001", + "104.480003", + "106.050003", + "104.209999", + "104.339996", + "102.949997", + "96.669998", + "97.339996", + "98.660004", + "99.43", + "99.959999", + "99.870003", + "99.830002", + "98.779999", + "98.790001", + "96.870003", + "97.419998", + "96.980003", + "96.68", + "95.940002", + "95.529999", + "94.989998", + "95.889999", + "95.599998", + "94.400002", + "93.589996", + "92.040001", + "93.400002", + "96.099998", + "95.550003", + "95.910004", + "95.099998", + "95.330002", + "97.550003", + "97.139999", + "97.459999", + "97.339996", + "98.830002", + "99.650002", + "98.940002", + "99.029999", + "98.629997", + "97.919998", + "97.720001", + "98.459999", + "99.860001", + "100.349998", + "100.410004", + "99.620003", + "97.900002", + "96.43", + "95.220001", + "94.199997", + "94.559998", + "93.489998", + "93.879997", + "90.519997", + "90.339996", + "92.510002", + "93.419998", + "92.790001", + "92.720001", + "93.239998", + "94.190002", + "95.18", + "93.639999", + "93.739998", + "94.830002", + "97.82", + "104.349998", + "105.080002", + "105.68", + "105.970001", + "107.129997", + "106.910004", + "107.480003", + "109.849998", + "112.099998", + "112.040001", + "110.440002", + "109.019997", + "108.660004", + "108.540001", + "110.959999", + "109.809998", + "111.120003", + "109.989998", + "108.989998", + "109.559998", + "107.68", + "105.190002", + "105.669998", + "106.129997", + "106.720001", + "105.910004", + "105.919998", + "105.800003", + "105.970001", + "104.580002", + "102.519997", + "102.260002", + "101.169998", + "101.120003", + "101.029999", + "101.870003", + "103.010002", + "101.50", + "100.75", + "100.529999", + "96.690002", + "96.910004", + "96.760002", + "96.099998", + "94.690002", + "96.879997", + "96.040001", + "96.260002", + "98.120003", + "96.639999", + "93.989998", + "93.699997", + "94.269997", + "94.989998", + "95.010002", + "94.019997", + "96.599998", + "96.349998", + "94.480003", + "96.43", + "97.339996", + "94.089996", + "93.419998", + "99.989998", + "99.440002", + "101.419998", + "96.300003", + "96.790001", + "96.660004", + "97.129997", + "99.519997", + "97.389999", + "99.959999", + "98.529999", + "96.959999", + "96.449997", + "100.699997", + "102.709999", + "105.349998" + ], + "increasing": { + "line": { + "color": "#b8e186" + }, + "name": "XYZ" + }, + "decreasing": { + "line": { + "color": "#f1b6da" + }, + "showlegend": false + }, + "yaxis": "y2" + } + ], + "layout": { + "xaxis": { + "range": [ + "2559-01-10 14:24:21.917", + "2559-08-15 01:54:22.978" + ], + "calendar": "thai", + "title": "Thai dates" + }, + "yaxis": { + "tickprefix": "$", + "showtickprefix": "last" + }, + "yaxis2": { + "overlaying": "y", + "side": "right", + "tickprefix": "$", + "showtickprefix": "last" + }, + "legend": { + "x": 0, + "xanchor": "left", + "y": 1, + "yanchor": "bottom" + }, + "height": 600, + "width": 1100 + } +} diff --git a/test/image/mocks/finance_style.json b/test/image/mocks/finance_style.json index c7fb15e7def..7adc4f2bdd6 100644 --- a/test/image/mocks/finance_style.json +++ b/test/image/mocks/finance_style.json @@ -90,6 +90,29 @@ 20.609242660033345, 20.9832297017119 ], + "x": [ + "0550-01-01", + "0550-01-02", + "0550-01-03", + "0550-01-04", + "0550-01-05", + "0550-01-06", + "0550-01-07", + "0550-01-08", + "0550-01-09", + "0550-01-10", + "0550-01-11", + "0550-01-12", + "0550-01-13", + "0550-01-14", + "0550-01-15", + "0550-01-16", + "0550-01-17", + "0550-01-18", + "0550-01-19", + "0550-01-20" + ], + "xcalendar": "nanakshahi", "increasing": { "line": { "color": "rgb(150, 200, 250)" @@ -196,6 +219,29 @@ 25.398644940922246, 25.093220170315078 ], + "x": [ + "0550-01-01", + "0550-01-02", + "0550-01-03", + "0550-01-04", + "0550-01-05", + "0550-01-06", + "0550-01-07", + "0550-01-08", + "0550-01-09", + "0550-01-10", + "0550-01-11", + "0550-01-12", + "0550-01-13", + "0550-01-14", + "0550-01-15", + "0550-01-16", + "0550-01-17", + "0550-01-18", + "0550-01-19", + "0550-01-20" + ], + "xcalendar": "nanakshahi", "increasing": { "line": { "color": "#d3d3d3" @@ -218,7 +264,9 @@ "xaxis": { "rangeslider": { "visible": false - } + }, + "calendar": "islamic", + "title": "Islamic dates" }, "showlegend": false, "height": 450, diff --git a/test/image/mocks/gl2d_date_axes.json b/test/image/mocks/gl2d_date_axes.json index 4f6fff945b2..d0ff732579d 100644 --- a/test/image/mocks/gl2d_date_axes.json +++ b/test/image/mocks/gl2d_date_axes.json @@ -14,19 +14,20 @@ "2016-10-25 22:23:00.004" ], "y": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 + "0100-01-01", + "0100-01-02", + "0100-01-03", + "0100-01-04", + "0100-01-05", + "0100-01-06", + "0100-01-07", + "0100-01-08", + "0100-01-09", + "0100-01-10" ], "type": "scattergl", - "mode": "markers" + "mode": "markers", + "ycalendar": "taiwan" }, { "x": [ @@ -42,28 +43,35 @@ "2016-10-25 22:23:00.004" ], "y": [ - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19 + "0100-01-11", + "0100-01-12", + "0100-01-13", + "0100-01-14", + "0100-01-15", + "0100-01-16", + "0100-01-17", + "0100-01-18", + "0100-01-19", + "0100-01-20" ], "type": "scattergl", - "mode": "lines+markers" + "mode": "lines+markers", + "ycalendar": "taiwan" } ], "layout": { "xaxis": { "range": [ - 1477434179998, - 1477434180004 + "2559-10-25 22:22:59.998", + "2559-10-25 22:23:00.004" ], - "autorange": false + "autorange": false, + "calendar": "thai", + "title": "Thai" + }, + "yaxis": { + "calendar": "mayan", + "title": "Mayan" } } } \ No newline at end of file diff --git a/test/image/mocks/gl3d_scatter-date.json b/test/image/mocks/gl3d_scatter-date.json deleted file mode 100644 index 65bdf83a472..00000000000 --- a/test/image/mocks/gl3d_scatter-date.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "data":[ - { - "y":[1.1,1.16,1.16,1.2,1.18,1.31,1.3,1.34,1.38,1.39,1.35,1.37,1.39], - "z":[1.1,1.16,1.16,1.2,1.18,1.31,1.3,1.34,1.38,1.39,1.35,1.37,1.39], - "x":["2013-07","2013-08","2013-09","2013-10","2013-11","2013-12","2014-01","2014-02","2014-03","2014-04","2014-05","2014-06","2014-07"], - "type":"scatter3d" - } - ], - "layout": { - "title": "Date values" - } -} diff --git a/test/image/mocks/gl3d_world-cals.json b/test/image/mocks/gl3d_world-cals.json new file mode 100644 index 00000000000..f54d1ff49cf --- /dev/null +++ b/test/image/mocks/gl3d_world-cals.json @@ -0,0 +1,129 @@ +{ + "data": [ + { + "i": [ + 0, + 0, + 0, + 1 + ], + "j": [ + 1, + 2, + 3, + 2 + ], + "k": [ + 2, + 3, + 1, + 3 + ], + "facecolor": [ + "rgb(0, 0, 0)", + "rgb(255,0,0)", + "rgb(0,255,0)", + "rgb(0,0,255)" + ], + "type": "mesh3d", + "x": [ + "2001-04-01", + "2001-04i-01", + "2001-05-01", + "2001-04-01" + ], + "xcalendar": "chinese", + "y": [ + "0100-01-01", + "0100-01-01", + "0100-02-01", + "0100-03-01" + ], + "ycalendar": "taiwan", + "z": [ + "2550-01-01", + "2550-03-01", + "2550-01-01", + "2550-02-01" + ], + "zcalendar": "thai" + }, + { + "x": [ + "2001-04-01", + "2001-04-01", + "2001-05-01", + "2001-05-01" + ], + "xcalendar": "chinese", + "y": [ + "0100-01-01", + "0100-03-01", + "0100-01-01", + "0100-03-01" + ], + "ycalendar": "taiwan", + "z": [ + "2550-01-01", + "2550-03-01", + "2550-03-01", + "2550-01-01" + ], + "zcalendar": "thai", + "type": "scatter3d" + }, + { + "x": [ + "2001-04-01", + "2001-05-01" + ], + "xcalendar": "chinese", + "y": [ + "0100-01-01", + "0100-03-01" + ], + "ycalendar": "taiwan", + "z": [ + [ + "2550-01-25", + "2550-02-10" + ], + [ + "2550-02-10", + "2550-01-25" + ] + ], + "zcalendar": "thai", + "type": "surface", + "showscale": false, + "surfacecolor": [[0, 1],[2, 3]] + } + ], + "layout": { + "scene": { + "xaxis": { + "title": "taiwan", + "calendar": "taiwan", + "type": "date" + }, + "yaxis": { + "title": "gregorian", + "type": "date" + }, + "zaxis": { + "title": "chinese", + "calendar": "chinese", + "type": "date" + }, + "camera": { + "eye": { + "x": -1.8, + "y": 1.38, + "z": 0.75 + } + } + }, + "width": 800, + "height": 700 + } +} \ No newline at end of file diff --git a/test/image/mocks/world-cals.json b/test/image/mocks/world-cals.json new file mode 100644 index 00000000000..8d06668a3d5 --- /dev/null +++ b/test/image/mocks/world-cals.json @@ -0,0 +1,443 @@ +{ + "data": [ + { + "type": "bar", + "x": [ + "1686-01-01", + "1687-01-01", + "1688-01-01" + ], + "y": [ + -1, + -2, + -3 + ], + "xcalendar": "coptic", + "name": "coptic bars", + "uid": "74ab24" + }, + { + "type": "box", + "x": [ + "1798-01-01", + "1798-04-04", + "1798-05-05", + "1798-05-05", + "1798-07-05", + "1798-07-22", + "1799-01-01" + ], + "orientation": "h", + "xcalendar": "discworld", + "y0": -3, + "name": "discworld box", + "uid": "8c8a7d" + }, + { + "type": "contour", + "x": [ + "1961-07-01", + "1961-09-01", + "1961-11-01" + ], + "y": [ + 3, + 4, + 5 + ], + "z": [ + [ + 1, + 2, + 1 + ], + [ + 3, + 4, + 3 + ], + [ + 1, + 2, + 3 + ] + ], + "xcalendar": "ethiopian", + "name": "ethiopian contour", + "showscale": false, + "uid": "506ec8", + "zmin": 1, + "zmax": 4, + "contours": { + "coloring": "fill", + "showlines": true, + "start": 1.5, + "size": 0.5, + "end": 3.505 + } + }, + { + "type": "heatmap", + "x": [ + "5730-08-01", + "5730-10-01", + "5730-12-01" + ], + "y": [ + 3, + 4, + 5 + ], + "z": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ] + ], + "xcalendar": "hebrew", + "name": "hebrew heatmap", + "showscale": false, + "uid": "2f8d3f", + "zmin": 1, + "zmax": 4 + }, + { + "type": "histogram2d", + "x": [ + "1389-01-01", + "1389-01-01", + "1389-01-01", + "1389-03-01", + "1389-03-01", + "1389-06-01" + ], + "y": [ + 1.5, + 1.5, + 1.5, + 2.5, + 2.5, + 1.5 + ], + "xcalendar": "islamic", + "name": "islamic histogram2d", + "nbinsx": 3, + "nbinsy": 2, + "showscale": false, + "uid": "beb4e5", + "xbins": { + "start": "1388-12-14 12:00", + "end": "1389-06-14 12:00", + "size": "M2" + }, + "ybins": { + "start": 1, + "end": 3, + "size": 1 + }, + "zmin": 0, + "zmax": 3 + }, + { + "type": "histogram2dcontour", + "x": [ + "1970-01-01", + "1970-01-01", + "1970-01-01", + "1970-03-01", + "1970-03-01", + "1970-06-01" + ], + "y": [ + 1.5, + 1.5, + 1.5, + 2.5, + 2.5, + 1.5 + ], + "xcalendar": "julian", + "name": "julian histogram2dcontour", + "nbinsx": 3, + "nbinsy": 2, + "showscale": false, + "uid": "d01089", + "xbins": { + "start": "1969-10-16 12:00", + "end": "1970-08-16 12:00", + "size": "M2" + }, + "ybins": { + "start": 0, + "end": 4, + "size": 1 + }, + "zmin": 0, + "zmax": 3, + "colorscale": [ + [ + 0, + "rgb(220,220,220)" + ], + [ + 0.2, + "rgb(245,195,157)" + ], + [ + 0.4, + "rgb(245,160,105)" + ], + [ + 1, + "rgb(178,10,28)" + ] + ], + "contours": { + "coloring": "fill", + "showlines": true, + "start": 0.5, + "size": 0.5, + "end": 2.505 + } + }, + { + "type": "scatter", + "x": [ + "5157-01-01", + "5157-02-01", + "5157-03-01", + "5157-04-01" + ], + "y": [ + 4, + 2, + 3, + 1 + ], + "xcalendar": "mayan", + "name": "mayan", + "uid": "af70cc" + }, + { + "type": "scatter", + "x": [ + "0502-09-01", + "0502-10-01", + "0502-11-01", + "0502-12-01" + ], + "y": [ + 4, + 2, + 3, + 1 + ], + "xcalendar": "nanakshahi", + "name": "nanakshahi", + "uid": "de4e9d" + }, + { + "type": "scatter", + "x": [ + "2027-10-01", + "2027-11-01", + "2027-12-01", + "2028-01-01" + ], + "y": [ + 4, + 2, + 3, + 1 + ], + "xcalendar": "nepali", + "name": "nepali", + "uid": "5d4950" + }, + { + "type": "scatter", + "x": [ + "1350-01-01", + "1350-02-01", + "1350-03-01", + "1350-04-01" + ], + "y": [ + 4, + 2, + 3, + 1 + ], + "xcalendar": "persian", + "name": "persian", + "uid": "d13636" + }, + { + "type": "scatter", + "x": [ + "1350-03-01", + "1350-04-01", + "1350-05-01", + "1350-06-01" + ], + "y": [ + 4, + 2, + 3, + 1 + ], + "xcalendar": "jalali", + "name": "jalali", + "uid": "612738" + }, + { + "type": "scatter", + "x": [ + "0060-08-01", + "0060-09-01", + "0060-10-01", + "0060-11-01" + ], + "y": [ + 4, + 2, + 3, + 1 + ], + "xcalendar": "taiwan", + "name": "taiwan", + "uid": "24ac23" + }, + { + "type": "scatter", + "x": [ + "2514-10-01", + "2514-11-01", + "2514-12-01", + "2515-01-01" + ], + "y": [ + 4, + 2, + 3, + 1 + ], + "xcalendar": "thai", + "name": "thai", + "uid": "830f65" + }, + { + "type": "histogram", + "y": [ + "5200-01-01", + "5200-01-01", + "5200-01-02", + "5200-01-03", + "5200-01-03", + "5200-01-03", + "5201-04-01", + "5199-01-01" + ], + "xaxis": "x2", + "yaxis": "y2", + "ycalendar": "hebrew", + "name": "hebrew histogram
jalali filtered", + "transforms": [ + { + "type": "filter", + "operation": "[]", + "calendar": "jalali", + "value": [ + "0818-08", + "0819-06" + ], + "target": "y" + } + ], + "uid": "cf8dad", + "ybins": { + "start": "5200-12-29 12:00", + "end": "5200-01-03 12:00", + "size": 86400000 + } + } + ], + "layout": { + "xaxis": { + "calendar": "ummalqura", + "title": "ummalqura axis", + "domain": [ + 0, + 0.8 + ], + "type": "date", + "range": [ + "1388-08-05 16:38:43.314", + "1392-01-29 11:07:58.7911" + ], + "autorange": true + }, + "xaxis2": { + "domain": [ + 0.9, + 1 + ], + "range": [ + 0, + 3.1578947368421053 + ], + "autorange": true + }, + "yaxis2": { + "anchor": "x2", + "calendar": "nepali", + "title": "nepali axis", + "type": "date", + "range": [ + "1496-11-30 12:00", + "1496-12-03 12:00" + ], + "autorange": true + }, + "width": 1200, + "height": 400, + "annotations": [ + { + "x": "1389-06-05", + "y": 4, + "text": "ethiopian
contour", + "showarrow": false + }, + { + "x": "1390-01-10", + "y": 4, + "text": "hebrew
heatmap", + "showarrow": false + }, + { + "x": "1389-08-01", + "y": 2, + "text": "islamic
hist2d", + "showarrow": false + }, + { + "x": "1390-03-15", + "y": 2, + "text": "julian
hist2dcontour", + "showarrow": false + } + ], + "yaxis": { + "type": "linear", + "range": [ + -3.5, + 5 + ], + "autorange": true + } + } +} \ No newline at end of file diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index eeac5f13c9a..fd99e6c6881 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -237,7 +237,8 @@ describe('annotations autosize', function() { expect(fullLayout.xaxis.range).toBeCloseToArray(x, PREC, '- xaxis'); expect(fullLayout.yaxis.range).toBeCloseToArray(y, PREC, '- yaxis'); - expect(dateAx.range.map(dateAx.r2l)).toBeCloseToArray(x2.map(dateAx.r2l), PRECX2, 'xaxis2 ' + dateAx.range); + expect(Lib.simpleMap(dateAx.range, dateAx.r2l)) + .toBeCloseToArray(Lib.simpleMap(x2, dateAx.r2l), PRECX2, 'xaxis2 ' + dateAx.range); expect(fullLayout.yaxis2.range).toBeCloseToArray(y2, PRECY2, 'yaxis2'); expect(fullLayout.xaxis3.range).toBeCloseToArray(x3, PREC, 'xaxis3'); expect(fullLayout.yaxis3.range).toBeCloseToArray(y3, PREC, 'yaxis3'); diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 7378c50b376..d08273bfa58 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -385,6 +385,34 @@ describe('Test axes', function() { expect(layoutOut.yaxis2.gridcolor) .toEqual(tinycolor.mix('#444', bgColor, frac).toRgbString()); }); + + it('should inherit calendar from the layout', function() { + layoutOut.calendar = 'nepali'; + layoutIn = { + calendar: 'nepali', + xaxis: {type: 'date'}, + yaxis: {type: 'date'} + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.calendar).toBe('nepali'); + expect(layoutOut.yaxis.calendar).toBe('nepali'); + }); + + it('should allow its own calendar', function() { + layoutOut.calendar = 'nepali'; + layoutIn = { + calendar: 'nepali', + xaxis: {type: 'date', calendar: 'coptic'}, + yaxis: {type: 'date', calendar: 'thai'} + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.calendar).toBe('coptic'); + expect(layoutOut.yaxis.calendar).toBe('thai'); + }); }); describe('categoryorder', function() { diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index 75f8bc01926..8edf4e1d608 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -27,14 +27,14 @@ describe('Bar.supplyDefaults', function() { it('should set visible to false when x and y are empty', function() { traceIn = {}; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.visible).toBe(false); traceIn = { x: [], y: [] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.visible).toBe(false); }); @@ -42,27 +42,27 @@ describe('Bar.supplyDefaults', function() { traceIn = { x: [] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.visible).toBe(false); traceIn = { x: [], y: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.visible).toBe(false); traceIn = { y: [] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.visible).toBe(false); traceIn = { x: [1, 2, 3], y: [] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.visible).toBe(false); }); @@ -70,7 +70,7 @@ describe('Bar.supplyDefaults', function() { traceIn = { y: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.base).toBeUndefined(); expect(traceOut.offset).toBeUndefined(); expect(traceOut.width).toBeUndefined(); @@ -81,7 +81,7 @@ describe('Bar.supplyDefaults', function() { width: -1, y: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.width).toBeUndefined(); }); @@ -89,7 +89,7 @@ describe('Bar.supplyDefaults', function() { traceIn = { y: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.textposition).toBe('none'); expect(traceOut.texfont).toBeUndefined(); expect(traceOut.insidetexfont).toBeUndefined(); @@ -116,6 +116,32 @@ describe('Bar.supplyDefaults', function() { expect(traceOut.insidetextfont).not.toBe(traceOut.textfont); expect(traceOut.outsidetexfont).toBeUndefined(); }); + + it('should inherit layout.calendar', function() { + traceIn = { + x: [1, 2, 3], + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('islamic'); + expect(traceOut.ycalendar).toBe('islamic'); + }); + + it('should take its own calendars', function() { + traceIn = { + x: [1, 2, 3], + y: [1, 2, 3], + xcalendar: 'coptic', + ycalendar: 'ethiopian' + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + expect(traceOut.xcalendar).toBe('coptic'); + expect(traceOut.ycalendar).toBe('ethiopian'); + }); }); describe('heatmap calc / setPositions', function() { diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index 5c357f26ad5..c292fc356c9 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -25,7 +25,7 @@ describe('Test boxes', function() { x: [], y: [] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.visible).toBe(false); }); @@ -33,27 +33,27 @@ describe('Test boxes', function() { traceIn = { x: [] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.visible).toBe(false); traceIn = { x: [], y: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.visible).toBe(false); traceIn = { y: [] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.visible).toBe(false); traceIn = { x: [1, 2, 3], y: [] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.visible).toBe(false); }); @@ -61,14 +61,14 @@ describe('Test boxes', function() { traceIn = { y: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.orientation).toBe('v'); traceIn = { x: [1, 1, 1], y: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.orientation).toBe('v'); }); @@ -76,11 +76,37 @@ describe('Test boxes', function() { traceIn = { x: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor); + supplyDefaults(traceIn, traceOut, defaultColor, {}); expect(traceOut.orientation).toBe('h'); }); + it('should inherit layout.calendar', function() { + traceIn = { + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('islamic'); + expect(traceOut.ycalendar).toBe('islamic'); + }); + + it('should take its own calendars', function() { + traceIn = { + y: [1, 2, 3], + xcalendar: 'coptic', + ycalendar: 'ethiopian' + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('coptic'); + expect(traceOut.ycalendar).toBe('ethiopian'); + }); + }); }); diff --git a/test/jasmine/tests/contour_test.js b/test/jasmine/tests/contour_test.js index 0cc1fd0c1be..2f41da50af1 100644 --- a/test/jasmine/tests/contour_test.js +++ b/test/jasmine/tests/contour_test.js @@ -51,6 +51,36 @@ describe('contour defaults', function() { supplyDefaults(traceIn, traceOut, defaultColor, layout); expect(traceOut.autocontour).toBe(true); }); + + it('should inherit layout.calendar', function() { + traceIn = { + x: [1, 2], + y: [1, 2], + z: [[1, 2], [3, 4]] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('islamic'); + expect(traceOut.ycalendar).toBe('islamic'); + }); + + it('should take its own calendars', function() { + traceIn = { + x: [1, 2], + y: [1, 2], + z: [[1, 2], [3, 4]], + xcalendar: 'coptic', + ycalendar: 'ethiopian' + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('coptic'); + expect(traceOut.ycalendar).toBe('ethiopian'); + }); }); describe('contour makeColorMap', function() { diff --git a/test/jasmine/tests/finance_test.js b/test/jasmine/tests/finance_test.js index 992beda676b..f6e292cf4dc 100644 --- a/test/jasmine/tests/finance_test.js +++ b/test/jasmine/tests/finance_test.js @@ -331,6 +331,42 @@ describe('finance charts defaults:', function() { expect(out1.layout.xaxis.rangeslider).toBeDefined(); expect(out1._fullLayout.xaxis.rangeslider.visible).toBe(false); }); + + it('pushes layout.calendar to all output traces', function() { + var trace0 = Lib.extendDeep({}, mock0, { + type: 'ohlc' + }); + + var trace1 = Lib.extendDeep({}, mock1, { + type: 'candlestick' + }); + + var out = _supply([trace0, trace1], {calendar: 'nanakshahi'}); + + + out._fullData.forEach(function(fullTrace) { + expect(fullTrace.xcalendar).toBe('nanakshahi'); + }); + }); + + it('accepts a calendar per input trace', function() { + var trace0 = Lib.extendDeep({}, mock0, { + type: 'ohlc', + xcalendar: 'hebrew' + }); + + var trace1 = Lib.extendDeep({}, mock1, { + type: 'candlestick', + xcalendar: 'julian' + }); + + var out = _supply([trace0, trace1], {calendar: 'nanakshahi'}); + + + out._fullData.forEach(function(fullTrace, i) { + expect(fullTrace.xcalendar).toBe(i < 2 ? 'hebrew' : 'julian'); + }); + }); }); describe('finance charts calc transforms:', function() { @@ -652,10 +688,10 @@ describe('finance charts calc transforms:', function() { var out = _calc([trace0, trace1]); - var x0 = out[0].x.map(Lib.dateTime2ms); + var x0 = Lib.simpleMap(out[0].x, Lib.dateTime2ms); expect(x0[x0.length - 2] - x0[0]).toEqual(1); - var x2 = out[2].x.map(Lib.dateTime2ms); + var x2 = Lib.simpleMap(out[2].x, Lib.dateTime2ms); expect(x2[x2.length - 2] - x2[0]).toEqual(1); expect(out[1].x).toEqual([]); diff --git a/test/jasmine/tests/gl3daxes_test.js b/test/jasmine/tests/gl3daxes_test.js index 1e4b61912bd..fad1f8c222b 100644 --- a/test/jasmine/tests/gl3daxes_test.js +++ b/test/jasmine/tests/gl3daxes_test.js @@ -75,5 +75,35 @@ describe('Test Gl3dAxes', function() { checkKeys(expected[axis], layoutOut[axis]); }); }); + + it('should inherit layout.calendar', function() { + layoutIn = { + xaxis: {type: 'date'}, + yaxis: {type: 'date'}, + zaxis: {type: 'date'} + }; + options.calendar = 'taiwan'; + + supplyLayoutDefaults(layoutIn, layoutOut, options); + + expect(layoutOut.xaxis.calendar).toBe('taiwan'); + expect(layoutOut.yaxis.calendar).toBe('taiwan'); + expect(layoutOut.zaxis.calendar).toBe('taiwan'); + }); + + it('should accept its own calendar', function() { + layoutIn = { + xaxis: {type: 'date', calendar: 'hebrew'}, + yaxis: {type: 'date', calendar: 'ummalqura'}, + zaxis: {type: 'date', calendar: 'discworld'} + }; + options.calendar = 'taiwan'; + + supplyLayoutDefaults(layoutIn, layoutOut, options); + + expect(layoutOut.xaxis.calendar).toBe('hebrew'); + expect(layoutOut.yaxis.calendar).toBe('ummalqura'); + expect(layoutOut.zaxis.calendar).toBe('discworld'); + }); }); }); diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index 61ccf74f4bf..9141ff5edf1 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -125,6 +125,35 @@ describe('heatmap supplyDefaults', function() { expect(traceOut.ygap).toBe(undefined); }); + it('should inherit layout.calendar', function() { + traceIn = { + x: [1, 2], + y: [1, 2], + z: [[1, 2], [3, 4]] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('islamic'); + expect(traceOut.ycalendar).toBe('islamic'); + }); + + it('should take its own calendars', function() { + traceIn = { + x: [1, 2], + y: [1, 2], + z: [[1, 2], [3, 4]], + xcalendar: 'coptic', + ycalendar: 'ethiopian' + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('coptic'); + expect(traceOut.ycalendar).toBe('ethiopian'); + }); }); describe('heatmap convertColumnXYZ', function() { diff --git a/test/jasmine/tests/histogram2d_test.js b/test/jasmine/tests/histogram2d_test.js index ae3922708bf..e6e421fad84 100644 --- a/test/jasmine/tests/histogram2d_test.js +++ b/test/jasmine/tests/histogram2d_test.js @@ -18,7 +18,7 @@ describe('Test histogram2d', function() { it('should set zsmooth to false when zsmooth is empty', function() { traceIn = {}; - supplyDefaults(traceIn, traceOut, {}); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.zsmooth).toBe(false); }); @@ -26,13 +26,13 @@ describe('Test histogram2d', function() { traceIn = { zsmooth: 'fast' }; - supplyDefaults(traceIn, traceOut, {}); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.zsmooth).toBe('fast'); }); it('should set xgap and ygap to 0 when xgap and ygap are empty', function() { traceIn = {}; - supplyDefaults(traceIn, traceOut, {}); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.xgap).toBe(0); expect(traceOut.ygap).toBe(0); }); @@ -42,7 +42,7 @@ describe('Test histogram2d', function() { xgap: 10, ygap: 5 }; - supplyDefaults(traceIn, traceOut, {}); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.xgap).toBe(10); expect(traceOut.ygap).toBe(5); }); @@ -53,11 +53,39 @@ describe('Test histogram2d', function() { ygap: 5, zsmooth: 'best' }; - supplyDefaults(traceIn, traceOut, {}); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.xgap).toBe(undefined); expect(traceOut.ygap).toBe(undefined); }); + + it('should inherit layout.calendar', function() { + traceIn = { + x: [1, 2, 3], + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, '', {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('islamic'); + expect(traceOut.ycalendar).toBe('islamic'); + }); + + it('should take its own calendars', function() { + traceIn = { + x: [1, 2, 3], + y: [1, 2, 3], + xcalendar: 'coptic', + ycalendar: 'ethiopian' + }; + supplyDefaults(traceIn, traceOut, '', {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('coptic'); + expect(traceOut.ycalendar).toBe('ethiopian'); + }); }); diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index 2fd1140df7e..684d2320939 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -20,13 +20,13 @@ describe('Test histogram', function() { traceIn = { x: [] }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); traceIn = { y: [] }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); }); @@ -36,7 +36,7 @@ describe('Test histogram', function() { x: [], y: [1, 2, 2] }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); traceIn = { @@ -44,7 +44,7 @@ describe('Test histogram', function() { x: [1, 2, 2], y: [] }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); traceIn = { @@ -52,7 +52,7 @@ describe('Test histogram', function() { x: [], y: [] }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); traceIn = { @@ -60,7 +60,7 @@ describe('Test histogram', function() { x: [], y: [1, 2, 2] }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); }); @@ -68,14 +68,14 @@ describe('Test histogram', function() { traceIn = { x: [1, 2, 2] }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.orientation).toBe('v'); traceIn = { x: [1, 2, 2], y: [1, 2, 2] }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.orientation).toBe('v'); }); @@ -83,7 +83,7 @@ describe('Test histogram', function() { traceIn = { y: [1, 2, 2] }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.orientation).toBe('h'); }); @@ -100,13 +100,13 @@ describe('Test histogram', function() { size: 1 } }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.autobinx).toBeUndefined(); traceIn = { x: [1, 2, 2] }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.autobinx).toBeUndefined(); }); @@ -119,16 +119,41 @@ describe('Test histogram', function() { size: 1 } }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.autobiny).toBeUndefined(); traceIn = { y: [1, 2, 2] }; - supplyDefaults(traceIn, traceOut); + supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.autobiny).toBeUndefined(); }); + it('should inherit layout.calendar', function() { + traceIn = { + x: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, '', {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + // size axis calendar is weird, but *might* be able to happen if + // we're using histfunc=min or max (does this work?) + expect(traceOut.xcalendar).toBe('islamic'); + expect(traceOut.ycalendar).toBe('islamic'); + }); + + it('should take its own calendars', function() { + traceIn = { + x: [1, 2, 3], + xcalendar: 'coptic', + ycalendar: 'nepali' + }; + supplyDefaults(traceIn, traceOut, '', {calendar: 'islamic'}); + + expect(traceOut.xcalendar).toBe('coptic'); + expect(traceOut.ycalendar).toBe('nepali'); + }); }); diff --git a/test/jasmine/tests/lib_date_test.js b/test/jasmine/tests/lib_date_test.js index a73f79c7b25..9df1ecfd99a 100644 --- a/test/jasmine/tests/lib_date_test.js +++ b/test/jasmine/tests/lib_date_test.js @@ -1,5 +1,10 @@ var isNumeric = require('fast-isnumeric'); + var Lib = require('@src/lib'); +var calComponent = require('@src/components/calendars'); + +// use only the parts of world-calendars that we've imported for our tests +var calendars = require('@src/components/calendars/calendars'); describe('dates', function() { 'use strict'; @@ -222,6 +227,118 @@ describe('dates', function() { expect(Lib.ms2DateTime(Lib.dateTime2ms(v[0]), v[1])).toBe(v[2], v); }); }); + + it('should work right with inputs beyond our precision', function() { + for(var i = -1; i <= 1; i += 0.001) { + var tenths = Math.round(i * 10), + base = i < -0.05 ? '1969-12-31 23:59:59.99' : '1970-01-01 00:00:00.00', + expected = (base + String(tenths + 200).substr(1)) + .replace(/0+$/, '') + .replace(/ 00:00:00[\.]$/, ''); + expect(Lib.ms2DateTime(i)).toBe(expected, i); + } + }); + }); + + describe('world calendar inputs', function() { + it('should give the right values near epoch zero', function() { + [ + [undefined, '1970-01-01'], + ['gregorian', '1970-01-01'], + ['chinese', '1969-11-24'], + ['coptic', '1686-04-23'], + ['discworld', '1798-12-27'], + ['ethiopian', '1962-04-23'], + ['hebrew', '5730-10-23'], + ['islamic', '1389-10-22'], + ['julian', '1969-12-19'], + ['mayan', '5156-07-05'], + ['nanakshahi', '0501-10-19'], + ['nepali', '2026-09-17'], + ['persian', '1348-10-11'], + ['jalali', '1348-10-11'], + ['taiwan', '0059-01-01'], + ['thai', '2513-01-01'], + ['ummalqura', '1389-10-23'] + ].forEach(function(v) { + var calendar = v[0], + dateStr = v[1]; + expect(Lib.ms2DateTime(0, 0, calendar)).toBe(dateStr, calendar); + expect(Lib.dateTime2ms(dateStr, calendar)).toBe(0, calendar); + + var expected_p1ms = dateStr + ' 00:00:00.0001', + expected_1s = dateStr + ' 00:00:01', + expected_1m = dateStr + ' 00:01', + expected_1h = dateStr + ' 01:00', + expected_lastinstant = dateStr + ' 23:59:59.9999'; + + var oneSec = 1000, + oneMin = 60 * oneSec, + oneHour = 60 * oneMin, + lastInstant = 24 * oneHour - 0.1; + + expect(Lib.ms2DateTime(0.1, 0, calendar)).toBe(expected_p1ms, calendar); + expect(Lib.ms2DateTime(oneSec, 0, calendar)).toBe(expected_1s, calendar); + expect(Lib.ms2DateTime(oneMin, 0, calendar)).toBe(expected_1m, calendar); + expect(Lib.ms2DateTime(oneHour, 0, calendar)).toBe(expected_1h, calendar); + expect(Lib.ms2DateTime(lastInstant, 0, calendar)).toBe(expected_lastinstant, calendar); + + expect(Lib.dateTime2ms(expected_p1ms, calendar)).toBe(0.1, calendar); + expect(Lib.dateTime2ms(expected_1s, calendar)).toBe(oneSec, calendar); + expect(Lib.dateTime2ms(expected_1m, calendar)).toBe(oneMin, calendar); + expect(Lib.dateTime2ms(expected_1h, calendar)).toBe(oneHour, calendar); + expect(Lib.dateTime2ms(expected_lastinstant, calendar)).toBe(lastInstant, calendar); + }); + }); + + it('should contain canonical ticks sundays, ranges for all calendars', function() { + var calList = Object.keys(calendars.calendars).filter(function(v) { + return v !== 'gregorian'; + }); + + var canonicalTick = calComponent.CANONICAL_TICK, + canonicalSunday = calComponent.CANONICAL_SUNDAY, + dfltRange = calComponent.DFLTRANGE; + expect(Object.keys(canonicalTick).length).toBe(calList.length); + expect(Object.keys(canonicalSunday).length).toBe(calList.length); + expect(Object.keys(dfltRange).length).toBe(calList.length); + + calList.forEach(function(calendar) { + expect(Lib.dateTime2ms(canonicalTick[calendar], calendar)).toBeDefined(calendar); + var sunday = Lib.dateTime2ms(canonicalSunday[calendar], calendar); + // convert back implicitly with gregorian calendar + expect(Lib.formatDate(sunday, '%A')).toBe('Sunday', calendar); + + expect(Lib.dateTime2ms(dfltRange[calendar][0], calendar)).toBeDefined(calendar); + expect(Lib.dateTime2ms(dfltRange[calendar][1], calendar)).toBeDefined(calendar); + }); + }); + + it('should handle Chinese intercalary months correctly', function() { + var intercalaryDates = [ + '1995-08i-01', + '1995-08i-29', + '1984-10i-15', + '2023-02i-29' + ]; + intercalaryDates.forEach(function(v) { + var ms = Lib.dateTime2ms(v, 'chinese'); + expect(Lib.ms2DateTime(ms, 0, 'chinese')).toBe(v); + + // should also work without leading zeros + var vShort = v.replace(/-0/g, '-'); + expect(Lib.dateTime2ms(vShort, 'chinese')).toBe(ms, vShort); + }); + + var badIntercalaryDates = [ + '1995-07i-01', + '1995-08i-30', + '1995-09i-01' + ]; + badIntercalaryDates.forEach(function(v) { + expect(Lib.dateTime2ms(v, 'chinese')).toBeUndefined(v); + }); + }); }); describe('cleanDate', function() { @@ -279,6 +396,51 @@ describe('dates', function() { }); }); + describe('incrementMonth', function() { + it('should include Chinese intercalary months', function() { + var start = '1995-06-01'; + var expected = [ + '1995-07-01', + '1995-08-01', + '1995-08i-01', + '1995-09-01', + '1995-10-01', + '1995-11-01', + '1995-12-01', + '1996-01-01' + ]; + var tick = Lib.dateTime2ms(start, 'chinese'); + expected.forEach(function(v) { + tick = Lib.incrementMonth(tick, 1, 'chinese'); + expect(tick).toBe(Lib.dateTime2ms(v, 'chinese'), v); + }); + }); + + it('should increment years even over leap years', function() { + var start = '1995-06-01'; + var expected = [ + '1996-06-01', + '1997-06-01', + '1998-06-01', + '1999-06-01', + '2000-06-01', + '2001-06-01', + '2002-06-01', + '2003-06-01', + '2004-06-01', + '2005-06-01', + '2006-06-01', + '2007-06-01', + '2008-06-01' + ]; + var tick = Lib.dateTime2ms(start, 'chinese'); + expected.forEach(function(v) { + tick = Lib.incrementMonth(tick, 12, 'chinese'); + expect(tick).toBe(Lib.dateTime2ms(v, 'chinese'), v); + }); + }); + }); + describe('isJSDate', function() { it('should return true for any Date object but not the equivalent numbers', function() { [ diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index eb0983b4857..2420632f3bf 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -39,7 +39,7 @@ describe('plot schema', function() { assertPlotSchema( function(attr) { if(isValObject(attr)) { - expect(ROLES.indexOf(attr.role) !== -1).toBe(true); + expect(ROLES.indexOf(attr.role) !== -1).toBe(true, attr); } } ); @@ -188,6 +188,17 @@ describe('plot schema', function() { }); }); + it('should work with registered components', function() { + expect(plotSchema.traces.scatter.attributes.xcalendar.valType).toEqual('enumerated'); + expect(plotSchema.traces.scatter3d.attributes.zcalendar.valType).toEqual('enumerated'); + + expect(plotSchema.layout.layoutAttributes.calendar.valType).toEqual('enumerated'); + expect(plotSchema.layout.layoutAttributes.xaxis.calendar.valType).toEqual('enumerated'); + expect(plotSchema.layout.layoutAttributes.scene.xaxis.calendar.valType).toEqual('enumerated'); + + expect(plotSchema.transforms.filter.attributes.calendar.valType).toEqual('enumerated'); + }); + it('should list correct defs', function() { expect(plotSchema.defs.valObjects).toBeDefined(); diff --git a/test/jasmine/tests/range_selector_test.js b/test/jasmine/tests/range_selector_test.js index cae2373e79b..1a306e4b24d 100644 --- a/test/jasmine/tests/range_selector_test.js +++ b/test/jasmine/tests/range_selector_test.js @@ -9,6 +9,7 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var getRectCenter = require('../assets/get_rect_center'); var mouseEvent = require('../assets/mouse_event'); +var setConvert = require('@src/plots/cartesian/set_convert'); describe('range selector defaults:', function() { @@ -16,7 +17,7 @@ describe('range selector defaults:', function() { var handleDefaults = RangeSelector.handleDefaults; - function supply(containerIn, containerOut) { + function supply(containerIn, containerOut, calendar) { containerOut.domain = [0, 1]; var layout = { @@ -25,7 +26,7 @@ describe('range selector defaults:', function() { var counterAxes = ['yaxis']; - handleDefaults(containerIn, containerOut, layout, counterAxes); + handleDefaults(containerIn, containerOut, layout, counterAxes, calendar); } it('should set \'visible\' to false when no buttons are present', function() { @@ -93,7 +94,7 @@ describe('range selector defaults:', function() { }; var containerOut = {}; - supply(containerIn, containerOut, {}, []); + supply(containerIn, containerOut); expect(containerOut.rangeselector.visible).toBe(true); expect(containerOut.rangeselector.buttons).toEqual([ @@ -113,7 +114,7 @@ describe('range selector defaults:', function() { }; var containerOut = {}; - supply(containerIn, containerOut, {}, []); + supply(containerIn, containerOut); expect(containerOut.rangeselector.buttons).toEqual([{ step: 'all', @@ -175,6 +176,53 @@ describe('range selector defaults:', function() { expect(containerOut.rangeselector.x).toEqual(0.5); expect(containerOut.rangeselector.y).toBeCloseTo(0.87); }); + + it('should not allow month/year todate with calendars other than Gregorian', function() { + var containerIn = { + rangeselector: { + buttons: [{ + step: 'year', + count: 1, + stepmode: 'todate' + }, { + step: 'month', + count: 6, + stepmode: 'todate' + }, { + step: 'day', + count: 1, + stepmode: 'todate' + }, { + step: 'hour', + count: 1, + stepmode: 'todate' + }] + } + }; + var containerOut; + function getStepmode(button) { return button.stepmode; } + + containerOut = {}; + supply(containerIn, containerOut); + + expect(containerOut.rangeselector.buttons.map(getStepmode)).toEqual([ + 'todate', 'todate', 'todate', 'todate' + ]); + + containerOut = {}; + supply(containerIn, containerOut, 'gregorian'); + + expect(containerOut.rangeselector.buttons.map(getStepmode)).toEqual([ + 'todate', 'todate', 'todate', 'todate' + ]); + + containerOut = {}; + supply(containerIn, containerOut, 'chinese'); + + expect(containerOut.rangeselector.buttons.map(getStepmode)).toEqual([ + 'backward', 'backward', 'todate', 'todate' + ]); + }); }); describe('range selector getUpdateObject:', function() { @@ -185,14 +233,20 @@ describe('range selector getUpdateObject:', function() { expect(update['xaxis.range[1]']).toEqual(range1); } + function setupAxis(opts) { + var axisOut = Lib.extendFlat({type: 'date'}, opts); + setConvert(axisOut); + return axisOut; + } + // buttonLayout: {step, stepmode, count} // range0out: expected resulting range[0] (input is always '1948-01-01') // range1: input range[1], expected to also be the output function assertUpdateCase(buttonLayout, range0out, range1) { - var axisLayout = { + var axisLayout = setupAxis({ _name: 'xaxis', range: ['1948-01-01', range1] - }; + }); var update = getUpdateObject(axisLayout, buttonLayout); @@ -280,10 +334,10 @@ describe('range selector getUpdateObject:', function() { }); it('should return update object (reset case)', function() { - var axisLayout = { + var axisLayout = setupAxis({ _name: 'xaxis', range: ['1948-01-01', '2015-11-30'] - }; + }); var buttonLayout = { step: 'all' @@ -375,10 +429,10 @@ describe('range selector getUpdateObject:', function() { }); it('should return update object with correct axis names', function() { - var axisLayout = { + var axisLayout = setupAxis({ _name: 'xaxis5', range: ['1948-01-01', '2015-11-30'] - }; + }); var buttonLayout = { step: 'month', diff --git a/test/jasmine/tests/scatter3d_test.js b/test/jasmine/tests/scatter3d_test.js index 1b165b7cff6..7518da16166 100644 --- a/test/jasmine/tests/scatter3d_test.js +++ b/test/jasmine/tests/scatter3d_test.js @@ -8,9 +8,9 @@ describe('Scatter3D defaults', function() { var defaultColor = '#d3d3d3'; - function _supply(traceIn) { + function _supply(traceIn, layoutEdits) { var traceOut = { visible: true }, - layout = { _dataLength: 1 }; + layout = Lib.extendFlat({ _dataLength: 1 }, layoutEdits); Scatter3D.supplyDefaults(traceIn, traceOut, defaultColor, layout); return traceOut; @@ -65,4 +65,27 @@ describe('Scatter3D defaults', function() { expect(out.marker.color).toBe(color); expect(out.marker.line.color).toBe(Color.defaultLine); }); + + it('should inherit layout.calendar', function() { + var out = _supply(base, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(out.xcalendar).toBe('islamic'); + expect(out.ycalendar).toBe('islamic'); + expect(out.zcalendar).toBe('islamic'); + }); + + it('should take its own calendars', function() { + var traceIn = Lib.extendFlat({}, base, { + xcalendar: 'coptic', + ycalendar: 'ethiopian', + zcalendar: 'mayan' + }); + var out = _supply(traceIn, {calendar: 'islamic'}); + + expect(out.xcalendar).toBe('coptic'); + expect(out.ycalendar).toBe('ethiopian'); + expect(out.zcalendar).toBe('mayan'); + }); }); diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index 8c948779459..1dcb3abbee3 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -92,6 +92,32 @@ describe('Test scatter', function() { supplyDefaults(traceIn, traceOut, defaultColor, layout); expect(traceOut.hoveron).toBe('points'); }); + + it('should inherit layout.calendar', function() { + traceIn = { + x: [1, 2, 3], + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('islamic'); + expect(traceOut.ycalendar).toBe('islamic'); + }); + + it('should take its own calendars', function() { + traceIn = { + x: [1, 2, 3], + y: [1, 2, 3], + xcalendar: 'coptic', + ycalendar: 'ethiopian' + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + expect(traceOut.xcalendar).toBe('coptic'); + expect(traceOut.ycalendar).toBe('ethiopian'); + }); }); describe('isBubble', function() { diff --git a/test/jasmine/tests/surface_test.js b/test/jasmine/tests/surface_test.js index 73905747f22..96aa26f0193 100644 --- a/test/jasmine/tests/surface_test.js +++ b/test/jasmine/tests/surface_test.js @@ -150,5 +150,32 @@ describe('Test surface', function() { expect(traceOut.cmin).toBeUndefined(); expect(traceOut.cmax).toBeUndefined(); }); + + it('should inherit layout.calendar', function() { + traceIn = { + z: [[1, 2, 3], [2, 1, 2]] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('islamic'); + expect(traceOut.ycalendar).toBe('islamic'); + expect(traceOut.zcalendar).toBe('islamic'); + }); + + it('should take its own calendars', function() { + var traceIn = { + z: [[1, 2, 3], [2, 1, 2]], + xcalendar: 'coptic', + ycalendar: 'ethiopian', + zcalendar: 'mayan' + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.xcalendar).toBe('coptic'); + expect(traceOut.ycalendar).toBe('ethiopian'); + expect(traceOut.zcalendar).toBe('mayan'); + }); }); }); diff --git a/test/jasmine/tests/updatemenus_test.js b/test/jasmine/tests/updatemenus_test.js index ef2128feaa2..37a3a415e2d 100644 --- a/test/jasmine/tests/updatemenus_test.js +++ b/test/jasmine/tests/updatemenus_test.js @@ -698,7 +698,7 @@ describe('update menus interactions', function() { // must compare with a tolerance as the exact result // is browser/font dependent (via getBBox) - expect(Math.abs(actualWidth - width)).toBeLessThan(12); + expect(Math.abs(actualWidth - width)).toBeLessThan(16); // height is determined by 'fontsize', // so no such tolerance is needed