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: * 27-Nov-2007 : Modified setFixedRangeAxisSpaceForSubplots() so as not to 85: * trigger change event in subplots (DG); 86: * 87: */ 88: 89: package org.jfree.chart.plot; 90: 91: import java.awt.Graphics2D; 92: import java.awt.geom.Point2D; 93: import java.awt.geom.Rectangle2D; 94: import java.io.Serializable; 95: import java.util.Collections; 96: import java.util.Iterator; 97: import java.util.List; 98: 99: import org.jfree.chart.LegendItemCollection; 100: import org.jfree.chart.axis.AxisSpace; 101: import org.jfree.chart.axis.AxisState; 102: import org.jfree.chart.axis.NumberAxis; 103: import org.jfree.chart.axis.ValueAxis; 104: import org.jfree.chart.event.PlotChangeEvent; 105: import org.jfree.chart.event.PlotChangeListener; 106: import org.jfree.chart.renderer.xy.XYItemRenderer; 107: import org.jfree.data.Range; 108: import org.jfree.ui.RectangleEdge; 109: import org.jfree.ui.RectangleInsets; 110: import org.jfree.util.ObjectUtilities; 111: import org.jfree.util.PublicCloneable; 112: 113: /** 114: * An extension of {@link XYPlot} that contains multiple subplots that share a 115: * common domain axis. 116: */ 117: public class CombinedDomainXYPlot extends XYPlot 118: implements Cloneable, PublicCloneable, 119: Serializable, 120: PlotChangeListener { 121: 122: /** For serialization. */ 123: private static final long serialVersionUID = -7765545541261907383L; 124: 125: /** Storage for the subplot references. */ 126: private List subplots; 127: 128: /** Total weight of all charts. */ 129: private int totalWeight = 0; 130: 131: /** The gap between subplots. */ 132: private double gap = 5.0; 133: 134: /** Temporary storage for the subplot areas. */ 135: private transient Rectangle2D[] subplotAreas; 136: // TODO: the subplot areas needs to be moved out of the plot into the plot 137: // state 138: 139: /** 140: * Default constructor. 141: */ 142: public CombinedDomainXYPlot() { 143: this(new NumberAxis()); 144: } 145: 146: /** 147: * Creates a new combined plot that shares a domain axis among multiple 148: * subplots. 149: * 150: * @param domainAxis the shared axis. 151: */ 152: public CombinedDomainXYPlot(ValueAxis domainAxis) { 153: 154: super( 155: null, // no data in the parent plot 156: domainAxis, 157: null, // no range axis 158: null // no rendereer 159: ); 160: 161: this.subplots = new java.util.ArrayList(); 162: 163: } 164: 165: /** 166: * Returns a string describing the type of plot. 167: * 168: * @return The type of plot. 169: */ 170: public String getPlotType() { 171: return "Combined_Domain_XYPlot"; 172: } 173: 174: /** 175: * Sets the orientation for the plot (also changes the orientation for all 176: * the subplots to match). 177: * 178: * @param orientation the orientation (<code>null</code> not allowed). 179: */ 180: public void setOrientation(PlotOrientation orientation) { 181: 182: super.setOrientation(orientation); 183: Iterator iterator = this.subplots.iterator(); 184: while (iterator.hasNext()) { 185: XYPlot plot = (XYPlot) iterator.next(); 186: plot.setOrientation(orientation); 187: } 188: 189: } 190: 191: /** 192: * Returns the range for the specified axis. This is the combined range 193: * of all the subplots. 194: * 195: * @param axis the axis. 196: * 197: * @return The range (possibly <code>null</code>). 198: */ 199: public Range getDataRange(ValueAxis axis) { 200: 201: Range result = null; 202: if (this.subplots != null) { 203: Iterator iterator = this.subplots.iterator(); 204: while (iterator.hasNext()) { 205: XYPlot subplot = (XYPlot) iterator.next(); 206: result = Range.combine(result, subplot.getDataRange(axis)); 207: } 208: } 209: return result; 210: 211: } 212: 213: /** 214: * Returns the gap between subplots, measured in Java2D units. 215: * 216: * @return The gap (in Java2D units). 217: */ 218: public double getGap() { 219: return this.gap; 220: } 221: 222: /** 223: * Sets the amount of space between subplots and sends a 224: * {@link PlotChangeEvent} to all registered listeners. 225: * 226: * @param gap the gap between subplots (in Java2D units). 227: */ 228: public void setGap(double gap) { 229: this.gap = gap; 230: notifyListeners(new PlotChangeEvent(this)); 231: } 232: 233: /** 234: * Adds a subplot (with a default 'weight' of 1) and sends a 235: * {@link PlotChangeEvent} to all registered listeners. 236: * <P> 237: * The domain axis for the subplot will be set to <code>null</code>. You 238: * must ensure that the subplot has a non-null range axis. 239: * 240: * @param subplot the subplot (<code>null</code> not permitted). 241: */ 242: public void add(XYPlot subplot) { 243: // defer argument checking 244: add(subplot, 1); 245: } 246: 247: /** 248: * Adds a subplot with the specified weight and sends a 249: * {@link PlotChangeEvent} to all registered listeners. The weight 250: * determines how much space is allocated to the subplot relative to all 251: * the other subplots. 252: * <P> 253: * The domain axis for the subplot will be set to <code>null</code>. You 254: * must ensure that the subplot has a non-null range axis. 255: * 256: * @param subplot the subplot (<code>null</code> not permitted). 257: * @param weight the weight (must be >= 1). 258: */ 259: public void add(XYPlot subplot, int weight) { 260: 261: if (subplot == null) { 262: throw new IllegalArgumentException("Null 'subplot' argument."); 263: } 264: if (weight <= 0) { 265: throw new IllegalArgumentException("Require weight >= 1."); 266: } 267: 268: // store the plot and its weight 269: subplot.setParent(this); 270: subplot.setWeight(weight); 271: subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0), false); 272: subplot.setDomainAxis(null); 273: subplot.addChangeListener(this); 274: this.subplots.add(subplot); 275: 276: // keep track of total weights 277: this.totalWeight += weight; 278: 279: ValueAxis axis = getDomainAxis(); 280: if (axis != null) { 281: axis.configure(); 282: } 283: 284: notifyListeners(new PlotChangeEvent(this)); 285: 286: } 287: 288: /** 289: * Removes a subplot from the combined chart and sends a 290: * {@link PlotChangeEvent} to all registered listeners. 291: * 292: * @param subplot the subplot (<code>null</code> not permitted). 293: */ 294: public void remove(XYPlot subplot) { 295: if (subplot == null) { 296: throw new IllegalArgumentException(" Null 'subplot' argument."); 297: } 298: int position = -1; 299: int size = this.subplots.size(); 300: int i = 0; 301: while (position == -1 && i < size) { 302: if (this.subplots.get(i) == subplot) { 303: position = i; 304: } 305: i++; 306: } 307: if (position != -1) { 308: this.subplots.remove(position); 309: subplot.setParent(null); 310: subplot.removeChangeListener(this); 311: this.totalWeight -= subplot.getWeight(); 312: 313: ValueAxis domain = getDomainAxis(); 314: if (domain != null) { 315: domain.configure(); 316: } 317: notifyListeners(new PlotChangeEvent(this)); 318: } 319: } 320: 321: /** 322: * Returns the list of subplots. 323: * 324: * @return An unmodifiable list of subplots. 325: */ 326: public List getSubplots() { 327: return Collections.unmodifiableList(this.subplots); 328: } 329: 330: /** 331: * Calculates the axis space required. 332: * 333: * @param g2 the graphics device. 334: * @param plotArea the plot area. 335: * 336: * @return The space. 337: */ 338: protected AxisSpace calculateAxisSpace(Graphics2D g2, 339: Rectangle2D plotArea) { 340: 341: AxisSpace space = new AxisSpace(); 342: PlotOrientation orientation = getOrientation(); 343: 344: // work out the space required by the domain axis... 345: AxisSpace fixed = getFixedDomainAxisSpace(); 346: if (fixed != null) { 347: if (orientation == PlotOrientation.HORIZONTAL) { 348: space.setLeft(fixed.getLeft()); 349: space.setRight(fixed.getRight()); 350: } 351: else if (orientation == PlotOrientation.VERTICAL) { 352: space.setTop(fixed.getTop()); 353: space.setBottom(fixed.getBottom()); 354: } 355: } 356: else { 357: ValueAxis xAxis = getDomainAxis(); 358: RectangleEdge xEdge = Plot.resolveDomainAxisLocation( 359: getDomainAxisLocation(), orientation); 360: if (xAxis != null) { 361: space = xAxis.reserveSpace(g2, this, plotArea, xEdge, space); 362: } 363: } 364: 365: Rectangle2D adjustedPlotArea = space.shrink(plotArea, null); 366: 367: // work out the maximum height or width of the non-shared axes... 368: int n = this.subplots.size(); 369: this.subplotAreas = new Rectangle2D[n]; 370: double x = adjustedPlotArea.getX(); 371: double y = adjustedPlotArea.getY(); 372: double usableSize = 0.0; 373: if (orientation == PlotOrientation.HORIZONTAL) { 374: usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1); 375: } 376: else if (orientation == PlotOrientation.VERTICAL) { 377: usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1); 378: } 379: 380: for (int i = 0; i < n; i++) { 381: XYPlot plot = (XYPlot) this.subplots.get(i); 382: 383: // calculate sub-plot area 384: if (orientation == PlotOrientation.HORIZONTAL) { 385: double w = usableSize * plot.getWeight() / this.totalWeight; 386: this.subplotAreas[i] = new Rectangle2D.Double(x, y, w, 387: adjustedPlotArea.getHeight()); 388: x = x + w + this.gap; 389: } 390: else if (orientation == PlotOrientation.VERTICAL) { 391: double h = usableSize * plot.getWeight() / this.totalWeight; 392: this.subplotAreas[i] = new Rectangle2D.Double(x, y, 393: adjustedPlotArea.getWidth(), h); 394: y = y + h + this.gap; 395: } 396: 397: AxisSpace subSpace = plot.calculateRangeAxisSpace(g2, 398: this.subplotAreas[i], null); 399: space.ensureAtLeast(subSpace); 400: 401: } 402: 403: return space; 404: } 405: 406: /** 407: * Draws the plot within the specified area on a graphics device. 408: * 409: * @param g2 the graphics device. 410: * @param area the plot area (in Java2D space). 411: * @param anchor an anchor point in Java2D space (<code>null</code> 412: * permitted). 413: * @param parentState the state from the parent plot, if there is one 414: * (<code>null</code> permitted). 415: * @param info collects chart drawing information (<code>null</code> 416: * permitted). 417: */ 418: public void draw(Graphics2D g2, 419: Rectangle2D area, 420: Point2D anchor, 421: PlotState parentState, 422: PlotRenderingInfo info) { 423: 424: // set up info collection... 425: if (info != null) { 426: info.setPlotArea(area); 427: } 428: 429: // adjust the drawing area for plot insets (if any)... 430: RectangleInsets insets = getInsets(); 431: insets.trim(area); 432: 433: AxisSpace space = calculateAxisSpace(g2, area); 434: Rectangle2D dataArea = space.shrink(area, null); 435: 436: // set the width and height of non-shared axis of all sub-plots 437: setFixedRangeAxisSpaceForSubplots(space); 438: 439: // draw the shared axis 440: ValueAxis axis = getDomainAxis(); 441: RectangleEdge edge = getDomainAxisEdge(); 442: double cursor = RectangleEdge.coordinate(dataArea, edge); 443: AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info); 444: if (parentState == null) { 445: parentState = new PlotState(); 446: } 447: parentState.getSharedAxisStates().put(axis, axisState); 448: 449: // draw all the subplots 450: for (int i = 0; i < this.subplots.size(); i++) { 451: XYPlot plot = (XYPlot) this.subplots.get(i); 452: PlotRenderingInfo subplotInfo = null; 453: if (info != null) { 454: subplotInfo = new PlotRenderingInfo(info.getOwner()); 455: info.addSubplotInfo(subplotInfo); 456: } 457: plot.draw(g2, this.subplotAreas[i], anchor, parentState, 458: subplotInfo); 459: } 460: 461: if (info != null) { 462: info.setDataArea(dataArea); 463: } 464: 465: } 466: 467: /** 468: * Returns a collection of legend items for the plot. 469: * 470: * @return The legend items. 471: */ 472: public LegendItemCollection getLegendItems() { 473: LegendItemCollection result = getFixedLegendItems(); 474: if (result == null) { 475: result = new LegendItemCollection(); 476: if (this.subplots != null) { 477: Iterator iterator = this.subplots.iterator(); 478: while (iterator.hasNext()) { 479: XYPlot plot = (XYPlot) iterator.next(); 480: LegendItemCollection more = plot.getLegendItems(); 481: result.addAll(more); 482: } 483: } 484: } 485: return result; 486: } 487: 488: /** 489: * Multiplies the range on the range axis/axes by the specified factor. 490: * 491: * @param factor the zoom factor. 492: * @param info the plot rendering info (<code>null</code> not permitted). 493: * @param source the source point (<code>null</code> not permitted). 494: */ 495: public void zoomRangeAxes(double factor, PlotRenderingInfo info, 496: Point2D source) { 497: // delegate 'info' and 'source' argument checks... 498: XYPlot subplot = findSubplot(info, source); 499: if (subplot != null) { 500: subplot.zoomRangeAxes(factor, info, source); 501: } 502: else { 503: // if the source point doesn't fall within a subplot, we do the 504: // zoom on all subplots... 505: Iterator iterator = getSubplots().iterator(); 506: while (iterator.hasNext()) { 507: subplot = (XYPlot) iterator.next(); 508: subplot.zoomRangeAxes(factor, info, source); 509: } 510: } 511: } 512: 513: /** 514: * Zooms in on the range axes. 515: * 516: * @param lowerPercent the lower bound. 517: * @param upperPercent the upper bound. 518: * @param info the plot rendering info (<code>null</code> not permitted). 519: * @param source the source point (<code>null</code> not permitted). 520: */ 521: public void zoomRangeAxes(double lowerPercent, double upperPercent, 522: PlotRenderingInfo info, Point2D source) { 523: // delegate 'info' and 'source' argument checks... 524: XYPlot subplot = findSubplot(info, source); 525: if (subplot != null) { 526: subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source); 527: } 528: else { 529: // if the source point doesn't fall within a subplot, we do the 530: // zoom on all subplots... 531: Iterator iterator = getSubplots().iterator(); 532: while (iterator.hasNext()) { 533: subplot = (XYPlot) iterator.next(); 534: subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source); 535: } 536: } 537: } 538: 539: /** 540: * Returns the subplot (if any) that contains the (x, y) point (specified 541: * in Java2D space). 542: * 543: * @param info the chart rendering info (<code>null</code> not permitted). 544: * @param source the source point (<code>null</code> not permitted). 545: * 546: * @return A subplot (possibly <code>null</code>). 547: */ 548: public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) { 549: if (info == null) { 550: throw new IllegalArgumentException("Null 'info' argument."); 551: } 552: if (source == null) { 553: throw new IllegalArgumentException("Null 'source' argument."); 554: } 555: XYPlot result = null; 556: int subplotIndex = info.getSubplotIndex(source); 557: if (subplotIndex >= 0) { 558: result = (XYPlot) this.subplots.get(subplotIndex); 559: } 560: return result; 561: } 562: 563: /** 564: * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are 565: * notified that the plot has been modified. 566: * <P> 567: * Note: usually you will want to set the renderer independently for each 568: * subplot, which is NOT what this method does. 569: * 570: * @param renderer the new renderer. 571: */ 572: public void setRenderer(XYItemRenderer renderer) { 573: 574: super.setRenderer(renderer); // not strictly necessary, since the 575: // renderer set for the 576: // parent plot is not used 577: 578: Iterator iterator = this.subplots.iterator(); 579: while (iterator.hasNext()) { 580: XYPlot plot = (XYPlot) iterator.next(); 581: plot.setRenderer(renderer); 582: } 583: 584: } 585: 586: /** 587: * Sets the fixed range axis space. 588: * 589: * @param space the space (<code>null</code> permitted). 590: */ 591: public void setFixedRangeAxisSpace(AxisSpace space) { 592: super.setFixedRangeAxisSpace(space); 593: setFixedRangeAxisSpaceForSubplots(space); 594: this.notifyListeners(new PlotChangeEvent(this)); 595: } 596: 597: /** 598: * Sets the size (width or height, depending on the orientation of the 599: * plot) for the domain axis of each subplot. 600: * 601: * @param space the space. 602: */ 603: protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) { 604: Iterator iterator = this.subplots.iterator(); 605: while (iterator.hasNext()) { 606: XYPlot plot = (XYPlot) iterator.next(); 607: plot.setFixedRangeAxisSpace(space, false); 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: }