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