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