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