ページ

2013年8月11日日曜日

JavaFX - ChartDataSource (2)

前回は、PieChart.Dataに変換するためのユーティリティクラスを考えてみました。今回は、このユーティリティクラスを利用して、以下のようなアプリケーションを作成してみたいと思います。

TableViewで表示されているデータをPieChartで表示し、TableViewで値を変更すると、その変更がPieChartに反映されるようにしています。また、PieChartに表示するTableViewのデータを選択できるようにもしています。

まずは、TableViewに表示するデータを以下のように定義します。

class RecordModel {
    @AutoTableColumn(order = 1, editable = false, cellStyleClass = {"name-table-cell"})
    public StringProperty nameProperty() { return name; }
    private final StringProperty name = new SimpleStringProperty(this, "name");
    public final String getName() { return name.get(); }
    public final void setName(String name) { this.name.set(name); }

    @AutoTableColumn(order = 2, prefWidth = 70, cellStyleClass = "value-table-cell")
    public IntegerProperty value1Property() { return value1; }
    private final IntegerProperty value1 = new SimpleIntegerProperty(this, "value1");
    public final int getValue1() { return value1.get(); }
    public final void setValue1(int value1) { this.value1.set(value1); }

    @AutoTableColumn(order = 3, prefWidth = 70, cellStyleClass = "value-table-cell")
    public IntegerProperty value2Property() { return value2; }
    private final IntegerProperty value2 = new SimpleIntegerProperty(this, "value2");
    public final int getValue2() { return value2.get(); }
    public final void setValue2(int value2) { this.value2.set(value2); }

    public RecordModel(String name, int value1, int value2) {
        setName(name);
        setValue1(value1);
        setValue2(value2);
    }
}

PieChartに表示する値として、value1とvalue2を定義しています。AutoTableColumnについては、こちらを参照してください。

次に、Viewを以下のように定義します。

<Scene xmlns:fx="http://javafx.com/fxml"
       fx:id="scene"
       width="720" height="480">
    <stylesheets>
        <URL value="@PieChartDynamicUpdateDemoSceneStyle.css"/>
    </stylesheets>

    <SplitPane dividerPositions="0.45">
        <HBox id="recordRegion">
            <TableView fx:id="recordTableView"
                       AutoTableColumnGeneration.enable="true"
                       editable="true"/>

            <VBox>
                <Button fx:id="addRecordButton" text="+" prefWidth="35"/>
                <Button fx:id="removeRecordButton" text="-" prefWidth="35"/>
            </VBox>
        </HBox>

        <VBox id="chartRegion">
            <Label text="Select pieValue property name."/>

            <HBox id="valueConditionRegion">
                <fx:define>
                    <ToggleGroup fx:id="valuePropertyNameGroup"/>
                </fx:define>
                <RadioButton text="Value1" toggleGroup="$valuePropertyNameGroup"
                             userData="value1"/>
                <RadioButton text="Value2" toggleGroup="$valuePropertyNameGroup"
                             userData="value2"/>
            </HBox>

            <PieChart fx:id="recordPieChart"/>
        </VBox>
    </SplitPane>
</Scene>

このViewに対するモデルを以下のように定義します。

class PieChartDynamicUpdateDemoModel {
    public ObjectProperty<ObservableList<RecordModel>> recordsProperty() { return records; }
    private final ObjectProperty<ObservableList<RecordModel>> records = new SimpleObjectProperty<>(this, "records");
    public final ObservableList<RecordModel> getRecords() { return records.get(); }
    public final void setRecords(ObservableList<RecordModel> records) { this.records.set(records); }

    public ObjectProperty<TableView.TableViewSelectionModel<RecordModel>> recordSelectionModelProperty() { return recordSelectionModel; }
    private final ObjectProperty<TableView.TableViewSelectionModel<RecordModel>> recordSelectionModel = new SimpleObjectProperty<>(this, "recordSelectionModel");
    public final TableView.TableViewSelectionModel<RecordModel> getRecordSelectionModel() { return recordSelectionModel.get(); }
    public final void setRecordSelectionModel(TableView.TableViewSelectionModel<RecordModel> recordSelectionModel) { this.recordSelectionModel.set(recordSelectionModel); }

    public ObjectProperty<String> pieChartDataNameStepProperty() { return pieChartDataNameStep; }
    private final ObjectProperty<String> pieChartDataNameStep = new SimpleObjectProperty<>(this, "pieChartDataNameStep", "name");
    public final String getPieChartDataNameStep() { return pieChartDataNameStep.get(); }
    public final void setPieChartDataNameStep(String pieChartDataNameStep) { this.pieChartDataNameStep.set(pieChartDataNameStep); }

    public ObjectProperty<String> pieChartDataValueStepProperty() { return pieChartDataValueStep; }
    private final ObjectProperty<String> pieChartDataValueStep = new SimpleObjectProperty<>(this, "pieChartDataValueStep", "value1");
    public final String getPieChartDataValueStep() { return pieChartDataValueStep.get(); }
    public final void setPieChartDataValueStep(String pieChartDataValueStep) { this.pieChartDataValueStep.set(pieChartDataValueStep); }

    public ObjectProperty<Toggle> selectedPieChartDataValueStepProperty() { return selectedPieChartDataValueStep; }
    private final ObjectProperty<Toggle> selectedPieChartDataValueStep = new SimpleObjectProperty<Toggle>(this, "selectedPieChartDataValueStep") {
        @Override
        protected void invalidated() {
            if (get() != null) {
                setPieChartDataValueStep(get().getUserData().toString());
            }
        }
    };
    public final Toggle getSelectedPieChartDataValueStep() { return selectedPieChartDataValueStep.get(); }
    public final void setSelectedPieChartDataValueStep(Toggle selectedPieChartDataValueStep) { this.selectedPieChartDataValueStep.set(selectedPieChartDataValueStep); }

