User Tools

Site Tools


java:prefuse-scatterplot

prefuse Tutorial: Scatter Plot

The documentation available for prefuse is quite sparse, it consists mainly of the JavaDoc API documentation and demo source code. Even though the demo source code provides as good starting point (cp. [Heer et al., 1995] ), in my opinion the examples are overly complex for beginners.

Therefore, this tutorial starts with the minimum source code to create a scatter plot and incrementally adds features. Further, it discusses an axis-based visualization technique, whereas the documentation of prefuse focuses on graph visualization. The demo application is inspired by Jeffrey Heer's Congress demo, but achieves its functionality in small incremental steps. 1)

Introduction

Developing with prefuse is for the most part configuration. You can achieve both basic and advanced visualizations simply by creating objects and setting their properties. However, this makes it very easy produce source code that is hard to understand.

I recommend to structure your prefuse configuration in four parts:

  • data setup
  • renderers
  • action lists
  • displays and controls

Phase 0: Infrastructure

Here we create the outline of our demo application. It is designed very simple and consists of four static methods:

  • private static Table generateTable()
    • prepare a prefuse data table with some hard coded values (see below)
  • private static JComponent createVisualization(Table data)
    • now this is only some dummy code, but in the following phases our prefuse code will be inserted here.
  • private static void createAndShowGUI(JComponent display)
    • some basic Swing code to set up a window for the demo application
  • public static void main(String[] args)
    • runs these three methods

This shows, how we create a data table for our tutorial. We use hard coded values so that the demo application stays self-contained. It would be easy to replace this method by a call to prefuse's DelimitedTextTableReader.

	private static Table generateTable() {
		Table table = new Table();
 
		// use a calendar for input of human-readable dates  
		GregorianCalendar cal = new GregorianCalendar();
 
		// set up table schema
		table.addColumn("Date", Date.class);
		table.addColumn("BMI", double.class);
		table.addColumn("NBZ", int.class);
		table.addColumn("Insult", String.class);
 
		table.addRows(3);
 
		cal.set(2007, 11, 23);
		table.set(0, 0, cal.getTime());
		table.set(0, 1, 21.0);
		table.set(0, 2, 236);
		table.set(0, 3, "F");
 
		cal.set(2008, 6, 22);
		table.set(1, 0, cal.getTime());
		table.set(1, 1, 35.8);
		table.set(1, 2, 400);
		table.set(1, 3, "F");
 
		cal.set(2009, 3, 8);
		table.set(2, 0, cal.getTime());
		table.set(2, 1, 28.8);
		table.set(2, 2, 309);
		table.set(2, 3, "T");
 
		return table;
	}

Source Code

Phase 1: Essentials

Next, we extend the method createVisualization(Table) and add the minimal source code to show a scatter plot.

First, we need to create instances of Visualization and Display and link these with each other and the data table.

		Visualization vis = new Visualization();
		Display display = new Display(vis);
		vis.add("data", data);

Second, we create a AxisLayout for the x axis and another one for the y axis. When these layouts are run, they will set the x and coordinate of all visual items.

		AxisLayout x_axis = new AxisLayout("data", "NBZ", Constants.X_AXIS,
				VisiblePredicate.TRUE);
 
		AxisLayout y_axis = new AxisLayout("data", "BMI", Constants.Y_AXIS,
				VisiblePredicate.TRUE);

Third, a ColorAction will set the color of all visual items to blue.

		ColorAction color = new ColorAction("data", VisualItem.STROKECOLOR,
				ColorLib.rgb(100, 100, 255));

Fourth, these actions are combined to an ActionList and linked to the Visualization.

		ActionList draw = new ActionList();
		draw.add(x_axis);
		draw.add(y_axis);
		draw.add(color);
		vis.putAction("draw", draw);

Finally, we run this ActionList and return the Display so that it can be shown in the window.

		vis.run("draw");
 
		return display;

This is the resulting visualization:

Source Code

Further Information

Alternative: Our visualization class can also be a subclass of either Visualization or Display instead of referencing them.

Note: A Visualization objects can handle multiple Display. However, in some situation prefuse only takes the first display in account – it is still beta.

Phase 2: Refinements

Now, we will add some more features to the scatter plot:

First, we want smaller shapes. For this, we create a ShapeRenderer with default size 7.

		vis.setRendererFactory(new DefaultRendererFactory(
						new ShapeRenderer(7)));

Second, we add a DataShapeAction, so that our nominal parameter is shown as star or ellipse.

		int[] palette = { Constants.SHAPE_STAR, Constants.SHAPE_ELLIPSE };
		DataShapeAction shape = new DataShapeAction("data", "Insult", palette);

