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
figs/swng2.1606.gif

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.



Java Swing
Graphic Java 2: Mastering the Jfc, By Geary, 3Rd Edition, Volume 2: Swing
ISBN: 0130796670
EAN: 2147483647
Year: 2001
Pages: 289
Authors: David Geary

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net