3

I have two XYCharts that I want to display vertically aligned.

The two graphs share the same x axis, but they use different data sets, which values are not of the same order of magnitude. This makes the y axis labels quite different in size. In the end, the two x axes are not aligned anymore.

My objective is to align those x axes.

One proposed solution offers a workaround by rotating the y axis labels, making the label width identical. While this works (and I thank the contributor for his answer), this is not entirely satisfactory: as soon as I have legends that have different sizes, I'm facing the same problem (see this question in R, where several satisfying solutions where found).

I really would like to keep the label orientation unchanged and "play" with the chart components size.

For the sake of reproducibility, here is an example built from Oracle's tutorial:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class Test extends Application {

    @Override
    public void start(Stage stage) {
        stage.setTitle("Line Chart Sample");
        final CategoryAxis xAxis1 = new CategoryAxis();
        final NumberAxis yAxis1 = new NumberAxis();
        xAxis1.setLabel("Month");
        final LineChart<String, Number> lineChart1 =
                new LineChart<>(xAxis1, yAxis1);

        final CategoryAxis xAxis2 = new CategoryAxis();
        xAxis2.setLabel("Month");
        final NumberAxis yAxis2 = new NumberAxis();
        final LineChart<String, Number> lineChart2 =
                new LineChart<>(xAxis2, yAxis2);

        lineChart1.setTitle("Charts");
        lineChart1.setLegendVisible(false);
        lineChart2.setLegendVisible(false);

        XYChart.Series series1 = new XYChart.Series();
        series1.setName("Portfolio 1");

        series1.getData().add(new XYChart.Data("Jan", 23));
        series1.getData().add(new XYChart.Data("Feb", 14));
        series1.getData().add(new XYChart.Data("Mar", 15));
        series1.getData().add(new XYChart.Data("Apr", 24));
        series1.getData().add(new XYChart.Data("May", 34));
        series1.getData().add(new XYChart.Data("Jun", 36));
        series1.getData().add(new XYChart.Data("Jul", 22));
        series1.getData().add(new XYChart.Data("Aug", 45));
        series1.getData().add(new XYChart.Data("Sep", 43));
        series1.getData().add(new XYChart.Data("Oct", 17));
        series1.getData().add(new XYChart.Data("Nov", 29));
        series1.getData().add(new XYChart.Data("Dec", 25));

        XYChart.Series series2 = new XYChart.Series();
        series2.setName("Portfolio 2");
        series2.getData().add(new XYChart.Data("Jan", 330000));
        series2.getData().add(new XYChart.Data("Feb", 340000));
        series2.getData().add(new XYChart.Data("Mar", 250000));
        series2.getData().add(new XYChart.Data("Apr", 440000));
        series2.getData().add(new XYChart.Data("May", 390000));
        series2.getData().add(new XYChart.Data("Jun", 160000));
        series2.getData().add(new XYChart.Data("Jul", 550000));
        series2.getData().add(new XYChart.Data("Aug", 540000));
        series2.getData().add(new XYChart.Data("Sep", 480000));
        series2.getData().add(new XYChart.Data("Oct", 270000));
        series2.getData().add(new XYChart.Data("Nov", 370000));
        series2.getData().add(new XYChart.Data("Dec", 290000));

        lineChart1.getData().addAll(series1);
        lineChart2.getData().addAll(series2);

        VBox vBox = new VBox();
        vBox.addAll(lineChart1, lineChart2);

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

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


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

}

Here is the result:

enter image description here

Is it possible to control the size of the chart component to make the two x axis aligned?

I have tried to change the y axis prefered and minimum width but it does not seem to work.

I'm using Java 8 at the moment. Migrating to Java 9 is possible if necessary.

Edit: As suggested in the answers, I have reduced the scope of this question to the issue of aligning the x axes. The sizing issue is asked here.

kleopatra
  • 51,061
  • 28
  • 99
  • 211
Ben
  • 6,321
  • 9
  • 40
  • 76
  • hmm ... I would try a custom layout that is aware of its children being charts and lays them out such that a) the xAxis are aligned and b) the leading "padding" is wide enough for the widest yAxis (including the labels) – kleopatra Mar 07 '18 at 12:48
  • Any indication where to start would be greatly appreciated – Ben Mar 08 '18 at 21:28

2 Answers2

3

Here's a quick shot on a specialized Layout that

  • only cares about children that are of type XYChart
  • lays them out vertically with a configurable spacing between them
  • sizes the yAxis width to its prefWidth, note that we need to pass-in the actual height of the axis (vs. the typical -1) ... took me a while ..
  • aligns each horizontally such that the xAxis are aligned and the yAxis has enough space for its labels

It's not production quality, just for starters on how to do it :)

/**
 * Lays out XYCharts vertically such that their x-axis are aligned and
 * there's enough space to fully show the labels of all y-axis.
 *   
 * @author Jeanette Winzenburg, Berlin
 */
public class VChartBox extends Pane {

