Issue
I have made programmatic changes to a Java FX Line Chart and I need a programmatic way to force a re-layout of the JavaFX Chart to occur. This question has been asked/answered before but not in my context.
I have tried the typical methods that have been presented as answers to this question (see complete, minimal example code below with in-line attempts at solving the problem). None of the typical solutions to this problem work.
Specifically (sp is a StackPane):
sp.requestLayout(); // does not work
and
sp.applyCss();
sp.layout(); // does not work
placing the above code in a .runLater()
does not work.
I know that my changes are present in the chart because (1) When I resize the chart by hand my changes suddenly appear (2) When I use the "resize" method programmatically my changes appear BUT there is a different error (plus only parent nodes are supposed to use the "resize" method - not us programmers).
Below is a minimal complete set of code which reproduces the problem. When you run the code I programmatically change one of the data points to be larger when the chart is displayed. This resize works correctly. When you right-click on the chart a context menu appears with one choice ("Resize ALL the points"). When you select that single option my code resizes all the points - BUT - none of the data points are resized visually. If I resize the chart manually by dragging a side, the chart does a re-layout and all the data node sizes immediately visually change to the correct size (The size I programmatically set them to).
How can I force the re-layout to occur programmatically that I can force to occur manually? I would NOT like to do a hack (like programmatically set the stage size to be 1 pixel smaller and then set it one pixel larger).
Note: I have read that attempts to do a requestLayout()
while a layout is in progress will be ignored so perhaps something like that is going on. I think a requestLayout()
inside of a runLater()
would fix the issue of an ongoing Layout() but that has not worked either.
Update: Scaling was suggested as an alternative to changing the StackPane size. This solution may be helpful to some but not to me. The Look and Feel of scaling a symbol is different than the look and feel of changing the regions size and allowing the "symbol" to grow into that size.
As a complete aside this is my first stackoverflow post. So thanks for all the previous examples a I have used from this forum in the past & thanks in advance for the answer to this problem.
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.LineChart.SortingPolicy;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class dummy extends Application {
@Override
public void start(Stage primaryStage) {
Random random = new Random();
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("X");
yAxis.setLabel("Y");
final LineChart<Number,Number> lineChart = new LineChart<Number,Number>(xAxis,yAxis);
Series<Number,Number> series = new Series<Number,Number>();
series.setName("Dummy Data");
// Generate data
double x = 0.0;
double y = 0.0;
for (int i = 0; i < 10; i++) {
Data<Number,Number> data = new Data(x += random.nextDouble(), y+=random.nextDouble());
series.getData().add(data);
}
lineChart.getData().add(series);
lineChart.setTitle("Random Data");
lineChart.setAxisSortingPolicy(SortingPolicy.NONE);
Scene scene = new Scene(lineChart,1200,600);
Stage stage = new Stage();
stage.setScene(scene);
stage.show();
// This resizes the first data point directly (this resize is displayed correctly when program is run)
Node node = series.getData().get(0).getNode();
setSize((StackPane)node,20);
// The context menu is invoked by a right click on the line Chart. It will resize the data point based on a context menu pick
// this resize does not work....unless I resize the window manually which causes a refresh/re-layout of the chart).
lineChart.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
if (MouseButton.SECONDARY.equals(mouseEvent.getButton())) {
Scene scene = ((Node)mouseEvent.getSource()).getScene();
ContextMenu menu = createMenu(lineChart);
menu.show(scene.getWindow(), mouseEvent.getScreenX(), mouseEvent.getScreenY());
}
}
});
}
private void setSize(StackPane sp, int size) {
sp.setMinSize(size, size);
sp.setMaxSize(size, size);
sp.setPrefSize(size, size);
}
// this creates a context menu that will allow you to resize all the data point nodes
private ContextMenu createMenu(LineChart<Number,Number> lineChart) {
final ContextMenu contextMenu = new ContextMenu();
final MenuItem resize = new MenuItem("Resize ALL the points");
resize.setOnAction(new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent event) {
for (Series<Number, Number> series : lineChart.getData()) {
for (Data<Number, Number> data : series.getData()) {
StackPane sp = (StackPane)data.getNode();
setSize (sp, 20);
// The above resizes do not take effect unless/until I manually resize the chart.
// the following two calls do not do anything;
sp.applyCss();
sp.layout();
// The request to layout the node does nothing
sp.requestLayout();
// Doing both of the above as runLaters does nothing
Platform.runLater(()->{sp.applyCss();sp.layout();});
Platform.runLater(()->{sp.requestLayout();});
// Going after the parent does nothing either
Group group = (Group)sp.getParent();
group.applyCss();
group.layout();
group.requestLayout();
// Going after the parent in a run later does nothing
Platform.runLater(()->{
group.applyCss();
group.layout();
group.requestLayout();
});
// note... doing a resize [commented out below] will work-ish.
// The documentation says NOT to use it thought that as it is for internal use only.
// By work-ish, the data points are enlarged BUT they are no longer centered on the data point
// When I resize the stage they get centered again - so this "solves" my original problem but causes a different problem
////////////////////////////////////
// sp.resize(20, 20); // Uncomment this line to see how it mostly works but introduces a new issue
////////////////////////////////////
}
}
}
});
contextMenu.getItems().add(resize);
return contextMenu;
}
public static void main(String[] args) {
Application.launch(args);
}
}
Solution
You can force a relayout by using e.g. an inner class
class LineChartX<X, Y> extends LineChart<X, Y>
{
public LineChartX(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis)
{
super(xAxis, yAxis);
}
@Override
public void layoutPlotChildren()
{
super.layoutPlotChildren();
}
}
and calling
lineChart.layoutPlotChildren();
in your menu action.
Simple one-line Solution:
nodes in LineChart scene graph have these parent-child relationships: Pane chartContent - Group plotArea - Group plotContent - Path seriesLine;
layout requests for Group plotArea, defined in class XYChart, are suppressed:
private final Group plotArea = new Group(){
@Override public void requestLayout() {} // suppress layout requests
};
but Pane chartContent accepts layout requests:
Node node = series.getNode();
if (node instanceof Path) {
Path seriesLine = (Path) node;
Parent parent = seriesLine.getParent();
if (parent instanceof Group) {
Group plotContent = (Group) parent;
parent = plotContent.getParent();
if (parent instanceof Group) {
Group plotArea = (Group) parent;
parent = plotArea.getParent();
if (parent instanceof Pane) {
Pane chartContent = (Pane) parent;
chartContent.requestLayout();
}
}
}
}
so relayout of your chart can be forced by addding this single line
series.getNode().getParent().getParent().getParent().requestLayout();
to the end of your menu action handler.
Answered By - kuleis