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