TAChart: Refactor Gantt chart demo project (data separate from series). Integrate "milestone" symbol for TStateSeries Gantt usage.

This commit is contained in:
wp_xyz 2025-03-02 01:33:54 +01:00
parent 955ddfa0bb
commit 1bcb76a36c
6 changed files with 274 additions and 93 deletions

View File

@ -48,6 +48,7 @@
<Filename Value="uganttframe.pas"/> <Filename Value="uganttframe.pas"/>
<IsPartOfProject Value="True"/> <IsPartOfProject Value="True"/>
<ComponentName Value="GanttFrame"/> <ComponentName Value="GanttFrame"/>
<HasResources Value="True"/>
<ResourceBaseClass Value="Frame"/> <ResourceBaseClass Value="Frame"/>
<UnitName Value="uGanttFrame"/> <UnitName Value="uGanttFrame"/>
</Unit> </Unit>
@ -55,9 +56,15 @@
<Filename Value="umachinestateframe.pas"/> <Filename Value="umachinestateframe.pas"/>
<IsPartOfProject Value="True"/> <IsPartOfProject Value="True"/>
<ComponentName Value="MachineStateFrame"/> <ComponentName Value="MachineStateFrame"/>
<HasResources Value="True"/>
<ResourceBaseClass Value="Frame"/> <ResourceBaseClass Value="Frame"/>
<UnitName Value="uMachineStateFrame"/> <UnitName Value="uMachineStateFrame"/>
</Unit> </Unit>
<Unit>
<Filename Value="uganttdata.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="uGanttData"/>
</Unit>
</Units> </Units>
</ProjectOptions> </ProjectOptions>
<CompilerOptions> <CompilerOptions>
@ -74,7 +81,17 @@
<Debugging> <Debugging>
<DebugInfoType Value="dsDwarf3"/> <DebugInfoType Value="dsDwarf3"/>
</Debugging> </Debugging>
<Options>
<Win32>
<GraphicApplication Value="True"/>
</Win32>
</Options>
</Linking> </Linking>
<Other>
<CompilerMessages>
<IgnoredMessages idx6058="True"/>
</CompilerMessages>
</Other>
</CompilerOptions> </CompilerOptions>
<Debugging> <Debugging>
<Exceptions> <Exceptions>

View File

@ -10,11 +10,13 @@ object GanttFrame: TGanttFrame
DesignTop = 307 DesignTop = 307
object GanttChart: TChart object GanttChart: TChart
Left = 0 Left = 0
Height = 381 Height = 350
Top = 0 Top = 0
Width = 935 Width = 935
AxisList = < AxisList = <
item item
Grid.Color = clSilver
Grid.Style = psSolid
Grid.Visible = False Grid.Visible = False
Marks.LabelBrush.Style = bsClear Marks.LabelBrush.Style = bsClear
Minors = <> Minors = <>
@ -46,26 +48,52 @@ object GanttFrame: TGanttFrame
BarBrush.Color = clSilver BarBrush.Color = clSilver
BarHeight = 0.7 BarHeight = 0.7
BarPen.Color = clMedGray BarPen.Color = clMedGray
OnBarHeightChanged = GanttSeriesBarHeightChanged
end
object MilestoneSeries: TLineSeries
LinePen.Color = clBlue
LineType = ltNone
Pointer.Brush.Color = clBlue
Pointer.HorizSize = 12
Pointer.Style = psDiamond
Pointer.VertSize = 12
Pointer.Visible = True
ShowPoints = True
end end
object GanttCompletedSeries: TStateSeries object GanttCompletedSeries: TStateSeries
BarBrush.Color = clGreen BarBrush.Color = clGreen
BarHeight = 0.7 BarHeight = 0.7
Source = Completed_ChartSource
end end
end end
object TasksChartSource: TListChartSource object Panel1: TPanel
Left = 120 Left = 0
Top = 40 Height = 31
Top = 350
Width = 935
Align = alBottom
AutoSize = True
BevelOuter = bvNone
ClientHeight = 31
ClientWidth = 935
TabOrder = 1
object cbShowCompleteness: TCheckBox
AnchorSideLeft.Control = Panel1
AnchorSideTop.Control = Panel1
Left = 6
Height = 19
Top = 6
Width = 147
BorderSpacing.Around = 6
Caption = 'Show task completeness'
Checked = True
State = cbChecked
TabOrder = 0
OnChange = cbShowCompletenessChange
end
object cbRotated: TCheckBox
AnchorSideLeft.Control = cbShowCompleteness
AnchorSideLeft.Side = asrBottom
AnchorSideTop.Control = Panel1
Left = 165
Height = 19
Top = 6
Width = 59
BorderSpacing.Left = 6
BorderSpacing.Around = 6
Caption = 'Rotated'
TabOrder = 1
OnChange = cbRotatedChange
end
end end
object GanttChartToolset: TChartToolset object GanttChartToolset: TChartToolset
Left = 120 Left = 120
@ -86,4 +114,16 @@ object GanttFrame: TGanttFrame
Left = 120 Left = 120
Top = 104 Top = 104
end end
object Tasks_ChartSource: TUserDefinedChartSource
OnGetChartDataItem = Tasks_ChartSourceGetChartDataItem
XCount = 2
Left = 120
Top = 32
end
object Completed_ChartSource: TUserDefinedChartSource
OnGetChartDataItem = Tasks_ChartSourceGetChartDataItem
XCount = 2
Left = 248
Top = 32
end
end end

