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