[gelöst] TAChart Normalisierte LineSeries

Rund um die LCL und andere Komponenten
Antworten
Michl
Beiträge: 2513
Registriert: Di 19. Jun 2012, 12:54

[gelöst] TAChart Normalisierte LineSeries

Beitrag von Michl »

Hallo wertes Forum,

bisher nutzte ich eine eigene Anzeige-Komponente für Daten in einem Projekt. Da ich dieses zur Zeit umstelle, will ich versuchen TAChart zu nutzen, da dieses mir mit Zoomen etc. mehr Möglichkeiten bietet.

Folgende Frage konnte ich bei meinen Tests bisher nicht klären: Ist es möglich Daten in diverse LineSeries zu schicken und diese normalisiert (z.B. immer MaxRange/MinRange +1/-1) in einem Chart darzustellen?

Ich kann natürlich die Ausgangsdaten, bevor ich diese der LineSeries übergebe "Normalisieren", aber eventuell gibt es ja eine ganz einfache Lösung (per y-Scale-Property oder so).

Beispiel, wie ich es mir vorstelle (oder als Projekt anbei):

Code: Alles auswählen

procedure TForm1.FormCreate(Sender: TObject);
var
  i: Integer;
begin
  CreateSomeData;
  SetLength(MyLS, Length(MyData));
  for i:=0 to High(MyLS) do begin
    MyLS[i]:=TLineSeries.Create(Self);
//    MyLS[i].AddArray(MyData[i]);  // <- So wäre es mir lieber + Setzen eines Properties
    AddNormalArray(MyLS[i], MyData[i]);  // als so!
    MyLS[i].SeriesColor:=$FF0000 SHR (i * 8);
    Chart1.AddSeries(MyLS[i]);
  end;
end;
 
procedure TForm1.CreateSomeData;
var
  i, j: Integer;
begin
  SetLength(MyData, 4);
  for j:=0 to High(MyData) do begin
    SetLength(MyData[j], 1000);
    for i:=0 to High(MyData[j]) do
      case j of
        0: MyData[j, i]:=Sin(i / 90) * 5;
        1: MyData[j, i]:=Sin(i / 83) * Sin(i div 20 mod 5);
        2: MyData[j, i]:=i mod 100 * Sin(i / 50);
        3: MyData[j, i]:=Sin(i / 120) * 500 + i;
      end;
  end;
end;
 
procedure TForm1.AddNormalArray(const LS: TLineSeries;
  var aData: TData);
var
  i: Integer;
  HighData: Double;
begin
  HighData:=0;
  for i:=0 to High(aData) do
    if Abs(aData[i]) > HighData then HighData:=Abs(aData[i]);
 
  for i:=0 to High(aData) do
    LS.AddXY(i, aData[i] / HighData);
end; 
Dateianhänge
TestChart.zip
(3.45 KiB) 72-mal heruntergeladen
Zuletzt geändert von Michl am Fr 1. Mai 2015, 11:56, insgesamt 3-mal geändert.

Code: Alles auswählen

type
  TLiveSelection = (lsMoney, lsChilds, lsTime);
  TLive = Array[0..1] of TLiveSelection;  

wp_xyz
Beiträge: 5382
Registriert: Fr 8. Apr 2011, 09:01

Re: TAChart Normalisierte LineSeries

Beitrag von wp_xyz »

