Tuesday, October 20, 2009

16.4 Charting Data with a TableModel




I l@ve RuBoard










16.4 Charting Data with a TableModel



Our last example shows that the table
machinery isn't just for building tables; you can
use it to build other kinds of components (like the pie chart in
Figure 16-6). If you think about it,
there's no essential difference between a pie chart,
a bar chart, and many other kinds of data displays; they are all
different ways of rendering data that's logically
kept in a table. When that's the case, it is easy to
use a TableModel to manage the data and build your
own component for the display.



With AWT, building a new component was straightforward: you simply
created a subclass of Component. With Swing,
it's a little more complex because of the
distinction between the component itself and the user-interface
implementation. But it's not terribly hard,
particularly if you don't want to brave the waters
of the Pluggable L&F. In this case, there's no
good reason to make pie charts that look different on different
platforms, so we'll opt for simplicity.
We'll call our new component a
TableChart; it extends
JComponent. Its big responsibility is keeping the
data for the component updated; to this end, it listens for
TableModelEvents from the
TableModel to determine when changes have been
made.



To do the actual drawing, TableChart relies on a
delegate, PieChartPainter. To keep things
flexible, PieChartPainter is a subclass of
ChartPainter, which gives us the option of
building other kinds of chart painters (bar chart painters, etc.) in
the future. ChartPainter extends
ComponentUI, which is the base class for user
interface delegates. Here's where the
model-view-controller architecture comes into play. The table model
contains the actual data, TableChart is a
controller that tells a delegate what and when to paint, and
PieChartPainter is the view that paints a
particular kind of representation on the screen.



Just to prove that the same TableModel can be used
with any kind of display, we also display an old-fashioned
JTable using the same data�which turns out
to be convenient because we can use the
JTable's built-in editing
capabilities to modify the data. If you change any field (including
the name), the pie chart immediately changes to reflect the new data.



The TableChart class is particularly interesting
because it shows the "other side"
of table model event processing. In the
PagingModel of the earlier example, we had to
generate events as the data changed. Here, you see how those events
might be handled. The TableChart has to register
itself as a TableModelListener and respond to
events so that it can redraw itself when you edit the table. The
TableChart also implements one (perhaps unsightly)
shortcut: it presents the data by summing and averaging along the
columns. It would have been more work (but not much more) to present
the data in any particular column, letting the user choose the column
to be displayed. (See Figure 16-6.)




Figure 16-6. A chart component using a TableModel



Here's the application that produces both the pie
chart and the table. It includes the TableModel as
an anonymous inner class. This inner class is very simple, much
simpler than the models we used earlier in this chapter; it provides
an array for storing the data, methods to get and set the data, and
methods to provide other information about the table. Notice that we
provided an isCellEditable( ) method that always
returns true (the default method always returns
false). Because we're allowing
the user to edit the table, we must also override
setValueAt( ); our implementation updates the
data array and calls
fireTableRowsUpdated( ) to notify any listeners
that data has changed and they need to redraw. The rest of
ChartTester just sets up the display; we display
the pie chart as a pop up.



// ChartTester.java
//
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class ChartTester extends JFrame {

public ChartTester( ) {
super("Simple JTable Test");
setSize(300, 200);
setDefaultCloseOperation(EXIT_ON_CLOSE);


TableModel tm = new AbstractTableModel( ) {
String data[][] = {
{"Ron", "0.00", "68.68", "77.34", "78.02"},
{"Ravi", "0.00", "70.89", "64.17", "75.00"},
{"Maria", "76.52", "71.12", "75.68", "74.14"},
{"James", "70.00", "15.72", "26.40", "38.32"},
{"Ellen", "80.32", "78.16", "83.80", "85.72"}
};
String headers[] = { "", "Q1", "Q2", "Q3", "Q4" };
public int getColumnCount( ) { return headers.length; }
public int getRowCount( ) { return data.length; }
public String getColumnName(int col) { return headers[col]; }
public Class getColumnClass(int col) {
return (col == 0) ? String.class : Number.class;
}

public boolean isCellEditable(int row, int col) { return true; }
public Object getValueAt(int row, int col) { return data[row][col]; }
public void setValueAt(Object value, int row, int col) {
data[row][col] = (String)value;
fireTableRowsUpdated(row,row);
}
};

JTable jt = new JTable(tm);
JScrollPane jsp = new JScrollPane(jt);
getContentPane( ).add(jsp, BorderLayout.CENTER);

final TableChartPopup tcp = new TableChartPopup(tm);
JButton button = new JButton("Show me a chart of this table");
button.addActionListener(new ActionListener( ) {
public void actionPerformed(ActionEvent ae) {
tcp.setVisible(true);
}
} );
getContentPane( ).add(button, BorderLayout.SOUTH);
}

public static void main(String args[]) {
ChartTester ct = new ChartTester( );
ct.setVisible(true);
}
}


