From 6025ec663cebb2961b700fa450d9466e9e4c3242 Mon Sep 17 00:00:00 2001 From: omkarht Date: Wed, 26 Nov 2025 13:39:22 +0530 Subject: [PATCH 01/10] 5496 : fixed UI crash from excessive tick generation with invalid dates/intervals on-behalf-of: @Mermaid-Chart --- .../mermaid/src/diagrams/gantt/ganttDb.js | 10 ++ .../src/diagrams/gantt/ganttRenderer.js | 141 ++++++++++++------ 2 files changed, 105 insertions(+), 46 deletions(-) diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.js b/packages/mermaid/src/diagrams/gantt/ganttDb.js index b1b2052c1..c07c6ca77 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.js +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.js @@ -300,6 +300,16 @@ const getStartDate = function (prevTime, dateFormat, str) { } else { log.debug('Invalid date:' + str); log.debug('With date format:' + dateFormat.trim()); + + // Only allow fallback for formats that are simple timestamps (x, X) + // which represent Unix timestamps. For all other formats, if dayjs + // strict parsing fails - throws an error. + const isTimestampFormat = dateFormat.trim() === 'x' || dateFormat.trim() === 'X'; + + if (!isTimestampFormat) { + throw new Error(`Invalid date: "${str}" does not match format "${dateFormat.trim()}".`); + } + const d = new Date(str); if ( d === undefined || diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js index 9f899a40f..287720315 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js +++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js @@ -78,6 +78,7 @@ const getMaxIntersections = (tasks, orderOffset) => { }; let w; +const MAX_TICK_COUNT = 10000; export const draw = function (text, id, version, diagObj) { const conf = getConfig().gantt; @@ -602,6 +603,30 @@ export const draw = function (text, id, version, diagObj) { .attr('class', 'exclude-range'); } + /** + * Calculates the estimated number of ticks based on the time domain and tick interval. + * Returns the count or Infinity if there would be too many ticks. + * @param {Date} minTime - The minimum time in the domain + * @param {Date} maxTime - The maximum time in the domain + * @param {number} every - The interval count (e.g., 1 for "1second") + * @param {string} interval - The interval unit (e.g., "second", "day") + * @returns {number} The estimated number of ticks + */ + function getEstimatedTickCount(minTime, maxTime, every, interval) { + const timeDiffMs = maxTime - minTime; + const msPerUnit = { + millisecond: 1, + second: 1000, + minute: 60 * 1000, + hour: 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + week: 7 * 24 * 60 * 60 * 1000, + month: 30 * 24 * 60 * 60 * 1000, // Approximate + }; + const intervalMs = (msPerUnit[interval] || msPerUnit.day) * every; + return Math.ceil(timeDiffMs / intervalMs); + } + /** * @param theSidePad * @param theTopPad @@ -630,32 +655,47 @@ export const draw = function (text, id, version, diagObj) { ); if (resultTickInterval !== null) { - const every = resultTickInterval[1]; + const every = parseInt(resultTickInterval[1], 10); const interval = resultTickInterval[2]; const weekday = diagObj.db.getWeekday() || conf.weekday; - switch (interval) { - case 'millisecond': - bottomXAxis.ticks(timeMillisecond.every(every)); - break; - case 'second': - bottomXAxis.ticks(timeSecond.every(every)); - break; - case 'minute': - bottomXAxis.ticks(timeMinute.every(every)); - break; - case 'hour': - bottomXAxis.ticks(timeHour.every(every)); - break; - case 'day': - bottomXAxis.ticks(timeDay.every(every)); - break; - case 'week': - bottomXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every)); - break; - case 'month': - bottomXAxis.ticks(timeMonth.every(every)); - break; + // Get the time domain to check tick count + const domain = timeScale.domain(); + const minTime = domain[0]; + const maxTime = domain[1]; + const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval); + + if (estimatedTicks > MAX_TICK_COUNT) { + log.warn( + `The tick interval "${every}${interval}" would generate ${estimatedTicks} ticks, ` + + `which exceeds the maximum allowed (${MAX_TICK_COUNT}). ` + + `This may indicate an invalid date or time range. Skipping custom tick interval.` + ); + // D3 using its default automatic tick generation + } else { + switch (interval) { + case 'millisecond': + bottomXAxis.ticks(timeMillisecond.every(every)); + break; + case 'second': + bottomXAxis.ticks(timeSecond.every(every)); + break; + case 'minute': + bottomXAxis.ticks(timeMinute.every(every)); + break; + case 'hour': + bottomXAxis.ticks(timeHour.every(every)); + break; + case 'day': + bottomXAxis.ticks(timeDay.every(every)); + break; + case 'week': + bottomXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every)); + break; + case 'month': + bottomXAxis.ticks(timeMonth.every(every)); + break; + } } } @@ -677,32 +717,41 @@ export const draw = function (text, id, version, diagObj) { .tickFormat(timeFormat(axisFormat)); if (resultTickInterval !== null) { - const every = resultTickInterval[1]; + const every = parseInt(resultTickInterval[1], 10); const interval = resultTickInterval[2]; const weekday = diagObj.db.getWeekday() || conf.weekday; - switch (interval) { - case 'millisecond': - topXAxis.ticks(timeMillisecond.every(every)); - break; - case 'second': - topXAxis.ticks(timeSecond.every(every)); - break; - case 'minute': - topXAxis.ticks(timeMinute.every(every)); - break; - case 'hour': - topXAxis.ticks(timeHour.every(every)); - break; - case 'day': - topXAxis.ticks(timeDay.every(every)); - break; - case 'week': - topXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every)); - break; - case 'month': - topXAxis.ticks(timeMonth.every(every)); - break; + // Get the time domain to check tick count + const domain = timeScale.domain(); + const minTime = domain[0]; + const maxTime = domain[1]; + const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval); + + // Only apply custom ticks if the count is reasonable + if (estimatedTicks <= MAX_TICK_COUNT) { + switch (interval) { + case 'millisecond': + topXAxis.ticks(timeMillisecond.every(every)); + break; + case 'second': + topXAxis.ticks(timeSecond.every(every)); + break; + case 'minute': + topXAxis.ticks(timeMinute.every(every)); + break; + case 'hour': + topXAxis.ticks(timeHour.every(every)); + break; + case 'day': + topXAxis.ticks(timeDay.every(every)); + break; + case 'week': + topXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every)); + break; + case 'month': + topXAxis.ticks(timeMonth.every(every)); + break; + } } } From 454238867bd3e9c247742cfed0568a8565094b16 Mon Sep 17 00:00:00 2001 From: omkarht Date: Wed, 26 Nov 2025 13:55:00 +0530 Subject: [PATCH 02/10] chore: add tests for handling invalid and non-standard date formats in ganttDb on-behalf-of: @Mermaid-Chart --- .../src/diagrams/gantt/ganttDb.spec.ts | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts b/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts index d2d38e86d..35147fa98 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts @@ -503,6 +503,51 @@ describe('when using the ganttDb', function () { it('should reject dates with ridiculous years', function () { ganttDb.setDateFormat('YYYYMMDD'); ganttDb.addTask('test1', 'id1,202304,1d'); - expect(() => ganttDb.getTasks()).toThrowError('Invalid date:202304'); + expect(() => ganttDb.getTasks()).toThrowError(/Invalid date/); + }); + + describe('when using non-standard date formats (issue #5496)', function () { + it('should reject invalid dates when using seconds-only format', function () { + ganttDb.setDateFormat('ss'); + ganttDb.addTask('RTT', 'rtt, 0, 20'); + expect(() => ganttDb.getTasks()).toThrowError(/Invalid date/); + }); + + it('should reject invalid dates when using time-only formats without year', function () { + // Formats without year info should not fall back to new Date() + ganttDb.setDateFormat('HH:mm'); + ganttDb.addTask('test', 'id1, invalid_date, 1h'); + expect(() => ganttDb.getTasks()).toThrowError(/Invalid date/); + }); + + it('should allow valid seconds-only format when date matches', function () { + // Valid case - the date format 'ss' should work when the value is valid + ganttDb.setDateFormat('ss'); + ganttDb.addTask('Task', 'task1, 00, 6s'); + // This should not throw - 00 is a valid 'ss' value + const tasks = ganttDb.getTasks(); + expect(tasks).toHaveLength(1); + }); + + it('should reject dates with typos in year like 202-12-01 instead of 2024-12-01', function () { + ganttDb.setDateFormat('YYYY-MM-DD'); + ganttDb.addSection('Vacation'); + ganttDb.addTask('London', '2024-12-01, 7d'); + ganttDb.addTask('London', '202-12-01, 7d'); + expect(() => ganttDb.getTasks()).toThrowError(/Invalid date/); + }); + + it('should work correctly with valid YYYY-MM-DD dates', function () { + // Valid case - both dates are correctly formatted + ganttDb.setDateFormat('YYYY-MM-DD'); + ganttDb.addSection('Vacation'); + ganttDb.addTask('London Trip 1', '2024-12-01, 7d'); + ganttDb.addTask('London Trip 2', '2024-12-15, 7d'); + const tasks = ganttDb.getTasks(); + expect(tasks).toHaveLength(2); + // Verify years are correct (2024) + expect(tasks[0].startTime.getFullYear()).toBe(2024); + expect(tasks[1].startTime.getFullYear()).toBe(2024); + }); }); }); From 88fd141276c674a5084764e0ba25631147609197 Mon Sep 17 00:00:00 2001 From: omkarht Date: Wed, 26 Nov 2025 15:46:59 +0530 Subject: [PATCH 03/10] fix: correct Gantt diagram dateFormat syntax in test case on-behalf-of: @Mermaid-Chart --- cypress/integration/rendering/theme.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/integration/rendering/theme.spec.js b/cypress/integration/rendering/theme.spec.js index 1965f8c99..ca7fd83a2 100644 --- a/cypress/integration/rendering/theme.spec.js +++ b/cypress/integration/rendering/theme.spec.js @@ -286,9 +286,9 @@ erDiagram accTitle: This is a title accDescr: This is a description - dateFormat :YYYY-MM-DD + dateFormat YYYY-MM-DD title :Adding GANTT diagram functionality to mermaid - excludes :excludes the named dates/days from being included in a charted task.. + excludes :excludes the named dates/days from being included in a charted task. section A section Completed task :done, des1, 2014-01-06,2014-01-08 Active task :active, des2, 2014-01-09, 3d From 8bfd47758ad5255459d0cced5210d3cb8cfa6f91 Mon Sep 17 00:00:00 2001 From: omkarht Date: Wed, 26 Nov 2025 17:30:00 +0530 Subject: [PATCH 04/10] chore: add changeset on-behalf-of: @Mermaid-Chart --- .changeset/tricky-rivers-stand.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tricky-rivers-stand.md diff --git a/.changeset/tricky-rivers-stand.md b/.changeset/tricky-rivers-stand.md new file mode 100644 index 000000000..8e0757f14 --- /dev/null +++ b/.changeset/tricky-rivers-stand.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: validate dates and tick interval to prevent UI freeze/crash in gantt diagramtype From bf50ce5237460adf470786d079c340809de9a5a6 Mon Sep 17 00:00:00 2001 From: omkarht Date: Wed, 26 Nov 2025 18:59:18 +0530 Subject: [PATCH 05/10] fix: handle uncaught exceptions in Gantt chart rendering test for invalid dates on-behalf-of: @Mermaid-Chart --- cypress/integration/rendering/gantt.spec.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/cypress/integration/rendering/gantt.spec.js b/cypress/integration/rendering/gantt.spec.js index 72cb6ea29..db4e2bd78 100644 --- a/cypress/integration/rendering/gantt.spec.js +++ b/cypress/integration/rendering/gantt.spec.js @@ -118,6 +118,15 @@ describe('Gantt diagram', () => { ); }); it('should FAIL rendering a gantt chart for issue #1060 with invalid date', () => { + let errorCaught = false; + + cy.on('uncaught:exception', (err) => { + // Expect error related to invalid or missing date format + expect(err.message).to.include('Invalid date'); + errorCaught = true; + return false; // prevent Cypress from failing the test + }); + imgSnapshotTest( ` gantt @@ -150,12 +159,16 @@ describe('Gantt diagram', () => { section Plasma Calls & updates OVM :ovm, 2019-07-12, 120d - Plasma call 26 :pc26, 2019-08-21, 1d - Plasma call 27 :pc27, 2019-09-03, 1d - Plasma call 28 :pc28, 2019-09-17, 1d - `, + Plasma call 26 :pc26, 2019-08-21, 1d + Plasma call 27 :pc27, 2019-09-03, 1d + Plasma call 28 :pc28, 2019-09-17, 1d + `, {} ); + + cy.then(() => { + expect(errorCaught, 'Expected rendering to fail with invalid date error').to.equal(true); + }); }); it('should default to showing today marker', () => { From 0843a2fa7ac99b742b2c94c80e16a17dc2f26e38 Mon Sep 17 00:00:00 2001 From: omkarht Date: Thu, 27 Nov 2025 12:38:30 +0530 Subject: [PATCH 06/10] fix: optimize tick interval calculation using dayjs for improved accuracy on-behalf-of: @Mermaid-Chart --- packages/mermaid/src/diagrams/gantt/ganttRenderer.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js index 287720315..da672da34 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js +++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js @@ -614,16 +614,7 @@ export const draw = function (text, id, version, diagObj) { */ function getEstimatedTickCount(minTime, maxTime, every, interval) { const timeDiffMs = maxTime - minTime; - const msPerUnit = { - millisecond: 1, - second: 1000, - minute: 60 * 1000, - hour: 60 * 60 * 1000, - day: 24 * 60 * 60 * 1000, - week: 7 * 24 * 60 * 60 * 1000, - month: 30 * 24 * 60 * 60 * 1000, // Approximate - }; - const intervalMs = (msPerUnit[interval] || msPerUnit.day) * every; + const intervalMs = dayjs.duration({ [interval ?? 'day']: every }).asMilliseconds(); return Math.ceil(timeDiffMs / intervalMs); } From 87c561615eda197263ec6a0e6a9e19e356ada42c Mon Sep 17 00:00:00 2001 From: omkarht Date: Thu, 27 Nov 2025 13:28:21 +0530 Subject: [PATCH 07/10] fix: extend dayjs with duration plugin for improved time calculations on-behalf-of: @Mermaid-Chart --- packages/mermaid/src/diagrams/gantt/ganttRenderer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js index da672da34..6320c4ce4 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js +++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js @@ -1,4 +1,5 @@ import dayjs from 'dayjs'; +import dayjsDuration from 'dayjs/plugin/duration.js'; import { log } from '../../logger.js'; import { select, @@ -28,6 +29,8 @@ import common from '../common/common.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; +dayjs.extend(dayjsDuration); + export const setConf = function () { log.debug('Something is calling, setConf, remove the call'); }; From 88e8ad6f5b0f2f9753d7ac6cfd05ae68e1920b6b Mon Sep 17 00:00:00 2001 From: omkarht Date: Thu, 27 Nov 2025 19:17:37 +0530 Subject: [PATCH 08/10] fix: improve error handling for invalid date formats in Gantt chart on-behalf-of: @Mermaid-Chart --- cypress/integration/rendering/gantt.spec.js | 21 ++------- cypress/integration/rendering/theme.spec.js | 4 +- .../mermaid/src/diagrams/gantt/ganttDb.js | 2 +- .../src/diagrams/gantt/ganttDb.spec.ts | 47 +------------------ 4 files changed, 8 insertions(+), 66 deletions(-) diff --git a/cypress/integration/rendering/gantt.spec.js b/cypress/integration/rendering/gantt.spec.js index db4e2bd78..72cb6ea29 100644 --- a/cypress/integration/rendering/gantt.spec.js +++ b/cypress/integration/rendering/gantt.spec.js @@ -118,15 +118,6 @@ describe('Gantt diagram', () => { ); }); it('should FAIL rendering a gantt chart for issue #1060 with invalid date', () => { - let errorCaught = false; - - cy.on('uncaught:exception', (err) => { - // Expect error related to invalid or missing date format - expect(err.message).to.include('Invalid date'); - errorCaught = true; - return false; // prevent Cypress from failing the test - }); - imgSnapshotTest( ` gantt @@ -159,16 +150,12 @@ describe('Gantt diagram', () => { section Plasma Calls & updates OVM :ovm, 2019-07-12, 120d - Plasma call 26 :pc26, 2019-08-21, 1d - Plasma call 27 :pc27, 2019-09-03, 1d - Plasma call 28 :pc28, 2019-09-17, 1d - `, + Plasma call 26 :pc26, 2019-08-21, 1d + Plasma call 27 :pc27, 2019-09-03, 1d + Plasma call 28 :pc28, 2019-09-17, 1d + `, {} ); - - cy.then(() => { - expect(errorCaught, 'Expected rendering to fail with invalid date error').to.equal(true); - }); }); it('should default to showing today marker', () => { diff --git a/cypress/integration/rendering/theme.spec.js b/cypress/integration/rendering/theme.spec.js index ca7fd83a2..1965f8c99 100644 --- a/cypress/integration/rendering/theme.spec.js +++ b/cypress/integration/rendering/theme.spec.js @@ -286,9 +286,9 @@ erDiagram accTitle: This is a title accDescr: This is a description - dateFormat YYYY-MM-DD + dateFormat :YYYY-MM-DD title :Adding GANTT diagram functionality to mermaid - excludes :excludes the named dates/days from being included in a charted task. + excludes :excludes the named dates/days from being included in a charted task.. section A section Completed task :done, des1, 2014-01-06,2014-01-08 Active task :active, des2, 2014-01-09, 3d diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.js b/packages/mermaid/src/diagrams/gantt/ganttDb.js index c07c6ca77..5f328f546 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.js +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.js @@ -307,7 +307,7 @@ const getStartDate = function (prevTime, dateFormat, str) { const isTimestampFormat = dateFormat.trim() === 'x' || dateFormat.trim() === 'X'; if (!isTimestampFormat) { - throw new Error(`Invalid date: "${str}" does not match format "${dateFormat.trim()}".`); + log.debug(`Invalid date: "${str}" does not match format "${dateFormat.trim()}".`); } const d = new Date(str); diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts b/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts index 35147fa98..d2d38e86d 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts @@ -503,51 +503,6 @@ describe('when using the ganttDb', function () { it('should reject dates with ridiculous years', function () { ganttDb.setDateFormat('YYYYMMDD'); ganttDb.addTask('test1', 'id1,202304,1d'); - expect(() => ganttDb.getTasks()).toThrowError(/Invalid date/); - }); - - describe('when using non-standard date formats (issue #5496)', function () { - it('should reject invalid dates when using seconds-only format', function () { - ganttDb.setDateFormat('ss'); - ganttDb.addTask('RTT', 'rtt, 0, 20'); - expect(() => ganttDb.getTasks()).toThrowError(/Invalid date/); - }); - - it('should reject invalid dates when using time-only formats without year', function () { - // Formats without year info should not fall back to new Date() - ganttDb.setDateFormat('HH:mm'); - ganttDb.addTask('test', 'id1, invalid_date, 1h'); - expect(() => ganttDb.getTasks()).toThrowError(/Invalid date/); - }); - - it('should allow valid seconds-only format when date matches', function () { - // Valid case - the date format 'ss' should work when the value is valid - ganttDb.setDateFormat('ss'); - ganttDb.addTask('Task', 'task1, 00, 6s'); - // This should not throw - 00 is a valid 'ss' value - const tasks = ganttDb.getTasks(); - expect(tasks).toHaveLength(1); - }); - - it('should reject dates with typos in year like 202-12-01 instead of 2024-12-01', function () { - ganttDb.setDateFormat('YYYY-MM-DD'); - ganttDb.addSection('Vacation'); - ganttDb.addTask('London', '2024-12-01, 7d'); - ganttDb.addTask('London', '202-12-01, 7d'); - expect(() => ganttDb.getTasks()).toThrowError(/Invalid date/); - }); - - it('should work correctly with valid YYYY-MM-DD dates', function () { - // Valid case - both dates are correctly formatted - ganttDb.setDateFormat('YYYY-MM-DD'); - ganttDb.addSection('Vacation'); - ganttDb.addTask('London Trip 1', '2024-12-01, 7d'); - ganttDb.addTask('London Trip 2', '2024-12-15, 7d'); - const tasks = ganttDb.getTasks(); - expect(tasks).toHaveLength(2); - // Verify years are correct (2024) - expect(tasks[0].startTime.getFullYear()).toBe(2024); - expect(tasks[1].startTime.getFullYear()).toBe(2024); - }); + expect(() => ganttDb.getTasks()).toThrowError('Invalid date:202304'); }); }); From 73b8626ab05fbffba391f18026d5b6efee4428c5 Mon Sep 17 00:00:00 2001 From: omkarht Date: Thu, 27 Nov 2025 20:42:59 +0530 Subject: [PATCH 09/10] fix: enhance Gantt chart handling for invalid date formats and tick intervals on-behalf-of: @Mermaid-Chart --- cypress/integration/rendering/gantt.spec.js | 30 ++++ .../mermaid/src/diagrams/gantt/ganttDb.js | 22 ++- .../src/diagrams/gantt/ganttDb.spec.ts | 23 +++ .../src/diagrams/gantt/ganttRenderer.js | 156 ++++++++++-------- 4 files changed, 155 insertions(+), 76 deletions(-) diff --git a/cypress/integration/rendering/gantt.spec.js b/cypress/integration/rendering/gantt.spec.js index 72cb6ea29..f250a188f 100644 --- a/cypress/integration/rendering/gantt.spec.js +++ b/cypress/integration/rendering/gantt.spec.js @@ -833,4 +833,34 @@ describe('Gantt diagram', () => { {} ); }); + it('should handle seconds-only format with tickInterval (issue #5496)', () => { + imgSnapshotTest( + ` + gantt + tickInterval 1second + dateFormat ss + axisFormat %s + + section Network Request + RTT : rtt, 0, 20 + `, + {} + ); + }); + it('should handle dates with year typo like 202 instead of 2024 (issue #5496)', () => { + imgSnapshotTest( + ` + gantt + title Schedule + dateFormat YYYY-MM-DD + tickInterval 1week + axisFormat %m-%d + + section Vacation + London : 2024-12-01, 7d + London : 202-12-01, 7d + `, + {} + ); + }); }); diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.js b/packages/mermaid/src/diagrams/gantt/ganttDb.js index 5f328f546..185fdae41 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.js +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.js @@ -268,7 +268,15 @@ const fixTaskDates = function (startTime, endTime, dateFormat, excludes, include const getStartDate = function (prevTime, dateFormat, str) { str = str.trim(); - if ((dateFormat.trim() === 'x' || dateFormat.trim() === 'X') && /^\d+$/.test(str)) { + + // Helper function to check if format is a timestamp format (x or X) + const isTimestampFormat = (format) => { + const trimmedFormat = format.trim(); + return trimmedFormat === 'x' || trimmedFormat === 'X'; + }; + + // Handle timestamp formats (x, X) with numeric strings + if (isTimestampFormat(dateFormat) && /^\d+$/.test(str)) { return new Date(Number(str)); } // Test for after @@ -293,7 +301,7 @@ const getStartDate = function (prevTime, dateFormat, str) { return today; } - // Check for actual date set + // Check for actual date set using dayjs strict parsing let mDate = dayjs(str, dateFormat.trim(), true); if (mDate.isValid()) { return mDate.toDate(); @@ -301,15 +309,13 @@ const getStartDate = function (prevTime, dateFormat, str) { log.debug('Invalid date:' + str); log.debug('With date format:' + dateFormat.trim()); - // Only allow fallback for formats that are simple timestamps (x, X) - // which represent Unix timestamps. For all other formats, if dayjs - // strict parsing fails - throws an error. - const isTimestampFormat = dateFormat.trim() === 'x' || dateFormat.trim() === 'X'; - - if (!isTimestampFormat) { + // Only allow fallback to new Date() for timestamp formats (x, X) + // For all other formats, if dayjs strict parsing fails, throw an error + if (!isTimestampFormat(dateFormat)) { log.debug(`Invalid date: "${str}" does not match format "${dateFormat.trim()}".`); } + // Timestamp formats can fall back to new Date() const d = new Date(str); if ( d === undefined || diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts b/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts index d2d38e86d..48d618938 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts @@ -505,4 +505,27 @@ describe('when using the ganttDb', function () { ganttDb.addTask('test1', 'id1,202304,1d'); expect(() => ganttDb.getTasks()).toThrowError('Invalid date:202304'); }); + + it('should handle seconds-only format with valid numeric values (issue #5496)', function () { + ganttDb.setDateFormat('ss'); + ganttDb.addSection('Network Request'); + ganttDb.addTask('RTT', 'rtt, 0, 20'); + const tasks = ganttDb.getTasks(); + expect(tasks).toHaveLength(1); + expect(tasks[0].task).toBe('RTT'); + expect(tasks[0].id).toBe('rtt'); + }); + + it('should handle dates with year typo like 202 instead of 2024 (issue #5496)', function () { + ganttDb.setDateFormat('YYYY-MM-DD'); + ganttDb.addSection('Vacation'); + ganttDb.addTask('London Trip 1', '2024-12-01, 7d'); + ganttDb.addTask('London Trip 2', '202-12-01, 7d'); + const tasks = ganttDb.getTasks(); + expect(tasks).toHaveLength(2); + // First task should be in year 2024 + expect(tasks[0].startTime.getFullYear()).toBe(2024); + // Second task will be parsed as year 202 (fallback to new Date()) + expect(tasks[1].startTime.getFullYear()).toBe(202); + }); }); diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js index 6320c4ce4..7a30e0d4a 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js +++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js @@ -608,7 +608,7 @@ export const draw = function (text, id, version, diagObj) { /** * Calculates the estimated number of ticks based on the time domain and tick interval. - * Returns the count or Infinity if there would be too many ticks. + * Returns the estimated number of ticks as a number. * @param {Date} minTime - The minimum time in the domain * @param {Date} maxTime - The maximum time in the domain * @param {number} every - The interval count (e.g., 1 for "1second") @@ -616,8 +616,14 @@ export const draw = function (text, id, version, diagObj) { * @returns {number} The estimated number of ticks */ function getEstimatedTickCount(minTime, maxTime, every, interval) { + if (every <= 0 || minTime > maxTime) { + return Infinity; + } const timeDiffMs = maxTime - minTime; const intervalMs = dayjs.duration({ [interval ?? 'day']: every }).asMilliseconds(); + if (intervalMs <= 0) { + return Infinity; + } return Math.ceil(timeDiffMs / intervalMs); } @@ -650,45 +656,52 @@ export const draw = function (text, id, version, diagObj) { if (resultTickInterval !== null) { const every = parseInt(resultTickInterval[1], 10); - const interval = resultTickInterval[2]; - const weekday = diagObj.db.getWeekday() || conf.weekday; - - // Get the time domain to check tick count - const domain = timeScale.domain(); - const minTime = domain[0]; - const maxTime = domain[1]; - const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval); - - if (estimatedTicks > MAX_TICK_COUNT) { + if (isNaN(every) || every <= 0) { log.warn( - `The tick interval "${every}${interval}" would generate ${estimatedTicks} ticks, ` + - `which exceeds the maximum allowed (${MAX_TICK_COUNT}). ` + - `This may indicate an invalid date or time range. Skipping custom tick interval.` + `Invalid tick interval value: "${resultTickInterval[1]}". Skipping custom tick interval.` ); - // D3 using its default automatic tick generation + // Skip applying custom ticks } else { - switch (interval) { - case 'millisecond': - bottomXAxis.ticks(timeMillisecond.every(every)); - break; - case 'second': - bottomXAxis.ticks(timeSecond.every(every)); - break; - case 'minute': - bottomXAxis.ticks(timeMinute.every(every)); - break; - case 'hour': - bottomXAxis.ticks(timeHour.every(every)); - break; - case 'day': - bottomXAxis.ticks(timeDay.every(every)); - break; - case 'week': - bottomXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every)); - break; - case 'month': - bottomXAxis.ticks(timeMonth.every(every)); - break; + const interval = resultTickInterval[2]; + const weekday = diagObj.db.getWeekday() || conf.weekday; + + // Get the time domain to check tick count + const domain = timeScale.domain(); + const minTime = domain[0]; + const maxTime = domain[1]; + const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval); + + if (estimatedTicks > MAX_TICK_COUNT) { + log.warn( + `The tick interval "${every}${interval}" would generate ${estimatedTicks} ticks, ` + + `which exceeds the maximum allowed (${MAX_TICK_COUNT}). ` + + `This may indicate an invalid date or time range. Skipping custom tick interval.` + ); + // D3 will use its default automatic tick generation + } else { + switch (interval) { + case 'millisecond': + bottomXAxis.ticks(timeMillisecond.every(every)); + break; + case 'second': + bottomXAxis.ticks(timeSecond.every(every)); + break; + case 'minute': + bottomXAxis.ticks(timeMinute.every(every)); + break; + case 'hour': + bottomXAxis.ticks(timeHour.every(every)); + break; + case 'day': + bottomXAxis.ticks(timeDay.every(every)); + break; + case 'week': + bottomXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every)); + break; + case 'month': + bottomXAxis.ticks(timeMonth.every(every)); + break; + } } } } @@ -712,39 +725,46 @@ export const draw = function (text, id, version, diagObj) { if (resultTickInterval !== null) { const every = parseInt(resultTickInterval[1], 10); - const interval = resultTickInterval[2]; - const weekday = diagObj.db.getWeekday() || conf.weekday; + if (isNaN(every) || every <= 0) { + log.warn( + `Invalid tick interval value: "${resultTickInterval[1]}". Skipping custom tick interval.` + ); + // Skip applying custom ticks + } else { + const interval = resultTickInterval[2]; + const weekday = diagObj.db.getWeekday() || conf.weekday; - // Get the time domain to check tick count - const domain = timeScale.domain(); - const minTime = domain[0]; - const maxTime = domain[1]; - const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval); + // Get the time domain to check tick count + const domain = timeScale.domain(); + const minTime = domain[0]; + const maxTime = domain[1]; + const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval); - // Only apply custom ticks if the count is reasonable - if (estimatedTicks <= MAX_TICK_COUNT) { - switch (interval) { - case 'millisecond': - topXAxis.ticks(timeMillisecond.every(every)); - break; - case 'second': - topXAxis.ticks(timeSecond.every(every)); - break; - case 'minute': - topXAxis.ticks(timeMinute.every(every)); - break; - case 'hour': - topXAxis.ticks(timeHour.every(every)); - break; - case 'day': - topXAxis.ticks(timeDay.every(every)); - break; - case 'week': - topXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every)); - break; - case 'month': - topXAxis.ticks(timeMonth.every(every)); - break; + // Only apply custom ticks if the count is reasonable + if (estimatedTicks <= MAX_TICK_COUNT) { + switch (interval) { + case 'millisecond': + topXAxis.ticks(timeMillisecond.every(every)); + break; + case 'second': + topXAxis.ticks(timeSecond.every(every)); + break; + case 'minute': + topXAxis.ticks(timeMinute.every(every)); + break; + case 'hour': + topXAxis.ticks(timeHour.every(every)); + break; + case 'day': + topXAxis.ticks(timeDay.every(every)); + break; + case 'week': + topXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every)); + break; + case 'month': + topXAxis.ticks(timeMonth.every(every)); + break; + } } } } From b115ad3cd77679e3cf14323ec5771f74d681759f Mon Sep 17 00:00:00 2001 From: omkarht Date: Thu, 27 Nov 2025 21:54:04 +0530 Subject: [PATCH 10/10] fix: remove fallback to new Date() for non-timestamp formats in date validation on-behalf-of: @Mermaid-Chart --- packages/mermaid/src/diagrams/gantt/ganttDb.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.js b/packages/mermaid/src/diagrams/gantt/ganttDb.js index 185fdae41..7faf357f2 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.js +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.js @@ -309,12 +309,6 @@ const getStartDate = function (prevTime, dateFormat, str) { log.debug('Invalid date:' + str); log.debug('With date format:' + dateFormat.trim()); - // Only allow fallback to new Date() for timestamp formats (x, X) - // For all other formats, if dayjs strict parsing fails, throw an error - if (!isTimestampFormat(dateFormat)) { - log.debug(`Invalid date: "${str}" does not match format "${dateFormat.trim()}".`); - } - // Timestamp formats can fall back to new Date() const d = new Date(str); if (