启动程序后重新加载并显示空白 JavaFX BarChart

Reload and display blank JavaFX BarChart after launching the program

我正在开发一个 JavaFX 程序,它可以将不同的杂货商品显示为条形图。我的代码结构使用初始加载 class、GroceryGrapher.java、控制器、GroceryGrapherController.java 和.fxml 文件,GroceryGrapher.fxml

我对 JavaFX 比较陌生,我尝试过不同的方法来创建和显示我的 BarChart。在大多数情况下,我已经解决了问题并且可以加载图表及其数据和组件,但我的问题是图表在程序启动后不会显示/更新。 BarChart 是我的 FXML 文件中 AnchorPane 的一部分(全部都在 StackPane 下),我试图在初始化后更新它,因为它开始时是空白的,但它仍然存在空白。

BarChartAnchorPane 的一部分,后者还有一个 MenuBar。我的目标是能够通过 MenuBar 更新 BarChart 的不同部分。当我加载程序时,加载 MenuBar 和空的 BarChart

这就是我初始化 BarChart 及其数据的方式:

public class GroceryGrapherController implements Initializable {

    @FXML
    private AnchorPane anchorPane = new AnchorPane();

    @FXML
    private NumberAxis xAxis = new NumberAxis();

    @FXML
    private CategoryAxis yAxis = new CategoryAxis();

    @FXML
    private BarChart<Number, String> groceryBarChart;

    @FXML
    //private Stage stage;
    Parent root;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        groceryBarChart = new BarChart<Number, String>(xAxis, yAxis);
        groceryBarChart.setTitle("Horizontal Grocery List");
        xAxis.setLabel("Number");
        xAxis.setTickLabelRotation(90);
        yAxis.setLabel("Grocery Type");

        // Populate BarChart
        XYChart.Series<Number, String> series1 = new XYChart.Series<>();
        series1.setName("Chocolates");       
        series1.getData().add(new XYChart.Data<Number, String>(25601.34, "Reese"));
        series1.getData().add(new XYChart.Data<Number, String>(20148.82, "Cadbury"));
        series1.getData().add(new XYChart.Data<Number, String>(10000, "Mars"));
        series1.getData().add(new XYChart.Data<Number, String>(35407.15, "Snickers"));
        series1.getData().add(new XYChart.Data<Number, String>(12000, "Lindt"));      

        XYChart.Series<Number, String> series2 = new XYChart.Series<>();
        series2.setName("Vegetables");
        series2.getData().add(new XYChart.Data<Number, String>(57401.85, "Cabbage"));
        series2.getData().add(new XYChart.Data<Number, String>(41941.19, "Lettuce"));
        series2.getData().add(new XYChart.Data<Number, String>(45263.37, "Tomato"));
        series2.getData().add(new XYChart.Data<Number, String>(117320.16, "Eggplant"));
        series2.getData().add(new XYChart.Data<Number, String>(14845.27, "Beetroot"));  

        XYChart.Series<Number, String> series3 = new XYChart.Series<>();
        series3.setName("Drinks");
        series3.getData().add(new XYChart.Data<Number, String>(45000.65, "Water"));
        series3.getData().add(new XYChart.Data<Number, String>(44835.76, "Coke"));
        series3.getData().add(new XYChart.Data<Number, String>(18722.18, "Sprite"));
        series3.getData().add(new XYChart.Data<Number, String>(17557.31, "Mountain Dew"));
        series3.getData().add(new XYChart.Data<Number, String>(92633.68, "Beer"));  

        // Need to do this because of a bug with CategoryAxis... manually set categories
        yAxis.setCategories(FXCollections.observableArrayList("Reese", "Cadbury", "Mars", "Snickers", "Lindt", "Cabbage", "Lettuce",
                "Tomato", "Eggplant", "Beetroot", "Water", "Coke", "Sprite", "Mountain Dew", "Beer"));
        groceryBarChart.getData().addAll(series1, series2, series3);
    }

现在,我有一个项目:

在我的 MenuBar 中加载 BarChart(因为它最初显示为空),它使用我创建的方法 loadGraph。我尝试了不同的方法来让我更新的 BarChart 出现,但没有取得重大成功。我最初的想法是在loadGraph方法中使用setScene将场景设置为BarChart,如图this example.

Stage stage = (Stage) anchorPane.getScene().getWindow();
stage.setScene(new Scene(groceryBarChart, 800, 600));

虽然这确实有效,但它自然地将场景替换为仅由 BarChart 组成,删除了我的 MenuBar 和以前的 GUI:

我看到 也有类似的问题(我认为),但我不确定如何将其应用到我的程序中。我得到了这个代码:

try {
    FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("GroceryGrapher.fxml"));
    root = (Parent) fxmlLoader.load();
    GroceryGrapherController ggController = fxmlLoader.getController();
} catch (Exception e) {
      e.printStackTrace();
}

我将初始化从 initialize 方法复制到一个新方法并尝试使用 ggController 调用它,但它没有改变任何东西。我还尝试创建一个新方法,updateData:

@FXML
private void updateData() {
    Platform.runLater(() -> {
        groceryBarChart.setTitle("Horizontal Grocery List");
    });
}

我尝试在 loadGraph 方法中使用 ggController 调用它,但我的 BarChart 仍然没有改变。