The TableChart object is actually made of three
pieces. The TableChart class extends
JComponent, which provides all the machinery for
getting a new component on the screen. It implements
TableModelListener because it has to register and
respond to TableModelEvents.



// TableChart.java
// A chart-generating class that uses the TableModel interface to get
// its data
//
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class TableChart extends JComponent implements TableModelListener {

protected TableModel model;
protected ChartPainter cp;
protected double[] percentages; // Pie slices
protected String[] labels; // Labels for slices
protected String[] tips; // Tooltips for slices

protected java.text.NumberFormat formatter =
java.text.NumberFormat.getPercentInstance( );

public TableChart(TableModel tm) {
setUI(cp = new PieChartPainter( ));
setModel(tm);
}

public void setTextFont(Font f) { cp.setTextFont(f); }
public Font getTextFont( ) { return cp.getTextFont( ); }

public void setTextColor(Color c) { cp.setTextColor(c); }
public Color getTextColor( ) { return cp.getTextColor( ); }

public void setColor(Color[] clist) { cp.setColor(clist); }
public Color[] getColor( ) { return cp.getColor( ); }

public void setColor(int index, Color c) { cp.setColor(index, c); }
public Color getColor(int index) { return cp.getColor(index); }

public String getToolTipText(MouseEvent me) {
if (tips != null) {
int whichTip = cp.indexOfEntryAt(me);
if (whichTip != -1) {
return tips[whichTip];
}
}
return null;
}

public void tableChanged(TableModelEvent tme) {
// Rebuild the arrays only if the structure changed.
updateLocalValues(tme.getType( ) != TableModelEvent.UPDATE);
}

public void setModel(TableModel tm) {
// Get listener code correct.
if (tm != model) {
if (model != null) {
model.removeTableModelListener(this);
}
model = tm;
model.addTableModelListener(this);
updateLocalValues(true);
}
}

public TableModel getModel( ) { return model; }

// Run through the model and count every cell (except the very first column,
// which we assume is the slice label column).
protected void calculatePercentages( ) {
double runningTotal = 0.0;
for (int i = model.getRowCount( ) - 1; i >= 0; i--) {
percentages[i] = 0.0;
for (int j = model.getColumnCount( ) - 1; j >=0; j--) {

// First, try the cell as a Number object.
Object val = model.getValueAt(i,j);
if (val instanceof Number) {
percentages[i] += ((Number)val).doubleValue( );
}
else if (val instanceof String) {
// Oops, it wasn't numeric, so try it as a string.
try {
percentages[i]+=Double.valueOf(val.toString( )).doubleValue( );
}
catch(Exception e) {
// Not a numeric string. Give up.
}
}
}
runningTotal += percentages[i];
}

// Make each entry a percentage of the total.
for (int i = model.getRowCount( ) - 1; i >= 0; i--) {
percentages[i] /= runningTotal;
}
}

// This method just takes the percentages and formats them as tooltips.
protected void createLabelsAndTips( ) {
for (int i = model.getRowCount( ) - 1; i >= 0; i--) {
labels[i] = (String)model.getValueAt(i, 0);
tips[i] = formatter.format(percentages[i]);
}
}

// Call this method to update the chart. We try to be more efficient here by
// allocating new storage arrays only if the new table has a different number of
// rows.
protected void updateLocalValues(boolean freshStart) {
if (freshStart) {
int count = model.getRowCount( );
if ((tips == null) || (count != tips.length)) {
percentages = new double[count];
labels = new String[count];
tips = new String[count];
}
}
calculatePercentages( );
createLabelsAndTips( );

// Now that everything's up-to-date, reset the chart painter with the new
// values.
cp.setValues(percentages);
cp.setLabels(labels);

// Finally, repaint the chart.
repaint( );
}
}


