001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, 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 * CombinedDomainCategoryPlot.java 029 * ------------------------------- 030 * (C) Copyright 2003-2008, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Nicolas Brodu; 034 * 035 * Changes: 036 * -------- 037 * 16-May-2003 : Version 1 (DG); 038 * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG); 039 * 19-Aug-2003 : Added equals() method, implemented Cloneable and 040 * Serializable (DG); 041 * 11-Sep-2003 : Fix cloning support (subplots) (NB); 042 * 15-Sep-2003 : Implemented PublicCloneable (DG); 043 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 044 * 17-Sep-2003 : Updated handling of 'clicks' (DG); 045 * 04-May-2004 : Added getter/setter methods for 'gap' attribute (DG); 046 * 12-Nov-2004 : Implemented the Zoomable interface (DG); 047 * 25-Nov-2004 : Small update to clone() implementation (DG); 048 * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend 049 * items if set (DG); 050 * 05-May-2005 : Updated draw() method parameters (DG); 051 * ------------- JFREECHART 1.0.x --------------------------------------------- 052 * 13-Sep-2006 : Updated API docs (DG); 053 * 30-Oct-2006 : Added new getCategoriesForAxis() override (DG); 054 * 17-Apr-2007 : Added null argument checks to findSubplot() (DG); 055 * 14-Nov-2007 : Updated setFixedRangeAxisSpaceForSubplots() method (DG); 056 * 27-Mar-2008 : Add documentation for getDataRange() method (DG); 057 * 31-Mar-2008 : Updated getSubplots() to return EMPTY_LIST for null 058 * subplots, as suggested by Richard West (DG); 059 * 28-Apr-2008 : Fixed zooming problem (see bug 1950037) (DG); 060 * 26-Jun-2008 : Fixed crosshair support (DG); 061 * 11-Aug-2008 : Don't store totalWeight of subplots, calculate it as 062 * required (DG); 063 * 064 */ 065 066 package org.jfree.chart.plot; 067 068 import java.awt.Graphics2D; 069 import java.awt.geom.Point2D; 070 import java.awt.geom.Rectangle2D; 071 import java.util.Collections; 072 import java.util.Iterator; 073 import java.util.List; 074 075 import org.jfree.chart.LegendItemCollection; 076 import org.jfree.chart.axis.AxisSpace; 077 import org.jfree.chart.axis.AxisState; 078 import org.jfree.chart.axis.CategoryAxis; 079 import org.jfree.chart.axis.ValueAxis; 080 import org.jfree.chart.event.PlotChangeEvent; 081 import org.jfree.chart.event.PlotChangeListener; 082 import org.jfree.data.Range; 083 import org.jfree.ui.RectangleEdge; 084 import org.jfree.ui.RectangleInsets; 085 import org.jfree.util.ObjectUtilities; 086 087 /** 088 * A combined category plot where the domain axis is shared. 089 */ 090 public class CombinedDomainCategoryPlot extends CategoryPlot 091 implements PlotChangeListener { 092 093 /** For serialization. */ 094 private static final long serialVersionUID = 8207194522653701572L; 095 096 /** Storage for the subplot references. */ 097 private List subplots; 098 099 /** The gap between subplots. */ 100 private double gap; 101 102 /** Temporary storage for the subplot areas. */ 103 private transient Rectangle2D[] subplotAreas; 104 // TODO: move the above to the plot state 105 106 /** 107 * Default constructor. 108 */ 109 public CombinedDomainCategoryPlot() { 110 this(new CategoryAxis()); 111 } 112 113 /** 114 * Creates a new plot. 115 * 116 * @param domainAxis the shared domain axis (<code>null</code> not 117 * permitted). 118 */ 119 public CombinedDomainCategoryPlot(CategoryAxis domainAxis) { 120 super(null, domainAxis, null, null); 121 this.subplots = new java.util.ArrayList(); 122 this.gap = 5.0; 123 } 124 125 /** 126 * Returns the space between subplots. 127 * 128 * @return The gap (in Java2D units). 129 */ 130 public double getGap() { 131 return this.gap; 132 } 133 134 /** 135 * Sets the amount of space between subplots and sends a 136 * {@link PlotChangeEvent} to all registered listeners. 137 * 138 * @param gap the gap between subplots (in Java2D units). 139 */ 140 public void setGap(double gap) { 141 this.gap = gap; 142 fireChangeEvent(); 143 } 144 145 /** 146 * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent} 147 * to all registered listeners. 148 * <br><br> 149 * The domain axis for the subplot will be set to <code>null</code>. You 150 * must ensure that the subplot has a non-null range axis. 151 * 152 * @param subplot the subplot (<code>null</code> not permitted). 153 */ 154 public void add(CategoryPlot subplot) { 155 add(subplot, 1); 156 } 157 158 /** 159 * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent} 160 * to all registered listeners. 161 * <br><br> 162 * The domain axis for the subplot will be set to <code>null</code>. You 163 * must ensure that the subplot has a non-null range axis. 164 * 165 * @param subplot the subplot (<code>null</code> not permitted). 166 * @param weight the weight (must be >= 1). 167 */ 168 public void add(CategoryPlot subplot, int weight) { 169 if (subplot == null) { 170 throw new IllegalArgumentException("Null 'subplot' argument."); 171 } 172 if (weight < 1) { 173 throw new IllegalArgumentException("Require weight >= 1."); 174 } 175 subplot.setParent(this); 176 subplot.setWeight(weight); 177 subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0)); 178 subplot.setDomainAxis(null); 179 subplot.setOrientation(getOrientation()); 180 subplot.addChangeListener(this); 181 this.subplots.add(subplot); 182 CategoryAxis axis = getDomainAxis(); 183 if (axis != null) { 184 axis.configure(); 185 } 186 fireChangeEvent(); 187 } 188 189 /** 190 * Removes a subplot from the combined chart. Potentially, this removes 191 * some unique categories from the overall union of the datasets...so the 192 * domain axis is reconfigured, then a {@link PlotChangeEvent} is sent to 193 * all registered listeners. 194 * 195 * @param subplot the subplot (<code>null</code> not permitted). 196 */ 197 public void remove(CategoryPlot subplot) { 198 if (subplot == null) { 199 throw new IllegalArgumentException("Null 'subplot' argument."); 200 } 201 int position = -1; 202 int size = this.subplots.size(); 203 int i = 0; 204 while (position == -1 && i < size) { 205 if (this.subplots.get(i) == subplot) { 206 position = i; 207 } 208 i++; 209 } 210 if (position != -1) { 211 this.subplots.remove(position); 212 subplot.setParent(null); 213 subplot.removeChangeListener(this); 214 CategoryAxis domain = getDomainAxis(); 215 if (domain != null) { 216 domain.configure(); 217 } 218 fireChangeEvent(); 219 } 220 } 221 222 /** 223 * Returns the list of subplots. The returned list may be empty, but is 224 * never <code>null</code>. 225 * 226 * @return An unmodifiable list of subplots. 227 */ 228 public List getSubplots() { 229 if (this.subplots != null) { 230 return Collections.unmodifiableList(this.subplots); 231 } 232 else { 233 return Collections.EMPTY_LIST; 234 } 235 } 236 237 /** 238 * Returns the subplot (if any) that contains the (x, y) point (specified 239 * in Java2D space). 240 * 241 * @param info the chart rendering info (<code>null</code> not permitted). 242 * @param source the source point (<code>null</code> not permitted). 243 * 244 * @return A subplot (possibly <code>null</code>). 245 */ 246 public CategoryPlot findSubplot(PlotRenderingInfo info, Point2D source) { 247 if (info == null) { 248 throw new IllegalArgumentException("Null 'info' argument."); 249 } 250 if (source == null) { 251 throw new IllegalArgumentException("Null 'source' argument."); 252 } 253 CategoryPlot result = null; 254 int subplotIndex = info.getSubplotIndex(source); 255 if (subplotIndex >= 0) { 256 result = (CategoryPlot) this.subplots.get(subplotIndex); 257 } 258 return result; 259 } 260 261 /** 262 * Multiplies the range on the range axis/axes by the specified factor. 263 * 264 * @param factor the zoom factor. 265 * @param info the plot rendering info (<code>null</code> not permitted). 266 * @param source the source point (<code>null</code> not permitted). 267 */ 268 public void zoomRangeAxes(double factor, PlotRenderingInfo info, 269 Point2D source) { 270 zoomRangeAxes(factor, info, source, false); 271 } 272 273 /** 274 * Multiplies the range on the range axis/axes by the specified factor. 275 * 276 * @param factor the zoom factor. 277 * @param info the plot rendering info (<code>null</code> not permitted). 278 * @param source the source point (<code>null</code> not permitted). 279 * @param useAnchor zoom about the anchor point? 280 */ 281 public void zoomRangeAxes(double factor, PlotRenderingInfo info, 282 Point2D source, boolean useAnchor) { 283 // delegate 'info' and 'source' argument checks... 284 CategoryPlot subplot = findSubplot(info, source); 285 if (subplot != null) { 286 subplot.zoomRangeAxes(factor, info, source, useAnchor); 287 } 288 else { 289 // if the source point doesn't fall within a subplot, we do the 290 // zoom on all subplots... 291 Iterator iterator = getSubplots().iterator(); 292 while (iterator.hasNext()) { 293 subplot = (CategoryPlot) iterator.next(); 294 subplot.zoomRangeAxes(factor, info, source, useAnchor); 295 } 296 } 297 } 298 299 /** 300 * Zooms in on the range axes. 301 * 302 * @param lowerPercent the lower bound. 303 * @param upperPercent the upper bound. 304 * @param info the plot rendering info (<code>null</code> not permitted). 305 * @param source the source point (<code>null</code> not permitted). 306 */ 307 public void zoomRangeAxes(double lowerPercent, double upperPercent, 308 PlotRenderingInfo info, Point2D source) { 309 // delegate 'info' and 'source' argument checks... 310 CategoryPlot subplot = findSubplot(info, source); 311 if (subplot != null) { 312 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source); 313 } 314 else { 315 // if the source point doesn't fall within a subplot, we do the 316 // zoom on all subplots... 317 Iterator iterator = getSubplots().iterator(); 318 while (iterator.hasNext()) { 319 subplot = (CategoryPlot) iterator.next(); 320 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source); 321 } 322 } 323 } 324 325 /** 326 * Calculates the space required for the axes. 327 * 328 * @param g2 the graphics device. 329 * @param plotArea the plot area. 330 * 331 * @return The space required for the axes. 332 */ 333 protected AxisSpace calculateAxisSpace(Graphics2D g2, 334 Rectangle2D plotArea) { 335 336 AxisSpace space = new AxisSpace(); 337 PlotOrientation orientation = getOrientation(); 338 339 // work out the space required by the domain axis... 340 AxisSpace fixed = getFixedDomainAxisSpace(); 341 if (fixed != null) { 342 if (orientation == PlotOrientation.HORIZONTAL) { 343 space.setLeft(fixed.getLeft()); 344 space.setRight(fixed.getRight()); 345 } 346 else if (orientation == PlotOrientation.VERTICAL) { 347 space.setTop(fixed.getTop()); 348 space.setBottom(fixed.getBottom()); 349 } 350 } 351 else { 352 CategoryAxis categoryAxis = getDomainAxis(); 353 RectangleEdge categoryEdge = Plot.resolveDomainAxisLocation( 354 getDomainAxisLocation(), orientation); 355 if (categoryAxis != null) { 356 space = categoryAxis.reserveSpace(g2, this, plotArea, 357 categoryEdge, space); 358 } 359 else { 360 if (getDrawSharedDomainAxis()) { 361 space = getDomainAxis().reserveSpace(g2, this, plotArea, 362 categoryEdge, space); 363 } 364 } 365 } 366 367 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null); 368 369 // work out the maximum height or width of the non-shared axes... 370 int n = this.subplots.size(); 371 int totalWeight = 0; 372 for (int i = 0; i < n; i++) { 373 CategoryPlot sub = (CategoryPlot) this.subplots.get(i); 374 totalWeight += sub.getWeight(); 375 } 376 this.subplotAreas = new Rectangle2D[n]; 377 double x = adjustedPlotArea.getX(); 378 double y = adjustedPlotArea.getY(); 379 double usableSize = 0.0; 380 if (orientation == PlotOrientation.HORIZONTAL) { 381 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1); 382 } 383 else if (orientation == PlotOrientation.VERTICAL) { 384 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1); 385 } 386 387 for (int i = 0; i < n; i++) { 388 CategoryPlot plot = (CategoryPlot) this.subplots.get(i); 389 390 // calculate sub-plot area 391 if (orientation == PlotOrientation.HORIZONTAL) { 392 double w = usableSize * plot.getWeight() / totalWeight; 393 this.subplotAreas[i] = new Rectangle2D.Double(x, y, w, 394 adjustedPlotArea.getHeight()); 395 x = x + w + this.gap; 396 } 397 else if (orientation == PlotOrientation.VERTICAL) { 398 double h = usableSize * plot.getWeight() / totalWeight; 399 this.subplotAreas[i] = new Rectangle2D.Double(x, y, 400 adjustedPlotArea.getWidth(), h); 401 y = y + h + this.gap; 402 } 403 404 AxisSpace subSpace = plot.calculateRangeAxisSpace(g2, 405 this.subplotAreas[i], null); 406 space.ensureAtLeast(subSpace); 407 408 } 409 410 return space; 411 } 412 413 /** 414 * Draws the plot on a Java 2D graphics device (such as the screen or a 415 * printer). Will perform all the placement calculations for each of the 416 * sub-plots and then tell these to draw themselves. 417 * 418 * @param g2 the graphics device. 419 * @param area the area within which the plot (including axis labels) 420 * should be drawn. 421 * @param anchor the anchor point (<code>null</code> permitted). 422 * @param parentState the state from the parent plot, if there is one. 423 * @param info collects information about the drawing (<code>null</code> 424 * permitted). 425 */ 426 public void draw(Graphics2D g2, 427 Rectangle2D area, 428 Point2D anchor, 429 PlotState parentState, 430 PlotRenderingInfo info) { 431 432 // set up info collection... 433 if (info != null) { 434 info.setPlotArea(area); 435 } 436 437 // adjust the drawing area for plot insets (if any)... 438 RectangleInsets insets = getInsets(); 439 area.setRect(area.getX() + insets.getLeft(), 440 area.getY() + insets.getTop(), 441 area.getWidth() - insets.getLeft() - insets.getRight(), 442 area.getHeight() - insets.getTop() - insets.getBottom()); 443 444 445 // calculate the data area... 446 setFixedRangeAxisSpaceForSubplots(null); 447 AxisSpace space = calculateAxisSpace(g2, area); 448 Rectangle2D dataArea = space.shrink(area, null); 449 450 // set the width and height of non-shared axis of all sub-plots 451 setFixedRangeAxisSpaceForSubplots(space); 452 453 // draw the shared axis 454 CategoryAxis axis = getDomainAxis(); 455 RectangleEdge domainEdge = getDomainAxisEdge(); 456 double cursor = RectangleEdge.coordinate(dataArea, domainEdge); 457 AxisState axisState = axis.draw(g2, cursor, area, dataArea, 458 domainEdge, info); 459 if (parentState == null) { 460 parentState = new PlotState(); 461 } 462 parentState.getSharedAxisStates().put(axis, axisState); 463 464 // draw all the subplots 465 for (int i = 0; i < this.subplots.size(); i++) { 466 CategoryPlot plot = (CategoryPlot) this.subplots.get(i); 467 PlotRenderingInfo subplotInfo = null; 468 if (info != null) { 469 subplotInfo = new PlotRenderingInfo(info.getOwner()); 470 info.addSubplotInfo(subplotInfo); 471 } 472 Point2D subAnchor = null; 473 if (anchor != null && this.subplotAreas[i].contains(anchor)) { 474 subAnchor = anchor; 475 } 476 plot.draw(g2, this.subplotAreas[i], subAnchor, parentState, 477 subplotInfo); 478 } 479 480 if (info != null) { 481 info.setDataArea(dataArea); 482 } 483 484 } 485 486 /** 487 * Sets the size (width or height, depending on the orientation of the 488 * plot) for the range axis of each subplot. 489 * 490 * @param space the space (<code>null</code> permitted). 491 */ 492 protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) { 493 Iterator iterator = this.subplots.iterator(); 494 while (iterator.hasNext()) { 495 CategoryPlot plot = (CategoryPlot) iterator.next(); 496 plot.setFixedRangeAxisSpace(space, false); 497 } 498 } 499 500 /** 501 * Sets the orientation of the plot (and all subplots). 502 * 503 * @param orientation the orientation (<code>null</code> not permitted). 504 */ 505 public void setOrientation(PlotOrientation orientation) { 506 507 super.setOrientation(orientation); 508 509 Iterator iterator = this.subplots.iterator(); 510 while (iterator.hasNext()) { 511 CategoryPlot plot = (CategoryPlot) iterator.next(); 512 plot.setOrientation(orientation); 513 } 514 515 } 516 517 /** 518 * Returns a range representing the extent of the data values in this plot 519 * (obtained from the subplots) that will be rendered against the specified 520 * axis. NOTE: This method is intended for internal JFreeChart use, and 521 * is public only so that code in the axis classes can call it. Since, 522 * for this class, the domain axis is a {@link CategoryAxis} 523 * (not a <code>ValueAxis</code}) and subplots have independent range axes, 524 * the JFreeChart code will never call this method (although this is not 525 * checked/enforced). 526 * 527 * @param axis the axis. 528 * 529 * @return The range. 530 */ 531 public Range getDataRange(ValueAxis axis) { 532 // override is only for documentation purposes 533 return super.getDataRange(axis); 534 } 535 536 /** 537 * Returns a collection of legend items for the plot. 538 * 539 * @return The legend items. 540 */ 541 public LegendItemCollection getLegendItems() { 542 LegendItemCollection result = getFixedLegendItems(); 543 if (result == null) { 544 result = new LegendItemCollection(); 545 if (this.subplots != null) { 546 Iterator iterator = this.subplots.iterator(); 547 while (iterator.hasNext()) { 548 CategoryPlot plot = (CategoryPlot) iterator.next(); 549 LegendItemCollection more = plot.getLegendItems(); 550 result.addAll(more); 551 } 552 } 553 } 554 return result; 555 } 556 557 /** 558 * Returns an unmodifiable list of the categories contained in all the 559 * subplots. 560 * 561 * @return The list. 562 */ 563 public List getCategories() { 564 List result = new java.util.ArrayList(); 565 if (this.subplots != null) { 566 Iterator iterator = this.subplots.iterator(); 567 while (iterator.hasNext()) { 568 CategoryPlot plot = (CategoryPlot) iterator.next(); 569 List more = plot.getCategories(); 570 Iterator moreIterator = more.iterator(); 571 while (moreIterator.hasNext()) { 572 Comparable category = (Comparable) moreIterator.next(); 573 if (!result.contains(category)) { 574 result.add(category); 575 } 576 } 577 } 578 } 579 return Collections.unmodifiableList(result); 580 } 581 582 /** 583 * Overridden to return the categories in the subplots. 584 * 585 * @param axis ignored. 586 * 587 * @return A list of the categories in the subplots. 588 * 589 * @since 1.0.3 590 */ 591 public List getCategoriesForAxis(CategoryAxis axis) { 592 // FIXME: this code means that it is not possible to use more than 593 // one domain axis for the combined plots... 594 return getCategories(); 595 } 596 597 /** 598 * Handles a 'click' on the plot. 599 * 600 * @param x x-coordinate of the click. 601 * @param y y-coordinate of the click. 602 * @param info information about the plot's dimensions. 603 * 604 */ 605 public void handleClick(int x, int y, PlotRenderingInfo info) { 606 607 Rectangle2D dataArea = info.getDataArea(); 608 if (dataArea.contains(x, y)) { 609 for (int i = 0; i < this.subplots.size(); i++) { 610 CategoryPlot subplot = (CategoryPlot) this.subplots.get(i); 611 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i); 612 subplot.handleClick(x, y, subplotInfo); 613 } 614 } 615 616 } 617 618 /** 619 * Receives a {@link PlotChangeEvent} and responds by notifying all 620 * listeners. 621 * 622 * @param event the event. 623 */ 624 public void plotChanged(PlotChangeEvent event) { 625 notifyListeners(event); 626 } 627 628 /** 629 * Tests the plot for equality with an arbitrary object. 630 * 631 * @param obj the object (<code>null</code> permitted). 632 * 633 * @return A boolean. 634 */ 635 public boolean equals(Object obj) { 636 if (obj == this) { 637 return true; 638 } 639 if (!(obj instanceof CombinedDomainCategoryPlot)) { 640 return false; 641 } 642 CombinedDomainCategoryPlot that = (CombinedDomainCategoryPlot) obj; 643 if (this.gap != that.gap) { 644 return false; 645 } 646 if (!ObjectUtilities.equal(this.subplots, that.subplots)) { 647 return false; 648 } 649 return super.equals(obj); 650 } 651 652 /** 653 * Returns a clone of the plot. 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 CombinedDomainCategoryPlot result 663 = (CombinedDomainCategoryPlot) super.clone(); 664 result.subplots = (List) ObjectUtilities.deepClone(this.subplots); 665 for (Iterator it = result.subplots.iterator(); it.hasNext();) { 666 Plot child = (Plot) it.next(); 667 child.setParent(result); 668 } 669 return result; 670 671 } 672 673 }