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 * 085 */ 086 087package org.jfree.chart.plot; 088 089import java.awt.Graphics2D; 090import java.awt.geom.Point2D; 091import java.awt.geom.Rectangle2D; 092import java.io.Serializable; 093import java.util.Collections; 094import java.util.Iterator; 095import java.util.List; 096 097import org.jfree.chart.LegendItemCollection; 098import org.jfree.chart.axis.AxisSpace; 099import org.jfree.chart.axis.AxisState; 100import org.jfree.chart.axis.NumberAxis; 101import org.jfree.chart.axis.ValueAxis; 102import org.jfree.chart.event.PlotChangeEvent; 103import org.jfree.chart.event.PlotChangeListener; 104import org.jfree.chart.renderer.xy.XYItemRenderer; 105import org.jfree.data.Range; 106import org.jfree.ui.RectangleEdge; 107import org.jfree.ui.RectangleInsets; 108import org.jfree.util.ObjectUtilities; 109import org.jfree.util.PublicCloneable; 110 111/** 112 * An extension of {@link XYPlot} that contains multiple subplots that share a 113 * common domain axis. 114 */ 115public class CombinedDomainXYPlot extends XYPlot 116 implements Cloneable, PublicCloneable, 117 Serializable, 118 PlotChangeListener { 119 120 /** For serialization. */ 121 private static final long serialVersionUID = -7765545541261907383L; 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 // TODO: the subplot areas needs to be moved out of the plot into the plot 135 // state 136 137 /** 138 * Default constructor. 139 */ 140 public CombinedDomainXYPlot() { 141 this(new NumberAxis()); 142 } 143 144 /** 145 * Creates a new combined plot that shares a domain axis among multiple 146 * subplots. 147 * 148 * @param domainAxis the shared axis. 149 */ 150 public CombinedDomainXYPlot(ValueAxis domainAxis) { 151 152 super( 153 null, // no data in the parent plot 154 domainAxis, 155 null, // no range axis 156 null // no rendereer 157 ); 158 159 this.subplots = new java.util.ArrayList(); 160 161 } 162 163 /** 164 * Returns a string describing the type of plot. 165 * 166 * @return The type of plot. 167 */ 168 public String getPlotType() { 169 return "Combined_Domain_XYPlot"; 170 } 171 172 /** 173 * Sets the orientation for the plot (also changes the orientation for all 174 * the subplots to match). 175 * 176 * @param orientation the orientation (<code>null</code> not allowed). 177 */ 178 public void setOrientation(PlotOrientation orientation) { 179 180 super.setOrientation(orientation); 181 Iterator iterator = this.subplots.iterator(); 182 while (iterator.hasNext()) { 183 XYPlot plot = (XYPlot) iterator.next(); 184 plot.setOrientation(orientation); 185 } 186 187 } 188 189 /** 190 * Returns the range for the specified axis. This is the combined range 191 * of all the subplots. 192 * 193 * @param axis the axis. 194 * 195 * @return The range (possibly <code>null</code>). 196 */ 197 public Range getDataRange(ValueAxis axis) { 198 199 Range result = null; 200 if (this.subplots != null) { 201 Iterator iterator = this.subplots.iterator(); 202 while (iterator.hasNext()) { 203 XYPlot subplot = (XYPlot) iterator.next(); 204 result = Range.combine(result, subplot.getDataRange(axis)); 205 } 206 } 207 return result; 208 209 } 210 211 /** 212 * Returns the gap between subplots, measured in Java2D units. 213 * 214 * @return The gap (in Java2D units). 215 */ 216 public double getGap() { 217 return this.gap; 218 } 219 220 /** 221 * Sets the amount of space between subplots and sends a 222 * {@link PlotChangeEvent} to all registered listeners. 223 * 224 * @param gap the gap between subplots (in Java2D units). 225 */ 226 public void setGap(double gap) { 227 this.gap = gap; 228 notifyListeners(new PlotChangeEvent(this)); 229 } 230 231 /** 232 * Adds a subplot (with a default 'weight' of 1) and sends a 233 * {@link PlotChangeEvent} to all registered listeners. 234 * <P> 235 * The domain axis for the subplot will be set to <code>null</code>. You 236 * must ensure that the subplot has a non-null range axis. 237 * 238 * @param subplot the subplot (<code>null</code> not permitted). 239 */ 240 public void add(XYPlot subplot) { 241 // defer argument checking 242 add(subplot, 1); 243 } 244 245 /** 246 * Adds a subplot with the specified weight and sends a 247 * {@link PlotChangeEvent} to all registered listeners. The weight 248 * determines how much space is allocated to the subplot relative to all 249 * the other subplots. 250 * <P> 251 * The domain axis for the subplot will be set to <code>null</code>. You 252 * must ensure that the subplot has a non-null range axis. 253 * 254 * @param subplot the subplot (<code>null</code> not permitted). 255 * @param weight the weight (must be >= 1). 256 */ 257 public void add(XYPlot subplot, int weight) { 258 259 if (subplot == null) { 260 throw new IllegalArgumentException("Null 'subplot' argument."); 261 } 262 if (weight <= 0) { 263 throw new IllegalArgumentException("Require weight >= 1."); 264 } 265 266 // store the plot and its weight 267 subplot.setParent(this); 268 subplot.setWeight(weight); 269 subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0), false); 270 subplot.setDomainAxis(null); 271 subplot.addChangeListener(this); 272 this.subplots.add(subplot); 273 274 // keep track of total weights 275 this.totalWeight += weight; 276 277 ValueAxis axis = getDomainAxis(); 278 if (axis != null) { 279 axis.configure(); 280 } 281 282 notifyListeners(new PlotChangeEvent(this)); 283 284 } 285 286 /** 287 * Removes a subplot from the combined chart and sends a 288 * {@link PlotChangeEvent} to all registered listeners. 289 * 290 * @param subplot the subplot (<code>null</code> not permitted). 291 */ 292 public void remove(XYPlot subplot) { 293 if (subplot == null) { 294 throw new IllegalArgumentException(" Null 'subplot' argument."); 295 } 296 int position = -1; 297 int size = this.subplots.size(); 298 int i = 0; 299 while (position == -1 && i < size) { 300 if (this.subplots.get(i) == subplot) { 301 position = i; 302 } 303 i++; 304 } 305 if (position != -1) { 306 this.subplots.remove(position); 307 subplot.setParent(null); 308 subplot.removeChangeListener(this); 309 this.totalWeight -= subplot.getWeight(); 310 311 ValueAxis domain = getDomainAxis(); 312 if (domain != null) { 313 domain.configure(); 314 } 315 notifyListeners(new PlotChangeEvent(this)); 316 } 317 } 318 319 /** 320 * Returns the list of subplots. 321 * 322 * @return An unmodifiable list of subplots. 323 */ 324 public List getSubplots() { 325 return Collections.unmodifiableList(this.subplots); 326 } 327 328 /** 329 * Calculates the axis space required. 330 * 331 * @param g2 the graphics device. 332 * @param plotArea the plot area. 333 * 334 * @return The space. 335 */ 336 protected AxisSpace calculateAxisSpace(Graphics2D g2, 337 Rectangle2D plotArea) { 338 339 AxisSpace space = new AxisSpace(); 340 PlotOrientation orientation = getOrientation(); 341 342 // work out the space required by the domain axis... 343 AxisSpace fixed = getFixedDomainAxisSpace(); 344 if (fixed != null) { 345 if (orientation == PlotOrientation.HORIZONTAL) { 346 space.setLeft(fixed.getLeft()); 347 space.setRight(fixed.getRight()); 348 } 349 else if (orientation == PlotOrientation.VERTICAL) { 350 space.setTop(fixed.getTop()); 351 space.setBottom(fixed.getBottom()); 352 } 353 } 354 else { 355 ValueAxis xAxis = getDomainAxis(); 356 RectangleEdge xEdge = Plot.resolveDomainAxisLocation( 357 getDomainAxisLocation(), orientation); 358 if (xAxis != null) { 359 space = xAxis.reserveSpace(g2, this, plotArea, xEdge, space); 360 } 361 } 362 363 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null); 364 365 // work out the maximum height or width of the non-shared axes... 366 int n = this.subplots.size(); 367 this.subplotAreas = new Rectangle2D[n]; 368 double x = adjustedPlotArea.getX(); 369 double y = adjustedPlotArea.getY(); 370 double usableSize = 0.0; 371 if (orientation == PlotOrientation.HORIZONTAL) { 372 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1); 373 } 374 else if (orientation == PlotOrientation.VERTICAL) { 375 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1); 376 } 377 378 for (int i = 0; i < n; i++) { 379 XYPlot plot = (XYPlot) this.subplots.get(i); 380 381 // calculate sub-plot area 382 if (orientation == PlotOrientation.HORIZONTAL) { 383 double w = usableSize * plot.getWeight() / this.totalWeight; 384 this.subplotAreas[i] = new Rectangle2D.Double(x, y, w, 385 adjustedPlotArea.getHeight()); 386 x = x + w + this.gap; 387 } 388 else if (orientation == PlotOrientation.VERTICAL) { 389 double h = usableSize * plot.getWeight() / this.totalWeight; 390 this.subplotAreas[i] = new Rectangle2D.Double(x, y, 391 adjustedPlotArea.getWidth(), h); 392 y = y + h + this.gap; 393 } 394 395 AxisSpace subSpace = plot.calculateRangeAxisSpace(g2, 396 this.subplotAreas[i], null); 397 space.ensureAtLeast(subSpace); 398 399 } 400 401 return space; 402 } 403 404 /** 405 * Draws the plot within the specified area on a graphics device. 406 * 407 * @param g2 the graphics device. 408 * @param area the plot area (in Java2D space). 409 * @param anchor an anchor point in Java2D space (<code>null</code> 410 * permitted). 411 * @param parentState the state from the parent plot, if there is one 412 * (<code>null</code> permitted). 413 * @param info collects chart drawing information (<code>null</code> 414 * permitted). 415 */ 416 public void draw(Graphics2D g2, 417 Rectangle2D area, 418 Point2D anchor, 419 PlotState parentState, 420 PlotRenderingInfo info) { 421 422 // set up info collection... 423 if (info != null) { 424 info.setPlotArea(area); 425 } 426 427 // adjust the drawing area for plot insets (if any)... 428 RectangleInsets insets = getInsets(); 429 insets.trim(area); 430 431 AxisSpace space = calculateAxisSpace(g2, area); 432 Rectangle2D dataArea = space.shrink(area, null); 433 434 // set the width and height of non-shared axis of all sub-plots 435 setFixedRangeAxisSpaceForSubplots(space); 436 437 // draw the shared axis 438 ValueAxis axis = getDomainAxis(); 439 RectangleEdge edge = getDomainAxisEdge(); 440 double cursor = RectangleEdge.coordinate(dataArea, edge); 441 AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info); 442 if (parentState == null) { 443 parentState = new PlotState(); 444 } 445 parentState.getSharedAxisStates().put(axis, axisState); 446 447 // draw all the subplots 448 for (int i = 0; i < this.subplots.size(); i++) { 449 XYPlot plot = (XYPlot) this.subplots.get(i); 450 PlotRenderingInfo subplotInfo = null; 451 if (info != null) { 452 subplotInfo = new PlotRenderingInfo(info.getOwner()); 453 info.addSubplotInfo(subplotInfo); 454 } 455 plot.draw(g2, this.subplotAreas[i], anchor, parentState, 456 subplotInfo); 457 } 458 459 if (info != null) { 460 info.setDataArea(dataArea); 461 } 462 463 } 464 465 /** 466 * Returns a collection of legend items for the plot. 467 * 468 * @return The legend items. 469 */ 470 public LegendItemCollection getLegendItems() { 471 LegendItemCollection result = getFixedLegendItems(); 472 if (result == null) { 473 result = new LegendItemCollection(); 474 if (this.subplots != null) { 475 Iterator iterator = this.subplots.iterator(); 476 while (iterator.hasNext()) { 477 XYPlot plot = (XYPlot) iterator.next(); 478 LegendItemCollection more = plot.getLegendItems(); 479 result.addAll(more); 480 } 481 } 482 } 483 return result; 484 } 485 486 /** 487 * Multiplies the range on the range axis/axes by the specified factor. 488 * 489 * @param factor the zoom factor. 490 * @param info the plot rendering info (<code>null</code> not permitted). 491 * @param source the source point (<code>null</code> not permitted). 492 */ 493 public void zoomRangeAxes(double factor, PlotRenderingInfo info, 494 Point2D source) { 495 // delegate 'info' and 'source' argument checks... 496 XYPlot subplot = findSubplot(info, source); 497 if (subplot != null) { 498 subplot.zoomRangeAxes(factor, info, source); 499 } 500 else { 501 // if the source point doesn't fall within a subplot, we do the 502 // zoom on all subplots... 503 Iterator iterator = getSubplots().iterator(); 504 while (iterator.hasNext()) { 505 subplot = (XYPlot) iterator.next(); 506 subplot.zoomRangeAxes(factor, info, source); 507 } 508 } 509 } 510 511 /** 512 * Zooms in on the range axes. 513 * 514 * @param lowerPercent the lower bound. 515 * @param upperPercent the upper bound. 516 * @param info the plot rendering info (<code>null</code> not permitted). 517 * @param source the source point (<code>null</code> not permitted). 518 */ 519 public void zoomRangeAxes(double lowerPercent, double upperPercent, 520 PlotRenderingInfo info, Point2D source) { 521 // delegate 'info' and 'source' argument checks... 522 XYPlot subplot = findSubplot(info, source); 523 if (subplot != null) { 524 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source); 525 } 526 else { 527 // if the source point doesn't fall within a subplot, we do the 528 // zoom on all subplots... 529 Iterator iterator = getSubplots().iterator(); 530 while (iterator.hasNext()) { 531 subplot = (XYPlot) iterator.next(); 532 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source); 533 } 534 } 535 } 536 537 /** 538 * Returns the subplot (if any) that contains the (x, y) point (specified 539 * in Java2D space). 540 * 541 * @param info the chart rendering info (<code>null</code> not permitted). 542 * @param source the source point (<code>null</code> not permitted). 543 * 544 * @return A subplot (possibly <code>null</code>). 545 */ 546 public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) { 547 if (info == null) { 548 throw new IllegalArgumentException("Null 'info' argument."); 549 } 550 if (source == null) { 551 throw new IllegalArgumentException("Null 'source' argument."); 552 } 553 XYPlot result = null; 554 int subplotIndex = info.getSubplotIndex(source); 555 if (subplotIndex >= 0) { 556 result = (XYPlot) this.subplots.get(subplotIndex); 557 } 558 return result; 559 } 560 561 /** 562 * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are 563 * notified that the plot has been modified. 564 * <P> 565 * Note: usually you will want to set the renderer independently for each 566 * subplot, which is NOT what this method does. 567 * 568 * @param renderer the new renderer. 569 */ 570 public void setRenderer(XYItemRenderer renderer) { 571 572 super.setRenderer(renderer); // not strictly necessary, since the 573 // renderer set for the 574 // parent plot is not used 575 576 Iterator iterator = this.subplots.iterator(); 577 while (iterator.hasNext()) { 578 XYPlot plot = (XYPlot) iterator.next(); 579 plot.setRenderer(renderer); 580 } 581 582 } 583 584 /** 585 * Sets the fixed range axis space. 586 * 587 * @param space the space (<code>null</code> permitted). 588 */ 589 public void setFixedRangeAxisSpace(AxisSpace space) { 590 super.setFixedRangeAxisSpace(space); 591 setFixedRangeAxisSpaceForSubplots(space); 592 this.notifyListeners(new PlotChangeEvent(this)); 593 } 594 595 /** 596 * Sets the size (width or height, depending on the orientation of the 597 * plot) for the domain axis of each subplot. 598 * 599 * @param space the space. 600 */ 601 protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) { 602 603 Iterator iterator = this.subplots.iterator(); 604 while (iterator.hasNext()) { 605 XYPlot plot = (XYPlot) iterator.next(); 606 plot.setFixedRangeAxisSpace(space); 607 } 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}