The constructor for TableChart sets the user
interface for this class to be the PieChartPainter
(which we discuss shortly). It also saves the
TableModel for the component by calling our
setModel( ) method; providing a separate
setModel( ) (rather than saving the model in the
constructor) lets us change the model at a later time�a nice
feature for a real component, though we don't take
advantage of it in this example. We also override
getToolTipText( ), which is called with a
MouseEvent as an argument. This method calls the
ChartPainter's
indexOfEntryAt( ) method to figure out which of
the model's entries corresponds to the current mouse
position, looks up the appropriate tooltip, and returns it.



tableChanged( ) listens for
TableModelEvents. It delegates the call to another
method, updateLocalValues( ), with an argument of
true if the table's structure has
changed (e.g., rows added or deleted), and false
if only the values have changed. The rest of
TableChart updates the data when the change
occurs. The focal point of this work is updateLocalValues( ); calculatePercentages( ) and
createLabelsAndTips( ) are helper methods that
keep the work modular. If updateLocalValues( ) is
called with its argument set to true, it finds out
the new number of rows for the table and creates new arrays to hold
the component's view of the data. It calculates
percentages, retrieves labels, makes up tooltips, and calls the
ChartPainter (the user interface object) to give
it the new information. It ends by calling repaint( ) to redraw the screen with updated data.



