如何使堆叠条形图中的负值从前一堆叠开始?

3
我希望您可以用堆叠条形图来可视化每个月的收入和支出金额,同时也应该直接看到差额。
我使用了以下示例代码:
public class Statistics extends Application {
    final CategoryAxis xAxis = new CategoryAxis();
    final NumberAxis yAxis = new NumberAxis();
    final StackedBarChart<String, Number> sbc = new StackedBarChart<>(xAxis, yAxis);
    final XYChart.Series<String, Number> incoming = new XYChart.Series<>();
    final XYChart.Series<String, Number> outgoing = new XYChart.Series<>();

    @Override
    public void start(Stage stage) {
        xAxis.setLabel("Month");
        xAxis.setCategories(FXCollections.observableArrayList(
                Arrays.asList("Jan", "Feb", "Mar")));
        yAxis.setLabel("Value");
        incoming.setName("Incoming");
        incoming.getData().add(new XYChart.Data("Jan", 25601.34));
        incoming.getData().add(new XYChart.Data("Feb2", 20148.82));
        incoming.getData().add(new XYChart.Data("Mar2", 10000));
        outgoing.setName("Outgoing");
        outgoing.getData().add(new XYChart.Data("Jan", -7401.85));
        outgoing.getData().add(new XYChart.Data("Feb2", -1941.19));
        outgoing.getData().add(new XYChart.Data("Mar2", -5263.37));
        Scene scene = new Scene(sbc, 800, 600);
        sbc.getData().addAll(incoming, outgoing);
        stage.setScene(scene);
        stage.show();
    }
}

结果如下:

结果如下:

负值显示在零以下的堆叠条形图

正如您所见,负数的 出账 值显示在零以下,而不是从正数的 进账 值中减去,这使得难以看到两者之间的差异。

我想要的是,负值的柱子从正值的柱子顶部开始,但由于它们会重叠,所以也要沿着 x 轴应用一个偏移量。就 “Jan” 系列为例,它应该类似于下面的样子:

输入图像描述

我试着使用 getNode().setTranslateX/Y(),但似乎这并不是一个好的解决方案,因为平移单位不是图表中的刻度,而是其他东西。

如何以一种优雅的方式创建一个类似第二张图片的“堆叠”条形图?


顺便说一句,我同时注意到这种图表与所谓的瀑布图非常相似,JavaFX不直接支持它,但可以通过使用JFreeChart来创建。 - sschuberth
1个回答

4
您的问题真的很酷!在这里,我想分享我的解决方案,使用 BarChart 而不是 StackedBarChart。虽然它可能有点 hacky,但它是我找到的唯一可行的方法。
我首先想到的是简单地取出条形图并更改其 Y 坐标,但不幸的是我们无法直接访问条形图。因此,在查看 XYChart 的源代码后,我发现所有图表内容都位于 Group plotContent 中,但获取器为 protected。在这种情况下,可以做的事情之一是扩展类并增加方法的范围。所以(最终),这里就有了代码:
public class Statistics extends Application {
    final CategoryAxis xAxis = new CategoryAxis();
    final NumberAxis yAxis = new NumberAxis();
    // here I use the modified version of chart
    final ModifiedBarChart<String, Number> chart = new ModifiedBarChart<>(xAxis, yAxis);
    final XYChart.Series<String, Number> incoming = new XYChart.Series<>();
    final XYChart.Series<String, Number> outgoing = new XYChart.Series<>();

    @Override
    public void start(Stage stage) {
        xAxis.setLabel("Month");
        xAxis.setCategories(FXCollections.observableArrayList(
                Arrays.asList("Jan", "Feb", "Mar")));
        yAxis.setLabel("Value");


        incoming.setName("Incoming");
        incoming.getData().add(new XYChart.Data("Jan", 25601.34));
        incoming.getData().add(new XYChart.Data("Feb", 20148.82));
        incoming.getData().add(new XYChart.Data("Mar", 10000));

        outgoing.setName("Outgoing");
        // To set the min value of yAxis you can either set lowerBound to 0 or don't use negative numbers here
        outgoing.getData().add(new XYChart.Data("Jan", -7401.85));
        outgoing.getData().add(new XYChart.Data("Feb", -1941.19));
        outgoing.getData().add(new XYChart.Data("Mar", -5263.37));

        chart.getData().addAll(incoming, outgoing);

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

        stage.setScene(scene);
        stage.show();

        // and here I iterate over all plotChildren elements
        // note, that firstly all positiveBars come, then all negative ones
        int dataSize = incoming.getData().size();
        for (int i = 0; i < dataSize; i++) {
            Node positiveBar = chart.getPlotChildren().get(i);
            // subtract 1 to make two bars y-axis aligned
            chart.getPlotChildren().get(i + dataSize).setLayoutY(positiveBar.getLayoutY() - 1);
        }

    }

    // note, that I extend BarChart here
    private static class ModifiedBarChart<X, Y> extends BarChart<X, Y> {

        public ModifiedBarChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis) {
            super(xAxis, yAxis);
        }

        public ModifiedBarChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X, Y>> data) {
            super(xAxis, yAxis, data);
        }

        public ModifiedBarChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X, Y>> data, @NamedArg("categoryGap") double categoryGap) {
            super(xAxis, yAxis, data, categoryGap);
        }


        @Override
        public ObservableList<Node> getPlotChildren() {
            return super.getPlotChildren();
        }
    }

结果: The result


只有一个小注释,似乎您需要使用 setLayoutY(positiveBar.getLayoutY() - 1) 来使 IncomingOutgoing 柱条对齐在相同的 y 轴值上。 - sschuberth
是的,现在我可以看到y坐标有一点差异了,可能是因为x轴本身的宽度。 - Enigo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接