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 * CombinedDomainXYPlot.java 029 * ------------------------- 030 * (C) Copyright 2001-2021, by Bill Kelemen and Contributors. 031 * 032 * Original Author: Bill Kelemen; 033 * Contributor(s): David Gilbert; 034 * Anthony Boulestreau; 035 * David Basten; 036 * Kevin Frechette (for ISTI); 037 * Nicolas Brodu; 038 * Petr Kubanek (bug 1606205); 039 * Vladimir Shirokov (bug 986); 040 */ 041 042package org.jfree.chart.plot; 043 044import java.awt.Graphics2D; 045import java.awt.geom.Point2D; 046import java.awt.geom.Rectangle2D; 047import java.util.ArrayList; 048import java.util.Collections; 049import java.util.List; 050import java.util.Objects; 051import org.jfree.chart.ChartElementVisitor; 052 053import org.jfree.chart.legend.LegendItemCollection; 054import org.jfree.chart.axis.AxisSpace; 055import org.jfree.chart.axis.AxisState; 056import org.jfree.chart.axis.NumberAxis; 057import org.jfree.chart.axis.ValueAxis; 058import org.jfree.chart.event.PlotChangeEvent; 059import org.jfree.chart.event.PlotChangeListener; 060import org.jfree.chart.renderer.xy.XYItemRenderer; 061import org.jfree.chart.api.RectangleEdge; 062import org.jfree.chart.api.RectangleInsets; 063import org.jfree.chart.internal.CloneUtils; 064import org.jfree.chart.internal.Args; 065import org.jfree.chart.util.ShadowGenerator; 066import org.jfree.data.Range; 067import org.jfree.data.general.DatasetChangeEvent; 068import org.jfree.data.xy.XYDataset; 069 070/** 071 * An extension of {@link XYPlot} that contains multiple subplots that share a 072 * common domain axis. 073 */ 074public class CombinedDomainXYPlot<S extends Comparable<S>> extends XYPlot<S> 075 implements PlotChangeListener { 076 077 /** For serialization. */ 078 private static final long serialVersionUID = -7765545541261907383L; 079 080 /** Storage for the subplot references (possibly empty but never null). */ 081 private List<XYPlot> subplots; 082 083 /** The gap between subplots. */ 084 private double gap = 5.0; 085 086 /** Temporary storage for the subplot areas. */ 087 private transient Rectangle2D[] subplotAreas; 088 // TODO: the subplot areas needs to be moved out of the plot into the plot 089 // state 090 091 /** 092 * Default constructor. 093 */ 094 public CombinedDomainXYPlot() { 095 this(new NumberAxis()); 096 } 097 098 /** 099 * Creates a new combined plot that shares a domain axis among multiple 100 * subplots. 101 * 102 * @param domainAxis the shared axis. 103 */ 104 public CombinedDomainXYPlot(ValueAxis domainAxis) { 105 super(null, // no data in the parent plot 106 domainAxis, 107 null, // no range axis 108 null); // no renderer 109 this.subplots = new ArrayList<>(); 110 } 111 112 /** 113 * Returns a string describing the type of plot. 114 * 115 * @return The type of plot. 116 */ 117 @Override 118 public String getPlotType() { 119 return "Combined_Domain_XYPlot"; 120 } 121 122 /** 123 * Returns the gap between subplots, measured in Java2D units. 124 * 125 * @return The gap (in Java2D units). 126 * 127 * @see #setGap(double) 128 */ 129 public double getGap() { 130 return this.gap; 131 } 132 133 /** 134 * Sets the amount of space between subplots and sends a 135 * {@link PlotChangeEvent} to all registered listeners. 136 * 137 * @param gap the gap between subplots (in Java2D units). 138 * 139 * @see #getGap() 140 */ 141 public void setGap(double gap) { 142 this.gap = gap; 143 fireChangeEvent(); 144 } 145 146 /** 147 * Returns {@code true} if the range is pannable for at least one subplot, 148 * and {@code false} otherwise. 149 * 150 * @return A boolean. 151 */ 152 @Override 153 public boolean isRangePannable() { 154 for (XYPlot subplot : this.subplots) { 155 if (subplot.isRangePannable()) { 156 return true; 157 } 158 } 159 return false; 160 } 161 162 /** 163 * Sets the flag, on each of the subplots, that controls whether or not the 164 * range is pannable. 165 * 166 * @param pannable the new flag value. 167 */ 168 @Override 169 public void setRangePannable(boolean pannable) { 170 for (XYPlot subplot : this.subplots) { 171 subplot.setRangePannable(pannable); 172 } 173 } 174 175 /** 176 * Sets the orientation for the plot (also changes the orientation for all 177 * the subplots to match). 178 * 179 * @param orientation the orientation ({@code null} not allowed). 180 */ 181 @Override 182 public void setOrientation(PlotOrientation orientation) { 183 super.setOrientation(orientation); 184 for (XYPlot p : this.subplots) { 185 p.setOrientation(orientation); 186 } 187 } 188 189 /** 190 * Sets the shadow generator for the plot (and all subplots) and sends 191 * a {@link PlotChangeEvent} to all registered listeners. 192 * 193 * @param generator the new generator ({@code null} permitted). 194 */ 195 @Override 196 public void setShadowGenerator(ShadowGenerator generator) { 197 setNotify(false); 198 super.setShadowGenerator(generator); 199 for (XYPlot p : this.subplots) { 200 p.setShadowGenerator(generator); 201 } 202 setNotify(true); 203 } 204 205 /** 206 * Returns a range representing the extent of the data values in this plot 207 * (obtained from the subplots) that will be rendered against the specified 208 * axis. NOTE: This method is intended for internal JFreeChart use, and 209 * is public only so that code in the axis classes can call it. Since 210 * only the domain axis is shared between subplots, the JFreeChart code 211 * will only call this method for the domain values (although this is not 212 * checked/enforced). 213 * 214 * @param axis the axis. 215 * 216 * @return The range (possibly {@code null}). 217 */ 218 @Override 219 public Range getDataRange(ValueAxis axis) { 220 if (this.subplots == null) { 221 return null; 222 } 223 Range result = null; 224 for (XYPlot p : this.subplots) { 225 result = Range.combine(result, p.getDataRange(axis)); 226 } 227 return result; 228 } 229 230 /** 231 * Adds a subplot (with a default 'weight' of 1) and sends a 232 * {@link PlotChangeEvent} to all registered listeners. 233 * <P> 234 * The domain axis for the subplot will be set to {@code null}. You 235 * must ensure that the subplot has a non-null range axis. 236 * 237 * @param subplot the subplot ({@code null} not permitted). 238 */ 239 public void add(XYPlot subplot) { 240 // defer argument checking 241 add(subplot, 1); 242 } 243 244 /** 245 * Adds a subplot with the specified weight and sends a 246 * {@link PlotChangeEvent} to all registered listeners. The weight 247 * determines how much space is allocated to the subplot relative to all 248 * the other subplots. 249 * <P> 250 * The domain axis for the subplot will be set to {@code null}. You 251 * must ensure that the subplot has a non-null range axis. 252 * 253 * @param subplot the subplot ({@code null} not permitted). 254 * @param weight the weight (must be >= 1). 255 */ 256 public void add(XYPlot subplot, int weight) { 257 Args.nullNotPermitted(subplot, "subplot"); 258 if (weight <= 0) { 259 throw new IllegalArgumentException("Require weight >= 1."); 260 } 261 262 // store the plot and its weight 263 subplot.setParent(this); 264 subplot.setWeight(weight); 265 subplot.setInsets(RectangleInsets.ZERO_INSETS, false); 266 subplot.setDomainAxis(null); 267 subplot.addChangeListener(this); 268 this.subplots.add(subplot); 269 270 ValueAxis axis = getDomainAxis(); 271 if (axis != null) { 272 axis.configure(); 273 } 274 fireChangeEvent(); 275 } 276 277 /** 278 * Removes a subplot from the combined chart and sends a 279 * {@link PlotChangeEvent} to all registered listeners. 280 * 281 * @param subplot the subplot ({@code null} not permitted). 282 */ 283 public void remove(XYPlot subplot) { 284 Args.nullNotPermitted(subplot, "subplot"); 285 int position = -1; 286 int size = this.subplots.size(); 287 int i = 0; 288 while (position == -1 && i < size) { 289 if (this.subplots.get(i) == subplot) { 290 position = i; 291 } 292 i++; 293 } 294 if (position != -1) { 295 this.subplots.remove(position); 296 subplot.setParent(null); 297 subplot.removeChangeListener(this); 298 ValueAxis domain = getDomainAxis(); 299 if (domain != null) { 300 domain.configure(); 301 } 302 fireChangeEvent(); 303 } 304 } 305 306 /** 307 * Returns the list of subplots. The returned list may be empty, but is 308 * never {@code null}. 309 * 310 * @return An unmodifiable list of subplots. 311 */ 312 public List<XYPlot> getSubplots() { 313 return Collections.unmodifiableList(this.subplots); 314 } 315 316 /** 317 * Calculates the axis space required. 318 * 319 * @param g2 the graphics device. 320 * @param plotArea the plot area. 321 * 322 * @return The space. 323 */ 324 @Override 325 protected AxisSpace calculateAxisSpace(Graphics2D g2, 326 Rectangle2D plotArea) { 327 328 AxisSpace space = new AxisSpace(); 329 PlotOrientation orientation = getOrientation(); 330 331 // work out the space required by the domain axis... 332 AxisSpace fixed = getFixedDomainAxisSpace(); 333 if (fixed != null) { 334 if (orientation == PlotOrientation.HORIZONTAL) { 335 space.setLeft(fixed.getLeft()); 336 space.setRight(fixed.getRight()); 337 } 338 else if (orientation == PlotOrientation.VERTICAL) { 339 space.setTop(fixed.getTop()); 340 space.setBottom(fixed.getBottom()); 341 } 342 } 343 else { 344 ValueAxis xAxis = getDomainAxis(); 345 RectangleEdge xEdge = Plot.resolveDomainAxisLocation( 346 getDomainAxisLocation(), orientation); 347 if (xAxis != null) { 348 space = xAxis.reserveSpace(g2, this, plotArea, xEdge, space); 349 } 350 } 351 352 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null); 353 354 // work out the maximum height or width of the non-shared axes... 355 int n = this.subplots.size(); 356 int totalWeight = 0; 357 for (int i = 0; i < n; i++) { 358 XYPlot sub = (XYPlot) this.subplots.get(i); 359 totalWeight += sub.getWeight(); 360 } 361 this.subplotAreas = new Rectangle2D[n]; 362 double x = adjustedPlotArea.getX(); 363 double y = adjustedPlotArea.getY(); 364 double usableSize = 0.0; 365 if (orientation == PlotOrientation.HORIZONTAL) { 366 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1); 367 } 368 else if (orientation == PlotOrientation.VERTICAL) { 369 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1); 370 } 371 372 for (int i = 0; i < n; i++) { 373 XYPlot plot = (XYPlot) this.subplots.get(i); 374 375 // calculate sub-plot area 376 if (orientation == PlotOrientation.HORIZONTAL) { 377 double w = usableSize * plot.getWeight() / totalWeight; 378 this.subplotAreas[i] = new Rectangle2D.Double(x, y, w, 379 adjustedPlotArea.getHeight()); 380 x = x + w + this.gap; 381 } 382 else if (orientation == PlotOrientation.VERTICAL) { 383 double h = usableSize * plot.getWeight() / totalWeight; 384 this.subplotAreas[i] = new Rectangle2D.Double(x, y, 385 adjustedPlotArea.getWidth(), h); 386 y = y + h + this.gap; 387 } 388 389 AxisSpace subSpace = plot.calculateRangeAxisSpace(g2, 390 this.subplotAreas[i], null); 391 space.ensureAtLeast(subSpace); 392 393 } 394 395 return space; 396 } 397 398 /** 399 * Receives a chart element visitor. Many plot subclasses will override 400 * this method to handle their subcomponents. 401 * 402 * @param visitor the visitor ({@code null} not permitted). 403 */ 404 @Override 405 public void receive(ChartElementVisitor visitor) { 406 subplots.forEach(subplot -> { 407 subplot.receive(visitor); 408 }); 409 super.receive(visitor); 410 } 411 412 /** 413 * Draws the plot within the specified area on a graphics device. 414 * 415 * @param g2 the graphics device. 416 * @param area the plot area (in Java2D space). 417 * @param anchor an anchor point in Java2D space ({@code null} 418 * permitted). 419 * @param parentState the state from the parent plot, if there is one 420 * ({@code null} permitted). 421 * @param info collects chart drawing information ({@code null} 422 * permitted). 423 */ 424 @Override 425 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 426 PlotState parentState, PlotRenderingInfo info) { 427 428 // set up info collection... 429 if (info != null) { 430 info.setPlotArea(area); 431 } 432 433 // adjust the drawing area for plot insets (if any)... 434 RectangleInsets insets = getInsets(); 435 insets.trim(area); 436 437 setFixedRangeAxisSpaceForSubplots(null); 438 AxisSpace space = calculateAxisSpace(g2, area); 439 Rectangle2D dataArea = space.shrink(area, null); 440 441 // set the width and height of non-shared axis of all sub-plots 442 setFixedRangeAxisSpaceForSubplots(space); 443 444 // draw the shared axis 445 ValueAxis axis = getDomainAxis(); 446 RectangleEdge edge = getDomainAxisEdge(); 447 double cursor = RectangleEdge.coordinate(dataArea, edge); 448 AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info); 449 if (parentState == null) { 450 parentState = new PlotState(); 451 } 452 parentState.getSharedAxisStates().put(axis, axisState); 453 454 // draw all the subplots 455 for (int i = 0; i < this.subplots.size(); i++) { 456 XYPlot plot = (XYPlot) this.subplots.get(i); 457 PlotRenderingInfo subplotInfo = null; 458 if (info != null) { 459 subplotInfo = new PlotRenderingInfo(info.getOwner()); 460 info.addSubplotInfo(subplotInfo); 461 } 462 plot.draw(g2, this.subplotAreas[i], anchor, parentState, 463 subplotInfo); 464 } 465 466 if (info != null) { 467 info.setDataArea(dataArea); 468 } 469 470 } 471 472 /** 473 * Returns a collection of legend items for the plot. 474 * 475 * @return The legend items. 476 */ 477 @Override 478 public LegendItemCollection getLegendItems() { 479 LegendItemCollection result = getFixedLegendItems(); 480 if (result == null) { 481 result = new LegendItemCollection(); 482 if (this.subplots != null) { 483 for (XYPlot plot : this.subplots) { 484 LegendItemCollection more = plot.getLegendItems(); 485 result.addAll(more); 486 } 487 } 488 } 489 return result; 490 } 491 492 /** 493 * Multiplies the range on the range axis/axes by the specified factor. 494 * 495 * @param factor the zoom factor. 496 * @param info the plot rendering info ({@code null} not permitted). 497 * @param source the source point ({@code null} not permitted). 498 */ 499 @Override 500 public void zoomRangeAxes(double factor, PlotRenderingInfo info, 501 Point2D source) { 502 zoomRangeAxes(factor, info, source, false); 503 } 504 505 /** 506 * Multiplies the range on the range axis/axes by the specified factor. 507 * 508 * @param factor the zoom factor. 509 * @param state the plot state. 510 * @param source the source point (in Java2D coordinates). 511 * @param useAnchor use source point as zoom anchor? 512 */ 513 @Override 514 public void zoomRangeAxes(double factor, PlotRenderingInfo state, 515 Point2D source, boolean useAnchor) { 516 // delegate 'state' and 'source' argument checks... 517 XYPlot subplot = findSubplot(state, source); 518 if (subplot != null) { 519 subplot.zoomRangeAxes(factor, state, source, useAnchor); 520 } else { 521 // if the source point doesn't fall within a subplot, we do the 522 // zoom on all subplots... 523 for (XYPlot p : this.subplots) { 524 p.zoomRangeAxes(factor, state, source, useAnchor); 525 } 526 } 527 } 528 529 /** 530 * Zooms in on the range axes. 531 * 532 * @param lowerPercent the lower bound. 533 * @param upperPercent the upper bound. 534 * @param info the plot rendering info ({@code null} not permitted). 535 * @param source the source point ({@code null} not permitted). 536 */ 537 @Override 538 public void zoomRangeAxes(double lowerPercent, double upperPercent, 539 PlotRenderingInfo info, Point2D source) { 540 // delegate 'info' and 'source' argument checks... 541 XYPlot subplot = findSubplot(info, source); 542 if (subplot != null) { 543 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source); 544 } else { 545 // if the source point doesn't fall within a subplot, we do the 546 // zoom on all subplots... 547 for (XYPlot p : this.subplots) { 548 p.zoomRangeAxes(lowerPercent, upperPercent, info, source); 549 } 550 } 551 } 552 553 /** 554 * Pans all range axes by the specified percentage. 555 * 556 * @param panRange the distance to pan (as a percentage of the axis length). 557 * @param info the plot info ({@code null} not permitted). 558 * @param source the source point where the pan action started. 559 */ 560 @Override 561 public void panRangeAxes(double panRange, PlotRenderingInfo info, 562 Point2D source) { 563 XYPlot subplot = findSubplot(info, source); 564 if (subplot == null) { 565 return; 566 } 567 if (!subplot.isRangePannable()) { 568 return; 569 } 570 PlotRenderingInfo subplotInfo = info.getSubplotInfo( 571 info.getSubplotIndex(source)); 572 if (subplotInfo == null) { 573 return; 574 } 575 for (int i = 0; i < subplot.getRangeAxisCount(); i++) { 576 ValueAxis rangeAxis = subplot.getRangeAxis(i); 577 if (rangeAxis != null) { 578 rangeAxis.pan(panRange); 579 } 580 } 581 } 582 583 /** 584 * Returns the subplot (if any) that contains the (x, y) point (specified 585 * in Java2D space). 586 * 587 * @param info the chart rendering info ({@code null} not permitted). 588 * @param source the source point ({@code null} not permitted). 589 * 590 * @return A subplot (possibly {@code null}). 591 */ 592 public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) { 593 Args.nullNotPermitted(info, "info"); 594 Args.nullNotPermitted(source, "source"); 595 XYPlot result = null; 596 int subplotIndex = info.getSubplotIndex(source); 597 if (subplotIndex >= 0) { 598 result = (XYPlot) this.subplots.get(subplotIndex); 599 } 600 return result; 601 } 602 603 /** 604 * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are 605 * notified that the plot has been modified. 606 * <P> 607 * Note: usually you will want to set the renderer independently for each 608 * subplot, which is NOT what this method does. 609 * 610 * @param renderer the new renderer. 611 */ 612 @Override 613 public void setRenderer(XYItemRenderer renderer) { 614 super.setRenderer(renderer); // not strictly necessary, since the 615 // renderer set for the 616 // parent plot is not used 617 for (XYPlot p : this.subplots) { 618 p.setRenderer(renderer); 619 } 620 } 621 622 /** 623 * Sets the fixed range axis space and sends a {@link PlotChangeEvent} to 624 * all registered listeners. 625 * 626 * @param space the space ({@code null} permitted). 627 */ 628 @Override 629 public void setFixedRangeAxisSpace(AxisSpace space) { 630 super.setFixedRangeAxisSpace(space); 631 setFixedRangeAxisSpaceForSubplots(space); 632 fireChangeEvent(); 633 } 634 635 /** 636 * Sets the size (width or height, depending on the orientation of the 637 * plot) for the domain axis of each subplot. 638 * 639 * @param space the space. 640 */ 641 protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) { 642 for (XYPlot p : this.subplots) { 643 p.setFixedRangeAxisSpace(space, false); 644 } 645 } 646 647 /** 648 * Handles a 'click' on the plot by updating the anchor values. 649 * 650 * @param x x-coordinate, where the click occurred. 651 * @param y y-coordinate, where the click occurred. 652 * @param info object containing information about the plot dimensions. 653 */ 654 @Override 655 public void handleClick(int x, int y, PlotRenderingInfo info) { 656 Rectangle2D dataArea = info.getDataArea(); 657 if (dataArea.contains(x, y)) { 658 for (int i = 0; i < this.subplots.size(); i++) { 659 XYPlot subplot = (XYPlot) this.subplots.get(i); 660 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i); 661 subplot.handleClick(x, y, subplotInfo); 662 } 663 } 664 } 665 666 /** 667 * Receives notification of a change to the plot's dataset. 668 * <P> 669 * The axis ranges are updated if necessary. 670 * 671 * @param event information about the event (not used here). 672 */ 673 @Override 674 public void datasetChanged(DatasetChangeEvent event) { 675 super.datasetChanged(event); 676 if (this.subplots == null) { 677 return; // this can happen during plot construction 678 } 679 XYDataset dataset = null; 680 if (event.getDataset() instanceof XYDataset) { 681 dataset = (XYDataset) event.getDataset(); 682 } 683 for (XYPlot subplot : this.subplots) { 684 if (subplot.indexOf(dataset) >= 0) { 685 subplot.configureRangeAxes(); 686 } 687 } 688 } 689 690 /** 691 * Receives a {@link PlotChangeEvent} and responds by notifying all 692 * listeners. 693 * 694 * @param event the event. 695 */ 696 @Override 697 public void plotChanged(PlotChangeEvent event) { 698 notifyListeners(event); 699 } 700 701 /** 702 * Tests this plot for equality with another object. 703 * 704 * @param obj the other object. 705 * 706 * @return {@code true} or {@code false}. 707 */ 708 @Override 709 public boolean equals(Object obj) { 710 if (obj == this) { 711 return true; 712 } 713 if (!(obj instanceof CombinedDomainXYPlot)) { 714 return false; 715 } 716 CombinedDomainXYPlot that = (CombinedDomainXYPlot) obj; 717 if (this.gap != that.gap) { 718 return false; 719 } 720 if (!Objects.equals(this.subplots, that.subplots)) { 721 return false; 722 } 723 return super.equals(obj); 724 } 725 726 /** 727 * Returns a clone of the annotation. 728 * 729 * @return A clone. 730 * 731 * @throws CloneNotSupportedException this class will not throw this 732 * exception, but subclasses (if any) might. 733 */ 734 @Override 735 public Object clone() throws CloneNotSupportedException { 736 737 CombinedDomainXYPlot<S> result = (CombinedDomainXYPlot) super.clone(); 738 result.subplots = CloneUtils.cloneList(this.subplots); 739 for (XYPlot<S> child : result.subplots) { 740 child.setParent(result); 741 } 742 743 // after setting up all the subplots, the shared domain axis may need 744 // reconfiguring 745 ValueAxis domainAxis = result.getDomainAxis(); 746 if (domainAxis != null) { 747 domainAxis.configure(); 748 } 749 750 return result; 751 752 } 753 754}