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