Wednesday, January 21, 2009

Scalable JFreeChart Applet

I created a new applet that allows one to scale a JFreeChart using the mouse scroll wheel. I use the term “scale” rather than “zoom” because JFreeChart already has a well-defined interface for zooming, and it is not the same behavior as my scaling. In the applet below, scrolling up will “zoom in” or magnify the view at the cursor position. Scrolling down “zooms out” centered at the cursor. The behavior is similar to the Mac OS X Universal Access Zoom behavior, which when enabled, allows the user to zoom in and out at the cursor position while holding the Control key and scrolling with the mouse wheel. To quickly get back to the original scaling, hold down the Control and Shift keys and scroll down.

Note for Windows users: if the scaling does not work or quits working, right-click to bring-up a popup menu, then click anywhere on the applet to dismiss the popup. I discussed this bug in a previous post.

The Code
The scaling is an extension of the generic ZoomUI by Piet Blok, which is built on JXLayer. Specifically, I extend two classes. My WheelZoomableUI class extends ZoomUI, and WheelZoomablePort extends ZoomPort. The WheelZoomableUI class contains only one method that overrides transformMouseWheelEvent(...). That method calls the appropriate scaling algorithms in the corresponding WheelZoomablePort class.

The scaling is performed by applying appropriate scaling and translation transformations to the AffineTransform field in the ZoomPort class. When zooming in, the operation is quite simple. First, the AffineTransform.scale(...) transformation applies the scaling. Then, the cursor position in the view is converted to the “unscaled” coordinate system. This position is then used to translate the view so that the “zoomed-in” point is centered at the original mouse cursor position. This simply means that the content under the cursor remains in the same position while scaling.

Zooming out requires one additional step. After the above scaling and translation have been applied, it is possible that part of the chart has fallen off the edge of the view. Another way of describing this is that the user zoomed-out so far that the chart has “slid over” toward the cursor. This results in a blank area in the applet window. In order to avoid this blank area, the chart needs to be “slid back” to cover the blank area. So a corrective translation is applied to ensure that no part of the view is empty.

The minimum scaling allowed is 1.0, meaning that one cannot make the chart smaller than the applet panel size. So no blank areas are possible in the applet. If you observe my magnifier applet, it is possible to zoom out to a scaling factor less than 1.0. This creates a blue area within the magnifying glass that shows the area outside the chart. This blue area corresponds to the empty area described above that necessitates the corrective translation when zooming out.

Constructing the applet is quite simple, as seen here:
public JXLayerScaleDemo() {
  super();
  XYDataset dataset = createDataset();
  JFreeChart chart = createChart(dataset);
  chartPanel = new ChartPanel(chart);
  final WheelZoomablePort zoomPort = new WheelZoomablePort();
  final WheelZoomableUI zoomUI = new WheelZoomableUI();
  zoomPort.setView(chartPanel);
  zoomPort.setOpaque(true);
  final JXLayer<ZoomPort> layer = new JXLayer<ZoomPort>(zoomPort, zoomUI);
  chartPanel.setPreferredSize(new java.awt.Dimension(480, 260));
  setContentPane(layer);
}

Instances of the WheelZoomablePort and WheelZoomableUI are created from the default constructors. The view of the port is set to the chart panel. Then the port and UI are sent to the JXLayer constructor. The JXLayer is then set as the main content.

Future Work
As mentioned above, the wheel zoomer behaves similarly to the Mac OS X Universal Access Zoom, but with one important omission. When zoomed-in, the user cannot pan and explore the hidden parts of the panel. Luckily, JXLayer already has a nice UI to handle panning and scrolling called MouseScrollableUI. I hope to add this functionality in the future.

From a coding perspective, it is not really necessary to subclass ZoomUI and ZoomPort. It may be better to simply add a MouseWheelListener to the ZoomPort and override the mouseWheelMoved method. In fact, the test classes in the ZoomUI and the new TransformUI packages do just this to achieve similar scaling behavior without subclassing.

Resources
To build the applet, you will need these:
  1. My code
  2. JXLayer library (I use version 3.0)
  3. JFreeChart library (I use version 1.0.12)
  4. JFree JCommon library (I use version 1.0.15)
The first link above is a modified subset of Piet Blok's generic ZoomUI released on January 11th, 2009.


Tuesday, January 13, 2009

JFreeChart and JXLayer Adjustable Magnifier

JXLayer is cool. I wrote about Dave Gilbert's demo applet that uses JXLayer and Piet Blok's work to show a magnifying glass over a JFreeChart. Since then Piet has updated his MagnifierUI code and I extended it into a more configurable magnifying glass. In the applet below, you should be able to use your mouse wheel to adjust the magnification factor of the magnifying glass. Also, while holding down the Control and Shift keys, the mouse wheel should adjust the size of the magnifying glass.

Note: Windows users may need to right-click on the chart to enable the mouse wheel adjustments. For some reason, the popup menu that appears after a right-click causes the applet to start listening to mouse wheel events. This is most noticeable when dragging the applet out of the browser and then closing the dragged-out applet and returning to the browser window. This problem does not occur on Linux.

The code is pretty straightforward. I created a subclass of MagnifierUI called AdjustableMagnifierUI. In it I override the processMouseWheelEvent method. If the Control and Shift keys are pressed on a mouse wheel event, the setMagnifyingFactor method in MagnifierUI is called. If no modifier keys are pressed, the setRadius method is called.

The Windows hack is in the applet class. I override setVisible as follows:
public void setVisible(boolean visible) {
  super.setVisible(visible);
  if (isShowing()) {
    // simulate right-mouse click to bring-up popup menu
    chartPanel.mousePressed(new MouseEvent(chartPanel,
        0, 0,
        java.awt.event.InputEvent.BUTTON3_DOWN_MASK,
        0, 0, 1, true));
    // immediately hide the popup menu
    chartPanel.getPopupMenu().setVisible(false);
  }
}
If the applet is showing, I simulate a right-mouse click to bring-up JFreeChart's popup menu. Then the popup is immediately hidden. This enables the mouse wheel events to be handled correctly in Windows XP SP3. This hack only works when setVisible is called. If the user has Java SE 6 Update 10 or later and he drags the applet out of the browser and then closes the dragged-out window, the applet returns to its original spot in the browser window WITHOUT calling setVisible. In this case, mouse wheel events will not cause any magnifier adjustments UNTIL the user right-clicks and shows the popup menu. Dismissing the popup will restore the desired mouse wheel behavior. Does anyone know why this is or have a better solution? This hack is not needed on Ubuntu Linux 8.04.

I ran into one other problem when posting the applet. In order to avoid an AccessControlException, I had to override the isAWTEventListenerEnabled() method to return false in AdjustableMagnifierUI:
@Override
protected boolean isAWTEventListenerEnabled() {
  return false;
}
I didn't notice any problems from overriding this method. Or perhaps this is the cause of the Windows misbehavior? I would appreciate any feedback on this.

To build the applet, you will need these:
  1. My Code: tar.gz or zip
  2. JXLayer library (I use version 3.0)
  3. JFreeChart library (I use version 1.0.12)
  4. JFree JCommon library (I use version 1.0.15)
The first code link above is a modified subset of Piet Blok's generic ZoomUI released on January 11th, 2009.