Third, the new action needs to be included in the action list.

We also add a RepaintAction, just to make sure that the visualization is repainted after the other actions.

		draw.add(shape);
		draw.add(new RepaintAction());

Fourth, we set the Display to high quality (anti-aliasing), and change its size.

We also add an empty border, so that visual items at the edge of the visualization are better visible.

		display.setHighQuality(true);
		display.setSize(700, 450);
 
		display.setBorder(BorderFactory.createEmptyBorder(15, 30, 15, 30));

Finally, we add a ToolTipControl to show the values of the two numeric parameters.

		String[] tooltipparams = { "NBZ", "BMI" };
		ToolTipControl ttc = new ToolTipControl(tooltipparams);
		display.addControlListener(ttc);

This is the resulting visualization:

Source Code

Phase 3: Axis Labels and Grid Lines

In this step, we will add axis labels and grid lines to the visualization.

First, we have to use a new RendererFactory. Visual items from our data table will still be rendered by the ShapeRenderer, but for axis labels we will use AxisRenderer.

		vis.setRendererFactory(new RendererFactory() {
			AbstractShapeRenderer sr = new ShapeRenderer(7);
			Renderer arY = new AxisRenderer(Constants.FAR_LEFT,
					Constants.CENTER);
			Renderer arX = new AxisRenderer(Constants.CENTER,
					Constants.FAR_BOTTOM);
 
			public Renderer getRenderer(VisualItem item) {
				return item.isInGroup("ylab") ? arY
						: item.isInGroup("xlab") ? arX : sr;
			}
		});

Second, we create two instances of AxisLabelLayout and initialize them with the AxisLayouts.

		AxisLabelLayout x_labels = new AxisLabelLayout("xlab", x_axis);
 
		AxisLabelLayout y_labels = new AxisLabelLayout("ylab", y_axis);

Third, the new layout actions needs to be included in the action list.

		draw.add(x_labels);
		draw.add(y_labels);

Fourth, we create an ItemSorter, so that data items will be displayed in front of the grid lines.

		display.setItemSorter(new ItemSorter() {
			public int score(VisualItem item) {
				int score = super.score(item);
				if (item.isInGroup("data"))
					score++;
				return score;
			}
		});

This is the resulting visualization:

Source Code

Further Information

Alternative: Instead of extending RendererFactory, we can use DefaultRendererFactory. In this case we use predicates to identify the axes.

		DefaultRendererFactory rf = new DefaultRendererFactory(); 
		rf.setDefaultRenderer(new ShapeRenderer(7));
		rf.add(new InGroupPredicate("ylab"), 
				new AxisRenderer(Constants.FAR_LEFT, Constants.CENTER));
		rf.add(new InGroupPredicate("xlab"), 
				new AxisRenderer(Constants.CENTER, Constants.FAR_BOTTOM));
		vis.setRendererFactory(rf);

Warning: When the AxisLabelLayout actions are run, they generate new visual items for the axis labels.

“xlab” and “xlab” are the group names of these visual items. Do not put the group name of your data here!

Phase 4: Interactivity

Here we will some interactivity to our demo application.

First, we create a new ActionList with all actions that we need to update the visualization.

		ActionList update = new ActionList();
		update.add(x_axis);
		update.add(y_axis);
		update.add(x_labels);
		update.add(y_labels);
		update.add(new RepaintAction());
		vis.putAction("update", update);

Second, we an implementation of ComponentListener to the Display, which will run the new ActionList, whenever the user changes the size of the window – and thus resizes the Display.

		display.addComponentListener(new ComponentAdapter() {
			public void componentResized(ComponentEvent e) {
				vis.run("update");
			}
		});

Third, we add three ready-made controls to the Display.

  • PanControl allows the user to pan by dragging the left mouse button in the background.
  • ZoomControl allows the user to zoom by dragging the right mouse button in the background.
  • ZoomToFitControl automatically zooms so that the whole visualization is visible, whenever the user clicks with the right mouse button.
		display.addControlListener(new PanControl());
		display.addControlListener(new ZoomControl());
		display.addControlListener(new ZoomToFitControl());

This is the resulting visualization:

Source Code

Further Information

Warning: Panning and Zooming doesn't work as one might expect: it simply performs a geometric transformation of the display.

Alternative: Using range sliders is an possible alternative to zoom and pan. However the underlying interaction metaphor is different and can be harder to understand.

Phase 5: Refinements on axes

Now, we add a few refinements on the display of axis.

First, we add a new column to the VisualTable, which will contain a derived value from two columns. We also insert captions and format one of the variables.

