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