001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, 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 * CombinedRangeCategoryPlot.java 029 * ------------------------------ 030 * (C) Copyright 2003-2008, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Nicolas Brodu; 034 * 035 * Changes: 036 * -------- 037 * 16-May-2003 : Version 1 (DG); 038 * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG); 039 * 19-Aug-2003 : Implemented Cloneable (DG); 040 * 11-Sep-2003 : Fix cloning support (subplots) (NB); 041 * 15-Sep-2003 : Implemented PublicCloneable. Fixed errors in cloning and 042 * serialization (DG); 043 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 044 * 17-Sep-2003 : Updated handling of 'clicks' (DG); 045 * 04-May-2004 : Added getter/setter methods for 'gap' attributes (DG); 046 * 12-Nov-2004 : Implements the new Zoomable interface (DG); 047 * 25-Nov-2004 : Small update to clone() implementation (DG); 048 * 21-Feb-2005 : Fixed bug in remove() method (id = 1121172) (DG); 049 * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend 050 * items if set (DG); 051 * 05-May-2005 : Updated draw() method parameters (DG); 052 * 14-Nov-2007 : Updated setFixedDomainAxisSpaceForSubplots() method (DG); 053 * 27-Mar-2008 : Add documentation for getDataRange() method (DG); 054 * 31-Mar-2008 : Updated getSubplots() to return EMPTY_LIST for null 055 * subplots, as suggested by Richard West (DG); 056 * 057 */ 058 059 package org.jfree.chart.plot; 060 061 import java.awt.Graphics2D; 062 import java.awt.geom.Point2D; 063 import java.awt.geom.Rectangle2D; 064 import java.io.IOException; 065 import java.io.ObjectInputStream; 066 import java.util.Collections; 067 import java.util.Iterator; 068 import java.util.List; 069 070 import org.jfree.chart.LegendItemCollection; 071 import org.jfree.chart.axis.AxisSpace; 072 import org.jfree.chart.axis.AxisState; 073 import org.jfree.chart.axis.NumberAxis; 074 import org.jfree.chart.axis.ValueAxis; 075 import org.jfree.chart.event.PlotChangeEvent; 076 import org.jfree.chart.event.PlotChangeListener; 077 import org.jfree.data.Range; 078 import org.jfree.ui.RectangleEdge; 079 import org.jfree.ui.RectangleInsets; 080 import org.jfree.util.ObjectUtilities; 081 082 /** 083 * A combined category plot where the range axis is shared. 084 */ 085 public class CombinedRangeCategoryPlot extends CategoryPlot 086 implements PlotChangeListener { 087 088 /** For serialization. */ 089 private static final long serialVersionUID = 7260210007554504515L; 090 091 /** Storage for the subplot references. */ 092 private List subplots; 093 094 /** Total weight of all charts. */ 095 private int totalWeight; 096 097 /** The gap between subplots. */ 098 private double gap; 099 100 /** Temporary storage for the subplot areas. */ 101 private transient Rectangle2D[] subplotArea; // TODO: move to plot state 102 103 /** 104 * Default constructor. 105 */ 106 public CombinedRangeCategoryPlot() { 107 this(new NumberAxis()); 108 } 109 110 /** 111 * Creates a new plot. 112 * 113 * @param rangeAxis the shared range axis. 114 */ 115 public CombinedRangeCategoryPlot(ValueAxis rangeAxis) { 116 super(null, null, rangeAxis, null); 117 this.subplots = new java.util.ArrayList(); 118 this.totalWeight = 0; 119 this.gap = 5.0; 120 } 121 122 /** 123 * Returns the space between subplots. 124 * 125 * @return The gap (in Java2D units). 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 public void setGap(double gap) { 138 this.gap = gap; 139 fireChangeEvent(); 140 } 141 142 /** 143 * Adds a subplot (with a default 'weight' of 1) and sends a 144 * {@link PlotChangeEvent} to all registered listeners. 145 * <br><br> 146 * You must ensure that the subplot has a non-null domain axis. The range 147 * axis for the subplot will be set to <code>null</code>. 148 * 149 * @param subplot the subplot (<code>null</code> not permitted). 150 */ 151 public void add(CategoryPlot subplot) { 152 // defer argument checking 153 add(subplot, 1); 154 } 155 156 /** 157 * Adds a subplot and sends a {@link PlotChangeEvent} to all registered 158 * listeners. 159 * <br><br> 160 * You must ensure that the subplot has a non-null domain axis. The range 161 * axis for the subplot will be set to <code>null</code>. 162 * 163 * @param subplot the subplot (<code>null</code> not permitted). 164 * @param weight the weight (must be >= 1). 165 */ 166 public void add(CategoryPlot subplot, int weight) { 167 if (subplot == null) { 168 throw new IllegalArgumentException("Null 'subplot' argument."); 169 } 170 if (weight <= 0) { 171 throw new IllegalArgumentException("Require weight >= 1."); 172 } 173 // store the plot and its weight 174 subplot.setParent(this); 175 subplot.setWeight(weight); 176 subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0)); 177 subplot.setRangeAxis(null); 178 subplot.setOrientation(getOrientation()); 179 subplot.addChangeListener(this); 180 this.subplots.add(subplot); 181 this.totalWeight += weight; 182 183 // configure the range axis... 184 ValueAxis axis = getRangeAxis(); 185 if (axis != null) { 186 axis.configure(); 187 } 188 fireChangeEvent(); 189 } 190 191 /** 192 * Removes a subplot from the combined chart. 193 * 194 * @param subplot the subplot (<code>null</code> not permitted). 195 */ 196 public void remove(CategoryPlot subplot) { 197 if (subplot == null) { 198 throw new IllegalArgumentException(" Null 'subplot' argument."); 199 } 200 int position = -1; 201 int size = this.subplots.size(); 202 int i = 0; 203 while (position == -1 && i < size) { 204 if (this.subplots.get(i) == subplot) { 205 position = i; 206 } 207 i++; 208 } 209 if (position != -1) { 210 this.subplots.remove(position); 211 subplot.setParent(null); 212 subplot.removeChangeListener(this); 213 this.totalWeight -= subplot.getWeight(); 214 215 ValueAxis range = getRangeAxis(); 216 if (range != null) { 217 range.configure(); 218 } 219 220 ValueAxis range2 = getRangeAxis(1); 221 if (range2 != null) { 222 range2.configure(); 223 } 224 fireChangeEvent(); 225 } 226 } 227 228 /** 229 * Returns the list of subplots. The returned list may be empty, but is 230 * never <code>null</code>. 231 * 232 * @return An unmodifiable list of subplots. 233 */ 234 public List getSubplots() { 235 if (this.subplots != null) { 236 return Collections.unmodifiableList(this.subplots); 237 } 238 else { 239 return Collections.EMPTY_LIST; 240 } 241 } 242 243 /** 244 * Calculates the space required for the axes. 245 * 246 * @param g2 the graphics device. 247 * @param plotArea the plot area. 248 * 249 * @return The space required for the axes. 250 */ 251 protected AxisSpace calculateAxisSpace(Graphics2D g2, 252 Rectangle2D plotArea) { 253 254 AxisSpace space = new AxisSpace(); 255 PlotOrientation orientation = getOrientation(); 256 257 // work out the space required by the domain axis... 258 AxisSpace fixed = getFixedRangeAxisSpace(); 259 if (fixed != null) { 260 if (orientation == PlotOrientation.VERTICAL) { 261 space.setLeft(fixed.getLeft()); 262 space.setRight(fixed.getRight()); 263 } 264 else if (orientation == PlotOrientation.HORIZONTAL) { 265 space.setTop(fixed.getTop()); 266 space.setBottom(fixed.getBottom()); 267 } 268 } 269 else { 270 ValueAxis valueAxis = getRangeAxis(); 271 RectangleEdge valueEdge = Plot.resolveRangeAxisLocation( 272 getRangeAxisLocation(), orientation); 273 if (valueAxis != null) { 274 space = valueAxis.reserveSpace(g2, this, plotArea, valueEdge, 275 space); 276 } 277 } 278 279 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null); 280 // work out the maximum height or width of the non-shared axes... 281 int n = this.subplots.size(); 282 283 // calculate plotAreas of all sub-plots, maximum vertical/horizontal 284 // axis width/height 285 this.subplotArea = new Rectangle2D[n]; 286 double x = adjustedPlotArea.getX(); 287 double y = adjustedPlotArea.getY(); 288 double usableSize = 0.0; 289 if (orientation == PlotOrientation.VERTICAL) { 290 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1); 291 } 292 else if (orientation == PlotOrientation.HORIZONTAL) { 293 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1); 294 } 295 296 for (int i = 0; i < n; i++) { 297 CategoryPlot plot = (CategoryPlot) this.subplots.get(i); 298 299 // calculate sub-plot area 300 if (orientation == PlotOrientation.VERTICAL) { 301 double w = usableSize * plot.getWeight() / this.totalWeight; 302 this.subplotArea[i] = new Rectangle2D.Double(x, y, w, 303 adjustedPlotArea.getHeight()); 304 x = x + w + this.gap; 305 } 306 else if (orientation == PlotOrientation.HORIZONTAL) { 307 double h = usableSize * plot.getWeight() / this.totalWeight; 308 this.subplotArea[i] = new Rectangle2D.Double(x, y, 309 adjustedPlotArea.getWidth(), h); 310 y = y + h + this.gap; 311 } 312 313 AxisSpace subSpace = plot.calculateDomainAxisSpace(g2, 314 this.subplotArea[i], null); 315 space.ensureAtLeast(subSpace); 316 317 } 318 319 return space; 320 } 321 322 /** 323 * Draws the plot on a Java 2D graphics device (such as the screen or a 324 * printer). Will perform all the placement calculations for each 325 * sub-plots and then tell these to draw themselves. 326 * 327 * @param g2 the graphics device. 328 * @param area the area within which the plot (including axis labels) 329 * should be drawn. 330 * @param anchor the anchor point (<code>null</code> permitted). 331 * @param parentState the parent state. 332 * @param info collects information about the drawing (<code>null</code> 333 * permitted). 334 */ 335 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 336 PlotState parentState, 337 PlotRenderingInfo info) { 338 339 // set up info collection... 340 if (info != null) { 341 info.setPlotArea(area); 342 } 343 344 // adjust the drawing area for plot insets (if any)... 345 RectangleInsets insets = getInsets(); 346 insets.trim(area); 347 348 // calculate the data area... 349 AxisSpace space = calculateAxisSpace(g2, area); 350 Rectangle2D dataArea = space.shrink(area, null); 351 352 // set the width and height of non-shared axis of all sub-plots 353 setFixedDomainAxisSpaceForSubplots(space); 354 355 // draw the shared axis 356 ValueAxis axis = getRangeAxis(); 357 RectangleEdge rangeEdge = getRangeAxisEdge(); 358 double cursor = RectangleEdge.coordinate(dataArea, rangeEdge); 359 AxisState state = axis.draw(g2, cursor, area, dataArea, rangeEdge, 360 info); 361 if (parentState == null) { 362 parentState = new PlotState(); 363 } 364 parentState.getSharedAxisStates().put(axis, state); 365 366 // draw all the charts 367 for (int i = 0; i < this.subplots.size(); i++) { 368 CategoryPlot plot = (CategoryPlot) this.subplots.get(i); 369 PlotRenderingInfo subplotInfo = null; 370 if (info != null) { 371 subplotInfo = new PlotRenderingInfo(info.getOwner()); 372 info.addSubplotInfo(subplotInfo); 373 } 374 plot.draw(g2, this.subplotArea[i], null, parentState, subplotInfo); 375 } 376 377 if (info != null) { 378 info.setDataArea(dataArea); 379 } 380 381 } 382 383 /** 384 * Sets the orientation for the plot (and all the subplots). 385 * 386 * @param orientation the orientation. 387 */ 388 public void setOrientation(PlotOrientation orientation) { 389 390 super.setOrientation(orientation); 391 392 Iterator iterator = this.subplots.iterator(); 393 while (iterator.hasNext()) { 394 CategoryPlot plot = (CategoryPlot) iterator.next(); 395 plot.setOrientation(orientation); 396 } 397 398 } 399 400 /** 401 * Returns a range representing the extent of the data values in this plot 402 * (obtained from the subplots) that will be rendered against the specified 403 * axis. NOTE: This method is intended for internal JFreeChart use, and 404 * is public only so that code in the axis classes can call it. Since 405 * only the range axis is shared between subplots, the JFreeChart code 406 * will only call this method for the range values (although this is not 407 * checked/enforced). 408 * 409 * @param axis the axis. 410 * 411 * @return The range. 412 */ 413 public Range getDataRange(ValueAxis axis) { 414 Range result = null; 415 if (this.subplots != null) { 416 Iterator iterator = this.subplots.iterator(); 417 while (iterator.hasNext()) { 418 CategoryPlot subplot = (CategoryPlot) iterator.next(); 419 result = Range.combine(result, subplot.getDataRange(axis)); 420 } 421 } 422 return result; 423 } 424 425 /** 426 * Returns a collection of legend items for the plot. 427 * 428 * @return The legend items. 429 */ 430 public LegendItemCollection getLegendItems() { 431 LegendItemCollection result = getFixedLegendItems(); 432 if (result == null) { 433 result = new LegendItemCollection(); 434 if (this.subplots != null) { 435 Iterator iterator = this.subplots.iterator(); 436 while (iterator.hasNext()) { 437 CategoryPlot plot = (CategoryPlot) iterator.next(); 438 LegendItemCollection more = plot.getLegendItems(); 439 result.addAll(more); 440 } 441 } 442 } 443 return result; 444 } 445 446 /** 447 * Sets the size (width or height, depending on the orientation of the 448 * plot) for the domain axis of each subplot. 449 * 450 * @param space the space. 451 */ 452 protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) { 453 Iterator iterator = this.subplots.iterator(); 454 while (iterator.hasNext()) { 455 CategoryPlot plot = (CategoryPlot) iterator.next(); 456 plot.setFixedDomainAxisSpace(space, false); 457 } 458 } 459 460 /** 461 * Handles a 'click' on the plot by updating the anchor value. 462 * 463 * @param x x-coordinate of the click. 464 * @param y y-coordinate of the click. 465 * @param info information about the plot's dimensions. 466 * 467 */ 468 public void handleClick(int x, int y, PlotRenderingInfo info) { 469 470 Rectangle2D dataArea = info.getDataArea(); 471 if (dataArea.contains(x, y)) { 472 for (int i = 0; i < this.subplots.size(); i++) { 473 CategoryPlot subplot = (CategoryPlot) this.subplots.get(i); 474 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i); 475 subplot.handleClick(x, y, subplotInfo); 476 } 477 } 478 479 } 480 481 /** 482 * Receives a {@link PlotChangeEvent} and responds by notifying all 483 * listeners. 484 * 485 * @param event the event. 486 */ 487 public void plotChanged(PlotChangeEvent event) { 488 notifyListeners(event); 489 } 490 491 /** 492 * Tests the plot for equality with an arbitrary object. 493 * 494 * @param obj the object (<code>null</code> permitted). 495 * 496 * @return <code>true</code> or <code>false</code>. 497 */ 498 public boolean equals(Object obj) { 499 if (obj == this) { 500 return true; 501 } 502 if (!(obj instanceof CombinedRangeCategoryPlot)) { 503 return false; 504 } 505 if (!super.equals(obj)) { 506 return false; 507 } 508 CombinedRangeCategoryPlot that = (CombinedRangeCategoryPlot) obj; 509 if (!ObjectUtilities.equal(this.subplots, that.subplots)) { 510 return false; 511 } 512 if (this.totalWeight != that.totalWeight) { 513 return false; 514 } 515 if (this.gap != that.gap) { 516 return false; 517 } 518 return true; 519 } 520 521 /** 522 * Returns a clone of the plot. 523 * 524 * @return A clone. 525 * 526 * @throws CloneNotSupportedException this class will not throw this 527 * exception, but subclasses (if any) might. 528 */ 529 public Object clone() throws CloneNotSupportedException { 530 CombinedRangeCategoryPlot result 531 = (CombinedRangeCategoryPlot) super.clone(); 532 result.subplots = (List) ObjectUtilities.deepClone(this.subplots); 533 for (Iterator it = result.subplots.iterator(); it.hasNext();) { 534 Plot child = (Plot) it.next(); 535 child.setParent(result); 536 } 537 538 // after setting up all the subplots, the shared range axis may need 539 // reconfiguring 540 ValueAxis rangeAxis = result.getRangeAxis(); 541 if (rangeAxis != null) { 542 rangeAxis.configure(); 543 } 544 545 return result; 546 } 547 548 /** 549 * Provides serialization support. 550 * 551 * @param stream the input stream. 552 * 553 * @throws IOException if there is an I/O error. 554 * @throws ClassNotFoundException if there is a classpath problem. 555 */ 556 private void readObject(ObjectInputStream stream) 557 throws IOException, ClassNotFoundException { 558 559 stream.defaultReadObject(); 560 561 // the range axis is deserialized before the subplots, so its value 562 // range is likely to be incorrect... 563 ValueAxis rangeAxis = getRangeAxis(); 564 if (rangeAxis != null) { 565 rangeAxis.configure(); 566 } 567 568 } 569 570 }