Chapter 5

Adding Graphs and Scrollbars to the Spreadsheet


CONTENTS


In the previous two chapters, you have developed a spreadsheet applet that supports general spreadsheet functionality, dialog boxes for setting fonts and colors, and saving and opening files. Along the way, many aspects of the AWT package have been discussed, from basic components to layouts to methods of the Graphics class to the AWT Toolkit.

With all these tools at hand, a few last touches can now be added to the applet. Scrollbars are added to the spreadsheet, as is support for marking cells. The latter will be used to generate the final feature of the spreadsheet applet, the runtime generation of graphs.

Since this chapter builds on the AWT tools explored in the earlier chapters, it will go straight into the tutorial. Any new AWT features that aren't covered in this exposition will be explained as part of the tutorial.

Tutorial

In this tutorial, you'll enhance the spreadsheet applet from the previous chapter to support scrollbars, cell marking, and dynamic graphs. Some new classes have been written to create the graphs and the supporting dialogs, and some classes from the previous section have been modified to support new functions.

Class Organization

Table 5.1 lists the classes used in this chapter's version of the spreadsheet applet. Since many of these classes were created in the previous chapter, the new classes have their names in boldface type; the classes that were modified have their names italicized.

Table 5.1. Spreadsheet classes.

ClassDescription
CellContains a String and evaluated value corresponding to a single cell.
CellContainerContains a matrix of Cells. The String in each Cell is evaluated according to whether it is a formula, a literal numeric value, or an empty cell. Can write or read its contents from a file.
FormulaParserUsed to parse out the individual string fragments that make up a single formula. Converts literal strings to their numeric values.
FormulaParserExceptionAn Exception that is thrown if a formula is poorly constructed.
ArgValueA FormulaParser helper class used to store information about an argument in a formula.
ChooseColorDialogDialog box for selecting the color of a spreadsheet element.
colorDisplayA canvas class that ChooseColorDialog uses to display what a spreadsheet cell would look like if certain colors were picked.
ColoredCheckboxDraws a checkbox with its colors set to a different color.
ColoredCheckboxGroupCreates a group of colored checkboxes.
ChooseFontDialogDialog box for selecting a font for the spreadsheet.
fontDisplayA canvas class that ChooseFontDialog uses to display what a font looks like.
GraphCanvasA canvas class that actually paints a graph.
GraphDataAn accessor class for holding data needed for drawing graphs.
GraphDialogDialog box that brings up the GraphCanvas that displays the requested graph.
SpreadsheetCellProvides for the visual presentation of a single Cell.
SpreadsheetContainerManages the visual presentation of a matrix of SpreadsheetCells. Provides an interface for changing the value of a cell. Supports the display of a currently highlighted cell. Sets fonts and colors of SpreadsheetCells.
SpreadsheetFrameProvides the main presentation of the spreadsheet by displaying the SpreadsheetContainer, managing mouse selections on that spreadsheet, reading a text field for changing cell values, and handling a menu for actions such as invoking dialog boxes.
SpreadsheetAppletResponsible for creating, showing, and hiding the SpreadsheetFrame that provides this applet's visual display.

Adding Scrollbars

As the spreadsheet applet stood at the end of Chapter 4, "Enhancing the Spreadsheet Applet," you couldn't look at any of the cells in the spreadsheet that didn't fit onscreen. This severely reduced the applet's usefulness, but this problem can be fixed by adding scrollbars. With horizontal and vertical scrollbars, you can scroll to the cell you want to view; therefore, the spreadsheet's full contents can be used.

Adding the Scrollbar Class

The AWT Scrollbar class is used to create vertical or horizontal scrollbars. A box at the center of the scrollbar represents the scrollbar's current position; this box has been called the thumbtack, the thumb, or elevator, but in this discussion "thumbtack" will be used. The thumbtack can be moved in line increments (when the scrollbar arrows are pressed) or in page increments (when the scrollbar area between the arrows is clicked). The thumbtack can also be dragged to a specific position.

