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