Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors. 6: * 7: * Project Info: http://www.jfree.org/jfreechart/index.html 8: * 9: * This library is free software; you can redistribute it and/or modify it 10: * under the terms of the GNU Lesser General Public License as published by 11: * the Free Software Foundation; either version 2.1 of the License, or 12: * (at your option) any later version. 13: * 14: * This library is distributed in the hope that it will be useful, but 15: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17: * License for more details. 18: * 19: * You should have received a copy of the GNU Lesser General Public 20: * License along with this library; if not, write to the Free Software 21: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22: * USA. 23: * 24: * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 25: * in the United States and other countries.] 26: * 27: * ------------------------- 28: * CombinedDomainXYPlot.java 29: * ------------------------- 30: * (C) Copyright 2001-2007, by Bill Kelemen and Contributors. 31: * 32: * Original Author: Bill Kelemen; 33: * Contributor(s): David Gilbert (for Object Refinery Limited); 34: * Anthony Boulestreau; 35: * David Basten; 36: * Kevin Frechette (for ISTI); 37: * Nicolas Brodu; 38: * Petr Kubanek (bug 1606205); 39: * 40: * $Id: CombinedDomainXYPlot.java,v 1.9.2.5 2007/03/23 14:38:52 mungady Exp $ 41: * 42: * Changes: 43: * -------- 44: * 06-Dec-2001 : Version 1 (BK); 45: * 12-Dec-2001 : Removed unnecessary 'throws' clause from constructor (DG); 46: * 18-Dec-2001 : Added plotArea attribute and get/set methods (BK); 47: * 22-Dec-2001 : Fixed bug in chartChanged with multiple combinations of 48: * CombinedPlots (BK); 49: * 08-Jan-2002 : Moved to new package com.jrefinery.chart.combination (DG); 50: * 25-Feb-2002 : Updated import statements (DG); 51: * 28-Feb-2002 : Readded "this.plotArea = plotArea" that was deleted from 52: * draw() method (BK); 53: * 26-Mar-2002 : Added an empty zoom method (this method needs to be written so 54: * that combined plots will support zooming (DG); 55: * 29-Mar-2002 : Changed the method createCombinedAxis adding the creation of 56: * OverlaidSymbolicAxis and CombinedSymbolicAxis(AB); 57: * 23-Apr-2002 : Renamed CombinedPlot-->MultiXYPlot, and simplified the 58: * structure (DG); 59: * 23-May-2002 : Renamed (again) MultiXYPlot-->CombinedXYPlot (DG); 60: * 19-Jun-2002 : Added get/setGap() methods suggested by David Basten (DG); 61: * 25-Jun-2002 : Removed redundant imports (DG); 62: * 16-Jul-2002 : Draws shared axis after subplots (to fix missing gridlines), 63: * added overrides of 'setSeriesPaint()' and 'setXYItemRenderer()' 64: * that pass changes down to subplots (KF); 65: * 09-Oct-2002 : Added add(XYPlot) method (DG); 66: * 26-Mar-2003 : Implemented Serializable (DG); 67: * 16-May-2003 : Renamed CombinedXYPlot --> CombinedDomainXYPlot (DG); 68: * 04-Aug-2003 : Removed leftover code that was causing domain axis drawing 69: * problem (DG); 70: * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG); 71: * 21-Aug-2003 : Implemented Cloneable (DG); 72: * 11-Sep-2003 : Fix cloning support (subplots) (NB); 73: * 15-Sep-2003 : Fixed error in cloning (DG); 74: * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 75: * 17-Sep-2003 : Updated handling of 'clicks' (DG); 76: * 12-Nov-2004 : Implemented the new Zoomable interface (DG); 77: * 25-Nov-2004 : Small update to clone() implementation (DG); 78: * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend 79: * items if set (DG); 80: * 05-May-2005 : Removed unused draw() method (DG); 81: * ------------- JFREECHART 1.0.x --------------------------------------------- 82: * 23-Aug-2006 : Override setFixedRangeAxisSpace() to update subplots (DG); 83: * 06-Feb-2007 : Fixed bug 1606205, draw shared axis after subplots (DG); 84: * 23-Mar-2007 : Reverted previous patch (bug fix 1606205) (DG); 85: * 86: */ 87: 88: package org.jfree.chart.plot; 89: 90: import java.awt.Graphics2D; 91: import java.awt.geom.Point2D; 92: import java.awt.geom.Rectangle2D; 93: import java.io.Serializable; 94: import java.util.Collections; 95: import java.util.Iterator; 96: import java.util.List; 97: 98: import org.jfree.chart.LegendItemCollection; 99: import org.jfree.chart.axis.AxisSpace; 100: import org.jfree.chart.axis.AxisState; 101: import org.jfree.chart.axis.NumberAxis; 102: import org.jfree.chart.axis.ValueAxis; 103: import org.jfree.chart.event.PlotChangeEvent; 104: import org.jfree.chart.event.PlotChangeListener; 105: import org.jfree.chart.renderer.xy.XYItemRenderer; 106: import org.jfree.data.Range; 107: import org.jfree.ui.RectangleEdge; 108: import org.jfree.ui.RectangleInsets; 109: import org.jfree.util.ObjectUtilities; 110: import org.jfree.util.PublicCloneable; 111: 112: /** 113: * An extension of {@link XYPlot} that contains multiple subplots that share a 114: * common domain axis. 115: */ 116: public class CombinedDomainXYPlot extends XYPlot 117: implements Cloneable, PublicCloneable, 118: Serializable, 119: PlotChangeListener { 120: 121: /** For serialization. */ 122: private static final long serialVersionUID = -7765545541261907383L; 123: 124: /** Storage for the subplot references. */ 125: private List subplots; 126: 127: /** Total weight of all charts. */ 128: private int totalWeight = 0; 129: 130: /** The gap between subplots. */ 131: private double gap = 5.0; 132: 133: /** Temporary storage for the subplot areas. */ 134: private transient Rectangle2D[] subplotAreas; 135: // TODO: the subplot areas needs to be moved out of the plot into the plot 136: // state 137: 138: /** 139: * Default constructor. 140: */ 141: public CombinedDomainXYPlot() { 142: this(new NumberAxis()); 143: } 144: 145: /** 146: * Creates a new combined plot that shares a domain axis among multiple 147: * subplots. 148: * 149: * @param domainAxis the shared axis. 150: */ 151: public CombinedDomainXYPlot(ValueAxis domainAxis) { 152: 153: super( 154: null, // no data in the parent plot 155: domainAxis, 156: null, // no range axis 157: null // no rendereer 158: ); 159: 160: this.subplots = new java.util.ArrayList(); 161: 162: } 163: 164: /** 165: * Returns a string describing the type of plot. 166: * 167: * @return The type of plot. 168: */ 169: public String getPlotType() { 170: return "Combined_Domain_XYPlot"; 171: } 172: 173: /** 174: * Sets the orientation for the plot (also changes the orientation for all 175: * the subplots to match). 176: * 177: * @param orientation the orientation (<code>null</code> not allowed). 178: */ 179: public void setOrientation(PlotOrientation orientation) { 180: 181: super.setOrientation(orientation); 182: Iterator iterator = this.subplots.iterator(); 183: while (iterator.hasNext()) { 184: XYPlot plot = (XYPlot) iterator.next(); 185: plot.setOrientation(orientation); 186: } 187: 188: } 189: 190: /** 191: * Returns the range for the specified axis. This is the combined range 192: * of all the subplots. 193: * 194: * @param axis the axis. 195: * 196: * @return The range (possibly <code>null</code>). 197: */ 198: public Range getDataRange(ValueAxis axis) { 199: 200: Range result = null; 201: if (this.subplots != null) { 202: Iterator iterator = this.subplots.iterator(); 203: while (iterator.hasNext()) { 204: XYPlot subplot = (XYPlot) iterator.next(); 205: result = Range.combine(result, subplot.getDataRange(axis)); 206: } 207: } 208: return result; 209: 210: } 211: 212: /** 213: * Returns the gap between subplots, measured in Java2D units. 214: * 215: * @return The gap (in Java2D units). 216: */ 217: public double getGap() { 218: return this.gap; 219: } 220: 221: /** 222: * Sets the amount of space between subplots and sends a 223: * {@link PlotChangeEvent} to all registered listeners. 224: * 225: * @param gap the gap between subplots (in Java2D units). 226: */ 227: public void setGap(double gap) { 228: this.gap = gap; 229: notifyListeners(new PlotChangeEvent(this)); 230: } 231: 232: /** 233: * Adds a subplot (with a default 'weight' of 1) and sends a 234: * {@link PlotChangeEvent} to all registered listeners. 235: * <P> 236: * The domain axis for the subplot will be set to <code>null</code>. You 237: * must ensure that the subplot has a non-null range axis. 238: * 239: * @param subplot the subplot (<code>null</code> not permitted). 240: */ 241: public void add(XYPlot subplot) { 242: // defer argument checking 243: add(subplot, 1); 244: } 245: 246: /** 247: * Adds a subplot with the specified weight and sends a 248: * {@link PlotChangeEvent} to all registered listeners. The weight 249: * determines how much space is allocated to the subplot relative to all 250: * the other subplots. 251: * <P> 252: * The domain axis for the subplot will be set to <code>null</code>. You 253: * must ensure that the subplot has a non-null range axis. 254: * 255: * @param subplot the subplot (<code>null</code> not permitted). 256: * @param weight the weight (must be >= 1). 257: */ 258: public void add(XYPlot subplot, int weight) { 259: 260: if (subplot == null) { 261: throw new IllegalArgumentException("Null 'subplot' argument."); 262: } 263: if (weight <= 0) { 264: throw new IllegalArgumentException("Require weight >= 1."); 265: } 266: 267: // store the plot and its weight 268: subplot.setParent(this); 269: subplot.setWeight(weight); 270: subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0), false); 271: subplot.setDomainAxis(null); 272: subplot.addChangeListener(this); 273: this.subplots.add(subplot); 274: 275: // keep track of total weights 276: this.totalWeight += weight; 277: 278: ValueAxis axis = getDomainAxis(); 279: if (axis != null) { 280: axis.configure(); 281: } 282: 283: notifyListeners(new PlotChangeEvent(this)); 284: 285: } 286: 287: /** 288: * Removes a subplot from the combined chart and sends a 289: * {@link PlotChangeEvent} to all registered listeners. 290: * 291: * @param subplot the subplot (<code>null</code> not permitted). 292: */ 293: public void remove(XYPlot subplot) { 294: if (subplot == null) { 295: throw new IllegalArgumentException(" Null 'subplot' argument."); 296: } 297: int position = -1; 298: int size = this.subplots.size(); 299: int i = 0; 300: while (position == -1 && i < size) { 301: if (this.subplots.get(i) == subplot) { 302: position = i; 303: } 304: i++; 305: } 306: if (position != -1) { 307: this.subplots.remove(position); 308: subplot.setParent(null); 309: subplot.removeChangeListener(this); 310: this.totalWeight -= subplot.getWeight(); 311: 312: ValueAxis domain = getDomainAxis(); 313: if (domain != null) { 314: domain.configure(); 315: } 316: notifyListeners(new PlotChangeEvent(this)); 317: } 318: } 319: 320: /** 321: * Returns the list of subplots. 322: * 323: * @return An unmodifiable list of subplots. 324: */ 325: public List getSubplots() { 326: return Collections.unmodifiableList(this.subplots); 327: } 328: 329: /** 330: * Calculates the axis space required. 331: * 332: * @param g2 the graphics device. 333: * @param plotArea the plot area. 334: * 335: * @return The space. 336: */ 337: protected AxisSpace calculateAxisSpace(Graphics2D g2, 338: Rectangle2D plotArea) { 339: 340: AxisSpace space = new AxisSpace(); 341: PlotOrientation orientation = getOrientation(); 342: 343: // work out the space required by the domain axis... 344: AxisSpace fixed = getFixedDomainAxisSpace(); 345: if (fixed != null) { 346: if (orientation == PlotOrientation.HORIZONTAL) { 347: space.setLeft(fixed.getLeft()); 348: space.setRight(fixed.getRight()); 349: } 350: else if (orientation == PlotOrientation.VERTICAL) { 351: space.setTop(fixed.getTop()); 352: space.setBottom(fixed.getBottom()); 353: } 354: } 355: else { 356: ValueAxis xAxis = getDomainAxis(); 357: RectangleEdge xEdge = Plot.resolveDomainAxisLocation( 358: getDomainAxisLocation(), orientation); 359: if (xAxis != null) { 360: space = xAxis.reserveSpace(g2, this, plotArea, xEdge, space); 361: } 362: } 363: 364: Rectangle2D adjustedPlotArea = space.shrink(plotArea, null); 365: 366: // work out the maximum height or width of the non-shared axes... 367: int n = this.subplots.size(); 368: this.subplotAreas = new Rectangle2D[n]; 369: double x = adjustedPlotArea.getX(); 370: double y = adjustedPlotArea.getY(); 371: double usableSize = 0.0; 372: if (orientation == PlotOrientation.HORIZONTAL) { 373: usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1); 374: } 375: else if (orientation == PlotOrientation.VERTICAL) { 376: usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1); 377: } 378: 379: for (int i = 0; i < n; i++) { 380: XYPlot plot = (XYPlot) this.subplots.get(i); 381: 382: // calculate sub-plot area 383: if (orientation == PlotOrientation.HORIZONTAL) { 384: double w = usableSize * plot.getWeight() / this.totalWeight; 385: this.subplotAreas[i] = new Rectangle2D.Double(x, y, w, 386: adjustedPlotArea.getHeight()); 387: x = x + w + this.gap; 388: } 389: else if (orientation == PlotOrientation.VERTICAL) { 390: double h = usableSize * plot.getWeight() / this.totalWeight; 391: this.subplotAreas[i] = new Rectangle2D.Double(x, y, 392: adjustedPlotArea.getWidth(), h); 393: y = y + h + this.gap; 394: } 395: 396: AxisSpace subSpace = plot.calculateRangeAxisSpace(g2, 397: this.subplotAreas[i], null); 398: space.ensureAtLeast(subSpace); 399: 400: } 401: 402: return space; 403: } 404: 405: /** 406: * Draws the plot within the specified area on a graphics device. 407: * 408: * @param g2 the graphics device. 409: * @param area the plot area (in Java2D space). 410: * @param anchor an anchor point in Java2D space (<code>null</code> 411: * permitted). 412: * @param parentState the state from the parent plot, if there is one 413: * (<code>null</code> permitted). 414: * @param info collects chart drawing information (<code>null</code> 415: * permitted). 416: */ 417: public void draw(Graphics2D g2, 418: Rectangle2D area, 419: Point2D anchor, 420: PlotState parentState, 421: PlotRenderingInfo info) { 422: 423: // set up info collection... 424: if (info != null) { 425: info.setPlotArea(area); 426: } 427: 428: // adjust the drawing area for plot insets (if any)... 429: RectangleInsets insets = getInsets(); 430: insets.trim(area); 431: 432: AxisSpace space = calculateAxisSpace(g2, area); 433: Rectangle2D dataArea = space.shrink(area, null); 434: 435: // set the width and height of non-shared axis of all sub-plots 436: setFixedRangeAxisSpaceForSubplots(space); 437: 438: // draw the shared axis 439: ValueAxis axis = getDomainAxis(); 440: RectangleEdge edge = getDomainAxisEdge(); 441: double cursor = RectangleEdge.coordinate(dataArea, edge); 442: AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info); 443: if (parentState == null) { 444: parentState = new PlotState(); 445: } 446: parentState.getSharedAxisStates().put(axis, axisState); 447: 448: // draw all the subplots 449: for (int i = 0; i < this.subplots.size(); i++) { 450: XYPlot plot = (XYPlot) this.subplots.get(i); 451: PlotRenderingInfo subplotInfo = null; 452: if (info != null) { 453: subplotInfo = new PlotRenderingInfo(info.getOwner()); 454: info.addSubplotInfo(subplotInfo); 455: } 456: plot.draw(g2, this.subplotAreas[i], anchor, parentState, 457: subplotInfo); 458: } 459: 460: if (info != null) { 461: info.setDataArea(dataArea); 462: } 463: 464: } 465: 466: /** 467: * Returns a collection of legend items for the plot. 468: * 469: * @return The legend items. 470: */ 471: public LegendItemCollection getLegendItems() { 472: LegendItemCollection result = getFixedLegendItems(); 473: if (result == null) { 474: result = new LegendItemCollection(); 475: if (this.subplots != null) { 476: Iterator iterator = this.subplots.iterator(); 477: while (iterator.hasNext()) { 478: XYPlot plot = (XYPlot) iterator.next(); 479: LegendItemCollection more = plot.getLegendItems(); 480: result.addAll(more); 481: } 482: } 483: } 484: return result; 485: } 486: 487: /** 488: * Multiplies the range on the range axis/axes by the specified factor. 489: * 490: * @param factor the zoom factor. 491: * @param info the plot rendering info. 492: * @param source the source point. 493: */ 494: public void zoomRangeAxes(double factor, PlotRenderingInfo info, 495: Point2D source) { 496: XYPlot subplot = findSubplot(info, source); 497: if (subplot != null) { 498: subplot.zoomRangeAxes(factor, info, source); 499: } 500: } 501: 502: /** 503: * Zooms in on the range axes. 504: * 505: * @param lowerPercent the lower bound. 506: * @param upperPercent the upper bound. 507: * @param info the plot rendering info. 508: * @param source the source point. 509: */ 510: public void zoomRangeAxes(double lowerPercent, double upperPercent, 511: PlotRenderingInfo info, Point2D source) { 512: XYPlot subplot = findSubplot(info, source); 513: if (subplot != null) { 514: subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source); 515: } 516: } 517: 518: /** 519: * Returns the subplot (if any) that contains the (x, y) point (specified 520: * in Java2D space). 521: * 522: * @param info the chart rendering info. 523: * @param source the source point. 524: * 525: * @return A subplot (possibly <code>null</code>). 526: */ 527: public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) { 528: XYPlot result = null; 529: int subplotIndex = info.getSubplotIndex(source); 530: if (subplotIndex >= 0) { 531: result = (XYPlot) this.subplots.get(subplotIndex); 532: } 533: return result; 534: } 535: 536: /** 537: * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are 538: * notified that the plot has been modified. 539: * <P> 540: * Note: usually you will want to set the renderer independently for each 541: * subplot, which is NOT what this method does. 542: * 543: * @param renderer the new renderer. 544: */ 545: public void setRenderer(XYItemRenderer renderer) { 546: 547: super.setRenderer(renderer); // not strictly necessary, since the 548: // renderer set for the 549: // parent plot is not used 550: 551: Iterator iterator = this.subplots.iterator(); 552: while (iterator.hasNext()) { 553: XYPlot plot = (XYPlot) iterator.next(); 554: plot.setRenderer(renderer); 555: } 556: 557: } 558: 559: /** 560: * Sets the fixed range axis space. 561: * 562: * @param space the space (<code>null</code> permitted). 563: */ 564: public void setFixedRangeAxisSpace(AxisSpace space) { 565: super.setFixedRangeAxisSpace(space); 566: setFixedRangeAxisSpaceForSubplots(space); 567: this.notifyListeners(new PlotChangeEvent(this)); 568: } 569: 570: /** 571: * Sets the size (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 setFixedRangeAxisSpaceForSubplots(AxisSpace space) { 577: 578: Iterator iterator = this.subplots.iterator(); 579: while (iterator.hasNext()) { 580: XYPlot plot = (XYPlot) iterator.next(); 581: plot.setFixedRangeAxisSpace(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: Rectangle2D dataArea = info.getDataArea(); 595: if (dataArea.contains(x, y)) { 596: for (int i = 0; i < this.subplots.size(); i++) { 597: XYPlot subplot = (XYPlot) this.subplots.get(i); 598: PlotRenderingInfo subplotInfo = info.getSubplotInfo(i); 599: subplot.handleClick(x, y, subplotInfo); 600: } 601: } 602: } 603: 604: /** 605: * Receives a {@link PlotChangeEvent} and responds by notifying all 606: * listeners. 607: * 608: * @param event the event. 609: */ 610: public void plotChanged(PlotChangeEvent event) { 611: notifyListeners(event); 612: } 613: 614: /** 615: * Tests this plot for equality with another object. 616: * 617: * @param obj the other object. 618: * 619: * @return <code>true</code> or <code>false</code>. 620: */ 621: public boolean equals(Object obj) { 622: 623: if (obj == null) { 624: return false; 625: } 626: 627: if (obj == this) { 628: return true; 629: } 630: 631: if (!(obj instanceof CombinedDomainXYPlot)) { 632: return false; 633: } 634: if (!super.equals(obj)) { 635: return false; 636: } 637: 638: CombinedDomainXYPlot p = (CombinedDomainXYPlot) obj; 639: if (this.totalWeight != p.totalWeight) { 640: return false; 641: } 642: if (this.gap != p.gap) { 643: return false; 644: } 645: if (!ObjectUtilities.equal(this.subplots, p.subplots)) { 646: return false; 647: } 648: 649: return true; 650: } 651: 652: /** 653: * Returns a clone of the annotation. 654: * 655: * @return A clone. 656: * 657: * @throws CloneNotSupportedException this class will not throw this 658: * exception, but subclasses (if any) might. 659: */ 660: public Object clone() throws CloneNotSupportedException { 661: 662: CombinedDomainXYPlot result = (CombinedDomainXYPlot) super.clone(); 663: result.subplots = (List) ObjectUtilities.deepClone(this.subplots); 664: for (Iterator it = result.subplots.iterator(); it.hasNext();) { 665: Plot child = (Plot) it.next(); 666: child.setParent(result); 667: } 668: 669: // after setting up all the subplots, the shared domain axis may need 670: // reconfiguring 671: ValueAxis domainAxis = result.getDomainAxis(); 672: if (domainAxis != null) { 673: domainAxis.configure(); 674: } 675: 676: return result; 677: 678: } 679: 680: }