简体   繁体   中英

How do I update a XYChart in realtime in Java (using JavaFX)?

I am creating a visualization of different sorting algos in Java using JavaFX and more specifically a scatterplot (XYChart) from the JavaFX SDK. My idea is that the scatterplot will be redrawn each time after a swap in the sorting algorithm has been conducted, I started with a simple version of insertionSort because it seemed the easiest algorithm to implement.

My problem is that when I start the sort the program locks up until the sort has been completely finished and redraws the graphic completely finished. I would like it to redraw the graph at every step. I added a Thread.sleep(x) then switched to TimeUnit.milliseconds,sleep(x) to try and add in a delay but alas this has not fixed the issue. it has just increased the time the program is locked up and doesn't redraw the graph until after the sort is completed? Is there a way to do what I want without switching frameworks away from JavaFX?

package sample;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Group;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import jdk.jfr.Event;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class Main extends Application {

    Button sortBtn = new Button();

    @Override
    public void start(Stage primaryStage) throws Exception {
        //Creating Bar Graph
        int size = 100;
        int maxValue = 100;
        int windowX = 1920;
        int windowY = 1020;

        int[] unsortedArray = createUnsortedArray(size, maxValue);

        NumberAxis xAxis = new NumberAxis();
        xAxis.setLabel("Position");

        NumberAxis yAxis = new NumberAxis();
        yAxis.setLabel("Value");

        XYChart.Series < Number, Number > graph = new XYChart.Series < > ();
        for (int i = 0; i < size; i++) {
            graph.getData().add(new XYChart.Data < > (i, unsortedArray[i]));
        }

        ScatterChart < Number, Number > scatterChart = new ScatterChart < > (xAxis, yAxis);
        scatterChart.setTitle("Unsorted Array");

        scatterChart.getData().addAll(graph);
        scatterChart.setPrefSize(windowX - 100, windowY);
        //End Creating Bar Graph
        primaryStage.setTitle("Sort Visualizer");
        GridPane layout = new GridPane();
        layout.getChildren().add(0, scatterChart);
        sortBtn = new Button("Sort");
        layout.getChildren().add(1, sortBtn);
        Scene scene = new Scene(layout, windowX, windowY);
        primaryStage.setScene(scene);
        primaryStage.show();

        sortBtn.setOnAction(actionEvent - > {
            try {
                insertionSort(graph, unsortedArray);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public void insertionSort(XYChart.Series < Number, Number > graph, int[] unsortedArray) throws InterruptedException {
        int lowPos = 0;
        int swappedValue = 0;
        for (int i = 0; i < unsortedArray.length; i++) {
            lowPos = i;
            for (int j = i; j < unsortedArray.length; j++) {
                if (unsortedArray[j] < unsortedArray[lowPos]) {
                    lowPos = j;
                }
            }
            //Swap lowPos value with i
            swappedValue = unsortedArray[i];
            unsortedArray[i] = unsortedArray[lowPos];
            unsortedArray[lowPos] = swappedValue;
            updateGraph(graph, unsortedArray);
            TimeUnit.MILLISECONDS.sleep(100);
        }
    }

    public void updateGraph(XYChart.Series < Number, Number > graph, int[] updatedArray) {
        graph.getData().clear();
        for (int i = 0; i < updatedArray.length; i++) {
            graph.getData().add(new XYChart.Data < > (i, updatedArray[i]));
        }
    }

    int[] createUnsortedArray(int size, int maxValue) {
        int[] unsortedArray = new int[size];
        Random randy = new Random();
        for (int i = 0; i < size; i++) {
            unsortedArray[i] = Math.abs(randy.nextInt() % maxValue);
        }
        return unsortedArray;
    }


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

Also apologies in advance for how sloppy my code is, I was just hoping this would be an easy tracerbullet to get something in working condition and then I was hoping to repay the technical debt later (bad habit I know)

The JavaFX application thread is responsible for rendering the UI and handling events. If your event handler takes a long time to run, then the UI can't be rendered until the handler completes. Consequently, you should never execute long-running tasks (including sleeping) in an event handler (or anywhere in code that is executed on the FX Application Thread).

In your example, you want to update the UI at specific time points, and the code to execute for each individual step does not take a long time to run. In this case, the best approach is an animation, such as a Timeline . It takes a little work to refactor your code so that each step of the iteration can be run individually, but the cleanest approach is to create a class that represents the sort, with a method to perform one step at a time:

private static class InsertionSort {
    private int lowPos = 0;
    private int[] data ;
    
    private int i ; // current iteration
    
    InsertionSort(int[] unsortedArray) {
        this.data = unsortedArray ;
    }
    
    private boolean isDone() {
        return i >= data.length ;
    }
    
    private int[] nextStep() {

        if (isDone()) throw new IllegalStateException("Sorting is complete");

        lowPos = i;
        for (int j = i; j < data.length; j++) {
            if(data[j] < data[lowPos]){
                lowPos = j;
            }
        }
        //Swap lowPos value with i
        int swappedValue = data[i];
        data[i] = data[lowPos];
        data[lowPos] = swappedValue;
        i++ ;
        return data ;
    }
    
}

(As an aside, since you said you want to create visualizations of multiple sorting algorithms, this should lend itself nicely to your application design. Create a Sort interface with nextStep() and isDone() methods, then create individual classes implementing that interface for the different algorithms. You can put them in a List<Sort> or ComboBox<Sort> so the user can choose one, etc.)

Now your event handler can create a timeline which performs a single step of the sort and updates the graph at specified timepoints:

public void insertionSort(XYChart.Series<Number, Number> graph, int[] unsortedArray) throws InterruptedException {

    InsertionSort sort = new InsertionSort(unsortedArray);
    Timeline timeline = new Timeline();
    timeline.getKeyFrames().add(
            new KeyFrame(Duration.millis(100), event ->  {
                updateGraph(graph, sort.nextStep());
                if (sort.isDone()) {
                    timeline.stop();
                }
            })
    );
    timeline.setCycleCount(Animation.INDEFINITE);
    timeline.play();
}

Here's the complete example refactored this way (note I also turned off the chart's default animation, which will not play nicely with the custom animation):

import java.util.Random;

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Main extends Application {

    Button sortBtn = new Button();

    @Override
    public void start(Stage primaryStage) throws Exception{
        //Creating Bar Graph
        int size = 100;
        int maxValue = 100;
        int windowX = 1920;
        int windowY = 1020;

        int[] unsortedArray = createUnsortedArray(size, maxValue);

        NumberAxis xAxis = new NumberAxis();
        xAxis.setLabel("Position");

        NumberAxis yAxis = new NumberAxis();
        yAxis.setLabel("Value");

        XYChart.Series<Number, Number> graph = new XYChart.Series<>();
        for(int i = 0; i < size; i++){
            graph.getData().add(new XYChart.Data<>(i, unsortedArray[i]));
        }

        ScatterChart<Number, Number> scatterChart = new ScatterChart<>(xAxis, yAxis);
        scatterChart.setTitle("Unsorted Array");
        
        scatterChart.setAnimated(false);

        scatterChart.getData().addAll(graph);
        scatterChart.setPrefSize(windowX-100,windowY);
        //End Creating Bar Graph
        primaryStage.setTitle("Sort Visualizer");
        GridPane layout = new GridPane();
        layout.getChildren().add(0,scatterChart);
        sortBtn = new Button("Sort");
        layout.getChildren().add(1,sortBtn);
        Scene scene = new Scene(layout, windowX,windowY);
        primaryStage.setScene(scene);
        primaryStage.show();

            sortBtn.setOnAction(actionEvent -> {
                try {
                    insertionSort(graph,unsortedArray);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });

        }

    public void insertionSort(XYChart.Series<Number, Number> graph, int[] unsortedArray) throws InterruptedException {

        InsertionSort sort = new InsertionSort(unsortedArray);
        Timeline timeline = new Timeline();
        timeline.getKeyFrames().add(
                new KeyFrame(Duration.millis(100), event ->  {
                    updateGraph(graph, sort.nextStep());
                    if (sort.isDone()) {
                        timeline.stop();
                    }
                })
        );
        timeline.setCycleCount(Animation.INDEFINITE);
        timeline.play();
    }

    public void updateGraph( XYChart.Series<Number, Number> graph, int[] updatedArray){
            graph.getData().clear();
            for(int i = 0; i < updatedArray.length; i++){
                graph.getData().add(new XYChart.Data<>(i, updatedArray[i]));
            }
        }

    int[] createUnsortedArray(int size, int maxValue){
        int[] unsortedArray = new int[size];
        Random randy = new Random();
        for(int i = 0; i < size; i++){
            unsortedArray[i] = Math.abs(randy.nextInt() % maxValue);
        }
        return unsortedArray;
    }
    
    private static class InsertionSort {
        private int lowPos = 0;
        private int[] data ;
        
        private int i ; // current iteration
        
        InsertionSort(int[] unsortedArray) {
            this.data = unsortedArray ;
        }
        
        private boolean isDone() {
            return i >= data.length ;
        }
        
        private int[] nextStep() {

            if (isDone()) throw new IllegalStateException("Sorting is complete");

            lowPos = i;
            for (int j = i; j < data.length; j++) {
                if(data[j] < data[lowPos]){
                    lowPos = j;
                }
            }
            //Swap lowPos value with i
            int swappedValue = data[i];
            data[i] = data[lowPos];
            data[lowPos] = swappedValue;
            i++ ;
            return data ;
        }
        
    }


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

Any non-trivial, non-UI-related processes should be run on a separate thread. You can create a Task and set its different properties as you require then place it in a Thread and run that thread. You ONLY should update the UI using the task.setOnRunning() , task.setOnSucceeded() , and task.setOnFailed() methods:

Task<XYChart.Series<Number, Number>> task = new Task<>() {
     @Override
     protected XYChart.Series<Number, Number> call() throws Exception {
          return aMethodThatReturnsSomeXYChartSeries();
     }
};

task.setOnSucceeded(e-> {
     XYChart.Series<Number, Number> value = task.getValue();
     updateChart(value);
});

Thread t = new Thread(task);
t.start();

Check task documentation for more details, and read about multithreading .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM