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 * 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 *
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 (never <code>null</code>).
194 *
195 * @see #setPieChart(JFreeChart)
196 */
197 public JFreeChart getPieChart() {
198 return this.pieChart;
199 }
200
201 /**
202 * Sets the chart that is used to draw the individual pie plots. The
203 * chart's plot must be an instance of {@link PiePlot}.
204 *
205 * @param pieChart the pie chart (<code>null</code> not permitted).
206 *
207 * @see #getPieChart()
208 */
209 public void setPieChart(JFreeChart pieChart) {
210 if (pieChart == null) {
211 throw new IllegalArgumentException("Null 'pieChart' argument.");
212 }
213 if (!(pieChart.getPlot() instanceof PiePlot)) {
214 throw new IllegalArgumentException("The 'pieChart' argument must "
215 + "be a chart based on a PiePlot.");
216 }
217 this.pieChart = pieChart;
218 notifyListeners(new PlotChangeEvent(this));
219 }
220
221 /**
222 * Returns the data extract order (by row or by column).
223 *
224 * @return The data extract order (never <code>null</code>).
225 */
226 public TableOrder getDataExtractOrder() {
227 return this.dataExtractOrder;
228 }
229
230 /**
231 * Sets the data extract order (by row or by column) and sends a
232 * {@link PlotChangeEvent} to all registered listeners.
233 *
234 * @param order the order (<code>null</code> not permitted).
235 */
236 public void setDataExtractOrder(TableOrder order) {
237 if (order == null) {
238 throw new IllegalArgumentException("Null 'order' argument");
239 }
240 this.dataExtractOrder = order;
241 notifyListeners(new PlotChangeEvent(this));
242 }
243
244 /**
245 * Returns the limit (as a percentage) below which small pie sections are
246 * aggregated.
247 *
248 * @return The limit percentage.
249 */
250 public double getLimit() {
251 return this.limit;
252 }
253
254 /**
255 * Sets the limit below which pie sections are aggregated.
256 * Set this to 0.0 if you don't want any aggregation to occur.
257 *
258 * @param limit the limit percent.
259 */
260 public void setLimit(double limit) {
261 this.limit = limit;
262 notifyListeners(new PlotChangeEvent(this));
263 }
264
265 /**
266 * Returns the key for aggregated items in the pie plots, if there are any.
267 * The default value is "Other".
268 *
269 * @return The aggregated items key.
270 *
271 * @since 1.0.2
272 */
273 public Comparable getAggregatedItemsKey() {
274 return this.aggregatedItemsKey;
275 }
276
277 /**
278 * Sets the key for aggregated items in the pie plots. You must ensure
279 * that this doesn't clash with any keys in the dataset.
280 *
281 * @param key the key (<code>null</code> not permitted).
282 *
283 * @since 1.0.2
284 */
285 public void setAggregatedItemsKey(Comparable key) {
286 if (key == null) {
287 throw new IllegalArgumentException("Null 'key' argument.");
288 }
289 this.aggregatedItemsKey = key;
290 notifyListeners(new PlotChangeEvent(this));
291 }
292
293 /**
294 * Returns the paint used to draw the pie section representing the
295 * aggregated items. The default value is <code>Color.lightGray</code>.
296 *
297 * @return The paint.
298 *
299 * @since 1.0.2
300 */
301 public Paint getAggregatedItemsPaint() {
302 return this.aggregatedItemsPaint;
303 }
304
305 /**
306 * Sets the paint used to draw the pie section representing the aggregated
307 * items and sends a {@link PlotChangeEvent} to all registered listeners.
308 *
309 * @param paint the paint (<code>null</code> not permitted).
310 *
311 * @since 1.0.2
312 */
313 public void setAggregatedItemsPaint(Paint paint) {
314 if (paint == null) {
315 throw new IllegalArgumentException("Null 'paint' argument.");
316 }
317 this.aggregatedItemsPaint = paint;
318 notifyListeners(new PlotChangeEvent(this));
319 }
320
321 /**
322 * Returns a short string describing the type of plot.
323 *
324 * @return The plot type.
325 */
326 public String getPlotType() {
327 return "Multiple Pie Plot";
328 // TODO: need to fetch this from localised resources
329 }
330
331 /**
332 * Draws the plot on a Java 2D graphics device (such as the screen or a
333 * printer).
334 *
335 * @param g2 the graphics device.
336 * @param area the area within which the plot should be drawn.
337 * @param anchor the anchor point (<code>null</code> permitted).
338 * @param parentState the state from the parent plot, if there is one.
339 * @param info collects info about the drawing.
340 */
341 public void draw(Graphics2D g2,
342 Rectangle2D area,
343 Point2D anchor,
344 PlotState parentState,
345 PlotRenderingInfo info) {
346
347
348 // adjust the drawing area for the plot insets (if any)...
349 RectangleInsets insets = getInsets();
350 insets.trim(area);
351 drawBackground(g2, area);
352 drawOutline(g2, area);
353
354 // check that there is some data to display...
355 if (DatasetUtilities.isEmptyOrNull(this.dataset)) {
356 drawNoDataMessage(g2, area);
357 return;
358 }
359
360 int pieCount = 0;
361 if (this.dataExtractOrder == TableOrder.BY_ROW) {
362 pieCount = this.dataset.getRowCount();
363 }
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
371 = (int) Math.ceil((double) pieCount / (double) displayCols);
372
373 // swap rows and columns to match plotArea shape
374 if (displayCols > displayRows && area.getWidth() < area.getHeight()) {
375 int temp = displayCols;
376 displayCols = displayRows;
377 displayRows = temp;
378 }
379
380 prefetchSectionPaints();
381
382 int x = (int) area.getX();
383 int y = (int) area.getY();
384 int width = ((int) area.getWidth()) / displayCols;
385 int height = ((int) area.getHeight()) / displayRows;
386 int row = 0;
387 int column = 0;
388 int diff = (displayRows * displayCols) - pieCount;
389 int xoffset = 0;
390 Rectangle rect = new Rectangle();
391
392 for (int pieIndex = 0; pieIndex < pieCount; pieIndex++) {
393 rect.setBounds(x + xoffset + (width * column), y + (height * row),
394 width, height);
395
396 String title = null;
397 if (this.dataExtractOrder == TableOrder.BY_ROW) {
398 title = this.dataset.getRowKey(pieIndex).toString();
399 }
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.createConsolidatedPieDataset(
410 dd, this.aggregatedItemsKey, this.limit);
411 }
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 }
426 else {
427 p = (Paint) this.sectionPaints.get(key);
428 }
429 piePlot.setSectionPaint(key, p);
430 }
431
432 ChartRenderingInfo subinfo = null;
433 if (info != null) {
434 subinfo = new ChartRenderingInfo();
435 }
436 this.pieChart.draw(g2, rect, subinfo);
437 if (info != null) {
438 info.getOwner().getEntityCollection().addAll(
439 subinfo.getEntityCollection());
440 info.addSubplotInfo(subinfo.getPlotInfo());
441 }
442
443 ++column;
444 if (column == displayCols) {
445 column = 0;
446 ++row;
447
448 if (row == displayRows - 1 && diff != 0) {
449 xoffset = (diff * width) / 2;
450 }
451 }
452 }
453
454 }
455
456 /**
457 * For each key in the dataset, check the <code>sectionPaints</code>
458 * cache to see if a paint is associated with that key and, if not,
459 * fetch one from the drawing supplier. These colors are cached so that
460 * the legend and all the subplots use consistent colors.
461 */
462 private void prefetchSectionPaints() {
463
464 // pre-fetch the colors for each key...this is because the subplots
465 // may not display every key, but we need the coloring to be
466 // consistent...
467
468 PiePlot piePlot = (PiePlot) getPieChart().getPlot();
469
470 if (this.dataExtractOrder == TableOrder.BY_ROW) {
471 // column keys provide potential keys for individual pies
472 for (int c = 0; c < this.dataset.getColumnCount(); c++) {
473 Comparable key = this.dataset.getColumnKey(c);
474 Paint p = piePlot.getSectionPaint(key);
475 if (p == null) {
476 p = (Paint) this.sectionPaints.get(key);
477 if (p == null) {
478 p = getDrawingSupplier().getNextPaint();
479 }
480 }
481 this.sectionPaints.put(key, p);
482 }
483 }
484 else {
485 // row keys provide potential keys for individual pies
486 for (int r = 0; r < this.dataset.getRowCount(); r++) {
487 Comparable key = this.dataset.getRowKey(r);
488 Paint p = piePlot.getSectionPaint(key);
489 if (p == null) {
490 p = (Paint) this.sectionPaints.get(key);
491 if (p == null) {
492 p = getDrawingSupplier().getNextPaint();
493 }
494 }
495 this.sectionPaints.put(key, p);
496 }
497 }
498
499 }
500
501 /**
502 * Returns a collection of legend items for the pie chart.
503 *
504 * @return The legend items.
505 */
506 public LegendItemCollection getLegendItems() {
507
508 LegendItemCollection result = new LegendItemCollection();
509
510 if (this.dataset != null) {
511 List keys = null;
512
513 prefetchSectionPaints();
514 if (this.dataExtractOrder == TableOrder.BY_ROW) {
515 keys = this.dataset.getColumnKeys();
516 }
517 else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
518 keys = this.dataset.getRowKeys();
519 }
520
521 if (keys != null) {
522 int section = 0;
523 Iterator iterator = keys.iterator();
524 while (iterator.hasNext()) {
525 Comparable key = (Comparable) iterator.next();
526 String label = key.toString();
527 String description = label;
528 Paint paint = (Paint) this.sectionPaints.get(key);
529 LegendItem item = new LegendItem(label, description,
530 null, null, Plot.DEFAULT_LEGEND_ITEM_CIRCLE,
531 paint, Plot.DEFAULT_OUTLINE_STROKE, paint);
532 item.setDataset(getDataset());
533 result.add(item);
534 section++;
535 }
536 }
537 if (this.limit > 0.0) {
538 result.add(new LegendItem(this.aggregatedItemsKey.toString(),
539 this.aggregatedItemsKey.toString(), null, null,
540 Plot.DEFAULT_LEGEND_ITEM_CIRCLE,
541 this.aggregatedItemsPaint,
542 Plot.DEFAULT_OUTLINE_STROKE,
543 this.aggregatedItemsPaint));
544 }
545 }
546 return result;
547 }
548
549 /**
550 * Tests this plot for equality with an arbitrary object. Note that the
551 * plot's dataset is not considered in the equality test.
552 *
553 * @param obj the object (<code>null</code> permitted).
554 *
555 * @return <code>true</code> if this plot is equal to <code>obj</code>, and
556 * <code>false</code> otherwise.
557 */
558 public boolean equals(Object obj) {
559 if (obj == this) {
560 return true;
561 }
562 if (!(obj instanceof MultiplePiePlot)) {
563 return false;
564 }
565 MultiplePiePlot that = (MultiplePiePlot) obj;
566 if (this.dataExtractOrder != that.dataExtractOrder) {
567 return false;
568 }
569 if (this.limit != that.limit) {
570 return false;
571 }
572 if (!this.aggregatedItemsKey.equals(that.aggregatedItemsKey)) {
573 return false;
574 }
575 if (!PaintUtilities.equal(this.aggregatedItemsPaint,
576 that.aggregatedItemsPaint)) {
577 return false;
578 }
579 if (!ObjectUtilities.equal(this.pieChart, that.pieChart)) {
580 return false;
581 }
582 if (!super.equals(obj)) {
583 return false;
584 }
585 return true;
586 }
587
588 /**
589 * Provides serialization support.
590 *
591 * @param stream the output stream.
592 *
593 * @throws IOException if there is an I/O error.
594 */
595 private void writeObject(ObjectOutputStream stream) 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
616 }