Lazarus
Programming => Graphics and Multimedia => TAChart => Topic started by: apeoperaio on December 17, 2014, 01:59:41 pm

I would like to represent a matrix on a chart using the colormapserie (since I suppose it is the right choice). I have a matrix with values between 1 and 1. I would like to visualize it using a chart and getting something similar to:
http://i.stack.imgur.com/3ZaFq.png
Is it possible?

Certainly. Have a look at the tutorial http://wiki.lazarus.freepascal.org/TAChart_Tutorial:_ColorMapSeries,_Zooming

I found some time and could assemble a little demo which shows what you want by means of dummy data. In addition to the tutorial it also has an ownerdrawn gradient legend.
Please be aware that there is quite a lot of adjustment of properties; therefore, you should not only look at the pas file. Feel free to ask here again if you should have difficulties in putting everything together.

wow, thanks.
I will have a look after holidays!

ok. I checked it. I am doing some tests but I have an additional question.
How can I show userdegined hint? For example if I would like to show an hint with the label of the x and y azis and the z value how can I do it?
I didn't find any event that can be used to set a user defined hint..

Since the colormap series covers the entire axis range it is easiest to use the mouse coordinates in the MouseMove event, convert them from pixels to graph coordinates, lookup the x/y texts and calculate the function according to the x/y coordinates.
In above example, add a statusbar and the following OnMouseMove event handler:
procedure TForm1.Chart1MouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
var
P: TDoublePoint;
ix, iy: Integer;
z: Double;
s: String;
begin
// Mouse outside chart axis area?
if (X < Chart1.ClipRect.Left) or (Y > Chart1.ClipRect.Bottom) then
s := ''
else begin
// Inside!
// Convert mouse coordinates to graph coordinates
P := Chart1.ImageToGraph(Point(X,Y));
// get index to axis labels array
ix := trunc(P.X+0.5);
iy := trunc(P.Y+0.5);
if (ix >= 0) and (ix <= High(DataNames)) and (iy >= 0) and (iy <= High(DataNames)) // make sure not to be out of range
then begin
// calculate function value
Chart1ColorMapSeries1Calculate(P.X, P.Y, z);
// Create hint string
s := Format('x = %s, y = %s: z = %f', [DataNames[ix], DataNames[iy], z]);
end;
end;
// Show hint string in status bar.
Statusbar1.SimpleText := s;
end;

thanks. I implemented the chart. I was not able to directly use the hint, since if I use the hint instead of the statusbar the hints areno updated when I move the mouse. Btw it is ok to use an additional control instead of the hint.
I have another question, I would like to avoid zooming or moving the graph showing the part of the graph with no values (the white part of the graph). Is it possible to set this kind of constraints?

See the attached demo for a version which shows a hint window at the mouse position when you press the left mouse button.
The demo makes use of the ChartTools that come along with TAChart. There are several prebuilt tools, even a TDataPointHintTool,but this one does not work here since it is for standard plots y(x), but you have a 3Dplot z(x,y). But the UserDefinedChartTool is very flexible and provides all the events needed.
 If you followed my previous posting remove the OnMouseMove event handler of the chart.
 Add a TChartToolSet component, doubleclick, and add a TUserDefinedChartTool.
 Select the "shift" (special key and/or mouse key) to show the hint window  I used ssLeft for the left mouse button.
 Assign the TChartToolset to the Toolset property of the chart.
 Write event handlers for the UserDefinedChartTool's OnAfterMouseDown (show hint window), OnAfterMouseUp (hide hint window), and OnAfterMouseMove (show hint window). If the selected "Shift" contains a special key you should also use OnAfterKeyDown to show and OnAfterKeyUp to hide the hint window.
 Use the attached code to show/hide the hint window; it is borrowed from TAChart's TDataPointHintTool.
