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