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