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