Note: In order to access the VisualTable we have to replace add with addTable.

		VisualTable vt = vis.addTable("data", data);
 
		vt.addColumn("label",
				"CONCAT('NBZ: ', [NBZ], '; BMI: ', FORMAT([BMI],1))");

Second, we set the range of the y axis, so that values from 1 to 40 are visible.

		y_axis.setRangeModel(new NumberRangeModel(1, 40, 1, 40));

Third, we use a square root scale for y axis.

		y_axis.setScale(Constants.SQRT_SCALE);
		y_labels.setScale(Constants.SQRT_SCALE);

Fourth, we define a number format and use it for y axis labels.

		NumberFormat nf = NumberFormat.getNumberInstance();
		nf.setMaximumFractionDigits(1);
		nf.setMinimumFractionDigits(1);
		y_labels.setNumberFormat(nf);

Finally, we use the new derived column for tool tips.

		ToolTipControl ttc = new ToolTipControl("label");

This is the resulting visualization:

Source Code

Phase 6: Bounding boxes for data and axes

Here we manipulate the bounding boxes for the visualization of the data and both axes. This has two effects:

  • We have fine-grained access on the location of visual items
    • In this example we do not display vertical grid lines behind the data visualizations.
    • Further, we draw horizontal grid line to the right edge, but do not place data items there.
    • Finally, we added some space between y axis labels and the left-most data items.
  • We do not need the empty border defined in phase 2 anymore.
    • Now we or someone else using the Display can add a border, and items at the edge of the visualization (especially axis labels) stay completely visible.

First, we define three rectangles for the visualization of the data and both axes.

		final Rectangle2D boundsData = new Rectangle2D.Double();
		final Rectangle2D boundsLabelsX = new Rectangle2D.Double();
		final Rectangle2D boundsLabelsY = new Rectangle2D.Double();

Second, we set the bounding box for data items on the AxisLayouts.

		x_axis.setLayoutBounds(boundsData);
		y_axis.setLayoutBounds(boundsData);

Third, we add the bounding box for axes to their initialization.

		AxisLabelLayout x_labels = new AxisLabelLayout("xlab", x_axis,
				boundsLabelsX);
 
		AxisLabelLayout y_labels = new AxisLabelLayout("ylab", y_axis,
				boundsLabelsY);

Fourth, instead of the empty border, we now use a titled border.

		display.setBorder(BorderFactory.createTitledBorder("Demo"));

Fifth, we add the call to a new method updateBounds(), before each call to an action list (two times).

				updateBounds(display, boundsData, boundsLabelsX, boundsLabelsY);
...
		updateBounds(display, boundsData, boundsLabelsX, boundsLabelsY);

Finally, this is the method updateBounds(), which sets the bounds as described above.

	private static void updateBounds(Display display, Rectangle2D boundsData,
			Rectangle2D boundsLabelsX, Rectangle2D boundsLabelsY) {
 
		int paddingLeft = 30;
		int paddingTop = 15;
		int paddingRight = 30;
		int paddingBottom = 15;
 
		int axisWidth = 20;
		int axisHeight = 10;
 
		Insets i = display.getInsets();
 
		int left = i.left + paddingLeft;
		int top = i.top + paddingTop;
		int innerWidth = display.getWidth() - i.left - i.right - paddingLeft
				- paddingRight;
		int innerHeight = display.getHeight() - i.top - i.bottom - paddingTop
				- paddingBottom;
 
		boundsData.setRect(left + axisWidth, top, innerWidth - axisWidth,
				innerHeight - axisHeight);
		boundsLabelsX.setRect(left + axisWidth, top + innerHeight - axisHeight,
				innerWidth - axisWidth, axisHeight);
		boundsLabelsY.setRect(left, top, innerWidth + paddingRight, innerHeight
				- axisHeight);
	}

This is the resulting visualization:

Source Code

Further Information

Note: If AxisRenderer have the FAR_LEFT, FAR_RIGHT, FAR_TOP, or FAR_BOTTOM, the grid lines will fill the bounding box completely and labels will be placed outside :!: the bounding box.

Warning: Be careful, prefuse will not stop you from designing misleading visualizations, if the bounding boxes of data and axes are not correctly aligned :!:

Note: The bounding rectangles are final variables so that they are accessible from inner classes. Alternatively they could be defined as instance variables.

Alternative: Instead of the method updateBounds(), we can write an action that takes care of updating the bounding rectangles. Then, this task can be included in the update action list.

1) Please let me know, if you spot any errors or misleading information.
java/prefuse-scatterplot.txt · Last modified: 2009/10/05 15:52 by Alexander Rind

alex @ ieg: home about me publications research