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