Ich denke, das geht mit einer AutoScaleAxisTransformation - genauer gesagt: mehreren, jeweils eine pro Series. Das legt hinter den Chart ein unsichtbares Achsensystem. Das Verfahren entspricht dem Vorgehen, wenn man eine Achse in verschiedene Bereiche ("Panes") einteilen will (http://wiki.lazarus.freepascal.org/TACh ... _one_Chart).

Im Detail:
  • Jede Series braucht eine eigene y-Achse: Rechtsklick im Komponentenbaum auf AxisList, "Add Item", "Alignment" auf "calLeft"
  • Jede y-Achse mit der zugehörigen Series verbinden: Dazu den "AxisIndexY" jeder Series auf den index der entsprechenden y-Achse in der AxisList setzen
  • Pro Series eine ChartAxisTransformations-Komponente aufs Formular klicken
  • Die 1.ChartAxisTransformations-Komponente im Feld "Transformations" der 1.y-Achse eintragen, entsprechend mit den anderen.
  • Auf jeder ChartAxisTransformations-Komponente einen Doppelklick und dann eine AutoscaleAxisTransform hinzufügen
  • Wenn die automatisch skalierte Achse von 0 bis 100% laufen soll, setze den MaxValue jeder AutoScaleTransform auf 100 und das UseMax auf true. Die unsichtbare gemeinsame y-Achse läuft vom Minimum bis zum Maximum, das so definiert ist, also von 0 bis 100.
  • Links werden jetzt entsprechend viele y-Achsen angezeigt entsprechend den Daten in den zugehörigen Series.
  • Um diese auszublenden, diese Achsen auf Visible = false setzen
  • Um eine neue gemeinsame y-Achse von 0-100 anzuzeigen, eine weitere y-Achse hinzufügen und deren Range auf 0-100 setzen (Range.Max=100, Range.UseMax=True)
  • Wenn ich nichts vergessen habe, sollten jetzt alle Series auf den vollen Achsenbereich skaliert sein und es sollte eine gemeinsame y-Achse von 0 bis 100 gezeigt werden.
Im Anhang ist ein Beispiel-Projekt, bei dem die LineSeries y-Werte zwischen 0 und 1 hat und die BarSeries zwischen 0 und 2. Beide Series werden über den ganzen Bereich der y-Achse skaliert - ohne eine Zeile Code... Vorteil ist, dass die y-Werte erhalten bleiben, wenn man also z.B. mit einem DataPointHint-Tool die y-Werte auslesen möchte, bleiben sie stimmig.
Dateianhänge
Normalized_Series.zip
(2.2 KiB) 92-mal heruntergeladen

wp_xyz
Beiträge: 5382
Registriert: Fr 8. Apr 2011, 09:01

Re: TAChart Normalisierte LineSeries

Beitrag von wp_xyz »

Übrigens, wenn die Rohdaten in einem Array vorliegen ("MyData" in deinem Code), musst du sie nicht extra per "Series.AddXY" o.ä. in die interne ListSource der Series kopieren, sondern du kannst sie mit Hilfe einer UserDefinedChartSource direkt verwenden: Definiere mit PointsNumber, wieviele Datenpunkte vorhanden sind, und lege im Event OnGetChartDateItem fest, wo x und y, sowie ggfs. Punkt-Label und -Farbe, im Array (oder einer anderen Datenstruktur) zu finden sind. Siehe http://wiki.lazarus.freepascal.org/TACh ... hartSource

Und noch eine Variante: Deine Daten oben sind mathematische Funktionen. Dafür ist die TFuncSeries besser geeignet, weil man beim Zoomen die volle Auflösung behält (die Funktion wird für jedes oder alle paar Pixel berechnet). Und sie kann die unerwünschte Verbindungslinie vom Plus- zum Minus-Ende von Polen unterdrücken ("Domain exclusions"). Siehe http://wiki.lazarus.freepascal.org/TACh ... ion_Series.

Aber jetzt Schluss mit der Eigenwerbung zu meinen Tutorials...

Michl
Beiträge: 2513
Registriert: Di 19. Jun 2012, 12:54

Re: TAChart Normalisierte LineSeries

Beitrag von Michl »

Hallo wp,

danke für die ausführlichen Infos, da habe ich erstmal genug Brot :D
wp_xyz hat geschrieben:Beide Series werden über den ganzen Bereich der y-Achse skaliert - ohne eine Zeile Code... Vorteil ist, dass die y-Werte erhalten bleiben, wenn man also z.B. mit einem DataPointHint-Tool die y-Werte auslesen möchte, bleiben sie stimmig.
Genau das wäre mir wichtig, dass ich die y-Werte als Hint ausgeben kann (per Designer geht das leider nicht, da die LineSeries in beliebiger Zahl vom Nutzer ein- oder ausgeblendet werden können - bekomme ich aber hin).
wp_xyz hat geschrieben:Übrigens, wenn die Rohdaten in einem Array vorliegen ("MyData" in deinem Code), musst du sie nicht extra per "Series.AddXY" o.ä. in die interne ListSource der Series kopieren, sondern du kannst sie mit Hilfe einer UserDefinedChartSource direkt verwenden: Definiere mit PointsNumber, wieviele Datenpunkte vorhanden sind, und lege im Event OnGetChartDateItem fest, wo x und y, sowie ggfs. Punkt-Label und -Farbe, im Array (oder einer anderen Datenstruktur) zu finden sind. Siehe http://wiki.lazarus.freepascal.org/TACh ... hartSource
Holla, soweit bin ich noch gar nicht vorgedrungen. Eine Nutzung, wie eine Virtual-Chart, ist eine prima Geschichte. Falls das alles so passt, wäre das mein Weg :D

PS: In meinem letztlichen Projekt, sind es hauptsächlich statistische und stochastische Daten, die angezeigt/übereinandergelegt werden sollen, die mathematischen Funktionen dienten nur zur Bereitstellung von Testdaten (ich hätte auch Zufallsdaten nehmen können), trotzdem danke für den Hinweis.

Nochmals vielen Dank und einen schönen Tag!

Michael

Code: Alles auswählen

type
  TLiveSelection = (lsMoney, lsChilds, lsTime);
  TLive = Array[0..1] of TLiveSelection;  

Michl
Beiträge: 2513
Registriert: Di 19. Jun 2012, 12:54

Re: TAChart Normalisierte LineSeries

Beitrag von Michl »

Hallo wp,

meine kleinen Testprogramme laufen, werde nun versuchen diese in meinem Projekt einzupflegen :mrgreen:

PS: du hättest gar nicht nochmal so viel Zeit investieren müssen, ein Verweis auf dein Tutorial http://wiki.freepascal.org/TAChart_Tuto ... is,_Legend hätte 99% meiner Fragen beantwortet (hatte ich eben noch zusätzlich durchgespielt - ohne die entscheidenden Suchbegriffe fischt man oft im Trüben).

Danke und viele Grüße

Michael

Code: Alles auswählen

type
  TLiveSelection = (lsMoney, lsChilds, lsTime);
  TLive = Array[0..1] of TLiveSelection;  

Michl
Beiträge: 2513
Registriert: Di 19. Jun 2012, 12:54

Re: TAChart Normalisierte LineSeries

Beitrag von Michl »

Hallo wp,

eine Frage hätte ich jetzt doch noch. Da für LineSeries ein identischer Ablauf erfolgt, habe ich das Ganze in eine Klasse verschoben und erstelle unter anderem die TChartAxisTransformations zur Laufzeit. Weisst du aus dem Stehgreif, warum es dann beim Aufräumen ein SIGSEGV gibt:

Code: Alles auswählen

constructor TSeries.Create(AOwner: TComponent; Data: Pointer
  );
begin
  if not (AOwner is TChart) then
    raise Exception.create('Owner must be TChart');
 
  FChart:=AOwner as TChart;
  FData:=Data;
 
  inherited Create(FChart);
 
  LineSeries     := TLineSeries.Create(AOwner);
  Source         := TUserDefinedChartSource.Create(AOwner);
  ChartTransform := TChartAxisTransformations.Create(Nil);  //<- übergebe ich hier einen Owner, kommt es zum SIGSEGV
  Transform      := TAutoScaleAxisTransform.Create(AOwner);       
...
Ich hatte auch versucht per Hand aufzuräumen, konnte bisher aber nicht lokalisieren, in welcher Reihenfolge das erfolgen muss.

Anbei als Projekt.
Dateianhänge
TestChart.zip
(5.41 KiB) 80-mal heruntergeladen

Code: Alles auswählen

type
  TLiveSelection = (lsMoney, lsChilds, lsTime);
  TLive = Array[0..1] of TLiveSelection;  

Michl
Beiträge: 2513
Registriert: Di 19. Jun 2012, 12:54

Re: TAChart Normalisierte LineSeries

Beitrag von Michl »

Habe es herausgefunden: Ich räume nun händisch auf und muss zuvor

Code: Alles auswählen

    aSeries.ChartTransform.List.Clear;     
aufrufen.

Sorry, falls sich schon jemand Gedanken gemacht hatte :wink:

Code: Alles auswählen

type
  TLiveSelection = (lsMoney, lsChilds, lsTime);
  TLive = Array[0..1] of TLiveSelection;  

wp_xyz
Beiträge: 5382
Registriert: Fr 8. Apr 2011, 09:01

Re: TAChart Normalisierte LineSeries

Beitrag von wp_xyz »

Ich muss zugeben, der Autor von TAChart (nicht ich!) hatte gelegentlich etwas ungewöhnliche Ideen, Klassen für Sammelcontainer zu erzeugen, insbesondere mochte er offenbar das "Add" von Collections nicht. Die folgenden Fehler habe ich schon öfter in Foren gesehen:
  • Eine neue Achse erzeugt man nicht mit Chart.AxisList.Add, sondern mit "axis := TChartAxis.Create(Chart.AxisList)" - siehe dazu auch http://www.lazarusforum.de/viewtopic.php?f=18&t=7603
  • Eine neue AxisTransform (Singular!) erzeugt man nicht mit AxisTransformations.Add, sondern mit "transform := TAutoscaleAxisTransform.Create(AOwner)" und "transform.Transformations := AxisTransformations".
Weiterhin fällt mir an deinem Code auf:
  • Als Owner eine Series verwende ich immer das Formular, nicht den Chart. Hintergedanke ist die Methode AddSeries: Offenbar kann eine Series auch ohne den Chart existieren, sie muss nach dem Erzeugen ja explizit in den Chart eingebaut werden. Wenn die Series mit dem Chart zerstört wird, gibt es dann Probleme. Daher: Owner zumindest Owner des Chart.
  • Auch die AxisTransformations würde ich dem Formular zum Besitz anbieten; auch diese existieren unabhängig vom Chart.
In der beigefügten modifzierten Version deines Programms gibt es keine Exception mehr.
Dateianhänge
Michl-Transformations.zip
(3.77 KiB) 79-mal heruntergeladen

Michl
Beiträge: 2513
Registriert: Di 19. Jun 2012, 12:54

Re: TAChart Normalisierte LineSeries

Beitrag von Michl »

Interessant, ich hätte nie gedacht, dass ich mit dem falschen Erstellen schon der Fehler eingepflegt habe. Wie gesagt, ich hatte auch eine andere Lösung gefunden gehabt, welche den SIGSEGV beim Aufräumen verhindert hatte, die richtige Erstellungsmethode ist mir aber dann doch lieber!

Ob ich die Series dem Formular oder dem Chart zuordne ist in meinem Fall eigentlich egal, da ich sie tatsächlich nur benötige, wenn der Chart existiert. Ich lass aber den Constructor so, wie von dir vorgeschlagen, aufgrund der besseren Lesbarkeit.

Nochmals ein dickes Danke, dass du nicht müde wirst mir und anderen zu helfen. :)

Ap­ro­pos helfen, eine Frage hätte ich doch noch, da ich dazu per Suchmaschine nichts finden konnte. Da das aber mit diesem Thema nicht wirklich was gemein hat, mache ich mal einenen neuen Thread auf.

Code: Alles auswählen

type
  TLiveSelection = (lsMoney, lsChilds, lsTime);
  TLive = Array[0..1] of TLiveSelection;  

Antworten