Implementierung eines PieCharts

JavaFX – Spaß mit PieCharts

Sven Ruppert
©Shutterstock.com/svilen_mitkov

JavaFX hat einige Charts, die im JDK mitgeliefert werden. Eine Variante davon ist die Implementierung eines PieCharts. Heute werden wir daraus eine n-Level Implementierung erstellen, mit der es möglich ist, in die Daten per DrillDown „hinein“ zu navigieren. 

Das Beispiel

Gehen wir im Folgenden davon aus, dass wir eine Tabelle mit 4 Spalten haben. (Vorname,  Nachname, Datum , Betrag in EUR).

Abbildung 1

Ziel ist es, in dem PieChart nacheinander durch die jeweiligen Ebenen einer Gruppierung bidirektional zu navigieren. In unserem Beispiel ist der Level 0 die Spalte 1 (Vornamen). Alle Vornamen werden in eine Gruppe pro Vornamen aufgeteilt. Damit erhalten wir zwei Gruppen. Gruppe eins ist „Willi“, Gruppe zwei „Holger“. Die Anzahl der Elemente pro Gruppe ergibt die prozentuale Gewichtung. Wenn der Benutzer nun auf eine der beiden Gruppen „klickt“, dann soll in dem PieChart der Level n+1 dargestellt werden, basierend auf der Auswahl, die getroffen worden ist. Wird also auf „Willi“ geklickt, dann erscheint ein PieChart, das die Untermenge von Willi zeigt. In diesem Fall die beiden Gruppen „Hampel“ und „Pampel“, da es Einträge zu dem Vornamen „Willi Hampel“ und „Willi Pampel“ gibt. Die Verteilung ist in unserem Beispiel wieder gleich der Anzahl der Elemente pro Gruppe.
Wieder kann der Benutzer sich für eine Gruppe entscheiden. Wird auf eine Gruppe mit der linken Maustaste geklickt, geht es einen Level tiefer (n+1). Wird mit der rechten Maustaste geklickt, geht es wieder einen Level nach oben (n-1). Wurde der letzte Level erreicht und mit der linken Maustaste geklickt, springt das Diagramm wieder auf Level 0.

Die Implementierung

Um die Werte der Tabelle zu gruppieren benötigen wir einen MapAggregator. Mit diesem Aggregator wird das allgemeine Verhalten zum Aggregieren einer Liste von Werten zu einer Map abgebildet. Die durch die Methode getKeyElement(..) zurückgelieferten Werte werden die Keys der Map bestimmt und stellen die Gruppierungswerte dar.

public abstract class MapAggregator <T, K> {

    public abstract K getKeyElement(T t);

    public Map<K, List<T>> 
    aggregate(final Collection<T> dataCollection) {
        final Map<K, List<T>> result 
             = new HashMap<>();
        for (final T dataObject : dataCollection) {
            final K key = getKeyElement(dataObject);
            if (result.containsKey(key)) {
                //ready, no action needed
            } else {
                result.put(key, new ArrayList<>());
            }
            result.get(key).add(dataObject);
        }
        return result;
    }
}

Damit die Aggregatoren in Abhängigkeit zueinander gesetzt werden, wird in der Klasse DrillDownPieChartMapAggregator eine doppelt verkettete Liste von Aggregatoren erzeugt. Jeder Aggregator kennt damit seinen Vater und seinen Nachfolger.

public abstract class DrillDownPieChartMapAggregator<T> 
            extends MapAggregator<T, String> {

    public abstract double aggregateValue(List<T> aggregatedValues);

    public abstract String getLevelName();

    private DrillDownPieChartMapAggregator<T> nextLevelAggregator;
    private DrillDownPieChartMapAggregator<T> parentLevelAggregator;

    public DrillDownPieChartMapAggregator<T> 
    getNextLevelAggregator() {
        return nextLevelAggregator;
    }

    public DrillDownPieChartMapAggregator<T>
    getParentLevelAggregator() {
        return parentLevelAggregator;
    }

    public void 
    setNextLevelAggregator(
        DrillDownPieChartMapAggregator<T> nextLevelAggregator) {
        this.nextLevelAggregator = nextLevelAggregator;
        this.nextLevelAggregator.parentLevelAggregator = this;
    }

    public boolean isLastOne() {
        return nextLevelAggregator == null;
    }

    public boolean isFirstOne() {
        return parentLevelAggregator == null;
    }

}

Für unser Beispiel gibt es noch die Klasse TransientDemoDataRow die in der Tabelle zur Darstellung einer Zeile verwendet wird. Hierbei handelt es sich lediglich um einen Dataholder.

public class TransientDemoDataRow {

    private String vorname;
    private String nachname;
    private String datum;
    private Double betrag;

    public String getVorname() {
        return vorname;
    }

    public void setVorname(String vorname) {
        this.vorname = vorname;
    }

    public String getNachname() {
        return nachname;
    }

    public void setNachname(String nachname) {
        this.nachname = nachname;
    }

    public String getDatum() {
        return datum;
    }

    public void setDatum(String datum) {
        this.datum = datum;
    }

    public Double getBetrag() {
        return betrag;
    }

    public void setBetrag(Double betrag) {
        this.betrag = betrag;
    }
}

Für unser Beispiel benötigen wir pro Spalte einen Aggregator. Diese werden in diesem Beispiel in der Klasse DrillDownPieChartPaneController definiert. Das Prinzip ist recht simpel, die Methode aggregateValue ist dafür verantwortlich, die Liste der übergebenen Werte zu einem Wert zu aggregieren/reduzieren. Hierüber wird der Prozentsatz des jeweiligen Anteils in dem PieChart berechnet. Die Methode getLevelName wird für die Anzeigen verwendet, um dem Benutzer zu dem Level einen symbolischen/logischen Namen anzeigen zu können. Für den Aggregator wird noch die Methode getKeyElement(..) benötigt, mit dessen Hilfe der Schlüssel für die Map geholt wird. Der Schlüssel ist hier immer ein String, da er auch für die Anzeige verwendet wird.

public class DrillDownPieChartPaneController implements Initializable {
    private VornameAggregator vornameAggregator 
        = new VornameAggregator();
    private NachnameAggregator nachnameAggregator 
        = new NachnameAggregator();
    private DatumAggregator datumAggregator 
        = new DatumAggregator();
    private BetragAggregator betragAggregator 
        = new BetragAggregator();

    @FXML DemoDrillDownPieChart piechart;

    @Override public void initialize(URL url, 
             ResourceBundle resourceBundle) {
        //setze Aggregatoren
        vornameAggregator.setNextLevelAggregator(nachnameAggregator);
        nachnameAggregator.setNextLevelAggregator(datumAggregator);
        datumAggregator.setNextLevelAggregator(betragAggregator);
        piechart.setRootAggregator(vornameAggregator);

        piechart.init();
    }

    public static class BetragAggregator 
            extends DrillDownPieChartMapAggregator<TransientDemoDataRow> {
        @Override public double aggregateValue(
            List<TransientDemoDataRow> aggregatedValues) {
            double betrag = 0;
            for (TransientDemoDataRow aggregatedValue : aggregatedValues) {
                betrag = betrag + aggregatedValue.getBetrag();
            }
            return betrag;
        }

        @Override public String getLevelName() {
            return "Betrag";
        }

        @Override public String 
        getKeyElement(TransientDemoDataRow transientDemoDataRow) {
            return transientDemoDataRow.getBetrag() + "";
        }
    }

    public static class DatumAggregator 
            extends DrillDownPieChartMapAggregator<TransientDemoDataRow> {
        @Override public double
        aggregateValue(List<TransientDemoDataRow> aggregatedValues) {
            return aggregatedValues.size();
        }

        @Override public String getLevelName() {
            return "Datum";
        }

        @Override public String 
        getKeyElement(TransientDemoDataRow transientDemoDataRow) {
            return transientDemoDataRow.getDatum();
        }
    }

    public static class VornameAggregator 
            extends DrillDownPieChartMapAggregator<TransientDemoDataRow> {
        @Override public double 
        aggregateValue(List<TransientDemoDataRow> aggregatedValues) {
            return aggregatedValues.size();
        }

        @Override public String getLevelName() {
            return "Vorname";
        }

        @Override public String 
        getKeyElement(TransientDemoDataRow transientDemoDataRow) {
            return transientDemoDataRow.getVorname();
        }
    }


    public static class NachnameAggregator 
            extends DrillDownPieChartMapAggregator<TransientDemoDataRow> {
        @Override public double 
        aggregateValue(List<TransientDemoDataRow> aggregatedValues) {
            return aggregatedValues.size();
        }

        @Override public String getLevelName() {
            return "Nachname";
        }

        @Override public String 
        getKeyElement(TransientDemoDataRow transientDemoDataRow) {
            return transientDemoDataRow.getNachname();
        }
    }

}

Damit ist auch die vollständige Funktion des Controllers beschrieben, da es ausschließlich ein Element in der GUI und Anwendung gibt. Zu diesem Controller gibt es noch die korrespondierende Klasse DemoDrillDownPieChartPane extends AnchorPane ohne weiteren Inhalt und die Datei DrillDownPieChartPane.fxml mit folgendem Inhalt:

