Performante Visualisierung groẞer Datenmengen (ohne JFreeChart)

SuperFamicom

Aktives Mitglied
Hallo,

Ich erstelle gerade eine Anwendung, in der enorm viele Zeitreihenmessungen x-y-geplottet werden sollen, wobei mehrere Plots gleichzeitig, nebeneinander dargestellt werden müssen (zwischen tausenden und hunderttausenden Datenpunkten pro Plot; die Anzahl der Plots schwankt zwischen 10 bis 1000). Stellt man alle Datenpunkte und alle Plots gleichzeitig dar, kommt es zu enormen Perfomance-Einbuẞen. Dargestellt werden die Plots innerhalb eines JScrollPanes. Um mit den Daten arbeiten zu können, muss bei deren Darstellung stark rausgezoomt werden, d.h. die Datenpunkte rücken sehr dicht zusammen. Bevor die Datenpunkt dargestellt werden, müssen diese von der Messeinheit auf Pixel-Positionen innerhalb der Panel-Dimensionen transformiert werden, wobei allerdings die Datenpunkt-Positionen auf dem Panel von der gewählten Skalierung (Zoomfaktor für entsprechend zu skalierende Achse) abhängig sind. Die Skalierung ist i.d.R. so gewählt, dass ein Groẞteil der Datenpunkte nicht gezeichnet werden muss, da diese überlappen.
Zudem sollen die Datenpunkte untereinander verbunden werden, d.h. Interpolationsberechnungen werden zwischen den Datenpunkten angestellt.

Bereits bei wenig dargestellten Plots, ruckelt es gewaltig.
Die Frage ist, welche Ansätze helfen, um hier die Performance zu verbessern? Wie sollten derartige Datenmenge aufbewahrt werden, bzw. wie kann man bei flexiblem Zoomfaktor möglichst performant auf die nur notwendigen Datenpunkte zugreifen, diese transformieren, interpolieren bzw. darstellen? Bisher speicher ich diese als ArrayList<double[/*Werte der Zeitachse*/][/*Werte der Datenpunkte*/]>, wobei jedes ArrayList-Element ein Zeitreihen-Log darstellt.

Leider bietet JFreeChart für dieses Projekt nicht adäquat Darstellungsmöglichkeiten, sodass ich die Plots selber schreiben muss.
 
Zuletzt bearbeitet:

Natac

Bekanntes Mitglied
Du speicherst deine Daten doch sicher in einer Datenbank, oder?

Ich denke, ich würde versuchen mir einen Algorithmus zu überlegen, mit dem du nur die Punkte aus der Datenbank holen musst, die du für die aktuelle Zoomstufe benötigst. Vielleicht kannst du auch schon beim abspeichern festlegen, bei welchen Zoomstufen ein Datenpunkt sichtbar wird.

Außerdem denke ich, dass eine ArrayList<double[][]> vielleicht nicht die Ideale Struktur für deine Werte sind. Ich würde das Doppel-double-Array auf jeden Fall in eine eigene Klasse "Messreihe" (o.ä.) auslagern, die ich mit entsprechenden Methoden versehe, um an die Werte zu kommen. Intern kannst du ja immernoch ein double[][]-array pflegen.
 
Zuletzt bearbeitet:

SuperFamicom

Aktives Mitglied
Du speicherst deine Daten doch sicher in einer Datenbank, oder?

Nein. Welches Datenbanksystem lässt sich in diesem Fall empfehlen?


Ich denke, ich würde versuchen mir einen Algorithmus zu überlegen, mit dem du nur die Punkte aus der Datenbank holen musst, die du für die aktuelle Zoomstufe benötigst. Vielleicht kannst du auch schon beim abspeichern festlegen, bei welchen Zoomstufen ein Datenpunkt sichtbar wird.

Tricky, tricky.

Als - zugegeben sehr unschöne - Lösung habe ich bisher ein ZoomEvent eingeführt, deren Listener (= die zu zeichnenden Logs) ein HashSet aus Datenpunkten aus dem Originaldatensatz erzeugen. Die Datenpunkte werden als int geparsed bzw. zu Pixelpositionen abgerundet. Fallen mehrere Datenpunkte auf ein Pixel, werden die Überschüssigen erst gar nicht im HashSet aufgenommen. Gezeichnet wird dann von Datenpunkt zu Datenpunkt des HashSets.


Außerdem denke ich, dass eine ArrayList<double[][]> vielleicht nicht die Ideale Struktur für deine Werte sind. Ich würde das Doppel-double-Array auf jeden Fall in eine eigene Klasse "Messreihe" (o.ä.) auslagern, die ich mit entsprechenden Methoden versehe, um an die Werte zu kommen. Intern kannst du ja immernoch ein double[][]-array pflegen.

ArrayList<double[][]> war eigentlich nich ganz korrekt. Sorry für die falsche Darstellung. Sowas wie die Klasse "Messreihe" mit double[] x und double[] y habe ich bereits.
Die einzelnen "Messreihe"-Objekte sind allerdings in der ArrayList hinterlegt.
 
Zuletzt bearbeitet:

Harry Kane

