Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors. 6: * 7: * Project Info: http://www.jfree.org/jfreechart/index.html 8: * 9: * This library is free software; you can redistribute it and/or modify it 10: * under the terms of the GNU Lesser General Public License as published by 11: * the Free Software Foundation; either version 2.1 of the License, or 12: * (at your option) any later version. 13: * 14: * This library is distributed in the hope that it will be useful, but 15: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17: * License for more details. 18: * 19: * You should have received a copy of the GNU Lesser General Public 20: * License along with this library; if not, write to the Free Software 21: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22: * USA. 23: * 24: * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 25: * in the United States and other countries.] 26: * 27: * ------------------------- 28: * TimeSeriesCollection.java 29: * ------------------------- 30: * (C) Copyright 2001-2007, by Object Refinery Limited. 31: * 32: * Original Author: David Gilbert (for Object Refinery Limited); 33: * Contributor(s): -; 34: * 35: * Changes 36: * ------- 37: * 11-Oct-2001 : Version 1 (DG); 38: * 18-Oct-2001 : Added implementation of IntervalXYDataSource so that bar plots 39: * (using numerical axes) can be plotted from time series 40: * data (DG); 41: * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG); 42: * 15-Nov-2001 : Added getSeries() method. Changed name from TimeSeriesDataset 43: * to TimeSeriesCollection (DG); 44: * 07-Dec-2001 : TimeSeries --> BasicTimeSeries (DG); 45: * 01-Mar-2002 : Added a time zone offset attribute, to enable fast calculation 46: * of the time period start and end values (DG); 47: * 29-Mar-2002 : The collection now registers itself with all the time series 48: * objects as a SeriesChangeListener. Removed redundant 49: * calculateZoneOffset method (DG); 50: * 06-Jun-2002 : Added a setting to control whether the x-value supplied in the 51: * getXValue() method comes from the START, MIDDLE, or END of the 52: * time period. This is a workaround for JFreeChart, where the 53: * current date axis always labels the start of a time 54: * period (DG); 55: * 24-Jun-2002 : Removed unnecessary import (DG); 56: * 24-Aug-2002 : Implemented DomainInfo interface, and added the 57: * DomainIsPointsInTime flag (DG); 58: * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG); 59: * 16-Oct-2002 : Added remove methods (DG); 60: * 10-Jan-2003 : Changed method names in RegularTimePeriod class (DG); 61: * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented 62: * Serializable (DG); 63: * 04-Sep-2003 : Added getSeries(String) method (DG); 64: * 15-Sep-2003 : Added a removeAllSeries() method to match 65: * XYSeriesCollection (DG); 66: * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG); 67: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 68: * getYValue() (DG); 69: * 06-Oct-2004 : Updated for changed in DomainInfo interface (DG); 70: * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 71: * release (DG); 72: * 28-Mar-2005 : Fixed bug in getSeries(int) method (1170825) (DG); 73: * ------------- JFREECHART 1.0.x --------------------------------------------- 74: * 13-Dec-2005 : Deprecated the 'domainIsPointsInTime' flag as it is 75: * redundant. Fixes bug 1243050 (DG); 76: * 04-May-2007 : Override getDomainOrder() to indicate that items are sorted 77: * by x-value (ascending) (DG); 78: * 08-May-2007 : Added indexOf(TimeSeries) method (DG); 79: * 80: */ 81: 82: package org.jfree.data.time; 83: 84: import java.io.Serializable; 85: import java.util.ArrayList; 86: import java.util.Calendar; 87: import java.util.Collections; 88: import java.util.Iterator; 89: import java.util.List; 90: import java.util.TimeZone; 91: 92: import org.jfree.data.DomainInfo; 93: import org.jfree.data.DomainOrder; 94: import org.jfree.data.Range; 95: import org.jfree.data.general.DatasetChangeEvent; 96: import org.jfree.data.xy.AbstractIntervalXYDataset; 97: import org.jfree.data.xy.IntervalXYDataset; 98: import org.jfree.data.xy.XYDataset; 99: import org.jfree.util.ObjectUtilities; 100: 101: /** 102: * A collection of time series objects. This class implements the 103: * {@link org.jfree.data.xy.XYDataset} interface, as well as the extended 104: * {@link IntervalXYDataset} interface. This makes it a convenient dataset for 105: * use with the {@link org.jfree.chart.plot.XYPlot} class. 106: */ 107: public class TimeSeriesCollection extends AbstractIntervalXYDataset 108: implements XYDataset, 109: IntervalXYDataset, 110: DomainInfo, 111: Serializable { 112: 113: /** For serialization. */ 114: private static final long serialVersionUID = 834149929022371137L; 115: 116: /** Storage for the time series. */ 117: private List data; 118: 119: /** A working calendar (to recycle) */ 120: private Calendar workingCalendar; 121: 122: /** 123: * The point within each time period that is used for the X value when this 124: * collection is used as an {@link org.jfree.data.xy.XYDataset}. This can 125: * be the start, middle or end of the time period. 126: */ 127: private TimePeriodAnchor xPosition; 128: 129: /** 130: * A flag that indicates that the domain is 'points in time'. If this 131: * flag is true, only the x-value is used to determine the range of values 132: * in the domain, the start and end x-values are ignored. 133: * 134: * @deprecated No longer used (as of 1.0.1). 135: */ 136: private boolean domainIsPointsInTime; 137: 138: /** 139: * Constructs an empty dataset, tied to the default timezone. 140: */ 141: public TimeSeriesCollection() { 142: this(null, TimeZone.getDefault()); 143: } 144: 145: /** 146: * Constructs an empty dataset, tied to a specific timezone. 147: * 148: * @param zone the timezone (<code>null</code> permitted, will use 149: * <code>TimeZone.getDefault()</code> in that case). 150: */ 151: public TimeSeriesCollection(TimeZone zone) { 152: this(null, zone); 153: } 154: 155: /** 156: * Constructs a dataset containing a single series (more can be added), 157: * tied to the default timezone. 158: * 159: * @param series the series (<code>null</code> permitted). 160: */ 161: public TimeSeriesCollection(TimeSeries series) { 162: this(series, TimeZone.getDefault()); 163: } 164: 165: /** 166: * Constructs a dataset containing a single series (more can be added), 167: * tied to a specific timezone. 168: * 169: * @param series a series to add to the collection (<code>null</code> 170: * permitted). 171: * @param zone the timezone (<code>null</code> permitted, will use 172: * <code>TimeZone.getDefault()</code> in that case). 173: */ 174: public TimeSeriesCollection(TimeSeries series, TimeZone zone) { 175: 176: if (zone == null) { 177: zone = TimeZone.getDefault(); 178: } 179: this.workingCalendar = Calendar.getInstance(zone); 180: this.data = new ArrayList(); 181: if (series != null) { 182: this.data.add(series); 183: series.addChangeListener(this); 184: } 185: this.xPosition = TimePeriodAnchor.START; 186: this.domainIsPointsInTime = true; 187: 188: } 189: 190: /** 191: * Returns a flag that controls whether the domain is treated as 'points in 192: * time'. This flag is used when determining the max and min values for 193: * the domain. If <code>true</code>, then only the x-values are considered 194: * for the max and min values. If <code>false</code>, then the start and 195: * end x-values will also be taken into consideration. 196: * 197: * @return The flag. 198: * 199: * @deprecated This flag is no longer used (as of 1.0.1). 200: */ 201: public boolean getDomainIsPointsInTime() { 202: return this.domainIsPointsInTime; 203: } 204: 205: /** 206: * Sets a flag that controls whether the domain is treated as 'points in 207: * time', or time periods. 208: * 209: * @param flag the flag. 210: * 211: * @deprecated This flag is no longer used, as of 1.0.1. The 212: * <code>includeInterval</code> flag in methods such as 213: * {@link #getDomainBounds(boolean)} makes this unnecessary. 214: */ 215: public void setDomainIsPointsInTime(boolean flag) { 216: this.domainIsPointsInTime = flag; 217: notifyListeners(new DatasetChangeEvent(this, this)); 218: } 219: 220: /** 221: * Returns the order of the domain values in this dataset. 222: * 223: * @return {@link DomainOrder#ASCENDING} 224: */ 225: public DomainOrder getDomainOrder() { 226: return DomainOrder.ASCENDING; 227: } 228: 229: /** 230: * Returns the position within each time period that is used for the X 231: * value when the collection is used as an 232: * {@link org.jfree.data.xy.XYDataset}. 233: * 234: * @return The anchor position (never <code>null</code>). 235: */ 236: public TimePeriodAnchor getXPosition() { 237: return this.xPosition; 238: } 239: 240: /** 241: * Sets the position within each time period that is used for the X values 242: * when the collection is used as an {@link XYDataset}, then sends a 243: * {@link DatasetChangeEvent} is sent to all registered listeners. 244: * 245: * @param anchor the anchor position (<code>null</code> not permitted). 246: */ 247: public void setXPosition(TimePeriodAnchor anchor) { 248: if (anchor == null) { 249: throw new IllegalArgumentException("Null 'anchor' argument."); 250: } 251: this.xPosition = anchor; 252: notifyListeners(new DatasetChangeEvent(this, this)); 253: } 254: 255: /** 256: * Returns a list of all the series in the collection. 257: * 258: * @return The list (which is unmodifiable). 259: */ 260: public List getSeries() { 261: return Collections.unmodifiableList(this.data); 262: } 263: 264: /** 265: * Returns the number of series in the collection. 266: * 267: * @return The series count. 268: */ 269: public int getSeriesCount() { 270: return this.data.size(); 271: } 272: 273: /** 274: * Returns the index of the specified series, or -1 if that series is not 275: * present in the dataset. 276: * 277: * @param series the series (<code>null</code> not permitted). 278: * 279: * @return The series index. 280: * 281: * @since 1.0.6 282: */ 283: public int indexOf(TimeSeries series) { 284: if (series == null) { 285: throw new IllegalArgumentException("Null 'series' argument."); 286: } 287: return this.data.indexOf(series); 288: } 289: 290: /** 291: * Returns a series. 292: * 293: * @param series the index of the series (zero-based). 294: * 295: * @return The series. 296: */ 297: public TimeSeries getSeries(int series) { 298: if ((series < 0) || (series >= getSeriesCount())) { 299: throw new IllegalArgumentException( 300: "The 'series' argument is out of bounds (" + series + ")."); 301: } 302: return (TimeSeries) this.data.get(series); 303: } 304: 305: /** 306: * Returns the series with the specified key, or <code>null</code> if 307: * there is no such series. 308: * 309: * @param key the series key (<code>null</code> permitted). 310: * 311: * @return The series with the given key. 312: */ 313: public TimeSeries getSeries(String key) { 314: TimeSeries result = null; 315: Iterator iterator = this.data.iterator(); 316: while (iterator.hasNext()) { 317: TimeSeries series = (TimeSeries) iterator.next(); 318: Comparable k = series.getKey(); 319: if (k != null && k.equals(key)) { 320: result = series; 321: } 322: } 323: return result; 324: } 325: 326: /** 327: * Returns the key for a series. 328: * 329: * @param series the index of the series (zero-based). 330: * 331: * @return The key for a series. 332: */ 333: public Comparable getSeriesKey(int series) { 334: // check arguments...delegated 335: // fetch the series name... 336: return getSeries(series).getKey(); 337: } 338: 339: /** 340: * Adds a series to the collection and sends a {@link DatasetChangeEvent} to 341: * all registered listeners. 342: * 343: * @param series the series (<code>null</code> not permitted). 344: */ 345: public void addSeries(TimeSeries series) { 346: if (series == null) { 347: throw new IllegalArgumentException("Null 'series' argument."); 348: } 349: this.data.add(series); 350: series.addChangeListener(this); 351: fireDatasetChanged(); 352: } 353: 354: /** 355: * Removes the specified series from the collection and sends a 356: * {@link DatasetChangeEvent} to all registered listeners. 357: * 358: * @param series the series (<code>null</code> not permitted). 359: */ 360: public void removeSeries(TimeSeries series) { 361: if (series == null) { 362: throw new IllegalArgumentException("Null 'series' argument."); 363: } 364: this.data.remove(series); 365: series.removeChangeListener(this); 366: fireDatasetChanged(); 367: } 368: 369: /** 370: * Removes a series from the collection. 371: * 372: * @param index the series index (zero-based). 373: */ 374: public void removeSeries(int index) { 375: TimeSeries series = getSeries(index); 376: if (series != null) { 377: removeSeries(series); 378: } 379: } 380: 381: /** 382: * Removes all the series from the collection and sends a 383: * {@link DatasetChangeEvent} to all registered listeners. 384: */ 385: public void removeAllSeries() { 386: 387: // deregister the collection as a change listener to each series in the 388: // collection 389: for (int i = 0; i < this.data.size(); i++) { 390: TimeSeries series = (TimeSeries) this.data.get(i); 391: series.removeChangeListener(this); 392: } 393: 394: // remove all the series from the collection and notify listeners. 395: this.data.clear(); 396: fireDatasetChanged(); 397: 398: } 399: 400: /** 401: * Returns the number of items in the specified series. This method is 402: * provided for convenience. 403: * 404: * @param series the series index (zero-based). 405: * 406: * @return The item count. 407: */ 408: public int getItemCount(int series) { 409: return getSeries(series).getItemCount(); 410: } 411: 412: /** 413: * Returns the x-value (as a double primitive) for an item within a series. 414: * 415: * @param series the series (zero-based index). 416: * @param item the item (zero-based index). 417: * 418: * @return The x-value. 419: */ 420: public double getXValue(int series, int item) { 421: TimeSeries s = (TimeSeries) this.data.get(series); 422: TimeSeriesDataItem i = s.getDataItem(item); 423: RegularTimePeriod period = i.getPeriod(); 424: return getX(period); 425: } 426: 427: /** 428: * Returns the x-value for the specified series and item. 429: * 430: * @param series the series (zero-based index). 431: * @param item the item (zero-based index). 432: * 433: * @return The value. 434: */ 435: public Number getX(int series, int item) { 436: TimeSeries ts = (TimeSeries) this.data.get(series); 437: TimeSeriesDataItem dp = ts.getDataItem(item); 438: RegularTimePeriod period = dp.getPeriod(); 439: return new Long(getX(period)); 440: } 441: 442: /** 443: * Returns the x-value for a time period. 444: * 445: * @param period the time period (<code>null</code> not permitted). 446: * 447: * @return The x-value. 448: */ 449: protected synchronized long getX(RegularTimePeriod period) { 450: long result = 0L; 451: if (this.xPosition == TimePeriodAnchor.START) { 452: result = period.getFirstMillisecond(this.workingCalendar); 453: } 454: else if (this.xPosition == TimePeriodAnchor.MIDDLE) { 455: result = period.getMiddleMillisecond(this.workingCalendar); 456: } 457: else if (this.xPosition == TimePeriodAnchor.END) { 458: result = period.getLastMillisecond(this.workingCalendar); 459: } 460: return result; 461: } 462: 463: /** 464: * Returns the starting X value for the specified series and item. 465: * 466: * @param series the series (zero-based index). 467: * @param item the item (zero-based index). 468: * 469: * @return The value. 470: */ 471: public synchronized Number getStartX(int series, int item) { 472: TimeSeries ts = (TimeSeries) this.data.get(series); 473: TimeSeriesDataItem dp = ts.getDataItem(item); 474: return new Long(dp.getPeriod().getFirstMillisecond( 475: this.workingCalendar)); 476: } 477: 478: /** 479: * Returns the ending X value for the specified series and item. 480: * 481: * @param series The series (zero-based index). 482: * @param item The item (zero-based index). 483: * 484: * @return The value. 485: */ 486: public synchronized Number getEndX(int series, int item) { 487: TimeSeries ts = (TimeSeries) this.data.get(series); 488: TimeSeriesDataItem dp = ts.getDataItem(item); 489: return new Long(dp.getPeriod().getLastMillisecond( 490: this.workingCalendar)); 491: } 492: 493: /** 494: * Returns the y-value for the specified series and item. 495: * 496: * @param series the series (zero-based index). 497: * @param item the item (zero-based index). 498: * 499: * @return The value (possibly <code>null</code>). 500: */ 501: public Number getY(int series, int item) { 502: TimeSeries ts = (TimeSeries) this.data.get(series); 503: TimeSeriesDataItem dp = ts.getDataItem(item); 504: return dp.getValue(); 505: } 506: 507: /** 508: * Returns the starting Y value for the specified series and item. 509: * 510: * @param series the series (zero-based index). 511: * @param item the item (zero-based index). 512: * 513: * @return The value (possibly <code>null</code>). 514: */ 515: public Number getStartY(int series, int item) { 516: return getY(series, item); 517: } 518: 519: /** 520: * Returns the ending Y value for the specified series and item. 521: * 522: * @param series te series (zero-based index). 523: * @param item the item (zero-based index). 524: * 525: * @return The value (possibly <code>null</code>). 526: */ 527: public Number getEndY(int series, int item) { 528: return getY(series, item); 529: } 530: 531: 532: /** 533: * Returns the indices of the two data items surrounding a particular 534: * millisecond value. 535: * 536: * @param series the series index. 537: * @param milliseconds the time. 538: * 539: * @return An array containing the (two) indices of the items surrounding 540: * the time. 541: */ 542: public int[] getSurroundingItems(int series, long milliseconds) { 543: int[] result = new int[] {-1, -1}; 544: TimeSeries timeSeries = getSeries(series); 545: for (int i = 0; i < timeSeries.getItemCount(); i++) { 546: Number x = getX(series, i); 547: long m = x.longValue(); 548: if (m <= milliseconds) { 549: result[0] = i; 550: } 551: if (m >= milliseconds) { 552: result[1] = i; 553: break; 554: } 555: } 556: return result; 557: } 558: 559: /** 560: * Returns the minimum x-value in the dataset. 561: * 562: * @param includeInterval a flag that determines whether or not the 563: * x-interval is taken into account. 564: * 565: * @return The minimum value. 566: */ 567: public double getDomainLowerBound(boolean includeInterval) { 568: double result = Double.NaN; 569: Range r = getDomainBounds(includeInterval); 570: if (r != null) { 571: result = r.getLowerBound(); 572: } 573: return result; 574: } 575: 576: /** 577: * Returns the maximum x-value in the dataset. 578: * 579: * @param includeInterval a flag that determines whether or not the 580: * x-interval is taken into account. 581: * 582: * @return The maximum value. 583: */ 584: public double getDomainUpperBound(boolean includeInterval) { 585: double result = Double.NaN; 586: Range r = getDomainBounds(includeInterval); 587: if (r != null) { 588: result = r.getUpperBound(); 589: } 590: return result; 591: } 592: 593: /** 594: * Returns the range of the values in this dataset's domain. 595: * 596: * @param includeInterval a flag that determines whether or not the 597: * x-interval is taken into account. 598: * 599: * @return The range. 600: */ 601: public Range getDomainBounds(boolean includeInterval) { 602: Range result = null; 603: Iterator iterator = this.data.iterator(); 604: while (iterator.hasNext()) { 605: TimeSeries series = (TimeSeries) iterator.next(); 606: int count = series.getItemCount(); 607: if (count > 0) { 608: RegularTimePeriod start = series.getTimePeriod(0); 609: RegularTimePeriod end = series.getTimePeriod(count - 1); 610: Range temp; 611: if (!includeInterval) { 612: temp = new Range(getX(start), getX(end)); 613: } 614: else { 615: temp = new Range( 616: start.getFirstMillisecond(this.workingCalendar), 617: end.getLastMillisecond(this.workingCalendar)); 618: } 619: result = Range.combine(result, temp); 620: } 621: } 622: return result; 623: } 624: 625: /** 626: * Tests this time series collection for equality with another object. 627: * 628: * @param obj the other object. 629: * 630: * @return A boolean. 631: */ 632: public boolean equals(Object obj) { 633: if (obj == this) { 634: return true; 635: } 636: if (!(obj instanceof TimeSeriesCollection)) { 637: return false; 638: } 639: TimeSeriesCollection that = (TimeSeriesCollection) obj; 640: if (this.xPosition != that.xPosition) { 641: return false; 642: } 643: if (this.domainIsPointsInTime != that.domainIsPointsInTime) { 644: return false; 645: } 646: if (!ObjectUtilities.equal(this.data, that.data)) { 647: return false; 648: } 649: return true; 650: } 651: 652: /** 653: * Returns a hash code value for the object. 654: * 655: * @return The hashcode 656: */ 657: public int hashCode() { 658: int result; 659: result = this.data.hashCode(); 660: result = 29 * result + (this.workingCalendar != null 661: ? this.workingCalendar.hashCode() : 0); 662: result = 29 * result + (this.xPosition != null 663: ? this.xPosition.hashCode() : 0); 664: result = 29 * result + (this.domainIsPointsInTime ? 1 : 0); 665: return result; 666: } 667: 668: }