From 86f39830077269b37ea7f186671872237e2d7faf Mon Sep 17 00:00:00 2001 From: Francesco Stefanini Date: Sat, 27 Jan 2018 11:18:05 +0100 Subject: [PATCH] [Timeline] Year quarters (#3717) * timeline: add 'quarter' support * add unit test * update docs * misc * 'quarters' scale hidden by default --- docs/graph2d/index.html | 4 +- docs/timeline/index.html | 8 ++-- .../timeline/other/functionLabelFormats.html | 6 +++ lib/timeline/TimeStep.js | 48 +++++++++++++++++-- lib/timeline/optionsGraph2d.js | 4 ++ lib/timeline/optionsTimeline.js | 6 ++- test/TimeStep.test.js | 9 ++++ 7 files changed, 76 insertions(+), 9 deletions(-) diff --git a/docs/graph2d/index.html b/docs/graph2d/index.html index e27d6de19..925b64147 100644 --- a/docs/graph2d/index.html +++ b/docs/graph2d/index.html @@ -841,6 +841,7 @@

Timeline Options

weekday: 'ddd D', day: 'D', month: 'MMM', + quarter: '[Q]Q', year: 'YYYY' }, majorLabels: { @@ -851,6 +852,7 @@

Timeline Options

weekday: 'MMMM YYYY', day: 'MMMM YYYY', month: 'YYYY', + quarter: 'YYYY', year: '' } } @@ -1009,7 +1011,7 @@

Timeline Options

timeAxis.scale String none - Set a fixed scale for the time axis of the Timeline. Choose from 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'. Example usage: + Set a fixed scale for the time axis of the Timeline. Choose from 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'quarter', 'year'. Example usage:
var options = {
   timeAxis: {scale: 'minute', step: 5}
 }
diff --git a/docs/timeline/index.html b/docs/timeline/index.html index 1a6b7c3ce..e38411904 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -623,6 +623,7 @@

Configuration Options

day: 'D', week: 'w', month: 'MMM', + quarter: '[Q]Q', year: 'YYYY' }, majorLabels: { @@ -634,6 +635,7 @@

Configuration Options

day: 'MMMM YYYY', week: 'MMMM YYYY', month: 'YYYY', + quarter: 'YYYY', year: '' } } @@ -1127,7 +1129,7 @@

Configuration Options

function When moving items on the Timeline, they will be snapped to nice dates like full hours or days, depending on the current scale. The snap function can be replaced with a custom function, or can be set to null to disable snapping. The signature of the snap function is:
function snap(date: Date, scale: string, step: number) : Date or number
- The parameter scale can be can be 'millisecond', 'second', 'minute', 'hour', 'weekday, 'week', 'day, 'month, or 'year'. The parameter step is a number like 1, 2, 4, 5. + The parameter scale can be can be 'millisecond', 'second', 'minute', 'hour', 'weekday, 'week', 'day', 'month', 'quarter' or 'year'. The parameter step is a number like 1, 2, 4, 5. @@ -1171,7 +1173,7 @@

Configuration Options

timeAxis.scale String none - Set a fixed scale for the time axis of the Timeline. Choose from 'millisecond', 'second', 'minute', 'hour', 'weekday', 'week', 'day', 'month', 'year'. Example usage: + Set a fixed scale for the time axis of the Timeline. Choose from 'millisecond', 'second', 'minute', 'hour', 'weekday', 'week', 'day', 'month', 'quarter', 'year'. Example usage:
var options = {
   timeAxis: {scale: 'minute', step: 5}
 }
@@ -2153,7 +2155,7 @@

Grid Backgrounds

- Note: the 'week' scale is not included in the automatic zoom levels as its scale is not a direct logical successor of 'days' nor a logical predecessor of 'months' + Note: the 'week' scale is not included in the automatic zoom levels as its scale is not a direct logical successor of 'days' nor a logical predecessor of 'months'. Same goes for the 'quarter' scale which is not a direct logical successor of 'months' nor a logical predecessor of 'years'.

