TAChart Tutorial: ColorMapSeries, Zooming

From Lazarus wiki
Revision as of 05:23, 14 October 2012 by Ask (talk | contribs) (Using TColormapSeries)

Introduction

The "Mandelbrot set", named after Benoit Mandelbrot, is a so-called "fractal" -- a two-dimensional shape that is famous for its self-similarity at various scales. Magnifying a fractal reveals small-scale details similar to the large-scale characteristics.

In this tutorial, we will use TAChart to draw the Mandelbrot set by means of a colormap series and practice a variety of zooming techniques, like using the zoom drag tool, or setting up an extent history.

As usual, we require a basic knowledge of working with TAChart which you can aquire by going through the "Getting Started" tutorial. And, of course, you must be familiar with the Lazarus IDE and the Object Pascal language.

Using TColormapSeries

What is a TColorMapSeries?

The series that you typically work with are for two-dimensional data -- there is one axis for the x coordinates, and another one for the y values which are assigned to the x values. TColorMapSeries is one exception: it displays three-dimensional data: we have a point in the xy plane and assign to it a third value as its height above the xy plane. Often, 3D plots, are considered to be like a mountain landscape above a base plane. Unfortunately, TAChart cannot draw 3d projections. But there is an alternative: the third value can be color-coded depending on its height above the base plane: every pixel in the chart's drawing area gets a color depending on the function value for the point (x, y).

TColorMapSeries is a functional series, this means that it cannot plot arbitrary data, only data that can be calculated from a function. Therefore, the series provides an event OnCalculate which is called for every (x,y) point to pass the function value. In this sense, the TColorMapSeries is similar to the TFuncSeries which we meet in another tutorial.

The second basic ingredient is a ColorSource which maps the function values to a color. It is convenient to use a TListChartSource for this purpose: it stores x and y values along with a color value and a descriptive label in TChartDataItems.

Preparation

Ready to start?

Create a new project, and add a client-aligned TChart component. Double-click on the chart to open the series editor and add a "Color map series" to the chart. Next, drop a TListChartSource to the form. This will be the ColorSource for the ColorMapSeries. So, name it "ColorSource" and assign it to the ColorSource property of the series.

Madelbrot ColorMapSeries.png

Finding the color map

To get some familiarity in this new terrain, let us begin with a simple example: we use the ColorMapSeries to draw a gradient along the x axis.

Next, we write an event handler for the ColorMapSeries' OnCalculate. This is easy since we want only a simple linear gradient:

procedure TForm1.Chart1ColorMapSeries1Calculate(const AX, AY: Double;
  out AZ: Double);
var
  ext: TDoubleRect;
begin
  ext := Chart1.GetFullExtent;
  AZ := (AX - ext.a.x) /(ext.b.x - ext.a.x);
end;

To be independent of the size of the chart we normalize the x coordinate by the range of x data (in graph units). The x range can be calculated from the FullExtent of the chart -- this is defined by the corner points of the chart's plotting rectangle if no zooming or panning has been applied. The normalization as shown above makes sure that AZ is 0 at the left end, and 1 at the right end of the x axis.

Finally we have to populate the ColorSource by values defining the colors of the gradient. Since this needs to be done only once we can use the OnCreate event of the form:

procedure TForm1.FormCreate(Sender: TObject);
const
  COLORSTEPS = 10;
var
  i: Integer;
  value: Double;
  clr: TColor;
begin
  with ColorSource do begin
    Clear;
    for i:=0 to COLORSTEPS-1 do begin
      value := i / (COLORSTEPS - 1);
      clr := InterpolateRGB(clBlue, clRed, value);
      Add(value, 0.0, '', clr);
    end;
  end;
end;

In this code, we add (number, color) pairs to the ColorSource: the number is the counter variable i normalized to 1, and the color is obtained by interpolating the r,g,b components between two colors, here blue and red. We use the function InterpolateRGB of the unit TChartUtils for that. The second and third parameters in the Add method of the ColorSource are the y coordinate and a text label that we don't need here.

Let's run the program. We get a banded gradient because we have only 10 steps (see constant COLORSTEPS above). To improve the gradient smoothness we can set the property Interpolate of the ColorMapSeries to true, or we can just increase the number of color steps - whatever you like.

Madelbrot FirstGradient.png Madelbrot GradientInterpolated.png

Let's go a step further and create a double gradient consisting of three colors: start color, middle color and end color. It is not difficult to apply the InterpolateRGB function to each interval:

function ValueToColor(AValue: Double; AMiddleValue: Double;
  AStartColor, AMiddleColor, AEndColor: TColor): TColor;
begin
  if AValue < AMiddleValue then
    Result := InterpolateRGB(AStartColor, AMiddleColor, 
      AValue / AMiddleValue)
  else
    Result := InterpolateRGB(AMiddleColor, AEndColor, 
      (AValue - AMiddleValue) / (1.0 - AMiddleValue) );
end;  

