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.


