ADDITIONAL SYSTEM INFORMATION :
Tested on Mac OS X 12.3 on Apple M1 MacBook Pro, Oracle JDK 20, JavaFX 21. Issue is platform independent (bug in core JavaFX chart API).
A DESCRIPTION OF THE PROBLEM :
If the data underpinning a series in an XYChart is from an observable list constructed with an extractor, then "update" events generated on the list will cause an IllegalArgumentException (duplicate children added).
The issue arises in the dataChangeListener defined in XYChart.Series (currently lines 1505-1565 of XYChart.java). This invokes dataItemsChanged (lines 550-560) which assumes that if c.getFrom() < g.getTo() from the underlying ListChangeListener.Change c, then the items have been added to the data list. Clearly this is not the case in an update fired by the extractor.
In the case where c.getFrom() < c.getTo(), dataItemsChanged invokes dataItemsAdded. In the case, e.g. of a LineChart, if the chart is not animated (animation does not really work when modifying data elements anyway), this results in calling getPlotChildren().add(symbol) with the existing symbol for the data node (ListChart.java, line 289).
Note that there are many possible use cases for an observable list that fires updates on its values (e.g. needing invalidation notifications from the data to invalidate computations of summary statistics, etc.).
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Execute the code provided and press the "Move Right" button to generate the IllegalArgumentException.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The data node should update its location without generating an exception (the node should not be added to the plot children in the case where the list is firing an update).
ACTUAL -
An update notification from the data list generates an IllegalArgumentException. In more complex examples than the one provided (e.g. dragging the data node), the exceptions can be generated at a rate that is detrimental to application performance.
---------- BEGIN SOURCE ----------
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ChartBugDemo extends Application {
@Override
public void start(Stage stage) throws Exception {
LineChart<Number, Number> chart = new LineChart<>(new NumberAxis(), new NumberAxis());
chart.setAnimated(false);
XYChart.Series<Number, Number> series = new XYChart.Series<>();
ObservableList<XYChart.Data<Number, Number>> dataList =
FXCollections.observableArrayList(data -> new Observable[]{data.XValueProperty()});
series.setData(dataList);
chart.getData().add(series);
XYChart.Data<Number, Number> left = new XYChart.Data<>(1,3);
XYChart.Data<Number, Number> middle = new XYChart.Data<>(10, 5);
XYChart.Data<Number, Number> right = new XYChart.Data<>(19, 3);
dataList.addAll(left, middle, right);
Button moveRight = new Button("Move Right");
moveRight.setOnAction(e -> middle.setXValue(middle.getXValue().doubleValue() + 1));
BorderPane root = new BorderPane();
root.setCenter(chart);
root.setTop(moveRight);
BorderPane.setAlignment(moveRight, Pos.CENTER);
BorderPane.setMargin(moveRight, new Insets(5));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
None known, other than implementing the invalidation on the data list elements values directly (essentially reimplementing the observable list extractor code from scratch).
FREQUENCY : always
Tested on Mac OS X 12.3 on Apple M1 MacBook Pro, Oracle JDK 20, JavaFX 21. Issue is platform independent (bug in core JavaFX chart API).
A DESCRIPTION OF THE PROBLEM :
If the data underpinning a series in an XYChart is from an observable list constructed with an extractor, then "update" events generated on the list will cause an IllegalArgumentException (duplicate children added).
The issue arises in the dataChangeListener defined in XYChart.Series (currently lines 1505-1565 of XYChart.java). This invokes dataItemsChanged (lines 550-560) which assumes that if c.getFrom() < g.getTo() from the underlying ListChangeListener.Change c, then the items have been added to the data list. Clearly this is not the case in an update fired by the extractor.
In the case where c.getFrom() < c.getTo(), dataItemsChanged invokes dataItemsAdded. In the case, e.g. of a LineChart, if the chart is not animated (animation does not really work when modifying data elements anyway), this results in calling getPlotChildren().add(symbol) with the existing symbol for the data node (ListChart.java, line 289).
Note that there are many possible use cases for an observable list that fires updates on its values (e.g. needing invalidation notifications from the data to invalidate computations of summary statistics, etc.).
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Execute the code provided and press the "Move Right" button to generate the IllegalArgumentException.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The data node should update its location without generating an exception (the node should not be added to the plot children in the case where the list is firing an update).
ACTUAL -
An update notification from the data list generates an IllegalArgumentException. In more complex examples than the one provided (e.g. dragging the data node), the exceptions can be generated at a rate that is detrimental to application performance.
---------- BEGIN SOURCE ----------
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ChartBugDemo extends Application {
@Override
public void start(Stage stage) throws Exception {
LineChart<Number, Number> chart = new LineChart<>(new NumberAxis(), new NumberAxis());
chart.setAnimated(false);
XYChart.Series<Number, Number> series = new XYChart.Series<>();
ObservableList<XYChart.Data<Number, Number>> dataList =
FXCollections.observableArrayList(data -> new Observable[]{data.XValueProperty()});
series.setData(dataList);
chart.getData().add(series);
XYChart.Data<Number, Number> left = new XYChart.Data<>(1,3);
XYChart.Data<Number, Number> middle = new XYChart.Data<>(10, 5);
XYChart.Data<Number, Number> right = new XYChart.Data<>(19, 3);
dataList.addAll(left, middle, right);
Button moveRight = new Button("Move Right");
moveRight.setOnAction(e -> middle.setXValue(middle.getXValue().doubleValue() + 1));
BorderPane root = new BorderPane();
root.setCenter(chart);
root.setTop(moveRight);
BorderPane.setAlignment(moveRight, Pos.CENTER);
BorderPane.setMargin(moveRight, new Insets(5));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
None known, other than implementing the invalidation on the data list elements values directly (essentially reimplementing the observable list extractor code from scratch).
FREQUENCY : always