001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2009, by Object Refinery Limited and Contributors.
006 *
007 * Project Info: http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * --------------------
028 * MultiplePiePlot.java
029 * --------------------
030 * (C) Copyright 2004-2009, by Object Refinery Limited.
031 *
032 * Original Author: David Gilbert (for Object Refinery Limited);
033 * Contributor(s): Brian Cabana (patch 1943021);
034 *
035 * Changes
036 * -------
037 * 29-Jan-2004 : Version 1 (DG);
038 * 31-Mar-2004 : Added setPieIndex() call during drawing (DG);
039 * 20-Apr-2005 : Small change for update to LegendItem constructors (DG);
040 * 05-May-2005 : Updated draw() method parameters (DG);
041 * 16-Jun-2005 : Added get/setDataset() and equals() methods (DG);
042 * ------------- JFREECHART 1.0.x ---------------------------------------------
043 * 06-Apr-2006 : Fixed bug 1190647 - legend and section colors not consistent
044 * when aggregation limit is specified (DG);
045 * 27-Sep-2006 : Updated draw() method for deprecated code (DG);
046 * 17-Jan-2007 : Updated prefetchSectionPaints() to check settings in
047 * underlying PiePlot (DG);
048 * 17-May-2007 : Added argument check to setPieChart() (DG);
049 * 18-May-2007 : Set dataset for LegendItem (DG);
050 * 18-Apr-2008 : In the constructor, register the plot as a dataset listener -
051 * see patch 1943021 from Brian Cabana (DG);
052 * 30-Dec-2008 : Added legendItemShape field, and fixed cloning bug (DG);
053 * 09-Jan-2009 : See ignoreNullValues to true for sub-chart (DG);
054 *
055 */
056
057 package org.jfree.chart.plot;
058
059 import java.awt.Color;
060 import java.awt.Font;
061 import java.awt.Graphics2D;
062 import java.awt.Paint;
063 import java.awt.Rectangle;
064 import java.awt.Shape;
065 import java.awt.geom.Ellipse2D;
066 import java.awt.geom.Point2D;
067 import java.awt.geom.Rectangle2D;
068 import java.io.IOException;
069 import java.io.ObjectInputStream;
070 import java.io.ObjectOutputStream;
071 import java.io.Serializable;
072 import java.util.HashMap;
073 import java.util.Iterator;
074 import java.util.List;
075 import java.util.Map;
076
077 import org.jfree.chart.ChartRenderingInfo;
078 import org.jfree.chart.JFreeChart;
079 import org.jfree.chart.LegendItem;
080 import org.jfree.chart.LegendItemCollection;
081 import org.jfree.chart.event.PlotChangeEvent;
082 import org.jfree.chart.title.TextTitle;
083 import org.jfree.data.category.CategoryDataset;
084 import org.jfree.data.category.CategoryToPieDataset;
085 import org.jfree.data.general.DatasetChangeEvent;
086 import org.jfree.data.general.DatasetUtilities;
087 import org.jfree.data.general.PieDataset;
088 import org.jfree.io.SerialUtilities;
089 import org.jfree.ui.RectangleEdge;
090 import org.jfree.ui.RectangleInsets;
091 import org.jfree.util.ObjectUtilities;
092 import org.jfree.util.PaintUtilities;
093 import org.jfree.util.ShapeUtilities;
094 import org.jfree.util.TableOrder;
095
096 /**
097 * A plot that displays multiple pie plots using data from a
098 * {@link CategoryDataset}.
099 */
100 public class MultiplePiePlot extends Plot implements Cloneable, Serializable {
101
102 /** For serialization. */
103 private static final long serialVersionUID = -355377800470807389L;
104
105 /** The chart object that draws the individual pie charts. */
106 private JFreeChart pieChart;
107
108 /** The dataset. */
109 private CategoryDataset dataset;
110
111 /** The data extract order (by row or by column). */
112 private TableOrder dataExtractOrder;
113
114 /** The pie section limit percentage. */
115 private double limit = 0.0;
116
117 /**
118 * The key for the aggregated items.
119 *
120 * @since 1.0.2
121 */
122 private Comparable aggregatedItemsKey;
123
124 /**
125 * The paint for the aggregated items.
126 *
127 * @since 1.0.2
128 */
129 private transient Paint aggregatedItemsPaint;
130
131 /**
132 * The colors to use for each section.
133 *
134 * @since 1.0.2
135 */
136 private transient Map sectionPaints;
137
138 /**
139 * The legend item shape (never null).
140 *
141 * @since 1.0.12
142 */
143 private transient Shape legendItemShape;
144
145 /**
146 * Creates a new plot with no data.
147 */
148 public MultiplePiePlot() {
149 this(null);
150 }
151
152 /**
153 * Creates a new plot.
154 *
155 * @param dataset the dataset (<code>null</code> permitted).
156 */
157 public MultiplePiePlot(CategoryDataset dataset) {
158 super();
159 setDataset(dataset);
160 PiePlot piePlot = new PiePlot(null);
161 piePlot.setIgnoreNullValues(true);
162 this.pieChart = new JFreeChart(piePlot);
163 this.pieChart.removeLegend();
164 this.dataExtractOrder = TableOrder.BY_COLUMN;
165 this.pieChart.setBackgroundPaint(null);
166 TextTitle seriesTitle = new TextTitle("Series Title",
167 new Font("SansSerif", Font.BOLD, 12));
168 seriesTitle.setPosition(RectangleEdge.BOTTOM);
169 this.pieChart.setTitle(seriesTitle);
170 this.aggregatedItemsKey = "Other";
171 this.aggregatedItemsPaint = Color.lightGray;
172 this.sectionPaints = new HashMap();
173 this.legendItemShape = new Ellipse2D.Double(-4.0, -4.0, 8.0, 8.0);
174 }
175
176 /**
177 * Returns the dataset used by the plot.
178 *
179 * @return The dataset (possibly <code>null</code>).
180 */
181 public CategoryDataset getDataset() {
182 return this.dataset;
183 }
184
185 /**
186 * Sets the dataset used by the plot and sends a {@link PlotChangeEvent}
187 * to all registered listeners.
188 *
189 * @param dataset the dataset (<code>null</code> permitted).
190 */
191 public void setDataset(CategoryDataset dataset) {
192 // if there is an existing dataset, remove the plot from the list of
193 // change listeners...
194 if (this.dataset != null) {
195 this.dataset.removeChangeListener(this);
196 }
197
198 // set the new dataset, and register the chart as a change listener...
199 this.dataset = dataset;
200 if (dataset != null) {
201 setDatasetGroup(dataset.getGroup());
202 dataset.addChangeListener(this);
203 }
204
205 // send a dataset change event to self to trigger plot change event
206 datasetChanged(new DatasetChangeEvent(this, dataset));
207 }
208
209 /**
210 * Returns the pie chart that is used to draw the individual pie plots.
211 * Note that there are some attributes on this chart instance that will
212 * be ignored at rendering time (for example, legend item settings).
213 *
214 * @return The pie chart (never <code>null</code>).
215 *
216 * @see #setPieChart(JFreeChart)
217 */
218 public JFreeChart getPieChart() {
219 return this.pieChart;
220 }
221
222 /**
223 * Sets the chart that is used to draw the individual pie plots. The
224 * chart's plot must be an instance of {@link PiePlot}.
225 *
226 * @param pieChart the pie chart (<code>null</code> not permitted).
227 *
228 * @see #getPieChart()
229 */
230 public void setPieChart(JFreeChart pieChart) {
231 if (pieChart == null) {
232 throw new IllegalArgumentException("Null 'pieChart' argument.");
233 }
234 if (!(pieChart.getPlot() instanceof PiePlot)) {
235 throw new IllegalArgumentException("The 'pieChart' argument must "
236 + "be a chart based on a PiePlot.");
237 }
238 this.pieChart = pieChart;
239 fireChangeEvent();
240 }
241
242 /**
243 * Returns the data extract order (by row or by column).
244 *
245 * @return The data extract order (never <code>null</code>).
246 */
247 public TableOrder getDataExtractOrder() {
248 return this.dataExtractOrder;
249 }
250
251 /**
252 * Sets the data extract order (by row or by column) and sends a
253 * {@link PlotChangeEvent} to all registered listeners.
254 *
255 * @param order the order (<code>null</code> not permitted).
256 */
257 public void setDataExtractOrder(TableOrder order) {
258 if (order == null) {
259 throw new IllegalArgumentException("Null 'order' argument");
260 }
261 this.dataExtractOrder = order;
262 fireChangeEvent();
263 }
264
265 /**
266 * Returns the limit (as a percentage) below which small pie sections are
267 * aggregated.
268 *
269 * @return The limit percentage.
270 */
271 public double getLimit() {
272 return this.limit;
273 }
274
275 /**
276 * Sets the limit below which pie sections are aggregated.
277 * Set this to 0.0 if you don't want any aggregation to occur.
278 *
279 * @param limit the limit percent.
280 */
281 public void setLimit(double limit) {
282 this.limit = limit;
283 fireChangeEvent();
284 }
285
286 /**
287 * Returns the key for aggregated items in the pie plots, if there are any.
288 * The default value is "Other".
289 *
290 * @return The aggregated items key.
291 *
292 * @since 1.0.2
293 */
294 public Comparable getAggregatedItemsKey() {
295 return this.aggregatedItemsKey;
296 }
297
298 /**
299 * Sets the key for aggregated items in the pie plots. You must ensure
300 * that this doesn't clash with any keys in the dataset.
301 *
302 * @param key the key (<code>null</code> not permitted).
303 *
304 * @since 1.0.2
305 */
306 public void setAggregatedItemsKey(Comparable key) {
307 if (key == null) {
308 throw new IllegalArgumentException("Null 'key' argument.");
309 }
310 this.aggregatedItemsKey = key;
311 fireChangeEvent();
312 }
313
314 /**
315 * Returns the paint used to draw the pie section representing the
316 * aggregated items. The default value is <code>Color.lightGray</code>.
317 *
318 * @return The paint.
319 *
320 * @since 1.0.2
321 */
322 public Paint getAggregatedItemsPaint() {
323 return this.aggregatedItemsPaint;
324 }
325
326 /**
327 * Sets the paint used to draw the pie section representing the aggregated
328 * items and sends a {@link PlotChangeEvent} to all registered listeners.
329 *
330 * @param paint the paint (<code>null</code> not permitted).
331 *
332 * @since 1.0.2
333 */
334 public void setAggregatedItemsPaint(Paint paint) {
335 if (paint == null) {
336 throw new IllegalArgumentException("Null 'paint' argument.");
337 }
338 this.aggregatedItemsPaint = paint;
339 fireChangeEvent();
340 }
341
342 /**
343 * Returns a short string describing the type of plot.
344 *
345 * @return The plot type.
346 */
347 public String getPlotType() {
348 return "Multiple Pie Plot";
349 // TODO: need to fetch this from localised resources
350 }
351
352 /**
353 * Returns the shape used for legend items.
354 *
355 * @return The shape (never <code>null</code>).
356 *
357 * @see #setLegendItemShape(Shape)
358 *
359 * @since 1.0.12
360 */
361 public Shape getLegendItemShape() {
362 return this.legendItemShape;
363 }
364
365 /**
366 * Sets the shape used for legend items and sends a {@link PlotChangeEvent}
367 * to all registered listeners.
368 *
369 * @param shape the shape (<code>null</code> not permitted).
370 *
371 * @see #getLegendItemShape()
372 *
373 * @since 1.0.12
374 */
375 public void setLegendItemShape(Shape shape) {
376 if (shape == null) {
377 throw new IllegalArgumentException("Null 'shape' argument.");
378 }
379 this.legendItemShape = shape;
380 fireChangeEvent();
381 }
382
383 /**
384 * Draws the plot on a Java 2D graphics device (such as the screen or a
385 * printer).
386 *
387 * @param g2 the graphics device.
388 * @param area the area within which the plot should be drawn.
389 * @param anchor the anchor point (<code>null</code> permitted).
390 * @param parentState the state from the parent plot, if there is one.
391 * @param info collects info about the drawing.
392 */
393 public void draw(Graphics2D g2,
394 Rectangle2D area,
395 Point2D anchor,
396 PlotState parentState,
397 PlotRenderingInfo info) {
398
399
400 // adjust the drawing area for the plot insets (if any)...
401 RectangleInsets insets = getInsets();
402 insets.trim(area);
403 drawBackground(g2, area);
404 drawOutline(g2, area);
405
406 // check that there is some data to display...
407 if (DatasetUtilities.isEmptyOrNull(this.dataset)) {
408 drawNoDataMessage(g2, area);
409 return;
410 }
411
412 int pieCount = 0;
413 if (this.dataExtractOrder == TableOrder.BY_ROW) {
414 pieCount = this.dataset.getRowCount();
415 }
416 else {
417 pieCount = this.dataset.getColumnCount();
418 }
419
420 // the columns variable is always >= rows
421 int displayCols = (int) Math.ceil(Math.sqrt(pieCount));
422 int displayRows
423 = (int) Math.ceil((double) pieCount / (double) displayCols);
424
425 // swap rows and columns to match plotArea shape
426 if (displayCols > displayRows && area.getWidth() < area.getHeight()) {
427 int temp = displayCols;
428 displayCols = displayRows;
429 displayRows = temp;
430 }
431
432 prefetchSectionPaints();
433
434 int x = (int) area.getX();
435 int y = (int) area.getY();
436 int width = ((int) area.getWidth()) / displayCols;
437 int height = ((int) area.getHeight()) / displayRows;
438 int row = 0;
439 int column = 0;
440 int diff = (displayRows * displayCols) - pieCount;
441 int xoffset = 0;
442 Rectangle rect = new Rectangle();
443
444 for (int pieIndex = 0; pieIndex < pieCount; pieIndex++) {
445 rect.setBounds(x + xoffset + (width * column), y + (height * row),
446 width, height);
447
448 String title = null;
449 if (this.dataExtractOrder == TableOrder.BY_ROW) {
450 title = this.dataset.getRowKey(pieIndex).toString();
451 }
452 else {
453 title = this.dataset.getColumnKey(pieIndex).toString();
454 }
455 this.pieChart.setTitle(title);
456
457 PieDataset piedataset = null;
458 PieDataset dd = new CategoryToPieDataset(this.dataset,
459 this.dataExtractOrder, pieIndex);
460 if (this.limit > 0.0) {
461 piedataset = DatasetUtilities.createConsolidatedPieDataset(
462 dd, this.aggregatedItemsKey, this.limit);
463 }
464 else {
465 piedataset = dd;
466 }
467 PiePlot piePlot = (PiePlot) this.pieChart.getPlot();
468 piePlot.setDataset(piedataset);
469 piePlot.setPieIndex(pieIndex);
470
471 // update the section colors to match the global colors...
472 for (int i = 0; i < piedataset.getItemCount(); i++) {
473 Comparable key = piedataset.getKey(i);
474 Paint p;
475 if (key.equals(this.aggregatedItemsKey)) {
476 p = this.aggregatedItemsPaint;
477 }
478 else {
479 p = (Paint) this.sectionPaints.get(key);
480 }
481 piePlot.setSectionPaint(key, p);
482 }
483
484 ChartRenderingInfo subinfo = null;
485 if (info != null) {
486 subinfo = new ChartRenderingInfo();
487 }
488 this.pieChart.draw(g2, rect, subinfo);
489 if (info != null) {
490 info.getOwner().getEntityCollection().addAll(
491 subinfo.getEntityCollection());
492 info.addSubplotInfo(subinfo.getPlotInfo());
493 }
494
495 ++column;
496 if (column == displayCols) {
497 column = 0;
498 ++row;
499
500 if (row == displayRows - 1 && diff != 0) {
501 xoffset = (diff * width) / 2;
502 }
503 }
504 }
505
506 }
507
508 /**
509 * For each key in the dataset, check the <code>sectionPaints</code>
510 * cache to see if a paint is associated with that key and, if not,
511 * fetch one from the drawing supplier. These colors are cached so that
512 * the legend and all the subplots use consistent colors.
513 */
514 private void prefetchSectionPaints() {
515
516 // pre-fetch the colors for each key...this is because the subplots
517 // may not display every key, but we need the coloring to be
518 // consistent...
519
520 PiePlot piePlot = (PiePlot) getPieChart().getPlot();
521
522 if (this.dataExtractOrder == TableOrder.BY_ROW) {
523 // column keys provide potential keys for individual pies
524 for (int c = 0; c < this.dataset.getColumnCount(); c++) {
525 Comparable key = this.dataset.getColumnKey(c);
526 Paint p = piePlot.getSectionPaint(key);
527 if (p == null) {
528 p = (Paint) this.sectionPaints.get(key);
529 if (p == null) {
530 p = getDrawingSupplier().getNextPaint();
531 }
532 }
533 this.sectionPaints.put(key, p);
534 }
535 }
536 else {
537 // row keys provide potential keys for individual pies
538 for (int r = 0; r < this.dataset.getRowCount(); r++) {
539 Comparable key = this.dataset.getRowKey(r);
540 Paint p = piePlot.getSectionPaint(key);
541 if (p == null) {
542 p = (Paint) this.sectionPaints.get(key);
543 if (p == null) {
544 p = getDrawingSupplier().getNextPaint();
545 }
546 }
547 this.sectionPaints.put(key, p);
548 }
549 }
550
551 }
552
553 /**
554 * Returns a collection of legend items for the pie chart.
555 *
556 * @return The legend items.
557 */
558 public LegendItemCollection getLegendItems() {
559
560 LegendItemCollection result = new LegendItemCollection();
561 if (this.dataset == null) {
562 return result;
563 }
564
565 List keys = null;
566 prefetchSectionPaints();
567 if (this.dataExtractOrder == TableOrder.BY_ROW) {
568 keys = this.dataset.getColumnKeys();
569 }
570 else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
571 keys = this.dataset.getRowKeys();
572 }
573
574 if (keys != null) {
575 int section = 0;
576 Iterator iterator = keys.iterator();
577 while (iterator.hasNext()) {
578 Comparable key = (Comparable) iterator.next();
579 String label = key.toString(); // TODO: use a generator here
580 String description = label;
581 Paint paint = (Paint) this.sectionPaints.get(key);
582 LegendItem item = new LegendItem(label, description, null,
583 null, getLegendItemShape(), paint,
584 Plot.DEFAULT_OUTLINE_STROKE, paint);
585 item.setDataset(getDataset());
586 result.add(item);
587 section++;
588 }
589 }
590 if (this.limit > 0.0) {
591 result.add(new LegendItem(this.aggregatedItemsKey.toString(),
592 this.aggregatedItemsKey.toString(), null, null,
593 getLegendItemShape(), this.aggregatedItemsPaint,
594 Plot.DEFAULT_OUTLINE_STROKE, this.aggregatedItemsPaint));
595 }
596 return result;
597 }
598
599 /**
600 * Tests this plot for equality with an arbitrary object. Note that the
601 * plot's dataset is not considered in the equality test.
602 *
603 * @param obj the object (<code>null</code> permitted).
604 *
605 * @return <code>true</code> if this plot is equal to <code>obj</code>, and
606 * <code>false</code> otherwise.
607 */
608 public boolean equals(Object obj) {
609 if (obj == this) {
610 return true;
611 }
612 if (!(obj instanceof MultiplePiePlot)) {
613 return false;
614 }
615 MultiplePiePlot that = (MultiplePiePlot) obj;
616 if (this.dataExtractOrder != that.dataExtractOrder) {
617 return false;
618 }
619 if (this.limit != that.limit) {
620 return false;
621 }
622 if (!this.aggregatedItemsKey.equals(that.aggregatedItemsKey)) {
623 return false;
624 }
625 if (!PaintUtilities.equal(this.aggregatedItemsPaint,
626 that.aggregatedItemsPaint)) {
627 return false;
628 }
629 if (!ObjectUtilities.equal(this.pieChart, that.pieChart)) {
630 return false;
631 }
632 if (!ShapeUtilities.equal(this.legendItemShape, that.legendItemShape)) {
633 return false;
634 }
635 if (!super.equals(obj)) {
636 return false;
637 }
638 return true;
639 }
640
641 /**
642 * Returns a clone of the plot.
643 *
644 * @return A clone.
645 *
646 * @throws CloneNotSupportedException if some component of the plot does
647 * not support cloning.
648 */
649 public Object clone() throws CloneNotSupportedException {
650 MultiplePiePlot clone = (MultiplePiePlot) super.clone();
651 clone.pieChart = (JFreeChart) this.pieChart.clone();
652 clone.sectionPaints = new HashMap(this.sectionPaints);
653 clone.legendItemShape = ShapeUtilities.clone(this.legendItemShape);
654 return clone;
655 }
656
657 /**
658 * Provides serialization support.
659 *
660 * @param stream the output stream.
661 *
662 * @throws IOException if there is an I/O error.
663 */
664 private void writeObject(ObjectOutputStream stream) throws IOException {
665 stream.defaultWriteObject();
666 SerialUtilities.writePaint(this.aggregatedItemsPaint, stream);
667 SerialUtilities.writeShape(this.legendItemShape, stream);
668 }
669
670 /**
671 * Provides serialization support.
672 *
673 * @param stream the input stream.
674 *
675 * @throws IOException if there is an I/O error.
676 * @throws ClassNotFoundException if there is a classpath problem.
677 */
678 private void readObject(ObjectInputStream stream)
679 throws IOException, ClassNotFoundException {
680 stream.defaultReadObject();
681 this.aggregatedItemsPaint = SerialUtilities.readPaint(stream);
682 this.legendItemShape = SerialUtilities.readShape(stream);
683 this.sectionPaints = new HashMap();
684 }
685
686 }