Source for org.jfree.chart.plot.MultiplePiePlot

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2006, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  *
  27:  * --------------------
  28:  * MultiplePiePlot.java
  29:  * --------------------
  30:  * (C) Copyright 2004-2006, by Object Refinery Limited.
  31:  *
  32:  * Original Author:  David Gilbert (for Object Refinery Limited);
  33:  * Contributor(s):   -;
  34:  *
  35:  * $Id: MultiplePiePlot.java,v 1.12.2.6 2006/08/01 15:35:39 mungady Exp $
  36:  *
  37:  * Changes (from 21-Jun-2001)
  38:  * --------------------------
  39:  * 29-Jan-2004 : Version 1 (DG);
  40:  * 31-Mar-2004 : Added setPieIndex() call during drawing (DG);
  41:  * 20-Apr-2005 : Small change for update to LegendItem constructors (DG);
  42:  * 05-May-2005 : Updated draw() method parameters (DG);
  43:  * 16-Jun-2005 : Added get/setDataset() and equals() methods (DG);
  44:  * ------------- JFREECHART 1.0.0 ---------------------------------------------
  45:  * 06-Apr-2006 : Fixed bug 1190647 - legend and section colors not consistent
  46:  *               when aggregation limit is specified (DG);
  47:  *
  48:  */
  49: 
  50: package org.jfree.chart.plot;
  51: 
  52: import java.awt.Color;
  53: import java.awt.Font;
  54: import java.awt.Graphics2D;
  55: import java.awt.Paint;
  56: import java.awt.Rectangle;
  57: import java.awt.geom.Point2D;
  58: import java.awt.geom.Rectangle2D;
  59: import java.io.IOException;
  60: import java.io.ObjectInputStream;
  61: import java.io.ObjectOutputStream;
  62: import java.io.Serializable;
  63: import java.util.HashMap;
  64: import java.util.Iterator;
  65: import java.util.List;
  66: import java.util.Map;
  67: 
  68: import org.jfree.chart.ChartRenderingInfo;
  69: import org.jfree.chart.JFreeChart;
  70: import org.jfree.chart.LegendItem;
  71: import org.jfree.chart.LegendItemCollection;
  72: import org.jfree.chart.event.PlotChangeEvent;
  73: import org.jfree.chart.title.TextTitle;
  74: import org.jfree.data.category.CategoryDataset;
  75: import org.jfree.data.category.CategoryToPieDataset;
  76: import org.jfree.data.general.DatasetChangeEvent;
  77: import org.jfree.data.general.DatasetUtilities;
  78: import org.jfree.data.general.PieDataset;
  79: import org.jfree.io.SerialUtilities;
  80: import org.jfree.ui.RectangleEdge;
  81: import org.jfree.ui.RectangleInsets;
  82: import org.jfree.util.ObjectUtilities;
  83: import org.jfree.util.PaintUtilities;
  84: import org.jfree.util.TableOrder;
  85: 
  86: /**
  87:  * A plot that displays multiple pie plots using data from a 
  88:  * {@link CategoryDataset}.
  89:  */
  90: public class MultiplePiePlot extends Plot implements Cloneable, Serializable {
  91:     
  92:     /** For serialization. */
  93:     private static final long serialVersionUID = -355377800470807389L;
  94:     
  95:     /** The chart object that draws the individual pie charts. */
  96:     private JFreeChart pieChart;
  97:     
  98:     /** The dataset. */
  99:     private CategoryDataset dataset;
 100:     
 101:     /** The data extract order (by row or by column). */
 102:     private TableOrder dataExtractOrder;
 103:     
 104:     /** The pie section limit percentage. */
 105:     private double limit = 0.0;
 106:     
 107:     /** 
 108:      * The key for the aggregated items. 
 109:      * @since 1.0.2
 110:      */
 111:     private Comparable aggregatedItemsKey;
 112:     
 113:     /** 
 114:      * The paint for the aggregated items. 
 115:      * @since 1.0.2
 116:      */
 117:     private transient Paint aggregatedItemsPaint;
 118:     
 119:     /** 
 120:      * The colors to use for each section. 
 121:      * @since 1.0.2
 122:      */
 123:     private transient Map sectionPaints;
 124:     
 125:     /**
 126:      * Creates a new plot with no data.
 127:      */
 128:     public MultiplePiePlot() {
 129:         this(null);
 130:     }
 131:     
 132:     /**
 133:      * Creates a new plot.
 134:      * 
 135:      * @param dataset  the dataset (<code>null</code> permitted).
 136:      */
 137:     public MultiplePiePlot(CategoryDataset dataset) {
 138:         super();
 139:         this.dataset = dataset;
 140:         PiePlot piePlot = new PiePlot(null);
 141:         this.pieChart = new JFreeChart(piePlot);
 142:         this.pieChart.removeLegend();
 143:         this.dataExtractOrder = TableOrder.BY_COLUMN;
 144:         this.pieChart.setBackgroundPaint(null);
 145:         TextTitle seriesTitle = new TextTitle("Series Title", 
 146:                 new Font("SansSerif", Font.BOLD, 12));
 147:         seriesTitle.setPosition(RectangleEdge.BOTTOM);
 148:         this.pieChart.setTitle(seriesTitle);
 149:         this.aggregatedItemsKey = "Other";
 150:         this.aggregatedItemsPaint = Color.lightGray;
 151:         this.sectionPaints = new HashMap();
 152:     }
 153:     
 154:     /**
 155:      * Returns the dataset used by the plot.
 156:      * 
 157:      * @return The dataset (possibly <code>null</code>).
 158:      */
 159:     public CategoryDataset getDataset() {
 160:         return this.dataset;   
 161:     }
 162:     
 163:     /**
 164:      * Sets the dataset used by the plot and sends a {@link PlotChangeEvent}
 165:      * to all registered listeners.
 166:      * 
 167:      * @param dataset  the dataset (<code>null</code> permitted).
 168:      */
 169:     public void setDataset(CategoryDataset dataset) {
 170:         // if there is an existing dataset, remove the plot from the list of 
 171:         // change listeners...
 172:         if (this.dataset != null) {
 173:             this.dataset.removeChangeListener(this);
 174:         }
 175: 
 176:         // set the new dataset, and register the chart as a change listener...
 177:         this.dataset = dataset;
 178:         if (dataset != null) {
 179:             setDatasetGroup(dataset.getGroup());
 180:             dataset.addChangeListener(this);
 181:         }
 182: 
 183:         // send a dataset change event to self to trigger plot change event
 184:         datasetChanged(new DatasetChangeEvent(this, dataset));
 185:     }
 186:     
 187:     /**
 188:      * Returns the pie chart that is used to draw the individual pie plots.
 189:      * 
 190:      * @return The pie chart.
 191:      */
 192:     public JFreeChart getPieChart() {
 193:         return this.pieChart;
 194:     }
 195:     
 196:     /**
 197:      * Sets the chart that is used to draw the individual pie plots.
 198:      * 
 199:      * @param pieChart  the pie chart.
 200:      */
 201:     public void setPieChart(JFreeChart pieChart) {
 202:         this.pieChart = pieChart;
 203:         notifyListeners(new PlotChangeEvent(this));
 204:     }
 205:     
 206:     /**
 207:      * Returns the data extract order (by row or by column).
 208:      * 
 209:      * @return The data extract order (never <code>null</code>).
 210:      */
 211:     public TableOrder getDataExtractOrder() {
 212:         return this.dataExtractOrder;
 213:     }
 214:     
 215:     /**
 216:      * Sets the data extract order (by row or by column) and sends a 
 217:      * {@link PlotChangeEvent} to all registered listeners.
 218:      * 
 219:      * @param order  the order (<code>null</code> not permitted).
 220:      */
 221:     public void setDataExtractOrder(TableOrder order) {
 222:         if (order == null) {
 223:             throw new IllegalArgumentException("Null 'order' argument");
 224:         }
 225:         this.dataExtractOrder = order;
 226:         notifyListeners(new PlotChangeEvent(this));
 227:     }
 228:     
 229:     /**
 230:      * Returns the limit (as a percentage) below which small pie sections are 
 231:      * aggregated.
 232:      * 
 233:      * @return The limit percentage.
 234:      */
 235:     public double getLimit() {
 236:         return this.limit;
 237:     }
 238:     
 239:     /**
 240:      * Sets the limit below which pie sections are aggregated.  
 241:      * Set this to 0.0 if you don't want any aggregation to occur.
 242:      * 
 243:      * @param limit  the limit percent.
 244:      */
 245:     public void setLimit(double limit) {
 246:         this.limit = limit;
 247:         notifyListeners(new PlotChangeEvent(this));
 248:     }
 249:     
 250:     /**
 251:      * Returns the key for aggregated items in the pie plots, if there are any.
 252:      * The default value is "Other".
 253:      * 
 254:      * @return The aggregated items key.
 255:      * 
 256:      * @since 1.0.2
 257:      */
 258:     public Comparable getAggregatedItemsKey() {
 259:         return this.aggregatedItemsKey;
 260:     }
 261:     
 262:     /**
 263:      * Sets the key for aggregated items in the pie plots.  You must ensure 
 264:      * that this doesn't clash with any keys in the dataset.
 265:      * 
 266:      * @param key  the key (<code>null</code> not permitted).
 267:      * 
 268:      * @since 1.0.2
 269:      */
 270:     public void setAggregatedItemsKey(Comparable key) {
 271:         if (key == null) {
 272:             throw new IllegalArgumentException("Null 'key' argument.");
 273:         }
 274:         this.aggregatedItemsKey = key;
 275:         notifyListeners(new PlotChangeEvent(this));
 276:     }
 277:     
 278:     /**
 279:      * Returns the paint used to draw the pie section representing the 
 280:      * aggregated items.  The default value is <code>Color.lightGray</code>.
 281:      * 
 282:      * @return The paint.
 283:      * 
 284:      * @since 1.0.2
 285:      */
 286:     public Paint getAggregatedItemsPaint() {
 287:         return this.aggregatedItemsPaint;
 288:     }
 289:     
 290:     /**
 291:      * Sets the paint used to draw the pie section representing the aggregated
 292:      * items and sends a {@link PlotChangeEvent} to all registered listeners.
 293:      * 
 294:      * @param paint  the paint (<code>null</code> not permitted).
 295:      * 
 296:      * @since 1.0.2
 297:      */
 298:     public void setAggregatedItemsPaint(Paint paint) {
 299:         if (paint == null) {
 300:             throw new IllegalArgumentException("Null 'paint' argument.");
 301:         }
 302:         this.aggregatedItemsPaint = paint;
 303:         notifyListeners(new PlotChangeEvent(this));
 304:     }
 305:     
 306:     /**
 307:      * Returns a short string describing the type of plot.
 308:      *
 309:      * @return The plot type.
 310:      */
 311:     public String getPlotType() {
 312:         return "Multiple Pie Plot";  
 313:          // TODO: need to fetch this from localised resources
 314:     }
 315: 
 316:     /**
 317:      * Draws the plot on a Java 2D graphics device (such as the screen or a 
 318:      * printer).
 319:      *
 320:      * @param g2  the graphics device.
 321:      * @param area  the area within which the plot should be drawn.
 322:      * @param anchor  the anchor point (<code>null</code> permitted).
 323:      * @param parentState  the state from the parent plot, if there is one.
 324:      * @param info  collects info about the drawing.
 325:      */
 326:     public void draw(Graphics2D g2, 
 327:                      Rectangle2D area,
 328:                      Point2D anchor,
 329:                      PlotState parentState,
 330:                      PlotRenderingInfo info) {
 331:         
 332:        
 333:         // adjust the drawing area for the plot insets (if any)...
 334:         RectangleInsets insets = getInsets();
 335:         insets.trim(area);
 336:         drawBackground(g2, area);
 337:         drawOutline(g2, area);
 338:         
 339:         // check that there is some data to display...
 340:         if (DatasetUtilities.isEmptyOrNull(this.dataset)) {
 341:             drawNoDataMessage(g2, area);
 342:             return;
 343:         }
 344: 
 345:         int pieCount = 0;
 346:         if (this.dataExtractOrder == TableOrder.BY_ROW) {
 347:             pieCount = this.dataset.getRowCount();
 348:         }
 349:         else {
 350:             pieCount = this.dataset.getColumnCount();
 351:         }
 352: 
 353:         // the columns variable is always >= rows
 354:         int displayCols = (int) Math.ceil(Math.sqrt(pieCount));
 355:         int displayRows 
 356:             = (int) Math.ceil((double) pieCount / (double) displayCols);
 357: 
 358:         // swap rows and columns to match plotArea shape
 359:         if (displayCols > displayRows && area.getWidth() < area.getHeight()) {
 360:             int temp = displayCols;
 361:             displayCols = displayRows;
 362:             displayRows = temp;
 363:         }
 364: 
 365:         prefetchSectionPaints();
 366:         
 367:         int x = (int) area.getX();
 368:         int y = (int) area.getY();
 369:         int width = ((int) area.getWidth()) / displayCols;
 370:         int height = ((int) area.getHeight()) / displayRows;
 371:         int row = 0;
 372:         int column = 0;
 373:         int diff = (displayRows * displayCols) - pieCount;
 374:         int xoffset = 0;
 375:         Rectangle rect = new Rectangle();
 376: 
 377:         for (int pieIndex = 0; pieIndex < pieCount; pieIndex++) {
 378:             rect.setBounds(x + xoffset + (width * column), y + (height * row), 
 379:                     width, height);
 380: 
 381:             String title = null;
 382:             if (this.dataExtractOrder == TableOrder.BY_ROW) {
 383:                 title = this.dataset.getRowKey(pieIndex).toString();
 384:             }
 385:             else {
 386:                 title = this.dataset.getColumnKey(pieIndex).toString();
 387:             }
 388:             this.pieChart.setTitle(title);
 389:             
 390:             PieDataset piedataset = null;
 391:             PieDataset dd = new CategoryToPieDataset(this.dataset, 
 392:                     this.dataExtractOrder, pieIndex);
 393:             if (this.limit > 0.0) {
 394:                 piedataset = DatasetUtilities.createConsolidatedPieDataset(
 395:                         dd, this.aggregatedItemsKey, this.limit);
 396:             }
 397:             else {
 398:                 piedataset = dd;
 399:             }
 400:             PiePlot piePlot = (PiePlot) this.pieChart.getPlot();
 401:             piePlot.setDataset(piedataset);
 402:             piePlot.setPieIndex(pieIndex);
 403:             
 404:             // update the section colors to match the global colors...
 405:             for (int i = 0; i < piedataset.getItemCount(); i++) {
 406:                 Comparable key = piedataset.getKey(i);
 407:                 Paint p;
 408:                 if (key.equals(this.aggregatedItemsKey)) {
 409:                     p = this.aggregatedItemsPaint;
 410:                 }
 411:                 else {
 412:                     p = (Paint) this.sectionPaints.get(key);
 413:                 }
 414:                 piePlot.setSectionPaint(i, p);
 415:             }
 416:             
 417:             ChartRenderingInfo subinfo = null;
 418:             if (info != null) {
 419:                 subinfo = new ChartRenderingInfo();
 420:             }
 421:             this.pieChart.draw(g2, rect, subinfo);
 422:             if (info != null) {
 423:                 info.getOwner().getEntityCollection().addAll(
 424:                         subinfo.getEntityCollection());
 425:                 info.addSubplotInfo(subinfo.getPlotInfo());
 426:             }
 427:             
 428:             ++column;
 429:             if (column == displayCols) {
 430:                 column = 0;
 431:                 ++row;
 432: 
 433:                 if (row == displayRows - 1 && diff != 0) {
 434:                     xoffset = (diff * width) / 2;
 435:                 }
 436:             }
 437:         }
 438: 
 439:     }
 440:     
 441:     /**
 442:      * For each key in the dataset, check the <code>sectionPaints</code>
 443:      * cache to see if a paint is associated with that key and, if not, 
 444:      * fetch one from the drawing supplier.  These colors are cached so that
 445:      * the legend and all the subplots use consistent colors.
 446:      */
 447:     private void prefetchSectionPaints() {
 448:         
 449:         // pre-fetch the colors for each key...this is because the subplots
 450:         // may not display every key, but we need the coloring to be
 451:         // consistent...
 452:         if (this.dataExtractOrder == TableOrder.BY_ROW) {
 453:             // column keys provide potential keys for individual pies
 454:             for (int c = 0; c < this.dataset.getColumnCount(); c++) {
 455:                 Comparable key = this.dataset.getColumnKey(c);
 456:                 Paint p = (Paint) this.sectionPaints.get(key);
 457:                 if (p == null) {
 458:                     this.sectionPaints.put(key, 
 459:                             this.getDrawingSupplier().getNextPaint());
 460:                 }
 461:             }
 462:         }
 463:         else {
 464:             // row keys provide potential keys for individual pies            
 465:             for (int r = 0; r < this.dataset.getRowCount(); r++) {
 466:                 Comparable key = this.dataset.getRowKey(r);
 467:                 Paint p = (Paint) this.sectionPaints.get(key);
 468:                 if (p == null) {
 469:                     this.sectionPaints.put(key, 
 470:                             this.getDrawingSupplier().getNextPaint());
 471:                 }
 472:             }
 473:         }
 474:         
 475:     }
 476:     
 477:     /**
 478:      * Returns a collection of legend items for the pie chart.
 479:      *
 480:      * @return The legend items.
 481:      */
 482:     public LegendItemCollection getLegendItems() {
 483: 
 484:         LegendItemCollection result = new LegendItemCollection();
 485:         
 486:         if (this.dataset != null) {
 487:             List keys = null;
 488:       
 489:             prefetchSectionPaints();
 490:             if (this.dataExtractOrder == TableOrder.BY_ROW) {
 491:                 keys = this.dataset.getColumnKeys();
 492:             }
 493:             else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
 494:                 keys = this.dataset.getRowKeys();
 495:             }
 496: 
 497:             if (keys != null) {
 498:                 int section = 0;
 499:                 Iterator iterator = keys.iterator();
 500:                 while (iterator.hasNext()) {
 501:                     Comparable key = (Comparable) iterator.next();
 502:                     String label = key.toString();
 503:                     String description = label;
 504:                     Paint paint = (Paint) this.sectionPaints.get(key);
 505:                     LegendItem item = new LegendItem(label, description, 
 506:                             null, null, Plot.DEFAULT_LEGEND_ITEM_CIRCLE, 
 507:                             paint, Plot.DEFAULT_OUTLINE_STROKE, paint);
 508: 
 509:                     result.add(item);
 510:                     section++;
 511:                 }
 512:             }
 513:             if (this.limit > 0.0) {
 514:                 result.add(new LegendItem(this.aggregatedItemsKey.toString(), 
 515:                         this.aggregatedItemsKey.toString(), null, null, 
 516:                         Plot.DEFAULT_LEGEND_ITEM_CIRCLE, 
 517:                         this.aggregatedItemsPaint,
 518:                         Plot.DEFAULT_OUTLINE_STROKE, 
 519:                         this.aggregatedItemsPaint));
 520:             }
 521:         }
 522:         return result;
 523:     }
 524:     
 525:     /**
 526:      * Tests this plot for equality with an arbitrary object.  Note that the 
 527:      * plot's dataset is not considered in the equality test.
 528:      * 
 529:      * @param obj  the object (<code>null</code> permitted).
 530:      * 
 531:      * @return <code>true</code> if this plot is equal to <code>obj</code>, and
 532:      *     <code>false</code> otherwise.
 533:      */
 534:     public boolean equals(Object obj) {
 535:         if (obj == this) {
 536:             return true;   
 537:         }
 538:         if (!(obj instanceof MultiplePiePlot)) {
 539:             return false;   
 540:         }
 541:         MultiplePiePlot that = (MultiplePiePlot) obj;
 542:         if (this.dataExtractOrder != that.dataExtractOrder) {
 543:             return false;   
 544:         }
 545:         if (this.limit != that.limit) {
 546:             return false;   
 547:         }
 548:         if (!this.aggregatedItemsKey.equals(that.aggregatedItemsKey)) {
 549:             return false;
 550:         }
 551:         if (!PaintUtilities.equal(this.aggregatedItemsPaint, 
 552:                 that.aggregatedItemsPaint)) {
 553:             return false;
 554:         }
 555:         if (!ObjectUtilities.equal(this.pieChart, that.pieChart)) {
 556:             return false;   
 557:         }
 558:         if (!super.equals(obj)) {
 559:             return false;   
 560:         }
 561:         return true;
 562:     }
 563:     
 564:     /**
 565:      * Provides serialization support.
 566:      *
 567:      * @param stream  the output stream.
 568:      *
 569:      * @throws IOException  if there is an I/O error.
 570:      */
 571:     private void writeObject(ObjectOutputStream stream) throws IOException {
 572:         stream.defaultWriteObject();
 573:         SerialUtilities.writePaint(this.aggregatedItemsPaint, stream);
 574:     }
 575: 
 576:     /**
 577:      * Provides serialization support.
 578:      *
 579:      * @param stream  the input stream.
 580:      *
 581:      * @throws IOException  if there is an I/O error.
 582:      * @throws ClassNotFoundException  if there is a classpath problem.
 583:      */
 584:     private void readObject(ObjectInputStream stream) 
 585:         throws IOException, ClassNotFoundException {
 586:         stream.defaultReadObject();
 587:         this.aggregatedItemsPaint = SerialUtilities.readPaint(stream);
 588:         this.sectionPaints = new HashMap();
 589:     }
 590: 
 591:     
 592: }