001: /* ===========================================================
002: * JFreeChart : a free chart library for the Java(tm) platform
003: * ===========================================================
004: *
005: * (C) Copyright 2000-2006, 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: * CombinedRangeCategoryPlot.java
029: * ------------------------------
030: * (C) Copyright 2003-2006, by Object Refinery Limited.
031: *
032: * Original Author: David Gilbert (for Object Refinery Limited);
033: * Contributor(s): Nicolas Brodu;
034: *
035: * $Id: CombinedRangeCategoryPlot.java,v 1.13.2.2 2006/09/13 10:32:36 mungady Exp $
036: *
037: * Changes:
038: * --------
039: * 16-May-2003 : Version 1 (DG);
040: * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
041: * 19-Aug-2003 : Implemented Cloneable (DG);
042: * 11-Sep-2003 : Fix cloning support (subplots) (NB);
043: * 15-Sep-2003 : Implemented PublicCloneable. Fixed errors in cloning and
044: * serialization (DG);
045: * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
046: * 17-Sep-2003 : Updated handling of 'clicks' (DG);
047: * 04-May-2004 : Added getter/setter methods for 'gap' attributes (DG);
048: * 12-Nov-2004 : Implements the new Zoomable interface (DG);
049: * 25-Nov-2004 : Small update to clone() implementation (DG);
050: * 21-Feb-2005 : Fixed bug in remove() method (id = 1121172) (DG);
051: * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
052: * items if set (DG);
053: * 05-May-2005 : Updated draw() method parameters (DG);
054: *
055: */
056:
057: package org.jfree.chart.plot;
058:
059: import java.awt.Graphics2D;
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.Serializable;
065: import java.util.Collections;
066: import java.util.Iterator;
067: import java.util.List;
068:
069: import org.jfree.chart.LegendItemCollection;
070: import org.jfree.chart.axis.AxisSpace;
071: import org.jfree.chart.axis.AxisState;
072: import org.jfree.chart.axis.NumberAxis;
073: import org.jfree.chart.axis.ValueAxis;
074: import org.jfree.chart.event.PlotChangeEvent;
075: import org.jfree.chart.event.PlotChangeListener;
076: import org.jfree.data.Range;
077: import org.jfree.ui.RectangleEdge;
078: import org.jfree.ui.RectangleInsets;
079: import org.jfree.util.ObjectUtilities;
080: import org.jfree.util.PublicCloneable;
081:
082: /**
083: * A combined category plot where the range axis is shared.
084: */
085: public class CombinedRangeCategoryPlot extends CategoryPlot implements
086: Zoomable, Cloneable, PublicCloneable, Serializable,
087: PlotChangeListener {
088:
089: /** For serialization. */
090: private static final long serialVersionUID = 7260210007554504515L;
091:
092: /** Storage for the subplot references. */
093: private List subplots;
094:
095: /** Total weight of all charts. */
096: private int totalWeight;
097:
098: /** The gap between subplots. */
099: private double gap;
100:
101: /** Temporary storage for the subplot areas. */
102: private transient Rectangle2D[] subplotArea; // TODO: move to plot state
103:
104: /**
105: * Default constructor.
106: */
107: public CombinedRangeCategoryPlot() {
108: this (new NumberAxis());
109: }
110:
111: /**
112: * Creates a new plot.
113: *
114: * @param rangeAxis the shared range axis.
115: */
116: public CombinedRangeCategoryPlot(ValueAxis rangeAxis) {
117: super (null, null, rangeAxis, null);
118: this .subplots = new java.util.ArrayList();
119: this .totalWeight = 0;
120: this .gap = 5.0;
121: }
122:
123: /**
124: * Returns the space between subplots.
125: *
126: * @return The gap (in Java2D units).
127: */
128: public double getGap() {
129: return this .gap;
130: }
131:
132: /**
133: * Sets the amount of space between subplots and sends a
134: * {@link PlotChangeEvent} to all registered listeners.
135: *
136: * @param gap the gap between subplots (in Java2D units).
137: */
138: public void setGap(double gap) {
139: this .gap = gap;
140: notifyListeners(new PlotChangeEvent(this ));
141: }
142:
143: /**
144: * Adds a subplot (with a default 'weight' of 1) and sends a
145: * {@link PlotChangeEvent} to all registered listeners.
146: * <br><br>
147: * You must ensure that the subplot has a non-null domain axis. The range
148: * axis for the subplot will be set to <code>null</code>.
149: *
150: * @param subplot the subplot (<code>null</code> not permitted).
151: */
152: public void add(CategoryPlot subplot) {
153: // defer argument checking
154: add(subplot, 1);
155: }
156:
157: /**
158: * Adds a subplot and sends a {@link PlotChangeEvent} to all registered
159: * listeners.
160: * <br><br>
161: * You must ensure that the subplot has a non-null domain axis. The range
162: * axis for the subplot will be set to <code>null</code>.
163: *
164: * @param subplot the subplot (<code>null</code> not permitted).
165: * @param weight the weight (must be >= 1).
166: */
167: public void add(CategoryPlot subplot, int weight) {
168: if (subplot == null) {
169: throw new IllegalArgumentException(
170: "Null 'subplot' argument.");
171: }
172: if (weight <= 0) {
173: throw new IllegalArgumentException("Require weight >= 1.");
174: }
175: // store the plot and its weight
176: subplot.setParent(this );
177: subplot.setWeight(weight);
178: subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
179: subplot.setRangeAxis(null);
180: subplot.setOrientation(getOrientation());
181: subplot.addChangeListener(this );
182: this .subplots.add(subplot);
183: this .totalWeight += weight;
184:
185: // configure the range axis...
186: ValueAxis axis = getRangeAxis();
187: if (axis != null) {
188: axis.configure();
189: }
190: notifyListeners(new PlotChangeEvent(this ));
191: }
192:
193: /**
194: * Removes a subplot from the combined chart.
195: *
196: * @param subplot the subplot (<code>null</code> not permitted).
197: */
198: public void remove(CategoryPlot subplot) {
199: if (subplot == null) {
200: throw new IllegalArgumentException(
201: " Null 'subplot' argument.");
202: }
203: int position = -1;
204: int size = this .subplots.size();
205: int i = 0;
206: while (position == -1 && i < size) {
207: if (this .subplots.get(i) == subplot) {
208: position = i;
209: }
210: i++;
211: }
212: if (position != -1) {
213: this .subplots.remove(position);
214: subplot.setParent(null);
215: subplot.removeChangeListener(this );
216: this .totalWeight -= subplot.getWeight();
217:
218: ValueAxis range = getRangeAxis();
219: if (range != null) {
220: range.configure();
221: }
222:
223: ValueAxis range2 = getRangeAxis(1);
224: if (range2 != null) {
225: range2.configure();
226: }
227: notifyListeners(new PlotChangeEvent(this ));
228: }
229: }
230:
231: /**
232: * Returns the list of subplots.
233: *
234: * @return The list (unmodifiable).
235: */
236: public List getSubplots() {
237: return Collections.unmodifiableList(this .subplots);
238: }
239:
240: /**
241: * Calculates the space required for the axes.
242: *
243: * @param g2 the graphics device.
244: * @param plotArea the plot area.
245: *
246: * @return The space required for the axes.
247: */
248: protected AxisSpace calculateAxisSpace(Graphics2D g2,
249: Rectangle2D plotArea) {
250:
251: AxisSpace space = new AxisSpace();
252: PlotOrientation orientation = getOrientation();
253:
254: // work out the space required by the domain axis...
255: AxisSpace fixed = getFixedRangeAxisSpace();
256: if (fixed != null) {
257: if (orientation == PlotOrientation.VERTICAL) {
258: space.setLeft(fixed.getLeft());
259: space.setRight(fixed.getRight());
260: } else if (orientation == PlotOrientation.HORIZONTAL) {
261: space.setTop(fixed.getTop());
262: space.setBottom(fixed.getBottom());
263: }
264: } else {
265: ValueAxis valueAxis = getRangeAxis();
266: RectangleEdge valueEdge = Plot.resolveRangeAxisLocation(
267: getRangeAxisLocation(), orientation);
268: if (valueAxis != null) {
269: space = valueAxis.reserveSpace(g2, this , plotArea,
270: valueEdge, space);
271: }
272: }
273:
274: Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
275: // work out the maximum height or width of the non-shared axes...
276: int n = this .subplots.size();
277:
278: // calculate plotAreas of all sub-plots, maximum vertical/horizontal
279: // axis width/height
280: this .subplotArea = new Rectangle2D[n];
281: double x = adjustedPlotArea.getX();
282: double y = adjustedPlotArea.getY();
283: double usableSize = 0.0;
284: if (orientation == PlotOrientation.VERTICAL) {
285: usableSize = adjustedPlotArea.getWidth() - this .gap
286: * (n - 1);
287: } else if (orientation == PlotOrientation.HORIZONTAL) {
288: usableSize = adjustedPlotArea.getHeight() - this .gap
289: * (n - 1);
290: }
291:
292: for (int i = 0; i < n; i++) {
293: CategoryPlot plot = (CategoryPlot) this .subplots.get(i);
294:
295: // calculate sub-plot area
296: if (orientation == PlotOrientation.VERTICAL) {
297: double w = usableSize * plot.getWeight()
298: / this .totalWeight;
299: this .subplotArea[i] = new Rectangle2D.Double(x, y, w,
300: adjustedPlotArea.getHeight());
301: x = x + w + this .gap;
302: } else if (orientation == PlotOrientation.HORIZONTAL) {
303: double h = usableSize * plot.getWeight()
304: / this .totalWeight;
305: this .subplotArea[i] = new Rectangle2D.Double(x, y,
306: adjustedPlotArea.getWidth(), h);
307: y = y + h + this .gap;
308: }
309:
310: AxisSpace subSpace = plot.calculateDomainAxisSpace(g2,
311: this .subplotArea[i], null);
312: space.ensureAtLeast(subSpace);
313:
314: }
315:
316: return space;
317: }
318:
319: /**
320: * Draws the plot on a Java 2D graphics device (such as the screen or a
321: * printer). Will perform all the placement calculations for each
322: * sub-plots and then tell these to draw themselves.
323: *
324: * @param g2 the graphics device.
325: * @param area the area within which the plot (including axis labels)
326: * should be drawn.
327: * @param anchor the anchor point (<code>null</code> permitted).
328: * @param parentState the parent state.
329: * @param info collects information about the drawing (<code>null</code>
330: * permitted).
331: */
332: public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
333: PlotState parentState, PlotRenderingInfo info) {
334:
335: // set up info collection...
336: if (info != null) {
337: info.setPlotArea(area);
338: }
339:
340: // adjust the drawing area for plot insets (if any)...
341: RectangleInsets insets = getInsets();
342: insets.trim(area);
343:
344: // calculate the data area...
345: AxisSpace space = calculateAxisSpace(g2, area);
346: Rectangle2D dataArea = space.shrink(area, null);
347:
348: // set the width and height of non-shared axis of all sub-plots
349: setFixedDomainAxisSpaceForSubplots(space);
350:
351: // draw the shared axis
352: ValueAxis axis = getRangeAxis();
353: RectangleEdge rangeEdge = getRangeAxisEdge();
354: double cursor = RectangleEdge.coordinate(dataArea, rangeEdge);
355: AxisState state = axis.draw(g2, cursor, area, dataArea,
356: rangeEdge, info);
357: if (parentState == null) {
358: parentState = new PlotState();
359: }
360: parentState.getSharedAxisStates().put(axis, state);
361:
362: // draw all the charts
363: for (int i = 0; i < this .subplots.size(); i++) {
364: CategoryPlot plot = (CategoryPlot) this .subplots.get(i);
365: PlotRenderingInfo subplotInfo = null;
366: if (info != null) {
367: subplotInfo = new PlotRenderingInfo(info.getOwner());
368: info.addSubplotInfo(subplotInfo);
369: }
370: plot.draw(g2, this .subplotArea[i], null, parentState,
371: subplotInfo);
372: }
373:
374: if (info != null) {
375: info.setDataArea(dataArea);
376: }
377:
378: }
379:
380: /**
381: * Sets the orientation for the plot (and all the subplots).
382: *
383: * @param orientation the orientation.
384: */
385: public void setOrientation(PlotOrientation orientation) {
386:
387: super .setOrientation(orientation);
388:
389: Iterator iterator = this .subplots.iterator();
390: while (iterator.hasNext()) {
391: CategoryPlot plot = (CategoryPlot) iterator.next();
392: plot.setOrientation(orientation);
393: }
394:
395: }
396:
397: /**
398: * Returns the range for the axis. This is the combined range of all the
399: * subplots.
400: *
401: * @param axis the axis.
402: *
403: * @return The range.
404: */
405: public Range getDataRange(ValueAxis axis) {
406:
407: Range result = null;
408: if (this .subplots != null) {
409: Iterator iterator = this .subplots.iterator();
410: while (iterator.hasNext()) {
411: CategoryPlot subplot = (CategoryPlot) iterator.next();
412: result = Range.combine(result, subplot
413: .getDataRange(axis));
414: }
415: }
416: return result;
417:
418: }
419:
420: /**
421: * Returns a collection of legend items for the plot.
422: *
423: * @return The legend items.
424: */
425: public LegendItemCollection getLegendItems() {
426: LegendItemCollection result = getFixedLegendItems();
427: if (result == null) {
428: result = new LegendItemCollection();
429: if (this .subplots != null) {
430: Iterator iterator = this .subplots.iterator();
431: while (iterator.hasNext()) {
432: CategoryPlot plot = (CategoryPlot) iterator.next();
433: LegendItemCollection more = plot.getLegendItems();
434: result.addAll(more);
435: }
436: }
437: }
438: return result;
439: }
440:
441: /**
442: * Sets the size (width or height, depending on the orientation of the
443: * plot) for the domain axis of each subplot.
444: *
445: * @param space the space.
446: */
447: protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) {
448:
449: Iterator iterator = this .subplots.iterator();
450: while (iterator.hasNext()) {
451: CategoryPlot plot = (CategoryPlot) iterator.next();
452: plot.setFixedDomainAxisSpace(space);
453: }
454:
455: }
456:
457: /**
458: * Handles a 'click' on the plot by updating the anchor value.
459: *
460: * @param x x-coordinate of the click.
461: * @param y y-coordinate of the click.
462: * @param info information about the plot's dimensions.
463: *
464: */
465: public void handleClick(int x, int y, PlotRenderingInfo info) {
466:
467: Rectangle2D dataArea = info.getDataArea();
468: if (dataArea.contains(x, y)) {
469: for (int i = 0; i < this .subplots.size(); i++) {
470: CategoryPlot subplot = (CategoryPlot) this .subplots
471: .get(i);
472: PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
473: subplot.handleClick(x, y, subplotInfo);
474: }
475: }
476:
477: }
478:
479: /**
480: * Receives a {@link PlotChangeEvent} and responds by notifying all
481: * listeners.
482: *
483: * @param event the event.
484: */
485: public void plotChanged(PlotChangeEvent event) {
486: notifyListeners(event);
487: }
488:
489: /**
490: * Tests the plot for equality with an arbitrary object.
491: *
492: * @param obj the object (<code>null</code> permitted).
493: *
494: * @return <code>true</code> or <code>false</code>.
495: */
496: public boolean equals(Object obj) {
497: if (obj == this ) {
498: return true;
499: }
500: if (!(obj instanceof CombinedRangeCategoryPlot)) {
501: return false;
502: }
503: if (!super .equals(obj)) {
504: return false;
505: }
506: CombinedRangeCategoryPlot that = (CombinedRangeCategoryPlot) obj;
507: if (!ObjectUtilities.equal(this .subplots, that.subplots)) {
508: return false;
509: }
510: if (this .totalWeight != that.totalWeight) {
511: return false;
512: }
513: if (this .gap != that.gap) {
514: return false;
515: }
516: return true;
517: }
518:
519: /**
520: * Returns a clone of the plot.
521: *
522: * @return A clone.
523: *
524: * @throws CloneNotSupportedException this class will not throw this
525: * exception, but subclasses (if any) might.
526: */
527: public Object clone() throws CloneNotSupportedException {
528: CombinedRangeCategoryPlot result = (CombinedRangeCategoryPlot) super
529: .clone();
530: result.subplots = (List) ObjectUtilities
531: .deepClone(this .subplots);
532: for (Iterator it = result.subplots.iterator(); it.hasNext();) {
533: Plot child = (Plot) it.next();
534: child.setParent(result);
535: }
536:
537: // after setting up all the subplots, the shared range axis may need
538: // reconfiguring
539: ValueAxis rangeAxis = result.getRangeAxis();
540: if (rangeAxis != null) {
541: rangeAxis.configure();
542: }
543:
544: return result;
545: }
546:
547: /**
548: * Provides serialization support.
549: *
550: * @param stream the input stream.
551: *
552: * @throws IOException if there is an I/O error.
553: * @throws ClassNotFoundException if there is a classpath problem.
554: */
555: private void readObject(ObjectInputStream stream)
556: throws IOException, ClassNotFoundException {
557:
558: stream.defaultReadObject();
559:
560: // the range axis is deserialized before the subplots, so its value
561: // range is likely to be incorrect...
562: ValueAxis rangeAxis = getRangeAxis();
563: if (rangeAxis != null) {
564: rangeAxis.configure();
565: }
566:
567: }
568:
569: }
|