001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2008, 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-2008, 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 *
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, Serializable {
096
097 /** For serialization. */
098 private static final long serialVersionUID = -355377800470807389L;
099
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 }