Desktop Java

JavaFX Tip 20: A lot to show? Use Canvas!

There seem to be two kinds of JavaFX applications: the first one is using a scene graph with nodes and CSS styling, and the second one is using a single canvas. However, it is perfectly legal to mix these two approaches. Especially when your application has to show a lot of detailed information where you would easily end up creating thousands and thousands of nodes. Even though the overall performance of JavaFX is fantastic you will most likely bring your system down to its knees when styling is required for all of these nodes (especially when styling is required over and over again because of the dynamic nature of your visualization).

For me it was an epiphany when I realized that the only way to guarantee high performance in FlexGanttFX was to use a ListView with each cell containing a canvas. Unfortunately the code of this framework is too complex to share it with you in a small blog, so I wrote a small example that illustrates the basic concepts. The image below shows the result when running the example. The data shown by the ListView covers the years of my life span with randomly generated values for each day of each year.

bildschirmfoto-2015-06-15-um-19-25-50

The most important class is called CanvasCell. It is a specialized list view cell containing a label and a canvas. The label is used to display the year, the canvas is used to draw the chart.

import java.util.Collections;
import java.util.List;

import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

public class CanvasCell extends ListCell<YearEntry> {

	private Label yearLabel;
	private ResizableCanvas canvas;

	public CanvasCell() {
		/*
		 * Important, otherwise we will keep seeing a horizontal scrollbar.
		 */
		setStyle("-fx-padding: 0px;");

		yearLabel = new Label();
		yearLabel
		  .setStyle("-fx-padding: 10px; -fx-font-size: 1.2em; -fx-font-weight: bold;");
		StackPane.setAlignment(yearLabel, Pos.TOP_LEFT);

		/*
		 * Create a resizable canvas and bind its width and height to the width
		 * and height of the table cell.
		 */
		canvas = new ResizableCanvas();
		canvas.widthProperty().bind(widthProperty());
		canvas.heightProperty().bind(heightProperty());

		StackPane pane = new StackPane();
		pane.getChildren().addAll(yearLabel, canvas);

		setGraphic(pane);
		setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
	}

	@Override
	protected void updateItem(YearEntry entry, boolean empty) {
		if (empty || entry == null) {
			yearLabel.setText("");
			canvas.setData(Collections.emptyList());
			canvas.draw();
		} else {
			yearLabel.setText(Integer.toString(entry.getYear()));
			canvas.setData(entry.getValues());
			canvas.draw();
		}
	}

	/*
	 * Canvas is normally not resizable but by overriding isResizable() and
	 * binding its width and height to the width and height of the cell it will
	 * automatically resize.
	 */
	class ResizableCanvas extends Canvas {

		private List<Double> data = Collections.emptyList();

		public ResizableCanvas() {

			/*
			 * Make sure the canvas draws its content again when its size
			 * changes.
			 */
			widthProperty().addListener(it -> draw());
			heightProperty().addListener(it -> draw());
		}

		@Override
		public boolean isResizable() {
			return true;
		}

		@Override
		public double prefWidth(double height) {
			return getWidth();
		}

		@Override
		public double prefHeight(double width) {
			return getHeight();
		}

		public void setData(List<Double> data) {
			this.data = data;
		}

		/*
		 * Draw a chart based on the data provided by the model.
		 */
		private void draw() {
			GraphicsContext gc = getGraphicsContext2D();
			gc.clearRect(0, 0, getWidth(), getHeight());

			Stop[] stops = new Stop[] { new Stop(0, Color.SKYBLUE),
					new Stop(1, Color.SKYBLUE.darker().darker()) };
			LinearGradient gradient = new LinearGradient(0, 0, 0, 300, false,
					CycleMethod.NO_CYCLE, stops);

			gc.setFill(gradient);

			double availableHeight = getHeight() * .8;
			double counter = 0;
			for (Double value : data) {
				double x = getWidth() / 365 * counter;
				double barHeight = availableHeight * value / 100;
				double barWidth = getWidth() / 365 + 1;
				gc.fillRect(x, getHeight() - barHeight, barWidth, barHeight);
				counter++;
			}
		}
	}
}

For the data we use a very simple class that stores the year and a list of values.

import java.util.ArrayList;
import java.util.List;


/**
 * Just some fake model object.
 */
public class YearEntry {

	private int year;

	public YearEntry(int year) {
		this.year = year;
	}

	public int getYear() {
		return year;
	}

	private List<Double> values = new ArrayList<>();

	/**
	 * Stores the values shown in the chart.
	 */
	public List<Double> getValues() {
		return values;
	}
}

And the following listing shows the main class.

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.stage.Stage;

public class CanvasApp extends Application {

	@Override
	public void start(Stage stage) throws Exception {

		/*
		 * Create some random data for my life span.
		 */
		ObservableList<YearEntry> data = 
			FXCollections.observableArrayList();
		for (int year = 1969; year < 2015; year++) {
			YearEntry entry = new YearEntry(year);
			for (int day = 0; day < 365; day++) {
				entry.getValues().add(Math.random() * 100);
			}
			data.add(entry);
		}

		ListView<YearEntry> listView = new ListView<>(data);
		listView.setCellFactory(param -> new CanvasCell());
		listView.setFixedCellSize(200);

		Scene scene = new Scene(listView);

		stage.setTitle("Canvas Cell");
		stage.setScene(scene);
		stage.setWidth(600);
		stage.setHeight(600);
		stage.show();
	}

	public static void main(String[] args) {
		launch(args);
	}
}
Reference: JavaFX Tip 20: A lot to show? Use Canvas! from our JCG partner Dirk Lemmermann at the Pixel Perfect blog.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
xzl
xzl
9 years ago

I love java!

Back to top button