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