Sign in Join
Wiki copytemporary

TAChart Tutorial: ColorMapSeries, Zooming: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11## Introduction
22
33![](/wiki-images/0/02/Mandelbrot_final.png)
44
55The \[<http://en.wikipedia.org/wiki/Mandelbrot_set>\|"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.
66
77In 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**.
88
99As usual, youl'll require a basic knowledge of working with TAChart which you can aquire by going through the [[TAChart Tutorial: Getting started|"Getting Started" tutorial]]. And, of course, you must be familiar with the Lazarus IDE and the Object Pascal language.
1010
1111## Using TColormapSeries
1212
1313What is a [[TAChart documentation|TColorMapSeries]]?
1414
1515The 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 height above the base plane can be can be mapped to a color, this means the chart fills the xy plane with colors which correspnd to the value of the third coordinate.
1616
1717`TColorMapSeries` is a functional series, this means that it cannot plot arbitrary data, but only data that can be calculated from a function. For this purpose, the series exposes 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 [[TAChart Tutorial: Function Series|another tutorial]].
1818
1919The second basic ingredient is a `ColorSource` which maps the function values to a color. It is convenient to use a `TListChartSource` for this purpose. In general, it stores *x* and *y* values along with a color value and a descriptive label in `TChartDataItem`s, but for the mapping purpose we need only the *x* and color entries.
2020
2121Ready to start?
2222
2323Create a new project, and add a client-aligned `TChart` component. Don't make the window too large since the project in this tutorial will require a *lot* of computation, and too many pixels will kill performance...
2424
2525Double-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 color source for the ColorMapSeries. So, name it "`ColorSource`" and assign it to the corresponding property of the series.
2626
2727![](/wiki-images/4/47/Madelbrot_ColorMapSeries.png%E2%80%8E)
2828
2929## Setting up the color map
3030
3131To 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.
3232
3333At first, we write an event handler for the ColorMapSeries' `OnCalculate`. This is easy since we want only a simple linear gradient:
3434
3535``` pascal
3636procedure TForm1.Chart1ColorMapSeries1Calculate(const AX, AY: Double;
3737 out AZ: Double);
3838var
3939 ext: TDoubleRect;
4040begin
4141 ext := Chart1.GetFullExtent;
4242 AZ := (AX - ext.a.x) /(ext.b.x - ext.a.x);
4343end;
4444```
4545
4646To be independent of the size of the chart we normalize the *x* coordinate to the range of *x* data (in [[TAChart documentation|graph units]]). The *x* range can be calculated from the [[TAChart documentation|FullExtent]] of the chart -- this is a rectangle of double-precision values defined by the corner points of the chart's plotting rectangle before any zooming or panning is applied. The normalization as shown above makes sure that AZ is 0 at the left and 1 at the right end of the *x* axis.
4747
4848Finally we have to populate the ColorSource to setup the gradient. This is very easy since the ColorMapSeries does all the interpolations if its property `Interpolate` is set to `true`.
4949
5050To define the gradient we begin with the value 0.0 and assign it to the color blue. The end of the gradient corresponds to the value 1.0 and it should appear in yellow. We make these assignments in a procedure `PopulateColorSource` that we call from the `OnCreate` event handler of the form; we use a separate procedure since we'll do some modifications throughout this tutorial and want to keep things separate.
5151
5252``` pascal
5353procedure TForm1.FormCreate(Sender: TObject);
5454begin
5555 PopulateColorSource;
5656end;
5757
5858procedure TForm1.PopulateColorSource;
5959const
6060 DUMMY = 0.0;
6161begin
6262 with ColorSource do begin
6363 Add(0.0, DUMMY, '', clBlue); // 0.0 --> blue
6464 Add(1.0, DUMMY, '', clYellow); // 1.0 --> yellow
6565 end;
6666end;
6767```
6868
6969Due to the syntax of `TListChartSource.Add` we also have to enter values for the *y* coordinate and the marks text, we use the constant `DUMMY` and an empty string since we don't need them.
7070
7171Let's run the program. If you don't see a gradient you may have forgotten to activate the `Interpolate` setting of the ColorMapSource. If you look carefully you will see some narrow banding of the color shades particularly in the center transition region. This is due to the `StepX` and `StepY` properties of the ColorMapSource which are still at their default value of 4. This means that the xy plane is not scanned pixel by pixel, but in blocks of 4x4 pixels, to speed up drawing. Set these values to 1 to get rid of the banding. The improved resolution is also advantageous for the Mandelbrot set later. However, it will slow down program execution which may be quite dramatic if you have a slow computer...
7272
7373![](/wiki-images/5/52/Madelbrot_FirstGradient.png) ![](/wiki-images/e/e0/Mandelbrot_TwoGradients.png%E2%80%8E)
7474
7575Let's go a step further and add another pivot to our gradient. What about red in the middle corresponding to, say, 0.3? Just put another call to `Add` into the `PopulateColorSource` procedure. Since the interpolation requires a sorted list you should add the new color between the other ones, or you should call `Sort` after adding color-value pairs. The result of this tree-color gradient is shown above, we will use it now when we draw the Mandelbrot set.
7676
7777``` pascal
7878procedure TForm1.PopulateColorSource;
7979const
8080 DUMMY = 0.0;
8181begin
8282 with ColorSource do begin
8383 Add(0.0, DUMMY, '', clBlue); // 0.0 --> blue
8484 Add(0.3, DUMMY, '', clRed); // 0.3 --> red
8585 Add(1.0, DUMMY, '', clYellow); // 1.0 --> yellow
8686 end;
8787end;
8888```
8989
9090## The Mandelbrot set
9191
9292So what is the Mandelbrot set? Roughly speaking, it is a set of points in the *xy* plane that follow a certain rule, like a circle which has the rule that all points on the circle (i.e. in the set) must have the same distance from the origin.
9393
9494For the Mandelbrot set the rule is a bit more complex. The world "complex" is to be understood here in a two-fold sense: complex in the sense of "complicated", but also complex in the mathematical sense that the points in the *xy* plane are treated as complex numbers. If you are not familiar with complex numbers, don't give up -- we will avoid calculations with these "strange" numbers completely.
9595
9696### Calculation
9797
9898The best way to understand the Mandelbrot set is do its construction. We apply the following recipe for each 2d point in the *xy* plane; these starting points will be called c = (c<sub>x</sub>, c<sub>y</sub>).
9999
1001001. Begin by assigning c to another point z = (z<sub>x</sub>, z<sub>y</sub>) = (c<sub>x</sub>, c<sub>y</sub>).
1011012. Calculate the "square" of z according to the following formula: z<sup>2</sup> = (z<sub>x</sub><sup>2</sup> - z<sub>y</sub><sup>2</sup>, 2 * z<sub>x</sub> * z<sub>y</sub>) -- this formula may appear a bit strange, but if you are familiar with complex numbers then you will notice that it is the way how to calculate the square of a complex number. If you are not just take it as given.
1021023. Then add the coordinates of c to those of `z`<sup>`2`</sup>. The result of this step is z<sup>2</sup> + c = (z<sub>x</sub><sup>2</sup> - z<sub>y</sub><sup>2</sup> + c<sub>x</sub>, 2 * z<sub>x</sub> * z<sub>y</sub> + c<sub>y</sub>)
1031034. Now take that result and put it into the calculation in step 2 again as a new value of z.
1041045. Repeat this procedure again and again. As a result the point z will follow some kind of trajectory in the *xy* plane. In the figure we show some examples of that trace:
105105 - ![](/wiki-images/a/a6/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 return 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 c.
106106 - The blue path, on the other hand, converges towards the origin. The green curve does not converge, but remains within the escape radius. 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 c for these trajectories are said to belong to the Mandelbrot set and will be drawn in black color.
107107
108108Although this description may sound a bit complicated only a few lines of Pascal code are required to do the calculation. The following function determines whether a point c is in the Mandelbrot set or not. It returns the number of iterations needed and the coordinates of the point after the last iteration performed.
109109
110110``` pascal
111111const
112112 MANDELBROT_NUM_ITERATIONS = 100;
113113 MANDELBROT_ESCAPE_RADIUS = 2.0;
114114 MANDELBROT_LIMIT = sqr(MANDELBROT_ESCAPE_RADIUS);
115115
116116function InMandelbrotSet(c:TDoublePoint; out Iterations:Integer; out z: TDoublePoint): Boolean;
117117var
118118 j: Integer;
119119begin
120120 Iterations := 0;
121121 z := DoublePoint(0.0, 0.0);
122122 for j:=0 to MANDELBROT_NUM_ITERATIONS-1 do begin
123123 z := DoublePoint(
124124 sqr(z.X) - sqr(z.Y) + c.X,
125125 2 * z.X * z.Y + c.Y
126126 );
127127 if sqr(z.X) + sqr(z.Y) > MANDELBROT_LIMIT then begin
128128 Result := false;
129129 // point did escape --> c is not in Mandelbrot set
130130 exit;
131131 end;
132132 inc(Iterations);
133133 end;
134134 Result := true;
135135end;
136136```
137137
138138### Drawing
139139
140140Now we want to draw the Mandelbrot set. You know what to do? Yes - we have to write a corresponding handler for `OnCalculate`. In this event handler we call our `InMandelbrotSet` procedure. If the point passed to the event handler is in the Mandelbrot set we set its color to black, if not we divide the number of iterations by the maximum number of iterations to get a value between 0 and 1 which has a corresponding entry in our ColorSource. Oh - but how can we give the points in the Mandelbrot set a black color? Just add another number-color pair to the ColorSource which links `clBlack` to, e.g., -1.
141141
142142``` pascal
143143procedure TForm1.PopulateColorSource;
144144const
145145 DUMMY = 0.0;
146146begin
147147 with ColorSource do begin
148148 Clear;
149149 Add(-1.0, DUMMY, '', clBlack);
150150 Add( 0.0, DUMMY, '', clBlue);
151151 Add( 0.3, DUMMY, '', clRed);
152152 Add( 1.0, DUMMY, '', clYellow);
153153 end;
154154end;
155155
156156procedure TForm1.Chart1ColorMapSeries1Calculate(const AX, AY: Double;
157157 out AZ: Double);
158158var
159159 iterations: Integer;
160160 z: TDoublePoint;
161161begin
162162 if InMandelBrotSet(DoublePoint(AX, AY), iterations, z) then
163163 AZ := -1
164164 else
165165 AZ := iterations / MANDELBROT_NUM_ITERATIONS;
166166end;
167167```
168168
169169And that's what we get - see the left image:
170170
171171![](/wiki-images/e/eb/Madelbrot_FirstChart.png) ![](/wiki-images/9/9e/Mandelbrot_SecondChart.png)
172172
173173Very nice. But as usual the first result is not yet the best one. These are two points that we can improve:
174174
175175- The picture is **clipped**. This is because we did not define the calculation range that the ColorMapSeries occupies for *x* and *y*. The overview of the Mandelbrot set is seen best when *x* and *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. You might as well use the `Extent` property of the series, but then you would have to set the chart's `Margins` to 0 in order to remove the non-filled background near the axes.
176176
177177- The picture looks somehow **distorted** if you compare it with the images in [1](http://en.wikipedia.org/wiki/Mandelbrot_set). This happens because *x* and *y* axes have different unit lengths. To avoid the distortion the distance between 0 and 1 must be equal for both axes. Of course, we could initialize the width and height of the form correctly, but the user could resize the form, and this would distort our image again. However, `TChart` has a practical property to enforce a "square" coordinate system: set `Proportional` to `true`. With this setting the extent of the *x* and *y* axes is adjusted automatically to always get equal units.
178178
179179The result after these modifications is displayed above in the right image. You maybe notice that the *x* axis range is wider than we had requested. This is a consequence of the `Proportional` setting since the aspect ratio of our window is not compatible with the *x* and *y* ranges set in the chart's `Extent`.
180180
181181Before we continue with the tutorial, here's a piece of "homework": Modify the program such that the Mandelbrot set is not black, but shows colors according to the distance of the trajectory from the origin when the iteration is aborted.
182182
183183## Zooming and panning
184184
185185It was mentioned in the introduction that the Mandelbrot set is self-similar at magnified scales. This calls for zooming -- and this will be the subject of the next part of the tutorial.
186186
187187### Built-in zooming and unzooming
188188
189189`TChart` has built-in zooming capabilities. You just have to drag a rectangle with the mouse from the top-left to the bottom-right corner of the region that you want to magnify and hold the left mouse button down while dragging. To restore the old viewport drag in any other direction, or just click into the image.
190190
191191![](/wiki-images/2/23/Mandelbrot_Zooming.png) ![](/wiki-images/f/f4/Mandelbrot_ZoomingResult.png) ![](/wiki-images/3/34/Mandelbrot_DeepZoom.png)
192192
193193Of course, you can use the new magnified image to zoom again and again. However, at some zoom level, the axis labels will get longer and longer and may eventually overlap. This does not look nice. Do we need the axes at all? No -- let's turn them off by going to the left and the bottom axis and setting their property `Visible` to `false`.
194194
195195But maybe we should have some indication on the size of the current viewport. We could display the current magnification in a label. In order to have space for other info labels later in this tutorial, we'll use a panel as a container for the label. Align this "info panel" to the right and delete its `Caption`. You may want to resize the form to get a sufficiently large chart again. Add the label to the top of the panel and name it `LblMagnification`.
196196
197197### Extents
198198
199199How do we get the current magnification? The viewport is described in TAChart by several [[TAChart documentation|"extents"]]; these are rectangles like `TRect`, but the corner points are given by floating point numbers. This data type is called `TDoubleRect` and is declared in unit `TAChartUtils`. We already had discussed the function `GetFullExtent` of `TChart` which returns the unzoomed/unpanned viewport. After zooming or panning the visible viewport can be accessed by `CurrentExtent` or `LogicalExtent`. Both rectangles are very similar, the former one is slightly larger than the latter one since it also takes care of the space needed for the axis tick marks and the inner chart margins. Because this extra space is not considered in the full extent, we have to compare the widths of the full extent to that of the logical extent to calculate the magnification. We need not consider the extent heights because of the `Proportional` setting of the chart.
200200
201201It should be noted that the extents are always given in [[TAChart documentation|graph units]], i.e. the underlying coordinate system of the chart. Since we do not use [[TAChart documentation|TChartTransformations]] this is the same as the coordinate system displayed at the axes (axes coordinates).
202202
203203When do we update the magnification label? Whenever the extent of the chart changes. `TChart` provides two events for this occasion: `OnExtentChanged` and `OnExtentChanging`. Both events are fired when the extent already has changed, OnExtentChanged is called from within the drawing routine, `OnExtentChanging` is called before any drawing. The difference is important if your event handler would initiate a re-draw of the chart again -- this might lock your application... In our application the difference is not important, so we'll vote against `OnExtentChanging` which is not yet available in the official Lazarus version.
204204
205205``` pascal
206206procedure TForm1.Chart1ExtentChanged(ASender: TChart);
207207var
208208 cex, fex: TDoubleRect;
209209 factor: double;
210210begin
211211 cex := Chart1.CurrentExtent;
212212 fex := Chart1.GetFullExtent;
213213 if cex.b.x = cex.a.x then exit;
214214
215215 factor := (fex.b.x - fex.a.x) / (cex.b.x - cex.a.x);
216216 if factor > 1e6 then
217217 LblMagnification.Caption := Format('Magnification: %.0e', [factor])
218218 else
219219 LblMagnification.Caption := Format('Magnification: %0.n', [factor]);
220220end;
221221```
222222
223223![](/wiki-images/2/25/Mandelbrot_ExtremeMag.png)
224224
225225This is a possible version of the event handler. Since very large magnification factors will be difficult to read we switch to exponential numeric notation above 1 million.
226226
227227### Zooming towards infinity...
228228
229229Play with the magnifications -- the image on the right is magnified by more than a 1 billion times (1E9). And when you are lucky you'll find locations where you can zoom to magnifications of 1E14 before the image gets ugly. Hold on for a moment and think about this number: if the original, non-zoomed window is 10 cm across -- then it would measure 1E13 meters at this magnification. This is the **size of the solar system**! It does not make sense to zoom any further because we reached the limit of double precision number used by TAChart internally. However, theoretically, it could go on and on and on...
230230
231231### Using ChartTools
232232
233233Maybe you want to shift the viewport a bit to the left or right at the same magnification. This operation is called **panning**. In the trunk version of Lazarus, you can pan the image by dragging it with the right mouse button. In the release version there is not built-in panning support. But you can achieve panning by of the so-called [[TAChart documentation|ChartTools]]. Most of all, these tools are important because they give better control of the zooming and panning process.
234234
235235We will not go into details of what ChartTools are and what they can do in general. Refer to the [[TAChart Tutorial: Chart Tools|corresponding tutorial]] for an introduction if they are not familiar to you.
236236
237237![](/wiki-images/4/40/TChartToolset-Icon.png) To begin with, add a ChartToolSet component to the form and link it to the property `Toolset` of the chart. Then double-click on the toolset and add a [[TAChart documentation|Panning by drag]] tool. Select the tool in the object inspector -- it is a child of the ChartToolset in the object tree -- and set its property `Shift` to `ssRight`. This will be the mouse button to activate the panning action. You may combine other keys in the `Shift` property if you like.
238238
239239Since the built-in tools are turned off by using a ChartToolset you also have to restore the zooming capabilities. -- Just the same procedure: add a [[TAChart documentation|Zoom by drag]] and set its `Shift` property to `ssLeft`.
240240
241241If you run the program now you can pan even if you have the official Lazarus version.
242242
243243### Zoom history
244244
245245For those who use the trunk version we can add another gadget, a **zoom history**. Since this was added to TAChart only recently, users of the official version are out of luck, they must skip this section - sorry...
246246
247247If you zoom deeper and deeper into the Mandelbrot set you may want to go one or more steps back to investigate details that are no longer visible at the current zoom level. The zoom history stores all the extents that once were active during the entire zooming and panning process.
248248
249249The units `TATypes` and `TAChartUtils` provide a class `TAChartExtentHistory` which allows to implement a zoom history. Since this is not a component it must be used by code. We create and destroy it in the `FormCreate` and `FormDestroy` events as the zoom history is needed throughout the entire program:
250250
251251``` pascal
252252procedure TForm1.FormCreate(Sender: TObject);
253253begin
254254 PopulateColorSource;
255255 Chart1ColorMapseries1.ColorSource := ColorSource;
256256 // this is new:
257257 ZoomHistory := TChartExtentHistory.Create;
258258 ZoomHistory.Capacity := 100;
259259 // our history can capture 100 items until the oldest item is dropped.
260260end;
261261
262262procedure TForm1.FormDestroy(Sender: TObject);
263263begin
264264 ZoomHistory.Free;
265265end;
266266```
267267
268268Whenever zooming or panning events occur the visible viewport of the chart must be added to the history. Let's begin with zooming where we use the `AfterMouseUp` event of the ZoomDragTool for simplicity. We could also take the more general `OnExtentChanged` event of the chart, but here we have to be careful to skip the extent changes caused by unzooming -- please study the *extent demo* that comes with TAChart for details.
269269
270270Note that the extent already has changed when the `AfterMouseUp` event fires. Therefore, the extent to be added to the history is the `PrevLogicalExtent` of the chart -- this is the viewport that was active before the extent was changed:
271271
272272``` pascal
273273procedure TForm1.ChartToolset1ZoomDragTool1AfterMouseUp(ATool: TChartTool;
274274 APoint: TPoint);
275275begin
276276 ZoomHistory.Add(Chart1.PrevLogicalExtent);
277277end;
278278```
279279
280280Panning is a bit more tricky because the extent changes occur while moving the mouse. At the moment of MouseUp the `PrevLogicalExtent` that was active when panning started has been overwritten many times. But the solution is simple: The PanDragTool can use above code in the `OnAfterMouseDown` event handler.
281281
282282It would be interesting to show in our info panel also the current zoom/pan level, this is the count of items in the history. The `TChartExtentHistory` has a corresponding property `Count` for that. So, let's add another label ("`LblHistoryCount`") to the info panel and modify the `OnExtentChanged` event handler of the chart by adding the following line:
283283
284284``` pascal
285285 LblHistoryCount.Caption := Format('History count: %d', [ZoomHistory.Count]);
286286```
287287
288288And how to we **restore the previous zoom level**? By using one more ChartTool. We don't have any special requirements, so we take a [[TAChart documentation|UserDefinedChartTool]]. Set its `Shift` property such that the tool reacts on a click of the middle mouse button. And in the `OnAfterMouseClick` event handler we read the last item from the history (`ZoomHistory.Pop`) and assign the extent rectangle to the chart's `LogicalExtent` which adjusts the viewport accordingly. The item is removed from the history stack after reading. The history count display in the info panel adjusts automatically because the event `OnExtentChanged` is triggered. Note that we have to check for an empty history to avoid an exception.
289289
290290``` pascal
291291procedure TForm1.ChartToolset1UserDefinedTool1AfterMouseUp(ATool: TChartTool;
292292 APoint: TPoint);
293293begin
294294 if ZoomHistory.Count > 0 then
295295 Chart1.LogicalExtent := ZoomHistory.Pop;
296296end;
297297```
298298
299299If you want to quickly **return to the unzoomed view at magnification 1** you can drag with the left mouse button in any direction other than left-top to right-bottom, or just click into the chart. Since we introduced the middle mouse button for unzooming the user interface may be more consistent if we'd add another [[TAChart documentation|UserDefinedChartTool]] and assign its `Shift` property to `ssMiddle` and `ssShift` -- I associate the `SHIFT` key with the action of making something bigger, like lowercase to uppercase characters. Therefore, the combination of the middle key for unzooming with the `SHIFT` key could be remembered to trigger unzooming in a big step.
300300
301301Nevertheless, it may be difficult for the user to keep in mind all the key combinations. Therefore, add to our info panel a few more labels in which you give **instructions on zooming, panning and unzooming**.
302302
303303So, here is the final result of this tutorial project. Have fun!
304304
305305![](/wiki-images/0/02/Mandelbrot_final.png)
306306
307307## Source code
308308
309309The sources of the tutorial project can be found in the folder *tutorial\mandelbrot* of the trunk TAChart installation.
310310
311311### project1.lpr
312312
313313``` pascal
314314program mandelbrot;
315315
316316{$mode objfpc}{$H+}
317317
318318uses
319319 {$IFDEF UNIX}{$IFDEF UseCThreads}
320320 cthreads,
321321 {$ENDIF}{$ENDIF}
322322 Interfaces, // this includes the LCL widgetset
323323 Forms, main, tachartlazaruspkg
324324 { you can add units after this };
325325
326326{$R *.res}
327327
328328begin
329329 RequireDerivedFormResource := True;
330330 Application.Initialize;
331331 Application.CreateForm(TForm1, Form1);
332332 Application.Run;
333333end.
334334```
335335
336336### main.pas
337337
338338``` pascal
339339unit main;
340340
341341{$mode objfpc}{$H+}
342342
343343interface
344344
345345uses
346346 Classes, ExtCtrls, StdCtrls, SysUtils, TAGraph, TAFuncSeries,
347347 TASources, Forms, Controls, Graphics, Dialogs, TATypes, TATools, Types;
348348
349349type
350350
351351 { TForm1 }
352352
353353 TForm1 = class(TForm)
354354 Chart1: TChart;
355355 Chart1ColorMapSeries1: TColorMapSeries;
356356 ChartToolset1: TChartToolset;
357357 ChartToolset1PanDragTool1: TPanDragTool;
358358 ChartToolset1UserDefinedTool1: TUserDefinedTool;
359359 ChartToolset1UserDefinedTool2: TUserDefinedTool;
360360 ChartToolset1ZoomDragTool1: TZoomDragTool;
361361 ColorSource: TListChartSource;
362362 Label1: TLabel;
363363 Label2: TLabel;
364364 Label3: TLabel;
365365 Label4: TLabel;
366366 Label5: TLabel;
367367 Label6: TLabel;
368368 Label7: TLabel;
369369 Label8: TLabel;
370370 Label9: TLabel;
371371 LblMagnification: TLabel;
372372 LblHistoryCount: TLabel;
373373 Panel1: TPanel;
374374 Panel2: TPanel;
375375 Panel3: TPanel;
376376 procedure Chart1ColorMapSeries1Calculate(const AX, AY: Double;
377377 out AZ: Double);
378378 procedure Chart1ExtentChanged(ASender: TChart);
379379 procedure ChartToolset1PanDragTool1AfterMouseDown(ATool: TChartTool;
380380 APoint: TPoint);
381381 procedure ChartToolset1UserDefinedTool1AfterMouseUp(ATool: TChartTool;
382382 APoint: TPoint);
383383 procedure ChartToolset1UserDefinedTool2AfterMouseUp(ATool: TChartTool;
384384 APoint: TPoint);
385385 procedure ChartToolset1ZoomDragTool1AfterMouseUp(ATool: TChartTool;
386386 APoint: TPoint);
387387 procedure FormCreate(Sender: TObject);
388388 procedure FormDestroy(Sender: TObject);
389389 private
390390 ZoomHistory: TChartExtentHistory;
391391 procedure PopulateColorSource;
392392 end;
393393
394394var
395395 Form1: TForm1;
396396
397397implementation
398398
399399{$R *.lfm}
400400
401401uses
402402 StrUtils,
403403 TAChartUtils, TAGeometry;
404404
405405const
406406 MANDELBROT_NUM_ITERATIONS = 100;
407407 MANDELBROT_ESCAPE_RADIUS = 2.0;
408408 MANDELBROT_LIMIT = sqr(MANDELBROT_ESCAPE_RADIUS);
409409
410410function InMandelBrotSet(
411411 AC: TDoublePoint; out AIterations: Integer; out AZ: TDoublePoint): Boolean;
412412var
413413 j: Integer;
414414begin
415415 AIterations := 0;
416416 AZ := DoublePoint(0.0, 0.0);
417417 for j := 0 to MANDELBROT_NUM_ITERATIONS - 1 do begin
418418 AZ := DoublePoint(
419419 Sqr(AZ.X) - Sqr(AZ.Y) + AC.X,
420420 2 * AZ.X * AZ.Y + AC.Y);
421421 if Sqr(AZ.X) + Sqr(AZ.Y) > MANDELBROT_LIMIT then
422422 // point did escape --> AC is not in Mandelbrot set
423423 exit(false);
424424 AIterations += 1;
425425 end;
426426 Result := true;
427427end;
428428
429429{ TForm1 }
430430
431431procedure TForm1.FormCreate(Sender:TObject);
432432begin
433433 PopulateColorSource;
434434 ZoomHistory := TChartExtentHistory.Create;
435435 ZoomHistory.Capacity := 100;
436436end;
437437
438438procedure TForm1.FormDestroy(Sender: TObject);
439439begin
440440 ZoomHistory.Free;
441441end;
442442
443443procedure TForm1.Chart1ColorMapSeries1Calculate(
444444 const AX, AY: Double; out AZ: Double);
445445var
446446 iterations: Integer;
447447 z: TDoublePoint;
448448begin
449449 if InMandelBrotSet(DoublePoint(AX, AY), iterations, z) then
450450 AZ := -1
451451 // or - as a solution to the "homework" exercise:
452452 // AZ := sqrt(sqr(z.x) + sqr(z.y)) / MANDELBROT_ESCAPE_RADIUS
453453 else
454454 AZ := iterations / MANDELBROT_NUM_ITERATIONS;
455455end;
456456
457457procedure TForm1.Chart1ExtentChanged(ASender: TChart);
458458var
459459 cex, fex: TDoubleRect;
460460 factor: Double;
461461begin
462462 cex := ASender.CurrentExtent;
463463 fex := ASender.GetFullExtent;
464464 if cex.b.x = cex.a.x then exit;
465465
466466 factor := (fex.b.x - fex.a.x) / (cex.b.x - cex.a.x);
467467 LblMagnification.Caption :=
468468 'Magnification: ' + Format(IfThen(factor > 1e6, '%.0e', '%0.g'), [factor]);
469469 LblHistoryCount.Caption := Format('History count: %d', [ZoomHistory.Count]);
470470end;
471471
472472procedure TForm1.ChartToolset1PanDragTool1AfterMouseDown(
473473 ATool: TChartTool; APoint: TPoint);
474474begin
475475 Unused(ATool, APoint);
476476 ZoomHistory.Add(Chart1.PrevLogicalExtent);
477477end;
478478
479479procedure TForm1.ChartToolset1UserDefinedTool1AfterMouseUp(
480480 ATool: TChartTool; APoint: TPoint);
481481begin
482482 Unused(ATool, APoint);
483483 if ZoomHistory.Count > 0 then
484484 Chart1.LogicalExtent := ZoomHistory.Pop;
485485end;
486486
487487procedure TForm1.ChartToolset1UserDefinedTool2AfterMouseUp(
488488 ATool: TChartTool; APoint: TPoint);
489489begin
490490 Unused(ATool, APoint);
491491 Chart1.ZoomFull;
492492end;
493493
494494procedure TForm1.ChartToolset1ZoomDragTool1AfterMouseUp(
495495 ATool: TChartTool; APoint: TPoint);
496496begin
497497 Unused(ATool, APoint);
498498 ZoomHistory.Add(Chart1.PrevLogicalExtent);
499499end;
500500
501501procedure TForm1.PopulateColorSource;
502502const
503503 DUMMY = 0.0;
504504begin
505505 with ColorSource do begin
506506 Clear;
507507 Add(-1.0, DUMMY, '', clBlack);
508508 Add( 0.0, DUMMY, '', clBlue);
509509 Add( 0.3, DUMMY, '', clRed);
510510 Add( 1.0, DUMMY, '', clYellow);
511511 end;
512512end;
513513
514514end.
515515```
516516
517517### main.lfm
518518
519-``` pascal
519+``` lfm
520520object Form1: TForm1
521521 Left = 326
522522 Height = 285
523523 Top = 155
524524 Width = 468
525525 Caption = 'Form1'
526526 ClientHeight = 285
527527 ClientWidth = 468
528528 OnCreate = FormCreate
529529 OnDestroy = FormDestroy
530530 LCLVersion = '1.1'
531531 object Chart1: TChart
532532 Left = 4
533533 Height = 277
534534 Top = 4
535535 Width = 288
536536 AxisList = <
537537 item
538538 Visible = False
539539 Minors = <>
540540 Title.LabelFont.Orientation = 900
541541 end
542542 item
543543 Visible = False
544544 Alignment = calBottom
545545 Minors = <>
546546 end>
547547 Extent.UseXMax = True
548548 Extent.UseXMin = True
549549 Extent.UseYMax = True
550550 Extent.UseYMin = True
551551 Extent.XMax = 0.8
552552 Extent.XMin = -2.2
553553 Extent.YMax = 1.5
554554 Extent.YMin = -1.5
555555 Foot.Brush.Color = clBtnFace
556556 Foot.Font.Color = clBlue
557557 Proportional = True
558558 Title.Brush.Color = clBtnFace
559559 Title.Font.Color = clBlue
560560 Title.Text.Strings = (
561561 'TAChart'
562562 )
563563 Toolset = ChartToolset1
564564 OnExtentChanged = Chart1ExtentChanged
565565 Align = alClient
566566 BorderSpacing.Around = 4
567567 DoubleBuffered = True
568568 ParentColor = False
569569 object Chart1ColorMapSeries1: TColorMapSeries
570570 ColorSource = ColorSource
571571 Interpolate = True
572572 OnCalculate = Chart1ColorMapSeries1Calculate
573573 StepX = 1
574574 StepY = 1
575575 end
576576 end
577577 object Panel1: TPanel
578578 Left = 296
579579 Height = 285
580580 Top = 0
581581 Width = 172
582582 Align = alRight
583583 BevelOuter = bvNone
584584 ClientHeight = 285
585585 ClientWidth = 172
586586 TabOrder = 1
587587 object Panel2: TPanel
588588 Left = 0
589589 Height = 213
590590 Top = 72
591591 Width = 172
592592 Align = alClient
593593 BevelOuter = bvNone
594594 ClientHeight = 213
595595 ClientWidth = 172
596596 TabOrder = 0
597597 object Label2: TLabel
598598 Left = 6
599599 Height = 13
600600 Top = 8
601601 Width = 69
602602 Caption = 'Instructions'
603603 Font.Style = [fsBold]
604604 ParentColor = False
605605 ParentFont = False
606606 end
607607 object Label1: TLabel
608608 Left = 6
609609 Height = 13
610610 Top = 37
611611 Width = 45
612612 Caption = 'Left-drag'
613613 Font.Style = [fsItalic]
614614 ParentColor = False
615615 ParentFont = False
616616 end
617617 object Label3: TLabel
618618 Left = 6
619619 Height = 13
620620 Top = 80
621621 Width = 53
622622 Caption = 'Middle-click'
623623 Font.Style = [fsItalic]
624624 ParentColor = False
625625 ParentFont = False
626626 end
627627 object Label4: TLabel
628628 Left = 6
629629 Height = 13
630630 Top = 120
631631 Width = 97
632632 Caption = 'Middle-click w/SHIFT'
633633 Font.Style = [fsItalic]
634634 ParentColor = False
635635 ParentFont = False
636636 end
637637 object Label5: TLabel
638638 Left = 6
639639 Height = 13
640640 Top = 161
641641 Width = 51
642642 Caption = 'Right-drag'
643643 Font.Style = [fsItalic]
644644 ParentColor = False
645645 ParentFont = False
646646 end
647647 object Label6: TLabel
648648 Left = 19
649649 Height = 13
650650 Top = 56
651651 Width = 25
652652 Caption = 'zoom'
653653 ParentColor = False
654654 end
655655 object Label7: TLabel
656656 Left = 22
657657 Height = 13
658658 Top = 96
659659 Width = 81
660660 Caption = 'unzoom (history)'
661661 ParentColor = False
662662 end
663663 object Label8: TLabel
664664 Left = 19
665665 Height = 13
666666 Top = 136
667667 Width = 54
668668 Caption = 'full unzoom'
669669 ParentColor = False
670670 end
671671 object Label9: TLabel
672672 Left = 19
673673 Height = 13
674674 Top = 177
675675 Width = 18
676676 Caption = 'pan'
677677 ParentColor = False
678678 end
679679 end
680680 object Panel3: TPanel
681681 Left = 0
682682 Height = 72
683683 Top = 0
684684 Width = 172
685685 Align = alTop
686686 BevelOuter = bvNone
687687 ClientHeight = 72
688688 ClientWidth = 172
689689 TabOrder = 1
690690 object LblMagnification: TLabel
691691 Left = 6
692692 Height = 13
693693 Top = 8
694694 Width = 67
695695 Caption = 'Magnification:'
696696 ParentColor = False
697697 end
698698 object LblHistoryCount: TLabel
699699 Left = 6
700700 Height = 13
701701 Top = 29
702702 Width = 68
703703 Caption = 'History count:'
704704 ParentColor = False
705705 end
706706 end
707707 end
708708 object ColorSource: TListChartSource
709709 left = 115
710710 top = 57
711711 end
712712 object ChartToolset1: TChartToolset
713713 left = 115
714714 top = 120
715715 object ChartToolset1ZoomDragTool1: TZoomDragTool
716716 Shift = [ssLeft]
717717 OnAfterMouseUp = ChartToolset1ZoomDragTool1AfterMouseUp
718718 end
719719 object ChartToolset1PanDragTool1: TPanDragTool
720720 Shift = [ssRight]
721721 OnAfterMouseDown = ChartToolset1PanDragTool1AfterMouseDown
722722 end
723723 object ChartToolset1UserDefinedTool1: TUserDefinedTool
724724 Shift = [ssMiddle]
725725 OnAfterMouseUp = ChartToolset1UserDefinedTool1AfterMouseUp
726726 end
727727 object ChartToolset1UserDefinedTool2: TUserDefinedTool
728728 Shift = [ssShift, ssMiddle]
729729 OnAfterMouseUp = ChartToolset1UserDefinedTool2AfterMouseUp
730730 end
731731 end
732732end
733733```
734734
735735## Related tutorials
736736
737737- [[TAChart Tutorial: Getting started]]: first steps with TAChart
738738- [[TAChart Tutorial: Chart Tools]]: introduction to chart tools
739739- [[TAChart Tutorial: Function Series]]: introduces `TFuncSeries` which follows a similar concept as `TColorMapSeries`
740740
741741---
742742*From the Free Pascal wiki, page "TAChart Tutorial: ColorMapSeries, Zooming", as it stood on 2020-02-28 after a change by Trev, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*

Back to history