The final step in creating a new chart type is drawing the chart type. Throughout our tutorial, we have been building the structures so that we can actually draw our new chart type. Last step, we finished customizing our chart class. In this step, we finish the tutorial by writing the draw code for our graph class. We will start by discussing some general do's and don'ts when you write your drawing routines and then go into the specifics of drawing our graph chart.
First, here are general rules for drawing charts with the Charting Component Suite:
• | Never override the public TRSCustomChart.Draw method, override the protected InternalDraw method. The InternalDraw method is intended to be overridden. The Draw method makes sure your chart is in the drawing state and fires events before and after you draw. |
• | Never directly convert the chart's values to canvas coordinates, use the chart's HorizontalAxis and VerticalAxis to call the AxisToPixel method. A chart's values are consistent with the chart only and have nothing to do with the canvas. These values specify chart graphical elements in floating point format and ignore any visual issues, such as is the chart zoomed. Every chart is bound to a horizontal axis (top or bottom) and vertical axis (left or right) of the chart panel. The axes are responsible for tracking if the view is zoomed, what are the min and max values etc. Call the AxisToPixel method to convert your chart value into the correct canvas value. |
• | Never assume the Canvas you are drawing on is the TRSChartPanel's Canvas. The Charting Component Suite has been designed so that all elements draw to a canvas that they are given and do not assume who owns the canvas. This allows you, the programmer, to draw a chart anywhere by calling the Draw method with a different canvas. For example, you can easily print a chart by passing in a TPrinter canvas. |
• | Use, if possible, FirstIndex and LastIndex to speed up drawing. The FirstIndex and LastIndex properties specify the indices of the Values that are currently being drawn in the chart. When the TRSChartPanel component has been zoomed, these properties reflect the point just before the first point in the zoomed area and the point just after the last point in the zoomed area (this ensures the visual aspect of the chart by allowing, for example, lines to be drawn into and out of the zoomed area. The FirstIndex and LastIndex properties provide more efficient access to only the values that are currently being drawn. |
Now, we will get into the specifics of drawing our graph chart. We are descending from the TRSShapeChart class, which adds a DrawShape method, already overrides the InternalDraw method and iterates through the FirstIndex to LastIndex values calling DrawShape. Unfortunately, this will not work for us as is.
First, we override the DrawShape method. In this method, we call inherited to draw the node. Then, we draw all the links of this node. For our graph, we created a DrawLinks method and a DrawLink method. The DrawLinks method will iterate over all the links of a node. If the link has a connection (e.g., Value is not nil), we call the DrawLink method to actually draw the link.
The DrawLink method deserves special attention. This first action we need to do in this level is find out where the starting and ending point of our link is on the canvas. So, we need to convert the starting and ending Shapes' rectangles from their internal coordinates to the Canvas' coordinates. Luckily, the TRSShapeChart already provides us with a method to do this (GetShapeRect protected method). After we have their rectangles, we need to decide where on the rectangle is the starting or ending point. Some properties in the code of a link that we didn't discuss in this tutorial is ConnectionPoint and ValueConnectionPoint. Using these properties, the rectangles we just calculated, and the RSGraphics.GetPointOnRect function, we are able to obtain our starting and ending point. At this point, we call the RSGraphics.DrawArrow function to actually draw our link.
Finally, we need to override the InternalDraw method. Unfortunately, iterating from FirstIndex to LastIndex does not work for us because we can have a node, outside of a zoomed view, linking with a node inside (or even worse, also outside) the zoomed view. If we do not call the DrawShape for that invisible node, its links will not get drawn. So, we overrode the InternalDraw method and iterated through all the nodes/values in the chart.
Here is the new code:
TRSGraphChart = class(TRSShapeChart)
private
{ Private declarations }
protected
{ Protected declarations }
procedure DrawLink( const Canvas: TCanvas; ARect: TRect; Link: TRSGraphChartLink ); virtual;
procedure DrawLinks(const Canvas: TCanvas; ARect: TRect;
Value: TRSGraphChartValue); virtual;
procedure DrawShape(const Canvas: TCanvas; ARect: TRect;
Value: TRSShapeChartValue); override;
procedure InternalDraw(const Canvas: TCanvas; ARect: TRect); override;
public
{ Public declarations }
published
{ Published declarations }
end; { TRSGraphChart }
{ TRSGraphChart }
procedure TRSGraphChart.DrawLink(const Canvas: TCanvas; ARect: TRect;
Link: TRSGraphChartLink);
var
ShapeRect: TRect;
LinkPoint: TPoint;
OriginPoint: TPoint;
Points: TPoints;
i: Integer;
begin
// first, translate the shape's arect to this arect
ShapeRect := GetShapeRect( ARect, Link.Collection.Owner );
OriginPoint := GetPointOnRect( ShapeRect, Link.ConnectionPoint );
ShapeRect := GetShapeRect( ARect, Link.Value );
LinkPoint := GetPointOnRect( ShapeRect, Link.ValueConnectionPoint );
// draw arrow from bottom of shape to top of link shape
if Length(Link.LinePoints) > 0 then
begin
SetLength(Points, Length(Link.LinePoints) + 2);
Points[0] := OriginPoint;
for i := 0 to Length(Link.LinePoints) - 1 do
Points[i+1] := Point( HorizontalAxis.AxisToPixel(Link.LinePoints[i].X, ARect),
VerticalAxis.AxisToPixel(Link.LinePoints[i].Y+GetAdjOffsetVertical, ARect) );
Points[Length(Points)-1] := LinkPoint;
DrawArrow( Canvas, Points, 10, Link.Caption );
end
else
DrawArrow( Canvas, OriginPoint.X, OriginPoint.Y, LinkPoint.X, LinkPoint.Y,
10, Link.Caption );
end;
procedure TRSGraphChart.DrawLinks(const Canvas: TCanvas; ARect: TRect;
Value: TRSGraphChartValue);
var
i: Integer;
begin
// draw the links
with Value do
for i := 0 to Links.Count - 1 do
begin
if (Links[i].Value = nil) or (not Links[i].Value.Visible) then Continue;
DrawLink( Canvas, ARect, Links[i] );
end;
end;
procedure TRSGraphChart.DrawShape(const Canvas: TCanvas; ARect: TRect;
Value: TRSShapeChartValue);
begin
inherited DrawShape(Canvas, ARect, Value);
// draw the links
DrawLinks( Canvas, ARect, TRSGraphChartValue(Value) );
end;
procedure TRSGraphChart.InternalDraw(const Canvas: TCanvas; ARect: TRect);
var
i: Integer;
begin
Canvas.Font := Font;
Canvas.Brush := Brush;
Canvas.Pen := Pen;
for i := 0 to Values.Count - 1 do
if Values[i].Visible then
DrawShape( Canvas, ARect, Values[i] );
end;
Well, that's it! Our graph chart is complete. We definitely encourage you to check out the source code in the RSGraphCharts unit. It contains additional properties and methods we did not get into here for purposes of clarity. Hopefully, this tutorial will help you to create your own charts. If you do write a chart type, consider submitting it to us at support@RiverSoftAVG.com (if you don't already have a commercial license to the RCCS and we accept it, you will have earned a license). If you have any questions or comments about this tutorial, please write at the above email address.