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