Top Contributor
Bist du sicher, daß du JFreeChart von der Performance her ausgereizt hast? Die in JFreeChart eingebauten Klassen sind zwar nur eingeschränkt für wirklich riesige Datemengen nur bedingt geeignet, da aber die Kommunikation zwischen den einzelnen Teilen weitestgehend über Interfaces erfolgt, kann man grundsätzlich eigene Komponenten/Algorithmen verwenden. Ich würde es mir sehr genau überlegen, bevor ich anfangen würde, ein komplettes Visualisierungspaket nachzubauen.
Noch ein paar Fragen zur Struktur der Daten:
- Sind die Datenpunkte nach aufsteigendem x-Wert sortiert oder können mit wenig Aufwand sortiert werden? Bei Zeitreihenmesungen sollte das der Fall sein, wenn die Datenpunkt in der Reihenfolge in der sie vom Messgerät kommen, abgelegt werden.
- Reden wir NUR über Plots bei denen die x-Achse eine Zeit ist, oder brauchst du auch Plots "y-Werte der Messreihe 1 gegen y-Werte der Messreihe 2"?
- Haben deine Daten zufällig eine tabellenartige Struktur, wo eine Spalte Zeitstempel enthält und die anderen Spalten die Messwerte, d. h. die x-Werte vom n-ten Datenpunkt sind für alle Datenreihen dieselben?
- Ist der Abstand der x-Koordinate zwischen zwei aufeinanderfolgenden Punkten konstant, d. h werden die Zeitreihenmessungen mit einer konstanten Frequenz durchgeführt?
 

SuperFamicom

Aktives Mitglied
Bist du sicher, daß du JFreeChart von der Performance her ausgereizt hast? Die in JFreeChart eingebauten Klassen sind zwar nur eingeschränkt für wirklich riesige Datemengen nur bedingt geeignet, da aber die Kommunikation zwischen den einzelnen Teilen weitestgehend über Interfaces erfolgt, kann man grundsätzlich eigene Komponenten/Algorithmen verwenden. Ich würde es mir sehr genau überlegen, bevor ich anfangen würde, ein komplettes Visualisierungspaket nachzubauen.

Auf Performance habe ich JFreeChart nicht getestet, weil wie bereits erwähnt, JFreeChart nicht die gewünschten Darstellungsmöglichkeiten bereitstellt, die zum Arbeiten mit den Logs gängig sind.


- Sind die Datenpunkte nach aufsteigendem x-Wert sortiert oder können mit wenig Aufwand sortiert werden? Bei Zeitreihenmesungen sollte das der Fall sein, wenn die Datenpunkt in der Reihenfolge in der sie vom Messgerät kommen, abgelegt werden.

Die Daten werden bereits aufsteigend sortiert eingelesen. Darum muss ich mich nicht kümmern.


- Reden wir NUR über Plots bei denen die x-Achse eine Zeit ist, oder brauchst du auch Plots "y-Werte der Messreihe 1 gegen y-Werte der Messreihe 2"?

Die verschiedene Logs sollten auch cross-plotbar sein; sprich y-Werte der Messreihe1 vs. y-Werte der Messreihe2.


- Haben deine Daten zufällig eine tabellenartige Struktur, wo eine Spalte Zeitstempel enthält und die anderen Spalten die Messwerte, d. h. die x-Werte vom n-ten Datenpunkt sind für alle Datenreihen dieselben?

Jap, die Daten liegen zwar im Tab-space getrennten Format vor, repräsentieren aber quasi eine Tabelle. Spalte1 enthält den Zeitstempel, während alle Messreihen mit Spalte1 zeitindiziert sind.


- Ist der Abstand der x-Koordinate zwischen zwei aufeinanderfolgenden Punkten konstant, d. h werden die Zeitreihenmessungen mit einer konstanten Frequenz durchgeführt?

Die Sample-Rate innerhalb einer Messreihe als auch zwischen den verschiedenen Messreihen ist ident.
 
Zuletzt bearbeitet:

Harry Kane

Top Contributor
Sorry, wenn ich weiter auf JFreeChart rumreite, aber was spricht z. B. dagegen, für n Datenreihenn ChartPanels in einem FlowLayout anzuordnen, oder dir statt des ChartPanels eine andere JComponent zu schreiben, die es erlaubt, charts in einer Reihe anzuzeigen?
ich weiss ja nicht, wie weit du schon bist, aber ich habe auch mal angefangen, mit eine Chartbibliothek zu basteln. Das ganze lief aber ziemlich schnell aus dem Ruder, und ich war sehr froh, als ich JFreeChart entdeckt habe.
Eins wäre noch wichtig: ändern sich die Daten häufig, d. h. werden schnell viele Datenpunkte hinzugefügt?
 
Zuletzt bearbeitet:

SuperFamicom

Aktives Mitglied
@HarryKane:

Sorry, wenn ich weiter auf JFreeChart rumreite, aber was spricht z. B. dagegen, für n Datenreihenn ChartPanels in einem FlowLayout anzuordnen, oder dir statt des ChartPanels eine andere JComponent zu schreiben, die es erlaubt, charts in einer Reihe anzuzeigen?

Hm, also ich bezweifle, dass sich mit JFreeChart wie auf folgenden Screenshots Dargestelltes realisieren lässt. Vielleicht irre ich mich. In diesem Fall bitte ich um Aufklärung ☺

Image View - TIS 1
Image View - TIS 2
Image View - TIS 3

Sorry für die schlechte Qualität der Bilder. Zum besseren Verständnis: Die "Zeit"achse entspricht der Ordinate.

Kurz noch etwas zu den Anforderungen:
Die Logs werden eingefärbt von Kurve zu Spaltenbegrenzung bzw. von Kurve zu Kurve in Kurven-overlays. Eingefärbt heisst hier auch mit Texturen oder anderen Messreihenwerten, die als Farbspektren ausgedrückt werden und mit der einzufärbenden Messreihe zeitindiziert sind. Ausserdem soll zwischen Plots interpoliert (gezeichnet) werden.


