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: * CombinedRangeXYPlot.java
029: * ------------------------
030: * (C) Copyright 2001-2007, by Bill Kelemen and Contributors.
031: *
032: * Original Author: Bill Kelemen;
033: * Contributor(s): David Gilbert (for Object Refinery Limited);
034: * Anthony Boulestreau;
035: * David Basten;
036: * Kevin Frechette (for ISTI);
037: * Arnaud Lelievre;
038: * Nicolas Brodu;
039: * Petr Kubanek (bug 1606205);
040: *
041: * $Id: CombinedRangeXYPlot.java,v 1.10.2.5 2007/04/17 11:13:25 mungady Exp $
042: *
043: * Changes:
044: * --------
045: * 06-Dec-2001 : Version 1 (BK);
046: * 12-Dec-2001 : Removed unnecessary 'throws' clause from constructor (DG);
047: * 18-Dec-2001 : Added plotArea attribute and get/set methods (BK);
048: * 22-Dec-2001 : Fixed bug in chartChanged with multiple combinations of
049: * CombinedPlots (BK);
050: * 08-Jan-2002 : Moved to new package com.jrefinery.chart.combination (DG);
051: * 25-Feb-2002 : Updated import statements (DG);
052: * 28-Feb-2002 : Readded "this.plotArea = plotArea" that was deleted from
053: * draw() method (BK);
054: * 26-Mar-2002 : Added an empty zoom method (this method needs to be written
055: * so that combined plots will support zooming (DG);
056: * 29-Mar-2002 : Changed the method createCombinedAxis adding the creation of
057: * OverlaidSymbolicAxis and CombinedSymbolicAxis(AB);
058: * 23-Apr-2002 : Renamed CombinedPlot-->MultiXYPlot, and simplified the
059: * structure (DG);
060: * 23-May-2002 : Renamed (again) MultiXYPlot-->CombinedXYPlot (DG);
061: * 19-Jun-2002 : Added get/setGap() methods suggested by David Basten (DG);
062: * 25-Jun-2002 : Removed redundant imports (DG);
063: * 16-Jul-2002 : Draws shared axis after subplots (to fix missing gridlines),
064: * added overrides of 'setSeriesPaint()' and 'setXYItemRenderer()'
065: * that pass changes down to subplots (KF);
066: * 09-Oct-2002 : Added add(XYPlot) method (DG);
067: * 26-Mar-2003 : Implemented Serializable (DG);
068: * 16-May-2003 : Renamed CombinedXYPlot --> CombinedRangeXYPlot (DG);
069: * 26-Jun-2003 : Fixed bug 755547 (DG);
070: * 16-Jul-2003 : Removed getSubPlots() method (duplicate of getSubplots()) (DG);
071: * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
072: * 21-Aug-2003 : Implemented Cloneable (DG);
073: * 08-Sep-2003 : Added internationalization via use of properties
074: * resourceBundle (RFE 690236) (AL);
075: * 11-Sep-2003 : Fix cloning support (subplots) (NB);
076: * 15-Sep-2003 : Fixed error in cloning (DG);
077: * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
078: * 17-Sep-2003 : Updated handling of 'clicks' (DG);
079: * 12-Nov-2004 : Implements the new Zoomable interface (DG);
080: * 25-Nov-2004 : Small update to clone() implementation (DG);
081: * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
082: * items if set (DG);
083: * 05-May-2005 : Removed unused draw() method (DG);
084: * ------------- JFREECHART 1.0.x ---------------------------------------------
085: * 13-Sep-2006 : Updated API docs (DG);
086: * 06-Feb-2007 : Fixed bug 1606205, draw shared axis after subplots (DG);
087: * 23-Mar-2007 : Reverted previous patch (DG);
088: * 17-Apr-2007 : Added null argument checks to findSubplot() (DG);
089: *
090: */
091:
092: package org.jfree.chart.plot;
093:
094: import java.awt.Graphics2D;
095: import java.awt.geom.Point2D;
096: import java.awt.geom.Rectangle2D;
097: import java.io.Serializable;
098: import java.util.Collections;
099: import java.util.Iterator;
100: import java.util.List;
101:
102: import org.jfree.chart.LegendItemCollection;
103: import org.jfree.chart.axis.AxisSpace;
104: import org.jfree.chart.axis.AxisState;
105: import org.jfree.chart.axis.NumberAxis;
106: import org.jfree.chart.axis.ValueAxis;
107: import org.jfree.chart.event.PlotChangeEvent;
108: import org.jfree.chart.event.PlotChangeListener;
109: import org.jfree.chart.renderer.xy.XYItemRenderer;
110: import org.jfree.data.Range;
111: import org.jfree.ui.RectangleEdge;
112: import org.jfree.ui.RectangleInsets;
113: import org.jfree.util.ObjectUtilities;
114: import org.jfree.util.PublicCloneable;
115:
116: /**
117: * An extension of {@link XYPlot} that contains multiple subplots that share a
118: * common range axis.
119: */
120: public class CombinedRangeXYPlot extends XYPlot implements Zoomable,
121: Cloneable, PublicCloneable, Serializable, PlotChangeListener {
122:
123: /** For serialization. */
124: private static final long serialVersionUID = -5177814085082031168L;
125:
126: /** Storage for the subplot references. */
127: private List subplots;
128:
129: /** Total weight of all charts. */
130: private int totalWeight = 0;
131:
132: /** The gap between subplots. */
133: private double gap = 5.0;
134:
135: /** Temporary storage for the subplot areas. */
136: private transient Rectangle2D[] subplotAreas;
137:
138: /**
139: * Default constructor.
140: */
141: public CombinedRangeXYPlot() {
142: this (new NumberAxis());
143: }
144:
145: /**
146: * Creates a new plot.
147: *
148: * @param rangeAxis the shared axis.
149: */
150: public CombinedRangeXYPlot(ValueAxis rangeAxis) {
151:
152: super (null, // no data in the parent plot
153: null, rangeAxis, null);
154:
155: this .subplots = new java.util.ArrayList();
156:
157: }
158:
159: /**
160: * Returns a string describing the type of plot.
161: *
162: * @return The type of plot.
163: */
164: public String getPlotType() {
165: return localizationResources.getString("Combined_Range_XYPlot");
166: }
167:
168: /**
169: * Returns the space between subplots.
170: *
171: * @return The gap
172: */
173: public double getGap() {
174: return this .gap;
175: }
176:
177: /**
178: * Sets the amount of space between subplots.
179: *
180: * @param gap the gap between subplots
181: */
182: public void setGap(double gap) {
183: this .gap = gap;
184: }
185:
186: /**
187: * Adds a subplot, with a default 'weight' of 1.
188: * <br><br>
189: * You must ensure that the subplot has a non-null domain axis. The range
190: * axis for the subplot will be set to <code>null</code>.
191: *
192: * @param subplot the subplot.
193: */
194: public void add(XYPlot subplot) {
195: add(subplot, 1);
196: }
197:
198: /**
199: * Adds a subplot with a particular weight (greater than or equal to one).
200: * The weight determines how much space is allocated to the subplot
201: * relative to all the other subplots.
202: * <br><br>
203: * You must ensure that the subplot has a non-null domain axis. The range
204: * axis for the subplot will be set to <code>null</code>.
205: *
206: * @param subplot the subplot.
207: * @param weight the weight (must be 1 or greater).
208: */
209: public void add(XYPlot subplot, int weight) {
210:
211: // verify valid weight
212: if (weight <= 0) {
213: String msg = "The 'weight' must be positive.";
214: throw new IllegalArgumentException(msg);
215: }
216:
217: // store the plot and its weight
218: subplot.setParent(this );
219: subplot.setWeight(weight);
220: subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
221: subplot.setRangeAxis(null);
222: subplot.addChangeListener(this );
223: this .subplots.add(subplot);
224:
225: // keep track of total weights
226: this .totalWeight += weight;
227: configureRangeAxes();
228: notifyListeners(new PlotChangeEvent(this ));
229:
230: }
231:
232: /**
233: * Removes a subplot from the combined chart.
234: *
235: * @param subplot the subplot (<code>null</code> not permitted).
236: */
237: public void remove(XYPlot subplot) {
238: if (subplot == null) {
239: throw new IllegalArgumentException(
240: " Null 'subplot' argument.");
241: }
242: int position = -1;
243: int size = this .subplots.size();
244: int i = 0;
245: while (position == -1 && i < size) {
246: if (this .subplots.get(i) == subplot) {
247: position = i;
248: }
249: i++;
250: }
251: if (position != -1) {
252: subplot.setParent(null);
253: subplot.removeChangeListener(this );
254: this .totalWeight -= subplot.getWeight();
255: configureRangeAxes();
256: notifyListeners(new PlotChangeEvent(this ));
257: }
258: }
259:
260: /**
261: * Returns a list of the subplots.
262: *
263: * @return The list (unmodifiable).
264: */
265: public List getSubplots() {
266: return Collections.unmodifiableList(this .subplots);
267: }
268:
269: /**
270: * Calculates the space required for the axes.
271: *
272: * @param g2 the graphics device.
273: * @param plotArea the plot area.
274: *
275: * @return The space required for the axes.
276: */
277: protected AxisSpace calculateAxisSpace(Graphics2D g2,
278: Rectangle2D plotArea) {
279:
280: AxisSpace space = new AxisSpace();
281: PlotOrientation orientation = getOrientation();
282:
283: // work out the space required by the domain axis...
284: AxisSpace fixed = getFixedRangeAxisSpace();
285: if (fixed != null) {
286: if (orientation == PlotOrientation.VERTICAL) {
287: space.setLeft(fixed.getLeft());
288: space.setRight(fixed.getRight());
289: } else if (orientation == PlotOrientation.HORIZONTAL) {
290: space.setTop(fixed.getTop());
291: space.setBottom(fixed.getBottom());
292: }
293: } else {
294: ValueAxis valueAxis = getRangeAxis();
295: RectangleEdge valueEdge = Plot.resolveRangeAxisLocation(
296: getRangeAxisLocation(), orientation);
297: if (valueAxis != null) {
298: space = valueAxis.reserveSpace(g2, this , plotArea,
299: valueEdge, space);
300: }
301: }
302:
303: Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
304: // work out the maximum height or width of the non-shared axes...
305: int n = this .subplots.size();
306:
307: // calculate plotAreas of all sub-plots, maximum vertical/horizontal
308: // axis width/height
309: this .subplotAreas = new Rectangle2D[n];
310: double x = adjustedPlotArea.getX();
311: double y = adjustedPlotArea.getY();
312: double usableSize = 0.0;
313: if (orientation == PlotOrientation.VERTICAL) {
314: usableSize = adjustedPlotArea.getWidth() - this .gap
315: * (n - 1);
316: } else if (orientation == PlotOrientation.HORIZONTAL) {
317: usableSize = adjustedPlotArea.getHeight() - this .gap
318: * (n - 1);
319: }
320:
321: for (int i = 0; i < n; i++) {
322: XYPlot plot = (XYPlot) this .subplots.get(i);
323:
324: // calculate sub-plot area
325: if (orientation == PlotOrientation.VERTICAL) {
326: double w = usableSize * plot.getWeight()
327: / this .totalWeight;
328: this .subplotAreas[i] = new Rectangle2D.Double(x, y, w,
329: adjustedPlotArea.getHeight());
330: x = x + w + this .gap;
331: } else if (orientation == PlotOrientation.HORIZONTAL) {
332: double h = usableSize * plot.getWeight()
333: / this .totalWeight;
334: this .subplotAreas[i] = new Rectangle2D.Double(x, y,
335: adjustedPlotArea.getWidth(), h);
336: y = y + h + this .gap;
337: }
338:
339: AxisSpace subSpace = plot.calculateDomainAxisSpace(g2,
340: this .subplotAreas[i], null);
341: space.ensureAtLeast(subSpace);
342:
343: }
344:
345: return space;
346: }
347:
348: /**
349: * Draws the plot within the specified area on a graphics device.
350: *
351: * @param g2 the graphics device.
352: * @param area the plot area (in Java2D space).
353: * @param anchor an anchor point in Java2D space (<code>null</code>
354: * permitted).
355: * @param parentState the state from the parent plot, if there is one
356: * (<code>null</code> permitted).
357: * @param info collects chart drawing information (<code>null</code>
358: * permitted).
359: */
360: public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
361: PlotState parentState, PlotRenderingInfo info) {
362:
363: // set up info collection...
364: if (info != null) {
365: info.setPlotArea(area);
366: }
367:
368: // adjust the drawing area for plot insets (if any)...
369: RectangleInsets insets = getInsets();
370: insets.trim(area);
371:
372: AxisSpace space = calculateAxisSpace(g2, area);
373: Rectangle2D dataArea = space.shrink(area, null);
374: //this.axisOffset.trim(dataArea);
375:
376: // set the width and height of non-shared axis of all sub-plots
377: setFixedDomainAxisSpaceForSubplots(space);
378:
379: // draw the shared axis
380: ValueAxis axis = getRangeAxis();
381: RectangleEdge edge = getRangeAxisEdge();
382: double cursor = RectangleEdge.coordinate(dataArea, edge);
383: AxisState axisState = axis.draw(g2, cursor, area, dataArea,
384: edge, info);
385:
386: if (parentState == null) {
387: parentState = new PlotState();
388: }
389: parentState.getSharedAxisStates().put(axis, axisState);
390:
391: // draw all the charts
392: for (int i = 0; i < this .subplots.size(); i++) {
393: XYPlot plot = (XYPlot) this .subplots.get(i);
394: PlotRenderingInfo subplotInfo = null;
395: if (info != null) {
396: subplotInfo = new PlotRenderingInfo(info.getOwner());
397: info.addSubplotInfo(subplotInfo);
398: }
399: plot.draw(g2, this .subplotAreas[i], anchor, parentState,
400: subplotInfo);
401: }
402:
403: if (info != null) {
404: info.setDataArea(dataArea);
405: }
406:
407: }
408:
409: /**
410: * Returns a collection of legend items for the plot.
411: *
412: * @return The legend items.
413: */
414: public LegendItemCollection getLegendItems() {
415: LegendItemCollection result = getFixedLegendItems();
416: if (result == null) {
417: result = new LegendItemCollection();
418:
419: if (this .subplots != null) {
420: Iterator iterator = this .subplots.iterator();
421: while (iterator.hasNext()) {
422: XYPlot plot = (XYPlot) iterator.next();
423: LegendItemCollection more = plot.getLegendItems();
424: result.addAll(more);
425: }
426: }
427: }
428: return result;
429: }
430:
431: /**
432: * Multiplies the range on the domain axis/axes by the specified factor.
433: *
434: * @param factor the zoom factor.
435: * @param info the plot rendering info (<code>null</code> not permitted).
436: * @param source the source point (<code>null</code> not permitted).
437: */
438: public void zoomDomainAxes(double factor, PlotRenderingInfo info,
439: Point2D source) {
440: // delegate 'info' and 'source' argument checks...
441: XYPlot subplot = findSubplot(info, source);
442: if (subplot != null) {
443: subplot.zoomDomainAxes(factor, info, source);
444: } else {
445: // if the source point doesn't fall within a subplot, we do the
446: // zoom on all subplots...
447: Iterator iterator = getSubplots().iterator();
448: while (iterator.hasNext()) {
449: subplot = (XYPlot) iterator.next();
450: subplot.zoomDomainAxes(factor, info, source);
451: }
452: }
453: }
454:
455: /**
456: * Zooms in on the domain axes.
457: *
458: * @param lowerPercent the lower bound.
459: * @param upperPercent the upper bound.
460: * @param info the plot rendering info (<code>null</code> not permitted).
461: * @param source the source point (<code>null</code> not permitted).
462: */
463: public void zoomDomainAxes(double lowerPercent,
464: double upperPercent, PlotRenderingInfo info, Point2D source) {
465: // delegate 'info' and 'source' argument checks...
466: XYPlot subplot = findSubplot(info, source);
467: if (subplot != null) {
468: subplot.zoomDomainAxes(lowerPercent, upperPercent, info,
469: source);
470: } else {
471: // if the source point doesn't fall within a subplot, we do the
472: // zoom on all subplots...
473: Iterator iterator = getSubplots().iterator();
474: while (iterator.hasNext()) {
475: subplot = (XYPlot) iterator.next();
476: subplot.zoomDomainAxes(lowerPercent, upperPercent,
477: info, source);
478: }
479: }
480: }
481:
482: /**
483: * Returns the subplot (if any) that contains the (x, y) point (specified
484: * in Java2D space).
485: *
486: * @param info the chart rendering info (<code>null</code> not permitted).
487: * @param source the source point (<code>null</code> not permitted).
488: *
489: * @return A subplot (possibly <code>null</code>).
490: */
491: public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) {
492: if (info == null) {
493: throw new IllegalArgumentException("Null 'info' argument.");
494: }
495: if (source == null) {
496: throw new IllegalArgumentException(
497: "Null 'source' argument.");
498: }
499: XYPlot result = null;
500: int subplotIndex = info.getSubplotIndex(source);
501: if (subplotIndex >= 0) {
502: result = (XYPlot) this .subplots.get(subplotIndex);
503: }
504: return result;
505: }
506:
507: /**
508: * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are
509: * notified that the plot has been modified.
510: * <P>
511: * Note: usually you will want to set the renderer independently for each
512: * subplot, which is NOT what this method does.
513: *
514: * @param renderer the new renderer.
515: */
516: public void setRenderer(XYItemRenderer renderer) {
517:
518: super .setRenderer(renderer); // not strictly necessary, since the
519: // renderer set for the
520: // parent plot is not used
521:
522: Iterator iterator = this .subplots.iterator();
523: while (iterator.hasNext()) {
524: XYPlot plot = (XYPlot) iterator.next();
525: plot.setRenderer(renderer);
526: }
527:
528: }
529:
530: /**
531: * Sets the orientation for the plot (and all its subplots).
532: *
533: * @param orientation the orientation.
534: */
535: public void setOrientation(PlotOrientation orientation) {
536:
537: super .setOrientation(orientation);
538:
539: Iterator iterator = this .subplots.iterator();
540: while (iterator.hasNext()) {
541: XYPlot plot = (XYPlot) iterator.next();
542: plot.setOrientation(orientation);
543: }
544:
545: }
546:
547: /**
548: * Returns the range for the axis. This is the combined range of all the
549: * subplots.
550: *
551: * @param axis the axis.
552: *
553: * @return The range.
554: */
555: public Range getDataRange(ValueAxis axis) {
556:
557: Range result = null;
558: if (this .subplots != null) {
559: Iterator iterator = this .subplots.iterator();
560: while (iterator.hasNext()) {
561: XYPlot subplot = (XYPlot) iterator.next();
562: result = Range.combine(result, subplot
563: .getDataRange(axis));
564: }
565: }
566: return result;
567:
568: }
569:
570: /**
571: * Sets the space (width or height, depending on the orientation of the
572: * plot) for the domain axis of each subplot.
573: *
574: * @param space the space.
575: */
576: protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) {
577:
578: Iterator iterator = this .subplots.iterator();
579: while (iterator.hasNext()) {
580: XYPlot plot = (XYPlot) iterator.next();
581: plot.setFixedDomainAxisSpace(space);
582: }
583:
584: }
585:
586: /**
587: * Handles a 'click' on the plot by updating the anchor values...
588: *
589: * @param x x-coordinate, where the click occured.
590: * @param y y-coordinate, where the click occured.
591: * @param info object containing information about the plot dimensions.
592: */
593: public void handleClick(int x, int y, PlotRenderingInfo info) {
594:
595: Rectangle2D dataArea = info.getDataArea();
596: if (dataArea.contains(x, y)) {
597: for (int i = 0; i < this .subplots.size(); i++) {
598: XYPlot subplot = (XYPlot) this .subplots.get(i);
599: PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
600: subplot.handleClick(x, y, subplotInfo);
601: }
602: }
603:
604: }
605:
606: /**
607: * Receives a {@link PlotChangeEvent} and responds by notifying all
608: * listeners.
609: *
610: * @param event the event.
611: */
612: public void plotChanged(PlotChangeEvent event) {
613: notifyListeners(event);
614: }
615:
616: /**
617: * Tests this plot for equality with another object.
618: *
619: * @param obj the other object.
620: *
621: * @return <code>true</code> or <code>false</code>.
622: */
623: public boolean equals(Object obj) {
624:
625: if (obj == this ) {
626: return true;
627: }
628:
629: if (!(obj instanceof CombinedRangeXYPlot)) {
630: return false;
631: }
632: if (!super .equals(obj)) {
633: return false;
634: }
635: CombinedRangeXYPlot that = (CombinedRangeXYPlot) obj;
636: if (!ObjectUtilities.equal(this .subplots, that.subplots)) {
637: return false;
638: }
639: if (this .totalWeight != that.totalWeight) {
640: return false;
641: }
642: if (this .gap != that.gap) {
643: return false;
644: }
645: return true;
646: }
647:
648: /**
649: * Returns a clone of the plot.
650: *
651: * @return A clone.
652: *
653: * @throws CloneNotSupportedException this class will not throw this
654: * exception, but subclasses (if any) might.
655: */
656: public Object clone() throws CloneNotSupportedException {
657:
658: CombinedRangeXYPlot result = (CombinedRangeXYPlot) super
659: .clone();
660: result.subplots = (List) ObjectUtilities
661: .deepClone(this .subplots);
662: for (Iterator it = result.subplots.iterator(); it.hasNext();) {
663: Plot child = (Plot) it.next();
664: child.setParent(result);
665: }
666:
667: // after setting up all the subplots, the shared range axis may need
668: // reconfiguring
669: ValueAxis rangeAxis = result.getRangeAxis();
670: if (rangeAxis != null) {
671: rangeAxis.configure();
672: }
673:
674: return result;
675: }
676:
677: }
|