Переключение нескольких положений бара javafx - PullRequest
0 голосов
/ 29 мая 2020

У меня есть барчат с 5 полосами, теперь я пытаюсь переместить каждую полосу слева направо на основе высокого значения (больше похоже на гонку на гистограмме от flouri sh @ https://app.flourish.studio ) не то же самое, но идея та же.

Чтобы сравнить маленькие числа с большими числами и большие числа с маленькими числами, я использую случайные целые числа.

Например, если barE больше затем все столбцы и меньше, чем barA, он должен переместиться на номер 2 и заменить столбец B. Я использую «оператор if», чтобы попытаться выполнить sh это. Проблема в том, что происходит только один переход - первый. Когда случайные числа меняются каждые 3 секунды, правильный переход не происходит. Кто-нибудь знает, как я могу исправить эту проблему?


import java.util.Calendar;

import java.util.TimeZone;

import java.util.concurrent.Executors;

import java.util.concurrent.ScheduledExecutorService;

import java.util.concurrent.TimeUnit;

import javafx.application.Application;

import javafx.application.Platform;

import javafx.beans.property.IntegerProperty;

import javafx.beans.property.SimpleIntegerProperty;

import javafx.beans.value.ChangeListener;

import javafx.geometry.Bounds;

import javafx.scene.Group;

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.chart.XYChart;

import javafx.scene.chart.XYChart.Data;

import javafx.scene.text.Text;

import javafx.stage.Stage;



public class App extends Application {

    private ScheduledExecutorService scheduledExecutorService;

    final static String austria = "Austria",  brazil = "Brazil",  france = "France", england = "England", belgium = "Belgium";

    private IntegerProperty secondA,  secondB , secondC, secondD, secondE;

    private Text secondAText, secondBText , secondCText, secondDText, secondEText;



    @Override

    public void start(Stage primaryStage) throws Exception {

        primaryStage.setTitle("Realtime Bar Chart Demo");



        //defining the axes

        final CategoryAxis xAxis = new CategoryAxis();

        final NumberAxis yAxis = new NumberAxis();

        xAxis.setAnimated(false);

        yAxis.setAnimated(false);



        //creating the bar chart with two axis

        final BarChart<String,Number> bc =  new BarChart<>(xAxis,yAxis);

        bc.setAnimated(false);

        bc.setTitle("Country Summary");

        xAxis.setLabel("Country");

        yAxis.setLabel("Value");



        //defining a series to display data

        XYChart.Series<String, Number> seriesA = new XYChart.Series<>();

        Data<String, Number> dataA = new XYChart.Data<>(austria,0);

        seriesA.getData().add(dataA);

        seriesA.setName("Austra");

        secondA = new SimpleIntegerProperty(0);

        secondAText = new Text("");

        secondA.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {

            dataA.setYValue(newValue);

            secondAText.setText(String.valueOf(newValue));

        });

        XYChart.Series<String, Number> seriesB = new XYChart.Series<>();

        Data<String, Number> dataB = new XYChart.Data<>(brazil,0);

        seriesB.getData().add(dataB);

        seriesB.setName("Brazil");

        secondB =  new SimpleIntegerProperty(0);

        secondB.bind(secondA.add(0));

        secondBText = new Text("");

        secondB.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {

            dataB.setYValue(newValue);

            secondBText.setText(String.valueOf(newValue));

        });

        XYChart.Series<String, Number> seriesC = new XYChart.Series<>();

        Data<String, Number> dataC = new XYChart.Data<>(france,0);

        seriesC.getData().add(dataC);

        seriesC.setName("France");

        secondC =  new SimpleIntegerProperty(0);

        secondC.bind(secondA.add(0));

        secondCText = new Text("");

        secondC.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {

            dataC.setYValue(newValue);

            secondCText.setText(String.valueOf(newValue));

        });



XYChart.Series<String, Number> seriesD = new XYChart.Series<>();

        Data<String, Number> dataD = new XYChart.Data<>(england,0);

        seriesD.getData().add(dataD);

        seriesD.setName("England");

        secondD =  new SimpleIntegerProperty(0);

        secondD.bind(secondA.add(0));

        secondDText = new Text("");

        secondD.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {

            dataD.setYValue(newValue);

            secondDText.setText(String.valueOf(newValue));

        });