ich weiss ja nicht, wie weit du schon bist, aber ich habe auch mal angefangen, mit eine Chartbibliothek zu basteln.

Nicht arg weit. Die Darstellung von Logs funktioniert bereits, diente aber lediglich dazu, zu testen, ob das Einlesen der Dateien soweit funktioniert. Der nächste Schritt sollte eigentlich das Erstellen von einer Chartbibliothek sein.

Eins wäre noch wichtig: ändern sich die Daten häufig, d. h. werden schnell viele Datenpunkte hinzugefügt?

Datenpunkte werden nicht hinzugefügt, höchstens (entlang der Abzisse) modifiziert. Die Logs dienen Basis für berechnete Logs, wobei Berechnungen eher einmalige anstelle von permanent zu kalkulierende Events sind.


@Natac:
Die Daten werden einmalig aus Dateien ausgelesen, die während des Logging-Vorganges generiert wurden. Die notwendigen Informationen der ausgelesenen Dateien werden in Objekte abgelegt. Die Messreihen selber sind double arrays, gesammelt in ArrayLists und lassen sich über Getter abgreifen.
Beantwortet das deine Frage?
 

Harry Kane

Top Contributor
Bei einigen screenshots gibt es für mich etwas zu viel grafisches blingbling, um zu erkennen, worum es da eigentlich geht.
Im wesentlichen sollten sich die Charts als XYPlots mit einem XYLineAndShapeRenderer, XYAreaRenderer oder XYBarRenderer realisieren lassen.
Wenn die Geschwindigkeit beim Einlesen der Daten ein Punkt ist: Wenn du das Format indem die Daten gespeichert werden, beeinflussen kannst, würde ich auf jeden Fall zu einem Binärformat raten. Du kannst ja mal folgendes laufen lassen:
Java:
public class StreamTest {
    public static void main(String[] args) throws Exception{
        writeNumber("zahlen", 5000000);
        //readBinaryAndText("zahlen");
        readTextAndBinary("zahlen");
        
    }
    static void writeNumber(String name, int count) throws Exception{
        DataOutputStream binary = new DataOutputStream(new FileOutputStream(name+".bin"));
        BufferedWriter text = new BufferedWriter(new FileWriter(name+".text"));
        Random r = new Random();
        binary.writeInt(count);
        text.write(String.valueOf(count));
        text.newLine();
        double line = 0.0;
        for(int i = 0; i < count; i++){
            line = r.nextDouble()*512;
            binary.writeDouble(line);
            text.write(String.valueOf(line));
            text.newLine();
        }
        binary.flush();
        binary.close();
        text.flush();
        text.close();
        System.out.println("Finished");
    }
    static void readBinaryAndText(String name) throws Exception{
        DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(name+".bin")));
        int count = dis.readInt();
        double value;
        long start = System.currentTimeMillis();
        for(int i = 0; i < count; i++){
            value = dis.readDouble();
        }
        System.out.println("Zeit für Binär :" + (System.currentTimeMillis() - start));
        dis.close();
        LineNumberReader lnr = new LineNumberReader(new FileReader(name+".text"));
        count = Integer.parseInt(lnr.readLine());
        start = System.currentTimeMillis();
        for(int i = 0; i < count; i++){
            value = Double.parseDouble(lnr.readLine());
        }
        System.out.println("Zeit für Text :" + (System.currentTimeMillis() - start));
        lnr.close();
    }
    static void readTextAndBinary(String name) throws Exception{
        LineNumberReader lnr = new LineNumberReader(new FileReader(name+".text"));
        double value;
        int count = Integer.parseInt(lnr.readLine());
        long start = System.currentTimeMillis();
        for(int i = 0; i < count; i++){
            value = Double.parseDouble(lnr.readLine());
        }
        System.out.println("Zeit für Text :" + (System.currentTimeMillis() - start));
        lnr.close();
        DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(name+".bin")));
        count = dis.readInt();
        start = System.currentTimeMillis();
        for(int i = 0; i < count; i++){
            value = dis.readDouble();
        }
        System.out.println("Zeit für Binär :" + (System.currentTimeMillis() - start));
        dis.close();
    }
}
Auf meiner schon etws betagten Maschine ist das Einlesen der Binärdatei um den Faktor 20 schneller.

Wenn deine Datenpunkte einen konstanten Abstand entlang der x-Achse haben, ist es übrigens nicht notwendig, diese separat mit abzuspeichern. Wenn die x-Koordinate des 1. Punktes bekannt ist (x0), und der Abstand (deltaX), ist die x-Koordinate des n-ten Punktes x0 + (n-1)*deltaX.
 

SuperFamicom

Aktives Mitglied
Bei einigen screenshots gibt es für mich etwas zu viel grafisches blingbling, um zu erkennen, worum es da eigentlich geht.

Okay, Screenshot 3 war wohl etwas zu viel ☺ Solange mit Screenshot 1 und 2 mehr oder weniger klar ist, wohin es gehen soll...


Im wesentlichen sollten sich die Charts als XYPlots mit einem XYLineAndShapeRenderer, XYAreaRenderer oder XYBarRenderer realisieren lassen.

Muss ich mich wohl wirklich nochmal hinsetzen und in JFreeChart reinfuchsen. Das wohl wichtigste Feature, was es zu realisieren gilt, wäre das Einfärben der Logs - wie im ersten Screenshot ganz gut zu erkennen ist: links der vertikal dargestellten Messreihenkurven sind Farbverläufe dargestellt. Diese Farbverläufe repräsentieren andere Messreihen (eine Messreihe ist skaliert zwischen so-und-so und lässt sich in eine Farbpalette überführen).


