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