View File

@ -5,12 +5,16 @@ unit uGanttFrame;
interface interface
uses uses
Classes, SysUtils, Graphics, Forms, Controls, SysUtils, Classes, Math,
TAGraph, TAChartUtils, TAIntervalSources, TASources, TATools, Graphics, Forms, Controls, StdCtrls, ExtCtrls,
TACustomSeries, TASeries, TAMultiSeries; TAGraph, TAChartUtils, TACustomSource, TAIntervalSources, TASources,
TACustomSeries, TASeries, TAMultiSeries, TATools,
uGanttData;
type type
TGanttFrame = class(TFrame) TGanttFrame = class(TFrame)
cbShowCompleteness: TCheckBox;
cbRotated: TCheckBox;
DateTimeIntervalChartSource: TDateTimeIntervalChartSource; DateTimeIntervalChartSource: TDateTimeIntervalChartSource;
GanttChart: TChart; GanttChart: TChart;
GanttChartToolset: TChartToolset; GanttChartToolset: TChartToolset;
@ -19,16 +23,22 @@ type
GanttPanDragTool: TPanDragTool; GanttPanDragTool: TPanDragTool;
GanttSeries: TStateSeries; GanttSeries: TStateSeries;
GanttZoomDragTool: TZoomDragTool; GanttZoomDragTool: TZoomDragTool;
MilestoneSeries: TLineSeries; Panel1: TPanel;
TasksChartSource: TListChartSource; Tasks_ChartSource: TUserDefinedChartSource;
procedure GanttSeriesBarHeightChanged(Sender: TObject); Completed_ChartSource: TUserDefinedChartSource;
procedure GanttSeriesGetMarkText(ASeries: TChartSeries; APointIndex, AXIndex procedure cbRotatedChange(Sender: TObject);
, AYIndex: Integer; var AFormattedMark: String); procedure cbShowCompletenessChange(Sender: TObject);
procedure GanttSeriesGetMarkText({%H-}ASeries: TChartSeries;
APointIndex, {%H-}AXIndex, {%H-}AYIndex: Integer; var AFormattedMark: String);
procedure Tasks_ChartSourceGetChartDataItem(ASource: TUserDefinedChartSource;
AIndex: Integer; var AItem: TChartDataItem);
private private
FTasks: TGanttTaskList;
procedure PrepareData; procedure PrepareData;
public public
constructor Create(AOwner: TComponent); override; constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end; end;
@ -39,73 +49,161 @@ implementation
constructor TGanttFrame.Create(AOwner: TComponent); constructor TGanttFrame.Create(AOwner: TComponent);
begin begin
inherited; inherited;
FTasks := TGanttTaskList.Create;
FTasks.AddTask('Kick-off', EncodeDate(2024,2,3), 1, 100);
FTasks.AddTask('Activity 1', EncodeDate(2024,2,4), 4, 100);
FTasks.AddTask('Activity 2', EncodeDate(2024,2,8), 3, 100);
FTasks.AddTask('Activity 3', EncodeDate(2024,2,11), 10, 100);
FTasks.AddMilestone('Intermediate Milestone', EncodeDate(2024,2,21), true);
FTasks.AddTask('Activity 4', EncodeDate(2024,2,21), 7, 80);
FTasks.AddTask('Activity 5', EncodeDate(2024,2,28), 8, 10);
FTasks.AddMilestone('Final Milestone', EncodeDate(2024,3,7), false);
PrepareData; PrepareData;
end; end;
procedure TGanttFrame.GanttSeriesGetMarkText(ASeries: TChartSeries; destructor TGanttFrame.Destroy;
APointIndex, AXIndex, AYIndex: Integer; var AFormattedMark: String);
var
tPlan1, tPlan2: Double;
tComplete1, tComplete2: Double;
txt: String;
begin begin
txt := GanttSeries.Source.Item[APointIndex]^.Text; FTasks.Free;
tPlan1 := GanttSeries.XValues[APointIndex, 0]; inherited Destroy;
tPlan2 := GanttSeries.XValues[APointIndex, 1];
tComplete1 := GanttCompletedSeries.XValues[APointIndex, 0];
tComplete2 := GanttCompletedSeries.XValues[APointIndex, 1];
AFormattedMark := Format('Task "%s": %.0f %% complete', [txt, (tComplete2 - tComplete1)/(tPlan2 - tPlan1)*100]);
end; end;
procedure TGanttFrame.GanttSeriesBarHeightChanged(Sender: TObject); // Checkbox clicked to toggle between "normal" and "rotated" state series
var // (horizontal or vertical orientation)
h: Integer; procedure TGanttFrame.cbRotatedChange(Sender: TObject);
begin begin
if GanttChart.ScalingValid then if cbRotated.Checked then
begin begin
h := round(GanttSeries.GetImgBarHeight * 0.66); GanttSeries.AxisIndexX := 0;
MilestoneSeries.Pointer.HorizSize := h; GanttSeries.AxisIndexY := 1;
MilestoneSeries.Pointer.VertSize := h; GanttCompletedSeries.AxisIndexX := 0;
GanttCompletedSeries.AxisIndexY := 1;
// Bottom axis marks
GanttChart.BottomAxis.Marks.Source := Tasks_ChartSource;
GanttChart.BottomAxis.Marks.Style := smsLabel;
GanttChart.BottomAxis.Marks.SourceExchangeXY := true;
GanttChart.BottomAxis.Marks.LabelFont.Orientation := 900;
GanttChart.BottomAxis.Grid.Visible := false;
// Left axis marks
GanttChart.LeftAxis.Marks.Source := DateTimeIntervalChartSource;
GanttChart.LeftAxis.Marks.Style := smsLabel;
GanttChart.LeftAxis.Grid.Visible := true;
GanttChart.Margins.Top := 0;
GanttChart.Margins.Left := 10;
end else
begin
GanttSeries.AxisIndexX := 1;
GanttSeries.AxisIndexY := 0;
GanttCompletedSeries.AxisIndexX := 1;
GanttCompletedSeries.AxisIndexY := 0;
// Left axis marks
GanttChart.LeftAxis.Marks.Source := Tasks_ChartSource;
GanttChart.LeftAxis.Marks.Style := smsLabel;
GanttChart.LeftAxis.Grid.Visible := false;
// Bottom axis marks
GanttChart.BottomAxis.Marks.Source := DateTimeIntervalChartSource;
GanttChart.BottomAxis.Marks.Style := smsLabel;
GanttChart.BottomAxis.Marks.SourceExchangeXY := false;
GanttChart.BottomAxis.Marks.LabelFont.Orientation := 0;
GanttChart.BottomAxis.Grid.Visible := true;
GanttChart.Margins.Left := 0;
GanttChart.Margins.Top := 10;
end; end;
end; end;
procedure TGanttFrame.cbShowCompletenessChange(Sender: TObject);
begin
GanttCompletedSeries.Active := cbShowCompleteness.Checked;
end;
{ Constructs the Mark text to be used in the popup hint. }
procedure TGanttFrame.GanttSeriesGetMarkText(ASeries: TChartSeries;
APointIndex, AXIndex, AYIndex: Integer; var AFormattedMark: String);
const
NO_Yes: array[boolean] of string = ('not ', '');
var
t, tPlan1, tPlan2: Double;
completeness: Double;
txt: String;
item: TBasicGanttItem;
begin
item := FTasks[APointIndex];
txt := item.Title;
if item is TGanttTask then
begin
tPlan1 := TGanttTask(item).StartDate;
tPlan2 := TGanttTask(item).EndDate;
completeness := TGanttTask(item).PercentageComplete;
AFormattedMark := Format(
'Task "%s":' + LineEnding +
'⯄ %s - %s' + LineEnding +
'⯄ %.0f%% complete', [
txt, DateToStr(tPlan1), DateToStr(tPlan2), completeness
]);
end
else
if item is TGanttMilestone then
begin
t := TGanttMileStone(item).DateDue;
AFormattedMark := Format(
'"%s":' + LineEnding +
'⯄ due %s' + LineEnding +
'⯄ %scomplete', [
txt, DateToStr(t), NO_YES[TGanttMilestone(item).Complete]
]);
end;
end;
{ Extracts the data for the data point at index AIndex and transfers them
to the chartdataitem AItem needed by the series. }
procedure TGanttFrame.Tasks_ChartSourceGetChartDataItem(ASource:
TUserDefinedChartSource; AIndex: Integer; var AItem: TChartDataItem);
var
dt1, dt2: TDateTime;
begin
if FTasks[AIndex] is TGanttTask then
begin
dt1 := TGanttTask(FTasks[AIndex]).StartDate;
dt2 := TGanttTask(FTasks[AIndex]).EndDate;
if ASource = Completed_ChartSource then
dt2 := dt1 + TGanttTask(FTasks[AIndex]).PercentageComplete/100 * (dt2 - dt1);
end else
if FTasks[AIndex] is TGanttMilestone then
begin
if TGanttMilestone(FTasks[AIndex]).Complete or (ASource = Tasks_ChartSource) then
dt1 := TGanttMilestone(FTasks[AIndex]).DateDue
else
dt1 := NaN;
dt2 := dt1;
end;
AItem.Text := FTasks[AIndex].Title;
AItem.SetX(0, dt1);
AItem.SetX(1, dt2);
AItem.SetY(AIndex);
end;
procedure TGanttFrame.PrepareData; procedure TGanttFrame.PrepareData;
begin begin
TasksChartSource.Add(0, 0, 'Kick-off'); // Series with planned tasks
TasksChartSource.Add(1, 1, 'Activity 1'); Tasks_ChartSource.XCount := 2;
TasksChartSource.Add(2, 2, 'Activity 2'); Tasks_ChartSource.PointsNumber := FTasks.Count;
TasksChartSource.Add(3, 3, 'Activity 3'); GanttSeries.Source := Tasks_ChartSource;
TasksChartSource.Add(4, 4, 'Intermediate Milestone');
TasksChartSource.Add(5, 5, 'Activity 4');
TasksChartSource.Add(6, 6, 'Activity 5');
TasksChartSource.Add(7, 7, 'Final Milestone');
// The x/y values in the TasksChartSource are the y values for the // Series showing to which percentage the planned tasks are completed
// GanttSeries data. Completed_ChartSource.XCount := 2;
Completed_ChartSource.PointsNumber := FTasks.Count;
GanttCompletedSeries.Source := Completed_ChartSource;
GanttCompletedSeries.ZPosition := 1; // Draw it before GanttSeries
// GanttSeries represents the project plan // Show the task titles as y axis labels
GanttSeries.AddXY(EncodeDate(2024,2,3), EncodeDate(2024,2,4), 0, TasksChartSource.Item[0]^.Text); GanttChart.LeftAxis.Marks.Source := Tasks_ChartSource;
GanttSeries.AddXY(EncodeDate(2024,2,4), EncodeDate(2024,2,8), 1, TasksChartSource.Item[1]^.Text);
GanttSeries.AddXY(EncodeDate(2024,2,8), EncodeDate(2024,2,11), 2, TasksChartSource.Item[2]^.Text);
GanttSeries.AddXY(EncodeDate(2024,2,11), EncodeDate(2024,2,21), 3, TasksChartSource.Item[3]^.Text);
GanttSeries.AddXY(EncodeDate(2024,2,21), EncodeDate(2024,2,28), 5, TasksChartSource.Item[5]^.Text);
GanttSeries.AddXY(EncodeDate(2024,2,28), EncodeDate(2024,3,7), 6, TasksChartSource.Item[6]^.Text);
// GanttCompletedSeries contains only the activities from GanttSeries which
// are completed to some extent
GanttCompletedSeries.AddXY(GanttSeries.XValues[0,0], GanttSeries.XValues[0,1], 0); // Kick-off is 100% finished
GanttCompletedSeries.AddXY(GanttSeries.XValues[1,0], GanttSeries.XValues[1,1], 1); // Activity 1 is 100% finished
GanttCompletedSeries.AddXY(GanttSeries.XValues[2,0], GanttSeries.XValues[2,1], 2); // Activity 2 is 100% finished
GanttCompletedSeries.AddXY(GanttSeries.XValues[3,0], GanttSeries.XValues[3,1], 3); // Activity 3 is 100% finished
GanttCompletedSeries.AddXY(GanttSeries.XValues[4,0], GanttSeries.XValues[4,0]+6, 5); // Activity 4 is almost finished
GanttCompletedSeries.AddXY(GanttSeries.XValues[5,0], GanttSeries.XValues[5,0]+1, 6); // Activity 6 has a lot left to be done
GanttCompletedSeries.ZPosition := 1; // draw this series over the GanttSeries
// Milestones
Milestoneseries.AddXY(EncodeDate(2024,2,21), 4, '', clGreen); // Milestone 1 reached
Milestoneseries.AddXY(EncodeDate(2024,3,7), 7, '', clSilver); // Milestone 2 open
GanttChart.LeftAxis.Marks.Source := TasksChartSource;
GanttChart.LeftAxis.Marks.Style := smsLabel; GanttChart.LeftAxis.Marks.Style := smsLabel;
GanttChart.LeftAxis.Inverted := true; GanttChart.LeftAxis.Inverted := true;
end; end;

