Wer sich als Java-Entwickler ein wenig in die zunächst ungewohnte Syntax von JavaFX eingearbeitet hat, stellt sich dann möglicherweise die Frage, wo sich die neuerworbenen Kenntnisse auch abseits von Mediaplayern, Fotoalben und Spielen sinnvoll einsetzen lassen. Ein Bereich, für den sich die Verwendung von JavaFX anbietet, ist die Visualisierung von Daten. Insbesondere wenn sich Diagramme dynamisch an veränderte Daten anpassen sollen, kann JavaFX seine Stärken ausspielen. Die Schlagworte heißen dabei Data Binding und Replace Triggers. Wie so etwas aussehen kann, soll in diesem Blogeintrag anhand eines Beispiels veranschaulicht werden. Auf Syntax und die grundlegenden Konzepte von JavaFX soll hier nicht näher eingegangen werden. Hierfür sei auf die offiziellen Hilfeseiten und insbesondere auf das Videotutorial von Robert Eckstein verwiesen. Weiterhin sollte an dieser Stelle nicht unerwähnt bleiben, dass die Entwicklung mit JavaFX derzeit noch auf die Betriebssysteme Windows und Mac Os X beschränkt ist und dass das JavaFX Plug-In für Eclipse gegenüber dem Plug-In für Netbeans deutlich weniger ausgereift wirkt.
Für unser Beispiel sollen ein Balken- und ein Kuchendiagramm erzeugt werden. Diese sollen sich dynamisch an die 5 verschiedenen Eingangsdaten anpassen. Mit Hilfe von Schiebereglern können die einzelnen Eingangswerte verändert werden. Das Balkendiagramm veranschaulicht die absoluten Werte und das Kuchendiagramm die Anteile der einzelnen Werte an der Gesamtsumme.

