Now that we have customized the chart values, the next step in creating your new chart type is to customize your TRSCustomChart descendant. For example, you may want to add special events (such as OnNewLink or OnRemoveLink), properties or methods. Note that we will discuss the most important customization, a new draw routine for your chart in the next section. For our graph type, we will create an Arrange method, which can help the programmer to automatically arrange the nodes in your graph.
The Arrange method will be responsible for "pretty" organizing our chart. This is not an easy problem so we will definitely make this method overrideable so that people can improve it :-) At the very least, the Arrange method should ensure that there are no overlapping shapes and, hopefully, avoid links running criss-crossing throughout the chart.
For our Arrange method, we decided to ask for a little help from the programmer. A lot of graph charts, such as org charts and flow charts, when nodes/values are laid out manually are not just scattered about the chart surface. There is a definite organization, e.g., things flow from a top to a bottom (or a left to a right) and many nodes are considered equivalent so they are placed at the same level as other nodes. We decided to add an optional Level property to our chart values (TRSGraphChartValue), which is an integer property from 0 to whatever specifying the level of a node. By using this Level property, our Arrange method will divide the chart canvas into a big grid, where each cell in the grid can contain one chart value. One axis of this virtual grid will be the Levels in the chart. The other axis will be all the nodes/values at that level.
For example, suppose we had a graph made up of the following nodes:
Level 0: Startup
Level 1: Research
Level 2: Development
Level 3: Manufacturing, Testing, and Marketing
Level 4: Purchases, Sales
Our grid would look like this:
-------------------------------------------------
| | | |
| Startup | | | Level 0
| | | |
-------------------------------------------------
| | | |
| Research | | | Level 1
| | | |
-------------------------------------------------
| | | |
| Development | | | Level 2
| | | |
-------------------------------------------------
| | | |
| Manufacturing | Testing | Marketing | Level 3
| | | |
-------------------------------------------------
| | | |
| Purchases | Sales | | Level 4
| | | |
-------------------------------------------------
This is not bad, but it could be better. An obvious improvement is to "center" the nodes in their level:
-------------------------------------------------
| | | |
| | Startup | | Level 0
| | | |
-------------------------------------------------
| | | |
| | Research | | Level 1
| | | |
-------------------------------------------------
| | | |
| | Development | | Level 2
| | | |
-------------------------------------------------
| | | |
| Manufacturing | Testing | Marketing | Level 3
| | | |
-------------------------------------------------
| | |
| Purchases | Sales | Level 4
| | |
---------------------------------
Much better, but what about the sizes of the cell? We could just make every cell the same size, e.g., the size of the largest node. This would be easy but would look awful for graphs where the largest node is disportionately larger than other nodes. For our Arrange method, we will make the improvement that nodes of the same level will have the same sized cell (to make it easier to manage) but different levels can have a different level size.
In addition, since there is no reason that the graph must be top down, we are going to add an Orientation to the graph: top-down, left-right, right-left, or bottom-up.
Our Arrange method works hard to arrange the nodes/values of the graph, but what about the links? This is not an easy problem for a general solution so we are going to punt. We will hope that since the graph has this arrangement from top to bottom that the links flow the same way and that the links don't jump a lot of levels, e.g., Startup is not directly connected to Sales. To help with the links, we should put some space between the levels so that the arrows and their captions can go there. For our graph, we will allow the user to specify a buffer, or level gap, that will be empty space between the levels.
Ok, we are now ready to write the pseudo-code of our Arrange method:
For all the nodes/values in the graph
Organize them into a level
Calculate the number of nodes in each level
For all the levels in the chart
Calculate the cell size for this level
Arrange the nodes for this level by evenly distributing their X Value, their Y value is the Level's Y value
For the first part of our algorithm, sorting the nodes by level, we are going to let the base class do the work and have it sort the nodes by level. Now, our pseudo code becomes:
For all the nodes/values in the graph
Calculate the number of nodes in each level
For all the levels in the chart
Calculate the cell size for this level
Arrange the nodes for this level by evenly distributing their X Value, their Y value is the Level's Y value
And here is our code (again, earlier code is omitted for clarity):
TRSGraphChart = class(TRSShapeChart)
private
{ Private declarations }
FOrientation: TRSChartOrientation;
FLevelGapPercent: Integer;
procedure SetOrientation(const Value: TRSChartOrientation);
procedure SetLevelGapPercent(const Value: Integer);
protected
{ Protected declarations }
public
{ Public declarations }
procedure Arrange; virtual;
published
{ Published declarations }
property LevelGapPercent: Integer read FLevelGapPercent write SetLevelGapPercent default DEFAULT_LEVEL_GAP;
property Orientation: TRSChartOrientation read FOrientation write SetOrientation;
end; { TRSGraphChart }
{ TRSGraphChart }
procedure TRSGraphChart.Arrange;
function ArrangeLevel( i, j: Integer; XSize, YSize, HeightSize, WidthSize,
XLoc, YLoc, XInc, YInc: TRSChartValueType): Integer;
begin
if XInc = 0 then
begin
XInc := XSize / j;
XLoc := XInc / 2;
YInc := 0;
end
else // yInc = 0
begin
YInc := YSize / j;
YLoc := YInc / 2;
XInc := 0;
end;
for result := i to i+j-1 do
begin
// center the shape
Values[result].X := XLoc+(WidthSize - Values[result].Width) / 2;
Values[result].Y := YLoc+(HeightSize - Values[result].Height) / 2;
XLoc := XLoc + XInc;
YLoc := YLoc + YInc;
end;
result := i+j;
end;
var
i, j: Integer;
Level: Integer;
MaxNodesInLevel: Integer;
NodesInLevel: Integer;
NodesPerLevel: TGHashIntTable;
HeightSize, WidthSize: TRSChartValueType;
XLoc, YLoc: TRSChartValueType;
XInc, YInc: TRSChartValueType;
XSize, YSize: TRSChartValueType;
IsPreview: Boolean;
begin
if (not Values.Sorted) or (Values.Count < 2) then Exit;
// arrange puts the chart values in levels, based on Orientation
// this is a very simple arrangment where we will create a grid where
// one axis is the number of levels and the second axis is maximum number
// of nodes at any one level... grid size is based on maximums of dimensions
// of width and height (+10%)
// first max nodes per level, store in hash table
NodesPerLevel := TGHashIntTable.Create;
IsPreview := Preview;
Values.BeginUpdate;
try
MaxNodesInLevel := 0;
NodesInLevel := 0;
Level := Values[0].Level;
for i := 0 to Values.Count - 1 do
begin
Values[i].Links.ArrangeConnections(Orientation);
if Values[i].Level = Level then
begin
Inc(NodesInLevel);
if NodesInLevel > MaxNodesInLevel then
MaxNodesInLevel := NodesInLevel;
end
else
begin
NodesPerLevel.Put(Level+1, TRSPointerType(NodesInLevel));
Level := Values[i].Level;
NodesInLevel := 1;
if NodesInLevel > MaxNodesInLevel then
MaxNodesInLevel := NodesInLevel;
end;
end;
NodesPerLevel.Put(Level+1, TRSPointerType(NodesInLevel));
HeightSize := Values.MaxValues[Values.HeightDim]*(1+LevelGapPercent/100);
WidthSize := Values.MaxValues[Values.WidthDim]*(1+LevelGapPercent/100);
// set up variables based on orientation
case Orientation of
coTopDown:
begin
XLoc := 0;
YLoc := (HeightSize-1)*Level;
YInc := -HeightSize;
XInc := 0;
XSize := WidthSize*MaxNodesInLevel;
YSize := HeightSize*Level;
end;
coBottomUp:
begin
XLoc := 0;
YLoc := 0;
YInc := HeightSize;
XInc := 0;
XSize := WidthSize*MaxNodesInLevel;
YSize := HeightSize*Level;
end;
coLeftRight:
begin
XLoc := 0;
YLoc := 0;
YInc := 0;
XInc := WidthSize;
XSize := WidthSize*Level;
YSize := HeightSize*MaxNodesInLevel;
end;
else // coRightLeft:
begin
XLoc := (WidthSize-1)*Level;
YLoc := 0;
YInc := 0;
XInc := -WidthSize;
XSize := WidthSize*Level;
YSize := HeightSize*MaxNodesInLevel;
end;
end;
i := 0;
while i < Values.Count do
begin
j := Integer(NodesPerLevel[Values[i].Level+1]);
i := ArrangeLevel(i, j, XSize, YSize, HeightSize, WidthSize, XLoc, YLoc, XInc, YInc);
XLoc := XLoc + XInc;
YLoc := YLoc + YInc;
end;
finally
NodesPerLevel.Free;
Values.EndUpdate;
Preview := IsPreview;
end;
end;
procedure TRSGraphChart.SetLevelGapPercent(const Value: Integer);
begin
if Value <> LevelGapPercent then
begin
FLevelGapPercent := Value;
Changed;
end;
end;
procedure TRSGraphChart.SetOrientation(const Value: TRSChartOrientation);
begin
if Value <> Orientation then
begin
FOrientation := Value;
OrientationChanged;
end;
end;
Whew, that was a lot of work. More, quite frankly, than setting up the graph structures themselves. Our final step is to draw our piece de resistance, which we discuss in Step 5: Write your draw routine by overriding the TRSCustomChart.InternalDraw method