Wenn die Geschwindigkeit beim Einlesen der Daten ein Punkt ist: Wenn du das Format indem die Daten gespeichert werden, beeinflussen kannst, würde ich auf jeden Fall zu einem Binärformat raten. Du kannst ja mal folgendes laufen lassen...

Wüsste nicht, dass Binärformate so ohne Weiteres möglich wären. Das Einlesen der Daten aus einer Datei ist eigentlich auch kein Thema. Einen Reader habe ich bereits gebaut und der funktioniert soweit auch ganz gut. Die Daten sind in einer Collection abgelegt. Nur am schnellen Abgreifen relevanter zu zeichnender Datenpunkte, die im derzeitig dargestellten Plotausschnitt benötigt werden, hapert es. Die Messreihen sind so lang, dass sie nicht auf den Bildschirm passen. Daher bediene ich mich eines JScrollPanes, mit denen man durch den ellenlangen Plot bei einem bestimmten Zoomfaktor durchscrollt.
Mir kam die Idee, man könne den Viewport abfragen und dessen Position und Dimension nutzen, um einen bestimmten Bereich aus dem Array oder der Collection herauszupicken und darzustellen. Genau hier fehlt mir aber der Brückenschlag zwischen Viewport und geclippter Datensatz. Ungewiss ist auch, ob das flüssig funktioniert, da häufig viele Datenpunkte auf einen Pixel zusammenfallen und enorm viele Messreihen parallel nebeneinander dargestellt werden und dementsprechend auch viele Arrays oder Collections während des Scrollens immer wieder auf's Neue durchgerödelt werden müssen.

Wenn deine Datenpunkte einen konstanten Abstand entlang der x-Achse haben, ist es übrigens nicht notwendig, diese separat mit abzuspeichern. Wenn die x-Koordinate des 1. Punktes bekannt ist (x0), und der Abstand (deltaX), ist die x-Koordinate des n-ten Punktes x0 + (n-1)*deltaX.

Interessante Idee. Die Sample-Rate ist in der Tat konstant und kann einfach auf den Array-Index multipliziert werden.
 
Zuletzt bearbeitet:

Harry Kane

Top Contributor
[...]
Nur am schnellen Abgreifen relevanter zu zeichnender Datenpunkte, die im derzeitig dargestellten Plotausschnitt benötigt werden, hapert es. Die Messreihen sind so lang, dass sie nicht auf den Bildschirm passen. Daher bediene ich mich eines JScrollPanes, mit denen man durch den ellenlangen Plot bei einem bestimmten Zoomfaktor durchscrollt.
Mir kam die Idee, man könne den Viewport abfragen und dessen Position und Dimension nutzen, um einen bestimmten Bereich aus dem Array oder der Collection herauszupicken und darzustellen. Genau hier fehlt mir aber der Brückenschlag zwischen Viewport und geclippter Datensatz. Ungewiss ist auch, ob das flüssig funktioniert, da häufig viele Datenpunkte auf einen Pixel zusammenfallen und enorm viele Messreihen parallel nebeneinander dargestellt werden und dementsprechend auch viele Arrays oder Collections während des Scrollens immer wieder auf's Neue durchgerödelt werden müssen.
Der "Brückenschlag zwischen View und Datensatz" erfolgt über ein Objekt, mit dem die Datenpunkte in Bildschirmpunkte umgerechnet werden. In JFreeChart sind das Objekte der Klasse ValueAxis. Sie haben die Methode
Code:
 double valueToJava2D(double value, java.awt.geom.Rectangle2D area, org.jfree.ui.RectangleEdge edge)
.
Die Daten in einem XYPlot werden in einem XYDataset vorgehalten. Dieser hat die u.a. die Methoden
Code:
DomainOrder getDomainOrder()
,
Code:
int getSeriesCount()
,
Code:
int getItemCount(int series)
, und
Code:
double getX/YValue(int series, int item)
.
Beim Zeichnen werden die Daten aus dem XYDataset abgefragt, mithilfe der ValueAxis in Bildschirmkoordinaten umgerechnet und dort dann die Symbole oder Verbindungslinien gezeichnet. Um das ganze performanent zu gestalten, gibt es zwei Ansätze:

a) Die Datenpunkte, die ausserhalb des Wertebereichs der x-Achse liegen, werden erst gar nicht aus dem XYDataset abgefragt. Mit der DomainOrder kann abgefragt werden, ob die Daten in bezug auf die x-Achse sortiert sind. Bevor die Datenpunkte gerendeert werden, wird mit RendererUtilities.findLiveItems(XYDataset dataset, int series,double xLow, double xHigh) der Index des ersten und letzten Datenpunktes abgefragt, der vor oder nach dem durch xLow und xHigh angegebenen Wertebereichs der x Achse liegt. Das geschieht u.a. mit einer binären Suche. Deinen Fall, daß aus xLow und xHigh direkt die Indices der Datenpunkte berechnet werden, könntest du beispielweise über ein zusätzliches Interface abdecken.

b) Um überflüssige Zeichenoperationen zu vermeiden, d.h. das mehrfache Zeichnen eines Symbols an einer identischen Pixelposition, verwendet der SamplingXYLineRenderer folgenden Ansatz: Für alle Datenpunkte, die auf dem Bildschirm an derselben x-Pixelkoordinate liegen, wird lediglich ein senkrechter Strich zwischen der minimalen und maximalen y-Pixelkoordinate gezeichnet.

Ein scroll-artige Funktion läßt sich bei JFreeChart auch ohne JScrollPane erzeugen. Man kann nämlich den Wertebereich der Achsen nicht zur vergrößen und verkleinen, sondern auch verschieben ("panning").

Wenn du JFreeChart runtergeladen hast, könnte ich dir ev. eine kleine Beispielanwendung programmieren, anhand derer du bewerten kannst, ob das ganze performancetechnisch ausreicht.
 
Zuletzt bearbeitet:

Harry Kane

Top Contributor
So, hier ein kleines Beispiel. Abgesehen von der selbst geschriebenen XYDataset-Klasse ist der Rest JFree-Standardkost.
Auf meiner 6 J alten Maschine mit 2 Gb Ram lassen sich 30 Serien mit je 1 Mio Datenpunkten erstellen. Das ganze ist auch noch einigermaßen performant. Der Speicherverbrauch liegt bei ca. 300 MB. Zu den 30*1000000*8Byte zum Vorhalten der Arrays mit den y Datenwerten kommt also noch ein Overhead von ca. 60 MB.
Java:
package jfree;
import javax.swing.JFrame;
import org.jfree.data.xy.AbstractXYDataset;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.renderer.xy.SamplingXYLineRenderer;
import java.util.Random;
import org.jfree.data.DomainOrder;
public class LargeXYDatasetDemo {
    public static void main(String[] args){
        CombinedDomainXYPlot main = new CombinedDomainXYPlot(new NumberAxis());
        double[][] settings = new double[][]{
            {100, 5},{200, 10},{250, 2},{150, 1},{300, 50},{280, 20},
            {100, 5},{200, 10},{250, 2},{150, 1},{300, 50},{280, 20},
            {100, 5},{200, 10},{250, 2},{150, 1},{300, 50},{280, 20},
            {100, 5},{200, 10},{250, 2},{150, 1},{300, 50},{280, 20},
            {100, 5},{200, 10},{250, 2},{150, 1},{300, 50},{280, 20},
        };
        for(int i = 0; i < settings.length; i++){
            RegularXGapDataset dataset = new RegularXGapDataset("Dataset " + (i+1), 0, 1, settings[i][0], settings[i][1], 1000000);
            NumberAxis yAxis = new NumberAxis("y");
            yAxis.setAutoRangeIncludesZero(false);
            XYPlot plot = new XYPlot(dataset, new NumberAxis("x"), yAxis, new SamplingXYLineRenderer());
            main.add(plot);
            System.out.println("Plot " + i + " completed!");
        }
        main.setDomainPannable(true);
        JFreeChart chart = new JFreeChart(main);
        JFrame frame = new JFrame("Combined XYPlot Demo");
        ChartPanel cp = new ChartPanel(chart);
        cp.setMouseWheelEnabled(true);
        frame.getContentPane().add(cp);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}
class RegularXGapDataset extends AbstractXYDataset{
    private double first;
    private double delta;
    private String name;
    private double[] values;
    
    RegularXGapDataset(String name, double first, double delta, double yVal, double yInterval, int count){
        this.name= name;
        this.first = first;
        this.delta = delta;
        values = new double[count];
        Random r = new Random();
        for(int i = 0; i < count; i++){
            values[i] = yVal + (r.nextDouble() - 0.5)*yInterval;
        }
    }
    public DomainOrder getDomainOrder(){
        return DomainOrder.ASCENDING;
    }
    public int getSeriesCount(){
        return 1;
    }
    public Comparable getSeriesKey(int series){
        return name;
    }
    public int getItemCount(int series){
        return values.length;
    }
    public double getYValue(int series, int item){
        return values[item];
    }
    public double getXValue(int series, int item){
        return first + (item - 1)*delta;
    }
    public Double getY(int series, int item){
        return new Double(getYValue(series, item));
    }
    public Double getX(int series, int item){
        return new Double(getXValue(series, item));
    }
}
 

SuperFamicom

Aktives Mitglied
Danke :)
Ich habe jetzt versucht, den Code auf mein Programm zuzuschneiden. Allerdings tritt hier ein Problem auf: Wie kommt JFreeChart an das XYDataset-Objekt ran? Versucht man es wie folgt, wird das XYDataset-Objekt in der Konsole als null ausgespuckt:

Java:
import java.util.Random;
import javax.swing.JFrame;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.SamplingXYLineRenderer;
import org.jfree.data.DomainOrder;
import org.jfree.data.xy.AbstractXYDataset;

