TTMSFNCGanttChart 1.3.3.10 - timeline export: bars spanning a page are missing, week/month/year units lose bars or hang; screen: group rectangle used uninitialized when ShowGroup = False

Three more timeline issues from testing printouts of real schedules. The first two are in ExportView (PDF and print are both affected), the third is in screen drawing. Line numbers refer to 1.3.3.10. A demo for all of them is attached.

1. DrawExportTimeLineItems skips bars that start before the page and end after it

When the export is split over several pages along the time axis, a task bar is drawn only on the page where it starts and on the page where it ends. On every page in between the row stays empty. This affects every task that is longer than one timeline page – in practice the parent tasks and long tasks, which are exactly the bars a schedule is read by. The screen is fine.

TTMSFNCCustomGanttChart.DrawExportTimeLine β†’ DrawExportTimeLineItems (VCL.TMSFNCGanttChart.pas, line 9646):

if ((tlStartCol >= AExportTimeLineParams.StartColumn) and (tlStartCol <= lc + 1)) or
   ((tlEndCol >= AExportTimeLineParams.StartColumn) and (tlEndCol <= lc + 1)) then

The condition asks whether the start column or the end column lies on the page. The third case – start before the page, end after it – is missing. An overlap test covers all three cases:

if (tlStartCol <= lc + 1) and (tlEndCol >= AExportTimeLineParams.StartColumn) then

The rectangle calculated after the condition already handles a bar that begins left of the page (that is today's "end on this page" case), and ExportView clips the timeline part to the page (AGraphics.ClipRect(drawParams.Rect), line 10533), so a long bar does not run into the task list. All drawing events of the item are inside this condition, so we cannot work around it from the application.

Example from our application: A4 landscape, day unit, 35 pages. A parent task from 27.09.2021 to 07.02.2022 appears on the first timeline page (its start) and on the last one (its end); the pages for October, November, December and January show an empty row. A normal task of 70 days (02.11.2021 to 10.01.2022) behaves the same: the page with its start is fine, the page in between is empty, the page with its end is fine.

2. Week, month and year units in the export: bars inside a unit are missing, the first unit is lost, the page loop does not end

With TimeLineUnit = gtuWeek, gtuMonth or gtuYear the export goes wrong in four ways. All four come from the way ExportView and DrawExportTimeLineItems use the column of a unit start where the last column of a page is meant.

GetTimeLineLastColumn (line 10189) starts with Result := 1 and accepts a column only at a unit start (DrawRow) and only if the whole unit (DrawRowSpan) still fits. So its result is the start column of the last unit on the page – and 1 if no unit fits, which cannot be told apart from "column 1 fits". ExportView passes this value as drawParams.LastColumn and continues with lastTimeCol := GetNextTimelineColumn(tempLastTimeCol) (line 10549).

a) Bars inside a unit are not drawn. DrawExportTimeLineItems takes lc := AExportTimeLineParams.LastColumn and draws a task only if its start or end column is <= lc + 1 (line 9646, see point 1). For days that is the last column. For weeks and months it is the first day of the last unit on the page, so a task that starts and ends later in that unit is skipped – on this page and, because it ends before the next page starts, on every page. Months: practically no bars at all. Weeks: a task from Wednesday to Friday in the last week of a page is missing.

b) The first, partial unit is lost. The export rarely starts on the 1st of a month. On page 1 the first unit start comes after the partial first month, and GetTimeLineLastColumn needs the partial month plus the next whole month to fit. If they do not, the result is 1, maxTimeW stays 0, page 1 shows no timeline, and page 2 starts at the first unit start after column 1 – the partial first month is never drawn. The same happens when the task list leaves no room for the timeline on page 1 (curRect.Left < workRect.Right - drawParams.TimeUnitWidth, line 10519): tempLastTimeCol keeps its initial 0, and GetNextTimelineColumn(0) skips the first unit.

c) Nothing at all when no unit starts inside the range. Year unit, range 09.10.2026 to 01.01.2027: one page with an empty timeline – no DrawRow is True before the last column, the result is 1 with maxTimeW = 0. With the range ending on 31.12.2026 (no 1 January at all) the export did not return in our demo; we did not capture the call stack, GetNextTimelineColumn with its while not DrawRow(Result) loop is the likely place.

d) Endless page loop when one unit is wider than the page. A page that starts at a unit start U whose unit does not fit returns 1; the next page starts again at the first unit start after column 1, which is at or before U. HasNextPage never becomes False. Measured in our application: month unit, A4 landscape, 25 pt per day (a 31-day month needs 775 pt, the page had about 757 pt): no result after 9 minutes, the call stack keeps going through AOnNewPage β†’ NewPage.

Demo (attached, GanttTimelineIssues.zip, VCL, Win32, no design-time components): A4 landscape, margins 20 pt, FitToPage = False, Mode = emTimeline, 25 pt per day unless stated. It runs by itself, writes one PDF per case and logs, per task, the pages on which OnBeforeDrawGanttTimeLineItem fired during the export. Every case gets a new chart, and a watchdog ends the process if a case does not return within 60 seconds. Result on our machine (log and PDFs are in the zip):