A Scrollbar object can be created through a variety of constructors. The most useful constructor is one that takes five integer parameters. A good way to illustrate this constructor is to look at how the vertical and horizontal scrollbars are declared and created in the SpreadsheetFrame:

Scrollbar vScroll; // The vertical scrollbar
Scrollbar hScroll;  // The horizontal scrollbar
vScroll = new Scrollbar(Scrollbar.VERTICAL,1,1,1,numRows);
hScroll = new Scrollbar(Scrollbar.HORIZONTAL,1,1,1,numCols);

The first parameter refers to the scrollbar's orientation, which can be defined by using either the Scrollbar.HORIZONTAL or Scrollbar.VERTICAL constant. The former is a scrollbar set horizontally across the native container; the latter is displayed up the side. Figure 5.1 shows what the spreadsheet applet looks like after the scrollbars are added. The orientation of the scrollbar can be retrieved through the getOrientation() method.

Figure 5.1 : The spreadsheet applet with scrollbars.

The next parameter specifies the initial value of the scrollbar. This must be between the minimum and maximum scrollbar values, specified in the last two parameters, respectively. In the case of the spreadsheet applet, the maximum value is the number of rows or columns supported by the spreadsheet. These are set at 26 and 40 in the SpreadsheetApplet class. The scrollbar's initial value of 1 indicates that the display begins at the top row and leftmost column of the spreadsheet.

The fourth parameter indicates the range of values that should be traversed when you click on the paging portion of the scrollbar. This corresponds to the visible portion of the scrollbar area. For example, if 10 rows are currently displayed on the screen, then the range is 10. This range is also known as the page increment and can be set by the setPageIncrement() method. Since it is usually possible to resize a window that has scrollbars, you will need to modify the page increment on-the-fly. This can involve somewhat lengthy code, so it isn't used in the spreadsheet applet. There also seems to be a problem with the setPageIncrement() method in the version of AWT available at this time. Consequently, only the line increment and thumbtack drag aspects of scrollbars are set up for the spreadsheet applet.

Recall that the SpreadsheetFrame class uses GridBagLayout as its layout mechanism. How the scrollbars are added to the frame display provides another interesting example of using the complex GridBagLayout class. See this book's CD-ROM to find out how the scrollbars are added to the frame's GridBagLayout instance; this is defined in the file SpreadsheetApplet.java.

Handling Scrollbar Events

The next step in creating and using scrollbars is to catch events initiated by scrollbar actions. Five constants in the Event class are allocated for handling scrollbar events. These are listed in Table 5.2. The following code shows how the SpreadsheetFrame class traps these events:

