Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2008, 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-2008, 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: * 18-Jan-2008 : Changed getSeries(String) to getSeries(Comparable) (DG); 80: * 81: */ 82: 83: package org.jfree.data.time; 84: 85: import java.io.Serializable; 86: import java.util.ArrayList; 87: import java.util.Calendar; 88: import java.util.Collections; 89: import java.util.Iterator; 90: import java.util.List; 91: import java.util.TimeZone; 92: 93: import org.jfree.data.DomainInfo; 94: import org.jfree.data.DomainOrder; 95: import org.jfree.data.Range; 96: import org.jfree.data.general.DatasetChangeEvent; 97: import org.jfree.data.xy.AbstractIntervalXYDataset; 98: import org.jfree.data.xy.IntervalXYDataset; 99: import org.jfree.data.xy.XYDataset; 100: import org.jfree.util.ObjectUtilities; 101: 102: /** 103: * A collection of time series objects. This class implements the 104: * {@link org.jfree.data.xy.XYDataset} interface, as well as the extended 105: * {@link IntervalXYDataset} interface. This makes it a convenient dataset for 106: * use with the {@link org.jfree.chart.plot.XYPlot} class. 107: */ 108: public class TimeSeriesCollection extends AbstractIntervalXYDataset 109: implements XYDataset, 110: IntervalXYDataset, 111: DomainInfo, 112: Serializable { 113: 114: /** For serialization. */ 115: private static final long serialVersionUID = 834149929022371137L; 116: 117: /** Storage for the time series. */ 118: private List data; 119: 120: /** A working calendar (to recycle) */ 121: private Calendar workingCalendar; 122: 123: /** 124: * The point within each time period that is used for the X value when this 125: * collection is used as an {@link org.jfree.data.xy.XYDataset}. This can 126: * be the start, middle or end of the time period. 127: */ 128: private TimePeriodAnchor xPosition; 129: 130: /** 131: * A flag that indicates that the domain is 'points in time'. If this 132: * flag is true, only the x-value is used to determine the range of values 133: * in the domain, the start and end x-values are ignored. 134: * 135: * @deprecated No longer used (as of 1.0.1). 136: */ 137: private boolean domainIsPointsInTime; 138: 139: /** 140: * Constructs an empty dataset, tied to the default timezone. 141: */ 142: public TimeSeriesCollection() { 143: this(null, TimeZone.getDefault()); 144: } 145: 146: /** 147: * Constructs an empty dataset, tied to a specific timezone. 148: * 149: * @param zone the timezone (<code>null</code> permitted, will use 150: * <code>TimeZone.getDefault()</code> in that case). 151: */ 152: public TimeSeriesCollection(TimeZone zone) { 153: this(null, zone); 154: } 155: 156: /** 157: * Constructs a dataset containing a single series (more can be added), 158: * tied to the default timezone. 159: * 160: * @param series the series (<code>null</code> permitted). 161: */ 162: public TimeSeriesCollection(TimeSeries series) { 163: this(series, TimeZone.getDefault()); 164: } 165: 166: /** 167: * Constructs a dataset containing a single series (more can be added), 168: * tied to a specific timezone. 169: * 170: * @param series a series to add to the collection (<code>null</code> 171: * permitted). 172: * @param zone the timezone (<code>null</code> permitted, will use 173: * <code>TimeZone.getDefault()</code> in that case). 174: */ 175: public TimeSeriesCollection(TimeSeries series, TimeZone zone) { 176: 177: if (zone == null) { 178: zone = TimeZone.getDefault(); 179: } 180: this.workingCalendar = Calendar.getInstance(zone); 181: this.data = new ArrayList(); 182: if (series != null) { 183: this.data.add(series); 184: series.addChangeListener(this); 185: } 186: this.xPosition = TimePeriodAnchor.START; 187: this.domainIsPointsInTime = true; 188: 189: } 190: 191: /** 192: * Returns a flag that controls whether the domain is treated as 'points in 193: * time'. This flag is used when determining the max and min values for 194: * the domain. If <code>true</code>, then only the x-values are considered 195: * for the max and min values. If <code>false</code>, then the start and 196: * end x-values will also be taken into consideration. 197: * 198: * @return The flag. 199: * 200: * @deprecated This flag is no longer used (as of 1.0.1). 201: */ 202: public boolean getDomainIsPointsInTime() { 203: return this.domainIsPointsInTime; 204: } 205: 206: /** 207: * Sets a flag that controls whether the domain is treated as 'points in 208: * time', or time periods. 209: * 210: * @param flag the flag. 211: * 212: * @deprecated This flag is no longer used, as of 1.0.1. The 213: * <code>includeInterval</code> flag in methods such as 214: * {@link #getDomainBounds(boolean)} makes this unnecessary. 215: */ 216: public void setDomainIsPointsInTime(boolean flag) { 217: this.domainIsPointsInTime = flag; 218: notifyListeners(new DatasetChangeEvent(this, this)); 219: } 220: 221: /** 222: * Returns the order of the domain values in this dataset. 223: * 224: * @return {@link DomainOrder#ASCENDING} 225: */ 226: public DomainOrder getDomainOrder() { 227: return DomainOrder.ASCENDING; 228: } 229: 230: /** 231: * Returns the position within each time period that is used for the X 232: * value when the collection is used as an 233: * {@link org.jfree.data.xy.XYDataset}. 234: * 235: * @return The anchor position (never <code>null</code>). 236: */ 237: public TimePeriodAnchor getXPosition() { 238: return this.xPosition; 239: } 240: 241: /** 242: * Sets the position within each time period that is used for the X values 243: * when the collection is used as an {@link XYDataset}, then sends a 244: * {@link DatasetChangeEvent} is sent to all registered listeners. 245: * 246: * @param anchor the anchor position (<code>null</code> not permitted). 247: */ 248: public void setXPosition(TimePeriodAnchor anchor) { 249: if (anchor == null) { 250: throw new IllegalArgumentException("Null 'anchor' argument."); 251: } 252: this.xPosition = anchor; 253: notifyListeners(new DatasetChangeEvent(this, this)); 254: } 255: 256: /** 257: * Returns a list of all the series in the collection. 258: * 259: * @return The list (which is unmodifiable). 260: */ 261: public List getSeries() { 262: return Collections.unmodifiableList(this.data); 263: } 264: 265: /** 266: * Returns the number of series in the collection. 267: * 268: * @return The series count. 269: */ 270: public int getSeriesCount() { 271: return this.data.size(); 272: } 273: 274: /** 275: * Returns the index of the specified series, or -1 if that series is not 276: * present in the dataset. 277: * 278: * @param series the series (<code>null</code> not permitted). 279: * 280: * @return The series index. 281: * 282: * @since 1.0.6 283: */ 284: public int indexOf(TimeSeries series) { 285: if (series == null) { 286: throw new IllegalArgumentException("Null 'series' argument."); 287: } 288: return this.data.indexOf(series); 289: } 290: 291: /** 292: * Returns a series. 293: * 294: * @param series the index of the series (zero-based). 295: * 296: * @return The series. 297: */ 298: public TimeSeries getSeries(int series) { 299: if ((series < 0) || (series >= getSeriesCount())) { 300: throw new IllegalArgumentException( 301: "The 'series' argument is out of bounds (" + series + ")."); 302: } 303: return (TimeSeries) this.data.get(series); 304: } 305: 306: /** 307: * Returns the series with the specified key, or <code>null</code> if 308: * there is no such series. 309: * 310: * @param key the series key (<code>null</code> permitted). 311: * 312: * @return The series with the given key. 313: */ 314: public TimeSeries getSeries(Comparable key) { 315: TimeSeries result = null; 316: Iterator iterator = this.data.iterator(); 317: while (iterator.hasNext()) { 318: TimeSeries series = (TimeSeries) iterator.next(); 319: Comparable k = series.getKey(); 320: if (k != null && k.equals(key)) { 321: result = series; 322: } 323: } 324: return result; 325: } 326: 327: /** 328: * Returns the key for a series. 329: * 330: * @param series the index of the series (zero-based). 331: * 332: * @return The key for a series. 333: */ 334: public Comparable getSeriesKey(int series) { 335: // check arguments...delegated 336: // fetch the series name... 337: return getSeries(series).getKey(); 338: } 339: 340: /** 341: * Adds a series to the collection and sends a {@link DatasetChangeEvent} to 342: * all registered listeners. 343: * 344: * @param series the series (<code>null</code> not permitted). 345: */ 346: public void addSeries(TimeSeries series) { 347: if (series == null) { 348: throw new IllegalArgumentException("Null 'series' argument."); 349: } 350: this.data.add(series); 351: series.addChangeListener(this); 352: fireDatasetChanged(); 353: } 354: 355: /** 356: * Removes the specified series from the collection and sends a 357: * {@link DatasetChangeEvent} to all registered listeners. 358: * 359: * @param series the series (<code>null</code> not permitted). 360: */ 361: public void removeSeries(TimeSeries series) { 362: if (series == null) { 363: throw new IllegalArgumentException("Null 'series' argument."); 364: } 365: this.data.remove(series); 366: series.removeChangeListener(this); 367: fireDatasetChanged(); 368: } 369: 370: /** 371: * Removes a series from the collection. 372: * 373: * @param index the series index (zero-based). 374: */ 375: public void removeSeries(int index) { 376: TimeSeries series = getSeries(index); 377: if (series != null) { 378: removeSeries(series); 379: } 380: } 381: 382: /** 383: * Removes all the series from the collection and sends a 384: * {@link DatasetChangeEvent} to all registered listeners. 385: */ 386: public void removeAllSeries() { 387: 388: // deregister the collection as a change listener to each series in the 389: // collection 390: for (int i = 0; i < this.data.size(); i++) { 391: TimeSeries series = (TimeSeries) this.data.get(i); 392: series.removeChangeListener(this); 393: } 394: 395: // remove all the series from the collection and notify listeners. 396: this.data.clear(); 397: fireDatasetChanged(); 398: 399: } 400: 401: /** 402: * Returns the number of items in the specified series. This method is 403: * provided for convenience. 404: * 405: * @param series the series index (zero-based). 406: * 407: * @return The item count. 408: */ 409: public int getItemCount(int series) { 410: return getSeries(series).getItemCount(); 411: } 412: 413: /** 414: * Returns the x-value (as a double primitive) for an item within a series. 415: * 416: * @param series the series (zero-based index). 417: * @param item the item (zero-based index). 418: * 419: * @return The x-value. 420: */ 421: public double getXValue(int series, int item) { 422: TimeSeries s = (TimeSeries) this.data.get(series); 423: TimeSeriesDataItem i = s.getDataItem(item); 424: RegularTimePeriod period = i.getPeriod(); 425: return getX(period); 426: } 427: 428: /** 429: * Returns the x-value for the specified series and item. 430: * 431: * @param series the series (zero-based index). 432: * @param item the item (zero-based index). 433: * 434: * @return The value. 435: */ 436: public Number getX(int series, int item) { 437: TimeSeries ts = (TimeSeries) this.data.get(series); 438: TimeSeriesDataItem dp = ts.getDataItem(item); 439: RegularTimePeriod period = dp.getPeriod(); 440: return new Long(getX(period)); 441: } 442: 443: /** 444: * Returns the x-value for a time period. 445: * 446: * @param period the time period (<code>null</code> not permitted). 447: * 448: * @return The x-value. 449: */ 450: protected synchronized long getX(RegularTimePeriod period) { 451: long result = 0L; 452: if (this.xPosition == TimePeriodAnchor.START) { 453: result = period.getFirstMillisecond(this.workingCalendar); 454: } 455: else if (this.xPosition == TimePeriodAnchor.MIDDLE) { 456: result = period.getMiddleMillisecond(this.workingCalendar); 457: } 458: else if (this.xPosition == TimePeriodAnchor.END) { 459: result = period.getLastMillisecond(this.workingCalendar); 460: } 461: return result; 462: } 463: 464: /** 465: * Returns the starting X value for the specified series and item. 466: * 467: * @param series the series (zero-based index). 468: * @param item the item (zero-based index). 469: * 470: * @return The value. 471: */ 472: public synchronized Number getStartX(int series, int item) { 473: TimeSeries ts = (TimeSeries) this.data.get(series); 474: TimeSeriesDataItem dp = ts.getDataItem(item); 475: return new Long(dp.getPeriod().getFirstMillisecond( 476: this.workingCalendar)); 477: } 478: 479: /** 480: * Returns the ending X value for the specified series and item. 481: * 482: * @param series The series (zero-based index). 483: * @param item The item (zero-based index). 484: * 485: * @return The value. 486: */ 487: public synchronized Number getEndX(int series, int item) { 488: TimeSeries ts = (TimeSeries) this.data.get(series); 489: TimeSeriesDataItem dp = ts.getDataItem(item); 490: return new Long(dp.getPeriod().getLastMillisecond( 491: this.workingCalendar)); 492: } 493: 494: /** 495: * Returns the y-value for the specified series and item. 496: * 497: * @param series the series (zero-based index). 498: * @param item the item (zero-based index). 499: * 500: * @return The value (possibly <code>null</code>). 501: */ 502: public Number getY(int series, int item) { 503: TimeSeries ts = (TimeSeries) this.data.get(series); 504: TimeSeriesDataItem dp = ts.getDataItem(item); 505: return dp.getValue(); 506: } 507: 508: /** 509: * Returns the starting Y value for the specified series and item. 510: * 511: * @param series the series (zero-based index). 512: * @param item the item (zero-based index). 513: * 514: * @return The value (possibly <code>null</code>). 515: */ 516: public Number getStartY(int series, int item) { 517: return getY(series, item); 518: } 519: 520: /** 521: * Returns the ending Y value for the specified series and item. 522: * 523: * @param series te series (zero-based index). 524: * @param item the item (zero-based index). 525: * 526: * @return The value (possibly <code>null</code>). 527: */ 528: public Number getEndY(int series, int item) { 529: return getY(series, item); 530: } 531: 532: 533: /** 534: * Returns the indices of the two data items surrounding a particular 535: * millisecond value. 536: * 537: * @param series the series index. 538: * @param milliseconds the time. 539: * 540: * @return An array containing the (two) indices of the items surrounding 541: * the time. 542: */ 543: public int[] getSurroundingItems(int series, long milliseconds) { 544: int[] result = new int[] {-1, -1}; 545: TimeSeries timeSeries = getSeries(series); 546: for (int i = 0; i < timeSeries.getItemCount(); i++) { 547: Number x = getX(series, i); 548: long m = x.longValue(); 549: if (m <= milliseconds) { 550: result[0] = i; 551: } 552: if (m >= milliseconds) { 553: result[1] = i; 554: break; 555: } 556: } 557: return result; 558: } 559: 560: /** 561: * Returns the minimum x-value in the dataset. 562: * 563: * @param includeInterval a flag that determines whether or not the 564: * x-interval is taken into account. 565: * 566: * @return The minimum value. 567: */ 568: public double getDomainLowerBound(boolean includeInterval) { 569: double result = Double.NaN; 570: Range r = getDomainBounds(includeInterval); 571: if (r != null) { 572: result = r.getLowerBound(); 573: } 574: return result; 575: } 576: 577: /** 578: * Returns the maximum x-value in the dataset. 579: * 580: * @param includeInterval a flag that determines whether or not the 581: * x-interval is taken into account. 582: * 583: * @return The maximum value. 584: */ 585: public double getDomainUpperBound(boolean includeInterval) { 586: double result = Double.NaN; 587: Range r = getDomainBounds(includeInterval); 588: if (r != null) { 589: result = r.getUpperBound(); 590: } 591: return result; 592: } 593: 594: /** 595: * Returns the range of the values in this dataset's domain. 596: * 597: * @param includeInterval a flag that determines whether or not the 598: * x-interval is taken into account. 599: * 600: * @return The range. 601: */ 602: public Range getDomainBounds(boolean includeInterval) { 603: Range result = null; 604: Iterator iterator = this.data.iterator(); 605: while (iterator.hasNext()) { 606: TimeSeries series = (TimeSeries) iterator.next(); 607: int count = series.getItemCount(); 608: if (count > 0) { 609: RegularTimePeriod start = series.getTimePeriod(0); 610: RegularTimePeriod end = series.getTimePeriod(count - 1); 611: Range temp; 612: if (!includeInterval) { 613: temp = new Range(getX(start), getX(end)); 614: } 615: else { 616: temp = new Range( 617: start.getFirstMillisecond(this.workingCalendar), 618: end.getLastMillisecond(this.workingCalendar)); 619: } 620: result = Range.combine(result, temp); 621: } 622: } 623: return result; 624: } 625: 626: /** 627: * Tests this time series collection for equality with another object. 628: * 629: * @param obj the other object. 630: * 631: * @return A boolean. 632: */ 633: public boolean equals(Object obj) { 634: if (obj == this) { 635: return true; 636: } 637: if (!(obj instanceof TimeSeriesCollection)) { 638: return false; 639: } 640: TimeSeriesCollection that = (TimeSeriesCollection) obj; 641: if (this.xPosition != that.xPosition) { 642: return false; 643: } 644: if (this.domainIsPointsInTime != that.domainIsPointsInTime) { 645: return false; 646: } 647: if (!ObjectUtilities.equal(this.data, that.data)) { 648: return false; 649: } 650: return true; 651: } 652: 653: /** 654: * Returns a hash code value for the object. 655: * 656: * @return The hashcode 657: */ 658: public int hashCode() { 659: int result; 660: result = this.data.hashCode(); 661: result = 29 * result + (this.workingCalendar != null 662: ? this.workingCalendar.hashCode() : 0); 663: result = 29 * result + (this.xPosition != null 664: ? this.xPosition.hashCode() : 0); 665: result = 29 * result + (this.domainIsPointsInTime ? 1 : 0); 666: return result; 667: } 668: 669: }