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.4 2007/02/06 17:09:28 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-2006 : Fixed bug 1606205, draw shared axis after subplots (DG); 084 * 085 */ 086 087 package org.jfree.chart.plot; 088 089 import java.awt.Graphics2D; 090 import java.awt.geom.Point2D; 091 import java.awt.geom.Rectangle2D; 092 import java.io.Serializable; 093 import java.util.Collections; 094 import java.util.Iterator; 095 import java.util.List; 096 097 import org.jfree.chart.LegendItemCollection; 098 import org.jfree.chart.axis.AxisSpace; 099 import org.jfree.chart.axis.AxisState; 100 import org.jfree.chart.axis.NumberAxis; 101 import org.jfree.chart.axis.ValueAxis; 102 import org.jfree.chart.event.PlotChangeEvent; 103 import org.jfree.chart.event.PlotChangeListener; 104 import org.jfree.chart.renderer.xy.XYItemRenderer; 105 import org.jfree.data.Range; 106 import org.jfree.ui.RectangleEdge; 107 import org.jfree.ui.RectangleInsets; 108 import org.jfree.util.ObjectUtilities; 109 import org.jfree.util.PublicCloneable; 110 111 /** 112 * An extension of {@link XYPlot} that contains multiple subplots that share a 113 * common domain axis. 114 */ 115 public 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 all the subplots 438 for (int i = 0; i < this.subplots.size(); i++) { 439 XYPlot plot = (XYPlot) this.subplots.get(i); 440 PlotRenderingInfo subplotInfo = null; 441 if (info != null) { 442 subplotInfo = new PlotRenderingInfo(info.getOwner()); 443 info.addSubplotInfo(subplotInfo); 444 } 445 plot.draw(g2, this.subplotAreas[i], anchor, parentState, 446 subplotInfo); 447 } 448 449 // draw the shared axis 450 ValueAxis axis = getDomainAxis(); 451 RectangleEdge edge = getDomainAxisEdge(); 452 double cursor = RectangleEdge.coordinate(dataArea, edge); 453 AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info); 454 if (parentState == null) { 455 parentState = new PlotState(); 456 } 457 parentState.getSharedAxisStates().put(axis, axisState); 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. 491 * @param source the source point. 492 */ 493 public void zoomRangeAxes(double factor, PlotRenderingInfo info, 494 Point2D source) { 495 XYPlot subplot = findSubplot(info, source); 496 if (subplot != null) { 497 subplot.zoomRangeAxes(factor, info, source); 498 } 499 } 500 501 /** 502 * Zooms in on the range axes. 503 * 504 * @param lowerPercent the lower bound. 505 * @param upperPercent the upper bound. 506 * @param info the plot rendering info. 507 * @param source the source point. 508 */ 509 public void zoomRangeAxes(double lowerPercent, double upperPercent, 510 PlotRenderingInfo info, Point2D source) { 511 XYPlot subplot = findSubplot(info, source); 512 if (subplot != null) { 513 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source); 514 } 515 } 516 517 /** 518 * Returns the subplot (if any) that contains the (x, y) point (specified 519 * in Java2D space). 520 * 521 * @param info the chart rendering info. 522 * @param source the source point. 523 * 524 * @return A subplot (possibly <code>null</code>). 525 */ 526 public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) { 527 XYPlot result = null; 528 int subplotIndex = info.getSubplotIndex(source); 529 if (subplotIndex >= 0) { 530 result = (XYPlot) this.subplots.get(subplotIndex); 531 } 532 return result; 533 } 534 535 /** 536 * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are 537 * notified that the plot has been modified. 538 * <P> 539 * Note: usually you will want to set the renderer independently for each 540 * subplot, which is NOT what this method does. 541 * 542 * @param renderer the new renderer. 543 */ 544 public void setRenderer(XYItemRenderer renderer) { 545 546 super.setRenderer(renderer); // not strictly necessary, since the 547 // renderer set for the 548 // parent plot is not used 549 550 Iterator iterator = this.subplots.iterator(); 551 while (iterator.hasNext()) { 552 XYPlot plot = (XYPlot) iterator.next(); 553 plot.setRenderer(renderer); 554 } 555 556 } 557 558 /** 559 * Sets the fixed range axis space. 560 * 561 * @param space the space (<code>null</code> permitted). 562 */ 563 public void setFixedRangeAxisSpace(AxisSpace space) { 564 super.setFixedRangeAxisSpace(space); 565 setFixedRangeAxisSpaceForSubplots(space); 566 this.notifyListeners(new PlotChangeEvent(this)); 567 } 568 569 /** 570 * Sets the size (width or height, depending on the orientation of the 571 * plot) for the domain axis of each subplot. 572 * 573 * @param space the space. 574 */ 575 protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) { 576 577 Iterator iterator = this.subplots.iterator(); 578 while (iterator.hasNext()) { 579 XYPlot plot = (XYPlot) iterator.next(); 580 plot.setFixedRangeAxisSpace(space); 581 } 582 583 } 584 585 /** 586 * Handles a 'click' on the plot by updating the anchor values. 587 * 588 * @param x x-coordinate, where the click occured. 589 * @param y y-coordinate, where the click occured. 590 * @param info object containing information about the plot dimensions. 591 */ 592 public void handleClick(int x, int y, PlotRenderingInfo info) { 593 Rectangle2D dataArea = info.getDataArea(); 594 if (dataArea.contains(x, y)) { 595 for (int i = 0; i < this.subplots.size(); i++) { 596 XYPlot subplot = (XYPlot) this.subplots.get(i); 597 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i); 598 subplot.handleClick(x, y, subplotInfo); 599 } 600 } 601 } 602 603 /** 604 * Receives a {@link PlotChangeEvent} and responds by notifying all 605 * listeners. 606 * 607 * @param event the event. 608 */ 609 public void plotChanged(PlotChangeEvent event) { 610 notifyListeners(event); 611 } 612 613 /** 614 * Tests this plot for equality with another object. 615 * 616 * @param obj the other object. 617 * 618 * @return <code>true</code> or <code>false</code>. 619 */ 620 public boolean equals(Object obj) { 621 622 if (obj == null) { 623 return false; 624 } 625 626 if (obj == this) { 627 return true; 628 } 629 630 if (!(obj instanceof CombinedDomainXYPlot)) { 631 return false; 632 } 633 if (!super.equals(obj)) { 634 return false; 635 } 636 637 CombinedDomainXYPlot p = (CombinedDomainXYPlot) obj; 638 if (this.totalWeight != p.totalWeight) { 639 return false; 640 } 641 if (this.gap != p.gap) { 642 return false; 643 } 644 if (!ObjectUtilities.equal(this.subplots, p.subplots)) { 645 return false; 646 } 647 648 return true; 649 } 650 651 /** 652 * Returns a clone of the annotation. 653 * 654 * @return A clone. 655 * 656 * @throws CloneNotSupportedException this class will not throw this 657 * exception, but subclasses (if any) might. 658 */ 659 public Object clone() throws CloneNotSupportedException { 660 661 CombinedDomainXYPlot result = (CombinedDomainXYPlot) super.clone(); 662 result.subplots = (List) ObjectUtilities.deepClone(this.subplots); 663 for (Iterator it = result.subplots.iterator(); it.hasNext();) { 664 Plot child = (Plot) it.next(); 665 child.setParent(result); 666 } 667 668 // after setting up all the subplots, the shared domain axis may need 669 // reconfiguring 670 ValueAxis domainAxis = result.getDomainAxis(); 671 if (domainAxis != null) { 672 domainAxis.configure(); 673 } 674 675 return result; 676 677 } 678 679 }