Hide Comments
Hide Comments

Comments (0)

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

Comments (0)

RiverSoftAVG Charting Component Suite (RCCS) © 2005-2015, Thomas G. Grubb