001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2014, 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 * [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 * CombinedRangeCategoryPlot.java 029 * ------------------------------ 030 * (C) Copyright 2003-2014, 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 * 26-Jun-2008 : Fixed crosshair support (DG); 057 * 11-Aug-2008 : Don't store totalWeight of subplots, calculate it as 058 * required (DG); 059 * 03-Jul-2013 : Use ParamChecks (DG); 060 * 061 */ 062 063package org.jfree.chart.plot; 064 065import java.awt.Graphics2D; 066import java.awt.geom.Point2D; 067import java.awt.geom.Rectangle2D; 068import java.io.IOException; 069import java.io.ObjectInputStream; 070import java.util.Collections; 071import java.util.Iterator; 072import java.util.List; 073 074import org.jfree.chart.LegendItemCollection; 075import org.jfree.chart.axis.AxisSpace; 076import org.jfree.chart.axis.AxisState; 077import org.jfree.chart.axis.NumberAxis; 078import org.jfree.chart.axis.ValueAxis; 079import org.jfree.chart.event.PlotChangeEvent; 080import org.jfree.chart.event.PlotChangeListener; 081import org.jfree.chart.util.ParamChecks; 082import org.jfree.chart.util.ShadowGenerator; 083import org.jfree.data.Range; 084import org.jfree.ui.RectangleEdge; 085import org.jfree.ui.RectangleInsets; 086import org.jfree.util.ObjectUtilities; 087 088/** 089 * A combined category plot where the range axis is shared. 090 */ 091public class CombinedRangeCategoryPlot extends CategoryPlot 092 implements PlotChangeListener { 093 094 /** For serialization. */ 095 private static final long serialVersionUID = 7260210007554504515L; 096 097 /** Storage for the subplot references. */ 098 private List subplots; 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.gap = 5.0; 122 } 123 124 /** 125 * Returns the space between subplots. 126 * 127 * @return The gap (in Java2D units). 128 */ 129 public double getGap() { 130 return this.gap; 131 } 132 133 /** 134 * Sets the amount of space between subplots and sends a 135 * {@link PlotChangeEvent} to all registered listeners. 136 * 137 * @param gap the gap between subplots (in Java2D units). 138 */ 139 public void setGap(double gap) { 140 this.gap = gap; 141 fireChangeEvent(); 142 } 143 144 /** 145 * Adds a subplot (with a default 'weight' of 1) and sends a 146 * {@link PlotChangeEvent} to all registered listeners. 147 * <br><br> 148 * You must ensure that the subplot has a non-null domain axis. The range 149 * axis for the subplot will be set to <code>null</code>. 150 * 151 * @param subplot the subplot (<code>null</code> not permitted). 152 */ 153 public void add(CategoryPlot subplot) { 154 // defer argument checking 155 add(subplot, 1); 156 } 157 158 /** 159 * Adds a subplot and sends a {@link PlotChangeEvent} to all registered 160 * listeners. 161 * <br><br> 162 * You must ensure that the subplot has a non-null domain axis. The range 163 * axis for the subplot will be set to <code>null</code>. 164 * 165 * @param subplot the subplot (<code>null</code> not permitted). 166 * @param weight the weight (must be >= 1). 167 */ 168 public void add(CategoryPlot subplot, int weight) { 169 ParamChecks.nullNotPermitted(subplot, "subplot"); 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 // configure the range axis... 182 ValueAxis axis = getRangeAxis(); 183 if (axis != null) { 184 axis.configure(); 185 } 186 fireChangeEvent(); 187 } 188 189 /** 190 * Removes a subplot from the combined chart. 191 * 192 * @param subplot the subplot (<code>null</code> not permitted). 193 */ 194 public void remove(CategoryPlot subplot) { 195 ParamChecks.nullNotPermitted(subplot, "subplot"); 196 int position = -1; 197 int size = this.subplots.size(); 198 int i = 0; 199 while (position == -1 && i < size) { 200 if (this.subplots.get(i) == subplot) { 201 position = i; 202 } 203 i++; 204 } 205 if (position != -1) { 206 this.subplots.remove(position); 207 subplot.setParent(null); 208 subplot.removeChangeListener(this); 209 210 ValueAxis range = getRangeAxis(); 211 if (range != null) { 212 range.configure(); 213 } 214 215 ValueAxis range2 = getRangeAxis(1); 216 if (range2 != null) { 217 range2.configure(); 218 } 219 fireChangeEvent(); 220 } 221 } 222 223 /** 224 * Returns the list of subplots. The returned list may be empty, but is 225 * never <code>null</code>. 226 * 227 * @return An unmodifiable list of subplots. 228 */ 229 public List getSubplots() { 230 if (this.subplots != null) { 231 return Collections.unmodifiableList(this.subplots); 232 } 233 else { 234 return Collections.EMPTY_LIST; 235 } 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 @Override 247 protected AxisSpace calculateAxisSpace(Graphics2D g2, 248 Rectangle2D plotArea) { 249 250 AxisSpace space = new AxisSpace(); 251 PlotOrientation orientation = getOrientation(); 252 253 // work out the space required by the domain axis... 254 AxisSpace fixed = getFixedRangeAxisSpace(); 255 if (fixed != null) { 256 if (orientation == PlotOrientation.VERTICAL) { 257 space.setLeft(fixed.getLeft()); 258 space.setRight(fixed.getRight()); 259 } 260 else if (orientation == PlotOrientation.HORIZONTAL) { 261 space.setTop(fixed.getTop()); 262 space.setBottom(fixed.getBottom()); 263 } 264 } 265 else { 266 ValueAxis valueAxis = getRangeAxis(); 267 RectangleEdge valueEdge = Plot.resolveRangeAxisLocation( 268 getRangeAxisLocation(), orientation); 269 if (valueAxis != null) { 270 space = valueAxis.reserveSpace(g2, this, plotArea, valueEdge, 271 space); 272 } 273 } 274 275 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null); 276 // work out the maximum height or width of the non-shared axes... 277 int n = this.subplots.size(); 278 int totalWeight = 0; 279 for (int i = 0; i < n; i++) { 280 CategoryPlot sub = (CategoryPlot) this.subplots.get(i); 281 totalWeight += sub.getWeight(); 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() / 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() / 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 @Override 336 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 337 PlotState parentState, 338 PlotRenderingInfo info) { 339 340 // set up info collection... 341 if (info != null) { 342 info.setPlotArea(area); 343 } 344 345 // adjust the drawing area for plot insets (if any)... 346 RectangleInsets insets = getInsets(); 347 insets.trim(area); 348 349 // calculate the data area... 350 AxisSpace space = calculateAxisSpace(g2, area); 351 Rectangle2D dataArea = space.shrink(area, null); 352 353 // set the width and height of non-shared axis of all sub-plots 354 setFixedDomainAxisSpaceForSubplots(space); 355 356 // draw the shared axis 357 ValueAxis axis = getRangeAxis(); 358 RectangleEdge rangeEdge = getRangeAxisEdge(); 359 double cursor = RectangleEdge.coordinate(dataArea, rangeEdge); 360 AxisState state = axis.draw(g2, cursor, area, dataArea, rangeEdge, 361 info); 362 if (parentState == null) { 363 parentState = new PlotState(); 364 } 365 parentState.getSharedAxisStates().put(axis, state); 366 367 // draw all the charts 368 for (int i = 0; i < this.subplots.size(); i++) { 369 CategoryPlot plot = (CategoryPlot) this.subplots.get(i); 370 PlotRenderingInfo subplotInfo = null; 371 if (info != null) { 372 subplotInfo = new PlotRenderingInfo(info.getOwner()); 373 info.addSubplotInfo(subplotInfo); 374 } 375 Point2D subAnchor = null; 376 if (anchor != null && this.subplotArea[i].contains(anchor)) { 377 subAnchor = anchor; 378 } 379 plot.draw(g2, this.subplotArea[i], subAnchor, parentState, 380 subplotInfo); 381 } 382 383 if (info != null) { 384 info.setDataArea(dataArea); 385 } 386 387 } 388 389 /** 390 * Sets the orientation for the plot (and all the subplots). 391 * 392 * @param orientation the orientation. 393 */ 394 @Override 395 public void setOrientation(PlotOrientation orientation) { 396 super.setOrientation(orientation); 397 Iterator iterator = this.subplots.iterator(); 398 while (iterator.hasNext()) { 399 CategoryPlot plot = (CategoryPlot) iterator.next(); 400 plot.setOrientation(orientation); 401 } 402 } 403 404 /** 405 * Sets the shadow generator for the plot (and all subplots) and sends 406 * a {@link PlotChangeEvent} to all registered listeners. 407 * 408 * @param generator the new generator (<code>null</code> permitted). 409 */ 410 @Override 411 public void setShadowGenerator(ShadowGenerator generator) { 412 setNotify(false); 413 super.setShadowGenerator(generator); 414 Iterator iterator = this.subplots.iterator(); 415 while (iterator.hasNext()) { 416 CategoryPlot plot = (CategoryPlot) iterator.next(); 417 plot.setShadowGenerator(generator); 418 } 419 setNotify(true); 420 } 421 422 /** 423 * Returns a range representing the extent of the data values in this plot 424 * (obtained from the subplots) that will be rendered against the specified 425 * axis. NOTE: This method is intended for internal JFreeChart use, and 426 * is public only so that code in the axis classes can call it. Since 427 * only the range axis is shared between subplots, the JFreeChart code 428 * will only call this method for the range values (although this is not 429 * checked/enforced). 430 * 431 * @param axis the axis. 432 * 433 * @return The range. 434 */ 435 @Override 436 public Range getDataRange(ValueAxis axis) { 437 Range result = null; 438 if (this.subplots != null) { 439 Iterator iterator = this.subplots.iterator(); 440 while (iterator.hasNext()) { 441 CategoryPlot subplot = (CategoryPlot) iterator.next(); 442 result = Range.combine(result, subplot.getDataRange(axis)); 443 } 444 } 445 return result; 446 } 447 448 /** 449 * Returns a collection of legend items for the plot. 450 * 451 * @return The legend items. 452 */ 453 @Override 454 public LegendItemCollection getLegendItems() { 455 LegendItemCollection result = getFixedLegendItems(); 456 if (result == null) { 457 result = new LegendItemCollection(); 458 if (this.subplots != null) { 459 Iterator iterator = this.subplots.iterator(); 460 while (iterator.hasNext()) { 461 CategoryPlot plot = (CategoryPlot) iterator.next(); 462 LegendItemCollection more = plot.getLegendItems(); 463 result.addAll(more); 464 } 465 } 466 } 467 return result; 468 } 469 470 /** 471 * Sets the size (width or height, depending on the orientation of the 472 * plot) for the domain axis of each subplot. 473 * 474 * @param space the space. 475 */ 476 protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) { 477 Iterator iterator = this.subplots.iterator(); 478 while (iterator.hasNext()) { 479 CategoryPlot plot = (CategoryPlot) iterator.next(); 480 plot.setFixedDomainAxisSpace(space, false); 481 } 482 } 483 484 /** 485 * Handles a 'click' on the plot by updating the anchor value. 486 * 487 * @param x x-coordinate of the click. 488 * @param y y-coordinate of the click. 489 * @param info information about the plot's dimensions. 490 * 491 */ 492 @Override 493 public void handleClick(int x, int y, PlotRenderingInfo info) { 494 Rectangle2D dataArea = info.getDataArea(); 495 if (dataArea.contains(x, y)) { 496 for (int i = 0; i < this.subplots.size(); i++) { 497 CategoryPlot subplot = (CategoryPlot) this.subplots.get(i); 498 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i); 499 subplot.handleClick(x, y, subplotInfo); 500 } 501 } 502 } 503 504 /** 505 * Receives a {@link PlotChangeEvent} and responds by notifying all 506 * listeners. 507 * 508 * @param event the event. 509 */ 510 @Override 511 public void plotChanged(PlotChangeEvent event) { 512 notifyListeners(event); 513 } 514 515 /** 516 * Tests the plot for equality with an arbitrary object. 517 * 518 * @param obj the object (<code>null</code> permitted). 519 * 520 * @return <code>true</code> or <code>false</code>. 521 */ 522 @Override 523 public boolean equals(Object obj) { 524 if (obj == this) { 525 return true; 526 } 527 if (!(obj instanceof CombinedRangeCategoryPlot)) { 528 return false; 529 } 530 CombinedRangeCategoryPlot that = (CombinedRangeCategoryPlot) obj; 531 if (this.gap != that.gap) { 532 return false; 533 } 534 if (!ObjectUtilities.equal(this.subplots, that.subplots)) { 535 return false; 536 } 537 return super.equals(obj); 538 } 539 540 /** 541 * Returns a clone of the plot. 542 * 543 * @return A clone. 544 * 545 * @throws CloneNotSupportedException this class will not throw this 546 * exception, but subclasses (if any) might. 547 */ 548 @Override 549 public Object clone() throws CloneNotSupportedException { 550 CombinedRangeCategoryPlot result 551 = (CombinedRangeCategoryPlot) super.clone(); 552 result.subplots = (List) ObjectUtilities.deepClone(this.subplots); 553 for (Iterator it = result.subplots.iterator(); it.hasNext();) { 554 Plot child = (Plot) it.next(); 555 child.setParent(result); 556 } 557 558 // after setting up all the subplots, the shared range axis may need 559 // reconfiguring 560 ValueAxis rangeAxis = result.getRangeAxis(); 561 if (rangeAxis != null) { 562 rangeAxis.configure(); 563 } 564 565 return result; 566 } 567 568 /** 569 * Provides serialization support. 570 * 571 * @param stream the input stream. 572 * 573 * @throws IOException if there is an I/O error. 574 * @throws ClassNotFoundException if there is a classpath problem. 575 */ 576 private void readObject(ObjectInputStream stream) 577 throws IOException, ClassNotFoundException { 578 579 stream.defaultReadObject(); 580 581 // the range axis is deserialized before the subplots, so its value 582 // range is likely to be incorrect... 583 ValueAxis rangeAxis = getRangeAxis(); 584 if (rangeAxis != null) { 585 rangeAxis.configure(); 586 } 587 588 } 589 590}