Delphi - plot math function

I have seen many times people asking for a working solution about the function plot and now I am showing you a very easy step-by-step guide! :) First of all I need to say that this method is able to plot any kind of function f(x) for example f(x) = x^2 - 2x - 1. Let's see what we need:

  • Create a new project and drop a container. It can be what you prefer, for example a TPanel or a TRectangle. I am going to use the latter but it's your choice, the important thing is that you can use the OnPaint event.
  • It is optional that you have a function parser (Parser10 or tbcParser) to parse a function. It is not needed but without this you'll have to hard code the function every time. Later we'll discuss about this.

I'll create an example with a new Firemonkey HD From (File > New > Multi-Device Application). Drop a TRectangle and align it to client; in the object inspector on the left expand the Fill property and change the Color to white. Add the following code above the declaration of your form.


 type
  TDoubleVector = record
   x, y: double;
  end;

This will be te container of our functions. Now add these procedures to the private part of your form and press CTRL + Shift + C to enable the code completion. br>

 private
  xmin, xmax, ymin, ymax: integer;
  h, w: single;
  procedure DrawAxes(aCanvas: TCanvas);
  function LogToScreen(LogPoint: TRealVector): TPoint;
  function ScreenToLog(ScreenPoint: TPoint): TRealVector;

Let's code! The first procedure will be used to draw the x/y axes and the other 2 functions will convert the function to a line in the TRectangle so that we can see the plot. Click on the Form in the Structure view and add a OnCreate event. Let's start with the first piece of code:


procedure TForm1.FormCreate(Sender: TObject);
begin

 xmin := -5;
 xmax := 5;
 ymin := -5;
 ymax := 5;

end;

procedure TForm1.DrawAxes(aCanvas: TCanvas);
var i: integer;
    pw, ph, w, h: single;
begin

 //draw intermediate lines
 aCanvas.Stroke.Color := TAlphaColorRec.Silver;

 w := (Rectangle1.Width / (xmax*2));
 h := (Rectangle1.Height / (ymax*2));
 pw := w;
 ph := h;

 for i := 0 to (xmax*2)-1 do
  begin
   aCanvas.DrawLine(TPointF.Create(pw, 0), TPointF.Create(pw, Rectangle1.Height), 1);
   aCanvas.DrawLine(TPointF.Create(0, ph), TPointF.Create(Rectangle1.Width, ph), 1);

   pw := w + pw;
   ph := h + ph;
  end;

 //draw x and y axes
 aCanvas.Stroke.Color := TAlphaColorRec.Black;
 aCanvas.Stroke.Thickness := 2;

 w := Rectangle1.Width;
 h := Rectangle1.Height;

 aCanvas.DrawLine(TPointF.Create((w/2), 0), TPointF.Create((w/2), h), 1);
 aCanvas.DrawLine(TPointF.Create(0, (h/2)), TPointF.Create(w, (h/2)), 1);

end;

procedure TForm1.Rectangle1Paint(Sender: TObject; Canvas: TCanvas;
  const ARect: TRectF);
begin

 DrawAxes(Canvas);

end;

The procedure DrawAxes is very important; just click on the Rectangle1 and put the procedure inside its OnPaint event. In the first part we calculate the space between the lines of the grid [ Rectangle1.Width / (xmax*2) ] and the in a for loop I draw the lines. The second part is the same but note that I am using a dark thick stroke. The result should be this:


Now we have to create the 2 most important function of the program: they will convert the points of a given function into a line in our chart! Let's use this code. Note that this has a scaling feature that allows a nice view of our function in the chart.


function TForm1.ScreenToLog(ScreenPoint: TPoint): TDoubleVector;
begin
  result.X := xmin + (ScreenPoint.X / Rectangle1.Width) * (xmax - xmin);
  result.Y := ymin + (ymax - ymin) * (Rectangle1.Height - ScreenPoint.Y) / Rectangle1.Height;
end;

function TForm1.LogToScreen(LogPoint: TDoubleVector): TPoint;
begin
  result.X := round(Rectangle1.Width * (LogPoint.X - xmin) / (xmax - xmin));
  result.Y := Rectangle1.Height - round(Rectangle1.Height * (LogPoint.Y - ymin) / (ymax - ymin));
