diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 594ebdbaf49..b1cae96206d 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -8,6 +8,8 @@ 'use strict'; +var isNumeric = require('fast-isnumeric'); + var Registry = require('../../registry'); var Lib = require('../../lib'); @@ -21,6 +23,9 @@ var handleCategoryOrderDefaults = require('./category_order_defaults'); var handleLineGridDefaults = require('./line_grid_defaults'); var setConvert = require('./set_convert'); +var DAY_OF_WEEK = require('./constants').WEEKDAY_PATTERN; +var HOUR = require('./constants').HOUR_PATTERN; + /** * options: object containing: * @@ -155,10 +160,67 @@ function rangebreaksDefaults(itemIn, itemOut, containerOut) { if(enabled) { var bnds = coerce('bounds'); - if(bnds && bnds.length >= 2) { - if(bnds.length > 2) { - itemOut.bounds = itemOut.bounds.slice(0, 2); + var dfltPattern = ''; + var i, q; + if(bnds.length === 2) { + for(i = 0; i < 2; i++) { + q = indexOfDay(bnds[i]); + if(q) { + dfltPattern = DAY_OF_WEEK; + break; + } + } + } + var pattern = coerce('pattern', dfltPattern); + if(pattern === DAY_OF_WEEK) { + for(i = 0; i < 2; i++) { + q = indexOfDay(bnds[i]); + if(q) { + // convert to integers i.e 'Sunday' --> 0 + itemOut.bounds[i] = bnds[i] = q - 1; + } + } + } + if(pattern) { + // ensure types and ranges + for(i = 0; i < 2; i++) { + q = bnds[i]; + switch(pattern) { + case DAY_OF_WEEK : + if(!isNumeric(q)) { + itemOut.enabled = false; + return; + } + q = +q; + + if( + q !== Math.floor(q) || // don't accept fractional days for mow + q < 0 || q >= 7 + ) { + itemOut.enabled = false; + return; + } + // use number + itemOut.bounds[i] = bnds[i] = q; + break; + + case HOUR : + if(!isNumeric(q)) { + itemOut.enabled = false; + return; + } + q = +q; + + if(q < 0 || q > 24) { // accept 24 + itemOut.enabled = false; + return; + } + // use number + itemOut.bounds[i] = bnds[i] = q; + break; + } + } } if(containerOut.autorange === false) { @@ -175,8 +237,6 @@ function rangebreaksDefaults(itemIn, itemOut, containerOut) { return; } } - - coerce('pattern'); } else { var values = coerce('values'); @@ -189,3 +249,21 @@ function rangebreaksDefaults(itemIn, itemOut, containerOut) { } } } + +// these numbers are one more than what bounds would be mapped to +var dayStrToNum = { + sun: 1, + mon: 2, + tue: 3, + wed: 4, + thu: 5, + fri: 6, + sat: 7 +}; + +function indexOfDay(v) { + if(typeof v !== 'string') return; + return dayStrToNum[ + v.substr(0, 3).toLowerCase() + ]; +} diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 2d219abd45c..194e2ea4ffa 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -280,16 +280,18 @@ module.exports = { pattern: { valType: 'enumerated', values: [DAY_OF_WEEK, HOUR, ''], - dflt: '', role: 'info', editType: 'calc', description: [ 'Determines a pattern on the time line that generates breaks.', - 'If *' + DAY_OF_WEEK + '* - Sunday-based weekday as a decimal number [0, 6].', + 'If *' + DAY_OF_WEEK + '* - days of the week in English e.g. \'Sunday\' or `\sun\`', + '(matching is case-insensitive and considers only the first three characters),', + 'as well as Sunday-based integers between 0 and 6.', 'If *' + HOUR + '* - hour (24-hour clock) as decimal numbers between 0 and 24.', 'for more info.', 'Examples:', - '- { pattern: \'' + DAY_OF_WEEK + '\', bounds: [6, 0] }', + '- { pattern: \'' + DAY_OF_WEEK + '\', bounds: [6, 1] }', + ' or simply { bounds: [\'sat\', \'mon\'] }', ' breaks from Saturday to Monday (i.e. skips the weekends).', '- { pattern: \'' + HOUR + '\', bounds: [17, 8] }', ' breaks from 5pm to 8am (i.e. skips non-work hours).' diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 71e4c26595e..ad06ff73e53 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1135,6 +1135,99 @@ describe('Test axes', function() { expect(layoutOut.xaxis2.rangebreaks[0].pattern).toBe('day of week', 'coerced'); expect(layoutOut.xaxis3.rangebreaks[0].pattern).toBe(undefined, 'not coerce, using *values*'); }); + + it('should auto default rangebreaks.pattern to *day of week* when *bounds* include a weekday string and convert bounds to integer days', function() { + layoutIn = { + xaxis: {type: 'date', rangebreaks: [ + {bounds: ['Saturday', 'Monday']} + ]}, + xaxis2: {type: 'date', rangebreaks: [ + {bounds: ['sun', 'thu']}, + {bounds: ['mon', 'fri']}, + {bounds: ['tue', 'sat']}, + {bounds: ['wed', '-1']} + ]} + }; + layoutOut._subplots.xaxis.push('x2'); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.rangebreaks[0].pattern).toBe('day of week', 'complete Capital'); + expect(layoutOut.xaxis2.rangebreaks[0].pattern).toBe('day of week', '3-letter case'); + expect(layoutOut.xaxis2.rangebreaks[0].bounds[0]).toBe(0, 'convert sun'); + expect(layoutOut.xaxis2.rangebreaks[1].bounds[0]).toBe(1, 'convert mon'); + expect(layoutOut.xaxis2.rangebreaks[2].bounds[0]).toBe(2, 'convert tue'); + expect(layoutOut.xaxis2.rangebreaks[3].bounds[0]).toBe(3, 'convert wed'); + expect(layoutOut.xaxis2.rangebreaks[0].bounds[1]).toBe(4, 'convert thu'); + expect(layoutOut.xaxis2.rangebreaks[1].bounds[1]).toBe(5, 'convert fri'); + expect(layoutOut.xaxis2.rangebreaks[2].bounds[1]).toBe(6, 'convert sat'); + expect(layoutOut.xaxis2.rangebreaks[3].bounds[1]).toBe('-1', 'string'); + }); + + it('should validate inputs in respect to *day of week* pattern', function() { + layoutIn = { + xaxis: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: ['6', '0'] }]}, + xaxis2: {type: 'date', rangebreaks: [{bounds: ['Sunday'] }]}, + xaxis3: {type: 'date', rangebreaks: [{bounds: ['sun', 'mon', 'tue'] }]}, + xaxis4: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, '-1'] }]}, + xaxis5: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, '-.25'] }]}, + xaxis6: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, '7'] }]}, + xaxis7: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, '6.75'] }]}, + xaxis8: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, ''] }]}, + xaxis9: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, null] }]}, + xaxis10: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, false] }]}, + xaxis11: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, true] }]} + }; + layoutOut._subplots.xaxis.push('x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', 'x10', 'x11'); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.rangebreaks[0].enabled).toBe(true, 'valid'); + expect(layoutOut.xaxis.rangebreaks[0].bounds[0]).toBe(6, 'cast float to int'); + expect(layoutOut.xaxis.rangebreaks[0].bounds[1]).toBe(0, 'cast string to int'); + expect(layoutOut.xaxis2.rangebreaks[0].enabled).toBe(false, 'reject bounds.length < 2'); + expect(layoutOut.xaxis3.rangebreaks[0].enabled).toBe(true, 'do not reject bounds.length > 2'); + expect(layoutOut.xaxis3.rangebreaks[0].bounds.length).toBe(2, 'pick first two'); + expect(layoutOut.xaxis4.rangebreaks[0].enabled).toBe(false, 'reject bound < 0'); + expect(layoutOut.xaxis5.rangebreaks[0].enabled).toBe(false, 'reject bound < 0'); + expect(layoutOut.xaxis6.rangebreaks[0].enabled).toBe(false, 'reject bound >= 7'); + expect(layoutOut.xaxis7.rangebreaks[0].enabled).toBe(false, 'reject bound < 7 - not supported yet'); + expect(layoutOut.xaxis8.rangebreaks[0].enabled).toBe(false, 'reject blank string'); + expect(layoutOut.xaxis9.rangebreaks[0].enabled).toBe(false, 'reject null'); + expect(layoutOut.xaxis10.rangebreaks[0].enabled).toBe(false, 'reject false'); + expect(layoutOut.xaxis11.rangebreaks[0].enabled).toBe(false, 'reject true'); + }); + + it('should validate inputs in respect to *hour* pattern', function() { + layoutIn = { + xaxis: {type: 'date', rangebreaks: [{pattern: 'hour', bounds: ['24', '1e-3'] }]}, + xaxis2: {type: 'date', rangebreaks: [{pattern: 'hour', bounds: [1] }]}, + xaxis3: {type: 'date', rangebreaks: [{pattern: 'hour', bounds: [1, 2, 3] }]}, + xaxis4: {type: 'date', rangebreaks: [{pattern: 'hour', bounds: [1, '-1'] }]}, + xaxis5: {type: 'date', rangebreaks: [{pattern: 'hour', bounds: [1, '-.001'] }]}, + xaxis6: {type: 'date', rangebreaks: [{pattern: 'hour', bounds: [1, '24.001'] }]}, + xaxis7: {type: 'date', rangebreaks: [{pattern: 'hour', bounds: [1, '23.999'] }]}, + xaxis8: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, ''] }]}, + xaxis9: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, null] }]}, + xaxis10: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, false] }]}, + xaxis11: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, true] }]} + }; + layoutOut._subplots.xaxis.push('x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', 'x10', 'x11'); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.rangebreaks[0].enabled).toBe(true, 'valid'); + expect(layoutOut.xaxis.rangebreaks[0].bounds[0]).toBe(24, 'accept 24'); + expect(layoutOut.xaxis.rangebreaks[0].bounds[1]).toBe(0.001, 'cast string to float'); + expect(layoutOut.xaxis2.rangebreaks[0].enabled).toBe(false, 'reject bounds.length < 2'); + expect(layoutOut.xaxis3.rangebreaks[0].enabled).toBe(true, 'do not reject bounds.length > 2'); + expect(layoutOut.xaxis3.rangebreaks[0].bounds.length).toBe(2, 'pick first two'); + expect(layoutOut.xaxis4.rangebreaks[0].enabled).toBe(false, 'reject bound < 0'); + expect(layoutOut.xaxis5.rangebreaks[0].enabled).toBe(false, 'reject bound < 0'); + expect(layoutOut.xaxis6.rangebreaks[0].enabled).toBe(false, 'reject bound > 24'); + expect(layoutOut.xaxis7.rangebreaks[0].enabled).toBe(true, 'do not reject bound <= 24'); + expect(layoutOut.xaxis8.rangebreaks[0].enabled).toBe(false, 'reject blank string'); + expect(layoutOut.xaxis9.rangebreaks[0].enabled).toBe(false, 'reject null'); + expect(layoutOut.xaxis10.rangebreaks[0].enabled).toBe(false, 'reject false'); + expect(layoutOut.xaxis11.rangebreaks[0].enabled).toBe(false, 'reject true'); + }); }); describe('constraints relayout', function() {