General Antrhax General Antrhax - 1 month ago 17
Java Question

JavaFX visualizing algorithms in bar charts with animations

So I'm really stuck on this one, I need to output the algorithms that are at the bottom of my code into JavaFX that shows how the algorithm is working, i.e. bubble sort moves the smaller bar to the front of the pair etc. the problem that I'm having is that I can't figure out how to get my algorithms to display properly in JavaFX. The swap halves and random work but I didn't work that portion of the code or the upper pieces so i'm still in the dark, i'm still new to java and JavaFX is kind of weird to me at the moment

import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import static javafx.scene.chart.XYChart.*;
import javafx.geometry.Insets;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;

public class hamrickP2 extends Application {
public static void main(String[] args) {
hamrickP2.launch(args);
}

private static final int
BAR_COUNT = 14,
MAX_BAR_HEIGHT = 50;

private static final String
COLOR_ACTIVE = "-fx-bar-fill: #f64",
COLOR_INITIAL = "-fx-bar-fill: #888",
COLOR_FINALIZED = "-fx-bar-fill: #3cf";

private static final int
DELAY_MILLIS = 700;

@Override
public void start(Stage stage) {
stage.setTitle("Sorting Animations");
stage.setWidth(800);
stage.setHeight(600);

final BorderPane pane = new BorderPane();
pane.setPadding(new Insets(10));

final BarChart<String,Number> chart = new BarChart<>(new CategoryAxis(), new NumberAxis(0, MAX_BAR_HEIGHT, 0));
chart.setLegendVisible(false);
chart.getYAxis().setTickLabelsVisible(false);
chart.getYAxis().setOpacity(0);
chart.getXAxis().setTickLabelsVisible(false);
chart.getXAxis().setOpacity(0);
chart.setHorizontalGridLinesVisible(false);
chart.setVerticalGridLinesVisible(false);

bars = new ArrayList<Data<String,Number>>();
final Series<String,Number> data = new Series<>();
chart.getData().add(data);
for (int i = 0; i < BAR_COUNT; i++) {
bars.add(new Data<>(Integer.toString(i+1), 1));
data.getData().add(bars.get(i));
paint(i, COLOR_INITIAL);
}
pane.setCenter(chart);

inputs = new FlowPane();
inputs.setHgap(5);
inputs.setVgap(5);
createButton("Randomize", () -> randomizeAll());
createButton("Swap Halves", () -> swapHalves());
createButton("Reverse ", () -> reverse());
createButton("Selection Sort", () -> selectionSort());


pane.setBottom(inputs);

stage.setScene(new Scene(pane));
stage.show();

Platform.runLater(() -> randomizeAll());
}

private ArrayList<Data<String,Number>> bars;
private FlowPane inputs;

private void createButton(String label, Runnable method) {
final Button test = new Button(label);
test.setOnAction(event -> new Thread(() -> {
Platform.runLater(() -> inputs.setDisable(true));
method.run();
Platform.runLater(() -> inputs.setDisable(false));
}).start());
inputs.getChildren().add(test);
}



// CHART ACCESSORS AND MUTATORS

private void assign(int index, int value) {
bars.get(index).setYValue(value);
}

private int retrieve(int index) {
return (int) bars.get(index).getYValue();
}

// ANIMATION CONTROLS

private void paint(int index, final String style) {
Platform.runLater(() -> {
bars.get(index).getNode().setStyle(style);
});
}

private void paintAll(final String style) {
Platform.runLater(() -> {
for (int i = 0; i < BAR_COUNT; i++) paint(i, style);
});
}

private void delay() {
try {
Thread.sleep(DELAY_MILLIS);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}

// ALGORITHMS FOR BUTTONS

private void randomizeAll() {
Random random = new Random();
for (int i = 0; i < BAR_COUNT; i++) {
assign(i, random.nextInt(MAX_BAR_HEIGHT) + 1);
}
}

private void swapHalves() {
final int half = bars.size()/2;
final int offset = bars.size() % 2;
for (int i = 0; i < half; i++) {
final int j = i + half + offset;

paint(i, COLOR_ACTIVE);
paint(j, COLOR_ACTIVE);

int temp = retrieve(i);
assign(i, retrieve(j));
assign(j, temp);

delay();

paint(i, COLOR_FINALIZED);
paint(j, COLOR_FINALIZED);
}
paintAll(COLOR_INITIAL);
}// Start of algorithms

/**
* Reverse algorithm
* @param array
* @return
*/
public void reverse(){
int array [] = new int [BAR_COUNT];
for(int pos = 0; pos < array.length; pos++)
{

int generic = array[pos];
array[pos] = array[array.length - 1 - pos];
array[array.length -1 - pos] = generic;

int temp = array[array.length -1 - pos];

assign(array[pos] , retrieve(array[array.length - 1 - pos]));
assign(temp , generic);

delay();


paint(pos, COLOR_FINALIZED);
paint( temp, COLOR_FINALIZED);


}
paintAll(COLOR_INITIAL);

}



/**
* Selection Sort algorithm
* @param words
*/
public void selectionSort(){
int arr [] = new int [BAR_COUNT];

for (int i = 0; i < arr.length - 1; i++)
{
paint(i, COLOR_ACTIVE);

int index = i;
for (int j = i + 1; j < arr.length; j++)
if (arr[j] < arr[index])
index = j;

int smallerNumber = arr[index];
arr[index] = arr[i];
arr[i] = smallerNumber;

int temp = retrieve(i);
assign(i, retrieve(i));
assign(index, temp);

delay();

paint(i, COLOR_FINALIZED);
paint(index, COLOR_FINALIZED);

}
paintAll(COLOR_INITIAL);
}
/**
* Bubble Sort algorithm
* @param words
*/
public void bubbleSort(){
int array [] = new int [BAR_COUNT];
int temp;
for(int i = 0; i <array.length; i++){

for(int j = 1; j <array.length -i; j++){
if(array[j-1] > array[j]){
temp = array[j -1];
array[j-1] = array[j];
array[j] = temp;
}
}
}
}

/**
* Insertion Sort algorithm
* @param array
*/
public void insertionSort (){

int array [] = new int [BAR_COUNT];
for (int i = 1; i < array.length; i++) {
int temp = array[i];
int j;
for(j = i -1; j>= 0 && temp < array[j]; j--){
array[j +1 ] = array[j];
array[j + 1 ] =temp;
}
paintAll(COLOR_INITIAL);
}
}
}

Answer

I found some problems within your code. Consider the following things:

