Wie lässt sich eine Diagrammachse mit frei definierten Eigenschaften erstellen?
Die in JavaFX für die Datenrepräsentation in
Diagrammen üblicherweise verwendeten Achsentypen CategoryAxis
für nicht-numerische und NumberAxis
für numerische Daten, können
bedauerlicherweise beide nicht direkt erweitert werden,
da sie final
deklariert sind. Im Falle von
NumberAxis
kann dies jedoch anhand der
Elternklasse, ValueAxis
, geschehen, wenn
für den Standardfall nicht vorgesehene
Änderungen an der Datenrepräsentation
notwendig werden.
Die Beispielklasse
Das Beispiel demonstriert dies anhand eines
Flächendiagramms, wie es bereits im gleichnamigen Artikel behandelt
wurde. Dargestellt wird auch hier wieder ein fiktiver
Gasverbrauch.
Allerdings soll die Anzeige der
Einheiten auf der Y-Achse auf den Minimum-, den
Maximum-, den 0-Wert, sowie einen zusätzlich zu
ermittelnden Durchschnittswert beschränkt werden.
Zudem
kann das Diagramm durch die Eingabe neuer Werte eines
Monatsverbrauchs editiert werden.
Die Klasse AreaChartMitEingabe
enthält
main()
und stellt das GUI des Programms
bereit. Zu Beginn werden die beiden Diagrammachsen und
eine Liste zur Speicherung des zu zeigenden Datensatzes
als Felder deklariert (Zeile 3). Die Y-Achse wird
hierbei durch eine Instanz einer von ValueAxis
abgeleiteten CustomValueAxis
realisiert,
deren Konzept weiter unten
besprochen wird. Beide Axis
-Objekte werden
in start()
dem Konstruktor der AreaChart
bei dessen Objektbildung übergeben (Zeile 11).
Nach der start()
-Methode wird in createLayout()
das Layout mit allen Komponenten erzeugt. Unter dem
Flächendiagramm werden ein Auswahlmenu zur
Monatsauswahl, ein Textfeld zur Eingabe eines
Verbrauchswertes und ein Button gesetzt. Letzterer
bewirkt durch Aufruf von updateData()
, dass
der im Texfeld eingetragene Wert dem ausgewählten
Monat zugewiesen und im Diagramm angezeigt wird. Das
Textfeld ist durch einen regulären Ausdruck so
konfiguriert, dass es nur numerische Werte akzeptiert
(Zeilen 53/54).
Die Methode createListData()
wird in start()
aufgerufen und erzeugt eine ArrayList
, die
Objekte vom Typ javafx.scene.chart.XYChart.Data
speichert. Sie dient der Initialisierung des Diagramms,
indem sie dem XYChart.Series
-Objekt
übergeben wird (Zeile 18). Es kapselt den gesamten
im Diagramm gezeigten Datensatz und wird dem
Chart-Objekt hinzugefügt (Zeile 19).
Die oberen und unteren Grenzwerte des Diagramms werden
in den Methoden getMinBoundValue()
und getMaxBoundValue()
aus den Minimal- und Maximalwerten der Liste ermittelt.
Sie werden zur Anpassung des Mittelwertes beim Update
nach der Änderung der Daten ausgeführt (Zeile
134/135).
Die oben bereits angesprochene Methode updateData()
wird bei Klick auf den Button aufgerufen und bewirkt die
Aktualisierung der im Diagramm dargestellten Daten
gemäß der Eingaben in Auswahlmenu und
Textfeld. Schließlich werden der Mittelwert der
Liste, sowie die obere und untere Grenze an das Objekt
der Y-Achse übermittelt.
Den Mittelwert liefert
computeAverage()
als einfaches
arithmetisches Mittel der in der Datenliste
gespeicherten numerischen Werte.
public class AreaChartMitEingabe extends Application { private final CategoryAxis xAxis = new CategoryAxis(); private final CustomValueAxis yAxis = new CustomValueAxis(); private List<Data<String, Number>> list = new ArrayList<Data<String, Number>>(); @Override public void start(Stage stage) { stage.setTitle("Area Chart Beispiel"); xAxis.setLabel("Monat"); final AreaChart<String, Number> ac = new AreaChart<>(xAxis, yAxis); XYChart.Series<String, Number> series = new XYChart.Series<String, Number>(); series.setName("2020"); createListData(); series.setData(FXCollections.observableArrayList(list)); ac.getData().add(series); yAxis.averageProperty().set(computeAverage(series)); Scene scene = new Scene(createLayout(ac, series)); String stylesheet = getClass().getResource("/styles/areaChartStyles.css").toExternalForm(); scene.getStylesheets().add(stylesheet); stage.setScene(scene); stage.show(); } private GridPane createLayout(AreaChart<String, Number> ac, AreaChart.Series<String, Number> series) { GridPane gp = new GridPane(); ac.setPrefSize(800, 600); ac.setMaxSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE); ac.setTitle("Gasverbrauch [m\u00b3]"); gp.add(ac, 0, 0); GridPane.setColumnSpan(ac, 3); GridPane.setHgrow(ac, Priority.ALWAYS); ComboBox<String> cb = new ComboBox<String>(); cb.setPromptText("Monat w\u00e4hlen"); ObservableList<Data<String, Number>> list = series.getData(); for (Data<String, Number> data : list) { String s = (String) data.XValueProperty().get(); cb.getItems().addAll(s); } gp.add(cb, 0, 1); GridPane.setMargin(cb, new Insets(10, 10, 10, 50)); TextField tf = new TextField(); tf.setPromptText("Wert eintragen"); tf.textProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { if (!newValue.matches("(-)\\d*")) { tf.setText(newValue.replaceAll("[^(-)\\d]", "")); } } }); gp.add(tf, 1, 1); GridPane.setMargin(tf, new Insets(10)); Button butt = new Button("Einf\u00fcgen"); butt.setOnAction(e -> updateData(series, cb.getValue(), tf.getText())); gp.add(butt, 2, 1); GridPane.setMargin(butt, new Insets(10)); return gp; } private void createListData() { if(list == null) { list = new ArrayList<Data<String, Number>>(); } list.add(new Data<String, Number>("Januar", 129d)); list.add(new Data<String, Number>("Februar", 88d)); list.add(new Data<String, Number>("M\u00e4rz", 69d)); list.add(new Data<String, Number>("April", 15d)); list.add(new Data<String, Number>("Mai", 3d)); list.add(new Data<String, Number>("Juni", 1d)); list.add(new Data<String, Number>("Juli", 0d)); list.add(new Data<String, Number>("August", 0d)); list.add(new Data<String, Number>("September", 0d)); list.add(new Data<String, Number>("Oktober", 42d)); list.add(new Data<String, Number>("November", 69d)); list.add(new Data<String, Number>("Dezember", 133d)); } // get the minimum value from list private double getMinBoundValue() { double result = Double.MAX_VALUE; for (Data<String, Number> d : list) { double n = (Double) d.getYValue(); if (n < result) { result = n; } } return result; } // get the maximum value from list private double getMaxBoundValue() { double result = Double.MIN_VALUE; for (Data<String, Number> d : list) { double n = (Double) d.getYValue(); if (n > result) { result = n; } } return result; } // compute the average of all list values private double computeAverage(XYChart.Series<String, Number> series) { ObservableList<Data<String, Number>> list = series.getData(); double d = 0; for (Data<String, Number> data : list) { Number n = (Number) data.YValueProperty().get(); d += n.doubleValue(); } double av = list.size() > 0 ? d / list.size() : 0; return av; } private void updateData(XYChart.Series<String, Number> series, String month, String doubleVal) { if (month == null || doubleVal == null) return; double value = Double.parseDouble(doubleVal); ObservableList<Data<String, Number>> list = series.getData(); for (Data<String, Number> data : list) { String s = (String) data.XValueProperty().get(); if (s.endsWith(month)) { data.setYValue(value); } } yAxis.averageProperty().set(computeAverage(series)); yAxis.upperBoundProperty().set(getMaxBoundValue()); yAxis.lowerBoundProperty().set(getMinBoundValue()); } public static void main(String[] args) { launch(args); } }
CustomValueAxis
als Erweiterung von ValueAxis
Die Klasse stellt eine numerische Y-Achse bereit, auf
der nicht, wie als Standard vorgegeben, eine
kontinuierliche Abfolge von Einheiten, sondern nur frei
definierte Markierungen angezeigt werden. Dies sind hier
der Maximal-, der Minimal-, der 0-Wert und der
Durchschnitt aller im Diagramm angezeigten Werte.
Hierzu
muss ValueAxis
erweitert und dabei eine
Reihe an Methoden überschrieben werden. Deren
jeweilige Funktionen werden in der Folge skizziert.
-
setRange()
undgetRange()
-
Die Methoden setzen, bzw. liefern die obere und
untere Anzeigegrenze der Achse unabhängig vom
Umfang des Datensatzes. Sie werden im Wesentlichen
dann verwendet, wenn
AutoRanging
auffalse
gesetzt ist. -
autoRange()
- Die Methode wird aufgerufen, wenn das AutoRanging, also die selbstständige Formatierung des Diagramms, aktiviert ist. Dies ist als Standard der Fall. Sie wird zur dynamischen Änderung der Anzeige aufgerufen und liefert die zur Formatierung notwendigen Werte.
-
calculateTickValues()
- Hier wird eine Liste mit den auf der Achse markierten Punkte (Tickmarks) zurückgegeben Es ist wichtig, darauf zu achten, dass der untere und obere Grenzwert als erster und letzter Punkt in die Liste eingetragen werden, da es ansonsten dazu kommen kann, dass einzelne Punkte nicht angezeigt werden.
-
calculateMinorTickMarks()
-
In Diagrammen kann eine zweite Ebene an
Markierungen, etwa in Form von Nachkommastellen,
angezeigt werden, wenn der Methode
setMinorTickVisible()
der Parametertrue
übergeben wurde. Die Methode liefert eine Liste mit diesen Tickmarks. Hier wird eine leere Liste zurückgegeben, da die Funktion hier nicht benötigt wird. -
getTickMarkLabel(Number value)
- Die Methode dient der Formatierung der Markierungsanzeige. Sie liefert den String des formatierten Wertes. Im Beispiel wird die Anzeige auf eine Nachkommastelle beschränkt.
-
getDisplayPosition()
-
Die Methode steuert die Positionierung der
Markierungen. Mittels
Axis.getSide()
kann die Ausrichtung der Achse ermittelt und die Position entsprechend angepasst werden.
Am Ende der Klasse, in Zeile 66, wird eine property average
deklariert, die den Durchschnittswert des gezeigten
Datensatzes abspeichert. Er wird aus dessen Werten in computeAverage()
als arithmetisches Mittel errechnet.
class CustomValueAxis extends ValueAxis<Number> { public CustomValueAxis() { super(); } @Override protected void setRange(Object range, boolean animate) { if (range != null) { Number lowerBound = ((Number[]) range)[0]; Number upperBound = ((Number[]) range)[1]; lowerBoundProperty().set(lowerBound.doubleValue()); upperBoundProperty().set(upperBound.doubleValue()); } } @Override protected Object getRange() { return new Double[] { lowerBoundProperty().get(), upperBoundProperty().get(), scaleProperty().get() }; } @Override protected Object autoRange(double minValue, double maxValue, double length, double labelSize) { return new Double[] { minValue, maxValue }; } @Override protected List<Number> calculateTickValues(double length, Object range) { ArrayList<Number> list = new ArrayList<Number>(); if (range != null) { Number lowerBound = ((Number[]) range)[0]; Number upperBound = ((Number[]) range)[1]; list.add(lowerBound); list.add(0d); list.add(averageProperty().getValue()); // Mittelwert list.add(upperBound); } return list; } @Override protected List<Number> calculateMinorTickMarks() { return new ArrayList<Number>(); } @Override protected String getTickMarkLabel(Number value) { NumberFormat formatter = NumberFormat.getInstance(); formatter.setMaximumFractionDigits(1); return formatter.format(value); } @Override public double getDisplayPosition(Number value) { double delta = upperBoundProperty().get() - lowerBoundProperty().get(); double val = value.doubleValue() - lowerBoundProperty().get(); if (getSide().isVertical()) { return (1 - (val / delta)) * getHeight(); } else { return (val / delta) * getWidth(); } } // ------------------------ Average Property ----------------------/ private SimpleDoubleProperty average = new SimpleDoubleProperty(); public final DoubleProperty averageProperty() { if (average == null) { average = new SimpleDoubleProperty(0); } return average; } public final double getAverage() { if (average != null) return average.get(); return 0; } }
Quellen
Wenn Ihnen javabeginners.de gefällt, freue ich mich über eine Spende an diese gemeinnützigen Organisationen.