Drawing More Complex Structures

In addition to dots, lines, and text, the Graphics class supports the drawing of other types of structures, such as rectangles, circles, ovals, and arcs. There are far too many different structures for me to demonstrate each and every one. I thought that demonstrating one of the more difficult structures—the pie chart—might aid you in drawing pie charts and other structures from within your programs.

PieChart app

When used properly, nothing gives users better insight into the relationship of data than the pie chart. Even with the support of the Graphics class, the pie chart seems daunting to create at first; however, with the proper use of a few simple trigonometric functions, the pie chart is easily mastered.

The PieChart program presented here accepts any number of numeric arguments that determine the relative size of each pie slice and draws the pie chart on the screen. In other words, if the user enters piechart 5 5 5 10, the PieChart program will draw a pie with three equally sized slices and a fourth slice that is twice as large as the others.

drawPie() method

The key method in drawing a pie chart is the Graphics method drawPie(). In a way, this method is misnamed because it really draws only one pie slice. The first argument to this method is a bounding rectangle. For almost all pie charts, including the one in this example, this rectangle will be a square. (If this isn't a square, the "pie" will be an oval rather than a circle.) The resulting pie chart will be constructed such that it just barely fits within this square.

The next two arguments are the starting and ending points of the pie slice. It's left up to the program to make sure that these points are on the circumference of the pie. As you'll see, this is where trigonometric functions come into play.

Forms Designer work

Start by creating the PieChart project as a Visual J Plus Plus Windows app in the usual way. Set the text property of the Form object to Pie Chart. You should also size the form so it is close to being square, although its exact dimensions aren't critical.

While you have the Forms Designer open, use the Properties window active properties to create two event handlers, one for the paint event and one for the resize event. You can accept the default names for these two methods, Form1_paint() and Form1_resize(). The resize event will be used to set the size of the bounding square for the pie chart and the paint event will be used to draw the pie chart.

code

The code for PieChart appears as follows:

import com.ms.wfc.app.*;
import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
import com.ms.wfc.html.*;
/**
 * This class can take a variable number of parameters on the command
 * line. Program execution begins with the main() method. The class
 * constructor is not invoked unless an object of type 'Form1' is
 * created in the main() method.
 */
public class Form1 extends Form
{
 // global variables used in the program
 // if there is an error, errorString will be non-null
 String errorString = null;
 // pieBox - the bounding square for the pie chart
 Rectangle pieBox;
 // values - the values to plot; these are read from the
 // command line at startup
 double[] values = null;
 // points, brushes - the points and brushes to plot for
 Point[] points;
 Brush[] brushes;
 // constants used in the program
 static int[] brushstyle = new int[]{
 BrushStyle.SOLID,
 BrushStyle.HORIZONTAL,
 BrushStyle.HOLLOW,
 BrushStyle.FORWARDDIAGONAL,
 BrushStyle.DIAGONALCROSS,
 BrushStyle.CROSS,
 BrushStyle.BACKWARDDIAGONAL
 };
 static Color[] color = new Color[] {
 Color.BLACK,
 Color.CONTROLTEXT,
 Color.CYAN
 };
 public Form1(String[] args)
 {
 // Required for Visual J Plus Plus Form Designer support
 initForm();
 try
 {
 // read the values in from the arguments
 values = readValues(args);
 // calculate the size of the enclosing box
 pieBox = calculatePieBox();
 // now create the pie chart plot points
 calculatePieChart();
 }
 catch(Exception e)
 {
 // on error, save the error string
 errorString = e.toString();
 }
 // force the window to repaint
 invalidate();
 }
 /**
 * Read the string arguments into an array of doubles.
 */
 double[] readValues(String[] args)
 throws Exception
 {
 // there must be at least two arguments
 if (args.length < 2)
 {
 throw new Exception("There must be at least 2 args");
 }
 // create an array of values to plot; the
 // first element should be a 0 and the rest
 // taken from the program argument list
 double[] values = new double[args.length + 1];
 values[0] = 0.0;
 for (int i = 1; i < values.length; i++)
 {
 values[i] = (Double.valueOf(args[i - 1])).doubleValue();
 if (values[i] <= 0)
 {
 throw new Exception("Values must be positive");
 }
 }
 return values;
 }
 /**
 * Calculate the bounding square for the pie chart
 * based on the minimum dimensions of the form.
 */
 Point size = new Point();
 Rectangle calculatePieBox()
 {
 // find the size of the display area of the form
 Rectangle r = this.getDisplayRect();
 size.x = r.width;
 size.y = r.height;
 // make the bounding square slightly smaller than
 // the form's display area (allow 10 pixels on all
 // sides)
 size.x -= 20;
 size.y -= 20; int minSize = Math.min(size.x, size.y);
 return new Rectangle(10, 10, minSize, minSize);
 }
 /**
 * Convert the values that were read into pie chart coordinates.
 */
 void calculatePieChart()
 throws Exception
 {
 // calculate the sum of all the values to plot
 // (each value will be converted into a percentage
 // of this sum)
 double sum = 0;
 for (int i = 1; i < values.length; i++)
 {
 sum += values[i];
 }
 // normalize each value to a number between 0
 // and 2PI
 double[] plotValues = new double[values.length];
 double factor = (2 * Math.PI) / sum;
 sum = 0;
 for (int i = 0; i < values.length; i++)
 {
 plotValues[i] = values[i] * factor;
 sum += plotValues[i];
 plotValues[i] = sum;
 }
 // now convert these values into x,y values
 int radius = pieBox.height / 2;
 points = new Point[values.length];
 brushes = new Brush[values.length];
 for (int i = 0; i < values.length; i++)
 {
 // calculate a point on the radius of a circle
 int xLength = (int)(radius * Math.sin(plotValues[i]));
 int yLength = (int)(radius * Math.cos(plotValues[i]));
 // now move these values relative to the middle of
 // the rectangle
 xLength += radius;
 yLength += radius;
 // now store each value as a point with its own brush
 points[i] = new Point(xLength, yLength);
 // create a brush for this slice
 int colorIndex = i / brushstyle.length;
 colorIndex %= color.length;
 int brushStyleIndex = i % brushstyle.length;
 brushes[i] = new Brush(color[colorIndex],
 brushstyle[brushStyleIndex]);
 }
 }
 /**
 * Paint the pie chart based on previously calculated values.
 */
 private void Form1_paint(Object source, PaintEvent e)
 {
 // if there was an error…
 Graphics g = e.graphics;
 if (errorString != null)
 {
 // display the message and quit
 g.drawString("Error: " + errorString, 10, 10);
 return;
 }
 // connect the pie slices
 int length = points.length - 1;
 for (int i = 0; i < length; i++)
 {
 // first draw the slice
 g.setBrush(brushes[i]);
 g.drawPie(pieBox, points[i], points[i + 1]);
 // now draw the value between the two slice points
 // (just take the average of two points)
 String s = Double.toString(values[i + 1]);
 int x = (int)((points[i].x + points[i + 1].x) / 2);
 int y = (int)((points[i].y + points[i + 1].y) / 2);
 g.drawString(s, x, y);
 }
 }
 /**
 * Enforce a minimum size of 100x100 pixels.
 */
 Point minSize = new Point(100, 100);
 protected Point getMinTrackSize()
 {
 return minSize;
 }
 /**
 * Recalculate the pie chart as the form resizes.
 */
 private void Form1_resize(Object source, Event e)
 {
 try
 {
 // recalculate the plot points based on the
 // new size, and repaint the window
 pieBox = calculatePieBox();
 calculatePieChart();
 invalidate();
 }
 catch (Exception ex)
 {
 errorString = ex.toString();
 }
 }
 /**
 * NOTE: The following code is required by the Visual J Plus Plus form
 * designer. It can be modified using the form editor. Do not
 * modify it using the code editor.
 */
 Container components = new Container();
 private void initForm()
 {
 // …created by the Forms Designer…
 }
 /**
 * The main entry point for the app. */
 public static void main(String args[])
 {
 app.run(new Form1(args));
 }
}


The PieChart() constructor starts by invoking Form1.readValues() to convert the arguments the user passed to it into an array of numbers named values. The constructor then calls Form1.calculatePieBox() to calculate the bounding square for the pie chart based on the current size of the form window. Finally, PieChart() calls Form1.calculatePieChart() to convert the values read into a pie chart within the bounding square. If an exception is thrown from any of these methods, the exception is converted to text and stored in the data member errorString for display by the Form1_paint() method.

The method readValues() converts each argument into a double value and stores it into the values array starting with index 1. The array value values[0] is hard coded to have a value of zero (0). An exception is thrown if any of the values is 0, negative, or not a number.

The method calculatePieBox() starts by retrieving the display area of the form. From this it calculates whether the width or the height of the form is smaller. The method creates a bounding square that fits within 10 pixels on both sides of the smaller of these two dimensions. By picking the shorter of the height or width, calculatePieBox() ensures that the pie chart won't be bigger than the display area on any side.

The calculatePieChart() method is where all the real fun happens. The method begins by adding up all the values to plot and storing the result in a variable named sum. In the next step, each value in values is converted into a series of angles in the array plotValues, such that the first angle starts at 0 and the last angle is 2PI. (2PI is a complete circle in radian.)

You might be wondering why we used a set of example numbers and degrees instead of radian. Suppose that the program has been passed the values 5, 5, and 10. These values are converted into a values array of 0.0, 5.0, 5.0, and 10.0. The for loop converts these values into plotValues of 0, 90, 180, and 360 degree angles. As you can see, in this case we will want to draw the first pie slice from 0 to 90 degrees, the second slice from 90 to 180 degrees, and the last from 180 to 360 degrees.

These angles are converted into a series of points by using this equation:

x = radius * sin(angle)
y = radius * cosin(angle)


Here, radius is the radius of the pie chart. (The radius of the pie chart is the distance to the middle of the bounding square.) This formula converts the angle into an x and y value on the circumference of a circle.

Finally, these x and y values are translated to the middle of the bounding square. This is done by adding radius to both values before converting them into a Point object and storing the values in the points array. The x and y coordinates now vary from 0 to two times radius, the edges of the bounding square, rather than varying from -radius to radius.

A unique Brush object is calculated for each pie slice by using the index i as a lookup into the array of possible styles called brushStyle, and the array of colors called color.

The Form1_paint() method is fairly simple. This method iterates through each of the pie points. First it sets the drawing brush for the current pie slice by calling Graphics.setBrush(). It then calls Graphics.drawPie() to draw a pie slice within the bounding square from the first pie point, points[i], to the next pie point, points[i + 1].

The Form1_paint() method labels each slice with its value from the values array. It uses the crude algorithm of drawing the value midway between the two pie points. This algorithm works fairly well when there are four or more values, especially when these values are roughly equal.

The resize event handler, Form1_resize(), is also simple to implement. First it recalculates the size of the bounding square by calling calculatePieBox(). Next the method calls calculatePieChart() to recalculate a new pie chart that fits within the new bounding square. The final call to invalidate() forces the new pie chart to be painted.

One final method, getMinTrackSize(), overrides Component.getMinTrackSize() to return the minimum size allowed for the form window. In this case, the form can't be shrunk to a size smaller than 100 x 100 pixels.

result

To execute the program from Visual J Plus Plus, you must provide the program with an argument list. To do so, from the Project menu select PieChart Properties. On the Launch tab, select Custom and add the desired values to the Arguments box.

When executed with the values shown in Figure 9-10, the program generates the pie chart shown in Figure 9-11.

Java Click to view at full size.

Screenshot-10. The Project Properties dialog box showing the argument values.

Screenshot

Screenshot-11. The pie chart resulting from the values shown in Figure 9-10. Comments