    private int currentLastRecordIndex = 0;

    public void loadRecords() {
        setRecords(
            FXCollections.<RecordModel>observableArrayList(
                new RecordModel("Data 1", 10, 30),
                new RecordModel("Data 2", 38, 22),
                new RecordModel("Data 3", 20, 43),
                new RecordModel("Data 4", 40, 11),
                new RecordModel("Data 5", 30, 25)
            )
        );

        currentLastRecordIndex = 5;
    }

    public void addNewRecord() {
        if (getRecords() == null) { return; }

        getRecords().add(new RecordModel(String.format("Data %1$s", ++currentLastRecordIndex), 0, 0));
    }

    public void removeSelectedRecord() {
        if (getRecords() == null) { return; }
        if (getRecordSelectionModel() == null) { return; }

        getRecords().remove(getRecordSelectionModel().getSelectedItem());
    }
}

PieChart.DataのnameプロパティにはRecordModelのnameプロパティの値を、PieChart.DataのpieValueプロパティには、ラジオボタンで選択されたRecordModelのプロパティの値をバインドできるようにしています。

また、このViewに対するコントローラーを以下のように定義します。

class PieChartDynamicUpdateDemoSceneController {
    public ObjectProperty<PieChartDynamicUpdateDemoModel> modelProperty() { return model; }
    private final ObjectProperty<PieChartDynamicUpdateDemoModel> model = new SimpleObjectProperty<PieChartDynamicUpdateDemoModel>(this, "model") {
        private PieChartDynamicUpdateDemoModel currentModel;
        @Override
        protected void invalidated() {
            PieChartDynamicUpdateDemoSceneController.this.unbind(currentModel);
            currentModel = get();
            PieChartDynamicUpdateDemoSceneController.this.bind(currentModel);
        }
    };
    public final PieChartDynamicUpdateDemoModel getModel() { return model.get(); }
    public final void setModel(PieChartDynamicUpdateDemoModel model) { this.model.set(model); }

    @FXML
    private Scene scene;
    @FXML
    private TableView<RecordModel> recordTableView;
    @FXML
    private Button addRecordButton;
    @FXML
    private Button removeRecordButton;
    @FXML
    private ToggleGroup valuePropertyNameGroup;
    @FXML
    private PieChart recordPieChart;

    private final PieChartDataSource<RecordModel> recordPieChartDataSource = new PieChartDataSource<>();

    public void performOn(Stage stage) {
        stage.setScene(scene);
        stage.setTitle("PieChart Dynamic Update Demo");
        stage.sizeToScene();
        stage.centerOnScreen();
        stage.show();
    }

    public PieChartDynamicUpdateDemoSceneController with(PieChartDynamicUpdateDemoModel model) {
        setModel(model);
        if (model != null) { model.loadRecords(); }

        return this;
    }

    protected void bind(PieChartDynamicUpdateDemoModel model) {
        if (model == null) { return; }

        recordTableView.itemsProperty().bindBidirectional(model.recordsProperty());
        model.recordSelectionModelProperty().bind(recordTableView.selectionModelProperty());

        model.selectedPieChartDataValueStepProperty().bind(valuePropertyNameGroup.selectedToggleProperty());

        recordPieChartDataSource.namePropertyStepProperty().bind(model.pieChartDataNameStepProperty());
        recordPieChartDataSource.pieValuePropertyStepProperty().bind(model.pieChartDataValueStepProperty());
        recordPieChartDataSource.itemsProperty().bind(model.recordsProperty());

        valuePropertyNameGroup.selectToggle(valuePropertyNameGroup.getToggles().get(0));
    }

    protected void unbind(PieChartDynamicUpdateDemoModel model) {
        if (model == null) { return; }

        recordTableView.itemsProperty().unbindBidirectional(model.recordsProperty());
        model.recordSelectionModelProperty().unbind();

        model.selectedPieChartDataValueStepProperty().unbind();

        recordPieChartDataSource.namePropertyStepProperty().unbind();
        recordPieChartDataSource.pieValuePropertyStepProperty().unbind();
        recordPieChartDataSource.itemsProperty().unbind();
    }

    protected void addRecord() {
        if (getModel() == null) { return; }

        getModel().addNewRecord();
        recordTableView.requestFocus();
        recordTableView.getSelectionModel().selectLast();
    }

    protected void removeRecord() {
        if (getModel() == null) { return; }

        getModel().removeSelectedRecord();
        recordTableView.requestFocus();
    }

    @FXML
    protected void initialize() {
        recordPieChartDataSource.setPieChart(recordPieChart);
        addRecordButton.setOnAction(e -> addRecord());
        removeRecordButton.setOnAction(e -> removeRecord());
    }
}

前回作成したPieChartDataSourceに対して、必要なプロパティをバインドして、RecordModelのデータをPieChartに表示できるように設定しています。

最後に、Applicationクラスを以下のように定義します。

public class PieChartDynamicUpdateDemo extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        FXController.of(new PieChartDynamicUpdateDemoSceneController())
            .fromDefaultLocation()
            .load()
            .with(new PieChartDynamicUpdateDemoModel())
            .performOn(primaryStage);
    }

    public static void main(String... args) {
        launch(args);
    }
}

次回は、XYChartに対して、同様のユーティリティクラスを考えてみたいと思います。

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にもその変更が適用されるようなアプリケーションを作成してみたいと思います。