Abb.: Die fertige Beispielanwendung
Die hier vorgestellte Beispielanwendung besteht aus 3 Dateien:
1. Das Hauptskript (Main.fx): Hier wird der Szenengraph zusammengesetzt. Dieser enthält hauptsächlich die beiden Diagramme und die 5 Schieberegler zur Steuerung der Eingangsdaten, die mittels HBox- und VBox-Knoten angeordnet werden. Hier wird intensiv Data Binding verwendet, um Werte aneinander zu binden:
- in Zeile 28 wird die Sequence (Array-artige Struktur in JavaFX) heights an die Sequence sliders gebunden. Jede Änderung an einem Element von sliders löst automatisch eine Änderung am entsprechenden Element von heights aus. Wird z.B. der erste Slider bewegt, so wird sein neuer Wert verwendet, um den neuen Wert des entsprechenden Elements von heights zu berechnen.
- in Zeile 33 wird die Variable sum an die Werte der der einzelnen Slider gebunden. Ändert sich einer dieser Werte, wird automatisch die Summe neu berechnet.
- in Zeile 38 wird schließlich die Berechnung der prozentualen Anteile ebenfalls an die Slider gebunden, so dass die Verschiebung eines Sliders auch hier eine Neuberechnung anstößt. Hier kommt auch das bereits oben erwähnte Konzept der Replace Triggers (eingeleitet durch on replace) zum Einsatz. Die Änderung an der Variablen percentages löst den Aufruf der updatePieces-Funktion aus.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | package datenvisualisierung; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.paint.Color; import javafx.scene.layout.VBox; import javafx.scene.layout.HBox; import javafx.ext.swing.SwingLabel; import javafx.scene.shape.Rectangle; import javafx.ext.swing.SwingSlider; def colors = [Color.GREY,Color.RED,Color.MAGENTA, Color.BLUE,Color.GREEN]; def sliders: SwingSlider[] = for (i in [0..4]) { SwingSlider { maximum: 100, minimum: 1, value: 20 } } def pieChart = PieChart { colors: colors } def barChart = BarChart { colors: colors, heights: bind for (s in sliders){ s.value * 3 } } var sum = bind (sliders[0].value + sliders[1].value + sliders[2].value + sliders[3].value + sliders[4].value); def percentages: Number[] = bind for (s in sliders) (s.value * 100.0) / (sum) on replace { pieChart.updatePieces(percentages) }; Stage { title: "Interaktive Diagramme mit Data Binding" width: 500 height: 400 scene: Scene { fill: Color.WHITE content: [ pieChart, barChart, VBox { translateX: 200, translateY: 20, content: [ for (i in [0..4]){ HBox{ spacing: 20, content: [ Rectangle { width: 15, height: 15 fill: colors[i] }, SwingLabel { text: "data{i}" }, sliders[i] ] } } ] } ] } } |
2. Die Klasse BarChart (BarChart.fx) erbt von CustomNode (das ist die Basisklasse für alle eigenen Knoten) und stellt das Balkendiagramm als Knoten für den Szenengraph bereit. Diese Klasse beinhaltet 5 Rechtecke, die die einzelnen Balken des Diagrammes repräsentieren. Über Data Binding werden die y-Position (Attribut y) und die Höhe (Attribut height) der einzelnen Balken an die jeweiligen Werte der Sequence heights gebunden. Ändert sich ein Wert in heights, werden auch die daran gebundenen Werte, d.h. die Attribute y und height neu berechnet und die Darstellung ändert sich automatisch. Um die Darstellung etwas ansprechender zu gestalten, verfügen sowohl BarChart als auch PieChart über einen einfachen Schatteneffekt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | package datenvisualisierung; import javafx.scene.Group; import javafx.scene.CustomNode; import javafx.scene.paint.Color; import javafx.scene.effect.DropShadow; import javafx.scene.shape.Rectangle; public class BarChart extends CustomNode { public-init var colors:Color[]; def bars = for (i in [0 .. 4]){ Rectangle { x: 30 + i * 20, y: bind 350 - heights[i], height: bind heights[i], stroke: Color.BLACK, width: 20, fill: colors[i] } } override function create() { return group; } def group:Group = Group{ effect: DropShadow { offsetX: 10, offsetY: 10, color: Color.web("#404040"), radius: 10 } content: bars }; package var heights:Integer[] = [100,100,100,100,100]; } |
3. Die Klasse PieChart (PieChart.fx) erbt ebenfalls von CustomNode und erzeugt ein Kuchendiagramm als Knoten für den Szenengraph. Hier wird die Synchronisierung über den oben bereits angesprochenen Replace Trigger Mechanismus erreicht.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | package datenvisualisierung; import javafx.scene.Group; import javafx.scene.CustomNode; import javafx.scene.paint.Color; import javafx.scene.effect.DropShadow; import javafx.scene.shape.Arc; import javafx.scene.shape.ArcType; public class PieChart extends CustomNode { public-init var colors:Color[]; def pieces = for (i in [0 .. 4]){ Arc { centerX: 350, centerY: 250, radiusX: 100, radiusY: 100, startAngle: i*20, length: 72 fill: colors[i], stroke: Color.BLACK, type: ArcType.ROUND }; } override function create() { return group; } def group:Group = Group{ effect: DropShadow { offsetX: 10, offsetY: 10, color: Color.web("#404040"), radius: 10 } content: pieces }; public function updatePieces(percentages:Number[]){ var start: Number = 0; for (i in [0..4]){ if (i > 0) { start = percentages[ i - 1] * 3.6 + start; } pieces[i].startAngle=start; pieces[i].length=percentages[i] * 3.6; } } } |
Anhand dieses einfachen Beispiels kann man vielleicht die Möglichkeiten erahnen, die JavaFX für die Visualisierung von Daten und die GUI-Entwicklung als Ganzes bietet. Durch die deklarative Syntax und das Szenengraph-Konzept bleibt der Code kompakt und (meistens) übersichtlich.
Ich hoffe, dass das hier gezeigte Beispiel einige Anregungen für eigene Experimente mit JavaFX liefert und wünsche viel Spaß beim Ausprobieren.