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