TAChart Tutorial: Background design
│
English (en) │
suomi (fi) │
Introduction
Standard charts created by the TAChart package have a gray background. Using the Color
property it is possibly to modify the color of the entire chart background, and the BackColor
controls the color of the chart area enclosed by the axes, the so-called "back wall". In any case, the design is quite conventional compared to other charting packages and applications. Is it possible to give the chart background a more interesting design?
In this tutorial, we will demonstrate that this is a relatively easy task by taking advantage of one or two events of TChart. We will create a plot charting changes in the number of Lazarus downloads per year over recent years. To highlight the connection with Lazarus we plan to show the Lazarus splash screen as a background image of the chart.
Preparation
Data
Sourceforge provides a list of the Lazarus download counts per month. From http://sourceforge.net/projects/lazarus/files/Lazarus%20Windows%2032%20bits/stats/timeline?dates=2003-01-01+to+2014-09-02 I extracted the download count per year for the windows-32 installation file:
Year | 2005 | 2006 | 2007 | 2008 | 2009 | 2010 | 2011 | 2012 | 2013 |
---|---|---|---|---|---|---|---|---|---|
Downloads | 53299 | 119613 | 158060 | 218915 | 190567 | 230108 | 267858 | 298335 | 280586 |
These data will be basis of our chart.
Setting up the chart
Create a new project and add a TChart
to the form. Client-align it such that the chart fills the entire form. Drag the form borders such that the window is about 400 pixels wide and 300 pixels high.
We will plot the "Years" along the x
axis, and the "Download count" along the y
axis of the chart. Therefore, go to bottom axis, find the property Title
and enter "Years" in the field for Caption
. Show the title by setting its property Visible
to true
. Adapt other properties to your liking: I often prefer to have the title in bold font and larger size (12). And sometimes the grid is annoying, you can turn if off by setting the Visible
property of the axis' Grid
to false
. Repeat with the vertical axis, the title should be named "Downloads per year".
We should also show a title above the chart. Select the text "Lazarus downloads" for the Text
of the chart's Title
. Again, set the property Visible
to true
in order to show the title. You may also want to switch the title font to bold and increase its size a bit.
A good series type for time-series data is a bar series. At first we add a TBarSeries
to the chart: Double-click on the chart to open the series editor, click "Add" and select "Bar series" from the dropdown list. We do not see the series because it has no data yet.
There are various ways to assign data to the series. Since we have static data which do not change during the program it is convenient to use a TListSource
which provides a datapoint editor for entering data at design-time. You find the ListChartSource in the "Chart" component palette, it is the second icon. In order to enter data click on the ellipsis button next to the property DataPoints
of the component - this opens the DataPoints editor. Enter the data from the table above into the editor grid: the "year" goes into the "X" column, the "downloads" go into the "Y" column. There is no need to enter anything into the "Color" and "Text" columns.
Finally, we connect the ListChartSource to the BarSeries. Each series has a property Source
. Click on the dropdown arrow and select the ListChartSource from the list. Immediately the bar chart shows up.
The TChart painting events
So far, the project has been standard. Now let's intercept the drawing process...
TChart
offers several events where our own code can be hooked in:
- At the beginning of the drawing pipeline there is
OnChartPaint
. This event is marked as experimental and can only be reached from code, not from the object inspector. It fires before anything is drawn by the chart and offers avar
parameterADoDefaultDrawing
. Setting this one tofalse
, therefore, by-passes the entire built-in drawing process and allows to paint the chart completely on your own. A very exciting feature, but too drastic for our purpose... - The next event happens before the background of the entire chart is drawn:
OnBeforeDrawBackground
. It, again, has theADoDefaultDrawing
parameter. If we set it tofalse
we can replace the background drawn by the chart by our own procedures. Very good for our purpose. If left attrue
the chart will paint its standard background that we can control by the object inspector. - After the background has been painted - either the default or the custom one - there is another event,
OnAfterDrawBackground
. It can be used to paint something on the background which is always underneath the series or axes which are painted later. - Still in the very early stages, we have the
OnBeforeDrawBackwall
event. It fires before the area enclosed by the axis rectangle ("back wall") is filled by its background color/pattern. Taking advantage of theADoDefaultDrawing
parameter again we can replace the back wall by our own procedure. Exactly what we need! - Then the chart paints the titles, the axes, the grid, the series, and the legend. Unfortunately, there are no events to intercept their painting (except for the legend which provides an
OnDrawLegend
event). But we do have some control of the drawing process within a group of chart elements by means of the theirZPosition
property. If, for example, you have a chart with a bar and a line series and do not want the line series to be partly covered by the bars you have to give the line series a greaterZPosition
than the bar series - the element with the largerZPosition
is drawn later, i.e. on top of the element with the smallerZPosition
. - Only at the very end of the drawing process there is another event,
OnAfterDraw
. It is usually intended for administrative purposes, for example measuring the painting time by reading a clock that was started in theOnChartPaint
event.
Showing the background image
The Lazarus logo makes a nice background for our chart of Lazarus download counts. We want to paint it in the chart area enclosed by the axis, the back wall. As you saw above in the listing of chart event there is an event which perfectly fits our need: OnBeforeDrawBackwall
.
At first we have to make the logo available to the program. The logo file has the name "splash_logo.png" and can be found in the folder "images" of your Lazarus installation. For simplicity, copy it to the folder which will hold the exe file of our project (or use the full file path in the code below). Since the logo is a png file we need an instance of a TPortableNetworkGraphic, or - which is more flexible - a TPicture. Let's declare a variable FBackImage
of the latter type to the form and add code in the form's OnCreate event to load the image file. Of course, don't forget to free the image when the program closes:
type
TForm1 = class(TForm)
...
private
FBackImage: TPicture;
...
procedure TForm1.FormCreate(Sender: TObject);
begin
FBackImage := TPicture.Create;
FBackImage.LoadFromFile('splash_logo.png');
// or: FBackImage.LoadFromFile('c:\lazarus\images\splash_logo.png');
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FBackImage.Free;
end;
Loading the image directly from file requires that the image is distributed along with the exe file. This can be avoided if the image is added as a resource to the binary. For this purpose you have to create a resource file containing the image - see the wiki article .... on how to do that. The Lazarus images folder contains a ready-made resource file with the logo, "splash_logo.res". If you want to use this approach, copy this file to the project folder and use this alternative OnCreate
code (don't forget to add {$R splash_logo.res}
at the beginning of the implemenation
section of the unit):
...
implementation
{$R *.lfm}
{$R splash_logo.res}
procedure TForm1.FormCreate(Sender: TObject);
begin
FBackImage := TPicture.Create;
FBackImage.LoadFromResourceName(HInstance, 'splash_logo');
end;
Whichever method you decide to use - now it's the time to add code to the OnBeforeDrawBackwall
event of the chart. It is very simple: we just "stretch-draw" the image on the canvas into the rectangle provided as a parameter, and - of course - we set the ADeDefaultDrawing
to false
to by-pass the default painting method. It is important that you draw on the canvas provided as a parameter, not on the chart's canvas, because painting of the chart is executed by special backend classes which can provide different canvases, for example, if the chart is to be printed.
procedure TForm1.Chart1BeforeDrawBackWall(ASender: TChart; ACanvas: TCanvas;
const ARect: TRect; var ADoDefaultDrawing: Boolean);
begin
ACanvas.StretchDraw(ARect, FBackImage.Graphic);
ADoDefaultDrawing := false;
end;
Because the event handler is run-time code you have to compile the program to see the current state of the chart:
Showing a background gradient
In the outer region of the chart we still see the famous gray background. How about replacing it by a decent gradient? From the predominant blue colors of the Lazarus logo we could select a vertical transition from bright blue (clSkyBlue
) at the top to white a the bottom.
We know from the summary of painting events that the OnBeforeDrawBackground
is the right event which we can use to draw the chart background on our own. But how to paint a gradient? Very easy: the canvas has a method GradientFill
which accepts start and end colors and the gradient direction.
procedure TForm1.Chart1BeforeDrawBackground(ASender: TChart; ACanvas: TCanvas;
const ARect: TRect; var ADoDefaultDrawing: Boolean);
begin
ACanvas.GradientFill(ARect, clSkyBlue, clWhite, gdVertical);
ADoDefaultDrawing := false;
end;
A note on Lazarus version 1.9 or newer
If you have Lazarus version 1.9 or newer you many notice that the events OnBeforeDrawBackground
and OnBeforeDrawBackWall
are marked as deprecated, it is recommended to use OnBeforeCustomDrawBackground
and OnBeforeCustomDrawBackWall
. This was introduced because the new events support the TAChart drawing backend architecture which enables drawing on devices which do not have an LCL-compatible canvas (e.g. an OpenGL rendering context, or an SVG file). A chart drawer essentially provides the same commands as TCanvas, however, some differences exist here and there - unfortunately also in drawing of images and gradients: Images are requested as descendants of TFPCustomImage. And gradients are not supported at all, but may be provided after painting on an auxiliary bitmap.
Here is the code which works for the OnBeforeCustomDrawBackWall
event:
procedure TForm1.Chart1BeforeCustomDrawBackWall(ASender: TChart;
ADrawer: IChartDrawer; const ARect: TRect; var ADoDefaultDrawing: Boolean);
var
bmp: TBitmap;
img: TLazIntfImage;
begin
img := TLazIntfImage.Create(0, 0);
try
bmp := TBitmap.Create;
try
bmp.SetSize(ARect.Right - ARect.Left, ARect.Bottom - ARect.Top);
bmp.Canvas.StretchDraw(Rect(0, 0, bmp.Width, bmp.Height), FBackImage.Graphic);
img.LoadFromBitmap(bmp.Handle, bmp.MaskHandle);
finally
bmp.Free;
end;
ADrawer.PutImage(ARect.Left, ARect.Top, img);
ADoDefaultDrawing := false;
finally
img.Free;
end;
end;
The image painting command of the drawer is PutImage
. It gets the image as a TLazIntfImage
which, unlike TBitmap, is a descendant of TFPCustomImage. The originally loaded picture is stretch-drawn onto an auxiliary bitmap because the drawer does not support this operation. This auxiliary bitmap, finally is passed to the LazIntfImage
via its method LoadFromBitmap
.
The OnBeforeCustomDrawBackground
event handler follows the same principle. But now the auxiliary bitmap is painted with the gradient requested.
procedure TForm1.Chart1BeforeCustomDrawBackground(ASender: TChart;
ADrawer: IChartDrawer; const ARect: TRect; var ADoDefaultDrawing: Boolean);
var
bmp: TBitmap;
img: TLazIntfImage;
begin
img := TLazIntfImage.Create(0, 0);
try
bmp := TBitmap.Create;
try
bmp.SetSize(ARect.Right - ARect.Left, ARect.Bottom - ARect.Top);
bmp.Canvas.GradientFill(Rect(0, 0, bmp.Width, bmp.Height), clSkyBlue, clWhite, gdVertical);
img.LoadFromBitmap(bmp.Handle, bmp.MaskHandle);
finally
bmp.Free;
end;
ADrawer.PutImage(ARect.Left, ARect.Top, img);
ADoDefaultDrawing := false;
finally
img.Free;
end;
end;
Finishing up
The red bars of the series look a bit "aggressive". How about a more decent sky-blue again? Select the bar series in the object inspector and set the SeriesColor
to clSkyBlue
.
And it would be better to see more of the Lazarus cheetah which is largely covered by the bars. The bar series has a property Transparency
. The default value, 0
, means opaque, the maximum allowable value, 255
means fully transparent. A low value, such as 64
, adds some transparency to show more details of the cheetah, but does not push the bars too much in the background.
And this brings us to the chart which is displayed at the top of this page, and to the end of this tutorial.
Summary
These are the basic steps for user-painting of chart backgrounds:
- Use the
OnBeforeDrawBackWall
event to provide your own painting procedure for the area enclosed by the axis rectangle. - Similarly, the
OnBeforeDrawBackground
event for painting of the entire chart background. - In these drawing procedures set
ADoDefaultDrawing
tofalse
in order to call your own painting procedures.
Source code
The source code of this tutorial project can be found in the folder tutorials/background of trunk TAChart installations.
Project file
program backimage;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
Interfaces, // this includes the LCL widgetset
Forms, main, tachartlazaruspkg
{ you can add units after this };
{$R *.res}
begin
RequireDerivedFormResource := True;
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
Unit main.pas
unit main;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, FileUtil, TAGraph, TASeries, TASources, Forms, Controls,
Graphics, Dialogs;
type
{ TForm1 }
TForm1 = class(TForm)
Chart1: TChart;
Chart1BarSeries1: TBarSeries;
ListChartSource1: TListChartSource;
procedure Chart1BeforeDrawBackground(ASender: TChart; ACanvas: TCanvas;
const ARect: TRect; var ADoDefaultDrawing: Boolean);
procedure Chart1BeforeDrawBackWall(ASender: TChart; ACanvas: TCanvas;
const ARect: TRect; var ADoDefaultDrawing: Boolean);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ private declarations }
FBackImage: TPicture;
public
{ public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.lfm}
//{$R splash_logo.res}
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
FBackImage := TPicture.Create;
FBackImage.LoadFromFile('splash_logo.png');
// this assumes that the logo is in the same folder as the binary
// or, using resources:
//FBackImage.LoadFromResourceName(HInstance, 'splash_logo');
// Don't forget ths {$R directive above...
end;
procedure TForm1.Chart1BeforeDrawBackWall(ASender: TChart; ACanvas: TCanvas;
const ARect: TRect; var ADoDefaultDrawing: Boolean);
begin
ACanvas.StretchDraw(ARect, FBackImage.Graphic);
ADoDefaultDrawing := false;
end;
procedure TForm1.Chart1BeforeDrawBackground(ASender: TChart; ACanvas: TCanvas;
const ARect: TRect; var ADoDefaultDrawing: Boolean);
begin
ACanvas.GradientFill(ARect, clSkyBlue, clWhite, gdVertical);
ADoDefaultDrawing := false;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FBackImage.Free;
end;
end.
Form file main.lfm
object Form1: TForm1
Left = 340
Height = 300
Top = 154
Width = 400
Caption = 'Form1'
ClientHeight = 300
ClientWidth = 400
OnCreate = FormCreate
OnDestroy = FormDestroy
LCLVersion = '1.3'
object Chart1: TChart
Left = 0
Height = 300
Top = 0
Width = 400
AxisList = <
item
Grid.Color = clSkyBlue
Grid.Visible = False
Marks.Format = '%.0n'
Marks.Style = smsCustom
Minors = <>
Title.LabelFont.Height = -16
Title.LabelFont.Orientation = 900
Title.LabelFont.Style = [fsBold]
Title.Visible = True
Title.Caption = 'Downloads per year'
end
item
Grid.Visible = False
Alignment = calBottom
Minors = <>
Title.LabelFont.Height = -16
Title.LabelFont.Style = [fsBold]
Title.Visible = True
Title.Caption = 'Year'
end>
Foot.Brush.Color = clBtnFace
Foot.Font.Color = clBlue
Title.Brush.Color = clBtnFace
Title.Brush.Style = bsClear
Title.Font.Color = clNavy
Title.Font.Height = -17
Title.Font.Style = [fsBold]
Title.Text.Strings = (
'Lazarus Downloads'
)
Title.Visible = True
OnBeforeDrawBackground = Chart1BeforeDrawBackground
OnBeforeDrawBackWall = Chart1BeforeDrawBackWall
Align = alClient
ParentColor = False
object Chart1BarSeries1: TBarSeries
Shadow.Color = clNavy
Shadow.OffsetX = 4
BarBrush.Color = clSkyBlue
Source = ListChartSource1
end
end
object ListChartSource1: TListChartSource
DataPoints.Strings = (
'2005|53299|?|'
'2006|119613|?|'
'2007|158060|?|'
'2008|218915|?|'
'2009|190567|?|'
'2010|230108|?|'
'2011|267858|?|'
'2012|298335|?|'
'2013|280586|?|'
)
left = 120
top = 56
end
end