public class LargeXYDatasetDemo {
    public static void main(String[] args){
        
        CombinedDomainXYPlot main = new CombinedDomainXYPlot(new NumberAxis());
        
        double[][] settings = new double[][]{
            {100, 5},{200, 10},{250, 2},{150, 1},{300, 50},{280, 20},
            {100, 5},{200, 10},{250, 2},{150, 1},{300, 50},{280, 20},
            {100, 5},{200, 10},{250, 2},{150, 1},{300, 50},{280, 20},
            {100, 5},{200, 10},{250, 2},{150, 1},{300, 50},{280, 20},
            {100, 5},{200, 10},{250, 2},{150, 1},{300, 50},{280, 20},
        };
        
        for(int i = 0; i < settings.length; i++){
            RegularXGapDataset dataset = new RegularXGapDataset("Dataset " + (i+1), 0, 1, settings[i][0], settings[i][1], 1000000);
            NumberAxis yAxis = new NumberAxis("y");
            yAxis.setAutoRangeIncludesZero(false);
            XYPlot plot = new XYPlot(dataset, new NumberAxis("x"), yAxis, new SamplingXYLineRenderer());            
            plot.setDataset(1, dataset);
            main.add(plot);                                   
            System.out.println("Plot " + i + " completed!");
        }
        main.setDomainPannable(true);
        JFreeChart chart = new JFreeChart(main);
        JFrame frame = new JFrame("Combined XYPlot Demo");
        ChartPanel cp = new ChartPanel(chart);
        cp.setMouseWheelEnabled(true);
        frame.getContentPane().add(cp);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
        
        JFreeChart c = cp.getChart();
        System.out.println(c);
        
        CombinedDomainXYPlot p = (CombinedDomainXYPlot) c.getPlot();
        System.out.println(p);
        
        RegularXGapDataset d = (RegularXGapDataset) p.getDataset();
        System.out.println(d);
        
    }
    
}
[...]
 

SuperFamicom

Aktives Mitglied
Okay, funktioniert soweit. Noch ein paar Fragen:

1. Gibt es denn die Möglichkeit, nur die x-Achse darzustellen?
2. Da die Messreihen parallel dargestellt werden und alle mit dem gleichen Zeitstempel indiziert sind, ist es unnötig für jeden Plot die x-Achse zu zeichnen. Es reicht, die x-Achse einmalig zu zeichnen. Lassen sich also für die anderen Plots die x-Achsen samt Ticks einfach ausblenden?
3. So ganz gefallen mir nicht die vertikalen Striche, wenn JFreeChart doppelte Messpunkte zeichnet, nicht. Können diese Striche irgendwie deaktiviert werden?
4. Last but not least und wie schon in den vorigen Posts angedeutet: lassen sich die Messkurven mit Füllmustern (Farbspektren und Images) belegen?
 

Harry Kane

Top Contributor
1. Gibt es denn die Möglichkeit, nur die x-Achse darzustellen?
2. Da die Messreihen parallel dargestellt werden und alle mit dem gleichen Zeitstempel indiziert sind, ist es unnötig für jeden Plot die x-Achse zu zeichnen. Es reicht, die x-Achse einmalig zu zeichnen. Lassen sich also für die anderen Plots die x-Achsen samt Ticks einfach ausblenden?
Jede Achse lässt sich
Code:
axis.setVisible(false)
ausblenden. Und bei einem CombinedPlot gibt es doch nur eine x-Achse. Darf ich aus
Code:
jeden Plot die x-Achse
schließen, daß du keinen CombinedPlot mehr verwendest?

3. So ganz gefallen mir nicht die vertikalen Striche, wenn JFreeChart doppelte Messpunkte zeichnet, nicht. Können diese Striche irgendwie deaktiviert werden?
Welche Striche? Klingt nach entweder den gridlines, die über
Code:
plot.setRange[Domain][Minor]GridlinesVisible(false)
ausblenden lassen, oder nach die tick labels. Beide haben aber mit "doppelten Messpunkten" nix zu tun.

4. Last but not least und wie schon in den vorigen Posts angedeutet: lassen sich die Messkurven mit Füllmustern (Farbspektren und Images) belegen?
Um den Bereich unter dre Kurvezu füllen, müsstest du einen XYAreaRenderer2 verwenden. Dieser hat aber keine Optimierungen für große Datenmengen und für Punkte, die zu einem physikalischen Pixel zusammenfallen. Diese müsstest du selber programmieren. Um die Farbspektren und Images zu realiseren, könntest du GradientPaints oder TexturePaints verwenden. Wenn du nur für einen ganz bestimmten Datenpunkt eine spezielle Farbe zurückgeben möchtest, müsstest du entweder getItemPaint(int series, int item) überschreiben oder, wenn du sowieso die ganze Rendererlogik neu aufsetzt, in der drawItem-Methode des renderers eine geeignete Logik einbauen.
 

SuperFamicom

Aktives Mitglied
Jede Achse lässt sich axis.setVisible(false) ausblenden. Und bei einem CombinedPlot gibt es doch nur eine x-Achse. Darf ich aus jeden Plot die x-Achse schließen, daß du keinen CombinedPlot mehr verwendest?

Gefixt. Bin zwischenzeitig auf normale Plots umgestiegen und hab die CombinedPlot's komplett auẞen vorgelassen.

--

Welche Striche? Klingt nach entweder den gridlines, die über plot.setRange[Domain][Minor]GridlinesVisible(false) ausblenden lassen, oder nach die tick labels. Beide haben aber mit "doppelten Messpunkten" nix zu tun.

Jene aus Beitrag #11:

b) Um überflüssige Zeichenoperationen zu vermeiden, d.h. das mehrfache Zeichnen eines Symbols an einer identischen Pixelposition, verwendet der SamplingXYLineRenderer folgenden Ansatz: Für alle Datenpunkte, die auf dem Bildschirm an derselben x-Pixelkoordinate liegen, wird lediglich ein senkrechter Strich zwischen der minimalen und maximalen y-Pixelkoordinate gezeichnet.

Hierzu noch zwei Bilder:

How it looks like now
How it should look like

--

