Subpages:
1. Java2D API overview
2. Rendering charts
3. Rendering text strings
4. Rendering images
23.2 Rendering charts
In this section we'll demonstrate the advantages of using the Java2D API for rendering charts. The following example introduces a custom component which is capable of rendering line graphs, bar charts, and pie charts using strokes, color gradients, and background images. This application demonstrates how to build such charts taking into account issues such as axis positioning and scaling based on the given coordinate data. Be prepared for a bit of math.

Figure 23.1 Charts2D displaying the three available JChart2D charts with various visual effects.
<<file figure23-1.gif>>
The Code: Charts2D.java
see \Chapter23\1
import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
public class Charts2D extends JFrame
{
public Charts2D() {
super("2D Charts");
setSize(720, 280);
getContentPane().setLayout(new GridLayout(1, 3, 10, 0));
getContentPane().setBackground(Color.white);
int nData = 8;
int[] xData = new int[nData];
int[] yData = new int[nData];
for (int k=0; k<nData; k++) {
xData[k] = k;
yData[k] = (int)(Math.random()*100);
if (k > 0)
yData[k] = (yData[k-1] + yData[k])/2;
}
JChart2D chart = new JChart2D(
JChart2D.CHART_LINE, nData, xData,
yData, "Line Chart");
chart.setStroke(new BasicStroke(5f, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_MITER));
chart.setLineColor(new Color(0, 128, 128));
getContentPane().add(chart);
chart = new JChart2D(JChart2D.CHART_COLUMN,
nData, xData, yData, "Column Chart");
GradientPaint gp = new GradientPaint(0, 100,
Color.white, 0, 300, Color.blue, true);
chart.setGradient(gp);
chart.setEffectIndex(JChart2D.EFFECT_GRADIENT);
chart.setDrawShadow(true);
getContentPane().add(chart);
chart = new JChart2D(JChart2D.CHART_PIE, nData, xData,
yData, "Pie Chart");
ImageIcon icon = new ImageIcon("hubble.gif");
chart.setForegroundImage(icon.getImage());
chart.setEffectIndex(JChart2D.EFFECT_IMAGE);
chart.setDrawShadow(true);
getContentPane().add(chart);
WindowListener wndCloser = new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
};
addWindowListener(wndCloser);
setVisible(true);
}
public static void main(String argv[]) {
new Charts2D();
}
}
class JChart2D extends JPanel
{
public static final int CHART_LINE = 0;
public static final int CHART_COLUMN = 1;
public static final int CHART_PIE = 2;
public static final int EFFECT_PLAIN = 0;
public static final int EFFECT_GRADIENT = 1;
public static final int EFFECT_IMAGE = 2;
protected int m_chartType = CHART_LINE;
protected JLabel m_title;
protected ChartPanel m_chart;
protected int m_nData;
protected int[] m_xData;
protected int[] m_yData;
protected int m_xMin;
protected int m_xMax;
protected int m_yMin;
protected int m_yMax;
protected double[] m_pieData;
protected int m_effectIndex = EFFECT_PLAIN;
protected Stroke m_stroke;
protected GradientPaint m_gradient;
protected Image m_foregroundImage;
protected Color m_lineColor = Color.black;
protected Color m_columnColor = Color.blue;
protected int m_columnWidth = 12;
protected boolean m_drawShadow = false;
public JChart2D(int type, int nData,
int[] yData, String text) {
this(type, nData, null, yData, text);
}
public JChart2D(int type, int nData, int[] xData,
int[] yData, String text) {
super(new BorderLayout());
setBackground(Color.white);
m_title = new JLabel(text, JLabel.CENTER);
add(m_title, BorderLayout.NORTH);
m_chartType = type;
if (xData==null) {
xData = new int[nData];
for (int k=0; k<nData; k++)
xData[k] = k;
}
if (yData == null)
throw new IllegalArgumentException(
"yData can't be null");
if (nData > yData.length)
throw new IllegalArgumentException(
"Insufficient yData length");
if (nData > xData.length)
throw new IllegalArgumentException(
"Insufficient xData length");
m_nData = nData;
m_xData = xData;
m_yData = yData;
m_xMin = m_xMax = 0; // To include 0 into the interval
m_yMin = m_yMax = 0;
for (int k=0; k<m_nData; k++) {
m_xMin = Math.min(m_xMin, m_xData[k]);
m_xMax = Math.max(m_xMax, m_xData[k]);
m_yMin = Math.min(m_yMin, m_yData[k]);
m_yMax = Math.max(m_yMax, m_yData[k]);
}
if (m_xMin == m_xMax)
m_xMax++;
if (m_yMin == m_yMax)
m_yMax++;
if (m_chartType == CHART_PIE) {
double sum = 0;
for (int k=0; k<m_nData; k++) {
m_yData[k] = Math.max(m_yData[k], 0);
sum += m_yData[k];
}
m_pieData = new double[m_nData];
for (int k=0; k<m_nData; k++)
m_pieData[k] = m_yData[k]*360.0/sum;
}
m_chart = new ChartPanel();
add(m_chart, BorderLayout.CENTER);
}
public void setEffectIndex(int effectIndex) {
m_effectIndex = effectIndex;
repaint();
}
public int getEffectIndex() { return m_effectIndex; }
public void setStroke(Stroke stroke) {
m_stroke = stroke;
m_chart.repaint();
}
public void setForegroundImage(Image img) {
m_foregroundImage = img;
repaint();
}
public Image getForegroundImage() { return m_foregroundImage; }
public Stroke getStroke() { return m_stroke; }
public void setGradient(GradientPaint gradient) {
m_gradient = gradient;
repaint();
}
public GradientPaint getGradient() { return m_gradient; }
public void setColumnWidth(int columnWidth) {
m_columnWidth = columnWidth;
m_chart.calcDimensions();
m_chart.repaint();
}
public int getColumnWidth() { return m_columnWidth; }
public void setColumnColor(Color c) {
m_columnColor = c;
m_chart.repaint();
}
public Color getColumnColor() { return m_columnColor; }
public void setLineColor(Color c) {
m_lineColor = c;
m_chart.repaint();
}
public Color getLineColor() { return m_lineColor; }
public void setDrawShadow(boolean drawShadow) {
m_drawShadow = drawShadow;
m_chart.repaint();
}
public boolean getDrawShadow() { return m_drawShadow; }
class ChartPanel extends JComponent
{
int m_xMargin = 5;
int m_yMargin = 5;
int m_pieGap = 10;
int m_x;
int m_y;
int m_w;
int m_h;
ChartPanel() {
enableEvents(ComponentEvent.COMPONENT_RESIZED);
}
protected void processComponentEvent(ComponentEvent e) {
calcDimensions();
}
public void calcDimensions() {
Dimension d = getSize();
m_x = m_xMargin;
m_y = m_yMargin;
m_w = d.width-2*m_xMargin;
m_h = d.height-2*m_yMargin;
if (m_chartType == CHART_COLUMN) {
m_x += m_columnWidth/2;
m_w -= m_columnWidth;
}
}
public int xChartToScreen(int x) {
return m_x + (x-m_xMin)*m_w/(m_xMax-m_xMin);
}
public int yChartToScreen(int y) {
return m_y + (m_yMax-y)*m_h/(m_yMax-m_yMin);
}
public void paintComponent(Graphics g) {
int x0 = 0;
int y0 = 0;
if (m_chartType != CHART_PIE) {
g.setColor(Color.black);
x0 = xChartToScreen(0);
g.drawLine(x0, m_y, x0, m_y+m_h);
y0 = yChartToScreen(0);
g.drawLine(m_x, y0, m_x+m_w, y0);
}
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
if (m_stroke != null)
g2.setStroke(m_stroke);
GeneralPath path = new GeneralPath();
switch (m_chartType) {
case CHART_LINE:
g2.setColor(m_lineColor);
path.moveTo(xChartToScreen(m_xData[0]),
yChartToScreen(m_yData[0]));
for (int k=1; k<m_nData; k++)
path.lineTo(xChartToScreen(m_xData[k]),
yChartToScreen(m_yData[k]));
g2.draw(path);
break;
case CHART_COLUMN:
for (int k=0; k<m_nData; k++) {
m_xMax ++;
int x = xChartToScreen(m_xData[k]);
int w = m_columnWidth;
int y1 = yChartToScreen(m_yData[k]);
int y = Math.min(y0, y1);
int h = Math.abs(y1 - y0);
Shape rc = new Rectangle2D.Double(x, y, w, h);
path.append(rc, false);
m_xMax --;
}
if (m_drawShadow) {
AffineTransform s0 = new AffineTransform(
1.0, 0.0, 0.0, -1.0, x0, y0);
s0.concatenate(AffineTransform.getScaleInstance(
1.0, 0.5));
s0.concatenate(AffineTransform.getShearInstance(
0.5, 0.0));
s0.concatenate(new AffineTransform(
1.0, 0.0, 0.0, -1.0, -x0, y0));
g2.setColor(Color.gray);
Shape shadow = s0.createTransformedShape(path);
g2.fill(shadow);
}
if (m_effectIndex==EFFECT_GRADIENT &&
m_gradient != null) {
g2.setPaint(m_gradient);
g2.fill(path);
}
else if (m_effectIndex==EFFECT_IMAGE &&
m_foregroundImage != null)
fillByImage(g2, path, 0);
else {
g2.setColor(m_columnColor);
g2.fill(path);
}
g2.setColor(m_lineColor);
g2.draw(path);
break;
case CHART_PIE:
double start = 0.0;
double finish = 0.0;
int ww = m_w - 2*m_pieGap;
int hh = m_h - 2*m_pieGap;
if (m_drawShadow) {
ww -= m_pieGap;
hh -= m_pieGap;
}
for (int k=0; k<m_nData; k++) {
finish = start+m_pieData[k];
double f1 = Math.min(90-start, 90-finish);
double f2 = Math.max(90-start, 90-finish);
Shape shp = new Arc2D.Double(m_x, m_y, ww, hh,
f1, f2-f1, Arc2D.PIE);
double f = (f1 + f2)/2*Math.PI/180;
AffineTransform s1 = AffineTransform.
getTranslateInstance(m_pieGap*Math.cos(f),
-m_pieGap*Math.sin(f));
s1.translate(m_pieGap, m_pieGap);
Shape piece = s1.createTransformedShape(shp);
path.append(piece, false);
start = finish;
}
if (m_drawShadow) {
AffineTransform s0 = AffineTransform.
getTranslateInstance(m_pieGap, m_pieGap);
g2.setColor(Color.gray);
Shape shadow = s0.createTransformedShape(path);
g2.fill(shadow);
}
if (m_effectIndex==EFFECT_GRADIENT && m_gradient != null) {
g2.setPaint(m_gradient);
g2.fill(path);
}
else if (m_effectIndex==EFFECT_IMAGE &&
m_foregroundImage != null)
fillByImage(g2, path, 0);
else {
g2.setColor(m_columnColor);
g2.fill(path);
}
g2.setColor(m_lineColor);
g2.draw(path);
break;
}
}
protected void fillByImage(Graphics2D g2,
Shape shape, int xOffset) {
if (m_foregroundImage == null)
return;
int wImg = m_foregroundImage.getWidth(this);
int hImg = m_foregroundImage.getHeight(this);
if (wImg <=0 || hImg <= 0)
return;
g2.setClip(shape);
Rectangle bounds = shape.getBounds();
for (int xx = bounds.x+xOffset;
xx < bounds.x+bounds.width; xx += wImg)
for (int yy = bounds.y; yy < bounds.y+bounds.height;
yy += hImg)
g2.drawImage(m_foregroundImage, xx, yy, this);
}
}
}
Understanding the Code
Class Charts2D
This class provides the frame encompassing this example. It creates an array of equidistant x-coordinates and random y-coordinates to be drawn in the charts. Three instances of our custom JChart2D class (see below) are created and placed in the frame using a GridLayout. The methods used to provide setup and initialization for our chart are built into the JChart2D class and will be explained below.
Class JChart2D
Several constants are defined for use as the available chart type and visual effect options:
int CHART_LINE: specifies a line chart.
int CHART_COLUMN: specifies a column chart.
int CHART_PIE: specifies a pie chart.
int EFFECT_PLAIN: use no visual effects (homogeneous chart).
int EFFECT_GRADIENT: use a color gradient to fill the chart.
int EFFECT_IMAGE: use an image to fill the chart.
Several instance variables are defined to hold data used by this class:
JLabel m_title: label used to display a chart's title.
ChartPanel m_chart: custom component used to display a chart's body (see below).
int m_nData: number of points in the chart.
int[] m_xData: array of x-coordinates in the chart.
int[] m_yData: array of y-coordinates in the chart.
int m_xMin: minimum x-coordinate.
int m_xMax: maximum x-coordinate.
int m_yMin: minimum y-coordinate.
int m_yMax: maximum y-coordinate.
double[] m_pieData: angles for each piece of the pie chart.
int m_chartType: maintains the chart's type (one of the constants listed above).
int m_effectIndex: maintains the chart's effect index (one of the constants listed above).
Stroke m_stroke: stroke instance used to outline the chart.
GradientPaint m_gradient: color gradient used to fill the chart (this only takes effect when m_effectIndex is set to EFFECT_GRADIENT).
Image m_foregroundImage: image used to fill the chart (this only takes effect when m_effectIndex is set to EFFECT_IMAGE).
Color m_lineColor: color used to outline the chart.
Color m_columnColor: color used to fill the chart (this only takes effect when m_effectIndex is set to EFFECT_PLAIN -- this is its default setting).
int m_columnWidth: width of columns in the column chart.
boolean m_drawShadow: flag to draw a shadow for column or pie chart.
Two constructors are provided in the JChart2D class. The first one takes four parameters and simply calls the second, passing it the given parameters and using a null value for a fifth. This second constructor is where a JChart2D is actually created and its five parameters are:
int type: the type of this chart (CHART_LINE, CHART_COLUMN, or CHART_PIE).
int nData: number of data points in this chart.
int[] xData: an array of x-coordinates for this chart (may be null -- this is passed as null from the first constructor).
int[] yData: an array of y-coordinates for this chart.
String text: this chart's title.
The constructor validates the input data and initializes all instance variables. In the case of a pie chart, an array, m_pieData, is created, which contains sectors with angles normalized to 360 degrees (the sum value used here was calculated previous to this code as the sum of all m_yData[] values):
m_pieData = new double[m_nData];
for (int k=0; k<m_nData; k++)
m_pieData[k] = m_yData[k]*360.0/sum;
This chart component extends JPanel and contains two child components managed using a BorderLayout: JLabel m_title, which displays the chart's title in the NORTH region, and an instance of our custom ChartPanel component, m_chart, which is placed in the CENTER region.
The rest of the code for this class consists of set/get methods supporting instance variables declared in this class and does not require further explanation.
Class JChart2D.ChartPanel
This inner class extends JComponent and represents the custom component that is actually responsible for rendering our charts. Several instance variables are declared:
int m_xMargin: the left and right margin size of the rendering area.
int m_yMargin: the top and bottom margin size of the rendering area.
int m_pieGap: radial shift for pieces of pie (i.e. spacing between each).
int m_x: left coordinate of the rendering area.
int m_y: top coordinate of the rendering area.
int m_w: width of the rendering area.
int m_h: height of the rendering area.
The ChartPanel constructor enables the processing of component resize events. When such an event occurs, the processComponentEvent() method triggers a call to calcDimensions() (note that this event will normally be generated when ChartPanel is added to a container for the first time). This method retrieves the current component's size, calculates the coordinates of the rendering area, and stores them in the appropriate instance variables listed above. In the case of a column chart, we offset the rendering area by an additional half of the column width, and then shrink it by a full column width. Otherwise, the first and the last columns will be rendered on top of the chart's border.
Methods xChartToScreen() and yChartToScreen() calculate screen coordinates from chart coordinates as illustrated in figure 23.2. We need to scale the chart data so the chart will occupy the entire component region, taking into account the margins. To get the necessary scaling ratios we divide the dimensions of the chart component (minus the margins) by the difference between max and min values of the chart data. These methods are used in rendering the line and column charts because they are based on coordinate data. The only sizing information the pie chart needs is m_w and m_h, as it does not rely on coordinate data.

