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