Skip to content

Handle weekday strings in rangebreaks #4661

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 19, 2020
88 changes: 83 additions & 5 deletions src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

'use strict';

var isNumeric = require('fast-isnumeric');

var Registry = require('../../registry');
var Lib = require('../../lib');

Expand All @@ -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:
*
Expand Down Expand Up @@ -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) {
Expand All @@ -175,8 +237,6 @@ function rangebreaksDefaults(itemIn, itemOut, containerOut) {
return;
}
}

coerce('pattern');
} else {
var values = coerce('values');

Expand All @@ -189,3 +249,21 @@ function rangebreaksDefaults(itemIn, itemOut, containerOut) {
}
}
}

// these numbers are one more than what bounds would be mapped to
Copy link
Collaborator

Choose a reason for hiding this comment

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

clever way to avoid having to distinguish 0 from undefined :)

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()
];
}
8 changes: 5 additions & 3 deletions src/plots/cartesian/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).'
Expand Down
93 changes: 93 additions & 0 deletions test/jasmine/tests/axes_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down