Chart your way to custom graph components

Learn to build a graph framework and custom graph components

Our custom graph components require manual drawing, so we'll need to subclass Canvas, which is the standard component provided for direct graphics manipulation. The technique we're going to use will be to override the paint method of Canvas with the custom drawing that we need. We will use the Graphics object, which is automatically passed into the paint method of all components, to access colors and drawing methods.

We'll create two custom graphing components: a bar chart and a line graph. We'll start by building a general framework class for the two graphs, which share some base elements.

Building a generic graph framework

The line graph and bar chart we're going to build are similar enough that we can create a generic

Graph

class to perform some of the tedious layout work. Once that's done we can then extend the class for the particular kind of graph we need.

The first thing to do when you design custom graphics components is to put pen to paper and draw a picture of what you need. Because we are counting pixels, it is easy to get mixed up about the placement of the elements. Putting some thought into the naming and positioning of elements will help you to keep the code cleaner and easier to read later on.

The line graph and the bar chart use the same layout for the title and lines, so we'll begin by creating a generic graph containing these two features. The layout we're going to create is shown in the figure below.

To create the generic Graph class, we'll subclass Canvas. The center region is where actual graph data will be displayed; we'll leave this to an extension of Graph to implement. We'll implement the other elements -- a title bar, a vertical line to the left, a horizontal line on the bottom, and values for the range -- in the base class. We could specify a font and hard-code the pixel measurements in, but the user would be unable to resize the graph. A better approach is to measure the elements against the current size of the component, so that resizing the application will result in a correct resizing of the graph.

Here's our plan: We'll take a String title, an int minimum value, and an int maximum value in the constructor. These give us all the information we need to lay out the framework. We'll keep four variables for use in subclasses -- the top, bottom, left, and right values for the borders of the graph drawing region. We'll use these variables to calculate positioning of graph items later. Let's begin with a quick look at the Graph class declaration.

import java.awt.*;
import java.util.*;
public class Graph extends Canvas {
   // variables needed
   public int top;
   public int bottom;
   public int left;
   public int right;
   int titleHeight;
   int labelWidth;
   FontMetrics fm;
   int padding = 4;
   String title;
   int min;
   int max;
   Vector items;

To calculate the correct placement of graph elements, we first need to calculate the regions in our generic graph layout that make up the framework. To improve the appearance of the component, we add a 4-pixel padding to the outer edges. We'll add the title centered at the top, taking into account the paddding area. To make sure that the graph is not drawn on top of the text, we need to subtract the height of the text from the title region. We need to do the same thing for the min and max value range labels. The width of this text is stored in the variable labelWidth. We need to keep a reference to the font metrics in order to do the measurements. The items vector is used to keep track of all the individual items that have been added to the Graph component. A class used to hold variables related to graph items is included (and explained) after the Graph class, which is shown next.

   public Graph(String title, int min, int max) {
      this.title = title;
      this.min = min;
      this.max = max;
      items = new Vector();
   } // end constructor

The constructor takes the graph title and the range of values, and we create empty vector for the individual graph items.

   public void reshape(int x, int y, int width, int height) {
      super.reshape(x, y,width, height);
      fm = getFontMetrics(getFont());
      titleHeight = fm.getHeight();
      labelWidth = Math.max(fm.stringWidth(new Integer(min).toString()),
                   fm.stringWidth(new Integer(max).toString())) + 2;
      top = padding + titleHeight;
      bottom = size().height - padding;
      left = padding + labelWidth;
      right = size().width - padding;
   } // end reshape

Note: In JDK 1.1, the reshape method is replaced with public void setBounds(Rectangle r). See the API documentation for details.

We override the reshape method, which is inherited down the chain from the Component class. The reshape method is called when the component is resized and when it is laid out the first time. We use this method to collect measurements, so that they will always be updated if the component is resized. We get the font metrics for the current font and assign the titleHeight variable the maximum height of that font. We get the maximum width of the labels, testing to see which one is bigger and then using that one. The top, bottom, left, and right variables are calculated from the other variables and represent the borders of the center graph drawing region. We'll use these variables in the subclasses of Graph. Note that all of the measurements take into account a current size of the component so that redrawing will be correct at any size or aspect. If we used hard-coded values, the component could not be resized.

Next, we'll draw the framework for the graph.

   public void paint(Graphics g) {
      // draw the title
      fm = getFontMetrics(getFont());
      g.drawString(title, (size().width - fm.stringWidth(title))/2, top);
      // draw the max and min values
      g.drawString(new Integer(min).toString(), padding, bottom);
      g.drawString(new Integer(max).toString(), padding, top + titleHeight);
      // draw the vertical and horizontal lines
      g.drawLine(left, top, left, bottom);
      g.drawLine(left, bottom, right, bottom);
   } // end paint

The framework is drawn in the paint method. We draw the title and labels in their appropriate places. We draw a vertical line at the left border of the graph drawing region, and a horizontal line at the bottom border.

In this next snippet we set the preferred size for the component by overriding the preferredSize method. The preferredSize method is also inherited from the Component class. Components can specify a preferred size and a minimum size. I have chosen a preferred width of 300 and a preferred height of 200. The layout manager will call this method when it lays out the component.

