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