Вот одна стратегия:
- Создайте
ObservableList
для данных - Создайте
SortedList
из базового списка Зарегистрировать слушатель с 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();
}
}