Examples:

diff --git a/examples/timeline/other/functionLabelFormats.html b/examples/timeline/other/functionLabelFormats.html index f8e442bda..8a7d69ac5 100644 --- a/examples/timeline/other/functionLabelFormats.html +++ b/examples/timeline/other/functionLabelFormats.html @@ -86,6 +86,9 @@ case 'month': divider = 1000 * 60 * 60 * 24 * 30; break; + case 'quarter': + divider = 1000 * 60 * 60 * 24 * 30 * 3; + break; case 'year': divider = 1000 * 60 * 60 * 24 * 365; break; @@ -120,6 +123,9 @@ case 'month': divider = 1000 * 60 * 60 * 24 * 30; break; + case 'quarter': + divider = 1000 * 60 * 60 * 24 * 30 * 3; + break; case 'year': divider = 1000 * 60 * 60 * 24 * 365; break; diff --git a/lib/timeline/TimeStep.js b/lib/timeline/TimeStep.js index 1b3c11112..70abd3ed8 100644 --- a/lib/timeline/TimeStep.js +++ b/lib/timeline/TimeStep.js @@ -75,6 +75,7 @@ TimeStep.FORMAT = { day: 'D', week: 'D', month: 'MMM', + quarter: 'MMM', year: 'YYYY' }, majorLabels: { @@ -86,6 +87,7 @@ TimeStep.FORMAT = { day: 'MMMM YYYY', week: 'MMMM YYYY', month: 'YYYY', + quarter: 'YYYY', year: '' } }; @@ -107,7 +109,7 @@ TimeStep.prototype.setMoment = function (moment) { /** * Set custom formatting for the minor an major labels of the TimeStep. * Both `minorLabels` and `majorLabels` are an Object with properties: - * 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'week', 'month', 'year'. + * 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'week', 'month', 'quarter', 'year'. * @param {{minorLabels: Object, majorLabels: Object}} format */ TimeStep.prototype.setFormat = function (format) { @@ -162,6 +164,7 @@ TimeStep.prototype.roundToMinor = function() { case 'year': this.current.year(this.step * Math.floor(this.current.year() / this.step)); this.current.month(0); + case 'quarter': this.current.month(0); // eslint-disable-line no-fallthrough case 'month': this.current.date(1); // eslint-disable-line no-fallthrough case 'week': // eslint-disable-line no-fallthrough case 'day': // eslint-disable-line no-fallthrough @@ -183,6 +186,7 @@ TimeStep.prototype.roundToMinor = function() { case 'day': this.current.subtract((this.current.date() - 1) % this.step, 'day'); break; case 'week': this.current.subtract(this.current.week() % this.step, 'week'); break; case 'month': this.current.subtract(this.current.month() % this.step, 'month'); break; + case 'quarter': this.current.subtract((this.current.quarter() - 1) % this.step, 'quarter'); break; case 'year': this.current.subtract(this.current.year() % this.step, 'year'); break; default: break; } @@ -240,6 +244,7 @@ TimeStep.prototype.next = function() { } break; case 'month': this.current.add(this.step, 'month'); break; + case 'quarter': this.current.add(this.step, 'quarter'); break; case 'year': this.current.add(this.step, 'year'); break; default: break; } @@ -255,6 +260,7 @@ TimeStep.prototype.next = function() { case 'day': if(this.current.date() < this.step+1) this.current.date(1); break; case 'week': if(this.current.week() < this.step) this.current.week(1); break; // week numbering starts at 1, not 0 case 'month': if(this.current.month() < this.step) this.current.month(0); break; + case 'quarter': if(this.current.quarter() < this.step+1) this.current.quarter(1); break; case 'year': break; // nothing to do for year default: break; } @@ -290,7 +296,7 @@ TimeStep.prototype.getCurrent = function() { * @param {{scale: string, step: number}} params * An object containing two properties: * - A string 'scale'. Choose from 'millisecond', 'second', - * 'minute', 'hour', 'weekday', 'day', 'week', 'month', 'year'. + * 'minute', 'hour', 'weekday', 'day', 'week', 'month', 'quarter, 'year'. * - A number 'step'. A step size, by default 1. * Choose for example 1, 2, 5, or 10. */ @@ -323,6 +329,7 @@ TimeStep.prototype.setMinimumStep = function(minimumStep) { //var b = asc + ds; var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); + var stepQuarter = (1000 * 60 * 60 * 24 * 30 * 3); var stepMonth = (1000 * 60 * 60 * 24 * 30); var stepDay = (1000 * 60 * 60 * 24); var stepHour = (1000 * 60 * 60); @@ -338,7 +345,7 @@ TimeStep.prototype.setMinimumStep = function(minimumStep) { if (stepYear*10 > minimumStep) {this.scale = 'year'; this.step = 10;} if (stepYear*5 > minimumStep) {this.scale = 'year'; this.step = 5;} if (stepYear > minimumStep) {this.scale = 'year'; this.step = 1;} - if (stepMonth*3 > minimumStep) {this.scale = 'month'; this.step = 3;} + if (stepQuarter > minimumStep) {this.scale = 'quarter'; this.step = 1;} if (stepMonth > minimumStep) {this.scale = 'month'; this.step = 1;} if (stepDay*7 > minimumStep) {this.scale = 'week'; this.step = 1;} if (stepDay*2 > minimumStep) {this.scale = 'day'; this.step = 2;} @@ -368,7 +375,7 @@ TimeStep.prototype.setMinimumStep = function(minimumStep) { * Static function * @param {Date} date the date to be snapped. * @param {string} scale Current scale, can be 'millisecond', 'second', - * 'minute', 'hour', 'weekday, 'day', 'week', 'month', 'year'. + * 'minute', 'hour', 'weekday, 'day', 'week', 'month', 'quarter', 'year'. * @param {number} step Current step (1, 2, 4, 5, ... * @return {Date} snappedDate */ @@ -385,6 +392,22 @@ TimeStep.snap = function(date, scale, step) { clone.seconds(0); clone.milliseconds(0); } + else if (scale == 'quarter') { + if ((clone.month() % 3 == 1 && clone.date() > 15) || clone.month() % 3 == 2) { + clone.date(1); + clone.month(Math.floor(clone.month() / 3) * 3); + clone.add(1, 'quarter'); + // important: first set Date to 1, after that change the month and the quarter. + } else { + clone.date(1); + clone.month(Math.floor(clone.month() / 3) * 3); + } + + clone.hours(0); + clone.minutes(0); + clone.seconds(0); + clone.milliseconds(0); + } else if (scale == 'month') { if (clone.date() > 15) { clone.date(1); @@ -495,6 +518,7 @@ TimeStep.prototype.isMajor = function() { if (this.switchedYear == true) { switch (this.scale) { case 'year': + case 'quarter': case 'month': case 'week': case 'weekday': @@ -551,6 +575,8 @@ TimeStep.prototype.isMajor = function() { return (date.date() == 1); case 'month': return (date.month() == 0); + case 'quarter': + return (date.quarter() == 1); case 'year': return false; default: @@ -665,6 +691,15 @@ TimeStep.prototype.getClassName = function() { return date.isSame(new Date(), 'month') ? ' vis-current-month' : ''; } + /** + * + * @param {Date} date + * @returns {String} + */ + function currentQuarter(date) { + return date.isSame(new Date(), 'quarter') ? ' vis-current-quarter' : ''; + } + /** * * @param {Date} date @@ -717,6 +752,11 @@ TimeStep.prototype.getClassName = function() { classNames.push(currentMonth(current)); classNames.push(even(current.month())); break; + case 'quarter': + classNames.push('vis-q' + current.quarter()); + classNames.push(currentQuarter(current)); + classNames.push(even(current.quarter())); + break; case 'year': classNames.push('vis-year' + current.year()); classNames.push(currentYear(current)); diff --git a/lib/timeline/optionsGraph2d.js b/lib/timeline/optionsGraph2d.js index 30fed2cf4..602465d5d 100644 --- a/lib/timeline/optionsGraph2d.js +++ b/lib/timeline/optionsGraph2d.js @@ -113,6 +113,7 @@ let allOptions = { weekday: {string,'undefined': 'undefined'}, day: {string,'undefined': 'undefined'}, month: {string,'undefined': 'undefined'}, + quarter: {string,'undefined': 'undefined'}, year: {string,'undefined': 'undefined'}, __type__: {object} }, @@ -124,6 +125,7 @@ let allOptions = { weekday: {string,'undefined': 'undefined'}, day: {string,'undefined': 'undefined'}, month: {string,'undefined': 'undefined'}, + quarter: {string,'undefined': 'undefined'}, year: {string,'undefined': 'undefined'}, __type__: {object} }, @@ -238,6 +240,7 @@ let configureOptions = { weekday: 'ddd D', day: 'D', month: 'MMM', + quarter: '[Q]Q', year: 'YYYY' }, majorLabels: { @@ -248,6 +251,7 @@ let configureOptions = { weekday: 'MMMM YYYY', day: 'MMMM YYYY', month: 'YYYY', + quarter: 'YYYY', year: '' } }, diff --git a/lib/timeline/optionsTimeline.js b/lib/timeline/optionsTimeline.js index 5f2a2f592..1d861896b 100644 --- a/lib/timeline/optionsTimeline.js +++ b/lib/timeline/optionsTimeline.js @@ -62,6 +62,7 @@ let allOptions = { day: {string,'undefined': 'undefined'}, week: {string,'undefined': 'undefined'}, month: {string,'undefined': 'undefined'}, + quarter: {string,'undefined': 'undefined'}, year: {string,'undefined': 'undefined'}, __type__: {object, 'function': 'function'} }, @@ -74,6 +75,7 @@ let allOptions = { day: {string,'undefined': 'undefined'}, week: {string,'undefined': 'undefined'}, month: {string,'undefined': 'undefined'}, + quarter: {string,'undefined': 'undefined'}, year: {string,'undefined': 'undefined'}, __type__: {object, 'function': 'function'} }, @@ -202,6 +204,7 @@ let configureOptions = { day: 'D', week: 'w', month: 'MMM', + quarter: '[Q]Q', year: 'YYYY' }, majorLabels: { @@ -213,6 +216,7 @@ let configureOptions = { day: 'MMMM YYYY', week: 'MMMM YYYY', month: 'YYYY', + quarter: 'YYYY', year: '' } }, @@ -257,7 +261,7 @@ let configureOptions = { start: '', //template: {'function': 'function'}, //timeAxis: { - // scale: ['millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'week', 'month', 'year'], + // scale: ['millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'week', 'month', 'quarter', 'year'], // step: [1, 1, 10, 1] //}, showTooltips: true, diff --git a/test/TimeStep.test.js b/test/TimeStep.test.js index 0410163d5..cca7e37fe 100644 --- a/test/TimeStep.test.js +++ b/test/TimeStep.test.js @@ -55,6 +55,15 @@ describe('TimeStep', function () { assert.equal(timestep.getCurrent().unix(), moment("2018-01-01T00:00:00.000").unix(), "should have the right value after a step"); }); + it('should perform the step with a specified scale (1 quarter)', function () { + var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5)); + timestep.setScale({ scale: 'quarter', step: 1 }); + timestep.start(); + assert.equal(timestep.getCurrent().unix(), moment("2017-01-01T00:00:00.000").unix(), "should have the right initial value"); + timestep.next(); + assert.equal(timestep.getCurrent().unix(), moment("2017-04-01T00:00:00.000").unix(), "should have the right value after a step"); + }); + it('should perform the step with a specified scale (1 month)', function () { var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5)); timestep.setScale({ scale: 'month', step: 1 });