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