ページ

2013年8月3日土曜日

JavaFX - ChartDataSource (1)

JavaFXでチャートを表示するには、各チャートのモデルに値を設定して、そのモデルをチャートに適用することになります。例えば、PieChartの場合はPieChart.Data、XYChartの場合はXYChart.Dataになります。そのため、チャート以外にも同じデータを、TableViewなど他の場面でも使用するために、独自でモデルを作成した場合でも、チャートに表示するためには、モデルの変換が必要になってきます。そこで、今回はこのモデルの変換を行うユーティリティクラスを考えてみたいと思います。

まずは、PieChartの場合を考えてみます。PieChartのモデルとなるPieChart.Dataには、以下のプロパティが定義されています。

  • ReadOnlyObjectProperty<PieChart> chart
  • StringProperty name
  • DoubleProperty pieValue

そこで、PieChartと独自で作成したモデルから、これらのプロパティを設定したPieChart.Dataに変換することができるようにします。

名称としては、PieChartDataSourceとし、以下のようなプロパティを定義します。

public class PieChartDataSource<T> {
    public ObjectProperty<PieChart> pieChartProperty() { return pieChart; }
    public ObjectProperty<ObservableList<T>> itemsProperty() { return items; }

    public ObjectProperty<String> namePropertyStepProperty() { return namePropertyStep; }
    public ObjectProperty<String> pieValuePropertyStepProperty() { return pieValuePropertyStep; }

    public ObjectProperty<ObservableValueSelection<T, String>> namePropertySelectionProperty() { return namePropertySelection; }
    public ObjectProperty<ObservableValueSelection<T, Double>> pieValuePropertySelectionProperty() { return pieValuePropertySelection; }
 }

PieChartと表示したいモデルのコレクションを指定し、各プロパティが設定されたときに、設定されたモデルをPieChart.Dataに変換し、PieChartに適用します。

指定されたモデルからPieChart.DataのnameとpieValueのプロパティに対する値の取得については、それぞれに対するモデルのプロパティを文字列で、namePropertyStepPropertyとpieValuePropertyStepPropertyに指定するようにします。

namePropertyStepPropertyとpieValuePropertyStepPropertyに設定された文字列から、各プロパティの値を取得する実装をnamePropertySelectionPropertyとpieValuePropertySelectionPropertyに指定します。ObservableValueSelectionは、以下のようなFunctionalInterfaceとして定義します。

@FunctionalInterface
public interface ObservableValueSelection<T, E> {
    ObservableValue<E> select(T root, String step);
}

PieChart.Dataへの変換は、各プロパティの値が変更されたときに行うようにします。

private final ObjectProperty<PieChart> pieChart = new SimpleObjectProperty<PieChart>(this, "pieChart") {
    private PieChart currentPieChart;

    @Override
    protected void invalidated() {
        if (currentPieChart != null) { PieChartDataSource.this.unbindFrom(currentPieChart); }
        currentPieChart = get();
        if (currentPieChart != null) { PieChartDataSource.this.bindTo(currentPieChart); }
    }
};

private final ObjectProperty<ObservableList<T>> items = new SimpleObjectProperty<ObservableList<T>>(this, "items") {
    private ObservableList<T> currentItems;
    @Override
    protected void invalidated() {
        if (currentItems != null) { PieChartDataSource.this.unbind(currentItems); }
        currentItems = get();
        if (currentItems != null) { PieChartDataSource.this.bind(currentItems); }
    }
};

private final ObjectProperty<String> namePropertyStep = new SimpleObjectProperty<String>(this, "namePropertyStep") {
    @Override
    protected void invalidated() {
        rebind();
    }
};

private final ObjectProperty<String> pieValuePropertyStep = new SimpleObjectProperty<String>(this, "pieValuePropertyStep") {
    @Override
    protected void invalidated() {
        rebind();
    }
};

private final ObjectProperty<ObservableValueSelection<T, String>> namePropertySelection = new SimpleObjectProperty<ObservableValueSelection<T, String>>(this, "namePropertySelection", PropertySelector::select) {
    @Override
    protected void invalidated() {
        rebind();
    }
};

private final ObjectProperty<ObservableValueSelection<T, Double>> pieValuePropertySelection = new SimpleObjectProperty<ObservableValueSelection<T, Double>>(this, "pieValuePropertySelection", PropertySelector::select) {
    @Override
    protected void invalidated() {
        rebind();
    }
};

private final ObjectProperty<ObservableList<PieChart.Data>> pieChartData = new SimpleObjectProperty<>(this, "pieChartData", FXCollections.<PieChart.Data>observableArrayList());
private final Map<T, PieChart.Data> itemPieChartDataMap = new HashMap<>();

private final ListChangeListener<T> itemListChangeListener = change -> {
    while (change.next()) {
        if (change.wasPermutated()) {
            Map<Integer, PieChart.Data> permutatedItem = new HashMap<>();
            for (int index = change.getFrom(); index < change.getTo(); ++index) {
                permutatedItem.put(change.getPermutation(index), pieChartData.get().get(index));
            }
            permutatedItem.forEach(pieChartData.get()::set);
        } else if (change.wasUpdated()) {
        } else {
            change.getRemoved().forEach(item -> {
                if (itemPieChartDataMap.containsKey(item)) {
                    pieChartData.get().remove(itemPieChartDataMap.remove(item));
                }
            });
            change.getAddedSubList().forEach(item -> pieChartData.get().add(createPieChartDataFrom(item)));
        }
    }
};

protected void bindTo(PieChart chart) {
    chart.dataProperty().bind(pieChartData);
}

protected void unbindFrom(PieChart chart) {
    chart.dataProperty().unbind();
}

protected void bind(ObservableList<T> items) {
    constructPieChartDataFrom(items);
}

protected void unbind(ObservableList<T> items) {
    pieChartData.get().clear();
    itemPieChartDataMap.clear();
    if (items != null) { items.removeListener(itemListChangeListener); }
}

protected void rebind() {
    unbind(getItems());
    bind(getItems());
}

protected void constructPieChartDataFrom(ObservableList<T> items) {
    if (items == null) { return; }
    if (getNamePropertyStep() == null || getPieValuePropertyStep() == null) { return; }

    for (T item : items) {
        pieChartData.get().add(createPieChartDataFrom(item));
    }

    items.addListener(itemListChangeListener);
}

private PieChart.Data createPieChartDataFrom(T item) {
    PieChart.Data data = new PieChart.Data("", 0);

    data.nameProperty().bind(getNamePropertySelection().select(item, getNamePropertyStep()));
    data.pieValueProperty().bind(getPieValuePropertySelection().select(item, getPieValuePropertyStep()));

    itemPieChartDataMap.put(item, data);

    return data;
}

使い方としては、チャートに表示したい以下のようなモデルのコレクションを保持するモデルを考えます。

interface RecordModel {
    StringProperty nameProperty();
    IntegerProperty valueProperty();
}

この場合、以下のように設定します。

recordPieChartDataSource = new PieChartDataSource<>();
recordPieChartDataSource.setPieChart(recordPieChart);

recordPieChartDataSource.setNamePropertyStep("name");
recordPieChartDataSource.setPieValuePropertyStep("value");
recordPieChartDataSource.itemsProperty().bind(model.recordsProperty());

今回は、PieChartに対して、PieChart.Dataへ変換するユーティリティクラスについて考えてみました。次回は、このクラスを用いて、TableViewに表示されたデータをPieChartに表示し、TableViewで値を変更するとPieChartにもその変更が適用されるようなアプリケーションを作成してみたいと思います。