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 * RingPlot.java
029 * -------------
030 * (C) Copyright 2004-2008, by Object Refinery Limited.
031 *
032 * Original Author: David Gilbert (for Object Refinery Limtied);
033 * Contributor(s): Christoph Beck (bug 2121818);
034 *
035 * Changes
036 * -------
037 * 08-Nov-2004 : Version 1 (DG);
038 * 22-Feb-2005 : Renamed DonutPlot --> RingPlot (DG);
039 * 06-Jun-2005 : Added default constructor and fixed equals() method to handle
040 * GradientPaint (DG);
041 * ------------- JFREECHART 1.0.x ---------------------------------------------
042 * 20-Dec-2005 : Fixed problem with entity shape (bug 1386328) (DG);
043 * 27-Sep-2006 : Updated drawItem() method for new lookup methods (DG);
044 * 12-Oct-2006 : Added configurable section depth (DG);
045 * 14-Feb-2007 : Added notification in setSectionDepth() method (DG);
046 * 23-Sep-2008 : Fix for bug 2121818 by Christoph Beck (DG);
047 *
048 */
049
050 package org.jfree.chart.plot;
051
052 import java.awt.BasicStroke;
053 import java.awt.Color;
054 import java.awt.Graphics2D;
055 import java.awt.Paint;
056 import java.awt.Shape;
057 import java.awt.Stroke;
058 import java.awt.geom.Arc2D;
059 import java.awt.geom.GeneralPath;
060 import java.awt.geom.Line2D;
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
067 import org.jfree.chart.entity.EntityCollection;
068 import org.jfree.chart.entity.PieSectionEntity;
069 import org.jfree.chart.event.PlotChangeEvent;
070 import org.jfree.chart.labels.PieToolTipGenerator;
071 import org.jfree.chart.urls.PieURLGenerator;
072 import org.jfree.data.general.PieDataset;
073 import org.jfree.io.SerialUtilities;
074 import org.jfree.ui.RectangleInsets;
075 import org.jfree.util.ObjectUtilities;
076 import org.jfree.util.PaintUtilities;
077 import org.jfree.util.Rotation;
078 import org.jfree.util.ShapeUtilities;
079 import org.jfree.util.UnitType;
080
081 /**
082 * A customised pie plot that leaves a hole in the middle.
083 */
084 public class RingPlot extends PiePlot implements Cloneable, Serializable {
085
086 /** For serialization. */
087 private static final long serialVersionUID = 1556064784129676620L;
088
089 /**
090 * A flag that controls whether or not separators are drawn between the
091 * sections of the chart.
092 */
093 private boolean separatorsVisible;
094
095 /** The stroke used to draw separators. */
096 private transient Stroke separatorStroke;
097
098 /** The paint used to draw separators. */
099 private transient Paint separatorPaint;
100
101 /**
102 * The length of the inner separator extension (as a percentage of the
103 * depth of the sections).
104 */
105 private double innerSeparatorExtension;
106
107 /**
108 * The length of the outer separator extension (as a percentage of the
109 * depth of the sections).
110 */
111 private double outerSeparatorExtension;
112
113 /**
114 * The depth of the section as a percentage of the diameter.
115 */
116 private double sectionDepth;
117
118 /**
119 * Creates a new plot with a <code>null</code> dataset.
120 */
121 public RingPlot() {
122 this(null);
123 }
124
125 /**
126 * Creates a new plot for the specified dataset.
127 *
128 * @param dataset the dataset (<code>null</code> permitted).
129 */
130 public RingPlot(PieDataset dataset) {
131 super(dataset);
132 this.separatorsVisible = true;
133 this.separatorStroke = new BasicStroke(0.5f);
134 this.separatorPaint = Color.gray;
135 this.innerSeparatorExtension = 0.20; // twenty percent
136 this.outerSeparatorExtension = 0.20; // twenty percent
137 this.sectionDepth = 0.20; // 20%
138 }
139
140 /**
141 * Returns a flag that indicates whether or not separators are drawn between
142 * the sections in the chart.
143 *
144 * @return A boolean.
145 *
146 * @see #setSeparatorsVisible(boolean)
147 */
148 public boolean getSeparatorsVisible() {
149 return this.separatorsVisible;
150 }
151
152 /**
153 * Sets the flag that controls whether or not separators are drawn between
154 * the sections in the chart, and sends a {@link PlotChangeEvent} to all
155 * registered listeners.
156 *
157 * @param visible the flag.
158 *
159 * @see #getSeparatorsVisible()
160 */
161 public void setSeparatorsVisible(boolean visible) {
162 this.separatorsVisible = visible;
163 fireChangeEvent();
164 }
165
166 /**
167 * Returns the separator stroke.
168 *
169 * @return The stroke (never <code>null</code>).
170 *
171 * @see #setSeparatorStroke(Stroke)
172 */
173 public Stroke getSeparatorStroke() {
174 return this.separatorStroke;
175 }
176
177 /**
178 * Sets the stroke used to draw the separator between sections and sends
179 * a {@link PlotChangeEvent} to all registered listeners.
180 *
181 * @param stroke the stroke (<code>null</code> not permitted).
182 *
183 * @see #getSeparatorStroke()
184 */
185 public void setSeparatorStroke(Stroke stroke) {
186 if (stroke == null) {
187 throw new IllegalArgumentException("Null 'stroke' argument.");
188 }
189 this.separatorStroke = stroke;
190 fireChangeEvent();
191 }
192
193 /**
194 * Returns the separator paint.
195 *
196 * @return The paint (never <code>null</code>).
197 *
198 * @see #setSeparatorPaint(Paint)
199 */
200 public Paint getSeparatorPaint() {
201 return this.separatorPaint;
202 }
203
204 /**
205 * Sets the paint used to draw the separator between sections and sends a
206 * {@link PlotChangeEvent} to all registered listeners.
207 *
208 * @param paint the paint (<code>null</code> not permitted).
209 *
210 * @see #getSeparatorPaint()
211 */
212 public void setSeparatorPaint(Paint paint) {
213 if (paint == null) {
214 throw new IllegalArgumentException("Null 'paint' argument.");
215 }
216 this.separatorPaint = paint;
217 fireChangeEvent();
218 }
219
220 /**
221 * Returns the length of the inner extension of the separator line that
222 * is drawn between sections, expressed as a percentage of the depth of
223 * the section.
224 *
225 * @return The inner separator extension (as a percentage).
226 *
227 * @see #setInnerSeparatorExtension(double)
228 */
229 public double getInnerSeparatorExtension() {
230 return this.innerSeparatorExtension;
231 }
232
233 /**
234 * Sets the length of the inner extension of the separator line that is
235 * drawn between sections, as a percentage of the depth of the
236 * sections, and sends a {@link PlotChangeEvent} to all registered
237 * listeners.
238 *
239 * @param percent the percentage.
240 *
241 * @see #getInnerSeparatorExtension()
242 * @see #setOuterSeparatorExtension(double)
243 */
244 public void setInnerSeparatorExtension(double percent) {
245 this.innerSeparatorExtension = percent;
246 fireChangeEvent();
247 }
248
249 /**
250 * Returns the length of the outer extension of the separator line that
251 * is drawn between sections, expressed as a percentage of the depth of
252 * the section.
253 *
254 * @return The outer separator extension (as a percentage).
255 *
256 * @see #setOuterSeparatorExtension(double)
257 */
258 public double getOuterSeparatorExtension() {
259 return this.outerSeparatorExtension;
260 }
261
262 /**
263 * Sets the length of the outer extension of the separator line that is
264 * drawn between sections, as a percentage of the depth of the
265 * sections, and sends a {@link PlotChangeEvent} to all registered
266 * listeners.
267 *
268 * @param percent the percentage.
269 *
270 * @see #getOuterSeparatorExtension()
271 */
272 public void setOuterSeparatorExtension(double percent) {
273 this.outerSeparatorExtension = percent;
274 fireChangeEvent();
275 }
276
277 /**
278 * Returns the depth of each section, expressed as a percentage of the
279 * plot radius.
280 *
281 * @return The depth of each section.
282 *
283 * @see #setSectionDepth(double)
284 * @since 1.0.3
285 */
286 public double getSectionDepth() {
287 return this.sectionDepth;
288 }
289
290 /**
291 * The section depth is given as percentage of the plot radius.
292 * Specifying 1.0 results in a straightforward pie chart.
293 *
294 * @param sectionDepth the section depth.
295 *
296 * @see #getSectionDepth()
297 * @since 1.0.3
298 */
299 public void setSectionDepth(double sectionDepth) {
300 this.sectionDepth = sectionDepth;
301 fireChangeEvent();
302 }
303
304 /**
305 * Initialises the plot state (which will store the total of all dataset
306 * values, among other things). This method is called once at the
307 * beginning of each drawing.
308 *
309 * @param g2 the graphics device.
310 * @param plotArea the plot area (<code>null</code> not permitted).
311 * @param plot the plot.
312 * @param index the secondary index (<code>null</code> for primary
313 * renderer).
314 * @param info collects chart rendering information for return to caller.
315 *
316 * @return A state object (maintains state information relevant to one
317 * chart drawing).
318 */
319 public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea,
320 PiePlot plot, Integer index, PlotRenderingInfo info) {
321
322 PiePlotState state = super.initialise(g2, plotArea, plot, index, info);
323 state.setPassesRequired(3);
324 return state;
325
326 }
327
328 /**
329 * Draws a single data item.
330 *
331 * @param g2 the graphics device (<code>null</code> not permitted).
332 * @param section the section index.
333 * @param dataArea the data plot area.
334 * @param state state information for one chart.
335 * @param currentPass the current pass index.
336 */
337 protected void drawItem(Graphics2D g2,
338 int section,
339 Rectangle2D dataArea,
340 PiePlotState state,
341 int currentPass) {
342
343 PieDataset dataset = getDataset();
344 Number n = dataset.getValue(section);
345 if (n == null) {
346 return;
347 }
348 double value = n.doubleValue();
349 double angle1 = 0.0;
350 double angle2 = 0.0;
351
352 Rotation direction = getDirection();
353 if (direction == Rotation.CLOCKWISE) {
354 angle1 = state.getLatestAngle();
355 angle2 = angle1 - value / state.getTotal() * 360.0;
356 }
357 else if (direction == Rotation.ANTICLOCKWISE) {
358 angle1 = state.getLatestAngle();
359 angle2 = angle1 + value / state.getTotal() * 360.0;
360 }
361 else {
362 throw new IllegalStateException("Rotation type not recognised.");
363 }
364
365 double angle = (angle2 - angle1);
366 if (Math.abs(angle) > getMinimumArcAngleToDraw()) {
367 Comparable key = getSectionKey(section);
368 double ep = 0.0;
369 double mep = getMaximumExplodePercent();
370 if (mep > 0.0) {
371 ep = getExplodePercent(key) / mep;
372 }
373 Rectangle2D arcBounds = getArcBounds(state.getPieArea(),
374 state.getExplodedPieArea(), angle1, angle, ep);
375 Arc2D.Double arc = new Arc2D.Double(arcBounds, angle1, angle,
376 Arc2D.OPEN);
377
378 // create the bounds for the inner arc
379 double depth = this.sectionDepth / 2.0;
380 RectangleInsets s = new RectangleInsets(UnitType.RELATIVE,
381 depth, depth, depth, depth);
382 Rectangle2D innerArcBounds = new Rectangle2D.Double();
383 innerArcBounds.setRect(arcBounds);
384 s.trim(innerArcBounds);
385 // calculate inner arc in reverse direction, for later
386 // GeneralPath construction
387 Arc2D.Double arc2 = new Arc2D.Double(innerArcBounds, angle1
388 + angle, -angle, Arc2D.OPEN);
389 GeneralPath path = new GeneralPath();
390 path.moveTo((float) arc.getStartPoint().getX(),
391 (float) arc.getStartPoint().getY());
392 path.append(arc.getPathIterator(null), false);
393 path.append(arc2.getPathIterator(null), true);
394 path.closePath();
395
396 Line2D separator = new Line2D.Double(arc2.getEndPoint(),
397 arc.getStartPoint());
398
399 if (currentPass == 0) {
400 Paint shadowPaint = getShadowPaint();
401 double shadowXOffset = getShadowXOffset();
402 double shadowYOffset = getShadowYOffset();
403 if (shadowPaint != null) {
404 Shape shadowArc = ShapeUtilities.createTranslatedShape(
405 path, (float) shadowXOffset, (float) shadowYOffset);
406 g2.setPaint(shadowPaint);
407 g2.fill(shadowArc);
408 }
409 }
410 else if (currentPass == 1) {
411 Paint paint = lookupSectionPaint(key);
412 g2.setPaint(paint);
413 g2.fill(path);
414 Paint outlinePaint = lookupSectionOutlinePaint(key);
415 Stroke outlineStroke = lookupSectionOutlineStroke(key);
416 if (outlinePaint != null && outlineStroke != null) {
417 g2.setPaint(outlinePaint);
418 g2.setStroke(outlineStroke);
419 g2.draw(path);
420 }
421
422 // add an entity for the pie section
423 if (state.getInfo() != null) {
424 EntityCollection entities = state.getEntityCollection();
425 if (entities != null) {
426 String tip = null;
427 PieToolTipGenerator toolTipGenerator
428 = getToolTipGenerator();
429 if (toolTipGenerator != null) {
430 tip = toolTipGenerator.generateToolTip(dataset,
431 key);
432 }
433 String url = null;
434 PieURLGenerator urlGenerator = getURLGenerator();
435 if (urlGenerator != null) {
436 url = urlGenerator.generateURL(dataset, key,
437 getPieIndex());
438 }
439 PieSectionEntity entity = new PieSectionEntity(path,
440 dataset, getPieIndex(), section, key, tip,
441 url);
442 entities.add(entity);
443 }
444 }
445 }
446 else if (currentPass == 2) {
447 if (this.separatorsVisible) {
448 Line2D extendedSeparator = extendLine(separator,
449 this.innerSeparatorExtension,
450 this.outerSeparatorExtension);
451 g2.setStroke(this.separatorStroke);
452 g2.setPaint(this.separatorPaint);
453 g2.draw(extendedSeparator);
454 }
455 }
456 }
457 state.setLatestAngle(angle2);
458 }
459
460 /**
461 * This method overrides the default value for cases where the ring plot
462 * is very thin. This fixes bug 2121818.
463 *
464 * @return The label link depth, as a percentage of the plot's radius.
465 */
466 protected double getLabelLinkDepth() {
467 return Math.min(super.getLabelLinkDepth(), getSectionDepth() / 2);
468 }
469
470 /**
471 * Tests this plot for equality with an arbitrary object.
472 *
473 * @param obj the object to test against (<code>null</code> permitted).
474 *
475 * @return A boolean.
476 */
477 public boolean equals(Object obj) {
478 if (this == obj) {
479 return true;
480 }
481 if (!(obj instanceof RingPlot)) {
482 return false;
483 }
484 RingPlot that = (RingPlot) obj;
485 if (this.separatorsVisible != that.separatorsVisible) {
486 return false;
487 }
488 if (!ObjectUtilities.equal(this.separatorStroke,
489 that.separatorStroke)) {
490 return false;
491 }
492 if (!PaintUtilities.equal(this.separatorPaint, that.separatorPaint)) {
493 return false;
494 }
495 if (this.innerSeparatorExtension != that.innerSeparatorExtension) {
496 return false;
497 }
498 if (this.outerSeparatorExtension != that.outerSeparatorExtension) {
499 return false;
500 }
501 if (this.sectionDepth != that.sectionDepth) {
502 return false;
503 }
504 return super.equals(obj);
505 }
506
507 /**
508 * Creates a new line by extending an existing line.
509 *
510 * @param line the line (<code>null</code> not permitted).
511 * @param startPercent the amount to extend the line at the start point
512 * end.
513 * @param endPercent the amount to extend the line at the end point end.
514 *
515 * @return A new line.
516 */
517 private Line2D extendLine(Line2D line, double startPercent,
518 double endPercent) {
519 if (line == null) {
520 throw new IllegalArgumentException("Null 'line' argument.");
521 }
522 double x1 = line.getX1();
523 double x2 = line.getX2();
524 double deltaX = x2 - x1;
525 double y1 = line.getY1();
526 double y2 = line.getY2();
527 double deltaY = y2 - y1;
528 x1 = x1 - (startPercent * deltaX);
529 y1 = y1 - (startPercent * deltaY);
530 x2 = x2 + (endPercent * deltaX);
531 y2 = y2 + (endPercent * deltaY);
532 return new Line2D.Double(x1, y1, x2, y2);
533 }
534
535 /**
536 * Provides serialization support.
537 *
538 * @param stream the output stream.
539 *
540 * @throws IOException if there is an I/O error.
541 */
542 private void writeObject(ObjectOutputStream stream) throws IOException {
543 stream.defaultWriteObject();
544 SerialUtilities.writeStroke(this.separatorStroke, stream);
545 SerialUtilities.writePaint(this.separatorPaint, stream);
546 }
547
548 /**
549 * Provides serialization support.
550 *
551 * @param stream the input stream.
552 *
553 * @throws IOException if there is an I/O error.
554 * @throws ClassNotFoundException if there is a classpath problem.
555 */
556 private void readObject(ObjectInputStream stream)
557 throws IOException, ClassNotFoundException {
558 stream.defaultReadObject();
559 this.separatorStroke = SerialUtilities.readStroke(stream);
560 this.separatorPaint = SerialUtilities.readPaint(stream);
561 }
562
563 }