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