Solution for
Programming Exercise 8.5
THIS PAGE DISCUSSES ONE POSSIBLE SOLUTION to the following exercise from this on-line Java textbook.
Exercise 8.5: To do this exercise, you need to know the material on components and layouts from Chapter 7. Write an applet that draws pie charts based on data entered by the user. A pie chart is a circle divided into colored wedges. Each wedge of the pie corresponds to one number in an array of positive numbers. The number of degrees in the wedge is proportional to the corresponding number. If g is a variable of type Graphics, then a wedge can be drawn with the command
g.fillArc(left, top, width, height, startAngle, degrees);All the parameters in this command are integers. The first four parameters give the left edge, top edge, width, and height of a rectangle containing the "pie". For a circle, the width and height must be the same. The fifth parameter tells the starting angle of the wedge, and the sixth tells the number of degrees in the wedge.
Your applet should include 12 TextFields where the user can enter the data for the pie chart. You will need an array of type TextField[] to keep track of these input boxes. Section 7.4 has an example that shows how to get a number of type double from an input box. You should ignore any input boxes that are empty. Use the data from the non-empty input boxes for the pie chart.
The applet should have a button that the user clicks to draw the pie chart. And it should have a sub-class of the Canvas class that will display the pie chart. I suggest that this class include an instance variable of type int[] to store the data needed for the pie chart. The data that you need is the angles of the dividing lines between the wedges. This is not the same as the data from the input boxes, but it can be computed from that data. Suppose the user's data is data[0], data[1], ..., data[dataCt-1]. Let dataSum be the sum of all the user's data, data[0] + data[1] + ... + data[dataCt-1]. Let sum be the sum of the first i data values, data[0] + data[1] + ... + data[i-1]. Then the angle for the i-th dividing line in the pie chart is given by "(int)(360*sum/dataSum + 0.5)". (The "360" is there because it represents the number of degrees in a full circle. The "+0.5" is there so the answer will be rounded to the nearest integer rather than truncated downwards.)
Here is my solution:
Discussion
This exercise needs three arrays: An array of type TextField[] to hold references to the text input boxes, an array of type int[] to hold the angle data for the pie chart, and an array of type double[] that holds the data values from the text input boxes. The double[] array is created when the user clicks the "Make Chart" button, and it is passed to a subroutine in the canvas class where it is used to create the array of angles for the pie chart. My solution also uses a fourth array, of type Color[], to hold the colors to be used in the pie chart.
Let's consider the canvas class, PieChartCanvas, first. This class includes an instance variable, angles, of type int[] that holds the angles of the lines between the wedges in the pie chart. Another instance variable, dataCt, if type int, tells how many wedges there are in the pie. When there is no data available, dataCt is zero. An array of twelve colors to use for the pie chart is stored as a static final variable, palette, of type Color[]. This variable is initialized as an array of 12 colors. The color of the i-th wedge of the pie chart will be palette[i].
The data in these arrays is used in the canvas's paint() method. The i-th wedge of the pie is drawn with the command
g.fillArc( left, top, diameter, diameter, angles[i], angles[i+1] - angles[i] );where the values of left, top, and diameter are calculated to produce a circle that is centered on the canvas and is inset 20 pixels from the nearest edge of the canvas. The starting angle for the i-th wedge is angles[i]. The number of degrees in the wedge is the difference between the starting angle for this wedge and the starting angle for the next wedge. This is computed as angles[i+1] - angles[i]. There is a potential problem here for the last wedge in the pie. In this case, there is no "next wedge". To make the formula work, I add an extra value, 360, in position number dataCt in the angles array. An angle of 360 degrees represents the ending point for the last wedge. The complete paint() method for the PieChartCanvas class is:
public void paint(Graphics g) { // Draw the pie chart, if there is data available. // If not, just show the message "No data available". if (dataCt == 0) { g.drawString("No data available.", 10,15); return; } /* The pie chart occupies a circle centered on the canvas. Compute some parameters for drawing it. */ int centerX = getSize().width / 2; // Center point of circle. int centerY = getSize().height / 2; int radius; // Radius of the circle. This is 20 pixels less // than the smaller of half the width and // half the height. if (centerX < centerY) radius = centerX - 20; else radius = centerY - 20; int top = centerY - radius; // Top edge of square // that contains the circle. int left = centerX - radius; // Left edge of square // that contains the circle. int diameter = 2*radius; // Length of a side of the square. for (int i = 0; i < dataCt; i++) { // Draw the next wedge. The start angle for the wedge // is angles[i]. The ending angle is angles[i+1}, so // the number of degrees in the wedge is // angles[i+1] - angles[i]. g.setColor( palette[i] ); g.fillArc( left, top, diameter, diameter, angles[i], angles[i+1] - angles[i] ); } } // end paint()The data for the angles array is computed in a method "void setData(double[] data, int count)". This method is called by the main applet class when the user clicks the "Make Chart" button. The numbers from the text input boxes are in the data array, and the number of numbers is indicated by the count parameter. The angles array is made just long enough to hold the dataCt+1 integers that the paint() method needs to draw the pie chart. The values in the array are computed using the formula given in the exercise, "angles[i] = (int)(360*sum/dataSum + 0.5)", for i from 1 to dataCt-1. The dataSum is computed by adding up all the numbers in the data array. The sum is the sum of the first i entries in that array. The values of angles[0] and angles[dataCt] are set to 360. The Java code for the computation is:
dataCt = count; // The number of data. angles = new int[dataCt + 1]; angles[0] = 0; angles[dataCt] = 360; double dataSum = 0; // The sum of all the data values. for (int i = 0; i < dataCt; i++) dataSum += data[i]; double sum = 0; // The sum of the first i data values. for (int i = 1; i < dataCt; i++) { sum += data[i-1]; // Add in next data value. angles[i] = (int)(360*sum/dataSum + 0.5); }
Let's turn to the applet class, PieChartApplet. This applet uses a rather complicated layout, which is set up in the init() method. See that method in the complete solution, below, for the details. Here, I want to discuss how arrays are used in the applet.
The applet class has an instance variable, inputs, of type TextField[], to hold the 12 input boxes. In the init() method, the array is created and is filled with 12 new TextField objects. The TextFields are also added to a panel that uses a GridLayout with six rows and two columns. This panel occupies the upper left section of the applet. All this is done with the code:
Panel left = new Panel(); // Panel to hold 12 input boxes. left.setLayout(new GridLayout(6,2,2,2)); inputs = new TextField[12]; // Make an array to hold the boxes. for (int i = 0; i < 12; i++) { inputs[i] = new TextField(); // Create the i-th input box. inputs[i].setBackground(Color.white); left.add(inputs[i]); // Add the box to the panel. }The applet also includes a method, doChart(), which is called by the actionPerformed() method when the user clicks on the "Make Chart" button. This method must get the numbers from the input boxes, store the data in an array, and call the canvas class's setData() method to display a pie chart with the new data. We have to allow for the possibility that some of the input boxes are empty. They should be ignored. As we go through the boxes extracting their contents, we have to keep track of the number of non-empty boxes that we have found. If there were no possibility of error, this could be done with the following Java code:
double[] data = new double[12]; int dataCt = 0; for (int i = 0; i < 12; i++) { // Get the data, if any, out of the i-th input box // and put it in the next available spot in the // data array. If the box is empty, ignore it. String str = inputs[i].getText(); // Get contents of i-th box. if (str.length() > 0) { Double d = new Double(str); // Convert str to a Double object. data[dataCt] = d.doubleValue(); // Get the numerical value. dataCt++; // Count this data value. } } if (dataCt > 0) { // Tell the canvas, chart, to draw a pie chart with // the new data. (But only do this if we actually // have some data.) chart.setData(data,dataCt); }But the process is complicated quite a bit, since we have to check for various types of errors. See the doChart() routine in the complete solution, below, for details.
The Solution
/* This applet draws "pie charts" with up to 12 wedges. The data for the pie chart is entered in input boxes by the user. (Each wedge of a pie chart corresponds to one data value. The number of degrees in the wedge is proportional to the data value.) The user can leave an input box empty, but if there is anything in it, it must contain a valid positive real number. */ import java.awt.*; import java.awt.event.*; import java.applet.Applet; public class PieChartApplet extends Applet implements ActionListener { PieChartCanvas chart; // A canvas that displays the pie chart. TextField[] inputs; // Input boxes where the user enters the data. Label message; // Label for displaying message to the user. public void init() { // Initialize the applet. setBackground(Color.darkGray); // Color for borders and gaps. setLayout(new BorderLayout(2,2)); // The main layout is a BorderLayout. /* The South position of the main BorderLayout is occupied by a Panel that contains the message label and two buttons. This panel also uses a BorderLayout. The message label occupies the Center position and the Buttons occupy the East and West positions. The applet listens for ActionEvents from the buttons. */ Panel bottom = new Panel(); add(bottom, BorderLayout.SOUTH); bottom.setLayout(new BorderLayout(2,2)); message = new Label("Enter your data and click here: ", Label.CENTER); message.setBackground(Color.lightGray); bottom.add(message, BorderLayout.CENTER); Button make = new Button("Make Chart"); make.addActionListener(this); make.setBackground(Color.lightGray); bottom.add(make, BorderLayout.EAST); Button clear = new Button("Clear"); clear.addActionListener(this); clear.setBackground(Color.lightGray); bottom.add(clear, BorderLayout.WEST); /* The Center position of the main BorderLayout is occupied by a Panel that contains the input boxes and the PieChartCanvas. This Panel uses a GridLayout that divides it horizontally into two pieces. The left half is occupied by another Panel that holds the input boxes. The right half is occupied by the chart. */ Panel top = new Panel(); add(top, BorderLayout.CENTER); top.setLayout(new GridLayout(1,2,2,2)); Panel left = new Panel(); // Panel to hold 12 input boxes in a grid. top.add(left); left.setLayout(new GridLayout(6,2,2,2)); inputs = new TextField[12]; // Make an array to hold the input boxes. for (int i = 0; i < 12; i++) { inputs[i] = new TextField(); // Create the i-th input box. inputs[i].setBackground(Color.white); left.add(inputs[i]); } chart = new PieChartCanvas(); top.add(chart); } // add init(); public Insets getInsets() { // Specify a 2-pixel border around the edges of the applet. return new Insets(2,2,2,2); } public void actionPerformed(ActionEvent evt) { // This is called when the user clicks on one of the two buttons. // Call another subroutine to handle the action. String cmd = evt.getActionCommand(); if (cmd.equals("Clear")) doClear(); else if (cmd.equals("Make Chart")) doChart(); } void doClear() { // Called by actionPerformed() when the user clicks the "Clear" // button. Empty all the TextFields, and clear the pie chart. // Reset the message label, in case it was showing an error message. chart.clearData(); message.setText("Enter your data and click here: "); for (int i = 0; i < 12; i++) inputs[i].setText(""); inputs[0].requestFocus(); } void doChart() { // Called by actionPerformedI() when the user clicks the "Make Chart" // button. Get up to 12 data values form the 12 input boxes. Ignore // any empty boxes and zeros. If illegal data is found, show a message // and exit. Otherwise, tell the PieChartCanvas to draw a pie chart // for the data. (If an error is found, clear the current pie chart // so the user will notice that something is wrong.) double[] data = new double[12]; // An array to hold the data. int dataCt = 0; // Number of data items found (ignoring empty boxes). for (int i = 0; i < 12; i++) { // Try to get data from the i-th input box. String numStr; // The contents of the i-th box, as a string. double num; // The contents of the i-th box, as a number. numStr = inputs[i].getText().trim(); if (numStr.length() > 0) { try { // Try to convert the string to a numerical value. Double d = new Double(numStr); num = d.doubleValue(); } catch (NumberFormatException e) { // The data in box i is not a legal number. message.setText("Invalid number input " + i); inputs[i].selectAll(); inputs[i].requestFocus(); chart.clearData(); return; } if (num < 0) { // The data in box i is a negative number. message.setText("Negative numbers not allowed."); inputs[i].selectAll(); inputs[i].requestFocus(); chart.clearData(); return; } if (num > 0) { // Put the number in the data array, but only if it // is positive. Zero values are ignored. data[dataCt] = num; dataCt++; } } // end if } // end for if (dataCt == 0) { // No positive numbers were found. message.setText("Please enter some data!"); inputs[0].requestFocus(); chart.clearData(); return; } /* Reset the message label, in case it was showing an error message. */ message.setText("Enter your data and click here: "); /* Tell the PieChartCanvas to draw a pie chart with the new data. */ chart.setData(data,dataCt); } // end doChart() } // end PieChartApplet class PieChartCanvas extends Canvas { // A PieChartCanvas can display a pie chart, based on an array // of data passed to it in its setData() method. There can be // up to 12 wedges in the pie. private int dataCt; // The number of data values for the chart. private int[] angles; // An array to hold the angles that divide // the wedges. For convenience, this array // is of size dataCt + 1, and it starts with // 0 and ends with 360. private final static Color[] palette = { // Colors for the chart. Color.red, Color.blue, Color.green, Color.magenta, Color.yellow, Color.cyan, new Color(180,0,0), new Color(200,200,255), new Color(0,180,0), new Color(0,180,180), new Color(180,180,0), new Color(180,0,180), }; PieChartCanvas() { // Constructor. Set the background to be white. setBackground(Color.white); } void clearData() { // Delete any data and redraw the canvas. The canvas will // be blank except for the message "No data available". dataCt = 0; angles = null; repaint(); } void setData(double[] data, int count) { // Use data and count for the pie chart. If data is null or // count is <= 0, the current pie chart will be cleared. // The count must be in the range 0 to data.length. If not, it // is set to data.length. All the entries in the array should // be positive numbers. If not, the results will be nonsense. // The data needed to draw the pie chart is computed and stored // in the angles array. Note that the number of degrees // in the i-th wedge is 360*data[i]/dataSum, where dataSum // is the total of all the data values. The cumulative angles // are computed, converted to ints, and stored in angles. if (count <= 0 || data == null) { clearData(); } else { if (count <= data.length) dataCt = count; else dataCt = data.length; angles = new int[dataCt + 1]; angles[0] = 0; angles[dataCt] = 360; double dataSum = 0; for (int i = 0; i < dataCt; i++) dataSum += data[i]; double sum = 0; for (int i = 1; i < dataCt; i++) { sum += data[i-1]; angles[i] = (int)(360*sum/dataSum + 0.5); } repaint(); } } // end setData() public void paint(Graphics g) { // Draw the pie chart, if there is data available. // If not, just show the message "No data available". if (dataCt == 0) { g.drawString("No data available.", 10,15); return; } /* The pie chart occupies a circle centered on the canvas. Compute some parameters for drawing it. */ int centerX = getSize().width / 2; // Center point of circle. int centerY = getSize().height / 2; int radius; // Radius of the circle. This is 20 pixels less // than the smaller of half the width and // half the height. if (centerX < centerY) radius = centerX - 20; else radius = centerY - 20; int top = centerY - radius; // Top edge of square // that contains the circle. int left = centerX - radius; // Left edge of square // that contains the circle. int diameter = 2*radius; // Length of a side of the square. for (int i = 0; i < dataCt; i++) { // Draw the next wedge. The start angle for the wedge // is angles[i]. The ending angle is angles[i+1}, so // the number of degrees in the wedge is // angles[i+1] - angles[i]. g.setColor( palette[i] ); g.fillArc( left, top, diameter, diameter, angles[i], angles[i+1] - angles[i] ) ; } } // end paint() } // end PieChartCanvas
[ Exercises | Chapter Index | Main Index ]