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