end;

We have almost finished! Now we have to draw the equation f(x) = x^2 - 2x - 1 we mentioned above and this is the code. Please note that it is inside the OnPaint event of the Rectangle. If you need to change the equation of the function at some point (for example when you click a button) don't forget to call the Rectangle1.Repaint procedure! Let's see the code:


procedure TForm1.Rectangle1Paint(Sender: TObject; Canvas: TCanvas;
  const ARect: TRectF);
var
  PrevPoint, CurrPoint: TPointF;
  x: integer;
  logx: double;
  logy: double;
  y: integer;
  punti: TDoubleVector;
begin

  DrawAxes(Canvas);

  Canvas.Stroke.Color := TAlphaColorRec.Cadetblue;
  PrevPoint := Point(0,0);
   
  for x := 0 to (round(Rectangle1.Width) - 1) do
  begin
    punti.X := logx;
    punti.Y := logy;

    logx := ScreenToLog(Point(x, 0)).X;
    logy := logx*logx - 2*logx - 1; // f(x) = x^2 - 2*x - 1

    punti.X := logx;
    punti.Y := logy;

    y := LogToScreen(punti).Y;
    CurrPoint := Point(x, y);
    Canvas.DrawLine(CurrPoint, PrevPoint, 1);
    PrevPoint := CurrPoint;
  end;

end;

Ta daa! Now we have written a code that can plot any kind of function :) Please note that the important line is when I give logy a value which is the value of the function. Let's see the result of our work: below we will discuss it.



  • Use the logy to define your function and use logx as variable. For example: we wanted to plot x^2 - 2*x - 1 and so we have written logx*logx - 2*logx - 1. What if we want to plot x^2 - 2? Very easy! Just use the code above and replace this line: logy := logx*logx - 2 (x*x - 2 <=> x^2 - 2).

  • The code has logx and logy that allow us to do a scaling and plot the function. The "problem" is that we have to hard-code the function and assing a value to logy at compile time. What if we want this to be more flexible? For example: get the function x^2 - 2x + 3 from a TEdit and click a button to show the function. Well, we need a parser to parse the function. A very good one is Parser10 which is free (even if a bit old it's very good in our case) or tbcParser.

We can improve our work even more! :) Let's add the possibility to resize the plot. Add 2 buttons on the bottom on the form and give them the Text + (Name = ButtonZoomIn) and - (Name = ButtonZoomOut). Double click both and add the following code:


procedure TForm1.ButtonZoomInClick(Sender: TObject);
begin

 //don't get negative numbers from the zoom + !!
 if ((xmax - 5) > 0) then
  begin
   xmax := xmax - 5;
   ymax := ymax - 5;
   xmin := xmin + 5;
   ymin := ymin + 5;
   Rectangle1.Repaint;
  end;

end;

procedure TForm1.ButtonZoomOutClick(Sender: TObject);
begin

 xmax := xmax + 5;
 ymax := ymax + 5;
 xmin := xmin - 5;
 ymin := ymin - 5;
 Rectangle1.Repaint;

end;

Now we have really finished! With this code we are able to zoom in and zoom out our function :D The important part is Rectangle.Repaint which is refreshing the content of our container (the Rectangle) with the new code. The 5 is the "scaling factor". You could declare at the beginning of the unit something like const scFactor = 5; (or whatever, it can be 10, 15... but I suggest you to keep the 5) and put scFactor instead of all those 5. Here's the final result:



This is an example of the great things you can do with Delphi! Imagine having this in your Andorid/iOS app, that would be great! You can addd Gestires to zoom in/zoom out with your fingers or support multiple functions in a single Rectangle (just draw and call Repaint). Stay tuned for more!
Article by Alberto Miola.

Comments

  1. Mi è stato utilissimo per fare l'app sullo studio di funzione, grande! :)

    ReplyDelete

Post a Comment

Popular posts from this blog

Delphi - Android full screen splash

Delphi - TLanguage localizaton tool

Delphi - monetize Android/iOS applications step-by-step