Skip to content
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

Handle weekday strings in rangebreaks #4661

Merged
merged 9 commits into from
Mar 19, 2020
76 changes: 71 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,55 @@ 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)) q = Math.floor(q);

q = Math.floor(+q);
Copy link
Collaborator

Choose a reason for hiding this comment

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

redundant - and dangerous - q='' would be turned into 0 for example. We should only cast to number when the value has already passed isNumeric (but might be a numeric string).

Also I don't think we should floor, we should just discard values that aren't integers (ie enabled=false). For one thing, that way if we ever add support for fractional days it won't break any existing behavior.

Can't this numeric validation be pushed into the if(pattern === DAY_OF_WEEK) loop above, and only executed if we didn't already convert from a day name to integer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call. Fixed in 9a0416a.

if(!(q >= 0 && q < 7)) {
itemOut.enabled = false;
return;
}
// use int [0, 7)
itemOut.bounds[i] = bnds[i] = q;
break;

case HOUR :
if(!(q >= 0 && q <= 24)) { // accept 24
itemOut.enabled = false;
return;
}
// use float [0, 24]
itemOut.bounds[i] = bnds[i] = q;
break;
}
}
}

if(containerOut.autorange === false) {
Expand All @@ -175,8 +225,6 @@ function rangebreaksDefaults(itemIn, itemOut, containerOut) {
return;
}
}

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

Expand All @@ -189,3 +237,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
79 changes: 79 additions & 0 deletions test/jasmine/tests/axes_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,85 @@ 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.999', '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, '-.001'] }]},
xaxis6: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, '7'] }]},
xaxis7: {type: 'date', rangebreaks: [{pattern: 'day of week', bounds: [1, '6.999'] }]}
};
layoutOut._subplots.xaxis.push('x2', 'x3', 'x4', 'x5', 'x6', 'x7');
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(true, 'do not reject bound < 7');
});

it('should validate inputs in respect to *hour* pattern', function() {
layoutIn = {
xaxis: {type: 'date', rangebreaks: [{pattern: 'hour', bounds: ['23.999', '0'] }]},
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: 'hour', bounds: [1, '24'] }]}
};
layoutOut._subplots.xaxis.push('x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8');
supplyLayoutDefaults(layoutIn, layoutOut, fullData);

expect(layoutOut.xaxis.rangebreaks[0].enabled).toBe(true, 'valid');
expect(layoutOut.xaxis.rangebreaks[0].bounds[0]).toBe('23.999', 'do not cast float to int');
expect(layoutOut.xaxis.rangebreaks[0].bounds[1]).toBe('0', 'do not 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 > 24');
expect(layoutOut.xaxis7.rangebreaks[0].enabled).toBe(true, 'do not reject bound <= 24');
expect(layoutOut.xaxis8.rangebreaks[0].enabled).toBe(true, 'do not reject 24');
});
});

describe('constraints relayout', function() {
Expand Down