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:  * $Id: MultiplePiePlot.java,v 1.12.2.8 2007/01/17 11:05:42 mungady Exp $
  36:  *
  37:  * Changes
  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.x ---------------------------------------------
  45:  * 06-Apr-2006 : Fixed bug 1190647 - legend and section colors not consistent
  46:  *               when aggregation limit is specified (DG);
  47:  * 27-Sep-2006 : Updated draw() method for deprecated code (DG);
  48:  * 17-Jan-2007 : Updated prefetchSectionPaints() to check settings in
  49:  *               underlying PiePlot (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.
 194:      */
 195:     public JFreeChart getPieChart() {
 196:         return this.pieChart;
 197:     }
 198:     
 199:     /**
 200:      * Sets the chart that is used to draw the individual pie plots.
 201:      * 
 202:      * @param pieChart  the pie chart.
 203:      */
 204:     public void setPieChart(JFreeChart pieChart) {
 205:         this.pieChart = pieChart;
 206:         notifyListeners(new PlotChangeEvent(this));
 207:     }
 208:     
 209:     /**
 210:      * Returns the data extract order (by row or by column).
 211:      * 
 212:      * @return The data extract order (never <code>null</code>).
 213:      */
 214:     public TableOrder getDataExtractOrder() {
 215:         return this.dataExtractOrder;
 216:     }
 217:     
 218:     /**
 219:      * Sets the data extract order (by row or by column) and sends a 
 220:      * {@link PlotChangeEvent} to all registered listeners.
 221:      * 
 222:      * @param order  the order (<code>null</code> not permitted).
 223:      */
 224:     public void setDataExtractOrder(TableOrder order) {
 225:         if (order == null) {
 226:             throw new IllegalArgumentException("Null 'order' argument");
 227:         }
 228:         this.dataExtractOrder = order;
 229:         notifyListeners(new PlotChangeEvent(this));
 230:     }
 231:     
 232:     /**
 233:      * Returns the limit (as a percentage) below which small pie sections are 
 234:      * aggregated.
 235:      * 
 236:      * @return The limit percentage.
 237:      */
 238:     public double getLimit() {
 239:         return this.limit;
 240:     }
 241:     
 242:     /**
 243:      * Sets the limit below which pie sections are aggregated.  
 244:      * Set this to 0.0 if you don't want any aggregation to occur.
 245:      * 
 246:      * @param limit  the limit percent.
 247:      */
 248:     public void setLimit(double limit) {
 249:         this.limit = limit;
 250:         notifyListeners(new PlotChangeEvent(this));
 251:     }
 252:     
 253:     /**
 254:      * Returns the key for aggregated items in the pie plots, if there are any.
 255:      * The default value is "Other".
 256:      * 
 257:      * @return The aggregated items key.
 258:      * 
 259:      * @since 1.0.2
 260:      */
 261:     public Comparable getAggregatedItemsKey() {
 262:         return this.aggregatedItemsKey;
 263:     }
 264:     
 265:     /**
 266:      * Sets the key for aggregated items in the pie plots.  You must ensure 
 267:      * that this doesn't clash with any keys in the dataset.
 268:      * 
 269:      * @param key  the key (<code>null</code> not permitted).
 270:      * 
 271:      * @since 1.0.2
 272:      */
 273:     public void setAggregatedItemsKey(Comparable key) {
 274:         if (key == null) {
 275:             throw new IllegalArgumentException("Null 'key' argument.");
 276:         }
 277:         this.aggregatedItemsKey = key;
 278:         notifyListeners(new PlotChangeEvent(this));
 279:     }
 280:     
 281:     /**
 282:      * Returns the paint used to draw the pie section representing the 
 283:      * aggregated items.  The default value is <code>Color.lightGray</code>.
 284:      * 
 285:      * @return The paint.
 286:      * 
 287:      * @since 1.0.2
 288:      */
 289:     public Paint getAggregatedItemsPaint() {
 290:         return this.aggregatedItemsPaint;
 291:     }
 292:     
 293:     /**
 294:      * Sets the paint used to draw the pie section representing the aggregated
 295:      * items and sends a {@link PlotChangeEvent} to all registered listeners.
 296:      * 
 297:      * @param paint  the paint (<code>null</code> not permitted).
 298:      * 
 299:      * @since 1.0.2
 300:      */
 301:     public void setAggregatedItemsPaint(Paint paint) {
 302:         if (paint == null) {
 303:             throw new IllegalArgumentException("Null 'paint' argument.");
 304:         }
 305:         this.aggregatedItemsPaint = paint;
 306:         notifyListeners(new PlotChangeEvent(this));
 307:     }
 308:     
 309:     /**
 310:      * Returns a short string describing the type of plot.
 311:      *
 312:      * @return The plot type.
 313:      */
 314:     public String getPlotType() {
 315:         return "Multiple Pie Plot";  
 316:          // TODO: need to fetch this from localised resources
 317:     }
 318: 
 319:     /**
 320:      * Draws the plot on a Java 2D graphics device (such as the screen or a 
 321:      * printer).
 322:      *
 323:      * @param g2  the graphics device.
 324:      * @param area  the area within which the plot should be drawn.
 325:      * @param anchor  the anchor point (<code>null</code> permitted).
 326:      * @param parentState  the state from the parent plot, if there is one.
 327:      * @param info  collects info about the drawing.
 328:      */
 329:     public void draw(Graphics2D g2, 
 330:                      Rectangle2D area,
 331:                      Point2D anchor,
 332:                      PlotState parentState,
 333:                      PlotRenderingInfo info) {
 334:         
 335:        
 336:         // adjust the drawing area for the plot insets (if any)...
 337:         RectangleInsets insets = getInsets();
 338:         insets.trim(area);
 339:         drawBackground(g2, area);
 340:         drawOutline(g2, area);
 341:         
 342:         // check that there is some data to display...
 343:         if (DatasetUtilities.isEmptyOrNull(this.dataset)) {
 344:             drawNoDataMessage(g2, area);
 345:             return;
 346:         }
 347: 
 348:         int pieCount = 0;
 349:         if (this.dataExtractOrder == TableOrder.BY_ROW) {
 350:             pieCount = this.dataset.getRowCount();
 351:         }
 352:         else {
 353:             pieCount = this.dataset.getColumnCount();
 354:         }
 355: 
 356:         // the columns variable is always >= rows
 357:         int displayCols = (int) Math.ceil(Math.sqrt(pieCount));
 358:         int displayRows 
 359:             = (int) Math.ceil((double) pieCount / (double) displayCols);
 360: 
 361:         // swap rows and columns to match plotArea shape
 362:         if (displayCols > displayRows && area.getWidth() < area.getHeight()) {
 363:             int temp = displayCols;
 364:             displayCols = displayRows;
 365:             displayRows = temp;
 366:         }
 367: 
 368:         prefetchSectionPaints();
 369:         
 370:         int x = (int) area.getX();
 371:         int y = (int) area.getY();
 372:         int width = ((int) area.getWidth()) / displayCols;
 373:         int height = ((int) area.getHeight()) / displayRows;
 374:         int row = 0;
 375:         int column = 0;
 376:         int diff = (displayRows * displayCols) - pieCount;
 377:         int xoffset = 0;
 378:         Rectangle rect = new Rectangle();
 379: 
 380:         for (int pieIndex = 0; pieIndex < pieCount; pieIndex++) {
 381:             rect.setBounds(x + xoffset + (width * column), y + (height * row), 
 382:                     width, height);
 383: 
 384:             String title = null;
 385:             if (this.dataExtractOrder == TableOrder.BY_ROW) {
 386:                 title = this.dataset.getRowKey(pieIndex).toString();
 387:             }
 388:             else {
 389:                 title = this.dataset.getColumnKey(pieIndex).toString();
 390:             }
 391:             this.pieChart.setTitle(title);
 392:             
 393:             PieDataset piedataset = null;
 394:             PieDataset dd = new CategoryToPieDataset(this.dataset, 
 395:                     this.dataExtractOrder, pieIndex);
 396:             if (this.limit > 0.0) {
 397:                 piedataset = DatasetUtilities.createConsolidatedPieDataset(
 398:                         dd, this.aggregatedItemsKey, this.limit);
 399:             }
 400:             else {
 401:                 piedataset = dd;
 402:             }
 403:             PiePlot piePlot = (PiePlot) this.pieChart.getPlot();
 404:             piePlot.setDataset(piedataset);
 405:             piePlot.setPieIndex(pieIndex);
 406:             
 407:             // update the section colors to match the global colors...
 408:             for (int i = 0; i < piedataset.getItemCount(); i++) {
 409:                 Comparable key = piedataset.getKey(i);
 410:                 Paint p;
 411:                 if (key.equals(this.aggregatedItemsKey)) {
 412:                     p = this.aggregatedItemsPaint;
 413:                 }
 414:                 else {
 415:                     p = (Paint) this.sectionPaints.get(key);
 416:                 }
 417:                 piePlot.setSectionPaint(key, p);
 418:             }
 419:             
 420:             ChartRenderingInfo subinfo = null;
 421:             if (info != null) {
 422:                 subinfo = new ChartRenderingInfo();
 423:             }
 424:             this.pieChart.draw(g2, rect, subinfo);
 425:             if (info != null) {
 426:                 info.getOwner().getEntityCollection().addAll(
 427:                         subinfo.getEntityCollection());
 428:                 info.addSubplotInfo(subinfo.getPlotInfo());
 429:             }
 430:             
 431:             ++column;
 432:             if (column == displayCols) {
 433:                 column = 0;
 434:                 ++row;
 435: 
 436:                 if (row == displayRows - 1 && diff != 0) {
 437:                     xoffset = (diff * width) / 2;
 438:                 }
 439:             }
 440:         }
 441: 
 442:     }
 443:     
 444:     /**
 445:      * For each key in the dataset, check the <code>sectionPaints</code>
 446:      * cache to see if a paint is associated with that key and, if not, 
 447:      * fetch one from the drawing supplier.  These colors are cached so that
 448:      * the legend and all the subplots use consistent colors.
 449:      */
 450:     private void prefetchSectionPaints() {
 451:         
 452:         // pre-fetch the colors for each key...this is because the subplots
 453:         // may not display every key, but we need the coloring to be
 454:         // consistent...
 455:         
 456:         PiePlot piePlot = (PiePlot) getPieChart().getPlot();
 457:         
 458:         if (this.dataExtractOrder == TableOrder.BY_ROW) {
 459:             // column keys provide potential keys for individual pies
 460:             for (int c = 0; c < this.dataset.getColumnCount(); c++) {
 461:                 Comparable key = this.dataset.getColumnKey(c);
 462:                 Paint p = piePlot.getSectionPaint(key); 
 463:                 if (p == null) {
 464:                     p = (Paint) this.sectionPaints.get(key);
 465:                     if (p == null) {
 466:                         p = getDrawingSupplier().getNextPaint();
 467:                     }
 468:                 }
 469:                 this.sectionPaints.put(key, p);
 470:             }
 471:         }
 472:         else {
 473:             // row keys provide potential keys for individual pies            
 474:             for (int r = 0; r < this.dataset.getRowCount(); r++) {
 475:                 Comparable key = this.dataset.getRowKey(r);
 476:                 Paint p = piePlot.getSectionPaint(key); 
 477:                 if (p == null) {
 478:                     p = (Paint) this.sectionPaints.get(key);
 479:                     if (p == null) {
 480:                         p = getDrawingSupplier().getNextPaint();
 481:                     }
 482:                 }
 483:                 this.sectionPaints.put(key, p);
 484:             }
 485:         }
 486:         
 487:     }
 488:     
 489:     /**
 490:      * Returns a collection of legend items for the pie chart.
 491:      *
 492:      * @return The legend items.
 493:      */
 494:     public LegendItemCollection getLegendItems() {
 495: 
 496:         LegendItemCollection result = new LegendItemCollection();
 497:         
 498:         if (this.dataset != null) {
 499:             List keys = null;
 500:       
 501:             prefetchSectionPaints();
 502:             if (this.dataExtractOrder == TableOrder.BY_ROW) {
 503:                 keys = this.dataset.getColumnKeys();
 504:             }
 505:             else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
 506:                 keys = this.dataset.getRowKeys();
 507:             }
 508: 
 509:             if (keys != null) {
 510:                 int section = 0;
 511:                 Iterator iterator = keys.iterator();
 512:                 while (iterator.hasNext()) {
 513:                     Comparable key = (Comparable) iterator.next();
 514:                     String label = key.toString();
 515:                     String description = label;
 516:                     Paint paint = (Paint) this.sectionPaints.get(key);
 517:                     LegendItem item = new LegendItem(label, description, 
 518:                             null, null, Plot.DEFAULT_LEGEND_ITEM_CIRCLE, 
 519:                             paint, Plot.DEFAULT_OUTLINE_STROKE, paint);
 520: 
 521:                     result.add(item);
 522:                     section++;
 523:                 }
 524:             }
 525:             if (this.limit > 0.0) {
 526:                 result.add(new LegendItem(this.aggregatedItemsKey.toString(), 
 527:                         this.aggregatedItemsKey.toString(), null, null, 
 528:                         Plot.DEFAULT_LEGEND_ITEM_CIRCLE, 
 529:                         this.aggregatedItemsPaint,
 530:                         Plot.DEFAULT_OUTLINE_STROKE, 
 531:                         this.aggregatedItemsPaint));
 532:             }
 533:         }
 534:         return result;
 535:     }
 536:     
 537:     /**
 538:      * Tests this plot for equality with an arbitrary object.  Note that the 
 539:      * plot's dataset is not considered in the equality test.
 540:      * 
 541:      * @param obj  the object (<code>null</code> permitted).
 542:      * 
 543:      * @return <code>true</code> if this plot is equal to <code>obj</code>, and
 544:      *     <code>false</code> otherwise.
 545:      */
 546:     public boolean equals(Object obj) {
 547:         if (obj == this) {
 548:             return true;   
 549:         }
 550:         if (!(obj instanceof MultiplePiePlot)) {
 551:             return false;   
 552:         }
 553:         MultiplePiePlot that = (MultiplePiePlot) obj;
 554:         if (this.dataExtractOrder != that.dataExtractOrder) {
 555:             return false;   
 556:         }
 557:         if (this.limit != that.limit) {
 558:             return false;   
 559:         }
 560:         if (!this.aggregatedItemsKey.equals(that.aggregatedItemsKey)) {
 561:             return false;
 562:         }
 563:         if (!PaintUtilities.equal(this.aggregatedItemsPaint, 
 564:                 that.aggregatedItemsPaint)) {
 565:             return false;
 566:         }
 567:         if (!ObjectUtilities.equal(this.pieChart, that.pieChart)) {
 568:             return false;   
 569:         }
 570:         if (!super.equals(obj)) {
 571:             return false;   
 572:         }
 573:         return true;
 574:     }
 575:     
 576:     /**
 577:      * Provides serialization support.
 578:      *
 579:      * @param stream  the output stream.
 580:      *
 581:      * @throws IOException  if there is an I/O error.
 582:      */
 583:     private void writeObject(ObjectOutputStream stream) throws IOException {
 584:         stream.defaultWriteObject();
 585:         SerialUtilities.writePaint(this.aggregatedItemsPaint, stream);
 586:     }
 587: 
 588:     /**
 589:      * Provides serialization support.
 590:      *
 591:      * @param stream  the input stream.
 592:      *
 593:      * @throws IOException  if there is an I/O error.
 594:      * @throws ClassNotFoundException  if there is a classpath problem.
 595:      */
 596:     private void readObject(ObjectInputStream stream) 
 597:         throws IOException, ClassNotFoundException {
 598:         stream.defaultReadObject();
 599:         this.aggregatedItemsPaint = SerialUtilities.readPaint(stream);
 600:         this.sectionPaints = new HashMap();
 601:     }
 602: 
 603:     
 604: }