Figure 23.2 Screen coordinates vs. chart coordinates.
<<file figure23-2.gif>>
The paintComponent() method performs the actual chart rendering. The coordinate axes are drawn first for line and column charts. Then we cast the Graphics instance to a Graphics2D so we have access to Java2D features. As we discussed earlier, we use two rendering hints and assign them with the setRenderingHint() method: anti-aliasing and the preference to render quality over speed. If the m_stroke instance variable has been initialized, the Graphics2D stroke attribute is set using the setStroke() method. The rest of the paintComponent() method is placed into a switch block with cases for each chart type. Before the switch block is entered we create a GeneralPath which we will use to construct each chart using the methods we described in section 23.1.2.
The line chart is the simplest case. It is drawn as a broken line through the array of points representing the chart data. First we start the GeneralPath out by passing the first coordinate of data using moveTo(). Then we iterate through the chart data adding lines to the path using its lineTo() method. Once we've done this we are ready to render it and use the Graphics2D draw() method to do so.
Note: The Java2D API provides ways to draw quadratic and cubic curves passing through 3 and 4 given points respectively. Unfortunately this functionality is not suitable for drawing a smooth line chart with interpolation.
The column chart is drawn as a set of vertical bars with a common baseline corresponding to the 0-value of the chart, y0 (note that this value is always included in the [m_yMin, m_yMax] interval). The GeneralPath instance accumulates these bars as Rectangle2D.Double instances using its append() method, passing false for the line connection option.
If the m_drawShadow flag is set, the next step forms and draws a shadow from these bars, which should be viewed as standing vertically. AffineTransform s0 is constructed to accomplish this in four steps:
1. Transform from screen coordinates to chart coordinates.
2. Scale y-axis by a factor of 0.5.
3. Shear x-axis by a factor of 1.0.
4. Transform chart coordinates back to screen coordinates.
As soon as this AffineTransform is constructed, we create a corresponding transformed version of our path Shape using AffineTransform's createTransformedShape() method. We then set the current color to gray and render it into the 2D graphics context using the fill() method. Finally the set of bars is drawn on the screen. Depending on the m_effectIndex setting we fill this shape with the gradient color, image (by calling our custom fillByImage() method), or with a solid color.
The pie chart is drawn as pieces of a circle with a common center. The larger the chart's value is for a given point, the larger the corresponding angle of that piece is. For an interesting resemblance with a cut pie, all pieces are shifted apart from the common center in the radial direction. To draw such a pie we first build each piece by iterating through the chart's data. Using class Arc2D.Double with its PIE setting provides a convenient way to build a slice of pie. We then translate this slice away from the pie's center in the radial direction using an AffineTransform and its createTransformShape() method. Each resulting shape is appended to our GeneralPath instance.
If the m_drawShadow flag is set, we form and draw a shadow from these pieces. Since this chart can be viewed as laying on a flat surface, the shadow has the same shape as the chart itself, but is translated in the south-east direction. Finally the set of pie pieces is drawn on the screen using the selected visual effect. Since at this point we operate with the chart as a single Shape (remember a GeneralPath is a Shape), the code is the same as for the column chart.
The custom fillByImage() method uses the given Shape instance's bounds as the Graphics2D clipping area, and, in a doubly nested for loop, fills this region using our previously assigned m_foregroundImage. (Note that the third parameter to this method, int xOffset, is used for horizontal displacement which we do not make use of in this example. However, we will see this method again in the next example where we will need this functionality.)
Running the Code
Figure 23.1 shows our Charts2D application containing three charts: line, column, and pie. Try modifying the settings specified in Charts2D class to try charts with various combinations of available visual effects. Also try resizing the frame container and note how each chart is scaled accordingly.
Our JChart2D component can easily be plugged into any Swing application. Since we have implemented full scalability and correct coordinate mapping, we have the beginnings of a professional chart component. The next step would be to add informative strings to the axis as well as pie pieces, bars, and data points of the line chart.



RSS feed Java FAQ News