我知道我做错了什么,但是我对BarChart class 和JavaFX 不是太熟悉,所以我想我会来这里求教帮助。我需要更新我的 GUI 中现有的 BarChart,这样我的数据就可以在不替换整个场景的情况下加载,以便 MenuBar 在能够执行功能的场景中保持可见。我将如何改变 AnchorPaneBarChart 组件部分,根据需要更新数据,而不是像以前那样完全替换场景?

非常感谢,如果这个问题不好或者我的编码习惯有误,我深表歉意。非常感谢所有反馈!

编辑 为了以防万一,这里是我的条形图 FXML 代码。

<BarChart fx:id="groceryBarChart" layoutY="31.0" prefHeight="566.0" prefWidth="802.0">
          <xAxis>
              <CategoryAxis fx:id="yAxis" side="BOTTOM" />
          </xAxis>
          <yAxis>
              <NumberAxis side="LEFT" fx:id="xAxis" />
          </yAxis>
</BarChart>

大部分问题似乎都解决了,所以我只评论这部分:

Ohh, I see. I increased the height and it seems to be thicker, thank you for your comment, that's very interesting behavior from the library. So I shouldn't use the approach I'm using?

好吧,这取决于。没有其他方法可以将每个数据系列视为 BarChart 中的单独类别列表(至少,我不知道),所以如果你想要类别组,每个组都有自己的颜色,你'您必须使用与示例中相同的方法。

备选方案是编写您自己的条形图 layoutPlotChildren() 方法实现,每个类别只显示一个条形(如果没有两个数据系列包含相同类别,则唯一一个),或者找到另一个具有这些功能的图表库。当然,您始终可以选择以不同的格式显示您的数据(每个月一个数据系列)。

如果您打算使用原来的方法,则必须调整条的垂直位置(再次居中),并且还必须解决条的高度问题:

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.chart.*;

import java.net.URL;
import java.util.ResourceBundle;


public class GroceryGrapherController implements Initializable {
    @FXML
    private NumberAxis xAxis;
    @FXML
    private CategoryAxis yAxis;
    @FXML
    private BarChart<Number, String> groceryBarChart;

    //the height of one bar in the chart (if it can fit category area); you can pick another value
    private final double barHeight = 10;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        groceryBarChart.setTitle("Horizontal Grocery List");
        //there's only one bar per category, so remove the gap (don't edit this, it's used in calculations)
        groceryBarChart.setBarGap(0);
        //gap between categories; you can pick another value
        groceryBarChart.setCategoryGap(5);
        //there're some bugs with the chart layout not updating when this is enabled, so disable it
        groceryBarChart.setAnimated(false);

        xAxis.setLabel("Number");
        xAxis.setTickLabelRotation(90);

        yAxis.setLabel("Grocery Type");
        //if category height changes (ex. when you resize the chart), bar positions and height need to be recalculated
        yAxis.categorySpacingProperty().addListener((observable, oldValue, newValue) ->
                recalculateBarPositionsAndHeight()
        );

        //your original data

        // Populate BarChart
        final XYChart.Series<Number, String> series1 = new XYChart.Series<>();
        series1.setName("Chocolates");
        series1.getData().addAll(
                new XYChart.Data<>(25601.34, "Reese"),
                new XYChart.Data<>(20148.82, "Cadbury"),
                new XYChart.Data<>(10000, "Mars"),
                new XYChart.Data<>(35407.15, "Snickers"),
                new XYChart.Data<>(12000, "Lindt")
        );

        final XYChart.Series<Number, String> series2 = new XYChart.Series<>();
        series2.setName("Vegetables");
        series2.getData().addAll(
                new XYChart.Data<>(57401.85, "Cabbage"),
                new XYChart.Data<>(41941.19, "Lettuce"),
                new XYChart.Data<>(45263.37, "Tomato"),
                new XYChart.Data<>(117320.16, "Eggplant"),
                new XYChart.Data<>(14845.27, "Beetroot")
        );

        final XYChart.Series<Number, String> series3 = new XYChart.Series<>();
        series3.setName("Drinks");
        series3.getData().addAll(
                new XYChart.Data<>(45000.65, "Water"),
                new XYChart.Data<>(44835.76, "Coke"),
                new XYChart.Data<>(18722.18, "Sprite"),
                new XYChart.Data<>(17557.31, "Mountain Dew"),
                new XYChart.Data<>(92633.68, "Beer")
        );

        groceryBarChart.getData().addAll(series1, series2, series3);
    }


    private void recalculateBarPositionsAndHeight() {
        final int seriesCount = groceryBarChart.getData().size();
        final double categoryHeight = yAxis.getCategorySpacing() - groceryBarChart.getCategoryGap();
        final double originalBarHeight = categoryHeight / seriesCount;
        final double barTranslateY = originalBarHeight * (seriesCount - 1) / 2;

        //find the (bar) node associated with each series data and adjust its size and position
        groceryBarChart.getData().forEach(numberStringSeries ->
                numberStringSeries.getData().forEach(numberStringData -> {
                    Node node = numberStringData.getNode();

                    //calculate the vertical scaling of the node, so that its height matches "barHeight"
                    //or maximum height available if it's too big
                    double scaleY = Math.min(barHeight, categoryHeight) / originalBarHeight;
                    node.setScaleY(scaleY);

                    //translate the node vertically to center it around the category label
                    node.setTranslateY(barTranslateY);
                })
        );
    }
}

我选择了固定条形高度,但您不需要(在这种情况下,您必须编辑 recalculateBarPositionsAndHeight 方法才能使用 originalBarHeight)。