What is a web site without a graph of some sort huh ? I mean, it's just another image and web sites are full of images. Besides, the TChart control that comes with Delphi is a really nice control. It would be a shame not to be able to use it for graphical reports etc. in an ISAPI application. The TChart is an extensive graphing control. This tutorial is by no means a tutorial on how to use the TChart per se, but more on how to use it in an ISAPI application. If you're not familiar with the TChart control, it may be a good option to first play with it in a standar PC application before embarking on the ISAPI version and try to tweek the chart to the way you'd like it to be.
The Web Data Module by design does not allow us to Drop TControl descendants on it. This is normally not a problem, since an ISAPI/CGI application is a server side application. But in this case, since we want to generate a graph as an image and have the web browser display the graph as an image, we would have liked to be able to drop a TChart onto the web data module and use it. The solution is to create the chart on the fly at run time. Once we've created the chart, we can set the various properties we're interested in and have it generate the graph the way we'd like to see it.
The Project
The ISAPI application we are about to embark on here has only one action. This action expects to Query strings that give it enough information to be able to create a chart and stream it back as a JPEG image. The Query string should contain:
- The title of the Graph
- The title of each Bar and its value in the form of a Name=Value pair
The number of bars is virtually unlimited in this case due to the design of the aplication. The links below are demos of the application we are about to build.
Years Experience in Programming Languages
Motorcycles used for Years
Have a look at the source of this page to see how the parameters are being sent to the ISAPI application and relate it to the graph you see for each link.

Note that there is no need to have the "Charting" functionlaity in a seperate ISAPI application. I've done this here to isolate other ISAPI related and database related issues and to show the "demos" above.
We could/should use a single Charting ISAPI in this way if there are a number of ISAPI applications that will need this functionality. Similar to the reason one would have DLLs or COM objects as part of an application. We should be aware however of the limitations of such a design. Since the QueryString variable in IIS is limited to 48K we may fall short in really extensive parameter passing using this method (GET method). There are atleast two options:
- Use some form of coded parameters in the URL rather than human readable parameters
- Use the POST method to send "data" to this ISAPI and use the ContentFields property of the Request object inside this ISAPI.
The Code
There is only one action is this application (waDrawChart) and its default property has been set to True. All this action really does is send back an JPEG image in the form of a stream.
procedure TWebModule1.WebModule1waDrawChartAction(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
{ The Query String for this action should be of the Format :
Title=xxxx&Bar1Title=Bar1Value&Bar2Title=Bar2Value....
If "Titles" has "spaces" then they should be replaced with "_" (underscore)
this pertains to all titles
}
Response.ContentType := 'image/jpeg';
Response.ContentStream := GenerateChart;
Response.SendResponse;
end;
The meat of the work is done in the function GenerateChart. This function basically parses the query string received in the URL and constructs two dynamic arrays. One to hold the titles of each bar and the other to hold the corresponding values. This function also extracts the Title of for the Graph from the query string.
function TWebModule1.GenerateChart : TMemoryStream;
var
ChartTitle : string;
NoOfBars : Integer;
BarTitles : array of string;
BarValues : array of Integer;
i : Integer;
MemStrm : TMemoryStream;
begin
MemStrm := TMemoryStream.Create;
try
{ No of Bars is 1 less than count, since TITLE is the first parameter }
NoOfBars := Request.QueryFields.Count -1;
{ Set the size of the dynamic arrays }
SetLength(BarTitles,NoOfBars);
SetLength(BarValues, NoOfBars);
{ Get the Title of the Chart }
ChartTitle := StringReplace(Request.QueryFields.Values['Title'],'_',' ',[rfReplaceAll]);
{ Start from 1 since we don't need the Chart Title here }
for i := 1 to Request.QueryFields.Count -1 do
begin
{ Don't Remove the underscores as yet ! }
{ Dynamic arrays are zero based ! }
BarTitles[i-1] := Request.QueryFields.Names[i];
BarValues[i-1] := StrToInt(Request.QueryFields.Values[BarTitles[i-1]]);
end;
{ We now have a populated arrays of Bar Titles and their respective values }
finally
end;
DrawChart(MemStrm, ChartTitle, BarTitles, BarValues);
Result := MemStrm;
end;
Once all this information has been parsed, it in turn calls the procedure DrawChart sending it all the values along with the Memory stream object into which the graphs image should be saved.
procedure TWebModule1.DrawChart(var MemStream : TMemoryStream;ChartTitle: string;
BarTitles: array of string; BarValues : array of Integer);
var
HorizBarSeries : THorizBarSeries;
FChart : TChart;
FJPEG : TJPEGImage;
Bitmap : TBitmap;
Rect : TRect;
i : Integer;
begin
{ This is the function the actually creates the TChart on the fly and sends
back a MemoryStream (containing a JPEG image
You need to add the units: JPEG, Chart, Series, Graphics, Controls, TeEngine
to the uses clause in the implementation section.
}
FChart := TChart.Create(nil);
FJPEG := TJPEGImage.Create;
Bitmap := nil;
try
{ Initialize Chart Properties }
FChart.Color := clWhite;
FChart.BevelOuter := bvNone;
FChart.Legend.Visible := False;
{ Define the Size of the Chart Image }
Rect.Left := 0;
Rect.Top := 0;
Rect.Right := 250;
Rect.Bottom := 180;
{ Draw the Chart }
HorizBarSeries := THorizBarSeries.Create(FChart);
HorizBarSeries.BarStyle := bsRectGradient;
HorizBarSeries.ParentChart := FChart;
HorizBarSeries.ShowInLegend := False;
HorizBarSeries.Marks.Style := smsValue;
Randomize;
with FChart do
begin
SeriesList.Clear;
for i := Low(BarTitles) to High(BarTitles) do
begin
with HorizBarSeries do
begin
Add(BarValues[i], StringReplace(BarTitles[i],'_',' ',[rfReplaceAll]),Random(2147483648));
end;
end; { for i := Low(BarTitles) to High(BarTitles) do }
SeriesList.Add(HorizBarSeries);
with Title do
begin
Font.Size := 10;
Font.Color := clBlack;
Text.Clear;
Text.Add(ChartTitle);
end;
end;
{ Generate the Chart as a Bitmap }
Bitmap := FChart.TeeCreateBitmap(clWhite, Rect);
FJPEG.Assign(Bitmap);
FJPEG.SaveToStream(MemStream);
MemStream.Position := 0;
finally
Bitmap.Free;
FChart.Free;
FJPEG.Free;
{ No need to Free MemStrm. The ISAPI Framework will take care of that }
end;
end;
The source code is quite well commented and so I won't go into the explanation of what we're doing here.
Other Tricks
You will have noticed that I use this technique on my web site quite a bit. For example, the image you see at each tutorial for Topic Ratings. You can see the ratings of each of the topics from the Site Statistics page as well. Another place I use this technique is in the graphical report of the hits I receive for each of the pages on this site. You can see such this in the Bar Chart link of the Site Statistics page.
The Topic Ratings image that is produced is actually 4 (or more) images "concatenated" to form one single image. Basically, in this case, I generate the individual images and then draw them onto a larger image that is as big as would be required depending on the layout. Each image is drawn onto a specific portion of the larger image. By the time of all this is done, the result is what you see at the end of each tutorial. Once single image.
You might want to try the above technique as an exercise after you've played around with this project a bit.