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