XYChart.Series<String, Number> seriesE = new XYChart.Series<>();

        Data<String, Number> dataE = new XYChart.Data<>(belgium,0);

        seriesE.getData().add(dataE);

        seriesE.setName("Belgium");

        secondE =  new SimpleIntegerProperty(0);

        secondE.bind(secondA.add(0));

        secondEText = new Text("");

        secondE.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {

            dataE.setYValue(newValue);

            secondEText.setText(String.valueOf(newValue));

        });



        // add series to chart

        bc.getData().add(seriesA);

        bc.getData().add(seriesB);

        bc.getData().add(seriesC);

        bc.getData().add(seriesD);

        bc.getData().add(seriesE);



        displayLabelForData(dataA, secondAText);

        displayLabelForData(dataB, secondBText);

        displayLabelForData(dataC, secondCText);

        displayLabelForData(dataD, secondDText);

        displayLabelForData(dataE, secondEText);

        // setup scene

        Scene scene = new Scene(bc, 800, 600);

        primaryStage.setScene(scene);

        // show the stage

        primaryStage.show();



        // setup a scheduled executor to periodically put data into the chart

        scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();



        // input data onto graph per second        scheduledExecutorService.scheduleAtFixedRate(() -> {

            Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));

double posA = dataA.getNode().localToScene(dataA.getNode().getBoundsInLocal()).getMinX();

double posB = dataB.getNode().localToScene(dataB.getNode().getBoundsInLocal()).getMinX();

double posC = dataC.getNode().localToScene(dataC.getNode().getBoundsInLocal()).getMinX();

double posD = dataD.getNode().localToScene(dataD.getNode().getBoundsInLocal()).getMinX();

double posE = dataE.getNode().localToScene(dataE.getNode().getBoundsInLocal()).getMinX();

TranslateTransition ttA = new TranslateTransition(Duration.millis(2000), dataA.getNode());

TranslateTransition ttB = new TranslateTransition(Duration.millis(2000), dataB.getNode());

TranslateTransition ttC = new TranslateTransition(Duration.millis(2000), dataC.getNode());

TranslateTransition ttD = new TranslateTransition(Duration.millis(2000), dataD.getNode());

TranslateTransition ttE = new TranslateTransition(Duration.millis(2000), dataE.getNode());

//Genarate random numbers

Integer randomB = ThreadLocalRandom.current().nextInt(60);

Integer randomC = ThreadLocalRandom.current().nextInt(60);

Integer randomD = ThreadLocalRandom.current().nextInt(60);

Integer randomE = ThreadLocalRandom.current().nextInt(60);

int intSecondB = secondB.bind(secondA.add(randomB));

int intSecondC = secondC.bind(secondA.add(randomC));

int intSecondD = secondD.bind(secondA.add(randomD));

int intSecondE = secondE.bind(secondA.add(randomE));

Я не уверен, что проблема связана с приведенными ниже операторами if или самим кодом. Чтобы код не был более длинным, я включил только операторы if, которые сравнивают только seriesB (barB) с другими барами.


//using if statement to swich each bar based on value

if (intSecondB >= intSecondA && intSecondB >= intSecondB && intSecondB >= intSecondC && intSecondB >= intSecondD && intSecondB >= intSecondE) {

double diffBA = posB - posA;

ttA.setByX(diffBA);

ttB.setByX(-diffBA);



ttA.setCycleCount(1);

    ttA.setAutoReverse(true);

    ttA.play();

ttB.setCycleCount(1);

    ttB.setAutoReverse(true);

    ttB.play();

}

if (intSecondB < intSecondA && intSecondB >= intSecondC && intSecondB >= intSecondD && intSecondB >= intSecondE) {

System.out.println("keep seriesB(barB) at its position");

}

if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB >= intSecondD && intSecondB >= intSecondE) {

double diffCB = posC - posB;

ttB.setByX(diffCB);

ttC.setByX(-diffCB);



ttB.setCycleCount(1);

    ttB.setAutoReverse(true);

    ttB.play();

ttC.setCycleCount(1);

    ttC.setAutoReverse(true);

    ttC.play();

}