  1. Make bars an ObservableList and use this for your Series. The Series instance will take this list and use it internally. This way you will be able to observe the list and to modify the list later, while the chart updates itself automatically. Accessing your data is as easy as using bars as a list or extracting an array out of bars. I made two bubblesort versions. One accessing bars as a list (inplace) and one extracting an array (not in place).
  2. After adding a new Data object to bars you will be able to access the Node to change the color. There is no need to use Platform.runlater. Runlate is misused anyway, because there is no guarantee that the resource you are waiting for will be present at the later point in time. If you want to do some action on a resource which will be present at an unknown point of time, observe it if it is a observable value. Use addListener and watch for changes.
  3. DO NOT use a Thread and modify data from your GUI within it. Using runlater to disable the button is okay, but you are doing a lot of other things within your thread. In addition, the body of your runlater may be called later (O.o). So much later, that your Thread has already been finished. Runlater is an asynchronous call.
  4. Use the JavaFX animation classes if you want to modify the gui in scheduled time frames. The Timeline is maybe wat your are looking for. This tutorial introduces the concept.

I made a example from your code. I delete some stuff to make it more clearly:

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

import java.util.List;
import java.util.Random;

import static javafx.scene.chart.XYChart.Data;
import static javafx.scene.chart.XYChart.Series;

public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    private static final int
        BAR_COUNT = 14,
        MAX_BAR_HEIGHT = 50;

    private static final String
        COLOR_ACTIVE = "-fx-bar-fill: #f64",
        COLOR_INITIAL = "-fx-bar-fill: #888",
        COLOR_FINALIZED = "-fx-bar-fill: #3cf";

    private static final int
        DELAY_MILLIS = 700;


    private ObservableList<Data<String, Number>> bars;
    private BarChart<String, Number> chart;
    private FlowPane inputs;