Using a dedicated ChartToolSet automatically disabled the builtin toolset which is responsible for zooming and panning. (If, in another application, you only want to disable zooming/panning without adding a toolset simply set the chart's AllowZoom to false  it automatically kills the builtin panning as well).
If you want to learn more about ChartTools read the tutorials http://wiki.lazarus.freepascal.org/TAChart_Tutorial:_Chart_Tools (http://wiki.lazarus.freepascal.org/TAChart_Tutorial:_Chart_Tools) and http://wiki.lazarus.freepascal.org/TAChart_Tutorial:_ColorMapSeries,_Zooming (http://wiki.lazarus.freepascal.org/TAChart_Tutorial:_ColorMapSeries,_Zooming).

I am still analyzing your first demo Chart_ColorMap.zip and I get an error if I increase the number of data (e.g. NUM_DATA = 120). The error is raised on line 698 tacustomsource.pas:
if not IsNan(X) and (X >= AXMin) then break;
Additionally I have two more question related to Chart_ColorMap demo
1. I didn't understand the lines:
// Populate color source (you can add more colors...)
with ColorSource do begin
Add(FMin, FMin, '', clBlue); // Data minimum > blue
Add(FMin+0.3*(FMaxFMin), FMin+0.3*(FMaxFMin), '', clRed); // 0.3 of range > red
Add(FMax, FMax, '', clYellow); // Data maximum > yellow
end;
How they works? How a gradient is introduced? What it means that there is a 0.3 of range?
2. Additionally I don't understand how the extent work:
with Chart1ColorMapSeries1.Extent do begin
XMin := 0.5;
XMax := NUM_DATA0.5;
YMin := 0.5;
YMax := NUM_DATA0.5;
UseXMin := true;
UseXMax := true;
UseYMin := true;
UseYMax := true;
end;
If I well understand XMax will be set to 11.5 if numdata is 12.
If I use the same approach in my application I got an error since in the Chart1ColorMapSeries1Calculate event the matrix limits are not respected, it tries to get an element that is not present in the matrix. e.g. if AY = 11.5 and I add 0.5 I got 12 that is a not valid index. I didn't get this error in your demo but in my application. I cannot understand why..
{ Event handler which returns the z value of each data point lying at (AX,AY)
in the plane }
procedure TForm1.Chart1ColorMapSeries1Calculate(const AX, AY: Double; out
AZ: Double);
begin
// Lookup data in matrix
// The shift by 0.5 is for centering the color boxes around the ticks.
AZ := DataValues[trunc(AX+0.5), trunc(AY+0.5)];
end;

I get an error if I increase the number of data (e.g. NUM_DATA = 120)
Yes, I can confirm. After some debugging I found that this is due to accessing the data array beyond its highest element. Please modify the OnCalculate event as follows:
{ Event handler which returns the z value of each data point lying at (AX,AY)
in the plane }
procedure TForm1.Chart1ColorMapSeries1Calculate(const AX, AY: Double; out
AZ: Double);
var
indexX, indexY: Integer;
begin
// Lookup data in matrix
// The shift by 0.5 is for centering the color boxes around the ticks.
// Make sure that we do not go beyond the highest index.
indexX := Min(NUM_DATA1, trunc(AX+0.5));
indexY := Min(NUM_DATA1, trunc(AY+0.5));
AZ := DataValues[indexX, indexY];
end;
This modification returns valid data if, due to rounding errors, the series requests a z value beyond the data array limits.
Maybe the 0.5 offset requires some more explanation:
TColorMapSeries is a "function" series, this means that it gets its data from a function calculated for every point in the x/y plane. This is what the OnCalculate event does: It gets the coordinates of the data point for which a function value z is needed and calculates the function value.
In your case, the x values are the indexes in the 2dimensional DataValues array. But the TColorMapSeries does not need z values only at integers, but also at any value in between. Mathematically speaking, it requires a "step function". Therefore I chop off all decimals from the x/y coordinates, and this is the array index, as simple as that. In this way the colormap series gets that tiled look. But without anything else the axis tick marks would be located at the lower left edge of each tile. Typically they should be in the center of each tile. Therefore, I am adding 0.5 (= half a tile) to the x/y coordinates. In this way the first tile extends between 0.5 and +0.5: the x coordinate of the very left point is 0.5, in the vent handler we add 0.5 to get 0.0, i.e. the data index is 0. This is true also for all points before they reach 0.5. Therefore the x data range of the series starts at 0.5. The same with y. And the last data point, after shifting by 0.5, is at NUM_DATA0.5. These values are to be assigned to the Extent of the series.
Your question 1 on the ColorSource: The ColorMapSeries has a builtin color interpolation procedure (ColorByValue()). It needs pivots defined by the ColorSource. Each pivot is defined by a valuecolor pair. Because interpolation has to be made for both axes a value has to be provided for x and y (first and second parameter fo the Add method of the ColorSource):
ColorSource.Add(value, value, '', color_of_value);
Suppose you want a twocolor gradient: the data minimum corresponds to the firstcolor, the maximum to the last color. Intermediate colors are calculated by the series using linear interpolation of the rgb components. If you want a threecolor gradient you have to assign a third color to an intermediate data value. In the demo I used the value which is at 0.3 of the distance between max and Min, closer to the min. If I had selected 0.5 the point would be exactly in the middle. 0.7 would be closer to the max value.