if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB >= intSecondE) {

double diffDB = posD - posB;

ttB.setByX(diffDB);

ttD.setByX(-diffDB);



ttB.setCycleCount(1);

    ttB.setAutoReverse(true);

    ttB.play();

ttD.setCycleCount(1);

    ttD.setAutoReverse(true);

    ttD.play();

}

if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {

double diffEB = posE - posB;

ttB.setByX(diffEB);

ttE.setByX(-diffEB);



ttB.setCycleCount(1);

    ttB.setAutoReverse(true);

    ttB.play();

ttE.setCycleCount(1);

    ttE.setAutoReverse(true);

    ttE.play();

}

if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {

double diffFB = posF - posB;

ttB.setByX(diffFB);

ttF.setByX(-diffFB);



ttB.setCycleCount(1);

    ttB.setAutoReverse(true);

    ttB.play();

ttF.setCycleCount(1);

    ttF.setAutoReverse(true);

    ttF.play();

}

if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {

double diffGB = posG - posB;

ttB.setByX(diffGB);

ttG.setByX(-diffGB);



ttB.setCycleCount(1);

    ttB.setAutoReverse(true);

    ttB.play();

ttG.setCycleCount(1);

    ttG.setAutoReverse(true);

    ttG.play();

}

if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {

double diffHB = posH - posB;

ttB.setByX(diffHB);

ttH.setByX(-diffHB);



ttB.setCycleCount(1);

    ttB.setAutoReverse(true);

    ttB.play();

ttH.setCycleCount(1);

    ttH.setAutoReverse(true);

    ttH.play();

}

if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {

double diffIB = posI - posB;

ttB.setByX(diffIB);

ttI.setByX(-diffIB);



ttB.setCycleCount(1);

    ttB.setAutoReverse(true);

    ttB.play();

ttI.setCycleCount(1);

    ttI.setAutoReverse(true);

    ttI.play();

}

if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {

double diffJB = posJ - posB;

ttB.setByX(diffJB);

ttJ.setByX(-diffJB);



ttB.setCycleCount(1);

    ttB.setAutoReverse(true);

    ttB.play();

ttJ.setCycleCount(1);

    ttJ.setAutoReverse(true);

    ttJ.play();

}

            // Update the chart

            Platform.runLater(() -> {

                secondA.set( cal.get(Calendar.SECOND));

            });

        }, 0, 3, TimeUnit.SECONDS);

    }

    @Override

    public void stop() throws Exception {

        super.stop();

        scheduledExecutorService.shutdownNow();

    }



    private void displayLabelForData(XYChart.Data<String, Number> data, Text text) {

        final Node node = data.getNode();

        ((Group) node.getParent()).getChildren().add(text);

      node.boundsInParentProperty().addListener((ChangeListener<Bounds>) (ov, oldBounds, bounds) -> {

            text.setLayoutX(

                    Math.round( bounds.getMinX() + bounds.getWidth() / 2 - text.prefWidth(-1) / 2));

            text.setLayoutY(Math.round( bounds.getMinY() - text.prefHeight(-1) * 0.5));

        });

    }

public static void main(String[] args) {

        launch(args);

    }

}

Любая помощь будет принята с благодарностью!

1 Ответ

2 голосов
/ 29 мая 2020

Вот одна стратегия:

  1. Создайте ObservableList для данных
  2. Создайте SortedList из базового списка
  3. Зарегистрировать слушатель с SortedList, и при изменении данных создать анимацию:

    a. Найдите для каждого бара его текущее положение и положение бара в индексе, соответствующее его новому порядку

    b. Используйте эти позиции для анимации свойства translateX полосы

    c. Анимируйте свойство yValue объекта XYChart.Data в той же анимации

    d. В конце анимации сбросьте данные диаграммы на новые отсортированные данные.

Здесь есть пара небольших ошибок: вам нужно отключить autoRanging на CategoryAxis (иначе он проигнорирует изменения порядка столбцов) и сбросит категории, используя новый порядок при обновлении данных.

Вот пример. Я создал класс только для хранения данных без какого-либо API диаграммы:

