This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
java:prefuse-scatterplot [2009/08/06 11:50] Alexander Rind Phase 1: Essentials |
java:prefuse-scatterplot [2009/10/05 15:52] (current) Alexander Rind some updates |
||
---|---|---|---|
Line 6: | Line 6: | ||
Therefore, this tutorial starts with the minimum source code to create a scatter plot and incrementally adds features. | 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. | 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. | ||
+ | ((Please [[http://ike.donau-uni.ac.at/~rind/ | let me know]], if you spot any errors or misleading information.)) | ||
+ | |||
+ | ===== 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 ===== | ===== Phase 0: Infrastructure ===== | ||
Line 59: | Line 73: | ||
</code> | </code> | ||
- | {{:it:scatterplot0.java| Source Code}} | + | {{:java:scatterplot0.java| Source Code}} |
===== Phase 1: Essentials ===== | ===== Phase 1: Essentials ===== | ||
Line 110: | Line 124: | ||
This is the resulting visualization: | This is the resulting visualization: | ||
- | {{:it:prefuse-scatterplot1.png?50}} | + | {{:java:prefuse-scatterplot1.png?100}} |
+ | |||
+ | {{:java:scatterplot1.java|Source Code}} | ||
+ | |||
+ | === Further Information === | ||
+ | |||
+ | __Alternative:__ Our visualization class can also be a subclass of either [[http://prefuse.org/doc/api/prefuse/Visualization.html | Visualization]] or [[http://prefuse.org/doc/api/prefuse/Display.html | Display]] instead of referencing them. | ||
+ | |||
+ | __Note:__ A [[http://prefuse.org/doc/api/prefuse/Visualization.html | Visualization]] objects can handle multiple [[http://prefuse.org/doc/api/prefuse/Display.html | 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 [[http://prefuse.org/doc/api/prefuse/render/ShapeRenderer.html | ShapeRenderer]] with default size 7. | ||
+ | |||
+ | <code java> | ||
+ | vis.setRendererFactory(new DefaultRendererFactory( | ||
+ | new ShapeRenderer(7))); | ||
+ | </code> | ||
+ | |||
+ | Second, we add a [[http://prefuse.org/doc/api/prefuse/action/assignment/DataShapeAction.html | DataShapeAction]], so that our nominal parameter is shown as star or ellipse. | ||
+ | |||
+ | <code java> | ||
+ | int[] palette = { Constants.SHAPE_STAR, Constants.SHAPE_ELLIPSE }; | ||
+ | DataShapeAction shape = new DataShapeAction("data", "Insult", palette); | ||
+ | </code> | ||
+ | |||
+ | Third, the new action needs to be included in the action list. | ||
+ | |||
+ | We also add a [[http://prefuse.org/doc/api/prefuse/action/RepaintAction.html | RepaintAction]], just to make sure that the visualization is repainted after the other actions. | ||
+ | |||
+ | <code java> | ||
+ | draw.add(shape); | ||
+ | draw.add(new RepaintAction()); | ||
+ | </code> | ||
+ | |||
+ | Fourth, we set the [[http://prefuse.org/doc/api/prefuse/Display.html | 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. | ||
+ | |||
+ | <code java> | ||
+ | display.setHighQuality(true); | ||
+ | display.setSize(700, 450); | ||
+ | |||
+ | display.setBorder(BorderFactory.createEmptyBorder(15, 30, 15, 30)); | ||
+ | </code> | ||
+ | |||
+ | Finally, we add a [[http://prefuse.org/doc/api/prefuse/controls/ToolTipControl.html | ToolTipControl]] to show the values of the two numeric parameters. | ||
+ | |||
+ | <code java> | ||
+ | String[] tooltipparams = { "NBZ", "BMI" }; | ||
+ | ToolTipControl ttc = new ToolTipControl(tooltipparams); | ||
+ | display.addControlListener(ttc); | ||
+ | </code> | ||
+ | |||
+ | This is the resulting visualization: | ||
+ | |||
+ | {{:java:prefuse-scatterplot2.png?150}} | ||
+ | |||
+ | {{:java:scatterplot2.java|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 [[http://prefuse.org/doc/api/prefuse/render/RendererFactory.html | RendererFactory]]. | ||
+ | Visual items from our data table will still be rendered by the [[http://prefuse.org/doc/api/prefuse/render/ShapeRenderer.html | ShapeRenderer]], | ||
+ | but for axis labels we will use [[http://prefuse.org/doc/api/prefuse/render/AxisRenderer.html | AxisRenderer]]. | ||
+ | |||
+ | <code java> | ||
+ | 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; | ||
+ | } | ||
+ | }); | ||
+ | </code> | ||
+ | |||
+ | Second, we create two instances of [[http://prefuse.org/doc/api/prefuse/action/layout/AxisLabelLayout.html | AxisLabelLayout]] and initialize them with the [[http://prefuse.org/doc/api/prefuse/action/layout/AxisLayout.html | AxisLayouts]]. | ||
+ | |||
+ | <code java> | ||
+ | AxisLabelLayout x_labels = new AxisLabelLayout("xlab", x_axis); | ||
+ | |||
+ | AxisLabelLayout y_labels = new AxisLabelLayout("ylab", y_axis); | ||
+ | </code> | ||
+ | |||
+ | Third, the new layout actions needs to be included in the action list. | ||
+ | |||
+ | <code java> | ||
+ | draw.add(x_labels); | ||
+ | draw.add(y_labels); | ||
+ | </code> | ||
+ | |||
+ | Fourth, we create an [[http://prefuse.org/doc/api/prefuse/visual/sort/ItemSorter.html | ItemSorter]], so that data items will be displayed in front of the grid lines. | ||
+ | |||
+ | <code java> | ||
+ | display.setItemSorter(new ItemSorter() { | ||
+ | public int score(VisualItem item) { | ||
+ | int score = super.score(item); | ||
+ | if (item.isInGroup("data")) | ||
+ | score++; | ||
+ | return score; | ||
+ | } | ||
+ | }); | ||
+ | </code> | ||
+ | |||
+ | This is the resulting visualization: | ||
+ | |||
+ | {{:java:prefuse-scatterplot3.png?150}} | ||
+ | |||
+ | {{:java:scatterplot3.java|Source Code}} | ||
+ | |||
+ | === Further Information === | ||
+ | |||
+ | __Alternative:__ Instead of extending RendererFactory, we can use [[http://prefuse.org/doc/api/prefuse/render/DefaultRendererFactory.html | DefaultRendererFactory]]. | ||
+ | In this case we use predicates to identify the axes. | ||
+ | |||
+ | <code java> | ||
+ | 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); | ||
+ | </code> | ||
+ | |||
+ | |||
+ | __Warning:__ When the [[http://prefuse.org/doc/api/prefuse/action/layout/AxisLabelLayout.html | 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 [[http://prefuse.org/doc/api/prefuse/action/ActionList.html | ActionList]] with all actions that we need to update the visualization. | ||
+ | |||
+ | <code java> | ||
+ | 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); | ||
+ | </code> | ||
+ | |||
+ | Second, we an implementation of [[http://java.sun.com/javase/6/docs/api/java/awt/event/ComponentListener.html | ComponentListener]] to the [[http://prefuse.org/doc/api/prefuse/Display.html | Display]], which will run the new [[http://prefuse.org/doc/api/prefuse/action/ActionList.html | ActionList]], whenever the user changes the size of the window -- and thus resizes the [[http://prefuse.org/doc/api/prefuse/Display.html | Display]]. | ||
+ | |||
+ | <code java> | ||
+ | display.addComponentListener(new ComponentAdapter() { | ||
+ | public void componentResized(ComponentEvent e) { | ||
+ | vis.run("update"); | ||
+ | } | ||
+ | }); | ||
+ | </code> | ||
+ | |||
+ | Third, we add three ready-made controls to the [[http://prefuse.org/doc/api/prefuse/Display.html | Display]]. | ||
+ | |||
+ | * [[http://prefuse.org/doc/api/prefuse/controls/PanControl.html | PanControl]] allows the user to pan by dragging the left mouse button in the background. | ||
+ | * [[http://prefuse.org/doc/api/prefuse/controls/ZoomControl.html | ZoomControl]] allows the user to zoom by dragging the right mouse button in the background. | ||
+ | * [[http://prefuse.org/doc/api/prefuse/controls/ZoomToFitControl.html | ZoomToFitControl]] automatically zooms so that the whole visualization is visible, whenever the user clicks with the right mouse button. | ||
+ | |||
+ | <code java> | ||
+ | display.addControlListener(new PanControl()); | ||
+ | display.addControlListener(new ZoomControl()); | ||
+ | display.addControlListener(new ZoomToFitControl()); | ||
+ | </code> | ||
+ | |||
+ | |||
+ | This is the resulting visualization: | ||
+ | |||
+ | {{:java:prefuse-scatterplot4.png?150}} | ||
+ | |||
+ | {{:java:scatterplot4.java|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 [[http://prefuse.org/doc/api/prefuse/visual/VisualTable.html | 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 [[http://prefuse.org/doc/api/prefuse/visual/VisualTable.html | VisualTable]] we have to replace ''add'' with ''addTable''. | ||
+ | |||
+ | <code java> | ||
+ | VisualTable vt = vis.addTable("data", data); | ||
+ | |||
+ | vt.addColumn("label", | ||
+ | "CONCAT('NBZ: ', [NBZ], '; BMI: ', FORMAT([BMI],1))"); | ||
+ | </code> | ||
+ | |||
+ | Second, we set the range of the y axis, so that values from 1 to 40 are visible. | ||
+ | <code java> | ||
+ | y_axis.setRangeModel(new NumberRangeModel(1, 40, 1, 40)); | ||
+ | </code> | ||
+ | |||
+ | Third, we use a square root scale for y axis. | ||
+ | <code java> | ||
+ | y_axis.setScale(Constants.SQRT_SCALE); | ||
+ | y_labels.setScale(Constants.SQRT_SCALE); | ||
+ | </code> | ||
+ | |||
+ | Fourth, we define a number format and use it for y axis labels. | ||
+ | <code java> | ||
+ | NumberFormat nf = NumberFormat.getNumberInstance(); | ||
+ | nf.setMaximumFractionDigits(1); | ||
+ | nf.setMinimumFractionDigits(1); | ||
+ | y_labels.setNumberFormat(nf); | ||
+ | </code> | ||
+ | |||
+ | Finally, we use the new derived column for tool tips. | ||
+ | <code java> | ||
+ | ToolTipControl ttc = new ToolTipControl("label"); | ||
+ | </code> | ||
+ | |||
+ | This is the resulting visualization: | ||
+ | |||
+ | {{:java:prefuse-scatterplot5.png?150}} | ||
+ | |||
+ | {{:java:scatterplot5.java|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 [[http://prefuse.org/doc/api/prefuse/Display.html | 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. | ||
+ | |||
+ | <code java> | ||
+ | final Rectangle2D boundsData = new Rectangle2D.Double(); | ||
+ | final Rectangle2D boundsLabelsX = new Rectangle2D.Double(); | ||
+ | final Rectangle2D boundsLabelsY = new Rectangle2D.Double(); | ||
+ | </code> | ||
+ | |||
+ | Second, we set the bounding box for data items on the [[http://prefuse.org/doc/api/prefuse/action/layout/AxisLayout.html | AxisLayouts]]. | ||
+ | |||
+ | <code java> | ||
+ | x_axis.setLayoutBounds(boundsData); | ||
+ | y_axis.setLayoutBounds(boundsData); | ||
+ | </code> | ||
+ | |||
+ | Third, we add the bounding box for axes to their initialization. | ||
+ | |||
+ | <code java> | ||
+ | AxisLabelLayout x_labels = new AxisLabelLayout("xlab", x_axis, | ||
+ | boundsLabelsX); | ||
+ | |||
+ | AxisLabelLayout y_labels = new AxisLabelLayout("ylab", y_axis, | ||
+ | boundsLabelsY); | ||
+ | </code> | ||
+ | |||
+ | Fourth, instead of the empty border, we now use a titled border. | ||
+ | |||
+ | <code java> | ||
+ | display.setBorder(BorderFactory.createTitledBorder("Demo")); | ||
+ | </code> | ||
+ | |||
+ | Fifth, we add the call to a new method ''updateBounds()'', before each call to an action list (two times). | ||
+ | |||
+ | <code java> | ||
+ | updateBounds(display, boundsData, boundsLabelsX, boundsLabelsY); | ||
+ | ... | ||
+ | updateBounds(display, boundsData, boundsLabelsX, boundsLabelsY); | ||
+ | </code> | ||
+ | |||
+ | Finally, this is the method ''updateBounds()'', which sets the bounds as described above. | ||
+ | |||
+ | <code java> | ||
+ | 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); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | |||
+ | This is the resulting visualization: | ||
+ | |||
+ | {{:java:prefuse-scatterplot6.png?150}} | ||
+ | |||
+ | {{:java:scatterplot6.java|Source Code}} | ||
+ | |||
+ | === Further Information === | ||
+ | |||
+ | __Note:__ If [[http://prefuse.org/doc/api/prefuse/render/AxisRenderer.html | 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. | ||
alex @ ieg: home about me publications research