public boolean handleEvent(Event evt) {
 switch(evt.id) {
case Event.SCROLL_LINE_UP:
         case Event.SCROLL_ABSOLUTE:
         case Event.SCROLL_LINE_DOWN:
         case Event.SCROLL_PAGE_UP:
         case Event.SCROLL_PAGE_DOWN:
           if (evt.target instanceof Scrollbar) {
               scrollbarAction((Scrollbar)evt.target);
            }  // end if
            return true;

Table 5.2. Scrollbar events.

EventDescription
EVENT.SCROLL_LINE_UP User hit the line-up arrow.
EVENT.SCROLL_LINE_DOWN User hit the line-down arrow.
EVENT.SCROLL_PAGE_UP User hit the page-up arrow.
EVENT.SCROLL_PAGE_DOWN User hit the page-down arrow.
EVENT.SCROLL_ABSOLUTE User moved the thumbtack.

The event-handling code calls the scrollbarAction() method, which goes as follows:

// Handle scroll bar actions...
   void scrollbarAction(Scrollbar sb) {
      if (sb.getOrientation() == Scrollbar.VERTICAL)
         s.setNewTop(sb.getValue());
      else
         s.setNewLeft(sb.getValue());
   }

It checks to see whether a horizontal or vertical scrollbar has been selected by using the getOrientation() method of the target and comparing it to the Scrollbar constant value. The code then gets the current location of the thumbtack with the getValue() method. This returns a value between the minimum and maximum. If it is the minimum value, then the thumbtack is at the top or leftmost position of the scrollbar. The maximum value represents the bottom or rightmost position. This value is set to the appropriate method of the SpreadsheetContainer class.

This class is modified to support scrollbars by introducing two variables that indicate the top row and leftmost column to be displayed. When the scrollbar scrolls upward or downward, the display's top row is adjusted in the corresponding direction. Likewise, moving the scrollbar left or right adjusts the column of the spreadsheet drawn on the display's leftmost portion. The introduction of the topCell and leftCell integer variables are used to track where the row and column display begins.

Listing 5.1 provides the two SpreadsheetContainer methods called by the scrollbarAction() method shown above. These set the top row or leftmost column to be displayed. The SpreadsheetContainer then repaints itself, displaying the new selections. The following code shows how this display works.


Listing 5.1. Setting the top row and leftmost column of the SpreadsheetContainer object.
// Handle scrolling by setting new top...
   public void setNewTop(int newTop) {
      // Set top, taking into account headings and constraints...
      if (newTop < numRows)
         topCell = newTop;
      else
         topCell = numRows - 1;
      resetMarking();
      repaint();
   }

   // Set new leftmost column...
   public void  setNewLeft(int newLeft) {
      // Set new left, taking into account headings and constraints...
      if (newLeft < numColumns)
         leftCell = newLeft;
      else
         leftCell = numColumns - 1;
      resetMarking();
      repaint();
   }

Inside the SpreadsheetContainer Paint Methods

Listing 5.2 shows the code involved in painting the SpreadsheetContainer. These routines paint the row and column headers on the sides, then fill up the interior with the evaluated values of the SpreadsheetCell objects that fit in the painted region. This painting has as its origin the topCell variable as the top row to display and the leftCell variable as the first column of SpreadsheetCell objects to be displayed.


Listing 5.2. SpreadsheetContainer paint methods.
// Update message sent when repainting is needed...
   // Prevent paint from getting cleared out...
   public void update(Graphics g) {
      paint(g);
   }

// Draw the displayable spreadsheet contents...
public synchronized void paint (Graphics g) {
// Go through the calculations of the paint while painting...
calculatePaint(g,true,0,0);
}

// This goes through the motions of calculating what is on the
// screen and either calculates coordinates or paints...
// If it is not paint, returns cell that fits in hit region...
SpreadsheetCell calculatePaint(Graphics g,boolean bPaint,int xHit,int yHit) {
// Get the current size of the display area...
Dimension dm = size();
Rectangle r = null;  //   The clipping rectangle...
Rectangle cellRect = null; // The cell clipping rectangle...
// Calculate the cell width and height
// Cell should be wide enough to show 8 digits...
if (bPaint == true) {
  cellWidth = g.getFontMetrics().stringWidth("12345.67");
  cellHeight = g.getFontMetrics().getHeight();
  r = g.getClipRect();
} // end if
// Figure out how many rows and cols can be displayed
int nCol = Math.min(numColumns,dm.width / cellWidth);
int nRow = Math.min((numRows + 1),dm.height / cellHeight);

// Draw the cells...
int index,i,x,j,y,currentRow,currentCol;
-nRow;
// Go across the rows...
// Show the headers and adjust for top and left cell...
for (currentRow = i = 0; i < nRow; ++i) {
  y = cellHeight + (i * cellHeight);
  // Go across the columns...
  for (currentCol = j = 0; j < nCol; ++j) {
   index = (currentRow * numColumns) + currentCol;
   x = (j * cellWidth);
   // Paint if told to...
   if (bPaint == true) {
                 // See if it is in the intersection of the
                 // clipping rectangle
                 cellRect = new Rectangle(x,y,cellWidth,cellHeight);
                 if (r.intersects(cellRect)) {
                   // Paint if you are at a valid row...
                   if ((currentRow < numRows) && (currentCol < numColumns)) {
                    matrix[index].paint(g,x,y,
                     cellWidth,cellHeight);
                   } // end inner if
                   else {  // Otherwise, fill it in with grey...
                     emptyCell.paint(g,x,y,cellWidth,cellHeight);
                   } // end else
                 } // end if
  } // end if
  else { // Otherwise, see if cell fits for highlight calculations…
   if ((currentRow < numRows) && (currentCol < numColumns)) {
     // See if it fits in the column...
     if ((xHit >= x) && (xHit < (x + cellWidth))) {
          // See if it fits in the row...
          if ((yHit >= y) && (yHit < (y + cellHeight))) {
                        paintIndex = index;
                        paintX = x;
                        paintY = y;
                        return matrix[index];
          } // end if
     } // end if
   } // end inner if
  } // end else
  // Adjust column display of cells
  if (j == 0)
        currentCol = leftCell;
  else
        ++currentCol;
}  // end column for
// Now start data cells at appropriate top...
if (i == 0)
       currentRow = topCell;
else
      ++currentRow;
} // end row for
return null;  // Only used if paint is false...
}

The painting begins with the update() method. Recall that the earlier versions of the spreadsheet applet have a brief flicker of white when the spreadsheet area is repainted. This is because the update() method, by default, clears out the area before the paint() area is called; this clearing results in the flicker, which can be solved by overriding the update() method. In this case, it calls the paint() method directly. This method, in turn, calls the calculatePaint() method.

The calculatePaint() method is at the heart of the SpreadsheetContainer class. It is not only used for painting, but also is called by other routines to calculate where a cell is located. The second parameter of calculatePaint() indicates whether the method should repaint the region or simply calculate a value. The first part of the method calculates how many rows and columns of SpreadsheetCells can be displayed in the current canvas area. The size() method returns the area's size, and the getFontMetrics() methods are used to calculate the cell dimensions. Note that the dimensions will change when the font of the spreadsheet is resized, as discussed in the previous chapter's section on fonts.

The next step is to loop through all the rows and columns to be displayed. The currentRow and currentCol variables indicate which SpreadsheetCell is to be painted. These values are initially set to 0 so that the row and column headers are displayed. At the bottom of the for loops, these are adjusted to the top cell or leftmost column. In the ensuing iterations, the variables are simply incremented to get the new row or column.

If the mode of the calculatePaint() method is true, then the cells are to be painted. In this case, the code checks to see whether the item falls in the clipping rectangle, retrieved by the getClipRect() method. This actually represents the area to be painted. The clipping rectangle could be the whole screen or a single cell. The code that sets a highlighted cell or marks an area (to be discussed) only forces the area affected to be repainted. If it does fall in the region, the individual SpreadsheetCell is painted at the current coordinates. Recall that the cell has colors and a font associated with it. Highlighting or marking a cell is really nothing more than changing a SpreadsheetCell object's internal color variables and forcing it to be repainted. See the Chapter 3, "Building a Spreadsheet Applet," tutorial for how the SpreadsheetCell class works.

Painting affects only those objects that fall in the clipping rectangle. The intersects() method is used to see whether two Rectangle objects overlap. By painting only the cells that intersect the clipping region, the code saves some unnecessary processing.

If the method is not in painting mode, then the processing is used to check which cell an x-y coordinate belongs to. This is useful when the user clicks on a cell to be highlighted and when marking occurs.

By using a central routine for determining the locations of SpreadsheetCells, such activities as painting, scrolling, marking, and highlighting become relatively easy. If something goes wrong, it probably could be traced back to the calculatePaint() method.

Marking Cells

It is useful to mark cells to indicate that they are subject to some upcoming operation, such as cutting or copying, or, in this case, producing a graph. Marking is performed in this applet by clicking on a valid cell, holding the mouse key down, and dragging in a southeasterly direction. When the mouse is released, marking is complete. Figure 5.2 shows what a spreadsheet with some cells marked looks like.

Figure 5.2 : A spreadsheet with cells marked.

Marking cells is actually a simple extension of the techniques that have been developed through the last three chapters. There are three key aspects to marking: tracking mouse movements, keeping track of the cells selected, and changing the color of the marked cells.

To track mouse movements, the SpreadsheetContainer handleEvent() method is modified to manage the mouse actions. The modified code is detailed in Listing 5.3.


Listing 5.3. SpreadsheetContainer event handler.
public boolean handleEvent(Event evt) {
 switch(evt.id) {
  // Mouse clicks. See whether you should highlight
  // cell on spreadsheet...
  case Event.MOUSE_DOWN:
    setMouseHighlight(evt.x,evt.y);
    // Handle cell marking...
    toggleMarking(evt.x,evt.y);
    return false;
 case Event.MOUSE_DRAG:
  // Select cells if marking...
  dragMarking(evt.x,evt.y);
  return false;
 case Event.MOUSE_UP:
  // If marking, then you are done!
  stopMarking(evt.x,evt.y);
  return false;
 }
 default:
  return false;
 }
}

The marking code is too long and involved to list here; refer to the CD-ROM for the complete code. However, the general approach and highlights can be presented.

When you click the mouse over a valid cell (not a row or column header), the cell becomes the current focus and is highlighted. This is done through the setMouseHighlight() method, which calls calculatePaint() to find the reference of the selected SpreadsheetCell. After the cell is highlighted, the toggleMarking() method is called to either begin marking or to reset the marking if there were cells selected at the time of the mouse click. The startMarking() method begins tracking the mouse, and resetMarking() resets the marking state. SpreadsheetCell objects are set to their default colors through the setNewColors() method, which is used to set the normal and highlighted values of the spreadsheet. This method is called by the color dialog discussed in the previous chapter, and is called here with the current normal and highlighted colors as a simple way of resetting colors.

As the mouse is being dragged (which means the mouse key is still down), MOUSE_DRAG events are issued. If marking is on, the dragMarking() method checks to see whether the mouse has left the current cell; if it has, then new cells are marked. The paintMarked() method looks at the first cell marked and the last cell marked and figures out the rectangular area to be painted. It then sets the colors of each of the cells that are marked. The method then forces a limited repaint of the SpreadsheetContainer so that painting occurs quickly. The following code from paintMarked() takes the top-left and bottom-right areas that are marked and forces them to be repainted:

repaint(startX,startY,endX-startX,endY-startY);

When the paint message is processed, the marked cells are drawn in their new color. Cells outside the clipping rectangle issued by the repaint() method are not drawn.

When the mouse is released, the stopMarking() method is called, and the marking is finished. The marked areas can then be used for further operations.

Drawing Graphs

The last feature of the spreadsheet applet to be developed is drawing graphs from the marked cells. Most of the work is done in the graphCanvas() class. However, the graph is displayed inside a dialog object based on the GraphDialog class.

Two spreadsheet applets can produce two types of graphs: a line graph and a bar chart. The latter is shown in Figure 5.3. They are invoked by new menu items off the SpreadsheetFrame object. The menu items result in a call to the SpreadsheetFrame method launchGraphicsDialog(), which gets the currently marked data and then displays the graphics dialog box (which was instantiated in the frame's constructor). If nothing is marked, an error message in the status bar indicates that nothing was drawn.

Figure 5.3 : Spreadsheet applet bar chart.

The GraphData class is an accessor class used to store the data to be displayed. It contains an array of double values representing the data to be plotted and an array of Strings used as the column labels. These arrays are prepared in the SpreadsheetContainer method getGraphData(), which is called by the frame's launchGraphicsDialog() method just discussed. The getGraphData() checks to see whether something is marked; if not, an IllegalArgumentException object is thrown. Otherwise, it stores the marked data in the GraphData object by moving across the columns. The bottom row marked is used to generate the data to be plotted and is placed in the double array. The top row is used to represent the column String headers. If only one row is marked, the cell headers (A1, A2, and so forth) are used as the column headers. The middle rows that are marked are ignored. As a further exercise, you can modify the graph-
plotting algorithms to support multiple rows of data.

Once the graph data is collected, the GraphDialog is displayed. The dialog box is very simple, consisting of only two components: the GraphCanvas object for drawing the graph and a button for shutting down the dialog box. The only interesting code in the dialog box involves resizing the dialog box to take up most of the screen.

// Get the screen dimensions...
Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
// Get the font and use to calculate some margins...
FontMetrics fm = Toolkit.getDefaultToolkit().getFontMetrics(getFont());
resize(screen.width,screen.height - (4 * fm.getHeight()) );

This code uses the AWT Toolkit to get the screen size and the current font. The dialog box then resizes itself to take up the screen's full width and most of its height.

When the dialog box is shown, it first calls the GraphCanvas prepareNewGraph() method, which takes the GraphData and prepares it for presentation. When the dialog box appears and a paint event is issued, the GraphCanvas object draws the graph.

Listing 5.4 gives the full code for the GraphCanvas class. It uses two constants to indicate the two graph modes it supports: line or bar graph. When the canvas is constructed, it creates an array of six Color objects. This array is used to randomly generate colors for each bar chart or line so that they are more easily distinguished. When the prepareNewGraph() method is called, which must happen before painting occurs, the maximum value is calculated and stored in the maxValue variable. This is an important figure since it represents the range of values the data is plotted against. The maximum value is not the highest value in the data area, but actually is the highest value rounded up to the nearest power of 10. Therefore, if the top value is 66, the maximum value will be set to 100. Doing this makes it easy to divide the range of data values into intervals of one-tenth the maximum. The data in Figure 5.3 shows the plotting of values when the highest data value is 66 and the range is set from 0 to 100.


Listing 5.4. The GraphCanvas class.
// Not a public class.  Imports are in GraphDialog class
// This paints a graph on a canvas...
class GraphCanvas extends Canvas {
    // Types of graphs...
   static final int LINE_GRAPH = 0;
   static final int BAR_GRAPH = 1;
   int graphMode;
   Color randomColors[];
   // This is the data for the display..
   GraphData gd;
   // Maximum scale of graph...
   int maxValue;

   // Constructor just inits data...
   public GraphCanvas() {
      gd = null;
      // Store the random colors...
      randomColors = new Color[6];
      randomColors[0] = Color.yellow;
      randomColors[1] = Color.red;
      randomColors[2] = Color.green;
      randomColors[3] = Color.magenta;
      randomColors[4] = Color.cyan;
      randomColors[5] = Color.blue;
   }
   // Set up graphics display...
   void prepareNewGraph(GraphData gphData,int graphMode) {
      // Store the data and string values...
      gd = gphData;
      this.graphMode = graphMode;
      // First calculate maximum value of graph...
      maxValue = calculateMaxValue();
   }

   // Calculate the maximum value of the graph...
   int calculateMaxValue() {
      double maximum = 0.0;
      double data;
      int temp,roundMax;
      // First get maximum figure...
      int length = gd.size();
      for (int i = 0; i < length; ++i) {
         data = gd.getData(i);
         if (data > maximum)
            maximum = data;
      } // end for
      // Now round it up to nearest power of 10
      roundMax = 1;
      for (temp = (int)maximum;temp > 0; temp /= 10)
         roundMax *= 10;
      return roundMax;
   }


   //  Draw the graph...
   public synchronized void paint (Graphics g) {
      if (gd == null)
         return;
      Dimension dm = size();

      // Calculate margins...
      int height = g.getFontMetrics().getHeight();
      int ymargin = 3 * height;
      int xmargin = g.getFontMetrics().stringWidth("1112345.67");
      int length = gd.size();

      // Select bottom-left origin
      Point origin = new Point(xmargin,dm.height - ymargin);

      // Draw X-Axis line
      int endx = dm.width - xmargin;
      g.drawLine(origin.x,origin.y,endx,origin.y);

      // Draw Y-Axis line
      g.drawLine(origin.x,ymargin,origin.x,origin.y);

      // Calculate how headers are spread out...
      int yIncrement = (origin.y - ymargin)/10;
      int xIncrement = (endx - origin.x) / (length + 1);

      // Draw horizontal axis headers
      int i,x;
      int yMarkStart = origin.y + height;
      int yTextStart = yMarkStart + height;
      for (i = 1; i < (length + 1); ++i) {
         // Draw marker...
         x = origin.x + (xIncrement * i);
         g.drawLine(x,yMarkStart,x,origin.y);
         // Print value header...
         g.drawString(gd.getHeading(i - 1),x,yTextStart);
      }

      // Draw vertical axis headers...
      int y;
      int inset = g.getFontMetrics().charWidth('W');
      int xMarkStart = origin.x - inset;
      int xTextStart = inset;
      int dataIncrement = maxValue / 10;
      String yHeader;
      for (i = 0; i <= 10; ++i) {
         // Draw marker...
         y = origin.y - (i * yIncrement);
         g.drawLine(xMarkStart,y,origin.x,y);
         // Print increment header...
         yHeader = String.valueOf(dataIncrement * i);
         g.drawString(yHeader,xTextStart,y);
      }

      // Call Graphic specific drawing..
      int vertLength = origin.y - ymargin;
      double dbLen = (double)randomColors.length;
      int index;
      int rectOffset = xIncrement / 2; // For bar graphs...
      Point lastPt = null;
      for (i = 1; i < (length + 1); ++i) {
         // Plot points, connecting points with lines...
         x = origin.x + (xIncrement * i);
         y = origin.y - (int)((gd.getData(i - 1)/maxValue) * vertLength);
         // Randomize colors...
         index = (int)(dbLen * Math.random());
         g.setColor(randomColors[index]);
         // If line graph, draw connecting lines...
         if (graphMode == LINE_GRAPH) {
              if (lastPt != null)
                  g.drawLine(lastPt.x,lastPt.y,x,y);
              lastPt = new Point(x,y);
         }
         // Otherwise, bar graph draw rectangle...
         else {
              g.fillRect(x - rectOffset,y,xIncrement,origin.y - y);
         }
      } // end for
   }
}

After the maximum value is determined, the graph is ready to be painted. This is surprisingly easy. The first step is to get the dimensions of the canvas area and to figure where the bottom-left origin of the graph should be; this is where the vertical line that marks the range of values and the horizontal line that provides the column headers will meet. The margins allow room at the sides and the bottom for displaying the range and column headers. These are calculated through the use of the FontMetrics class's getHeight() and stringWidth() methods. These margins make it easy to calculate the operations that follow. The next step is drawing the vertical and horizontal lines from the origin to the appropriate margin.

The column headers are next drawn underneath the horizontal line. The location of the headers is based on the length of the line divided by the number of columns. The horizontal range of values is calculated similarly, except that the locations are based on dividing the maximum value by ten and incrementing accordingly. The range values and headers are drawn through the drawString() method. The nearby drawLine() methods are used to make a small line marker indicating the range or header position.

The last step is to plot the graph. This is done in a for loop that moves across each column of data. For each column, a random color is chosen for the ensuing graph figure. This is done by using the Math.random() method, which returns a number between 0.0 and 1.0. The color is then set by calling the Graphics object's setColor() method. The data is then plotted based on its position in relation to the origin and the maximum value. If it is a line graph, a line is drawn from the endpoint of the last line drawn to the new value; the drawLine() method is used to do this. If it is a bar graph, a rectangle is drawn from the horizontal baseline to the plotted value with the fillRect() method of the Graphics class. Both the line and the interior of the bar rectangle will be the color just set by the random procedure.

Summary

This concludes the development of the spreadsheet applet. By going through this part of the book, you have learned most of the fundamental techniques needed to use the AWT package. You have also been exposed to exception handling, as well as the underlying principles of using input/output streams.

The next step is to tie these techniques in with some of Java's more advanced features, such as multithreading and sockets programming. With a good understanding of these new techniques, you can produce a network-enabled applet ready for prime-time use on the Internet!