   public Dimension preferredSize() {
      return(new Dimension(300, 200));
   }
} // end Graph

Note: In JDK 1.1, the preferredSize method is replaced with public Dimension getPreferredSize().

Next, we need a facility for adding and removing the items to be graphed.

   public void addItem(String name, int value, Color col) {
      items.addElement(new GraphItem(name, value, col));
   } // end addItem
   public void addItem(String name, int value) {
      items.addElement(new GraphItem(name, value, Color.black));
   } // end addItem
   public void removeItem(String name) {
      for (int i = 0; i < items.size(); i++) {
         if (((GraphItem)items.elementAt(i)).title.equals(name))
            items.removeElementAt(i);
      }
   } // end removeItem
} // end Graph

I've modeled the addItem and removeItem methods after similar methods in the Choice class, so the code will have a familiar feel. Notice that we use two addItem methods here; we need a way to add items with or without a color. When an item is added, a new GraphItem object is created and added to the items vector. When an item is removed, the first one in the vector with that name will be removed. The GraphItem class is very simple; here is the code:

import java.awt.*;
class GraphItem {
   String title;
   int value;
   Color color;
   public GraphItem(String title, int value, Color color) {
      this.title = title;
      this.value = value;
      this.color = color;
   } // end constructor
} // end GraphItem

The GraphItem class acts as a holder for the variables relating to graph items. I've included Color here in case it will be used in a subclass of Graph.

With this framework in place, we can create extensions to handle each type of graph. This strategy is quite convenient; we don't have to go to the trouble of measuring the pixels for the framework again, and we can create subclasses to focus on filling in the graph drawing region.

Building the bar chart

Now that we have a graphing framework, we can customize it by extending

Graph

and implementing custom drawing. We'll begin with a simple bar chart, which we can use just like any other component. A typical bar chart is illustrated below. We'll fill in the graph drawing region by overriding the

paint

method to call the superclass

paint

method (to draw the framework), then we'll perform the custom drawing needed for this type of graph.

import java.awt.*;
public class BarChart extends Graph {
   int position;
   int increment;
   public BarChart(String title, int min, int max) {
      super(title, min, max);
   } // end constructor

To space the items evenly, we keep an increment variable to indicate the amount we will shift to the right for each item. The position variable is the current position, and the increment value is added to it each time. The constructor simply takes in values for the super constructor (Graph), which we call explicitly.

Now we can get down to some actual drawing.

   public void paint(Graphics g) {
      super.paint(g);
      increment = (right - left)/(items.size());
      position = left;
      Color temp = g.getColor();
      for (int i = 0; i < items.size(); i++) {
         GraphItem item = (GraphItem)items.elementAt(i);
         int adjustedValue = bottom - (((item.value - min)*(bottom - top))
                                       /(max - min));
         g.drawString(item.title, position + (increment -
                      fm.stringWidth(item.title))/2, adjustedValue - 2);
         g.setColor(item.color);
         g.fillRect(position, adjustedValue, increment,
                    bottom - adjustedValue);
         position+=increment;
         g.setColor(temp);
      }
   } // end paint
} // end BarChart

Let's take a close look at what's happening here. In the paint method, we call the superclass paint method to draw the graph framework. We then find the increment by subtracting the right edge from the left edge, and then dividing the result by the number of items. This value is the distance between the left edges of the graph items. Because we want the graph to be resizable, we base these values on the current value of the left and right variables inherited from Graph. Recall that the left, right, top, and bottom values are the current actual pixel measurements of the graph drawing region taken in the reshape method of Graph, and therefore available for our use. If we did not base our measurements on these values, the graph would not be resizable.

We'll draw the rectangles in the color specified by the GraphItem. To allow us to go back to the original color, we set a temporary color variable to hold the current value before we change it. We cycle through the vector of graph items, calculating an adjusted vertical value for each one, drawing the title of the item and a filled rectangle representing its value. The increment is added to the x position variable each time through the loop.

The adjusted vertical value ensures that if the component is stretched vertically, the graph will still remain true to its plotted values. To do this properly, we need to take the percentage of the range the item represents and multiply that value by the actual pixel range of the graph drawing region. We then subtract the result from the bottom value to correctly plot the point.

As you can see from the following diagram, the total horizontal pixel size is represented by right - left and the total vertical size is represented by bottom - top.

We take care of the horizontal stretching by initializing the position variable to the left edge and increasing it by the increment variable for each item. Because the position and increment variables are dependent on the actual current pixel values, the component is always resized correctly in the horizontal direction.

