diff --git a/draftlogs/8167_fix.md b/draftlogs/8167_fix.md new file mode 100644 index 00000000000..541c11ea856 --- /dev/null +++ b/draftlogs/8167_fix.md @@ -0,0 +1 @@ +- Fix `rangebreaks` when one break overlaps two other breaks, which hid the plot or drew later data over earlier data [[#8167](https://github.com/plotly/plotly.js/pull/8167)] \ No newline at end of file diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index b4ea95f235d..c268a267eed 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -774,22 +774,24 @@ module.exports = function setConvert(ax, fullLayout) { max = Lib.constrain(max, r0, r1); if(min === max) return; - var isNewBreak = true; - for(var j = 0; j < rangebreaksOut.length; j++) { - var brkj = rangebreaksOut[j]; + let merged; + for(let j = 0; j < rangebreaksOut.length; j++) { + const brkj = rangebreaksOut[j]; if(min < brkj.max && max >= brkj.min) { - if(min < brkj.min) { - brkj.min = min; - } - if(max > brkj.max) { - brkj.max = max; + if(merged) { + // The axis code sums the break lengths, so the located breaks must not overlap. + merged.min = Math.min(merged.min, brkj.min); + merged.max = Math.max(merged.max, brkj.max); + rangebreaksOut.splice(j, 1); + j--; + } else { + merged = brkj; + merged.min = Math.min(merged.min, min); + merged.max = Math.max(merged.max, max); } - isNewBreak = false; } } - if(isNewBreak) { - rangebreaksOut.push({min: min, max: max}); - } + if(!merged) rangebreaksOut.push({min, max}); }; for(i = 0; i < rangebreaksIn.length; i++) { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 38b959c36d0..0754bd80fa9 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -13,7 +13,7 @@ var Cartesian = require('../../../src/plots/cartesian'); var Axes = require('../../../src/plots/cartesian/axes'); var Fx = require('../../../src/components/fx'); var supplyLayoutDefaults = require('../../../src/plots/cartesian/layout_defaults'); -const {BADNUM, MINUS_SIGN, ONEDAY, ONEWEEK} = require('../../../src/constants/numerical'); +const {BADNUM, MINUS_SIGN, ONEDAY, ONEHOUR, ONEWEEK} = require('../../../src/constants/numerical'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); @@ -6062,6 +6062,63 @@ describe('Test axes', function() { }) .then(done, done.fail); }); + + it('should merge a holiday rangebreak that overlaps two located breaks', (done) => { + const assertBreaks = (msg, exp) => { + const xa = gd._fullLayout.xaxis; + const brks = xa._rangebreaks.map((brk) => [brk.min, brk.max]); + + for(let i = 1; i < brks.length; i++) { + expect(brks[i][0]) + .withContext(`${msg} | break ${i} starts before break ${i - 1} ends`) + .not.toBeLessThan(brks[i - 1][1]); + } + expect(brks).withContext(`${msg} | merged break`).toContain(exp.merged.map(Lib.dateTime2ms)); + expect(xa._lBreaks).withContext(`${msg} | total break length`).toBe(exp.lBreaks); + expect(xa._m2).withContext(`${msg} | l2p slope`).toBeGreaterThan(0); + }; + const layout = (range, rangebreaks) => ({xaxis: {range, rangebreaks}}); + + // Thanksgiving 2023 is a Thursday. Its break bridges the overnight breaks before and after it. + const thanksgivingRange = ['2023-11-20 09:30', '2023-11-24 13:00']; + const thanksgiving = [ + {pattern: 'day of week', bounds: [6, 1]}, + {pattern: 'hour', bounds: [16, 9.5]}, + {values: ['2023-11-23']} + ]; + const thanksgivingBreaks = { + merged: ['2023-11-22 16:00', '2023-11-24 09:30'], + lBreaks: 76.5 * ONEHOUR + }; + + // Christmas 2020 is a Friday. Its break bridges the Thursday overnight break and the weekend break. + const christmasRange = ['2020-12-14 09:00', '2020-12-31 15:00']; + const christmas = [ + {pattern: 'hour', bounds: [15, 9]}, + {pattern: 'day of week', bounds: [6, 1]}, + {values: ['2020-12-25']} + ]; + + Plotly.newPlot(gd, [{x: thanksgivingRange}], layout(thanksgivingRange, thanksgiving)) + .then(() => { + assertBreaks('Thanksgiving', thanksgivingBreaks); + + const reversed = thanksgiving.slice().reverse(); + return Plotly.react(gd, [{x: thanksgivingRange}], layout(thanksgivingRange, reversed)); + }) + .then(() => { + assertBreaks('Thanksgiving, reversed rangebreaks', thanksgivingBreaks); + + return Plotly.react(gd, [{x: christmasRange}], layout(christmasRange, christmas)); + }) + .then(() => { + assertBreaks('Christmas', { + merged: ['2020-12-24 15:00', '2020-12-28 09:00'], + lBreaks: 336 * ONEHOUR + }); + }) + .then(done, done.fail); + }); }); describe('during calcTicks', function() {