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