View File

@ -6,8 +6,8 @@ object MachineStateFrame: TMachineStateFrame
ClientHeight = 343 ClientHeight = 343
ClientWidth = 988 ClientWidth = 988
TabOrder = 0 TabOrder = 0
DesignLeft = 587 DesignLeft = 594
DesignTop = 331 DesignTop = 432
object Chart: TChart object Chart: TChart
Left = 0 Left = 0
Height = 287 Height = 287

View File

@ -691,7 +691,7 @@ var
end; end;
const const
MAX_COUNT = 1000; // Arbitraty limit to prevent OOM in case of a bug. MAX_COUNT = 1000; // Arbitrary limit to prevent OOM in case of a bug.
var var
i, cnt: Integer; i, cnt: Integer;
x, start, stepLen: Double; x, start, stepLen: Double;

View File

@ -2702,11 +2702,9 @@ var
pointIndex: Integer; pointIndex: Integer;
scaledDepth: Integer; scaledDepth: Integer;
procedure DrawBar(const ARect: TRect); procedure PrepareDrawer;
var var
sz: TSize;
c: TColor; c: TColor;
// ic: IChartTCanvasDrawer; -- maybe later...
begin begin
ADrawer.Pen := FBarPen; ADrawer.Pen := FBarPen;
if FBarPen.Color = clDefault then if FBarPen.Color = clDefault then
@ -2722,23 +2720,43 @@ var
c := Source[pointIndex]^.Color; c := Source[pointIndex]^.Color;
if c <> clTAColor then if c <> clTAColor then
ADrawer.BrushColor := c; ADrawer.BrushColor := c;
end;
procedure DrawDiamond(const ARect: TRect);
var
P: array of TPoint = nil;
sz: Integer;
ctr: TPoint;
begin
PrepareDrawer;
ctr := Point((ARect.Left + ARect.Right) div 2, (ARect.Top + ARect.Bottom) div 2);
if IsRotated then
sz := (ARect.Right - ARect.Left) div 2
else
sz := (ARect.Bottom - ARect.Top) div 2;
SetLength(P, 4);
P[0] := Point(ctr.X - sz, ctr.Y);
P[1] := Point(ctr.X, ctr.Y - sz);
P[2] := Point(ctr.X + sz, ctr.Y);
P[3] := Point(ctr.X, ctr.Y + sz);
ADrawer.Polygon(P, 0, 4);
end;
procedure DrawBar(const ARect: TRect);
var
c: TColor;
sz: TSize;
begin
PrepareDrawer;
sz := Size(ARect); sz := Size(ARect);
if (sz.cx <= 2*FBarPen.Width) or (sz.cy <= 2*FBarPen.Width) then begin if (sz.cx <= 2*FBarPen.Width) or (sz.cy <= 2*FBarPen.Width) then begin
// Bars are too small to distinguish the border from the interior. // Bars are too small to distinguish the border from the interior.
ADrawer.SetPenParams(psSolid, ADrawer.BrushColor); ADrawer.SetPenParams(psSolid, ADrawer.BrushColor);
end; end;
(* todo --- add me
if Assigned(FOnCustomDrawBar) then begin
FOnCustomDrawBar(Self, ADrawer, AR, pointIndex, stackIndex);
exit;
end;
if Supports(ADrawer, IChartTCanvasDrawer, ic) and Assigned(OnBeforeDrawBar) then
OnBeforeDrawBar(Self, ic.Canvas, AR, pointIndex, stackIndex, defaultDrawing);
if not defaultDrawing then exit; *)
ADrawer.Rectangle(ARect); ADrawer.Rectangle(ARect);
if scaledDepth > 0 then begin if scaledDepth > 0 then begin
c := ADrawer.BrushColor; c := ADrawer.BrushColor;
ADrawer.BrushColor := GetDepthColor(c, true); ADrawer.BrushColor := GetDepthColor(c, true);
@ -2753,6 +2771,7 @@ var
var var
ext2: TDoubleRect; ext2: TDoubleRect;
p: TDoublePoint; p: TDoublePoint;
ctr: TPoint;
procedure BuildBar(x1, x2, y, barH: Double); procedure BuildBar(x1, x2, y, barH: Double);
var var
@ -2778,7 +2797,10 @@ var
if (Left = Right) and IsRotated then Dec(Left); if (Left = Right) and IsRotated then Dec(Left);
if (Bottom = Top) and not IsRotated then Inc(Bottom); if (Bottom = Top) and not IsRotated then Inc(Bottom);
end; end;
DrawBar(imageBar); if x1 <> x2 then
DrawBar(imageBar)
else
DrawDiamond(imageBar);
end; end;
var var
@ -2812,8 +2834,12 @@ begin
with Source[pointIndex]^ do with Source[pointIndex]^ do
begin begin
x1 := AxisToGraphX(GetX(0)); x1 := GetX(0);
x2 := AxisToGraphX(GetX(1)); x2 := GetX(1);
if IsNaN(x1) then
Continue;
x1 := AxisToGraphX(x1);
x2 := AxisToGraphX(x2);
end; end;
BuildBar(x1, x2, p.Y, h); BuildBar(x1, x2, p.Y, h);