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