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