    @Override
    public void start(Stage stage) {
        stage.setTitle("Sorting Animations");
        stage.setWidth(800);
        stage.setHeight(600);

        final BorderPane pane = new BorderPane();
        pane.setPadding(new Insets(10));

        makeChart(pane);
        makeButtons(pane);

        stage.setScene(new Scene(pane));
        stage.show();

        randomizeAll();
    }

    private void makeChart(BorderPane pane) {
        chart = new BarChart<>(new CategoryAxis(), new NumberAxis(0, MAX_BAR_HEIGHT, 0));
        chart.setLegendVisible(false);
        chart.getYAxis().setTickLabelsVisible(false);
        chart.getYAxis().setOpacity(0);
        chart.getXAxis().setTickLabelsVisible(false);
        chart.getXAxis().setOpacity(0);
        chart.setHorizontalGridLinesVisible(false);
        chart.setVerticalGridLinesVisible(false);

        bars = FXCollections.observableArrayList();
        chart.getData().add(new Series<>(bars));

        for (int i = 0; i < BAR_COUNT; i++) {
            Data<String, Number> dataObject = new Data<>(Integer.toString(i + 1), 1);
            bars.add(dataObject); // node will be present after this
            addPainting(dataObject.getNode(), COLOR_INITIAL); // do this after bars.add
        }
        pane.setCenter(chart);
    }


    private void makeButtons(BorderPane pane) {
        inputs = new FlowPane();
        inputs.setHgap(5);
        inputs.setVgap(5);
        createButton("Randomize", () -> randomizeAll());
        createButton("Bubble Sort 1", () -> bubbleSort1());
        createButton("Bubble Sort 2", () -> bubbleSort2());
        pane.setBottom(inputs);
    }

    private void addPainting(Node newNode, String colorInitial) {
        if (newNode != null) {
            newNode.setStyle(colorInitial);
        }
    }

    private void createButton(String label, Runnable method) {
        final Button test = new Button(label);
        test.setOnAction(event -> method.run());
        inputs.getChildren().add(test);
    }


    private void randomizeAll() {
        Random random = new Random();
        for (Data<String, Number> bar : bars) {
            bar.setYValue(random.nextInt(MAX_BAR_HEIGHT) + 1);
        }
    }


    /**
     * Bubble Sort algorithm
     */
    private void bubbleSort1(){
        List<Data<String, Number>> list = bars;

        double temp;        
        for(int i = 0; i < list.size(); i++){
            for(int j = 1; j < list.size() - i; j++){
                if (getValue(list, j - 1) > getValue(list, j)){
                    temp = getValue(list, j - 1);
                    list.get(j - 1).setYValue(list.get(j).getYValue());
                    list.get(j).setYValue(temp);
                }
            }
        }
    }

    private double getValue(List<Data<String, Number>> list, int index) {
        return list.get(index).getYValue().doubleValue();
    }


    /**
     * Bubble Sort algorithm
     */
    private void bubbleSort2(){
        double[] array = bars.stream().mapToDouble(data -> data.getYValue().doubleValue()).toArray();

        double temp;
        for(int i = 0; i <array.length; i++){
            for(int j = 1; j <array.length -i; j++){
                if(array[j-1] > array[j]){
                    temp = array[j -1];
                    array[j-1] = array[j];
                    array[j] = temp;
                }
            }
        }
        for (int i = 0; i < array.length; i++) {
            bars.get(i).setYValue(array[i]);
        }
    }

}

Update 1 Assigning and retrieving works as you have already done in your question, if you implemented it in the observable way.

private void assign(int index, int value) {
    bars.get(index).setYValue(value);
}

private int retrieve(int index) {
    return (int) bars.get(index).getYValue();
}

I made it the same way; e.g. see the body of the last for-loop.

To animate the chart you have to rewrite the algorithm so that it makes only one step per call instead of sorting the whole array at once. Eg. the function call to bubblesort returns after it made its first swap.

The final step would be to initialize a TimeLine (Timer) to call every t seconds the one-step-only bubblesort algorithm.

The tricky part is how to remember where the last run of the algorithm has stop and how to continue it.

Comments