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