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