0

I have been trying to do some work with XYCharts, specifically placing things on or around them. I have been needing to use the coordinates of the bounding boxes of the axes in relation to the coordinate space defined by the whole chart. However, I have realised that the .getBoundsInParent() method is not returning what I expect when applied to the axes. It seems to be because the axis parent isn't the chart and I don't know what the parent is. For example the following boolean is false which to me seems like terrible design (assume chart is an instance of LineChart):

chart.getXAxis().getParent() == chart

what is going on here? Thanks.

th0masb
  • 303
  • 1
  • 11
  • 2
    Why would you expect the direct parent to be the chart? How the internal layout of the chart is managed is an implementation detail that you shouldn't be concerned with. This just sounds like you are using the wrong approach to whatever problem it is you're trying to solve. – James_D Nov 14 '16 at 14:00
  • all I need is the coordinates of the charting area as they appear on the screen, that is the grey space defined by the axes do you know how to get that? – th0masb Nov 14 '16 at 14:02
  • In which coordinate system? – James_D Nov 14 '16 at 14:03
  • I need the bounding box of the grey charting area relative to the space occupied by the whole chart node (which includes the axes, legend, charting area etc). – th0masb Nov 14 '16 at 14:04
  • You probably don't need that at all. Why do you think you need it? Are you just trying to add other content to a chart? (To get that bounding box you would have to do some pretty ugly lookups, and then use the bounds transforms relative to something fixed (e.g. the scene) and then use the inverse transform from the scene back to the chart. But this is almost the wrong approach to solve the problem you haven't described.) – James_D Nov 14 '16 at 14:07
  • I have a populated LineChart and on the RHS I have added a series of coloured bands visually separating the areas on the graph (not on the graph but anchored next to). For each data point there is a tooltip displayed when the mouse hovers over but I am needing to adjust the location of the text so that it doesn't stray outside the grey charting area. I just don't understand why there aren't these simple bounding boxes supplied. – th0masb Nov 14 '16 at 14:13
  • 1
    I assume there aren't "simple bounding boxes" provided as public API because it would tie the API to a particular implementation of the layout, which would prevent the JavaFX programmers from making improvements in the future, if they needed/wanted. I haven't tried this, but it sounds like you should really be overriding `layoutChartChildren(...)` to implement what you are looking to do. As a bit of a hack, try `Node plotContent = chart.lookup(".plot-content");`, and then `chart.sceneToLocal(plotContent.localToScene(plotContent.getBoundsInLocal()))`. – James_D Nov 14 '16 at 14:20
  • You might be able to replace the `plotContent` with the axis in the above, to get the location of the axis relative to the chart. That may be enough to do what you need. – James_D Nov 14 '16 at 14:22
  • I will play around with those suggestions, I agree there will be reasons it's just frustrating to a noob! Thanks for replying as well. – th0masb Nov 14 '16 at 14:23
  • It turns out `layoutChartChildren` is final in `XYChart`, so that option is not available. Added an answer using a region to hold the chart and some extra content to the right of it. – James_D Nov 14 '16 at 15:12

1 Answers1

1

There's no direct API to get the bounds of the chart plot area. One approach would be to use lookups, but that is not very robust.

If you want additional content in the chart plot area, the recommended approach is to override the chart's layoutPlotChildren method, as in this question.

Probably the best approach for adding content outside the chart area, is to subclass Region and override the layoutChildren method to layout the chart and the extra content you want. You can determine coordinates for a specific "chart value" by using ValueAxis.getDisplayPosition(value) to get the coordinates relative to the axis, and then use a couple of transformations to get those coordinates relative to the region.

Here is a quick example:

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

import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class LineChartWithRightPanel extends Application {

    private final Color[] RECT_COLORS = 
            new Color[] {Color.RED, Color.ORANGE, Color.YELLOW, Color.GREEN, Color.BLUE};
    private final double RECT_WIDTH = 20 ;

    @Override
    public void start(Stage primaryStage) {

        NumberAxis xAxis = new NumberAxis();
        NumberAxis yAxis = new NumberAxis(0, 1, 0.2);
        LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);

        Region chartWithPanel = new Region() {

            private List<Rectangle> rectangles = 
                    Stream.of(RECT_COLORS)
                    .map(c -> new Rectangle(RECT_WIDTH, 0, c))
                    .collect(Collectors.toList());

            {
                getChildren().add(chart);
                getChildren().addAll(rectangles);
            }

            @Override
            protected void layoutChildren() {
                double w = getWidth();
                double h = getHeight();
                chart.resizeRelocate(0, 0, w-RECT_WIDTH, h);
                chart.layout();

                double chartMinY = yAxis.getLowerBound();
                double chartMaxY = yAxis.getUpperBound();
                double chartYRange = chartMaxY - chartMinY ;

                for (int i = 0 ; i < rectangles.size(); i++) {

                    // easier ways to do this in this example, 
                    // but this technique shows how to map "chart coords"
                    // to coords in this pane:

                    double rectMinYInChart = chartMinY + i * chartYRange / rectangles.size();
                    double rectMaxYInChart = chartMinY + (i+1) * chartYRange / rectangles.size();
                    double rectMinYInAxis = yAxis.getDisplayPosition(rectMinYInChart);
                    double rectMaxYInAxis = yAxis.getDisplayPosition(rectMaxYInChart);
                    double rectMinY = sceneToLocal(yAxis.localToScene(new Point2D(0, rectMinYInAxis))).getY();
                    double rectMaxY = sceneToLocal(yAxis.localToScene(new Point2D(0, rectMaxYInAxis))).getY();

                    rectangles.get(i).setHeight(Math.abs(rectMaxY - rectMinY));
                    rectangles.get(i).setX(w - RECT_WIDTH);
                    rectangles.get(i).setY(Math.min(rectMinY, rectMaxY));

                    Tooltip.install(rectangles.get(i), 
                            new Tooltip(String.format("%.2f - %.2f", rectMinYInChart, rectMaxYInChart)));
                }
            }
        };

        Series<Number, Number> series = new Series<>();
        series.setName("Data");
        Random rng = new Random();
        for (int i = 0; i < 20; i++) {
            series.getData().add(new Data<>(i, rng.nextDouble()));
        }
        chart.getData().add(series);

        BorderPane root = new BorderPane(chartWithPanel);
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }


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

Screenshot of chart with colored bands on right

Note that you can get the bounds of the entire plot area using the same idea:

double minXInAxis = xAxis.getDisplayPosition(xAxis.getLowerBound());
double maxXInAxis = xAxis.getDisplayPosition(xAxis.getUpperBound());
double minYInAxis = yAxis.getDisplayPosition(yAxis.getLowerBound());
double maxYInAxis = yAxis.getDisplayPosition(yAxis.getUpperBound());

double minX = sceneToLocal(xAxis.localToScene(new Point2D(minXInAxis,0))).getX();
double maxX = sceneToLocal(xAxis.localToScene(new Point2D(maxXInAxis,0))).getX();
double minY = sceneToLocal(yAxis.localToScene(new Point2D(0,minYInAxis))).getY();
double maxY = sceneToLocal(yAxis.localToScene(new Point2D(0,maxYInAxis))).getY();

Then minX, maxX, minY, maxY define the bounds of the plot area.

Community
  • 1
  • 1
James_D
  • 201,275
  • 16
  • 291
  • 322
  • I would just like to say thank you again, this is definitely the way to do what I wanted to do. I have been trying to implement it today and I have nearly done it. My implementation is somewhat more complex because the chart is embedded in a large piece of software and is not generated in such a straightforward way but the idea is the same. – th0masb Nov 16 '16 at 15:58