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