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