public static class CountryValue {
    private final String country ;
    private final double value ;
    public CountryValue(String country, double value) {
        super();
        this.country = country;
        this.value = value;
    }
    public String getCountry() {
        return country;
    }
    public double getValue() {
        return value;
    }

}

и простую модель данных для хранения их списка:

public static class Model {
    private final ObservableList<CountryValue> values ;

    public Model(CountryValue... countryValues) {
        values = FXCollections.observableArrayList(countryValues) ;
    }

    public ObservableList<CountryValue> getValues() {
        return values ;
    }

}

Затем ключевые части выглядят например:

    Model model = new Model() ;
    SortedList<CountryValue> sortedData = new SortedList<>(
            model.getValues(), 
            Comparator.comparingDouble(CountryValue::getValue).reversed());

    ObservableList<XYChart.Data<String, Number>> chartData = FXCollections.observableArrayList();

    CategoryAxis countryAxis = new CategoryAxis();
    countryAxis.setAutoRanging(false);
    populateChartData(sortedData, chartData, countryAxis);

    sortedData.addListener((Change<? extends CountryValue> c) -> {

        Timeline timeline = new Timeline() ;

        for (int newIndex = 0 ; newIndex < sortedData.size() ; newIndex++) {

            CountryValue cv = sortedData.get(newIndex);
            int currentIndex = indexByCountry(cv.getCountry(), chartData);
            Data<String, Number> data = chartData.get(currentIndex);
            double currentX = data.getNode().getBoundsInParent().getCenterX();
            double targetX = chartData.get(newIndex).getNode().getBoundsInParent().getCenterX();
            DoubleProperty translateXProperty = data.getNode().translateXProperty();
            KeyValue kvx1 = new KeyValue(translateXProperty, 0);
            KeyValue kvx2 = new KeyValue(translateXProperty, targetX - currentX);
            ObjectProperty<Number> yValueProperty = data.YValueProperty();
            KeyValue kvy1 = new KeyValue(yValueProperty, data.getYValue());
            KeyValue kvy2 = new KeyValue(yValueProperty, cv.getValue());
            timeline.getKeyFrames().addAll(
                    new KeyFrame(Duration.ZERO, kvx1),
                    new KeyFrame(Duration.ZERO, kvy1),
                    new KeyFrame(animationDuration, kvx2),
                    new KeyFrame(animationDuration, kvy2)
            );
        }

        timeline.setOnFinished(e -> populateChartData(sortedData, chartData, countryAxis));

        timeline.play();
    });

Метод утилиты populateChartData() обновляет и ось категорий, и данные:

private void populateChartData(ObservableList<CountryValue> source, 
        ObservableList<XYChart.Data<String, Number>> chartData,
        CategoryAxis countryAxis) {

    countryAxis.getCategories().setAll(
        source.stream()
            .map(CountryValue::getCountry)
            .collect(Collectors.toList())
    );

    chartData.setAll(
        source.stream()
            .map(cv -> new XYChart.Data<String, Number>(cv.getCountry(), cv.getValue()))
            .collect(Collectors.toList())
    );
}

Вот полный пример. Анимация немного «рывкая»; Думаю, потому, что шкала оси Y меняется непредсказуемо. Вы можете управлять этим самостоятельно, отключив автоматический выбор диапазона по оси Y, вычислив максимальное значение Y из новых данных и анимировав диапазон по оси Y, а также столбцы. Также обратите внимание, что важно, чтобы данные не обновлялись во время работы анимации (иначе вы получите несколько анимаций, запущенных одновременно). Здесь это просто управляется синхронизацией, но более надежное решение будет проверять это и либо ограничивать обновления, либо просто завершать текущую анимацию перед запуском новой.

import java.util.Comparator;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
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.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Duration;

/**
 * JavaFX App
 */
public class FlourishChart extends Application {

    private final Duration animationDuration = Duration.millis(250);

