001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ------------------------ 028 * CombinedRangeXYPlot.java 029 * ------------------------ 030 * (C) Copyright 2001-2008, by Bill Kelemen and Contributors. 031 * 032 * Original Author: Bill Kelemen; 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * Anthony Boulestreau; 035 * David Basten; 036 * Kevin Frechette (for ISTI); 037 * Arnaud Lelievre; 038 * Nicolas Brodu; 039 * Petr Kubanek (bug 1606205); 040 * 041 * Changes: 042 * -------- 043 * 06-Dec-2001 : Version 1 (BK); 044 * 12-Dec-2001 : Removed unnecessary 'throws' clause from constructor (DG); 045 * 18-Dec-2001 : Added plotArea attribute and get/set methods (BK); 046 * 22-Dec-2001 : Fixed bug in chartChanged with multiple combinations of 047 * CombinedPlots (BK); 048 * 08-Jan-2002 : Moved to new package com.jrefinery.chart.combination (DG); 049 * 25-Feb-2002 : Updated import statements (DG); 050 * 28-Feb-2002 : Readded "this.plotArea = plotArea" that was deleted from 051 * draw() method (BK); 052 * 26-Mar-2002 : Added an empty zoom method (this method needs to be written 053 * so that combined plots will support zooming (DG); 054 * 29-Mar-2002 : Changed the method createCombinedAxis adding the creation of 055 * OverlaidSymbolicAxis and CombinedSymbolicAxis(AB); 056 * 23-Apr-2002 : Renamed CombinedPlot-->MultiXYPlot, and simplified the 057 * structure (DG); 058 * 23-May-2002 : Renamed (again) MultiXYPlot-->CombinedXYPlot (DG); 059 * 19-Jun-2002 : Added get/setGap() methods suggested by David Basten (DG); 060 * 25-Jun-2002 : Removed redundant imports (DG); 061 * 16-Jul-2002 : Draws shared axis after subplots (to fix missing gridlines), 062 * added overrides of 'setSeriesPaint()' and 'setXYItemRenderer()' 063 * that pass changes down to subplots (KF); 064 * 09-Oct-2002 : Added add(XYPlot) method (DG); 065 * 26-Mar-2003 : Implemented Serializable (DG); 066 * 16-May-2003 : Renamed CombinedXYPlot --> CombinedRangeXYPlot (DG); 067 * 26-Jun-2003 : Fixed bug 755547 (DG); 068 * 16-Jul-2003 : Removed getSubPlots() method (duplicate of getSubplots()) (DG); 069 * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG); 070 * 21-Aug-2003 : Implemented Cloneable (DG); 071 * 08-Sep-2003 : Added internationalization via use of properties 072 * resourceBundle (RFE 690236) (AL); 073 * 11-Sep-2003 : Fix cloning support (subplots) (NB); 074 * 15-Sep-2003 : Fixed error in cloning (DG); 075 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 076 * 17-Sep-2003 : Updated handling of 'clicks' (DG); 077 * 12-Nov-2004 : Implements the new Zoomable interface (DG); 078 * 25-Nov-2004 : Small update to clone() implementation (DG); 079 * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend 080 * items if set (DG); 081 * 05-May-2005 : Removed unused draw() method (DG); 082 * ------------- JFREECHART 1.0.x --------------------------------------------- 083 * 13-Sep-2006 : Updated API docs (DG); 084 * 06-Feb-2007 : Fixed bug 1606205, draw shared axis after subplots (DG); 085 * 23-Mar-2007 : Reverted previous patch (DG); 086 * 17-Apr-2007 : Added null argument checks to findSubplot() (DG); 087 * 18-Jul-2007 : Fixed bug in removeSubplot (DG); 088 * 27-Nov-2007 : Modified setFixedDomainAxisSpaceForSubplots() so as not to 089 * trigger change events in subplots (DG); 090 * 27-Mar-2008 : Add documentation for getDataRange() method (DG); 091 * 31-Mar-2008 : Updated getSubplots() to return EMPTY_LIST for null 092 * subplots, as suggested by Richard West (DG); 093 * 28-Apr-2008 : Fixed zooming problem (see bug 1950037) (DG); 094 * 11-Aug-2008 : Don't store totalWeight of subplots, calculate it as 095 * required (DG); 096 * 097 */ 098 099 package org.jfree.chart.plot; 100 101 import java.awt.Graphics2D; 102 import java.awt.geom.Point2D; 103 import java.awt.geom.Rectangle2D; 104 import java.util.Collections; 105 import java.util.Iterator; 106 import java.util.List; 107 108 import org.jfree.chart.LegendItemCollection; 109 import org.jfree.chart.axis.AxisSpace; 110 import org.jfree.chart.axis.AxisState; 111 import org.jfree.chart.axis.NumberAxis; 112 import org.jfree.chart.axis.ValueAxis; 113 import org.jfree.chart.event.PlotChangeEvent; 114 import org.jfree.chart.event.PlotChangeListener; 115 import org.jfree.chart.renderer.xy.XYItemRenderer; 116 import org.jfree.data.Range; 117 import org.jfree.ui.RectangleEdge; 118 import org.jfree.ui.RectangleInsets; 119 import org.jfree.util.ObjectUtilities; 120 121 /** 122 * An extension of {@link XYPlot} that contains multiple subplots that share a 123 * common range axis. 124 */ 125 public class CombinedRangeXYPlot extends XYPlot 126 implements PlotChangeListener { 127 128 /** For serialization. */ 129 private static final long serialVersionUID = -5177814085082031168L; 130 131 /** Storage for the subplot references. */ 132 private List subplots; 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 configureRangeAxes(); 229 fireChangeEvent(); 230 231 } 232 233 /** 234 * Removes a subplot from the combined chart. 235 * 236 * @param subplot the subplot (<code>null</code> not permitted). 237 */ 238 public void remove(XYPlot subplot) { 239 if (subplot == null) { 240 throw new IllegalArgumentException(" Null 'subplot' argument."); 241 } 242 int position = -1; 243 int size = this.subplots.size(); 244 int i = 0; 245 while (position == -1 && i < size) { 246 if (this.subplots.get(i) == subplot) { 247 position = i; 248 } 249 i++; 250 } 251 if (position != -1) { 252 this.subplots.remove(position); 253 subplot.setParent(null); 254 subplot.removeChangeListener(this); 255 configureRangeAxes(); 256 fireChangeEvent(); 257 } 258 } 259 260 /** 261 * Returns the list of subplots. The returned list may be empty, but is 262 * never <code>null</code>. 263 * 264 * @return An unmodifiable list of subplots. 265 */ 266 public List getSubplots() { 267 if (this.subplots != null) { 268 return Collections.unmodifiableList(this.subplots); 269 } 270 else { 271 return Collections.EMPTY_LIST; 272 } 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 int totalWeight = 0; 316 for (int i = 0; i < n; i++) { 317 XYPlot sub = (XYPlot) this.subplots.get(i); 318 totalWeight += sub.getWeight(); 319 } 320 321 // calculate plotAreas of all sub-plots, maximum vertical/horizontal 322 // axis width/height 323 this.subplotAreas = new Rectangle2D[n]; 324 double x = adjustedPlotArea.getX(); 325 double y = adjustedPlotArea.getY(); 326 double usableSize = 0.0; 327 if (orientation == PlotOrientation.VERTICAL) { 328 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1); 329 } 330 else if (orientation == PlotOrientation.HORIZONTAL) { 331 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1); 332 } 333 334 for (int i = 0; i < n; i++) { 335 XYPlot plot = (XYPlot) this.subplots.get(i); 336 337 // calculate sub-plot area 338 if (orientation == PlotOrientation.VERTICAL) { 339 double w = usableSize * plot.getWeight() / totalWeight; 340 this.subplotAreas[i] = new Rectangle2D.Double(x, y, w, 341 adjustedPlotArea.getHeight()); 342 x = x + w + this.gap; 343 } 344 else if (orientation == PlotOrientation.HORIZONTAL) { 345 double h = usableSize * plot.getWeight() / totalWeight; 346 this.subplotAreas[i] = new Rectangle2D.Double(x, y, 347 adjustedPlotArea.getWidth(), h); 348 y = y + h + this.gap; 349 } 350 351 AxisSpace subSpace = plot.calculateDomainAxisSpace(g2, 352 this.subplotAreas[i], null); 353 space.ensureAtLeast(subSpace); 354 355 } 356 357 return space; 358 } 359 360 /** 361 * Draws the plot within the specified area on a graphics device. 362 * 363 * @param g2 the graphics device. 364 * @param area the plot area (in Java2D space). 365 * @param anchor an anchor point in Java2D space (<code>null</code> 366 * permitted). 367 * @param parentState the state from the parent plot, if there is one 368 * (<code>null</code> permitted). 369 * @param info collects chart drawing information (<code>null</code> 370 * permitted). 371 */ 372 public void draw(Graphics2D g2, 373 Rectangle2D area, 374 Point2D anchor, 375 PlotState parentState, 376 PlotRenderingInfo info) { 377 378 // set up info collection... 379 if (info != null) { 380 info.setPlotArea(area); 381 } 382 383 // adjust the drawing area for plot insets (if any)... 384 RectangleInsets insets = getInsets(); 385 insets.trim(area); 386 387 AxisSpace space = calculateAxisSpace(g2, area); 388 Rectangle2D dataArea = space.shrink(area, null); 389 //this.axisOffset.trim(dataArea); 390 391 // set the width and height of non-shared axis of all sub-plots 392 setFixedDomainAxisSpaceForSubplots(space); 393 394 // draw the shared axis 395 ValueAxis axis = getRangeAxis(); 396 RectangleEdge edge = getRangeAxisEdge(); 397 double cursor = RectangleEdge.coordinate(dataArea, edge); 398 AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info); 399 400 if (parentState == null) { 401 parentState = new PlotState(); 402 } 403 parentState.getSharedAxisStates().put(axis, axisState); 404 405 // draw all the charts 406 for (int i = 0; i < this.subplots.size(); i++) { 407 XYPlot plot = (XYPlot) this.subplots.get(i); 408 PlotRenderingInfo subplotInfo = null; 409 if (info != null) { 410 subplotInfo = new PlotRenderingInfo(info.getOwner()); 411 info.addSubplotInfo(subplotInfo); 412 } 413 plot.draw(g2, this.subplotAreas[i], anchor, parentState, 414 subplotInfo); 415 } 416 417 if (info != null) { 418 info.setDataArea(dataArea); 419 } 420 421 } 422 423 /** 424 * Returns a collection of legend items for the plot. 425 * 426 * @return The legend items. 427 */ 428 public LegendItemCollection getLegendItems() { 429 LegendItemCollection result = getFixedLegendItems(); 430 if (result == null) { 431 result = new LegendItemCollection(); 432 433 if (this.subplots != null) { 434 Iterator iterator = this.subplots.iterator(); 435 while (iterator.hasNext()) { 436 XYPlot plot = (XYPlot) iterator.next(); 437 LegendItemCollection more = plot.getLegendItems(); 438 result.addAll(more); 439 } 440 } 441 } 442 return result; 443 } 444 445 /** 446 * Multiplies the range on the domain axis/axes by the specified factor. 447 * 448 * @param factor the zoom factor. 449 * @param info the plot rendering info (<code>null</code> not permitted). 450 * @param source the source point (<code>null</code> not permitted). 451 */ 452 public void zoomDomainAxes(double factor, PlotRenderingInfo info, 453 Point2D source) { 454 zoomDomainAxes(factor, info, source, false); 455 } 456 457 /** 458 * Multiplies the range on the domain axis/axes by the specified factor. 459 * 460 * @param factor the zoom factor. 461 * @param info the plot rendering info (<code>null</code> not permitted). 462 * @param source the source point (<code>null</code> not permitted). 463 * @param useAnchor zoom about the anchor point? 464 */ 465 public void zoomDomainAxes(double factor, PlotRenderingInfo info, 466 Point2D source, boolean useAnchor) { 467 // delegate 'info' and 'source' argument checks... 468 XYPlot subplot = findSubplot(info, source); 469 if (subplot != null) { 470 subplot.zoomDomainAxes(factor, info, source, useAnchor); 471 } 472 else { 473 // if the source point doesn't fall within a subplot, we do the 474 // zoom on all subplots... 475 Iterator iterator = getSubplots().iterator(); 476 while (iterator.hasNext()) { 477 subplot = (XYPlot) iterator.next(); 478 subplot.zoomDomainAxes(factor, info, source, useAnchor); 479 } 480 } 481 } 482 483 /** 484 * Zooms in on the domain axes. 485 * 486 * @param lowerPercent the lower bound. 487 * @param upperPercent the upper bound. 488 * @param info the plot rendering info (<code>null</code> not permitted). 489 * @param source the source point (<code>null</code> not permitted). 490 */ 491 public void zoomDomainAxes(double lowerPercent, double upperPercent, 492 PlotRenderingInfo info, Point2D source) { 493 // delegate 'info' and 'source' argument checks... 494 XYPlot subplot = findSubplot(info, source); 495 if (subplot != null) { 496 subplot.zoomDomainAxes(lowerPercent, upperPercent, info, source); 497 } 498 else { 499 // if the source point doesn't fall within a subplot, we do the 500 // zoom on all subplots... 501 Iterator iterator = getSubplots().iterator(); 502 while (iterator.hasNext()) { 503 subplot = (XYPlot) iterator.next(); 504 subplot.zoomDomainAxes(lowerPercent, upperPercent, info, 505 source); 506 } 507 } 508 } 509 510 /** 511 * Returns the subplot (if any) that contains the (x, y) point (specified 512 * in Java2D space). 513 * 514 * @param info the chart rendering info (<code>null</code> not permitted). 515 * @param source the source point (<code>null</code> not permitted). 516 * 517 * @return A subplot (possibly <code>null</code>). 518 */ 519 public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) { 520 if (info == null) { 521 throw new IllegalArgumentException("Null 'info' argument."); 522 } 523 if (source == null) { 524 throw new IllegalArgumentException("Null 'source' argument."); 525 } 526 XYPlot result = null; 527 int subplotIndex = info.getSubplotIndex(source); 528 if (subplotIndex >= 0) { 529 result = (XYPlot) this.subplots.get(subplotIndex); 530 } 531 return result; 532 } 533 534 /** 535 * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are 536 * notified that the plot has been modified. 537 * <P> 538 * Note: usually you will want to set the renderer independently for each 539 * subplot, which is NOT what this method does. 540 * 541 * @param renderer the new renderer. 542 */ 543 public void setRenderer(XYItemRenderer renderer) { 544 545 super.setRenderer(renderer); // not strictly necessary, since the 546 // renderer set for the 547 // parent plot is not used 548 549 Iterator iterator = this.subplots.iterator(); 550 while (iterator.hasNext()) { 551 XYPlot plot = (XYPlot) iterator.next(); 552 plot.setRenderer(renderer); 553 } 554 555 } 556 557 /** 558 * Sets the orientation for the plot (and all its subplots). 559 * 560 * @param orientation the orientation. 561 */ 562 public void setOrientation(PlotOrientation orientation) { 563 564 super.setOrientation(orientation); 565 566 Iterator iterator = this.subplots.iterator(); 567 while (iterator.hasNext()) { 568 XYPlot plot = (XYPlot) iterator.next(); 569 plot.setOrientation(orientation); 570 } 571 572 } 573 574 /** 575 * Returns a range representing the extent of the data values in this plot 576 * (obtained from the subplots) that will be rendered against the specified 577 * axis. NOTE: This method is intended for internal JFreeChart use, and 578 * is public only so that code in the axis classes can call it. Since 579 * only the range axis is shared between subplots, the JFreeChart code 580 * will only call this method for the range values (although this is not 581 * checked/enforced). 582 * 583 * @param axis the axis. 584 * 585 * @return The range. 586 */ 587 public Range getDataRange(ValueAxis axis) { 588 Range result = null; 589 if (this.subplots != null) { 590 Iterator iterator = this.subplots.iterator(); 591 while (iterator.hasNext()) { 592 XYPlot subplot = (XYPlot) iterator.next(); 593 result = Range.combine(result, subplot.getDataRange(axis)); 594 } 595 } 596 return result; 597 } 598 599 /** 600 * Sets the space (width or height, depending on the orientation of the 601 * plot) for the domain axis of each subplot. 602 * 603 * @param space the space. 604 */ 605 protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) { 606 Iterator iterator = this.subplots.iterator(); 607 while (iterator.hasNext()) { 608 XYPlot plot = (XYPlot) iterator.next(); 609 plot.setFixedDomainAxisSpace(space, false); 610 } 611 } 612 613 /** 614 * Handles a 'click' on the plot by updating the anchor values... 615 * 616 * @param x x-coordinate, where the click occured. 617 * @param y y-coordinate, where the click occured. 618 * @param info object containing information about the plot dimensions. 619 */ 620 public void handleClick(int x, int y, PlotRenderingInfo info) { 621 622 Rectangle2D dataArea = info.getDataArea(); 623 if (dataArea.contains(x, y)) { 624 for (int i = 0; i < this.subplots.size(); i++) { 625 XYPlot subplot = (XYPlot) this.subplots.get(i); 626 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i); 627 subplot.handleClick(x, y, subplotInfo); 628 } 629 } 630 631 } 632 633 /** 634 * Receives a {@link PlotChangeEvent} and responds by notifying all 635 * listeners. 636 * 637 * @param event the event. 638 */ 639 public void plotChanged(PlotChangeEvent event) { 640 notifyListeners(event); 641 } 642 643 /** 644 * Tests this plot for equality with another object. 645 * 646 * @param obj the other object. 647 * 648 * @return <code>true</code> or <code>false</code>. 649 */ 650 public boolean equals(Object obj) { 651 if (obj == this) { 652 return true; 653 } 654 if (!(obj instanceof CombinedRangeXYPlot)) { 655 return false; 656 } 657 CombinedRangeXYPlot that = (CombinedRangeXYPlot) obj; 658 if (this.gap != that.gap) { 659 return false; 660 } 661 if (!ObjectUtilities.equal(this.subplots, that.subplots)) { 662 return false; 663 } 664 return super.equals(obj); 665 } 666 667 /** 668 * Returns a clone of the plot. 669 * 670 * @return A clone. 671 * 672 * @throws CloneNotSupportedException this class will not throw this 673 * exception, but subclasses (if any) might. 674 */ 675 public Object clone() throws CloneNotSupportedException { 676 677 CombinedRangeXYPlot result = (CombinedRangeXYPlot) super.clone(); 678 result.subplots = (List) ObjectUtilities.deepClone(this.subplots); 679 for (Iterator it = result.subplots.iterator(); it.hasNext();) { 680 Plot child = (Plot) it.next(); 681 child.setParent(result); 682 } 683 684 // after setting up all the subplots, the shared range axis may need 685 // reconfiguring 686 ValueAxis rangeAxis = result.getRangeAxis(); 687 if (rangeAxis != null) { 688 rangeAxis.configure(); 689 } 690 691 return result; 692 } 693 694 }