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,
086: Serializable {
087:
088: /** For serialization. */
089: private static final long serialVersionUID = 1556064784129676620L;
090:
091: /**
092: * A flag that controls whether or not separators are drawn between the
093: * sections of the chart.
094: */
095: private boolean separatorsVisible;
096:
097: /** The stroke used to draw separators. */
098: private transient Stroke separatorStroke;
099:
100: /** The paint used to draw separators. */
101: private transient Paint separatorPaint;
102:
103: /**
104: * The length of the inner separator extension (as a percentage of the
105: * depth of the sections).
106: */
107: private double innerSeparatorExtension;
108:
109: /**
110: * The length of the outer separator extension (as a percentage of the
111: * depth of the sections).
112: */
113: private double outerSeparatorExtension;
114:
115: /**
116: * The depth of the section as a percentage of the diameter.
117: */
118: private double sectionDepth;
119:
120: /**
121: * Creates a new plot with a <code>null</code> dataset.
122: */
123: public RingPlot() {
124: this (null);
125: }
126:
127: /**
128: * Creates a new plot for the specified dataset.
129: *
130: * @param dataset the dataset (<code>null</code> permitted).
131: */
132: public RingPlot(PieDataset dataset) {
133: super (dataset);
134: this .separatorsVisible = true;
135: this .separatorStroke = new BasicStroke(0.5f);
136: this .separatorPaint = Color.gray;
137: this .innerSeparatorExtension = 0.20; // twenty percent
138: this .outerSeparatorExtension = 0.20; // twenty percent
139: this .sectionDepth = 0.20; // 20%
140: }
141:
142: /**
143: * Returns a flag that indicates whether or not separators are drawn between
144: * the sections in the chart.
145: *
146: * @return A boolean.
147: *
148: * @see #setSeparatorsVisible(boolean)
149: */
150: public boolean getSeparatorsVisible() {
151: return this .separatorsVisible;
152: }
153:
154: /**
155: * Sets the flag that controls whether or not separators are drawn between
156: * the sections in the chart, and sends a {@link PlotChangeEvent} to all
157: * registered listeners.
158: *
159: * @param visible the flag.
160: *
161: * @see #getSeparatorsVisible()
162: */
163: public void setSeparatorsVisible(boolean visible) {
164: this .separatorsVisible = visible;
165: notifyListeners(new PlotChangeEvent(this ));
166: }
167:
168: /**
169: * Returns the separator stroke.
170: *
171: * @return The stroke (never <code>null</code>).
172: *
173: * @see #setSeparatorStroke(Stroke)
174: */
175: public Stroke getSeparatorStroke() {
176: return this .separatorStroke;
177: }
178:
179: /**
180: * Sets the stroke used to draw the separator between sections and sends
181: * a {@link PlotChangeEvent} to all registered listeners.
182: *
183: * @param stroke the stroke (<code>null</code> not permitted).
184: *
185: * @see #getSeparatorStroke()
186: */
187: public void setSeparatorStroke(Stroke stroke) {
188: if (stroke == null) {
189: throw new IllegalArgumentException(
190: "Null 'stroke' argument.");
191: }
192: this .separatorStroke = stroke;
193: notifyListeners(new PlotChangeEvent(this ));
194: }
195:
196: /**
197: * Returns the separator paint.
198: *
199: * @return The paint (never <code>null</code>).
200: *
201: * @see #setSeparatorPaint(Paint)
202: */
203: public Paint getSeparatorPaint() {
204: return this .separatorPaint;
205: }
206:
207: /**
208: * Sets the paint used to draw the separator between sections and sends a
209: * {@link PlotChangeEvent} to all registered listeners.
210: *
211: * @param paint the paint (<code>null</code> not permitted).
212: *
213: * @see #getSeparatorPaint()
214: */
215: public void setSeparatorPaint(Paint paint) {
216: if (paint == null) {
217: throw new IllegalArgumentException("Null 'paint' argument.");
218: }
219: this .separatorPaint = paint;
220: notifyListeners(new PlotChangeEvent(this ));
221: }
222:
223: /**
224: * Returns the length of the inner extension of the separator line that
225: * is drawn between sections, expressed as a percentage of the depth of
226: * the section.
227: *
228: * @return The inner separator extension (as a percentage).
229: *
230: * @see #setInnerSeparatorExtension(double)
231: */
232: public double getInnerSeparatorExtension() {
233: return this .innerSeparatorExtension;
234: }
235:
236: /**
237: * Sets the length of the inner extension of the separator line that is
238: * drawn between sections, as a percentage of the depth of the
239: * sections, and sends a {@link PlotChangeEvent} to all registered
240: * listeners.
241: *
242: * @param percent the percentage.
243: *
244: * @see #getInnerSeparatorExtension()
245: * @see #setOuterSeparatorExtension(double)
246: */
247: public void setInnerSeparatorExtension(double percent) {
248: this .innerSeparatorExtension = percent;
249: notifyListeners(new PlotChangeEvent(this ));
250: }
251:
252: /**
253: * Returns the length of the outer extension of the separator line that
254: * is drawn between sections, expressed as a percentage of the depth of
255: * the section.
256: *
257: * @return The outer separator extension (as a percentage).
258: *
259: * @see #setOuterSeparatorExtension(double)
260: */
261: public double getOuterSeparatorExtension() {
262: return this .outerSeparatorExtension;
263: }
264:
265: /**
266: * Sets the length of the outer extension of the separator line that is
267: * drawn between sections, as a percentage of the depth of the
268: * sections, and sends a {@link PlotChangeEvent} to all registered
269: * listeners.
270: *
271: * @param percent the percentage.
272: *
273: * @see #getOuterSeparatorExtension()
274: */
275: public void setOuterSeparatorExtension(double percent) {
276: this .outerSeparatorExtension = percent;
277: notifyListeners(new PlotChangeEvent(this ));
278: }
279:
280: /**
281: * Returns the depth of each section, expressed as a percentage of the
282: * plot radius.
283: *
284: * @return The depth of each section.
285: *
286: * @see #setSectionDepth(double)
287: * @since 1.0.3
288: */
289: public double getSectionDepth() {
290: return this .sectionDepth;
291: }
292:
293: /**
294: * The section depth is given as percentage of the plot radius.
295: * Specifying 1.0 results in a straightforward pie chart.
296: *
297: * @param sectionDepth the section depth.
298: *
299: * @see #getSectionDepth()
300: * @since 1.0.3
301: */
302: public void setSectionDepth(double sectionDepth) {
303: this .sectionDepth = sectionDepth;
304: notifyListeners(new PlotChangeEvent(this ));
305: }
306:
307: /**
308: * Initialises the plot state (which will store the total of all dataset
309: * values, among other things). This method is called once at the
310: * beginning of each drawing.
311: *
312: * @param g2 the graphics device.
313: * @param plotArea the plot area (<code>null</code> not permitted).
314: * @param plot the plot.
315: * @param index the secondary index (<code>null</code> for primary
316: * renderer).
317: * @param info collects chart rendering information for return to caller.
318: *
319: * @return A state object (maintains state information relevant to one
320: * chart drawing).
321: */
322: public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea,
323: PiePlot plot, Integer index, PlotRenderingInfo info) {
324:
325: PiePlotState state = super .initialise(g2, plotArea, plot,
326: index, info);
327: state.setPassesRequired(3);
328: return state;
329:
330: }
331:
332: /**
333: * Draws a single data item.
334: *
335: * @param g2 the graphics device (<code>null</code> not permitted).
336: * @param section the section index.
337: * @param dataArea the data plot area.
338: * @param state state information for one chart.
339: * @param currentPass the current pass index.
340: */
341: protected void drawItem(Graphics2D g2, int section,
342: Rectangle2D dataArea, PiePlotState state, 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: } else if (direction == Rotation.ANTICLOCKWISE) {
358: angle1 = state.getLatestAngle();
359: angle2 = angle1 + value / state.getTotal() * 360.0;
360: } else {
361: throw new IllegalStateException(
362: "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,
376: angle, 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(), (float) arc
391: .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
405: .createTranslatedShape(path,
406: (float) shadowXOffset,
407: (float) shadowYOffset);
408: g2.setPaint(shadowPaint);
409: g2.fill(shadowArc);
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
426: .getEntityCollection();
427: if (entities != null) {
428: String tip = null;
429: PieToolTipGenerator toolTipGenerator = getToolTipGenerator();
430: if (toolTipGenerator != null) {
431: tip = toolTipGenerator.generateToolTip(
432: dataset, key);
433: }
434: String url = null;
435: PieURLGenerator urlGenerator = getURLGenerator();
436: if (urlGenerator != null) {
437: url = urlGenerator.generateURL(dataset,
438: key, getPieIndex());
439: }
440: PieSectionEntity entity = new PieSectionEntity(
441: path, dataset, getPieIndex(), section,
442: key, tip, url);
443: entities.add(entity);
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: * Tests this plot for equality with an arbitrary object.
462: *
463: * @param obj the object to test against (<code>null</code> permitted).
464: *
465: * @return A boolean.
466: */
467: public boolean equals(Object obj) {
468: if (this == obj) {
469: return true;
470: }
471: if (!(obj instanceof RingPlot)) {
472: return false;
473: }
474: RingPlot that = (RingPlot) obj;
475: if (this .separatorsVisible != that.separatorsVisible) {
476: return false;
477: }
478: if (!ObjectUtilities.equal(this .separatorStroke,
479: that.separatorStroke)) {
480: return false;
481: }
482: if (!PaintUtilities.equal(this .separatorPaint,
483: 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)
534: throws IOException {
535: stream.defaultWriteObject();
536: SerialUtilities.writeStroke(this .separatorStroke, stream);
537: SerialUtilities.writePaint(this .separatorPaint, stream);
538: }
539:
540: /**
541: * Provides serialization support.
542: *
543: * @param stream the input stream.
544: *
545: * @throws IOException if there is an I/O error.
546: * @throws ClassNotFoundException if there is a classpath problem.
547: */
548: private void readObject(ObjectInputStream stream)
549: throws IOException, ClassNotFoundException {
550: stream.defaultReadObject();
551: this.separatorStroke = SerialUtilities.readStroke(stream);
552: this.separatorPaint = SerialUtilities.readPaint(stream);
553: }
554:
555: }
|