001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2022, 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-2022, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 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.ArrayList; 045import java.util.Collections; 046import java.util.List; 047import java.util.Objects; 048import org.jfree.chart.ChartElementVisitor; 049 050import org.jfree.chart.legend.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.api.RectangleEdge; 058import org.jfree.chart.api.RectangleInsets; 059import org.jfree.chart.internal.Args; 060import org.jfree.chart.internal.CloneUtils; 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<CategoryPlot> 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 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<CategoryPlot> 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 = 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 = 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 * Receives a chart element visitor. Many plot subclasses will override 300 * this method to handle their subcomponents. 301 * 302 * @param visitor the visitor ({@code null} not permitted). 303 */ 304 @Override 305 public void receive(ChartElementVisitor visitor) { 306 subplots.forEach(subplot -> { 307 subplot.receive(visitor); 308 }); 309 super.receive(visitor); 310 } 311 312 /** 313 * Draws the plot on a Java 2D graphics device (such as the screen or a 314 * printer). Will perform all the placement calculations for each 315 * sub-plots and then tell these to draw themselves. 316 * 317 * @param g2 the graphics device. 318 * @param area the area within which the plot (including axis labels) 319 * should be drawn. 320 * @param anchor the anchor point ({@code null} permitted). 321 * @param parentState the parent state. 322 * @param info collects information about the drawing ({@code null} 323 * permitted). 324 */ 325 @Override 326 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 327 PlotState parentState, PlotRenderingInfo info) { 328 329 // set up info collection... 330 if (info != null) { 331 info.setPlotArea(area); 332 } 333 334 // adjust the drawing area for plot insets (if any)... 335 RectangleInsets insets = getInsets(); 336 insets.trim(area); 337 338 // calculate the data area... 339 AxisSpace space = calculateAxisSpace(g2, area); 340 Rectangle2D dataArea = space.shrink(area, null); 341 342 // set the width and height of non-shared axis of all sub-plots 343 setFixedDomainAxisSpaceForSubplots(space); 344 345 // draw the shared axis 346 ValueAxis axis = getRangeAxis(); 347 RectangleEdge rangeEdge = getRangeAxisEdge(); 348 double cursor = RectangleEdge.coordinate(dataArea, rangeEdge); 349 AxisState state = axis.draw(g2, cursor, area, dataArea, rangeEdge, 350 info); 351 if (parentState == null) { 352 parentState = new PlotState(); 353 } 354 parentState.getSharedAxisStates().put(axis, state); 355 356 // draw all the charts 357 for (int i = 0; i < this.subplots.size(); i++) { 358 CategoryPlot plot = this.subplots.get(i); 359 PlotRenderingInfo subplotInfo = null; 360 if (info != null) { 361 subplotInfo = new PlotRenderingInfo(info.getOwner()); 362 info.addSubplotInfo(subplotInfo); 363 } 364 Point2D subAnchor = null; 365 if (anchor != null && this.subplotArea[i].contains(anchor)) { 366 subAnchor = anchor; 367 } 368 plot.draw(g2, this.subplotArea[i], subAnchor, parentState, 369 subplotInfo); 370 } 371 372 if (info != null) { 373 info.setDataArea(dataArea); 374 } 375 376 } 377 378 /** 379 * Sets the orientation for the plot (and all the subplots). 380 * 381 * @param orientation the orientation. 382 */ 383 @Override 384 public void setOrientation(PlotOrientation orientation) { 385 super.setOrientation(orientation); 386 for (CategoryPlot subplot : this.subplots) { 387 subplot.setOrientation(orientation); 388 } 389 } 390 391 /** 392 * Sets the shadow generator for the plot (and all subplots) and sends 393 * a {@link PlotChangeEvent} to all registered listeners. 394 * 395 * @param generator the new generator ({@code null} permitted). 396 */ 397 @Override 398 public void setShadowGenerator(ShadowGenerator generator) { 399 setNotify(false); 400 super.setShadowGenerator(generator); 401 for (CategoryPlot subplot : this.subplots) { 402 subplot.setShadowGenerator(generator); 403 } 404 setNotify(true); 405 } 406 407 /** 408 * Returns a range representing the extent of the data values in this plot 409 * (obtained from the subplots) that will be rendered against the specified 410 * axis. NOTE: This method is intended for internal JFreeChart use, and 411 * is public only so that code in the axis classes can call it. Since 412 * only the range axis is shared between subplots, the JFreeChart code 413 * will only call this method for the range values (although this is not 414 * checked/enforced). 415 * 416 * @param axis the axis. 417 * 418 * @return The range. 419 */ 420 @Override 421 public Range getDataRange(ValueAxis axis) { 422 Range result = null; 423 if (this.subplots != null) { 424 for (CategoryPlot subplot : this.subplots) { 425 result = Range.combine(result, subplot.getDataRange(axis)); 426 } 427 } 428 return result; 429 } 430 431 /** 432 * Returns a collection of legend items for the plot. 433 * 434 * @return The legend items. 435 */ 436 @Override 437 public LegendItemCollection getLegendItems() { 438 LegendItemCollection result = getFixedLegendItems(); 439 if (result == null) { 440 result = new LegendItemCollection(); 441 if (this.subplots != null) { 442 for (CategoryPlot subplot : this.subplots) { 443 LegendItemCollection more = subplot.getLegendItems(); 444 result.addAll(more); 445 } 446 } 447 } 448 return result; 449 } 450 451 /** 452 * Sets the size (width or height, depending on the orientation of the 453 * plot) for the domain axis of each subplot. 454 * 455 * @param space the space. 456 */ 457 protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) { 458 for (CategoryPlot subplot : this.subplots) { 459 subplot.setFixedDomainAxisSpace(space, false); 460 } 461 } 462 463 /** 464 * Handles a 'click' on the plot by updating the anchor value. 465 * 466 * @param x x-coordinate of the click. 467 * @param y y-coordinate of the click. 468 * @param info information about the plot's dimensions. 469 * 470 */ 471 @Override 472 public void handleClick(int x, int y, PlotRenderingInfo info) { 473 Rectangle2D dataArea = info.getDataArea(); 474 if (dataArea.contains(x, y)) { 475 for (int i = 0; i < this.subplots.size(); i++) { 476 CategoryPlot subplot = this.subplots.get(i); 477 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i); 478 subplot.handleClick(x, y, subplotInfo); 479 } 480 } 481 } 482 483 /** 484 * Receives a {@link PlotChangeEvent} and responds by notifying all 485 * listeners. 486 * 487 * @param event the event. 488 */ 489 @Override 490 public void plotChanged(PlotChangeEvent event) { 491 notifyListeners(event); 492 } 493 494 /** 495 * Tests the plot for equality with an arbitrary object. 496 * 497 * @param obj the object ({@code null} permitted). 498 * 499 * @return {@code true} or {@code false}. 500 */ 501 @Override 502 public boolean equals(Object obj) { 503 if (obj == this) { 504 return true; 505 } 506 if (!(obj instanceof CombinedRangeCategoryPlot)) { 507 return false; 508 } 509 CombinedRangeCategoryPlot that = (CombinedRangeCategoryPlot) obj; 510 if (this.gap != that.gap) { 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 * Returns a clone of the plot. 521 * 522 * @return A clone. 523 * 524 * @throws CloneNotSupportedException this class will not throw this 525 * exception, but subclasses (if any) might. 526 */ 527 @Override 528 public Object clone() throws CloneNotSupportedException { 529 CombinedRangeCategoryPlot result 530 = (CombinedRangeCategoryPlot) super.clone(); 531 result.subplots = CloneUtils.cloneList(this.subplots); 532 for (Plot child : result.subplots) { 533 child.setParent(result); 534 } 535 536 // after setting up all the subplots, the shared range axis may need 537 // reconfiguring 538 ValueAxis rangeAxis = result.getRangeAxis(); 539 if (rangeAxis != null) { 540 rangeAxis.configure(); 541 } 542 543 return result; 544 } 545 546 /** 547 * Provides serialization support. 548 * 549 * @param stream the input stream. 550 * 551 * @throws IOException if there is an I/O error. 552 * @throws ClassNotFoundException if there is a classpath problem. 553 */ 554 private void readObject(ObjectInputStream stream) throws IOException, 555 ClassNotFoundException { 556 557 stream.defaultReadObject(); 558 559 // the range axis is deserialized before the subplots, so its value 560 // range is likely to be incorrect... 561 ValueAxis rangeAxis = getRangeAxis(); 562 if (rangeAxis != null) { 563 rangeAxis.configure(); 564 } 565 566 } 567 568}