001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert 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-present, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Nicolas Brodu; 034 * Tracy Hiltbrand (equals/hashCode comply with EqualsVerifier); 035 * 036 */ 037 038package org.jfree.chart.plot; 039 040import java.awt.Graphics2D; 041import java.awt.geom.Point2D; 042import java.awt.geom.Rectangle2D; 043import java.io.IOException; 044import java.io.ObjectInputStream; 045import java.util.Collections; 046import java.util.Iterator; 047import java.util.List; 048import java.util.Objects; 049 050import org.jfree.chart.LegendItemCollection; 051import org.jfree.chart.axis.AxisSpace; 052import org.jfree.chart.axis.AxisState; 053import org.jfree.chart.axis.NumberAxis; 054import org.jfree.chart.axis.ValueAxis; 055import org.jfree.chart.event.PlotChangeEvent; 056import org.jfree.chart.event.PlotChangeListener; 057import org.jfree.chart.ui.RectangleEdge; 058import org.jfree.chart.ui.RectangleInsets; 059import org.jfree.chart.util.ObjectUtils; 060import org.jfree.chart.util.Args; 061import org.jfree.chart.util.ShadowGenerator; 062import org.jfree.data.Range; 063 064/** 065 * A combined category plot where the range axis is shared. 066 */ 067public class CombinedRangeCategoryPlot extends CategoryPlot 068 implements PlotChangeListener { 069 070 /** For serialization. */ 071 private static final long serialVersionUID = 7260210007554504515L; 072 073 /** Storage for the subplot references. */ 074 private List subplots; 075 076 /** The gap between subplots. */ 077 private double gap; 078 079 /** Temporary storage for the subplot areas. */ 080 private transient Rectangle2D[] subplotArea; // TODO: move to plot state 081 082 /** 083 * Default constructor. 084 */ 085 public CombinedRangeCategoryPlot() { 086 this(new NumberAxis()); 087 } 088 089 /** 090 * Creates a new plot. 091 * 092 * @param rangeAxis the shared range axis. 093 */ 094 public CombinedRangeCategoryPlot(ValueAxis rangeAxis) { 095 super(null, null, rangeAxis, null); 096 this.subplots = new java.util.ArrayList(); 097 this.gap = 5.0; 098 } 099 100 /** 101 * Returns the space between subplots. 102 * 103 * @return The gap (in Java2D units). 104 */ 105 public double getGap() { 106 return this.gap; 107 } 108 109 /** 110 * Sets the amount of space between subplots and sends a 111 * {@link PlotChangeEvent} to all registered listeners. 112 * 113 * @param gap the gap between subplots (in Java2D units). 114 */ 115 public void setGap(double gap) { 116 this.gap = gap; 117 fireChangeEvent(); 118 } 119 120 /** 121 * Adds a subplot (with a default 'weight' of 1) and sends a 122 * {@link PlotChangeEvent} to all registered listeners. 123 * <br><br> 124 * You must ensure that the subplot has a non-null domain axis. The range 125 * axis for the subplot will be set to {@code null}. 126 * 127 * @param subplot the subplot ({@code null} not permitted). 128 */ 129 public void add(CategoryPlot subplot) { 130 // defer argument checking 131 add(subplot, 1); 132 } 133 134 /** 135 * Adds a subplot and sends a {@link PlotChangeEvent} to all registered 136 * listeners. 137 * <br><br> 138 * You must ensure that the subplot has a non-null domain axis. The range 139 * axis for the subplot will be set to {@code null}. 140 * 141 * @param subplot the subplot ({@code null} not permitted). 142 * @param weight the weight (must be >= 1). 143 */ 144 public void add(CategoryPlot subplot, int weight) { 145 Args.nullNotPermitted(subplot, "subplot"); 146 if (weight <= 0) { 147 throw new IllegalArgumentException("Require weight >= 1."); 148 } 149 // store the plot and its weight 150 subplot.setParent(this); 151 subplot.setWeight(weight); 152 subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0)); 153 subplot.setRangeAxis(null); 154 subplot.setOrientation(getOrientation()); 155 subplot.addChangeListener(this); 156 this.subplots.add(subplot); 157 // configure the range axis... 158 ValueAxis axis = getRangeAxis(); 159 if (axis != null) { 160 axis.configure(); 161 } 162 fireChangeEvent(); 163 } 164 165 /** 166 * Removes a subplot from the combined chart. 167 * 168 * @param subplot the subplot ({@code null} not permitted). 169 */ 170 public void remove(CategoryPlot subplot) { 171 Args.nullNotPermitted(subplot, "subplot"); 172 int position = -1; 173 int size = this.subplots.size(); 174 int i = 0; 175 while (position == -1 && i < size) { 176 if (this.subplots.get(i) == subplot) { 177 position = i; 178 } 179 i++; 180 } 181 if (position != -1) { 182 this.subplots.remove(position); 183 subplot.setParent(null); 184 subplot.removeChangeListener(this); 185 186 ValueAxis range = getRangeAxis(); 187 if (range != null) { 188 range.configure(); 189 } 190 191 ValueAxis range2 = getRangeAxis(1); 192 if (range2 != null) { 193 range2.configure(); 194 } 195 fireChangeEvent(); 196 } 197 } 198 199 /** 200 * Returns the list of subplots. The returned list may be empty, but is 201 * never {@code null}. 202 * 203 * @return An unmodifiable list of subplots. 204 */ 205 public List getSubplots() { 206 if (this.subplots != null) { 207 return Collections.unmodifiableList(this.subplots); 208 } 209 else { 210 return Collections.EMPTY_LIST; 211 } 212 } 213 214 /** 215 * Calculates the space required for the axes. 216 * 217 * @param g2 the graphics device. 218 * @param plotArea the plot area. 219 * 220 * @return The space required for the axes. 221 */ 222 @Override 223 protected AxisSpace calculateAxisSpace(Graphics2D g2, 224 Rectangle2D plotArea) { 225 226 AxisSpace space = new AxisSpace(); 227 PlotOrientation orientation = getOrientation(); 228 229 // work out the space required by the domain axis... 230 AxisSpace fixed = getFixedRangeAxisSpace(); 231 if (fixed != null) { 232 if (orientation == PlotOrientation.VERTICAL) { 233 space.setLeft(fixed.getLeft()); 234 space.setRight(fixed.getRight()); 235 } 236 else if (orientation == PlotOrientation.HORIZONTAL) { 237 space.setTop(fixed.getTop()); 238 space.setBottom(fixed.getBottom()); 239 } 240 } 241 else { 242 ValueAxis valueAxis = getRangeAxis(); 243 RectangleEdge valueEdge = Plot.resolveRangeAxisLocation( 244 getRangeAxisLocation(), orientation); 245 if (valueAxis != null) { 246 space = valueAxis.reserveSpace(g2, this, plotArea, valueEdge, 247 space); 248 } 249 } 250 251 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null); 252 // work out the maximum height or width of the non-shared axes... 253 int n = this.subplots.size(); 254 int totalWeight = 0; 255 for (int i = 0; i < n; i++) { 256 CategoryPlot sub = (CategoryPlot) this.subplots.get(i); 257 totalWeight += sub.getWeight(); 258 } 259 // calculate plotAreas of all sub-plots, maximum vertical/horizontal 260 // axis width/height 261 this.subplotArea = new Rectangle2D[n]; 262 double x = adjustedPlotArea.getX(); 263 double y = adjustedPlotArea.getY(); 264 double usableSize = 0.0; 265 if (orientation == PlotOrientation.VERTICAL) { 266 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1); 267 } 268 else if (orientation == PlotOrientation.HORIZONTAL) { 269 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1); 270 } 271 272 for (int i = 0; i < n; i++) { 273 CategoryPlot plot = (CategoryPlot) this.subplots.get(i); 274 275 // calculate sub-plot area 276 if (orientation == PlotOrientation.VERTICAL) { 277 double w = usableSize * plot.getWeight() / totalWeight; 278 this.subplotArea[i] = new Rectangle2D.Double(x, y, w, 279 adjustedPlotArea.getHeight()); 280 x = x + w + this.gap; 281 } 282 else if (orientation == PlotOrientation.HORIZONTAL) { 283 double h = usableSize * plot.getWeight() / totalWeight; 284 this.subplotArea[i] = new Rectangle2D.Double(x, y, 285 adjustedPlotArea.getWidth(), h); 286 y = y + h + this.gap; 287 } 288 289 AxisSpace subSpace = plot.calculateDomainAxisSpace(g2, 290 this.subplotArea[i], null); 291 space.ensureAtLeast(subSpace); 292 293 } 294 295 return space; 296 } 297 298 /** 299 * Draws the plot on a Java 2D graphics device (such as the screen or a 300 * printer). Will perform all the placement calculations for each 301 * sub-plots and then tell these to draw themselves. 302 * 303 * @param g2 the graphics device. 304 * @param area the area within which the plot (including axis labels) 305 * should be drawn. 306 * @param anchor the anchor point ({@code null} permitted). 307 * @param parentState the parent state. 308 * @param info collects information about the drawing ({@code null} 309 * permitted). 310 */ 311 @Override 312 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 313 PlotState parentState, 314 PlotRenderingInfo info) { 315 316 // set up info collection... 317 if (info != null) { 318 info.setPlotArea(area); 319 } 320 321 // adjust the drawing area for plot insets (if any)... 322 RectangleInsets insets = getInsets(); 323 insets.trim(area); 324 325 // calculate the data area... 326 AxisSpace space = calculateAxisSpace(g2, area); 327 Rectangle2D dataArea = space.shrink(area, null); 328 329 // set the width and height of non-shared axis of all sub-plots 330 setFixedDomainAxisSpaceForSubplots(space); 331 332 // draw the shared axis 333 ValueAxis axis = getRangeAxis(); 334 RectangleEdge rangeEdge = getRangeAxisEdge(); 335 double cursor = RectangleEdge.coordinate(dataArea, rangeEdge); 336 AxisState state = axis.draw(g2, cursor, area, dataArea, rangeEdge, 337 info); 338 if (parentState == null) { 339 parentState = new PlotState(); 340 } 341 parentState.getSharedAxisStates().put(axis, state); 342 343 // draw all the charts 344 for (int i = 0; i < this.subplots.size(); i++) { 345 CategoryPlot plot = (CategoryPlot) this.subplots.get(i); 346 PlotRenderingInfo subplotInfo = null; 347 if (info != null) { 348 subplotInfo = new PlotRenderingInfo(info.getOwner()); 349 info.addSubplotInfo(subplotInfo); 350 } 351 Point2D subAnchor = null; 352 if (anchor != null && this.subplotArea[i].contains(anchor)) { 353 subAnchor = anchor; 354 } 355 plot.draw(g2, this.subplotArea[i], subAnchor, parentState, 356 subplotInfo); 357 } 358 359 if (info != null) { 360 info.setDataArea(dataArea); 361 } 362 363 } 364 365 /** 366 * Sets the orientation for the plot (and all the subplots). 367 * 368 * @param orientation the orientation. 369 */ 370 @Override 371 public void setOrientation(PlotOrientation orientation) { 372 super.setOrientation(orientation); 373 Iterator iterator = this.subplots.iterator(); 374 while (iterator.hasNext()) { 375 CategoryPlot plot = (CategoryPlot) iterator.next(); 376 plot.setOrientation(orientation); 377 } 378 } 379 380 /** 381 * Sets the shadow generator for the plot (and all subplots) and sends 382 * a {@link PlotChangeEvent} to all registered listeners. 383 * 384 * @param generator the new generator ({@code null} permitted). 385 */ 386 @Override 387 public void setShadowGenerator(ShadowGenerator generator) { 388 setNotify(false); 389 super.setShadowGenerator(generator); 390 Iterator iterator = this.subplots.iterator(); 391 while (iterator.hasNext()) { 392 CategoryPlot plot = (CategoryPlot) iterator.next(); 393 plot.setShadowGenerator(generator); 394 } 395 setNotify(true); 396 } 397 398 /** 399 * Returns a range representing the extent of the data values in this plot 400 * (obtained from the subplots) that will be rendered against the specified 401 * axis. NOTE: This method is intended for internal JFreeChart use, and 402 * is public only so that code in the axis classes can call it. Since 403 * only the range axis is shared between subplots, the JFreeChart code 404 * will only call this method for the range values (although this is not 405 * checked/enforced). 406 * 407 * @param axis the axis. 408 * 409 * @return The range. 410 */ 411 @Override 412 public Range getDataRange(ValueAxis axis) { 413 Range result = null; 414 if (this.subplots != null) { 415 Iterator iterator = this.subplots.iterator(); 416 while (iterator.hasNext()) { 417 CategoryPlot subplot = (CategoryPlot) iterator.next(); 418 result = Range.combine(result, subplot.getDataRange(axis)); 419 } 420 } 421 return result; 422 } 423 424 /** 425 * Returns a collection of legend items for the plot. 426 * 427 * @return The legend items. 428 */ 429 @Override 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 @Override 469 public void handleClick(int x, int y, PlotRenderingInfo info) { 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 * Receives a {@link PlotChangeEvent} and responds by notifying all 482 * listeners. 483 * 484 * @param event the event. 485 */ 486 @Override 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} permitted). 495 * 496 * @return {@code true} or {@code false}. 497 */ 498 @Override 499 public boolean equals(Object obj) { 500 if (obj == this) { 501 return true; 502 } 503 if (!(obj instanceof CombinedRangeCategoryPlot)) { 504 return false; 505 } 506 CombinedRangeCategoryPlot that = (CombinedRangeCategoryPlot) obj; 507 if (!that.canEqual(this)){ 508 return false; 509 } 510 if (Double.compare(this.gap, that.gap) != 0) { 511 return false; 512 } 513 if (!Objects.equals(this.subplots, that.subplots)) { 514 return false; 515 } 516 return super.equals(obj); 517 } 518 519 /** 520 * Ensures symmetry between super/subclass implementations of equals. For 521 * more detail, see http://jqno.nl/equalsverifier/manual/inheritance. 522 * 523 * @param other Object 524 * 525 * @return true ONLY if the parameter is THIS class type 526 */ 527 @Override 528 public boolean canEqual(Object other) { 529 // Solves Problem: equals not symmetric 530 return (other instanceof CombinedRangeCategoryPlot); 531 } 532 533 @Override 534 public int hashCode() { 535 int hash = super.hashCode(); 536 hash = 61 * hash + Objects.hashCode(this.subplots); 537 hash = 61 * hash + (int) (Double.doubleToLongBits(this.gap) ^ 538 (Double.doubleToLongBits(this.gap) >>> 32)); 539 return hash; 540 } 541 542 /** 543 * Returns a clone of the plot. 544 * 545 * @return A clone. 546 * 547 * @throws CloneNotSupportedException this class will not throw this 548 * exception, but subclasses (if any) might. 549 */ 550 @Override 551 public Object clone() throws CloneNotSupportedException { 552 CombinedRangeCategoryPlot result 553 = (CombinedRangeCategoryPlot) super.clone(); 554 result.subplots = (List) ObjectUtils.deepClone(this.subplots); 555 for (Iterator it = result.subplots.iterator(); it.hasNext();) { 556 Plot child = (Plot) it.next(); 557 child.setParent(result); 558 } 559 560 // after setting up all the subplots, the shared range axis may need 561 // reconfiguring 562 ValueAxis rangeAxis = result.getRangeAxis(); 563 if (rangeAxis != null) { 564 rangeAxis.configure(); 565 } 566 567 return result; 568 } 569 570 /** 571 * Provides serialization support. 572 * 573 * @param stream the input stream. 574 * 575 * @throws IOException if there is an I/O error. 576 * @throws ClassNotFoundException if there is a classpath problem. 577 */ 578 private void readObject(ObjectInputStream stream) 579 throws IOException, ClassNotFoundException { 580 581 stream.defaultReadObject(); 582 583 // the range axis is deserialized before the subplots, so its value 584 // range is likely to be incorrect... 585 ValueAxis rangeAxis = getRangeAxis(); 586 if (rangeAxis != null) { 587 rangeAxis.configure(); 588 } 589 590 } 591 592}