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.8 2007/01/17 11:05:42 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 *
051 */
052
053 package org.jfree.chart.plot;
054
055 import java.awt.Color;
056 import java.awt.Font;
057 import java.awt.Graphics2D;
058 import java.awt.Paint;
059 import java.awt.Rectangle;
060 import java.awt.geom.Point2D;
061 import java.awt.geom.Rectangle2D;
062 import java.io.IOException;
063 import java.io.ObjectInputStream;
064 import java.io.ObjectOutputStream;
065 import java.io.Serializable;
066 import java.util.HashMap;
067 import java.util.Iterator;
068 import java.util.List;
069 import java.util.Map;
070
071 import org.jfree.chart.ChartRenderingInfo;
072 import org.jfree.chart.JFreeChart;
073 import org.jfree.chart.LegendItem;
074 import org.jfree.chart.LegendItemCollection;
075 import org.jfree.chart.event.PlotChangeEvent;
076 import org.jfree.chart.title.TextTitle;
077 import org.jfree.data.category.CategoryDataset;
078 import org.jfree.data.category.CategoryToPieDataset;
079 import org.jfree.data.general.DatasetChangeEvent;
080 import org.jfree.data.general.DatasetUtilities;
081 import org.jfree.data.general.PieDataset;
082 import org.jfree.io.SerialUtilities;
083 import org.jfree.ui.RectangleEdge;
084 import org.jfree.ui.RectangleInsets;
085 import org.jfree.util.ObjectUtilities;
086 import org.jfree.util.PaintUtilities;
087 import org.jfree.util.TableOrder;
088
089 /**
090 * A plot that displays multiple pie plots using data from a
091 * {@link CategoryDataset}.
092 */
093 public class MultiplePiePlot extends Plot implements Cloneable, Serializable {
094
095 /** For serialization. */
096 private static final long serialVersionUID = -355377800470807389L;
097
098 /** The chart object that draws the individual pie charts. */
099 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 }