Case Unit, range Tasks Result
1 (point 1) day, 30.10.2026–01.02.2027, 3 pages A over 60 work days A on pages 1 and 3, page 2 empty
2a (b) month, 09.10.2026–01.01.2027 one task each on 12.10., 02.11., 01.12. page 1 empty, October missing; November and December fine
2b (a) month, 01.10.2026–01.01.2027 12.–16.10., 16.–20.11., 07.–11.12. no bar on any page
2c (a) week, 31.08.–02.11.2026 07.09.; Wed–Fri 23.–25.09.; Wed–Fri 14.–16.10. 23.–25.09. on no page – it lies in the last week unit of page 1 (weeks start on Sunday: 31.08.–05.09., 06.09., 13.09., 20.09.)
2d (c) year, 09.10.2026–01.01.2027, 3 pt per day (252 pt) 12.–16.10. one page, empty timeline, no bar (the screen shows the bar)
2e (d) month, 01.10.2026–01.01.2027, 30 pt per day (October = 930 pt, usable width 802 pt) 12.–16.10. does not end: the demo raises EAbort in OnBeforeDrawHeader when page 51 starts (expected: 3 or 4 pages)

Suggestion:

  • In DrawExportTimeLineItems, test against the real last column of the page (last unit start + its span βˆ’ 1), together with the overlap test from point 1.
  • Let GetTimeLineLastColumn report "nothing fits" in a way the caller can recognise (for example AStartCol - 1). In ExportView, if nothing fits on a page that also shows the task list, keep lastTimeCol for the next page; if nothing fits on a page with the full width, draw the unit clipped (or split it over pages) and continue with the next unit, so the loop always advances.
  • Start with "no column drawn yet" instead of tempLastTimeCol := 0, so that a page without a timeline part does not skip the first unit.

Until this is fixed we offer only the day unit in our print dialog.

3. DoOnBeforeDrawTime draws the group rectangle uninitialized when ShowGroup = False

Screen drawing, not export. With TimelineOptions.ShowGroup := False (we use it for month and year units) repainting the timeline header raised "Range check error" in our debug build (one to three messages each time the unit is switched).

TTMSFNCCustomGanttChart.DoOnBeforeDrawTime (line 4861): the group rectangle gr is only assigned inside if FTimelineOptions.FShowGroup and (groupHeight <> 0) then gr := r; (line 4891). The group part further down runs under if gd or FFirstDrawDate then and calls AGraphics.DrawRectangle(gr) (line 4974). gd is False without groups, but FFirstDrawDate is True on the first item of every timeline paint (it is reset at line 4302). So gr is drawn, and its Right is computed, with whatever is on the stack. Values seen in the debugger: gd = False, gr = (4.59e-41; 3.95e+32; 1; 2.88). TTMSFNCGraphicsContextGeneral.FillRect rounds to a TRect β†’ range check error. Without range checking there is no message, but a random rectangle is drawn.

The demo (case 3: month unit, ShowGroup = False) counts the calls of OnBeforeDrawGanttTimeLineGroupTime during one repaint: 1 call (expected 0), with ARect = (Left 2.2e-38; Top 0; Right 1; Bottom 3.1e-37) – the values change from run to run.

Suggestion: skip the group part when ShowGroup is False (for example if FTimelineOptions.FShowGroup and (gd or FFirstDrawDate) then), or initialise gr. Our workaround: OnBeforeDrawGanttTimeLineGroupTime with AAllow := False when ShowGroup is False. The arithmetic on gr before the event (line 4966) still runs on the uninitialized value.

Thanks!

GanttTimelineIssues.zip (762.3 KB)

The fixes for these issues will also be adressed in the upcoming update v1.3.3.12

Thank you, Gjalt! Once 1.3.3.12 is available, I'll run the demo from this thread again and let you know the results.

Thanks, Gjalt. With 1.3.3.13 the demo from this thread gives the expected result in every case:

  1. Day unit: the long bar is drawn on pages 1, 2 and 3.
    2a. Month unit, export starting mid-month: the first, partial month is there.
    2b. Month unit: the bars inside a month are drawn on their pages.
    2c. Week unit: the Wed–Fri task in the last week of page 1 is drawn.
    2d. Year unit: the bar is drawn.
    2e. Month unit wider than the page: the export finishes (6 pages) instead of looping.
  2. ShowGroup = False: OnBeforeDrawGanttTimeLineGroupTime is no longer called.

In our application, a plan with 56 tasks exported to A4 landscape with the day unit now shows the summary bars on all timeline pages, including the middle pages where they were missing. We have also enabled the week and month units in our print dialog again: PDF export and printing, with and without the task list, show every bar and the first unit, even a partial one, and every export finishes. Because the partial first unit is drawn now, we could also remove the extra lead-in days we used to print in front of it. On screen, month and "none" units without the group row no longer need our event handler, so we removed it. The variant of 2d without any 1 January in the range is not in the demo, so I have not tested that one, and our print dialog still does not offer the year unit.

Thank you for the fixes!