ChartPainter is
the actual user-interface class. It is abstract; we subclass it to
implement specific kinds of charts. It extends the
ComponentUI class, which makes it sound rather
complex, but it isn't. We've made
one simplifying assumption: the chart looks the same in any L&F.
(The component in which the chart is embedded changes its appearance,
but that's another issue�and one we
don't have to worry about.) All our
ComponentUI has to do is implement paint( ), which we leave abstract, forcing the subclass to
implement it. Our other abstract method, indexOfEntryAt( ), is required by TableChart.



// ChartPainter.java
// A simple, chart-drawing UI base class. This class tracks the basic fonts and
// colors for various types of charts, including pie and bar. The paint( ) method is
// abstract and must be implemented by subclasses for each type.
//
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.plaf.*;

public abstract class ChartPainter extends ComponentUI {

protected Font textFont = new Font("Serif", Font.PLAIN, 12);
protected Color textColor = Color.black;
protected Color colors[] = new Color[] {
Color.red, Color.blue, Color.yellow, Color.black, Color.green,
Color.white, Color.gray, Color.cyan, Color.magenta, Color.darkGray
};
protected double values[] = new double[0];
protected String labels[] = new String[0];

public void setTextFont(Font f) { textFont = f; }
public Font getTextFont( ) { return textFont; }

public void setColor(Color[] clist) { colors = clist; }
public Color[] getColor( ) { return colors; }

public void setColor(int index, Color c) { colors[index] = c; }
public Color getColor(int index) { return colors[index]; }

public void setTextColor(Color c) { textColor = c; }
public Color getTextColor( ) { return textColor; }

public void setLabels(String[] l) { labels = l; }
public void setValues(double[] v) { values = v; }

public abstract int indexOfEntryAt(MouseEvent me);
public abstract void paint(Graphics g, JComponent c);
}


There's not much mystery here. Except for the two
abstract methods, these methods just maintain various simple
properties of ChartPainter: the colors used for
painting, the font, and the labels and values for the chart.



The real work takes place in the PieChartPainter
class, which implements the indexOfEntryAt( ) and
paint( ) methods. The indexOfEntryAt( ) method allows our TableChart class to
figure out which tooltip to show. The paint( )
method allows us to draw a pie chart of our data.



// PieChartPainter.java
// A pie chart implementation of the ChartPainter class
//
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.plaf.*;

public class PieChartPainter extends ChartPainter {

protected static PieChartPainter chartUI = new PieChartPainter( );
protected int originX, originY;
protected int radius;

private static double piby2 = Math.PI / 2.0;
private static double twopi = Math.PI * 2.0;
private static double d2r = Math.PI / 180.0; // Degrees to radians
private static int xGap = 5;
private static int inset = 40;

public int indexOfEntryAt(MouseEvent me) {
int x = me.getX( ) - originX;
int y = originY - me.getY( ); // Upside-down coordinate system

// Is (x,y) in the circle?
if (Math.sqrt(x*x + y*y) > radius) { return -1; }

double percent = Math.atan2(Math.abs(y), Math.abs(x));
if (x >= 0) {
if (y <= 0) { // (IV)
percent = (piby2 - percent) + 3 * piby2; // (IV)
}
}
else {
if (y >= 0) { // (II)
percent = Math.PI - percent;
}
else { // (III)
percent = Math.PI + percent;
}
}
percent /= twopi;
double t = 0.0;
if (values != null) {
for (int i = 0; i < values.length; i++) {
if (t + values[i] > percent) {
return i;
}
t += values[i];
}
}
return -1;
}

public void paint(Graphics g, JComponent c) {
Dimension size = c.getSize( );
originX = size.width / 2;
originY = size.height / 2;
int diameter = (originX < originY ? size.width - inset
: size.height - inset);
radius = (diameter / 2) + 1;
int cornerX = (originX - (diameter / 2));
int cornerY = (originY - (diameter / 2));

int startAngle = 0;
int arcAngle = 0;
for (int i = 0; i < values.length; i++) {
arcAngle = (int)(i < values.length - 1 ?
Math.round(values[i] * 360) :
360 - startAngle);
g.setColor(colors[i % colors.length]);
g.fillArc(cornerX, cornerY, diameter, diameter,
startAngle, arcAngle);
drawLabel(g, labels[i], startAngle + (arcAngle / 2));
startAngle += arcAngle;
}
g.setColor(Color.black);
g.drawOval(cornerX, cornerY, diameter, diameter); // Cap the circle.
}

public void drawLabel(Graphics g, String text, double angle) {
g.setFont(textFont);
g.setColor(textColor);
double radians = angle * d2r;
int x = (int) ((radius + xGap) * Math.cos(radians));
int y = (int) ((radius + xGap) * Math.sin(radians));
if (x < 0) {
x -= SwingUtilities.computeStringWidth(g.getFontMetrics( ), text);
}
if (y < 0) {
y -= g.getFontMetrics( ).getHeight( );
}
g.drawString(text, x + originX, originY - y);
}

public static ComponentUI createUI(JComponent c) {
return chartUI;
}
}


There's nothing really complex here;
it's just a lot of trigonometry and a little bit of
simple AWT drawing. paint( ) is called with a
graphics context and a JComponent as arguments;
the JComponent allows you to figure out the size
of the area we have to work with.



Here's the code for the pop up containing the chart:



// TableChartPopup.java
//
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class TableChartPopup extends JFrame {

public TableChartPopup(TableModel tm) {
super("Table Chart");
setSize(300,200);
TableChart tc = new TableChart(tm);
getContentPane( ).add(tc, BorderLayout.CENTER);
// Use the following line to turn on tooltips:
ToolTipManager.sharedInstance( ).registerComponent(tc);
}
}


As you can see, the TableChart component can be
used on its own without a JTable. We just need a
model to base it on. You could expand this example to chart only
selected rows or columns, but we'll leave that as an
exercise that you can do on your own.











    I l@ve RuBoard



    No comments: