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