    @Override
    public void start(Stage stage) {

        Model model = new Model() ;
        Simulator simulator = new Simulator(model);

        SortedList<CountryValue> sortedData = new SortedList<>(
                model.getValues(), 
                Comparator.comparingDouble(CountryValue::getValue).reversed());

        ObservableList<XYChart.Data<String, Number>> chartData = FXCollections.observableArrayList();

        CategoryAxis countryAxis = new CategoryAxis();
        countryAxis.setAutoRanging(false);
        populateChartData(sortedData, chartData, countryAxis);


        BarChart<String, Number> chart = new BarChart<>(countryAxis, new NumberAxis());
        // turn off default animation:
        chart.setAnimated(false);
        Series<String, Number> series = new Series<>(chartData);
        chart.getData().add(series);

        // when sorted data change, animate bar chart nodes
        // at end of animation, update chart data with new data
        sortedData.addListener((Change<? extends CountryValue> c) -> {

            Timeline timeline = new Timeline() ;

            for (int newIndex = 0 ; newIndex < sortedData.size() ; newIndex++) {

                CountryValue cv = sortedData.get(newIndex);
                int currentIndex = indexByCountry(cv.getCountry(), chartData);
                Data<String, Number> data = chartData.get(currentIndex);
                double currentX = data.getNode().getBoundsInParent().getCenterX();
                double targetX = chartData.get(newIndex).getNode().getBoundsInParent().getCenterX();
                DoubleProperty translateXProperty = data.getNode().translateXProperty();
                KeyValue kvx1 = new KeyValue(translateXProperty, 0);
                KeyValue kvx2 = new KeyValue(translateXProperty, targetX - currentX);
                ObjectProperty<Number> yValueProperty = data.YValueProperty();
                KeyValue kvy1 = new KeyValue(yValueProperty, data.getYValue());
                KeyValue kvy2 = new KeyValue(yValueProperty, cv.getValue());
                timeline.getKeyFrames().addAll(
                        new KeyFrame(Duration.ZERO, kvx1),
                        new KeyFrame(Duration.ZERO, kvy1),
                        new KeyFrame(animationDuration, kvx2),
                        new KeyFrame(animationDuration, kvy2)
                );
            }

            timeline.setOnFinished(e -> populateChartData(sortedData, chartData, countryAxis));

            timeline.play();
        });


        BorderPane root = new BorderPane(chart);
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();

        new Thread(simulator).start();
    }



    private int indexByCountry(String country, ObservableList<Data<String, Number>> chartData) {
        for (int index = 0 ; index < chartData.size(); index++) {
            if (chartData.get(index).getXValue().equals(country))
                return index ;
        }
        return -1 ;
    }

    private void populateChartData(ObservableList<CountryValue> source, 
            ObservableList<XYChart.Data<String, Number>> chartData,
            CategoryAxis countryAxis) {

        countryAxis.getCategories().setAll(
            source.stream()
                .map(CountryValue::getCountry)
                .collect(Collectors.toList())
        );

        chartData.setAll(
            source.stream()
                .map(cv -> new XYChart.Data<String, Number>(cv.getCountry(), cv.getValue()))
                .collect(Collectors.toList())
        );
    }

    public static class Model {
        private final ObservableList<CountryValue> values ;

        public Model(CountryValue... countryValues) {
            values = FXCollections.observableArrayList(countryValues) ;
        }

        public ObservableList<CountryValue> getValues() {
            return values ;
        }

    }


    // replace with record when they are standard in Java:
    public static class CountryValue {
        private final String country ;
        private final double value ;
        public CountryValue(String country, double value) {
            super();
            this.country = country;
            this.value = value;
        }
        public String getCountry() {
            return country;
        }
        public double getValue() {
            return value;
        }

    }

    // Not really relevant to problem; just simulates changing data in model
    public class Simulator implements Runnable {

        private final Model model ;
        private final Random rng = new Random();

        public Simulator(Model model) {
            this.model = model ;
            createData();
        }

        @Override
        public void run() {
            for (int i = 0 ; i < 10 ; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                Platform.runLater(this::createData);
            }
        }

        private void createData() {
            model.getValues().setAll(
                Stream.of("Austria", "Brazil", "France", "England", "Belgium")
                    .map(country -> new CountryValue(country, 50 * rng.nextDouble() + 50))
                    .collect(Collectors.toList())
            );
        }
    }

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

}
...