BAR
BAR

Reputation: 17151

How to get low level access to JavaFX chart so an image can be drawn?

I want to draw an image behind the LineChart so that any grid and lines are drawn over the image. It seems LineChart would need to expose some lower level accessor to the Canvas so this could be done.

public class ChartBgImageTest {
    public ChartBgImageTest() {

        final CategoryAxis xAxis = new CategoryAxis();
        final NumberAxis yAxis = new NumberAxis(1, 21, 0.1);
        final LineChart<String, Number> lineChart = new LineChart<String, Number>(xAxis, yAxis);

        BufferedImage bufferedImage = GraphicsEnvironment
                .getLocalGraphicsEnvironment()
                .getDefaultScreenDevice()
                .getDefaultConfiguration()
                .createCompatibleImage(600, 400);


        BufferedImage img = new BufferedImage(600, 400, BufferedImage.TYPE_INT_RGB);
        img.createGraphics().fillRect(0, 0, 50, 50);
        // Fill img code here

        Graphics g2d = bufferedImage.createGraphics();

        g2d.drawImage(img, 0, 0, null);

        // TODO How to draw the img to lineChart background.
        // So that any grids or lines on the chart are drawn above the img.

    }
}

Upvotes: 0

Views: 292

Answers (1)

Slaw
Slaw

Reputation: 46170

The charts in the javafx.scene.chart package don't use a Canvas to draw the charts. They instead use the scene graph. In your question you're creating an image on the fly, but assuming you have an already-created image, possibly as an embedded resource, you could use CSS to add the image to the chart's background. Looking at the XYChart section of JavaFX CSS Reference Guide, you'll see one of the substructures is labeled chart-plot-background and is a Region, which means a background image can be applied to it via CSS. For example:

App.java:

import java.util.Random;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
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.stage.Stage;

public class App extends Application {

  @Override
  public void start(Stage primaryStage) {
    var chart = new LineChart<>(new NumberAxis(), new NumberAxis(), createChartData());
    chart.getXAxis().setLabel("X");
    chart.getYAxis().setLabel("Y");

    var scene = new Scene(chart, 1000, 650);
    scene.getStylesheets().add(getClass().getResource("/style.css").toString());

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

  private static ObservableList<Series<Number, Number>> createChartData() {
    var random = new Random();

    var chartData = FXCollections.<Series<Number, Number>>observableArrayList();
    for (int i = 1; i <= 3; i++) {
      var data = FXCollections.<Data<Number, Number>>observableArrayList();
      for (int j = 0; j <= 15; j++) {
        data.add(new Data<>(j, random.nextInt(250)));
      }
      chartData.add(new Series<>("Series #" + i, data));
    }
    return chartData;
  }
}

style.css:

.chart-plot-background {
    -fx-background-image: url(/* your URL */);
    -fx-background-size: cover;
}

That will only draw the image behind the actual chart content (i.e. the data). The axis, legend, and surrounding space won't have the image behind it. If you want the entire chart to have the background image then you can utilize the fact that Chart, which all chart implementations inherit from, extends from Region. Change the CSS to:

.chart {
    -fx-background-image: url(/* your URL */);
    -fx-background-size: cover;
}

.chart-plot-background,
.chart-legend {
    -fx-background-color: null;
}

If you want something between the plot background and the entire chart you can add the image to .chart-content (documented here).


If you have to stay within code (e.g. because you're creating the image on the fly) then you'll have to get a reference to the necessary Region. To do that you can use Node#lookup(String). Be aware, however, that you may need to wait until the chart has been rendered on screen before calling lookup, as it's possible the needed descendant node has not been created and added to the scene graph beforehand (this is especially true of controls).

import java.util.Random;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
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.image.Image;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.Region;
import javafx.stage.Stage;

public class App extends Application {

  @Override
  public void start(Stage primaryStage) {
    var chart = new LineChart<>(new NumberAxis(), new NumberAxis(), createChartData());
    chart.setTitle("Example Chart");
    chart.getXAxis().setLabel("X");
    chart.getYAxis().setLabel("Y");

    var scene = new Scene(chart, 1000, 650);

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

    var plotBackground = (Region) chart.lookup(".chart-plot-background");
    plotBackground.setBackground(
        new Background(
            new BackgroundImage(
                new Image(/* your URL */),
                null,
                null,
                BackgroundPosition.CENTER,
                new BackgroundSize(0, 0, false, false, true, false))));
  }

  private static ObservableList<Series<Number, Number>> createChartData() {
    var random = new Random();

    var chartData = FXCollections.<Series<Number, Number>>observableArrayList();
    for (int i = 1; i <= 3; i++) {
      var data = FXCollections.<Data<Number, Number>>observableArrayList();
      for (int j = 0; j <= 15; j++) {
        data.add(new Data<>(j, random.nextInt(250)));
      }
      chartData.add(new Series<>("Series #" + i, data));
    }
    return chartData;
  }
}

Notice that you can't use BufferedImage directly with the JavaFX API. If you need to actually draw an Image in code you have a few options:

Upvotes: 4

Related Questions