Um den Bereich unter dre Kurvezu füllen, müsstest du einen XYAreaRenderer2 verwenden. Dieser hat aber keine Optimierungen für große Datenmengen und für Punkte, die zu einem physikalischen Pixel zusammenfallen. Diese müsstest du selber programmieren. Um die Farbspektren und Images zu realiseren, könntest du GradientPaints oder TexturePaints verwenden. Wenn du nur für einen ganz bestimmten Datenpunkt eine spezielle Farbe zurückgeben möchtest, müsstest du entweder getItemPaint(int series, int item) überschreiben oder, wenn du sowieso die ganze Rendererlogik neu aufsetzt, in der drawItem-Methode des renderers eine geeignete Logik einbauen.

Okay, das lauffähig zu bekommen, bedarf sicher einiges an Zeit. Ich versuch mich mal dran.

Danke dir soweit.
 

Harry Kane

Top Contributor
Zu den senkrechten Strichen: Der SamplingXYLineRenderer ist offenbar nur für XYPlots mit horizontaler x-Achse gedacht. In deinem Fall ist die Orientierung vertikal. Daher wundert es mich, daß das ganze überhaupt einigermaßen brauchbar aussieht.:)

Ein relativ einfacher Algorithmus, um das mehrfache Zeichnen von Symbolen oder Verbindungslinien auf oder zwischen demselben physikalischen Pixel zu vermeiden, könnte so aussehen:
1. Du merkst dir z. B. in dem XYItemRendererState Objekt die letzten Pixelkoordinaten, an denen was gezeichnet wurde. Beim SamplingXYLineRenderer wäre das s.lastX und eine neu zu definierende Variable s.lastY.
2. Nach dem Umrechnen der Datenkoordinaten (x1, y1) in Bildschirmkoordinaten (transX, transY) vergleichst du transX mit s.lastX und transY mit s.lastY. Die Werte stellen dabei jeweils Pixelkoordinaten dar. Nur wenn der Abstand bei einem Paar größer als ein bestimmter Grenzwert ist, wird etwas gezeichnet. Der Grenzwert sollte bei mindestens 1 liegen. Größere Werte (3-5) sollten das zeichnen weiter beschleunigen, führen aber auch zu einer "gröberen" Darstellung. Da musst du halt ein bisschen spielen, um den besten Kompromiss zu finden.
 

SuperFamicom

Aktives Mitglied
Kannst du das Einbauen eines Renderer- und RendererState-Objektes anhand eines kleinen Beispiels illustrieren?

Beim Ableiten von SamplingXYLineRenderer und überschreiben von drawItem mit nachfolgendem Code, wird

Exception in thread "AWT-EventQueue-0" java.lang.ClassCastException: org.jfree.chart.renderer.xy.SamplingXYLineRenderer$State cannot be cast to gui.LogCurveRenderer$State


geschmissen.

Java:
public class LogCurveRenderer extends SamplingXYLineRenderer {
       
        public static class State extends XYItemRendererState {

            GeneralPath seriesPath;
            GeneralPath intervalPath;
            double dX = 1.0;
            double dY = 1.0;
            double lastX;
            double lastY;
            double openY = 0.0;
            double highY = 0.0;
            double lowY = 0.0;
            double closeY = 0.0;
            boolean lastPointGood;

            public State(PlotRenderingInfo info) {
                super(info);
            }

            public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, XYPlot plot, XYDataset data, PlotRenderingInfo info) {
                double dpi = 72;                
                LogCurveRenderer.State state = new LogCurveRenderer.State(info);
                state.seriesPath = new GeneralPath();
                state.intervalPath = new GeneralPath();
                state.dX = 72.0 / dpi;
                state.dY = 72.0 / dpi;
                return state;
            }

            @Override
            public void startSeriesPass(XYDataset dataset, int series, int firstItem, int lastItem, int pass, int passCount) {
                this.seriesPath.reset();
                this.intervalPath.reset();
                this.lastPointGood = false;
                super.startSeriesPass(dataset, series, firstItem, lastItem, pass, passCount);
            }
        
        }
       
        @Override
        public void drawItem(java.awt.Graphics2D g2, XYItemRendererState state, java.awt.geom.Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, int series, int item, CrosshairState crosshairState, int pass) {
            
            if (!getItemVisible(series, item)) {
                return;
            }

            RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
            RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
            double x1 = dataset.getXValue(series, item);
            double y1 = dataset.getYValue(series, item);
            double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
            double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
            LogCurveRenderer.State s = (LogCurveRenderer.State) state;

            if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) {
                float x = (float) transX1;
                float y = (float) transY1;
                PlotOrientation orientation = plot.getOrientation();

                if (orientation == PlotOrientation.HORIZONTAL) {
                    x = (float) transY1;
                    y = (float) transX1;
                }

                if (s.lastPointGood) {                                      
                    // Calculation routine
                } else {
                    // ...
                }
                s.lastPointGood = true;
            } else {
                s.lastPointGood = false;
            }

            if (item == s.getLastItemIndex()) {
                PathIterator it = s.seriesPath.getPathIterator(null);
                int count = 0;
                while (!it.isDone()) {
                    count++;
                    it.next();
                }
                g2.setStroke(getItemStroke(series, item));
                g2.setPaint(getItemPaint(series, item));
                g2.draw(s.seriesPath);
                g2.draw(s.intervalPath);
            }
        }

    }
 
Zuletzt bearbeitet:

Harry Kane

Top Contributor
Die Methode initialise hast du im Rumpf deiner State-Klasse deklariert. Da sie nicht Bestandteil der offiziellen XYItemRendererState-API ist, wird sie niemals aufgerufen.
Die Methode gehört in den Rumpf der Renderer-Klasse.
 

SuperFamicom

Aktives Mitglied
Hat geklappt. Habe nun Kontrolle über SamplingXYLineRenderer. Die vertikalen Linien sind eliminiert.