To ensure that the vertical plotting is always correct, we must map the graph item values with actual pixel placements. There is one complication: The max and min values should be meaningful to the position of the graph item value. In other words, if the graph starts at 150 and goes to 200, an item with a value of 175 should appear halfway up the vertical axis. To achieve this, we find the percentage of the graph range that the item represents and multiply it by the actual pixel range. Because our graph is upside down from the graphics context's coordinate system, we subtract this number from bottom to find the correct plot point. Remember, the origin (0,0) is in the upper-left corner for the code, but the bottom-left corner for the style of graph we are creating.

Building the line graph

The bar chart was just a warm up. Now we're ready to tackle a slightly more complicated type of graph -- a line graph, which is shown in the following figure. The technique we're going to use is the same as with the bar chart, but we'll be required to do more counting and measuring.

import java.awt.*;
   public class LineGraph extends Graph {
      int increment;
      int position;
   public LineGraph(String title, int min, int max) {
      super(title, min, max);
   }

Just as before, we need to space the items evenly, so we keep an increment variable to indicate the amount we will shift to the right for each item. The position variable is the current position, and the increment variable is added to it each time. The constructor simply calls the super constructor.

Almost all of the work is done in the paint method. Let's look at that now.

   public void paint(Graphics g) {
      super.paint(g);
      increment = (right - left)/(items.size() - 1);
      position = left;
      Color temp = g.getColor();
      GraphItem firstItem = (GraphItem)items.firstElement();
      int firstAdjustedValue = bottom - (((firstItem.value - min)*(bottom - top)
                                         )/(max - min));
      g.setColor(firstItem.color);
      g.drawString(firstItem.title, position - fm.stringWidth(firstItem.title),
                   firstAdjustedValue - 2);
      g.fillOval(position - 2, firstAdjustedValue - 2, 4, 4);
      g.setColor(temp);
      for (int i = 0; i < items.size() - 1; i++) {
         GraphItem thisItem = (GraphItem)items.elementAt(i);
         int thisAdjustedValue = bottom - (((thisItem.value - min)*
                                 (bottom - top))/(max - min));
         GraphItem nextItem = (GraphItem)items.elementAt(i+1);
         int nextAdjustedValue = bottom - (((nextItem.value - min)*
                                 (bottom - top))/(max - min));
         g.drawLine(position, thisAdjustedValue,
                    position+=increment, nextAdjustedValue);
         g.setColor(nextItem.color);
         if (nextAdjustedValue < thisAdjustedValue)
            g.drawString(nextItem.title, position - fm.stringWidth(nextItem.title),
                         nextAdjustedValue + titleHeight + 4);
         else
            g.drawString(nextItem.title, position - fm.stringWidth(nextItem.title),
                         nextAdjustedValue - 4);
         g.fillOval(position - 2, nextAdjustedValue - 2, 4, 4);
         g.setColor(temp);
      }
   } // end paint

We first call the super paint method to draw the framework, then we implement our custom graph drawing. We find the value of the increment by measuring the difference between the left and right edges of the graph region and then by dividing the result by the number of elements minus 1. This formula will produce the correct increment value. Because we are drawing points with the first one at the left edge and the last one at the right edge, the increment is slightly different than it was with the bar chart. The position is initialized to the left edge of the drawing area.

Because we may have colors associated with the graph items, we keep the original color in a temp variable, then set the color to be the first item's color. We draw a small circle and the name of the first item in the correct position. We then set the color back to the original.

In the for loop, we find the correct pixel values of the current and next element in the vector, adjusted for the actual size of the component. This is the same concept and code as we used in the bar chart example, but this time we need a reference to the current and next item so that we can draw connecting lines. We check to see if the next value is less than the current value to decide where to draw the label. If the line will go up, we draw the label under the point, and if the line will go down, we draw the label above the point. This technique ensures that the lines won't cross our labels.

After a little bit of pixel counting and algebra, we now have a line graph component that is ready to use. Create it, add elements to it, and then add it to a container. You can lay out the line graph component in the container as you would any other component.

Conclusion

You can create a wide variety of custom components with the graphics primitives available in the

Graphics

class once you've mastered the basic drawing techniques. It is important that you try and make the components as generic as possible to allow for maximum reusability. Why spend the time writing and testing the same code over and over? Create the foundation first and then extend it to meet your needs. We performed some rather tedious measuring in this chapter, but we have reusable graph components to show for our efforts.

We covered quite a bit of territory this month, but there's still one type of standard chart we haven't examined -- a pie chart. If you're interested in building a pie chart, I've provided a sidebar, Building a pie chart, complete with code examples and detailed explanations, to help you out.

Maria Winslow co-founded Prominence Dot Com in 1995, and is currently developing top-secret Java applications. She co-authored Java Network Programming, from Manning/Prentice-Hall.

Learn more about this topic

  • Previous Step by Step articles

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more