    protected void layoutChildren() {
        Insets insets = getInsets();
        double width = getWidth();
        double height = getHeight();
        // not entirely certain when to apply all the snaps, this is 
        // simply copied from vbox 
        double top = snapSpaceY(insets.getTop());
        double left = snapSpaceX(insets.getLeft());
        double bottom = snapSpaceY(insets.getBottom());
        double right = snapSpaceX(insets.getRight());
        double space = snapSpaceY(getSpacing());

        double availableWidth = snapSpaceX(width - left - right);
        List<XYChart> charts = getCharts();
        if (charts.isEmpty()) return;
        double heightPerChart = height / charts.size() - space;
        OptionalDouble maxYAxisWidth = charts.stream()
                .filter(chart -> chart.getYAxis() != null)
                .mapToDouble(chart -> chart.getYAxis().prefWidth(heightPerChart))
                .max();
        double maxYWidth = maxYAxisWidth.orElse(0);
        double remainingWidth = availableWidth - maxYWidth;
        for (XYChart c : charts) {
            Axis axis = c.getYAxis();
            double axisWidth = axis != null ? axis.prefWidth(heightPerChart) : 0;
            double axisOffset = maxYWidth - axisWidth;
            double xOffset = axisOffset + left;
            double chartWidth = remainingWidth + axisWidth;
            c.resizeRelocate(xOffset, top, chartWidth, heightPerChart);
            top += snapSpaceY(c.getHeight() + getSpacing());
        }
    }

    protected List<XYChart> getCharts() {
        return getChildren().stream().filter(child -> child instanceof XYChart)
                .map(chart -> (XYChart) chart).collect(toList());
    }

    // properties
    /**
     * The amount of vertical space between each child in the vbox.
     * 
     * @return the amount of vertical space between each child in the vbox
     */
    public final DoubleProperty spacingProperty() {
        if (spacing == null) {
            spacing = new SimpleDoubleProperty(this, "spacing", 20) {
                @Override
                public void invalidated() {
                    requestLayout();
                }

            };
        }
        return spacing;
    }

    private DoubleProperty spacing;

    public final void setSpacing(double value) {
        spacingProperty().set(value);
    }

    public final double getSpacing() {
        return spacing == null ? 0 : spacing.get();
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(VChartBox.class.getName());
}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
2

Get rid of the GridPane code and replace it with:

VBox vBox = new VBox(lineChart1, lineChart2);
Scene scene = new Scene(vBox, 800, 600);

Full code:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 *
 * @author blj0011
 */
public class JavaFXApplication139 extends Application
{

    @Override
    public void start(Stage primaryStage)
    {
        primaryStage.setTitle("Line Chart Sample");
        final CategoryAxis xAxis1 = new CategoryAxis();
        final NumberAxis yAxis1 = new NumberAxis();
        xAxis1.setLabel("Month");
        final LineChart<String, Number> lineChart1
                = new LineChart<>(xAxis1, yAxis1);

        final CategoryAxis xAxis2 = new CategoryAxis();
        xAxis2.setLabel("Month");
        final NumberAxis yAxis2 = new NumberAxis();
        final LineChart<String, Number> lineChart2
                = new LineChart<>(xAxis2, yAxis2);

        lineChart1.setTitle("Charts");
        lineChart1.setLegendVisible(false);
        lineChart1.getYAxis().setTickLabelRotation(270);
        lineChart2.setLegendVisible(false);
        lineChart2.getYAxis().setTickLabelRotation(270);

        XYChart.Series series1 = new XYChart.Series();
        series1.setName("Portfolio 1");

        series1.getData().add(new XYChart.Data("Jan", 23));
        series1.getData().add(new XYChart.Data("Feb", 14));
        series1.getData().add(new XYChart.Data("Mar", 15));
        series1.getData().add(new XYChart.Data("Apr", 24));
        series1.getData().add(new XYChart.Data("May", 34));
        series1.getData().add(new XYChart.Data("Jun", 36));
        series1.getData().add(new XYChart.Data("Jul", 22));
        series1.getData().add(new XYChart.Data("Aug", 45));
        series1.getData().add(new XYChart.Data("Sep", 43));
        series1.getData().add(new XYChart.Data("Oct", 17));
        series1.getData().add(new XYChart.Data("Nov", 29));
        series1.getData().add(new XYChart.Data("Dec", 25));

        XYChart.Series series2 = new XYChart.Series();
        series2.setName("Portfolio 2");
        series2.getData().add(new XYChart.Data("Jan", 330000));
        series2.getData().add(new XYChart.Data("Feb", 340000));
        series2.getData().add(new XYChart.Data("Mar", 250000));
        series2.getData().add(new XYChart.Data("Apr", 440000));
        series2.getData().add(new XYChart.Data("May", 390000));
        series2.getData().add(new XYChart.Data("Jun", 160000));
        series2.getData().add(new XYChart.Data("Jul", 550000));
        series2.getData().add(new XYChart.Data("Aug", 540000));
        series2.getData().add(new XYChart.Data("Sep", 480000));
        series2.getData().add(new XYChart.Data("Oct", 270000));
        series2.getData().add(new XYChart.Data("Nov", 370000));
        series2.getData().add(new XYChart.Data("Dec", 290000));

        lineChart1.getData().addAll(series1);
        lineChart2.getData().addAll(series2);

        VBox vBox = new VBox(lineChart1, lineChart2);
        Scene scene = new Scene(vBox, 800, 600);

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args)
    {
        launch(args);
    }

}

enter image description here

SedJ601
  • 12,173
  • 3
  • 41
  • 59