procedure TForm1.FormCreate(Sender:TObject);
const
  COLORSTEPS = 10;
  STARTCOLOR = clBlue;
  MIDCOLOR = clRed;
  ENDCOLOR = clYellow;
  MIDVALUE = 0.3;
var
  i: Integer;
  value: Double;
  clr: TColor;
begin
  with ColorSource do begin
    Clear;
    for i:=0 to COLORSTEPS-1 do begin
      value := i / (COLORSTEPS - 1);
      clr := ValueToColor(value, MIDVALUE, STARTCOLOR, MIDCOLOR, ENDCOLOR);
      Add(value, 0.0, '', clr);
    end;
  end;
end;

For testing this gradient, we use this function in the FormCreate event handler to populate the ColorSource. Now we get a beautiful gradient starting with blue, going to red and ending with yellow. This is perfect of the Mandelbrot set that we will draw now.

Mandelbrot TwoGradients.png



At first, we have to define the range of the x and y data. As we will see later, an overview of Mandelbrot set is seen best when x amd y range between -2.2 and 0.8, and between -1.5 and 1.5, respectively. Enter these numbers in the chart's Extent fields XMin, XMax, YMin and YMax, and set the fields UseXMin, UseXMax, UseYMin and UseYMin to true to activate these values as axis limits.



Although the Mandelbrot set is self-similar at magnified scales, the small scale details are not identical to the whole. In fact, the Mandelbrot set is infinitely complex. Yet the process of generating it is based on an extremely simple equation involving complex numbers.


Calculation of the Mandelbrot set

The Mandelbrot set is calculated for each 2d point c = (c.x, c.y) by applying the following recipe:

  1. Begin by assigning the coordinates of c to another point z = (z.x, z.y) = (c.x, c.y).
  2. Calculate the "square" of z by means of the following formula: z² = ((z.x)²-(z.y)², 2*(z.x)*(z.y)) (this formula may appear to be a bit strange, but if you are familiar with complex numbers then you will see that it is the square of a complex number). Then add the coordinates of c to those of . The result of this step is z² + c
  3. Now take that result and put it into the calculation in step 2 again.
  4. Repeat this procedure again and again until you can distinguish whether the trace of the movement of the point z in the 2d plane diverges into infinity or not.

In the figure we show some examples of that trace:

Mandelbrot Tracks.png
  • The red and fuchsia curves eventually move away from the origin, these paths are "unbounded". It can be shown that once the path has crossed a critical distance of 2 from the origin it will never turn back and will escape into infinity. Usually the calculation counts the iterations until the distance from the origin exceeds 2. The number of iterations is mapped to a color which is used to draw the pixel at the starting point.
  • The blue path, on the other hand, converges towards the origin. The green curve does not converge, but remains within the escape radius. Therefore, both cases are called "bounded". The iterative calculation would go on forever, therefore it is stopped after a maximum count of iterations. The starting points are said to belong to the Mandelbrot set and are drawn in black color.

Here is the function that we will use to determine whether a point c is in the Mandelbrot set or not. The function result is the iteration count divided by the maximum iteration count. The result 1.0 indicates that c belongs to the Mandelbrot set.

const
  MANDELBROT_NUM_ITERATIONS = 100;
  MANDELBROT_ESCAPE_RADIUS = 2.0;
  MANDELBROT_LIMIT = sqr(MANDELBROT_ESCAPE_RADIUS);     

function MandelBrot(c:TDoublePoint): Integer;
var
  iteration: Integer;
  j: Integer;
  z: TDoublePoint;
begin
  Iteration := 0;
  z := DoublePoint(0.0, 0.0);
  for j:=0 to MANDELBROT_NUM_ITERATIONS-1 do begin
    z := DoublePoint(
      sqr(z.X) - sqr(z.Y) + c.X,
      2 * z.X * z.Y + c.Y
    );
    if sqr(z.X) + sqr(z.Y) > MANDELBROT_LIMIT then begin
      Result := Iteration / MANDELBROT_NUM_ITERATIONS;
      exit;
    end;
    inc(Iteration);
  end;
  Result := 1.0; 
end;

We will also need a function which maps the result of the Mandelbrot function to a color. A nice color gradient can be obtained by the following procedure which is an interpolation of rgb values applied to two intervals. The input parameter AValue must be number between 0 and 1. (The function InterpolateRGB belongs to the TAChart package and is found in the unit TAChartUtils):

const
  STARTCOLOR = clBlue;
  MIDDLECOLOR = clRed;
  ENDCOLOR = clYellow;
  MIDDLEVALUE = 0.3; 

function ValueToColor(AValue: Double; AMiddleValue: Double;
  AStartColor, AMiddleColor, AEndColor: TColor): TColor;
begin
  if AValue < AMiddleValue then
    Result := InterpolateRGB(AStartColor, AMiddleColor, 
      AValue/AMiddleValue)
  else
    Result := InterpolateRGB(AMiddleColor, AEndColor, 
      (AValue - AMiddleValue) / (1.0 - AMiddleValue) 
    );
end;