Last but not least versuche ich mich nun am XYAreaRenderer2. Leider erzielt dieser nicht den gewünschten Effekt:

How it looks like now
How it should look like

Nach langem Rumprobieren mit den Achsen und fillDataset (jener Datensatz, der in RGB-Werte transformiert wird und die Kurve füllen soll), habe ich keinen Schimmer, was hier falsch läuft. Der Renderer überschreibt bisher nur getItemPaint:

Java:
public class LogCurveFillRenderer extends XYAreaRenderer2 {

    private ArrayList<Color> colorMap = new ArrayList<>(); {
        colorMap.add(new Color(0, 14, 229));
        colorMap.add(new Color(16, 29, 213));
        colorMap.add(new Color(33, 45, 198));
        colorMap.add(new Color(50, 60, 183));
        colorMap.add(new Color(66, 76, 167));
        colorMap.add(new Color(83, 91, 152));
        colorMap.add(new Color(100, 107, 137));
        colorMap.add(new Color(116, 122, 122));
        colorMap.add(new Color(133, 138, 106));
        colorMap.add(new Color(150, 153, 91));
        colorMap.add(new Color(166, 169, 76));
        colorMap.add(new Color(183, 184, 61));
        colorMap.add(new Color(200, 200, 45));
        colorMap.add(new Color(216, 215, 30));
        colorMap.add(new Color(233, 231, 15));
        colorMap.add(new Color(250, 247, 0));
    }
        
    private XYDataset fillDataset = null;
    
    public LogCurveFillRenderer(DrawableLog log) {
        fillDataset = new TranslatingXYDataset(log);        
    }        
        
    @Override
    public java.awt.Paint getItemPaint(int series, int item) {        
        
        System.out.println();

        if (fillDataset != null) {      
          
            double x = fillDataset.getXValue(series, item);
            double y = fillDataset.getYValue(series, item);
            
            if (Double.isNaN(x)) {
                x = 0.0;
            }         
   
            x = Math.max(x, 0);

            if (Double.isNaN(y)) {
                y = 0.0;
            }                   
                           
            Color result = colorMap.get(0);
            double min = ((TranslatingXYDataset) fillDataset).getLog().getMinValue();
            double max = ((TranslatingXYDataset) fillDataset).getLog().getMaxValue();                        
            double xIndex = (x - min) / (max - min);                
            int colorIndex = (int) (xIndex * (colorMap.size() - 1));            
            int index = Math.min(colorIndex, colorMap.size() - 1);            
            index = Math.max(index, 0);
            
            System.out.println(
                        "series = " + series + ", item = " + item + ", colorIndex = " + colorIndex + 
                        ", index = " + index + ": " + colorMap.get(index).getRed() + "|" + 
                        colorMap.get(index).getGreen() + "|" + colorMap.get(index).getBlue()
            );
               
            result = colorMap.get(index);
            
            return result;
        
        } else {
            
            return new Color(0.0f, 0.0f, 0.0f, 0.5f);
            
        }
    }

}
Du meintest, ich könne die komplette Renderlogik in drawItem neu aufsetzen. Ich nehme an, dies soll nur dem Reduzieren unnötig zu zeichnender Datenpunkte dienlich sein, da drawItem standardmässig funktioniert und sich mit getItemPaint das Paint-Objekt abholt.
 

Harry Kane

Top Contributor
KA, was da schief läuft. Wenn du die Punkte mit dem LogCurveRenderer renderst, sieht dann alles ok aus?
Ev. hat das komische Aussehen des area charts etwas mit der Sortierung der Datenpunkte zu tun? Ev. sind auch x und y vertauscht?
Das hier funktioniert auf jeden Fall:
Java:
public class StackedXYChartDemo {
    public static void main(String[] args){
        int itemCount = 50;
        Random random = new Random();
        DefaultXYDataset ascending = new DefaultXYDataset(){
            @Override
            public DomainOrder getDomainOrder(){
                return DomainOrder.ASCENDING;
            }
        };
        for(int s = 1; s < 2; s++){
            double[][] data = new double[2][itemCount];
            for(int v = 0; v < itemCount; v++){
                data[0][v] = v;
                data[1][v] = 50 + random.nextDouble() * 100;
            }
            ascending.addSeries("Series "+s, data);
        }
        XYItemRenderer r = new MultiColorAreaRenderer(100);
        XYPlot plot = new XYPlot(ascending, new NumberAxis("x"), new NumberAxis("y"), r);
        plot.setOrientation(PlotOrientation.HORIZONTAL);
        JFreeChart chart = new JFreeChart(plot);
        JFrame frame = new JFrame("Domain Order Test");
        ChartPanel cp = new ChartPanel(chart);
        cp.setMouseWheelEnabled(true);
        frame.getContentPane().add(cp);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}
class MultiColorAreaRenderer extends XYAreaRenderer2{
    private XYDataset dataset;
    private double threshold;
    
    MultiColorAreaRenderer(double threshold){
        this.threshold = threshold;
    }
    
    public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, XYPlot plot, XYDataset dataset, PlotRenderingInfo info){
        this.dataset = dataset;
        return super.initialise(g2, dataArea, plot, dataset, info);
    }
    public Paint getItemPaint(int series, int item){
        if(dataset == null) return super.getItemPaint(series, item);
        double yValue = dataset.getYValue(series, item);
        if(yValue <= threshold) return Color.green;
        return Color.red;
    }
}
 

Ähnliche Java Themen

Neue Themen


Oben