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 * XYSeriesCollection.java 029 * ----------------------- 030 * (C) Copyright 2001-2021, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Aaron Metzger; 034 * 035 */ 036 037package org.jfree.data.xy; 038 039import java.beans.PropertyChangeEvent; 040import java.beans.PropertyVetoException; 041import java.beans.VetoableChangeListener; 042import java.io.Serializable; 043import java.util.Collections; 044import java.util.Iterator; 045import java.util.List; 046import java.util.Objects; 047 048import org.jfree.chart.HashUtils; 049import org.jfree.chart.util.ObjectUtils; 050import org.jfree.chart.util.Args; 051import org.jfree.chart.util.PublicCloneable; 052import org.jfree.data.DomainInfo; 053import org.jfree.data.DomainOrder; 054import org.jfree.data.Range; 055import org.jfree.data.RangeInfo; 056import org.jfree.data.UnknownKeyException; 057import org.jfree.data.general.DatasetChangeEvent; 058import org.jfree.data.general.Series; 059 060/** 061 * Represents a collection of {@link XYSeries} objects that can be used as a 062 * dataset. 063 */ 064public class XYSeriesCollection extends AbstractIntervalXYDataset 065 implements IntervalXYDataset, DomainInfo, RangeInfo, 066 VetoableChangeListener, PublicCloneable, Serializable { 067 068 /** For serialization. */ 069 private static final long serialVersionUID = -7590013825931496766L; 070 071 /** The series that are included in the collection. */ 072 private List data; 073 074 /** The interval delegate (used to calculate the start and end x-values). */ 075 private IntervalXYDelegate intervalDelegate; 076 077 /** 078 * Constructs an empty dataset. 079 */ 080 public XYSeriesCollection() { 081 this(null); 082 } 083 084 /** 085 * Constructs a dataset and populates it with a single series. 086 * 087 * @param series the series ({@code null} ignored). 088 */ 089 public XYSeriesCollection(XYSeries series) { 090 this.data = new java.util.ArrayList(); 091 this.intervalDelegate = new IntervalXYDelegate(this, false); 092 addChangeListener(this.intervalDelegate); 093 if (series != null) { 094 this.data.add(series); 095 series.addChangeListener(this); 096 series.addVetoableChangeListener(this); 097 } 098 } 099 100 /** 101 * Returns the order of the domain (X) values, if this is known. 102 * 103 * @return The domain order. 104 */ 105 @Override 106 public DomainOrder getDomainOrder() { 107 int seriesCount = getSeriesCount(); 108 for (int i = 0; i < seriesCount; i++) { 109 XYSeries s = getSeries(i); 110 if (!s.getAutoSort()) { 111 return DomainOrder.NONE; // we can't be sure of the order 112 } 113 } 114 return DomainOrder.ASCENDING; 115 } 116 117 /** 118 * Adds a series to the collection and sends a {@link DatasetChangeEvent} 119 * to all registered listeners. 120 * 121 * @param series the series ({@code null} not permitted). 122 * 123 * @throws IllegalArgumentException if the key for the series is null or 124 * not unique within the dataset. 125 */ 126 public void addSeries(XYSeries series) { 127 Args.nullNotPermitted(series, "series"); 128 if (getSeriesIndex(series.getKey()) >= 0) { 129 throw new IllegalArgumentException( 130 "This dataset already contains a series with the key " 131 + series.getKey()); 132 } 133 this.data.add(series); 134 series.addChangeListener(this); 135 series.addVetoableChangeListener(this); 136 fireDatasetChanged(); 137 } 138 139 /** 140 * Removes a series from the collection and sends a 141 * {@link DatasetChangeEvent} to all registered listeners. 142 * 143 * @param series the series index (zero-based). 144 */ 145 public void removeSeries(int series) { 146 if ((series < 0) || (series >= getSeriesCount())) { 147 throw new IllegalArgumentException("Series index out of bounds."); 148 } 149 XYSeries s = (XYSeries) this.data.get(series); 150 if (s != null) { 151 removeSeries(s); 152 } 153 } 154 155 /** 156 * Removes a series from the collection and sends a 157 * {@link DatasetChangeEvent} to all registered listeners. 158 * 159 * @param series the series ({@code null} not permitted). 160 */ 161 public void removeSeries(XYSeries series) { 162 Args.nullNotPermitted(series, "series"); 163 if (this.data.contains(series)) { 164 series.removeChangeListener(this); 165 series.removeVetoableChangeListener(this); 166 this.data.remove(series); 167 fireDatasetChanged(); 168 } 169 } 170 171 /** 172 * Removes all the series from the collection and sends a 173 * {@link DatasetChangeEvent} to all registered listeners. 174 */ 175 public void removeAllSeries() { 176 // Unregister the collection as a change listener to each series in 177 // the collection. 178 for (int i = 0; i < this.data.size(); i++) { 179 XYSeries series = (XYSeries) this.data.get(i); 180 series.removeChangeListener(this); 181 series.removeVetoableChangeListener(this); 182 } 183 184 // Remove all the series from the collection and notify listeners. 185 this.data.clear(); 186 fireDatasetChanged(); 187 } 188 189 /** 190 * Returns the number of series in the collection. 191 * 192 * @return The series count. 193 */ 194 @Override 195 public int getSeriesCount() { 196 return this.data.size(); 197 } 198 199 /** 200 * Returns a list of all the series in the collection. 201 * 202 * @return The list (which is unmodifiable). 203 */ 204 public List getSeries() { 205 return Collections.unmodifiableList(this.data); 206 } 207 208 /** 209 * Returns the index of the specified series, or -1 if that series is not 210 * present in the dataset. 211 * 212 * @param series the series ({@code null} not permitted). 213 * 214 * @return The series index. 215 */ 216 public int indexOf(XYSeries series) { 217 Args.nullNotPermitted(series, "series"); 218 return this.data.indexOf(series); 219 } 220 221 /** 222 * Returns a series from the collection. 223 * 224 * @param series the series index (zero-based). 225 * 226 * @return The series. 227 * 228 * @throws IllegalArgumentException if {@code series} is not in the 229 * range {@code 0} to {@code getSeriesCount() - 1}. 230 */ 231 public XYSeries getSeries(int series) { 232 if ((series < 0) || (series >= getSeriesCount())) { 233 throw new IllegalArgumentException("Series index out of bounds"); 234 } 235 return (XYSeries) this.data.get(series); 236 } 237 238 /** 239 * Returns a series from the collection. 240 * 241 * @param key the key ({@code null} not permitted). 242 * 243 * @return The series with the specified key. 244 * 245 * @throws UnknownKeyException if {@code key} is not found in the 246 * collection. 247 */ 248 public XYSeries getSeries(Comparable key) { 249 Args.nullNotPermitted(key, "key"); 250 Iterator iterator = this.data.iterator(); 251 while (iterator.hasNext()) { 252 XYSeries series = (XYSeries) iterator.next(); 253 if (key.equals(series.getKey())) { 254 return series; 255 } 256 } 257 throw new UnknownKeyException("Key not found: " + key); 258 } 259 260 /** 261 * Returns the key for a series. 262 * 263 * @param series the series index (in the range {@code 0} to 264 * {@code getSeriesCount() - 1}). 265 * 266 * @return The key for a series. 267 * 268 * @throws IllegalArgumentException if {@code series} is not in the 269 * specified range. 270 */ 271 @Override 272 public Comparable getSeriesKey(int series) { 273 // defer argument checking 274 return getSeries(series).getKey(); 275 } 276 277 /** 278 * Returns the index of the series with the specified key, or -1 if no 279 * series has that key. 280 * 281 * @param key the key ({@code null} not permitted). 282 * 283 * @return The index. 284 */ 285 public int getSeriesIndex(Comparable key) { 286 Args.nullNotPermitted(key, "key"); 287 int seriesCount = getSeriesCount(); 288 for (int i = 0; i < seriesCount; i++) { 289 XYSeries series = (XYSeries) this.data.get(i); 290 if (key.equals(series.getKey())) { 291 return i; 292 } 293 } 294 return -1; 295 } 296 297 /** 298 * Returns the number of items in the specified series. 299 * 300 * @param series the series (zero-based index). 301 * 302 * @return The item count. 303 * 304 * @throws IllegalArgumentException if {@code series} is not in the 305 * range {@code 0} to {@code getSeriesCount() - 1}. 306 */ 307 @Override 308 public int getItemCount(int series) { 309 // defer argument checking 310 return getSeries(series).getItemCount(); 311 } 312 313 /** 314 * Returns the x-value for the specified series and item. 315 * 316 * @param series the series (zero-based index). 317 * @param item the item (zero-based index). 318 * 319 * @return The value. 320 */ 321 @Override 322 public Number getX(int series, int item) { 323 XYSeries s = (XYSeries) this.data.get(series); 324 return s.getX(item); 325 } 326 327 /** 328 * Returns the starting X value for the specified series and item. 329 * 330 * @param series the series (zero-based index). 331 * @param item the item (zero-based index). 332 * 333 * @return The starting X value. 334 */ 335 @Override 336 public Number getStartX(int series, int item) { 337 return this.intervalDelegate.getStartX(series, item); 338 } 339 340 /** 341 * Returns the ending X value for the specified series and item. 342 * 343 * @param series the series (zero-based index). 344 * @param item the item (zero-based index). 345 * 346 * @return The ending X value. 347 */ 348 @Override 349 public Number getEndX(int series, int item) { 350 return this.intervalDelegate.getEndX(series, item); 351 } 352 353 /** 354 * Returns the y-value for the specified series and item. 355 * 356 * @param series the series (zero-based index). 357 * @param index the index of the item of interest (zero-based). 358 * 359 * @return The value (possibly {@code null}). 360 */ 361 @Override 362 public Number getY(int series, int index) { 363 XYSeries s = (XYSeries) this.data.get(series); 364 return s.getY(index); 365 } 366 367 /** 368 * Returns the starting Y value for the specified series and item. 369 * 370 * @param series the series (zero-based index). 371 * @param item the item (zero-based index). 372 * 373 * @return The starting Y value. 374 */ 375 @Override 376 public Number getStartY(int series, int item) { 377 return getY(series, item); 378 } 379 380 /** 381 * Returns the ending Y value for the specified series and item. 382 * 383 * @param series the series (zero-based index). 384 * @param item the item (zero-based index). 385 * 386 * @return The ending Y value. 387 */ 388 @Override 389 public Number getEndY(int series, int item) { 390 return getY(series, item); 391 } 392 393 /** 394 * Tests this collection for equality with an arbitrary object. 395 * 396 * @param obj the object ({@code null} permitted). 397 * 398 * @return A boolean. 399 */ 400 @Override 401 public boolean equals(Object obj) { 402 if (obj == this) { 403 return true; 404 } 405 if (!(obj instanceof XYSeriesCollection)) { 406 return false; 407 } 408 XYSeriesCollection that = (XYSeriesCollection) obj; 409 if (!this.intervalDelegate.equals(that.intervalDelegate)) { 410 return false; 411 } 412 return Objects.equals(this.data, that.data); 413 } 414 415 /** 416 * Returns a clone of this instance. 417 * 418 * @return A clone. 419 * 420 * @throws CloneNotSupportedException if there is a problem. 421 */ 422 @Override 423 public Object clone() throws CloneNotSupportedException { 424 XYSeriesCollection clone = (XYSeriesCollection) super.clone(); 425 clone.data = (List) ObjectUtils.deepClone(this.data); 426 clone.intervalDelegate 427 = (IntervalXYDelegate) this.intervalDelegate.clone(); 428 return clone; 429 } 430 431 /** 432 * Returns a hash code. 433 * 434 * @return A hash code. 435 */ 436 @Override 437 public int hashCode() { 438 int hash = 5; 439 hash = HashUtils.hashCode(hash, this.intervalDelegate); 440 hash = HashUtils.hashCode(hash, this.data); 441 return hash; 442 } 443 444 /** 445 * Returns the minimum x-value in the dataset. 446 * 447 * @param includeInterval a flag that determines whether or not the 448 * x-interval is taken into account. 449 * 450 * @return The minimum value. 451 */ 452 @Override 453 public double getDomainLowerBound(boolean includeInterval) { 454 if (includeInterval) { 455 return this.intervalDelegate.getDomainLowerBound(includeInterval); 456 } 457 double result = Double.NaN; 458 int seriesCount = getSeriesCount(); 459 for (int s = 0; s < seriesCount; s++) { 460 XYSeries series = getSeries(s); 461 double lowX = series.getMinX(); 462 if (Double.isNaN(result)) { 463 result = lowX; 464 } 465 else { 466 if (!Double.isNaN(lowX)) { 467 result = Math.min(result, lowX); 468 } 469 } 470 } 471 return result; 472 } 473 474 /** 475 * Returns the maximum x-value in the dataset. 476 * 477 * @param includeInterval a flag that determines whether or not the 478 * x-interval is taken into account. 479 * 480 * @return The maximum value. 481 */ 482 @Override 483 public double getDomainUpperBound(boolean includeInterval) { 484 if (includeInterval) { 485 return this.intervalDelegate.getDomainUpperBound(includeInterval); 486 } 487 else { 488 double result = Double.NaN; 489 int seriesCount = getSeriesCount(); 490 for (int s = 0; s < seriesCount; s++) { 491 XYSeries series = getSeries(s); 492 double hiX = series.getMaxX(); 493 if (Double.isNaN(result)) { 494 result = hiX; 495 } 496 else { 497 if (!Double.isNaN(hiX)) { 498 result = Math.max(result, hiX); 499 } 500 } 501 } 502 return result; 503 } 504 } 505 506 /** 507 * Returns the range of the values in this dataset's domain. 508 * 509 * @param includeInterval a flag that determines whether or not the 510 * x-interval is taken into account. 511 * 512 * @return The range (or {@code null} if the dataset contains no 513 * values). 514 */ 515 @Override 516 public Range getDomainBounds(boolean includeInterval) { 517 if (includeInterval) { 518 return this.intervalDelegate.getDomainBounds(includeInterval); 519 } 520 else { 521 double lower = Double.POSITIVE_INFINITY; 522 double upper = Double.NEGATIVE_INFINITY; 523 int seriesCount = getSeriesCount(); 524 for (int s = 0; s < seriesCount; s++) { 525 XYSeries series = getSeries(s); 526 double minX = series.getMinX(); 527 if (!Double.isNaN(minX)) { 528 lower = Math.min(lower, minX); 529 } 530 double maxX = series.getMaxX(); 531 if (!Double.isNaN(maxX)) { 532 upper = Math.max(upper, maxX); 533 } 534 } 535 if (lower > upper) { 536 return null; 537 } 538 else { 539 return new Range(lower, upper); 540 } 541 } 542 } 543 544 /** 545 * Returns the interval width. This is used to calculate the start and end 546 * x-values, if/when the dataset is used as an {@link IntervalXYDataset}. 547 * 548 * @return The interval width. 549 */ 550 public double getIntervalWidth() { 551 return this.intervalDelegate.getIntervalWidth(); 552 } 553 554 /** 555 * Sets the interval width and sends a {@link DatasetChangeEvent} to all 556 * registered listeners. 557 * 558 * @param width the width (negative values not permitted). 559 */ 560 public void setIntervalWidth(double width) { 561 if (width < 0.0) { 562 throw new IllegalArgumentException("Negative 'width' argument."); 563 } 564 this.intervalDelegate.setFixedIntervalWidth(width); 565 fireDatasetChanged(); 566 } 567 568 /** 569 * Returns the interval position factor. 570 * 571 * @return The interval position factor. 572 */ 573 public double getIntervalPositionFactor() { 574 return this.intervalDelegate.getIntervalPositionFactor(); 575 } 576 577 /** 578 * Sets the interval position factor. This controls where the x-value is in 579 * relation to the interval surrounding the x-value (0.0 means the x-value 580 * will be positioned at the start, 0.5 in the middle, and 1.0 at the end). 581 * 582 * @param factor the factor. 583 */ 584 public void setIntervalPositionFactor(double factor) { 585 this.intervalDelegate.setIntervalPositionFactor(factor); 586 fireDatasetChanged(); 587 } 588 589 /** 590 * Returns whether the interval width is automatically calculated or not. 591 * 592 * @return Whether the width is automatically calculated or not. 593 */ 594 public boolean isAutoWidth() { 595 return this.intervalDelegate.isAutoWidth(); 596 } 597 598 /** 599 * Sets the flag that indicates whether the interval width is automatically 600 * calculated or not. 601 * 602 * @param b a boolean. 603 */ 604 public void setAutoWidth(boolean b) { 605 this.intervalDelegate.setAutoWidth(b); 606 fireDatasetChanged(); 607 } 608 609 /** 610 * Returns the range of the values in this dataset's range. 611 * 612 * @param includeInterval ignored. 613 * 614 * @return The range (or {@code null} if the dataset contains no 615 * values). 616 */ 617 @Override 618 public Range getRangeBounds(boolean includeInterval) { 619 double lower = Double.POSITIVE_INFINITY; 620 double upper = Double.NEGATIVE_INFINITY; 621 int seriesCount = getSeriesCount(); 622 for (int s = 0; s < seriesCount; s++) { 623 XYSeries series = getSeries(s); 624 double minY = series.getMinY(); 625 if (!Double.isNaN(minY)) { 626 lower = Math.min(lower, minY); 627 } 628 double maxY = series.getMaxY(); 629 if (!Double.isNaN(maxY)) { 630 upper = Math.max(upper, maxY); 631 } 632 } 633 if (lower > upper) { 634 return null; 635 } 636 else { 637 return new Range(lower, upper); 638 } 639 } 640 641 /** 642 * Returns the minimum y-value in the dataset. 643 * 644 * @param includeInterval a flag that determines whether or not the 645 * y-interval is taken into account. 646 * 647 * @return The minimum value. 648 */ 649 @Override 650 public double getRangeLowerBound(boolean includeInterval) { 651 double result = Double.NaN; 652 int seriesCount = getSeriesCount(); 653 for (int s = 0; s < seriesCount; s++) { 654 XYSeries series = getSeries(s); 655 double lowY = series.getMinY(); 656 if (Double.isNaN(result)) { 657 result = lowY; 658 } 659 else { 660 if (!Double.isNaN(lowY)) { 661 result = Math.min(result, lowY); 662 } 663 } 664 } 665 return result; 666 } 667 668 /** 669 * Returns the maximum y-value in the dataset. 670 * 671 * @param includeInterval a flag that determines whether or not the 672 * y-interval is taken into account. 673 * 674 * @return The maximum value. 675 */ 676 @Override 677 public double getRangeUpperBound(boolean includeInterval) { 678 double result = Double.NaN; 679 int seriesCount = getSeriesCount(); 680 for (int s = 0; s < seriesCount; s++) { 681 XYSeries series = getSeries(s); 682 double hiY = series.getMaxY(); 683 if (Double.isNaN(result)) { 684 result = hiY; 685 } 686 else { 687 if (!Double.isNaN(hiY)) { 688 result = Math.max(result, hiY); 689 } 690 } 691 } 692 return result; 693 } 694 695 /** 696 * Receives notification that the key for one of the series in the 697 * collection has changed, and vetos it if the key is already present in 698 * the collection. 699 * 700 * @param e the event. 701 * 702 * @throws PropertyVetoException if the series name is already present in 703 * the collection. 704 */ 705 @Override 706 public void vetoableChange(PropertyChangeEvent e) throws PropertyVetoException { 707 // if it is not the series name, then we have no interest 708 if (!"Key".equals(e.getPropertyName())) { 709 return; 710 } 711 712 // to be defensive, let's check that the source series does in fact 713 // belong to this collection 714 Series s = (Series) e.getSource(); 715 if (getSeriesIndex(s.getKey()) == -1) { 716 throw new IllegalStateException("Receiving events from a series " + 717 "that does not belong to this collection."); 718 } 719 // check if the new series name already exists for another series 720 Comparable key = (Comparable) e.getNewValue(); 721 if (getSeriesIndex(key) >= 0) { 722 throw new PropertyVetoException("Duplicate key2", e); 723 } 724 } 725 726}