From 5233664813e4fd38a09df4147a742fe8d0040723 Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 9 Oct 2020 20:06:41 -0400 Subject: [PATCH 1/9] handle monthly tickformat with weekly auto dtick - issue 5207 --- src/plots/cartesian/axes.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index c9cdf51cec0..bbc3ba8798c 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -634,6 +634,7 @@ axes.calcTicks = function calcTicks(ax, opts) { var definedDelta; if(isPeriod && tickformat) { var noDtick = ax._dtickInit !== ax.dtick; + var prevDtick = ax.dtick; if( !(/%[fLQsSMX]/.test(tickformat)) // %f: microseconds as a decimal number [000000, 999999] @@ -706,11 +707,16 @@ axes.calcTicks = function calcTicks(ax, opts) { ) ax.dtick = 'M12'; } } + + if(prevDtick !== ax.dtick) { + // move tick0 back + ax.tick0 = axes.tickIncrement(ax.tick0, prevDtick, !axrev, ax.calendar); + + // redo first tick + ax._tmin = axes.tickFirst(ax, opts); + } } - var maxTicks = Math.max(1000, ax._length || 0); - var tickVals = []; - var xPrevious = null; var x = ax._tmin; if(ax.rangebreaks && ax._tick0Init !== ax.tick0) { @@ -726,6 +732,9 @@ axes.calcTicks = function calcTicks(ax, opts) { x = axes.tickIncrement(x, ax.dtick, !axrev, ax.calendar); } + var maxTicks = Math.max(1000, ax._length || 0); + var tickVals = []; + var xPrevious = null; for(; (axrev) ? (x >= endTick) : (x <= endTick); x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar) From f8a6e625290d499dbc04db47ebfd8f36d33b6ae2 Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 9 Oct 2020 21:19:34 -0400 Subject: [PATCH 2/9] add jasmine tests --- test/jasmine/tests/axes_test.js | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index bd1c5c4a1cd..267a1c65e64 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -5681,6 +5681,53 @@ describe('Test axes', function() { }); }); + [ + { + range: ['2019-12-10', '2020-01-10'], + positions: ['2019-12-16 12:00', '2020-01-10'], + labels: ['2019-Dec', ' '] + }, + { + range: ['2019-12-20', '2020-01-20'], + positions: ['2019-12-20', '2020-01-16 12:00'], + labels: [' ', '2020-Jan'] + }, + { + range: ['2020-01-20', '2019-12-20'], + positions: ['2020-01-20', '2020-01-16 12:00'], + labels: [' ', '2020-Jan'] + } + ].forEach(function(t) { + it('should position labels with monthly tickformat when auto dtick is weekly | range:' + t.range, function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [ + '2020-01-01', + '2020-01-02' + ], + mode: 'lines+text', + text: [ + 'Jan 01', + 'Jan 02' + ] + }], + layout: { + width: 600, + xaxis: { + range: t.range, + ticklabelmode: 'period', + tickformat: '%Y-%b' + } + } + }) + .then(function() { + _assert('', t.positions, t.labels); + }) + .catch(failTest) + .then(done); + }); + }); + [ { range: ['2020-12-15', '2084-12-15'], From a4cb02beb936e32a0cdc9b48b4cf73acc81ae856 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 14 Oct 2020 09:16:07 -0400 Subject: [PATCH 3/9] maintain ax.tick0 --- src/plots/cartesian/axes.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index bbc3ba8798c..a26a558a4d4 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -708,12 +708,17 @@ axes.calcTicks = function calcTicks(ax, opts) { } } - if(prevDtick !== ax.dtick) { + if(prevDtick !== ax.dtick && ax._tick0Init !== ax.tick0) { + var prevTick0 = ax.tick0; + // move tick0 back ax.tick0 = axes.tickIncrement(ax.tick0, prevDtick, !axrev, ax.calendar); // redo first tick ax._tmin = axes.tickFirst(ax, opts); + + // maintain tick0 value + ax.tick0 = prevTick0; } } From ecb506729d2e29c9c9ba212a941287f5056d1fb6 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 14 Oct 2020 11:13:00 -0400 Subject: [PATCH 4/9] fix tick0 bug introduced in previous commit a4cb02b --- src/plots/cartesian/axes.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index a26a558a4d4..b4b6bd19a56 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -709,16 +709,11 @@ axes.calcTicks = function calcTicks(ax, opts) { } if(prevDtick !== ax.dtick && ax._tick0Init !== ax.tick0) { - var prevTick0 = ax.tick0; - // move tick0 back ax.tick0 = axes.tickIncrement(ax.tick0, prevDtick, !axrev, ax.calendar); // redo first tick ax._tmin = axes.tickFirst(ax, opts); - - // maintain tick0 value - ax.tick0 = prevTick0; } } From 5d61cef1140b7de94858e339cabb808e7e7df6f6 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 14 Oct 2020 15:15:36 -0400 Subject: [PATCH 5/9] drop tick0 tweaking logic --- src/plots/cartesian/axes.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index b4b6bd19a56..d9dd565e4e4 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -634,7 +634,6 @@ axes.calcTicks = function calcTicks(ax, opts) { var definedDelta; if(isPeriod && tickformat) { var noDtick = ax._dtickInit !== ax.dtick; - var prevDtick = ax.dtick; if( !(/%[fLQsSMX]/.test(tickformat)) // %f: microseconds as a decimal number [000000, 999999] @@ -707,14 +706,6 @@ axes.calcTicks = function calcTicks(ax, opts) { ) ax.dtick = 'M12'; } } - - if(prevDtick !== ax.dtick && ax._tick0Init !== ax.tick0) { - // move tick0 back - ax.tick0 = axes.tickIncrement(ax.tick0, prevDtick, !axrev, ax.calendar); - - // redo first tick - ax._tmin = axes.tickFirst(ax, opts); - } } var x = ax._tmin; From 77a51d34b3f5e0b06fed94e61758df4225f210da Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 14 Oct 2020 15:31:32 -0400 Subject: [PATCH 6/9] move the logic above firstTick so that we know ax.dtick by that time --- src/plots/cartesian/axes.js | 26 +++++++++++++------------- test/jasmine/tests/axes_test.js | 6 +++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index d9dd565e4e4..e4667f10801 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -613,19 +613,6 @@ axes.calcTicks = function calcTicks(ax, opts) { var minRange = Math.min(rng[0], rng[1]); var maxRange = Math.max(rng[0], rng[1]); - // find the first tick - ax._tmin = axes.tickFirst(ax, opts); - - // No visible ticks? Quit. - // I've only seen this on category axes with all categories off the edge. - if((ax._tmin < startTick) !== axrev) return []; - - // return the full set of tick vals - if(ax.type === 'category' || ax.type === 'multicategory') { - endTick = (axrev) ? Math.max(-0.5, endTick) : - Math.min(ax._categories.length - 0.5, endTick); - } - var isDLog = (ax.type === 'log') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'L'); var isMDate = (ax.type === 'date') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'M'); @@ -708,6 +695,19 @@ axes.calcTicks = function calcTicks(ax, opts) { } } + // find the first tick + ax._tmin = axes.tickFirst(ax, opts); + + // No visible ticks? Quit. + // I've only seen this on category axes with all categories off the edge. + if((ax._tmin < startTick) !== axrev) return []; + + // return the full set of tick vals + if(ax.type === 'category' || ax.type === 'multicategory') { + endTick = (axrev) ? Math.max(-0.5, endTick) : + Math.min(ax._categories.length - 0.5, endTick); + } + var x = ax._tmin; if(ax.rangebreaks && ax._tick0Init !== ax.tick0) { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 267a1c65e64..7d8dba55967 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -5684,17 +5684,17 @@ describe('Test axes', function() { [ { range: ['2019-12-10', '2020-01-10'], - positions: ['2019-12-16 12:00', '2020-01-10'], + positions: ['2019-12-17 12:00', '2020-01-10'], labels: ['2019-Dec', ' '] }, { range: ['2019-12-20', '2020-01-20'], - positions: ['2019-12-20', '2020-01-16 12:00'], + positions: ['2019-12-20', '2020-01-17 12:00'], labels: [' ', '2020-Jan'] }, { range: ['2020-01-20', '2019-12-20'], - positions: ['2020-01-20', '2020-01-16 12:00'], + positions: ['2020-01-20', '2020-01-17 12:00'], labels: [' ', '2020-Jan'] } ].forEach(function(t) { From c42358c128c7ea2b44b2531052ea9ac3f877aaf4 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 14 Oct 2020 15:45:47 -0400 Subject: [PATCH 7/9] move & refactor period dtick logic inside a function --- src/plots/cartesian/axes.js | 56 ++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index e4667f10801..78a9c1462c1 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -592,34 +592,12 @@ function nMonths(dtick) { return +(dtick.substring(1)); } -// calculate the ticks: text, values, positioning -// if ticks are set to automatic, determine the right values (tick0,dtick) -// 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, opts) { - axes.prepTicks(ax, opts); - var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts); - - // now that we've figured out the auto values for formatting - // in case we're missing some ticktext, we can break out for array ticks - if(ax.tickmode === 'array') return arrayTicks(ax); - - // add a tiny bit so we get ticks which may have rounded out - var exRng = expandRange(rng); - var startTick = exRng[0]; - var endTick = exRng[1]; - // check for reversed axis - var axrev = (rng[1] < rng[0]); - var minRange = Math.min(rng[0], rng[1]); - var maxRange = Math.max(rng[0], rng[1]); +function adjustPeriodDelta(ax) { // adjusts ax.dtick and returns definedDelta + var definedDelta; - var isDLog = (ax.type === 'log') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'L'); var isMDate = (ax.type === 'date') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'M'); - var tickformat = axes.getTickFormat(ax); - var isPeriod = ax.ticklabelmode === 'period'; - var definedDelta; - if(isPeriod && tickformat) { + if(tickformat) { var noDtick = ax._dtickInit !== ax.dtick; if( !(/%[fLQsSMX]/.test(tickformat)) @@ -695,6 +673,34 @@ axes.calcTicks = function calcTicks(ax, opts) { } } + return definedDelta; +} + +// calculate the ticks: text, values, positioning +// if ticks are set to automatic, determine the right values (tick0,dtick) +// 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, opts) { + axes.prepTicks(ax, opts); + var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts); + + // now that we've figured out the auto values for formatting + // in case we're missing some ticktext, we can break out for array ticks + if(ax.tickmode === 'array') return arrayTicks(ax); + + // add a tiny bit so we get ticks which may have rounded out + var exRng = expandRange(rng); + var startTick = exRng[0]; + var endTick = exRng[1]; + // check for reversed axis + var axrev = (rng[1] < rng[0]); + var minRange = Math.min(rng[0], rng[1]); + var maxRange = Math.max(rng[0], rng[1]); + + var isDLog = (ax.type === 'log') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'L'); + var isPeriod = ax.ticklabelmode === 'period'; + var definedDelta = isPeriod ? adjustPeriodDelta(ax) : undefined; + // find the first tick ax._tmin = axes.tickFirst(ax, opts); From b78a80bb03950b30f18e0e01a3cae442ddc0df6c Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 14 Oct 2020 15:50:58 -0400 Subject: [PATCH 8/9] move and refactor period tick positioning logic inside a function --- src/plots/cartesian/axes.js | 170 ++++++++++++++++++------------------ 1 file changed, 86 insertions(+), 84 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 78a9c1462c1..0833acbc981 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -676,6 +676,90 @@ function adjustPeriodDelta(ax) { // adjusts ax.dtick and returns definedDelta return definedDelta; } +function positionPeriodTicks(tickVals, ax, definedDelta) { + for(var i = 0; i < tickVals.length; i++) { + var v = tickVals[i].value; + + var a = i; + var b = i + 1; + if(i < tickVals.length - 1) { + a = i; + b = i + 1; + } else if(i > 0) { + a = i - 1; + b = i; + } else { + a = i; + b = i; + } + + var A = tickVals[a].value; + var B = tickVals[b].value; + var actualDelta = Math.abs(B - A); + var delta = definedDelta || actualDelta; + var periodLength = 0; + + if(delta >= ONEMINYEAR) { + if(actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) { + periodLength = actualDelta; + } else { + periodLength = ONEAVGYEAR; + } + } else if(definedDelta === ONEAVGQUARTER && delta >= ONEMINQUARTER) { + if(actualDelta >= ONEMINQUARTER && actualDelta <= ONEMAXQUARTER) { + periodLength = actualDelta; + } else { + periodLength = ONEAVGQUARTER; + } + } else if(delta >= ONEMINMONTH) { + if(actualDelta >= ONEMINMONTH && actualDelta <= ONEMAXMONTH) { + periodLength = actualDelta; + } else { + periodLength = ONEAVGMONTH; + } + } else if(definedDelta === ONEWEEK && delta >= ONEWEEK) { + periodLength = ONEWEEK; + } else if(delta >= ONEDAY) { + periodLength = ONEDAY; + } else if(definedDelta === HALFDAY && delta >= HALFDAY) { + periodLength = HALFDAY; + } else if(definedDelta === ONEHOUR && delta >= ONEHOUR) { + periodLength = ONEHOUR; + } + + var inBetween; + if(periodLength >= actualDelta) { + // ensure new label positions remain between ticks + periodLength = actualDelta; + inBetween = true; + } + + var endPeriod = v + periodLength; + if(ax.rangebreaks && periodLength > 0) { + var nAll = 84; // highly divisible 7 * 12 + var n = 0; + for(var c = 0; c < nAll; c++) { + var r = (c + 0.5) / nAll; + if(ax.maskBreaks(v * (1 - r) + r * endPeriod) !== BADNUM) n++; + } + periodLength *= n / nAll; + + if(!periodLength) { + tickVals[i].drop = true; + } + + if(inBetween && actualDelta > ONEWEEK) periodLength = actualDelta; // center monthly & longer periods + } + + if( + periodLength > 0 || // not instant + i === 0 // taking care first tick added + ) { + tickVals[i].periodX = v + periodLength / 2; + } + } +} + // calculate the ticks: text, values, positioning // if ticks are set to automatic, determine the right values (tick0,dtick) // in any case, set tickround to # of digits to round tick labels to, @@ -759,91 +843,9 @@ axes.calcTicks = function calcTicks(ax, opts) { }); } - var i; - if(isPeriod) { - for(i = 0; i < tickVals.length; i++) { - var v = tickVals[i].value; - - var a = i; - var b = i + 1; - if(i < tickVals.length - 1) { - a = i; - b = i + 1; - } else if(i > 0) { - a = i - 1; - b = i; - } else { - a = i; - b = i; - } - - var A = tickVals[a].value; - var B = tickVals[b].value; - var actualDelta = Math.abs(B - A); - var delta = definedDelta || actualDelta; - var periodLength = 0; - - if(delta >= ONEMINYEAR) { - if(actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) { - periodLength = actualDelta; - } else { - periodLength = ONEAVGYEAR; - } - } else if(definedDelta === ONEAVGQUARTER && delta >= ONEMINQUARTER) { - if(actualDelta >= ONEMINQUARTER && actualDelta <= ONEMAXQUARTER) { - periodLength = actualDelta; - } else { - periodLength = ONEAVGQUARTER; - } - } else if(delta >= ONEMINMONTH) { - if(actualDelta >= ONEMINMONTH && actualDelta <= ONEMAXMONTH) { - periodLength = actualDelta; - } else { - periodLength = ONEAVGMONTH; - } - } else if(definedDelta === ONEWEEK && delta >= ONEWEEK) { - periodLength = ONEWEEK; - } else if(delta >= ONEDAY) { - periodLength = ONEDAY; - } else if(definedDelta === HALFDAY && delta >= HALFDAY) { - periodLength = HALFDAY; - } else if(definedDelta === ONEHOUR && delta >= ONEHOUR) { - periodLength = ONEHOUR; - } - - var inBetween; - if(periodLength >= actualDelta) { - // ensure new label positions remain between ticks - periodLength = actualDelta; - inBetween = true; - } - - var endPeriod = v + periodLength; - if(ax.rangebreaks && periodLength > 0) { - var nAll = 84; // highly divisible 7 * 12 - var n = 0; - for(var c = 0; c < nAll; c++) { - var r = (c + 0.5) / nAll; - if(ax.maskBreaks(v * (1 - r) + r * endPeriod) !== BADNUM) n++; - } - periodLength *= n / nAll; - - if(!periodLength) { - tickVals[i].drop = true; - } - - if(inBetween && actualDelta > ONEWEEK) periodLength = actualDelta; // center monthly & longer periods - } - - if( - periodLength > 0 || // not instant - i === 0 // taking care first tick added - ) { - tickVals[i].periodX = v + periodLength / 2; - } - } - } + if(isPeriod) positionPeriodTicks(tickVals, ax, definedDelta); + var i; if(ax.rangebreaks) { var flip = ax._id.charAt(0) === 'y'; From bcf2a71343cd248652fbcb561fc294130c3967ef Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 15 Oct 2020 15:07:30 -0400 Subject: [PATCH 9/9] move adjustPeriodDelta logic to prepTicks and discard sunday/monday tweaks on monthly dticks --- src/plots/cartesian/axes.js | 30 +++++++++++++++++++++++++----- test/jasmine/tests/axes_test.js | 6 +++--- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 0833acbc981..099848120ac 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -575,6 +575,10 @@ axes.prepTicks = function(ax, opts) { } } + if(ax.ticklabelmode === 'period') { + adjustPeriodDelta(ax); + } + // check for missing tick0 if(!ax.tick0) { ax.tick0 = (ax.type === 'date') ? '2000-01-01' : 0; @@ -592,10 +596,16 @@ function nMonths(dtick) { return +(dtick.substring(1)); } -function adjustPeriodDelta(ax) { // adjusts ax.dtick and returns definedDelta +function adjustPeriodDelta(ax) { // adjusts ax.dtick and sets ax._definedDelta var definedDelta; - var isMDate = (ax.type === 'date') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'M'); + function mDate() { + return !( + isNumeric(ax.dtick) || + ax.dtick.charAt(0) !== 'M' + ); + } + var isMDate = mDate(); var tickformat = axes.getTickFormat(ax); if(tickformat) { var noDtick = ax._dtickInit !== ax.dtick; @@ -673,7 +683,13 @@ function adjustPeriodDelta(ax) { // adjusts ax.dtick and returns definedDelta } } - return definedDelta; + isMDate = mDate(); + if(isMDate && ax.tick0 === ax._dowTick0) { + // discard Sunday/Monday tweaks + ax.tick0 = ax._rawTick0; + } + + ax._definedDelta = definedDelta; } function positionPeriodTicks(tickVals, ax, definedDelta) { @@ -783,7 +799,6 @@ axes.calcTicks = function calcTicks(ax, opts) { var isDLog = (ax.type === 'log') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'L'); var isPeriod = ax.ticklabelmode === 'period'; - var definedDelta = isPeriod ? adjustPeriodDelta(ax) : undefined; // find the first tick ax._tmin = axes.tickFirst(ax, opts); @@ -843,7 +858,7 @@ axes.calcTicks = function calcTicks(ax, opts) { }); } - if(isPeriod) positionPeriodTicks(tickVals, ax, definedDelta); + if(isPeriod) positionPeriodTicks(tickVals, ax, ax._definedDelta); var i; if(ax.rangebreaks) { @@ -1030,11 +1045,16 @@ axes.autoTicks = function(ax, roughDTick) { // 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. var tickformat = axes.getTickFormat(ax); + var isPeriod = ax.ticklabelmode === 'period'; + if(isPeriod) ax._rawTick0 = ax.tick0; + if(/%[uVW]/.test(tickformat)) { ax.tick0 = Lib.dateTick0(ax.calendar, 2); // Monday } else { ax.tick0 = Lib.dateTick0(ax.calendar, 1); // Sunday } + + if(isPeriod) ax._dowTick0 = ax.tick0; } else if(roughX2 > ONEHOUR) { ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24); } else if(roughX2 > ONEMIN) { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 7d8dba55967..267a1c65e64 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -5684,17 +5684,17 @@ describe('Test axes', function() { [ { range: ['2019-12-10', '2020-01-10'], - positions: ['2019-12-17 12:00', '2020-01-10'], + positions: ['2019-12-16 12:00', '2020-01-10'], labels: ['2019-Dec', ' '] }, { range: ['2019-12-20', '2020-01-20'], - positions: ['2019-12-20', '2020-01-17 12:00'], + positions: ['2019-12-20', '2020-01-16 12:00'], labels: [' ', '2020-Jan'] }, { range: ['2020-01-20', '2019-12-20'], - positions: ['2020-01-20', '2020-01-17 12:00'], + positions: ['2020-01-20', '2020-01-16 12:00'], labels: [' ', '2020-Jan'] } ].forEach(function(t) {