001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2005, 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ------------------------- 028 * CombinedDomainXYPlot.java 029 * ------------------------- 030 * (C) Copyright 2001-2005, 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 * 039 * $Id: CombinedDomainXYPlot.java,v 1.9.2.1 2005/10/25 20:52:07 mungady Exp $ 040 * 041 * Changes: 042 * -------- 043 * 06-Dec-2001 : Version 1 (BK); 044 * 12-Dec-2001 : Removed unnecessary 'throws' clause from constructor (DG); 045 * 18-Dec-2001 : Added plotArea attribute and get/set methods (BK); 046 * 22-Dec-2001 : Fixed bug in chartChanged with multiple combinations of 047 * CombinedPlots (BK); 048 * 08-Jan-2002 : Moved to new package com.jrefinery.chart.combination (DG); 049 * 25-Feb-2002 : Updated import statements (DG); 050 * 28-Feb-2002 : Readded "this.plotArea = plotArea" that was deleted from 051 * draw() method (BK); 052 * 26-Mar-2002 : Added an empty zoom method (this method needs to be written so 053 * that combined plots will support zooming (DG); 054 * 29-Mar-2002 : Changed the method createCombinedAxis adding the creation of 055 * OverlaidSymbolicAxis and CombinedSymbolicAxis(AB); 056 * 23-Apr-2002 : Renamed CombinedPlot-->MultiXYPlot, and simplified the 057 * structure (DG); 058 * 23-May-2002 : Renamed (again) MultiXYPlot-->CombinedXYPlot (DG); 059 * 19-Jun-2002 : Added get/setGap() methods suggested by David Basten (DG); 060 * 25-Jun-2002 : Removed redundant imports (DG); 061 * 16-Jul-2002 : Draws shared axis after subplots (to fix missing gridlines), 062 * added overrides of 'setSeriesPaint()' and 'setXYItemRenderer()' 063 * that pass changes down to subplots (KF); 064 * 09-Oct-2002 : Added add(XYPlot) method (DG); 065 * 26-Mar-2003 : Implemented Serializable (DG); 066 * 16-May-2003 : Renamed CombinedXYPlot --> CombinedDomainXYPlot (DG); 067 * 04-Aug-2003 : Removed leftover code that was causing domain axis drawing 068 * problem (DG); 069 * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG); 070 * 21-Aug-2003 : Implemented Cloneable (DG); 071 * 11-Sep-2003 : Fix cloning support (subplots) (NB); 072 * 15-Sep-2003 : Fixed error in cloning (DG); 073 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 074 * 17-Sep-2003 : Updated handling of 'clicks' (DG); 075 * 12-Nov-2004 : Implemented the new Zoomable interface (DG); 076 * 25-Nov-2004 : Small update to clone() implementation (DG); 077 * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend 078 * items if set (DG); 079 * 05-May-2005 : Removed unused draw() method (DG); 080 * 081 */ 082 083 package org.jfree.chart.plot; 084 085 import java.awt.Graphics2D; 086 import java.awt.geom.Point2D; 087 import java.awt.geom.Rectangle2D; 088 import java.io.Serializable; 089 import java.util.Collections; 090 import java.util.Iterator; 091 import java.util.List; 092 093 import org.jfree.chart.LegendItemCollection; 094 import org.jfree.chart.axis.AxisSpace; 095 import org.jfree.chart.axis.AxisState; 096 import org.jfree.chart.axis.NumberAxis; 097 import org.jfree.chart.axis.ValueAxis; 098 import org.jfree.chart.event.PlotChangeEvent; 099 import org.jfree.chart.event.PlotChangeListener; 100 import org.jfree.chart.renderer.xy.XYItemRenderer; 101 import org.jfree.data.Range; 102 import org.jfree.ui.RectangleEdge; 103 import org.jfree.ui.RectangleInsets; 104 import org.jfree.util.ObjectUtilities; 105 import org.jfree.util.PublicCloneable; 106 107 /** 108 * An extension of {@link XYPlot} that contains multiple subplots that share a 109 * common domain axis. 110 */ 111 public class CombinedDomainXYPlot extends XYPlot 112 implements Cloneable, PublicCloneable, 113 Serializable, 114 PlotChangeListener { 115 116 /** For serialization. */ 117 private static final long serialVersionUID = -7765545541261907383L; 118 119 /** Storage for the subplot references. */ 120 private List subplots; 121 122 /** Total weight of all charts. */ 123 private int totalWeight = 0; 124 125 /** The gap between subplots. */ 126 private double gap = 5.0; 127 128 /** Temporary storage for the subplot areas. */ 129 private transient Rectangle2D[] subplotAreas; 130 // TODO: the subplot areas needs to be moved out of the plot into the plot 131 // state 132 133 /** 134 * Default constructor. 135 */ 136 public CombinedDomainXYPlot() { 137 this(new NumberAxis()); 138 } 139 140 /** 141 * Creates a new combined plot that shares a domain axis among multiple 142 * subplots. 143 * 144 * @param domainAxis the shared axis. 145 */ 146 public CombinedDomainXYPlot(ValueAxis domainAxis) { 147 148 super( 149 null, // no data in the parent plot 150 domainAxis, 151 null, // no range axis 152 null // no rendereer 153 ); 154 155 this.subplots = new java.util.ArrayList(); 156 157 } 158 159 /** 160 * Returns a string describing the type of plot. 161 * 162 * @return The type of plot. 163 */ 164 public String getPlotType() { 165 return "Combined_Domain_XYPlot"; 166 } 167 168 /** 169 * Sets the orientation for the plot (also changes the orientation for all 170 * the subplots to match). 171 * 172 * @param orientation the orientation (<code>null</code> not allowed). 173 */ 174 public void setOrientation(PlotOrientation orientation) { 175 176 super.setOrientation(orientation); 177 Iterator iterator = this.subplots.iterator(); 178 while (iterator.hasNext()) { 179 XYPlot plot = (XYPlot) iterator.next(); 180 plot.setOrientation(orientation); 181 } 182 183 } 184 185 /** 186 * Returns the range for the specified axis. This is the combined range 187 * of all the subplots. 188 * 189 * @param axis the axis. 190 * 191 * @return The range (possibly <code>null</code>). 192 */ 193 public Range getDataRange(ValueAxis axis) { 194 195 Range result = null; 196 if (this.subplots != null) { 197 Iterator iterator = this.subplots.iterator(); 198 while (iterator.hasNext()) { 199 XYPlot subplot = (XYPlot) iterator.next(); 200 result = Range.combine(result, subplot.getDataRange(axis)); 201 } 202 } 203 return result; 204 205 } 206 207 /** 208 * Returns the gap between subplots, measured in Java2D units. 209 * 210 * @return The gap (in Java2D units). 211 */ 212 public double getGap() { 213 return this.gap; 214 } 215 216 /** 217 * Sets the amount of space between subplots and sends a 218 * {@link PlotChangeEvent} to all registered listeners. 219 * 220 * @param gap the gap between subplots (in Java2D units). 221 */ 222 public void setGap(double gap) { 223 this.gap = gap; 224 notifyListeners(new PlotChangeEvent(this)); 225 } 226 227 /** 228 * Adds a subplot (with a default 'weight' of 1) and sends a 229 * {@link PlotChangeEvent} to all registered listeners. 230 * <P> 231 * The domain axis for the subplot will be set to <code>null</code>. 232 * 233 * @param subplot the subplot (<code>null</code> not permitted). 234 */ 235 public void add(XYPlot subplot) { 236 // defer argument checking 237 add(subplot, 1); 238 } 239 240 /** 241 * Adds a subplot with the specified weight and sends a 242 * {@link PlotChangeEvent} to all registered listeners. The weight 243 * determines how much space is allocated to the subplot relative to all 244 * the other subplots. 245 * <P> 246 * The domain axis for the subplot will be set to <code>null</code>. 247 * 248 * @param subplot the subplot (<code>null</code> not permitted). 249 * @param weight the weight (must be >= 1). 250 */ 251 public void add(XYPlot subplot, int weight) { 252 253 if (subplot == null) { 254 throw new IllegalArgumentException("Null 'subplot' argument."); 255 } 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(new RectangleInsets(0.0, 0.0, 0.0, 0.0), false); 264 subplot.setDomainAxis(null); 265 subplot.addChangeListener(this); 266 this.subplots.add(subplot); 267 268 // keep track of total weights 269 this.totalWeight += weight; 270 271 ValueAxis axis = getDomainAxis(); 272 if (axis != null) { 273 axis.configure(); 274 } 275 276 notifyListeners(new PlotChangeEvent(this)); 277 278 } 279 280 /** 281 * Removes a subplot from the combined chart and sends a 282 * {@link PlotChangeEvent} to all registered listeners. 283 * 284 * @param subplot the subplot (<code>null</code> not permitted). 285 */ 286 public void remove(XYPlot subplot) { 287 if (subplot == null) { 288 throw new IllegalArgumentException(" Null 'subplot' argument."); 289 } 290 int position = -1; 291 int size = this.subplots.size(); 292 int i = 0; 293 while (position == -1 && i < size) { 294 if (this.subplots.get(i) == subplot) { 295 position = i; 296 } 297 i++; 298 } 299 if (position != -1) { 300 this.subplots.remove(position); 301 subplot.setParent(null); 302 subplot.removeChangeListener(this); 303 this.totalWeight -= subplot.getWeight(); 304 305 ValueAxis domain = getDomainAxis(); 306 if (domain != null) { 307 domain.configure(); 308 } 309 notifyListeners(new PlotChangeEvent(this)); 310 } 311 } 312 313 /** 314 * Returns the list of subplots. 315 * 316 * @return An unmodifiable list of subplots. 317 */ 318 public List getSubplots() { 319 return Collections.unmodifiableList(this.subplots); 320 } 321 322 /** 323 * Calculates the axis space required. 324 * 325 * @param g2 the graphics device. 326 * @param plotArea the plot area. 327 * 328 * @return The space. 329 */ 330 protected AxisSpace calculateAxisSpace(Graphics2D g2, 331 Rectangle2D plotArea) { 332 333 AxisSpace space = new AxisSpace(); 334 PlotOrientation orientation = getOrientation(); 335 336 // work out the space required by the domain axis... 337 AxisSpace fixed = getFixedDomainAxisSpace(); 338 if (fixed != null) { 339 if (orientation == PlotOrientation.HORIZONTAL) { 340 space.setLeft(fixed.getLeft()); 341 space.setRight(fixed.getRight()); 342 } 343 else if (orientation == PlotOrientation.VERTICAL) { 344 space.setTop(fixed.getTop()); 345 space.setBottom(fixed.getBottom()); 346 } 347 } 348 else { 349 ValueAxis xAxis = getDomainAxis(); 350 RectangleEdge xEdge = Plot.resolveDomainAxisLocation( 351 getDomainAxisLocation(), orientation 352 ); 353 if (xAxis != null) { 354 space = xAxis.reserveSpace(g2, this, plotArea, xEdge, space); 355 } 356 } 357 358 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null); 359 360 // work out the maximum height or width of the non-shared axes... 361 int n = this.subplots.size(); 362 this.subplotAreas = new Rectangle2D[n]; 363 double x = adjustedPlotArea.getX(); 364 double y = adjustedPlotArea.getY(); 365 double usableSize = 0.0; 366 if (orientation == PlotOrientation.HORIZONTAL) { 367 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1); 368 } 369 else if (orientation == PlotOrientation.VERTICAL) { 370 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1); 371 } 372 373 for (int i = 0; i < n; i++) { 374 XYPlot plot = (XYPlot) this.subplots.get(i); 375 376 // calculate sub-plot area 377 if (orientation == PlotOrientation.HORIZONTAL) { 378 double w = usableSize * plot.getWeight() / this.totalWeight; 379 this.subplotAreas[i] = new Rectangle2D.Double( 380 x, y, w, adjustedPlotArea.getHeight() 381 ); 382 x = x + w + this.gap; 383 } 384 else if (orientation == PlotOrientation.VERTICAL) { 385 double h = usableSize * plot.getWeight() / this.totalWeight; 386 this.subplotAreas[i] = new Rectangle2D.Double( 387 x, y, adjustedPlotArea.getWidth(), h 388 ); 389 y = y + h + this.gap; 390 } 391 392 AxisSpace subSpace = plot.calculateRangeAxisSpace( 393 g2, this.subplotAreas[i], null 394 ); 395 space.ensureAtLeast(subSpace); 396 397 } 398 399 return space; 400 } 401 402 /** 403 * Draws the plot within the specified area on a graphics device. 404 * 405 * @param g2 the graphics device. 406 * @param area the plot area (in Java2D space). 407 * @param anchor an anchor point in Java2D space (<code>null</code> 408 * permitted). 409 * @param parentState the state from the parent plot, if there is one 410 * (<code>null</code> permitted). 411 * @param info collects chart drawing information (<code>null</code> 412 * permitted). 413 */ 414 public void draw(Graphics2D g2, 415 Rectangle2D area, 416 Point2D anchor, 417 PlotState parentState, 418 PlotRenderingInfo info) { 419 420 // set up info collection... 421 if (info != null) { 422 info.setPlotArea(area); 423 } 424 425 // adjust the drawing area for plot insets (if any)... 426 RectangleInsets insets = getInsets(); 427 insets.trim(area); 428 429 AxisSpace space = calculateAxisSpace(g2, area); 430 Rectangle2D dataArea = space.shrink(area, null); 431 432 // set the width and height of non-shared axis of all sub-plots 433 setFixedRangeAxisSpaceForSubplots(space); 434 435 // draw the shared axis 436 ValueAxis axis = getDomainAxis(); 437 RectangleEdge edge = getDomainAxisEdge(); 438 double cursor = RectangleEdge.coordinate(dataArea, edge); 439 AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info); 440 if (parentState == null) { 441 parentState = new PlotState(); 442 } 443 parentState.getSharedAxisStates().put(axis, axisState); 444 445 // draw all the subplots 446 for (int i = 0; i < this.subplots.size(); i++) { 447 XYPlot plot = (XYPlot) this.subplots.get(i); 448 PlotRenderingInfo subplotInfo = null; 449 if (info != null) { 450 subplotInfo = new PlotRenderingInfo(info.getOwner()); 451 info.addSubplotInfo(subplotInfo); 452 } 453 plot.draw( 454 g2, this.subplotAreas[i], anchor, parentState, subplotInfo 455 ); 456 } 457 458 if (info != null) { 459 info.setDataArea(dataArea); 460 } 461 462 } 463 464 /** 465 * Returns a collection of legend items for the plot. 466 * 467 * @return The legend items. 468 */ 469 public LegendItemCollection getLegendItems() { 470 LegendItemCollection result = getFixedLegendItems(); 471 if (result == null) { 472 result = new LegendItemCollection(); 473 if (this.subplots != null) { 474 Iterator iterator = this.subplots.iterator(); 475 while (iterator.hasNext()) { 476 XYPlot plot = (XYPlot) iterator.next(); 477 LegendItemCollection more = plot.getLegendItems(); 478 result.addAll(more); 479 } 480 } 481 } 482 return result; 483 } 484 485 /** 486 * Multiplies the range on the range axis/axes by the specified factor. 487 * 488 * @param factor the zoom factor. 489 * @param info the plot rendering info. 490 * @param source the source point. 491 */ 492 public void zoomRangeAxes(double factor, PlotRenderingInfo info, 493 Point2D source) { 494 XYPlot subplot = findSubplot(info, source); 495 if (subplot != null) { 496 subplot.zoomRangeAxes(factor, info, source); 497 } 498 } 499 500 /** 501 * Zooms in on the range axes. 502 * 503 * @param lowerPercent the lower bound. 504 * @param upperPercent the upper bound. 505 * @param info the plot rendering info. 506 * @param source the source point. 507 */ 508 public void zoomRangeAxes(double lowerPercent, double upperPercent, 509 PlotRenderingInfo info, Point2D source) { 510 XYPlot subplot = findSubplot(info, source); 511 if (subplot != null) { 512 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source); 513 } 514 } 515 516 /** 517 * Returns the subplot (if any) that contains the (x, y) point (specified 518 * in Java2D space). 519 * 520 * @param info the chart rendering info. 521 * @param source the source point. 522 * 523 * @return A subplot (possibly <code>null</code>). 524 */ 525 public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) { 526 XYPlot result = null; 527 int subplotIndex = info.getSubplotIndex(source); 528 if (subplotIndex >= 0) { 529 result = (XYPlot) this.subplots.get(subplotIndex); 530 } 531 return result; 532 } 533 534 /** 535 * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are 536 * notified that the plot has been modified. 537 * <P> 538 * Note: usually you will want to set the renderer independently for each 539 * subplot, which is NOT what this method does. 540 * 541 * @param renderer the new renderer. 542 */ 543 public void setRenderer(XYItemRenderer renderer) { 544 545 super.setRenderer(renderer); // not strictly necessary, since the 546 // renderer set for the 547 // parent plot is not used 548 549 Iterator iterator = this.subplots.iterator(); 550 while (iterator.hasNext()) { 551 XYPlot plot = (XYPlot) iterator.next(); 552 plot.setRenderer(renderer); 553 } 554 555 } 556 557 /** 558 * Sets the size (width or height, depending on the orientation of the 559 * plot) for the domain axis of each subplot. 560 * 561 * @param space the space. 562 */ 563 protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) { 564 565 Iterator iterator = this.subplots.iterator(); 566 while (iterator.hasNext()) { 567 XYPlot plot = (XYPlot) iterator.next(); 568 plot.setFixedRangeAxisSpace(space); 569 } 570 571 } 572 573 /** 574 * Handles a 'click' on the plot by updating the anchor values. 575 * 576 * @param x x-coordinate, where the click occured. 577 * @param y y-coordinate, where the click occured. 578 * @param info object containing information about the plot dimensions. 579 */ 580 public void handleClick(int x, int y, PlotRenderingInfo info) { 581 Rectangle2D dataArea = info.getDataArea(); 582 if (dataArea.contains(x, y)) { 583 for (int i = 0; i < this.subplots.size(); i++) { 584 XYPlot subplot = (XYPlot) this.subplots.get(i); 585 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i); 586 subplot.handleClick(x, y, subplotInfo); 587 } 588 } 589 } 590 591 /** 592 * Receives a {@link PlotChangeEvent} and responds by notifying all 593 * listeners. 594 * 595 * @param event the event. 596 */ 597 public void plotChanged(PlotChangeEvent event) { 598 notifyListeners(event); 599 } 600 601 /** 602 * Tests this plot for equality with another object. 603 * 604 * @param obj the other object. 605 * 606 * @return <code>true</code> or <code>false</code>. 607 */ 608 public boolean equals(Object obj) { 609 610 if (obj == null) { 611 return false; 612 } 613 614 if (obj == this) { 615 return true; 616 } 617 618 if (!(obj instanceof CombinedDomainXYPlot)) { 619 return false; 620 } 621 if (!super.equals(obj)) { 622 return false; 623 } 624 625 CombinedDomainXYPlot p = (CombinedDomainXYPlot) obj; 626 if (this.totalWeight != p.totalWeight) { 627 return false; 628 } 629 if (this.gap != p.gap) { 630 return false; 631 } 632 if (!ObjectUtilities.equal(this.subplots, p.subplots)) { 633 return false; 634 } 635 636 return true; 637 } 638 639 /** 640 * Returns a clone of the annotation. 641 * 642 * @return A clone. 643 * 644 * @throws CloneNotSupportedException this class will not throw this 645 * exception, but subclasses (if any) might. 646 */ 647 public Object clone() throws CloneNotSupportedException { 648 649 CombinedDomainXYPlot result = (CombinedDomainXYPlot) super.clone(); 650 result.subplots = (List) ObjectUtilities.deepClone(this.subplots); 651 for (Iterator it = result.subplots.iterator(); it.hasNext();) { 652 Plot child = (Plot) it.next(); 653 child.setParent(result); 654 } 655 656 // after setting up all the subplots, the shared domain axis may need 657 // reconfiguring 658 ValueAxis domainAxis = result.getDomainAxis(); 659 if (domainAxis != null) { 660 domainAxis.configure(); 661 } 662 663 return result; 664 665 } 666 667 }