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