Reputation: 21
I want to create a barchart with multiple series and update dynamically from a tableview. I found this code from http://java-buddy.blogspot.sg/2013/03/javafx-2-update-stackedbarchart.html but I changed the code to work on BarChart. However I don't know why it is able to work on his video but the series on the barchart did not seems to update properly for me. The two series seems to merged together like a StackedBarChart. I realized that the code below with multiple series doesn't work properly on BarChart/StackedBarChart but work perfectly on LineChart. Can anyone tell me what's wrong and why is it not working on BarChart? Here is a preview of how the code run on my computer http://www.youtube.com/watch?v=mApuxJbt92g. Thank you for your time. NOTE: I am using JRE 1.8.0_20
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.StackedBarChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;
/**
*
* @web http://java-buddy.blogspot.com/
*/
public class JavaFXMultiColumnChart extends Application {
public class Record{
private SimpleStringProperty fieldMonth;
private SimpleDoubleProperty fieldValue1;
private SimpleDoubleProperty fieldValue2;
Record(String fMonth, double fValue1, double fValue2){
this.fieldMonth = new SimpleStringProperty(fMonth);
this.fieldValue1 = new SimpleDoubleProperty(fValue1);
this.fieldValue2 = new SimpleDoubleProperty(fValue2);
}
public String getFieldMonth() {
return fieldMonth.get();
}
public double getFieldValue1() {
return fieldValue1.get();
}
public double getFieldValue2() {
return fieldValue2.get();
}
public void setFieldMonth(String fMonth) {
fieldMonth.set(fMonth);
}
public void setFieldValue1(Double fValue1) {
fieldValue1.set(fValue1);
}
public void setFieldValue2(Double fValue2) {
fieldValue2.set(fValue2);
}
}
class MyList {
ObservableList<Record> dataList;
ObservableList<XYChart.Data> xyList1;
ObservableList<XYChart.Data> xyList2;
MyList(){
dataList = FXCollections.observableArrayList();
xyList1 = FXCollections.observableArrayList();
xyList2 = FXCollections.observableArrayList();
}
public void add(Record r){
dataList.add(r);
xyList1.add(new XYChart.Data(r.getFieldMonth(), r.getFieldValue1()));
xyList2.add(new XYChart.Data(r.getFieldMonth(), r.getFieldValue2()));
}
public void update1(int pos, Double val){
xyList1.set(pos, new XYChart.Data(xyList1.get(pos).getXValue(), val));
}
public void update2(int pos, Double val){
xyList2.set(pos, new XYChart.Data(xyList2.get(pos).getXValue(), val));
}
}
MyList myList;
private TableView<Record> tableView = new TableView<>();
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("java-buddy.blogspot.com");
//prepare myList
myList = new MyList();
myList.add(new Record("January", 100, 120));
myList.add(new Record("February", 200, 210));
myList.add(new Record("March", 50, 70));
myList.add(new Record("April", 75, 50));
myList.add(new Record("May", 110, 120));
myList.add(new Record("June", 300, 200));
myList.add(new Record("July", 111, 100));
myList.add(new Record("August", 30, 50));
myList.add(new Record("September", 75, 70));
myList.add(new Record("October", 55, 50));
myList.add(new Record("November", 225, 225));
myList.add(new Record("December", 99, 100));
Group root = new Group();
tableView.setEditable(true);
Callback<TableColumn, TableCell> cellFactory =
new Callback<TableColumn, TableCell>() {
@Override
public TableCell call(TableColumn p) {
return new EditingCell();
}
};
TableColumn columnMonth = new TableColumn("Month");
columnMonth.setCellValueFactory(
new PropertyValueFactory<Record,String>("fieldMonth"));
columnMonth.setMinWidth(60);
TableColumn columnValue1 = new TableColumn("Value 1");
columnValue1.setCellValueFactory(
new PropertyValueFactory<Record,Double>("fieldValue1"));
columnValue1.setMinWidth(60);
TableColumn columnValue2 = new TableColumn("Value 2");
columnValue2.setCellValueFactory(
new PropertyValueFactory<Record,Double>("fieldValue2"));
columnValue2.setMinWidth(60);
//--- Add for Editable Cell of Value field, in Double
columnValue1.setCellFactory(cellFactory);
columnValue1.setOnEditCommit(
new EventHandler<TableColumn.CellEditEvent<Record, Double>>() {
@Override public void handle(TableColumn.CellEditEvent<Record, Double> t) {
((Record)t.getTableView().getItems().get(
t.getTablePosition().getRow())).setFieldValue1(t.getNewValue());
int pos = t.getTablePosition().getRow();
myList.update1(pos, t.getNewValue());
}
});
columnValue2.setCellFactory(cellFactory);
columnValue2.setOnEditCommit(
new EventHandler<TableColumn.CellEditEvent<Record, Double>>() {
@Override public void handle(TableColumn.CellEditEvent<Record, Double> t) {
((Record)t.getTableView()
.getItems()
.get(t.getTablePosition().getRow())).setFieldValue2(t.getNewValue());
int pos = t.getTablePosition().getRow();
myList.update2(pos, t.getNewValue());
}
});
//--- Prepare StackedBarChart
List<String> monthLabels = Arrays.asList(
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December");
final CategoryAxis xAxis1 = new CategoryAxis();
final NumberAxis yAxis1 = new NumberAxis();
xAxis1.setLabel("Month");
xAxis1.setCategories(FXCollections.<String> observableArrayList(monthLabels));
yAxis1.setLabel("Value 1");
XYChart.Series XYSeries1 = new XYChart.Series(myList.xyList1);
XYSeries1.setName("XYChart.Series 1");
final CategoryAxis xAxis2 = new CategoryAxis();
final NumberAxis yAxis2 = new NumberAxis();
xAxis2.setLabel("Month");
xAxis2.setCategories(FXCollections.<String> observableArrayList(monthLabels));
yAxis2.setLabel("Value 2");
XYChart.Series XYSeries2 = new XYChart.Series(myList.xyList2);
XYSeries2.setName("XYChart.Series 2");
/*
final StackedBarChart<String,Number> stackedBarChart = new StackedBarChart<>(xAxis1,yAxis1);
stackedBarChart.setTitle("StackedBarChart");
stackedBarChart.getData().addAll(XYSeries1, XYSeries2);
*/
final BarChart<String,Number> barChart = new BarChart<>(xAxis1,yAxis1);
barChart.setTitle("BarChart");
barChart.getData().addAll(XYSeries1, XYSeries2);
tableView.setItems(myList.dataList);
tableView.getColumns().addAll(columnMonth, columnValue1, columnValue2);
HBox hBox = new HBox();
hBox.setSpacing(10);
hBox.getChildren().addAll(tableView, barChart);
root.getChildren().add(hBox);
primaryStage.setScene(new Scene(root, 750, 400));
primaryStage.show();
}
class EditingCell extends TableCell<Record, Double> {
private TextField textField;
public EditingCell() {}
@Override
public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.selectAll();
}
@Override
public void cancelEdit() {
super.cancelEdit();
setText(String.valueOf(getItem()));
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
@Override
public void updateItem(Double item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
} else {
setText(getString());
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap()*2);
textField.setOnKeyPressed(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(Double.parseDouble(textField.getText()));
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
}
Upvotes: 2
Views: 6811
Reputation: 45456
By default, the chart has animation enabled, and when data is changed, the series is animated from the old value to the new one.
Just by setting:
barChart.setAnimated(false);
it turns out the problem disappears, no more weird behavior and both series keep separated.
But if you'll still want animation when data is changed, set:
barChart.setAnimated(true);
and to avoid the weird behavior you've noticed (both series are drawn at the same position, with no gap between them), use this (ugly) workaround:
class MyList {
...
public void update1(int pos, Double val){
xyList1.set(pos, new BarChart.Data(xyList1.get(pos).getXValue(), val));
// Workaround:
xyList2.set(pos, new BarChart.Data(xyList2.get(pos).getXValue(),
xyList2.get(pos).getYValue()));
}
public void update2(int pos, Double val){
// Workaround:
xyList1.set(pos, new BarChart.Data(xyList1.get(pos).getXValue(),
xyList1.get(pos).getYValue()));
xyList2.set(pos, new BarChart.Data(xyList2.get(pos).getXValue(), val));
}
}
By updating both series, even when only one changes, both of them are animated, and they keep their position and the gap between them.
Anyway, consider filing a bug to JIRA.
EDIT:
When adding higher values to the chart, the yAxis is not properly updated. This is a workaround to solve this.
Set the type of the data:
private class MyList {
ObservableList<Record> dataList;
ObservableList<BarChart.Data<String,Double>> xyList1;
ObservableList<BarChart.Data<String,Double>> xyList2;
...
}
Now set one pair of axes for the chart (you had two pairs).
For yAxis
set auto ranging to false
, and set the proper bounds and the tickUnit property:
final CategoryAxis xAxis = new CategoryAxis();
xAxis.setLabel("Month");
xAxis.setCategories(FXCollections.<String> observableArrayList(monthLabels));
final NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Values");
yAxis.setAutoRanging(false);
yAxis.setLowerBound(0d);
double max= Math.max(
myList.xyList1.stream().mapToDouble(d->d.getYValue()).max().getAsDouble(),
myList.xyList2.stream().mapToDouble(d->d.getYValue()).max().getAsDouble());
yAxis.setUpperBound(max);
yAxis.setTickUnit((yAxis.getUpperBound()-yAxis.getLowerBound())/10);
final BarChart<String,Number> barChart = new BarChart<>(xAxis,yAxis);
Finally you have to update the bounds after any change in the data:
columnValue1.setOnEditCommit(t-> {
t.getRowValue().setFieldValue1(t.getNewValue());
myList.update1(t.getTablePosition().getRow(), t.getNewValue());
yAxis.setUpperBound(Math.max(myList.xyList1.stream()
.mapToDouble(d->d.getYValue()).max().getAsDouble(),
myList.xyList2.stream()
.mapToDouble(d->d.getYValue()).max().getAsDouble()));
yAxis.setTickUnit((yAxis.getUpperBound()-yAxis.getLowerBound())/10);
});
columnValue2.setOnEditCommit(t-> {
t.getRowValue().setFieldValue2(t.getNewValue());
myList.update2(t.getTablePosition().getRow(), t.getNewValue());
yAxis.setUpperBound(Math.max(myList.xyList1.stream()
.mapToDouble(d->d.getYValue()).max().getAsDouble(),
myList.xyList2.stream()
.mapToDouble(d->d.getYValue()).max().getAsDouble()));
yAxis.setTickUnit((yAxis.getUpperBound()-yAxis.getLowerBound())/10);
});
Upvotes: 0
Reputation: 209330
The link from which you based your code is pretty out-of-date, and consequently the code has quite a "legacy code" feel to it. There are many new APIs you can take advantage of, such as TextFieldTableCell
. If you use this, and properly use JavaFX properties in your model class, you can eliminate all the wiring for the updates from the table cell to the data.
Additionally, if you use the EasyBind framework, it's very easy to map the table data (list of your model objects) directly to a list of XYChart.Data
objects.
These changes seem to eliminate the issues you describe, and will additionally make your code much easier to maintain:
import java.time.Month;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.util.converter.NumberStringConverter;
import org.fxmisc.easybind.EasyBind;
public class UpdatableBarChart extends Application {
@Override
public void start(Stage primaryStage) {
ObservableList<Item> items = FXCollections.observableArrayList(
item -> new Observable[] {
item.nameProperty(), item.value1Property(), item.value2Property()
});
Random rng = new Random();
Stream.of(Month.values())
.map(month -> new Item(month.toString(), rng.nextInt(2000)+ 1000, rng.nextInt(2000)+1000))
.forEach(items::add);
ObservableList<XYChart.Data<String, Number>> dataSet1 = EasyBind.map(items,
item -> new XYChart.Data<>(item.getName(), item.getValue1()));
ObservableList<XYChart.Data<String, Number>> dataSet2 = EasyBind.map(items,
item -> new XYChart.Data<>(item.getName(), item.getValue2()));
BarChart<String, Number> chart =
new BarChart<String, Number>(new CategoryAxis(), new NumberAxis());
chart.getData().add(new Series<>("Value 1", dataSet1));
chart.getData().add(new Series<>("Value 2", dataSet2));
TableView<Item> table = new TableView<>();
table.setItems(items);
table.setEditable(true);
NumberStringConverter numConverter = new NumberStringConverter();
table.getColumns().add(createCol("Name", Item::nameProperty, null));
table.getColumns().add(createCol("Value 1", Item::value1Property, numConverter));
table.getColumns().add(createCol("Value 2", Item::value2Property, numConverter));
BorderPane root = new BorderPane(table, chart, null, null, null);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private <S, T> TableColumn<S,T> createCol(String title,
Function<S,ObservableValue<T>> propertySelector,
StringConverter<T> converter) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellFactory(TextFieldTableCell.forTableColumn(converter));
col.setCellValueFactory(cellData -> propertySelector.apply(cellData.getValue()));
return col ;
}
public static void main(String[] args) {
launch(args);
}
}
With the model class as below:
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Item {
private final StringProperty name = new SimpleStringProperty(this, "name");
private final DoubleProperty value1 = new SimpleDoubleProperty(this, "value1");
private final DoubleProperty value2 = new SimpleDoubleProperty(this, "value2");
public Item(String name, double value1, double value2) {
setName(name);
setValue1(value1);
setValue2(value2);
}
public final StringProperty nameProperty() {
return this.name;
}
public final java.lang.String getName() {
return this.nameProperty().get();
}
public final void setName(final java.lang.String name) {
this.nameProperty().set(name);
}
public final DoubleProperty value1Property() {
return this.value1;
}
public final double getValue1() {
return this.value1Property().get();
}
public final void setValue1(final double value1) {
this.value1Property().set(value1);
}
public final DoubleProperty value2Property() {
return this.value2;
}
public final double getValue2() {
return this.value2Property().get();
}
public final void setValue2(final double value2) {
this.value2Property().set(value2);
}
}
Upvotes: 2