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