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