<?import demo.DemoDrillDownPieChart?>
<fx:root type="demo.DrillDownPieChartPane"
         fx:controller="demo.DrillDownPieChartPaneController"
         fx:id="DrillDownPieChartPane"
         xmlns:fx="http://javafx.com/fxml" ">
    <children>
        <DemoDrillDownPieChart fx:id="piechart"/>
    </children>
</fx:root>

Aufmacherbild: Online trading concept von Shutterstock / Urheberrecht: svilen_mitkov

[ header = Seite 2: Der Kern – Erweiterung des PieChart ]

Der Kern – DrillDownPieChart<T> extends PieChart

Bis hierhin haben wir also die Aggregatoren, den Controller und das restliche Beiwerk. Final fehlt nun die Klasse DrillDownPieChart<T>. Die Klasse erbt direkt von PieChart und erweitert diese um die Funktion des DrillDown. Die Klasse bekommt zwei Attribute der Klasse DrillDownPieChartMapAggregator. Eine Instanz stellt den initialen Aggregator auf der Ebene Level 0 dar (rootAggregator). Der zweite ist der jeweils aktive Aggregator. Somit kann durch ein späteres Ersetzen des rootAggregators und folgendem init() das Verhalten des PieChart geändert werden.

private ObservableList<PieChart.Data> rootDataList;
private ObservableList<T> dataValueList;

private DrillDownPieChartMapAggregator<T> rootAggregator;
private DrillDownPieChartMapAggregator<T> activeAggregator;

protected DrillDownPieChart() {
    this.rootDataList = observableArrayList();
    this.dataValueList = observableArrayList();
}

public void init() {
    rootDataList.clear();
    this.setTitle("");

    activeAggregator = rootAggregator;
    setVisible(true);

    aggregateNextLevel(dataValueList);
}

Das fehlende Puzzleteil ist nun der Ablauf der Aggregation selbst. Er ist recht simpel. Für die aktive Liste von Daten aus der Tabelle wird der aktive Aggregator aufgerufen. Aus der erzeugten Map werden für jede Key/Value Kombination der Map die Liste der Values zu einem Wert aggregiert und ein Data-Element für das PieChart erzeugt. Abschließend wird für das graphische Element ein OnMouseClicked EventHandler registriert. In diesem wird basierend auf dem gedrückten Button der Maus entschieden, ob es einen Level nach oben (n-1) oder nach unten (n+1) geht. Das wird durch das Setzen des aktiven Aggregators erreicht, der auch sofort auf die Wertemenge angewendet wird. Siehe auch http://www.youtube.com/watch?v=TYvEBZKsDkU

private void aggregateNextLevel(
    final ObservableList<T> dataValueList) {
        ObservableList<Data> levelDataList 
            = observableArrayList();
        setData(levelDataList);

        if (activeAggregator == null) {
            init();
        } else {
            final Map<String, List<T>> aggregatedMap 
                = activeAggregator.aggregate(dataValueList);
            setTitle("..." 
                + activeAggregator.getLevelName() 
                + "...");
            aggregatedMap.entrySet().forEach((entry)->{
                final String key = entry.getKey();
                final ObservableList<T> valuesForNextLevel 
                    = observableArrayList(entry.getValue());
                final Data data 
                    = new Data(key, activeAggregator
                            .aggregateValue(valuesForNextLevel));
                levelDataList.add(data);
                final Node dataNode = data.getNode();
                dataNode.setOnMouseClicked(mouseEvent -> {
                    final MouseButton mouseButton 
                        = mouseEvent.getButton();
                    if (mouseButton.equals(SECONDARY)) {
                        //level back
                        activeAggregator = activeAggregator
                            .getParentLevelAggregator();
                    } else {
                        activeAggregator = activeAggregator
                            .getNextLevelAggregator();
                    }
                    aggregateNextLevel(valuesForNextLevel);
                });
            });
        }
    }

Fazit

Die Basis Charts von JavaFX sind sehr gut und lassen sich funktional sehr einfach um recht komplexe Eigenschaften erweitern. In einem späteren Artikel werde ich zeigen, wie man eine CDI-Integration vornehmen kann. Im genannten Repository befindet sich eine Java-8-Lösung dieser Komponente.
Die Quelltexte zu diesem Text sind unter [1] zu finden. Wer umfangreichere Beispiele zu diesem Thema sehen möchte, dem empfehle einen Blick auf [2].

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: