001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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-2007, 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 * 091 */ 092 093 package org.jfree.chart.plot; 094 095 import java.awt.Graphics2D; 096 import java.awt.geom.Point2D; 097 import java.awt.geom.Rectangle2D; 098 import java.io.Serializable; 099 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 }