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