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 * TimeSeriesCollection.java 029 * ------------------------- 030 * (C) Copyright 2001-2009, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * Changes 036 * ------- 037 * 11-Oct-2001 : Version 1 (DG); 038 * 18-Oct-2001 : Added implementation of IntervalXYDataSource so that bar plots 039 * (using numerical axes) can be plotted from time series 040 * data (DG); 041 * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG); 042 * 15-Nov-2001 : Added getSeries() method. Changed name from TimeSeriesDataset 043 * to TimeSeriesCollection (DG); 044 * 07-Dec-2001 : TimeSeries --> BasicTimeSeries (DG); 045 * 01-Mar-2002 : Added a time zone offset attribute, to enable fast calculation 046 * of the time period start and end values (DG); 047 * 29-Mar-2002 : The collection now registers itself with all the time series 048 * objects as a SeriesChangeListener. Removed redundant 049 * calculateZoneOffset method (DG); 050 * 06-Jun-2002 : Added a setting to control whether the x-value supplied in the 051 * getXValue() method comes from the START, MIDDLE, or END of the 052 * time period. This is a workaround for JFreeChart, where the 053 * current date axis always labels the start of a time 054 * period (DG); 055 * 24-Jun-2002 : Removed unnecessary import (DG); 056 * 24-Aug-2002 : Implemented DomainInfo interface, and added the 057 * DomainIsPointsInTime flag (DG); 058 * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG); 059 * 16-Oct-2002 : Added remove methods (DG); 060 * 10-Jan-2003 : Changed method names in RegularTimePeriod class (DG); 061 * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented 062 * Serializable (DG); 063 * 04-Sep-2003 : Added getSeries(String) method (DG); 064 * 15-Sep-2003 : Added a removeAllSeries() method to match 065 * XYSeriesCollection (DG); 066 * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG); 067 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 068 * getYValue() (DG); 069 * 06-Oct-2004 : Updated for changed in DomainInfo interface (DG); 070 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 071 * release (DG); 072 * 28-Mar-2005 : Fixed bug in getSeries(int) method (1170825) (DG); 073 * ------------- JFREECHART 1.0.x --------------------------------------------- 074 * 13-Dec-2005 : Deprecated the 'domainIsPointsInTime' flag as it is 075 * redundant. Fixes bug 1243050 (DG); 076 * 04-May-2007 : Override getDomainOrder() to indicate that items are sorted 077 * by x-value (ascending) (DG); 078 * 08-May-2007 : Added indexOf(TimeSeries) method (DG); 079 * 18-Jan-2008 : Changed getSeries(String) to getSeries(Comparable) (DG); 080 * 081 */ 082 083 package org.jfree.data.time; 084 085 import java.io.Serializable; 086 import java.util.ArrayList; 087 import java.util.Calendar; 088 import java.util.Collections; 089 import java.util.Iterator; 090 import java.util.List; 091 import java.util.TimeZone; 092 093 import org.jfree.data.DomainInfo; 094 import org.jfree.data.DomainOrder; 095 import org.jfree.data.Range; 096 import org.jfree.data.general.DatasetChangeEvent; 097 import org.jfree.data.xy.AbstractIntervalXYDataset; 098 import org.jfree.data.xy.IntervalXYDataset; 099 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, IntervalXYDataset, DomainInfo, Serializable { 110 111 /** For serialization. */ 112 private static final long serialVersionUID = 834149929022371137L; 113 114 /** Storage for the time series. */ 115 private List data; 116 117 /** A working calendar (to recycle) */ 118 private Calendar workingCalendar; 119 120 /** 121 * The point within each time period that is used for the X value when this 122 * collection is used as an {@link org.jfree.data.xy.XYDataset}. This can 123 * be the start, middle or end of the time period. 124 */ 125 private TimePeriodAnchor xPosition; 126 127 /** 128 * A flag that indicates that the domain is 'points in time'. If this 129 * flag is true, only the x-value is used to determine the range of values 130 * in the domain, the start and end x-values are ignored. 131 * 132 * @deprecated No longer used (as of 1.0.1). 133 */ 134 private boolean domainIsPointsInTime; 135 136 /** 137 * Constructs an empty dataset, tied to the default timezone. 138 */ 139 public TimeSeriesCollection() { 140 this(null, TimeZone.getDefault()); 141 } 142 143 /** 144 * Constructs an empty dataset, tied to a specific timezone. 145 * 146 * @param zone the timezone (<code>null</code> permitted, will use 147 * <code>TimeZone.getDefault()</code> in that case). 148 */ 149 public TimeSeriesCollection(TimeZone zone) { 150 // FIXME: need a locale as well as a timezone 151 this(null, zone); 152 } 153 154 /** 155 * Constructs a dataset containing a single series (more can be added), 156 * tied to the default timezone. 157 * 158 * @param series the series (<code>null</code> permitted). 159 */ 160 public TimeSeriesCollection(TimeSeries series) { 161 this(series, TimeZone.getDefault()); 162 } 163 164 /** 165 * Constructs a dataset containing a single series (more can be added), 166 * tied to a specific timezone. 167 * 168 * @param series a series to add to the collection (<code>null</code> 169 * permitted). 170 * @param zone the timezone (<code>null</code> permitted, will use 171 * <code>TimeZone.getDefault()</code> in that case). 172 */ 173 public TimeSeriesCollection(TimeSeries series, TimeZone zone) { 174 // FIXME: need a locale as well as a timezone 175 if (zone == null) { 176 zone = TimeZone.getDefault(); 177 } 178 this.workingCalendar = Calendar.getInstance(zone); 179 this.data = new ArrayList(); 180 if (series != null) { 181 this.data.add(series); 182 series.addChangeListener(this); 183 } 184 this.xPosition = TimePeriodAnchor.START; 185 this.domainIsPointsInTime = true; 186 187 } 188 189 /** 190 * Returns a flag that controls whether the domain is treated as 'points in 191 * time'. This flag is used when determining the max and min values for 192 * the domain. If <code>true</code>, then only the x-values are considered 193 * for the max and min values. If <code>false</code>, then the start and 194 * end x-values will also be taken into consideration. 195 * 196 * @return The flag. 197 * 198 * @deprecated This flag is no longer used (as of 1.0.1). 199 */ 200 public boolean getDomainIsPointsInTime() { 201 return this.domainIsPointsInTime; 202 } 203 204 /** 205 * Sets a flag that controls whether the domain is treated as 'points in 206 * time', or time periods. 207 * 208 * @param flag the flag. 209 * 210 * @deprecated This flag is no longer used, as of 1.0.1. The 211 * <code>includeInterval</code> flag in methods such as 212 * {@link #getDomainBounds(boolean)} makes this unnecessary. 213 */ 214 public void setDomainIsPointsInTime(boolean flag) { 215 this.domainIsPointsInTime = flag; 216 notifyListeners(new DatasetChangeEvent(this, this)); 217 } 218 219 /** 220 * Returns the order of the domain values in this dataset. 221 * 222 * @return {@link DomainOrder#ASCENDING} 223 */ 224 public DomainOrder getDomainOrder() { 225 return DomainOrder.ASCENDING; 226 } 227 228 /** 229 * Returns the position within each time period that is used for the X 230 * value when the collection is used as an 231 * {@link org.jfree.data.xy.XYDataset}. 232 * 233 * @return The anchor position (never <code>null</code>). 234 */ 235 public TimePeriodAnchor getXPosition() { 236 return this.xPosition; 237 } 238 239 /** 240 * Sets the position within each time period that is used for the X values 241 * when the collection is used as an {@link XYDataset}, then sends a 242 * {@link DatasetChangeEvent} is sent to all registered listeners. 243 * 244 * @param anchor the anchor position (<code>null</code> not permitted). 245 */ 246 public void setXPosition(TimePeriodAnchor anchor) { 247 if (anchor == null) { 248 throw new IllegalArgumentException("Null 'anchor' argument."); 249 } 250 this.xPosition = anchor; 251 notifyListeners(new DatasetChangeEvent(this, this)); 252 } 253 254 /** 255 * Returns a list of all the series in the collection. 256 * 257 * @return The list (which is unmodifiable). 258 */ 259 public List getSeries() { 260 return Collections.unmodifiableList(this.data); 261 } 262 263 /** 264 * Returns the number of series in the collection. 265 * 266 * @return The series count. 267 */ 268 public int getSeriesCount() { 269 return this.data.size(); 270 } 271 272 /** 273 * Returns the index of the specified series, or -1 if that series is not 274 * present in the dataset. 275 * 276 * @param series the series (<code>null</code> not permitted). 277 * 278 * @return The series index. 279 * 280 * @since 1.0.6 281 */ 282 public int indexOf(TimeSeries series) { 283 if (series == null) { 284 throw new IllegalArgumentException("Null 'series' argument."); 285 } 286 return this.data.indexOf(series); 287 } 288 289 /** 290 * Returns a series. 291 * 292 * @param series the index of the series (zero-based). 293 * 294 * @return The series. 295 */ 296 public TimeSeries getSeries(int series) { 297 if ((series < 0) || (series >= getSeriesCount())) { 298 throw new IllegalArgumentException( 299 "The 'series' argument is out of bounds (" + series + ")."); 300 } 301 return (TimeSeries) this.data.get(series); 302 } 303 304 /** 305 * Returns the series with the specified key, or <code>null</code> if 306 * there is no such series. 307 * 308 * @param key the series key (<code>null</code> permitted). 309 * 310 * @return The series with the given key. 311 */ 312 public TimeSeries getSeries(Comparable key) { 313 TimeSeries result = null; 314 Iterator iterator = this.data.iterator(); 315 while (iterator.hasNext()) { 316 TimeSeries series = (TimeSeries) iterator.next(); 317 Comparable k = series.getKey(); 318 if (k != null && k.equals(key)) { 319 result = series; 320 } 321 } 322 return result; 323 } 324 325 /** 326 * Returns the key for a series. 327 * 328 * @param series the index of the series (zero-based). 329 * 330 * @return The key for a series. 331 */ 332 public Comparable getSeriesKey(int series) { 333 // check arguments...delegated 334 // fetch the series name... 335 return getSeries(series).getKey(); 336 } 337 338 /** 339 * Adds a series to the collection and sends a {@link DatasetChangeEvent} to 340 * all registered listeners. 341 * 342 * @param series the series (<code>null</code> not permitted). 343 */ 344 public void addSeries(TimeSeries series) { 345 if (series == null) { 346 throw new IllegalArgumentException("Null 'series' argument."); 347 } 348 this.data.add(series); 349 series.addChangeListener(this); 350 fireDatasetChanged(); 351 } 352 353 /** 354 * Removes the specified series from the collection and sends a 355 * {@link DatasetChangeEvent} to all registered listeners. 356 * 357 * @param series the series (<code>null</code> not permitted). 358 */ 359 public void removeSeries(TimeSeries series) { 360 if (series == null) { 361 throw new IllegalArgumentException("Null 'series' argument."); 362 } 363 this.data.remove(series); 364 series.removeChangeListener(this); 365 fireDatasetChanged(); 366 } 367 368 /** 369 * Removes a series from the collection. 370 * 371 * @param index the series index (zero-based). 372 */ 373 public void removeSeries(int index) { 374 TimeSeries series = getSeries(index); 375 if (series != null) { 376 removeSeries(series); 377 } 378 } 379 380 /** 381 * Removes all the series from the collection and sends a 382 * {@link DatasetChangeEvent} to all registered listeners. 383 */ 384 public void removeAllSeries() { 385 386 // deregister the collection as a change listener to each series in the 387 // collection 388 for (int i = 0; i < this.data.size(); i++) { 389 TimeSeries series = (TimeSeries) this.data.get(i); 390 series.removeChangeListener(this); 391 } 392 393 // remove all the series from the collection and notify listeners. 394 this.data.clear(); 395 fireDatasetChanged(); 396 397 } 398 399 /** 400 * Returns the number of items in the specified series. This method is 401 * provided for convenience. 402 * 403 * @param series the series index (zero-based). 404 * 405 * @return The item count. 406 */ 407 public int getItemCount(int series) { 408 return getSeries(series).getItemCount(); 409 } 410 411 /** 412 * Returns the x-value (as a double primitive) for an item within a series. 413 * 414 * @param series the series (zero-based index). 415 * @param item the item (zero-based index). 416 * 417 * @return The x-value. 418 */ 419 public double getXValue(int series, int item) { 420 TimeSeries s = (TimeSeries) this.data.get(series); 421 TimeSeriesDataItem i = s.getDataItem(item); 422 RegularTimePeriod period = i.getPeriod(); 423 return getX(period); 424 } 425 426 /** 427 * Returns the x-value for the specified series and item. 428 * 429 * @param series the series (zero-based index). 430 * @param item the item (zero-based index). 431 * 432 * @return The value. 433 */ 434 public Number getX(int series, int item) { 435 TimeSeries ts = (TimeSeries) this.data.get(series); 436 TimeSeriesDataItem dp = ts.getDataItem(item); 437 RegularTimePeriod period = dp.getPeriod(); 438 return new Long(getX(period)); 439 } 440 441 /** 442 * Returns the x-value for a time period. 443 * 444 * @param period the time period (<code>null</code> not permitted). 445 * 446 * @return The x-value. 447 */ 448 protected synchronized long getX(RegularTimePeriod period) { 449 long result = 0L; 450 if (this.xPosition == TimePeriodAnchor.START) { 451 result = period.getFirstMillisecond(this.workingCalendar); 452 } 453 else if (this.xPosition == TimePeriodAnchor.MIDDLE) { 454 result = period.getMiddleMillisecond(this.workingCalendar); 455 } 456 else if (this.xPosition == TimePeriodAnchor.END) { 457 result = period.getLastMillisecond(this.workingCalendar); 458 } 459 return result; 460 } 461 462 /** 463 * Returns the starting X value for the specified series and item. 464 * 465 * @param series the series (zero-based index). 466 * @param item the item (zero-based index). 467 * 468 * @return The value. 469 */ 470 public synchronized Number getStartX(int series, int item) { 471 TimeSeries ts = (TimeSeries) this.data.get(series); 472 TimeSeriesDataItem dp = ts.getDataItem(item); 473 return new Long(dp.getPeriod().getFirstMillisecond( 474 this.workingCalendar)); 475 } 476 477 /** 478 * Returns the ending X value for the specified series and item. 479 * 480 * @param series The series (zero-based index). 481 * @param item The item (zero-based index). 482 * 483 * @return The value. 484 */ 485 public synchronized Number getEndX(int series, int item) { 486 TimeSeries ts = (TimeSeries) this.data.get(series); 487 TimeSeriesDataItem dp = ts.getDataItem(item); 488 return new Long(dp.getPeriod().getLastMillisecond( 489 this.workingCalendar)); 490 } 491 492 /** 493 * Returns the y-value for the specified series and item. 494 * 495 * @param series the series (zero-based index). 496 * @param item the item (zero-based index). 497 * 498 * @return The value (possibly <code>null</code>). 499 */ 500 public Number getY(int series, int item) { 501 TimeSeries ts = (TimeSeries) this.data.get(series); 502 TimeSeriesDataItem dp = ts.getDataItem(item); 503 return dp.getValue(); 504 } 505 506 /** 507 * Returns the starting Y value for the specified series and item. 508 * 509 * @param series the series (zero-based index). 510 * @param item the item (zero-based index). 511 * 512 * @return The value (possibly <code>null</code>). 513 */ 514 public Number getStartY(int series, int item) { 515 return getY(series, item); 516 } 517 518 /** 519 * Returns the ending Y value for the specified series and item. 520 * 521 * @param series te series (zero-based index). 522 * @param item the item (zero-based index). 523 * 524 * @return The value (possibly <code>null</code>). 525 */ 526 public Number getEndY(int series, int item) { 527 return getY(series, item); 528 } 529 530 531 /** 532 * Returns the indices of the two data items surrounding a particular 533 * millisecond value. 534 * 535 * @param series the series index. 536 * @param milliseconds the time. 537 * 538 * @return An array containing the (two) indices of the items surrounding 539 * the time. 540 */ 541 public int[] getSurroundingItems(int series, long milliseconds) { 542 int[] result = new int[] {-1, -1}; 543 TimeSeries timeSeries = getSeries(series); 544 for (int i = 0; i < timeSeries.getItemCount(); i++) { 545 Number x = getX(series, i); 546 long m = x.longValue(); 547 if (m <= milliseconds) { 548 result[0] = i; 549 } 550 if (m >= milliseconds) { 551 result[1] = i; 552 break; 553 } 554 } 555 return result; 556 } 557 558 /** 559 * Returns the minimum x-value in the dataset. 560 * 561 * @param includeInterval a flag that determines whether or not the 562 * x-interval is taken into account. 563 * 564 * @return The minimum value. 565 */ 566 public double getDomainLowerBound(boolean includeInterval) { 567 double result = Double.NaN; 568 Range r = getDomainBounds(includeInterval); 569 if (r != null) { 570 result = r.getLowerBound(); 571 } 572 return result; 573 } 574 575 /** 576 * Returns the maximum x-value in the dataset. 577 * 578 * @param includeInterval a flag that determines whether or not the 579 * x-interval is taken into account. 580 * 581 * @return The maximum value. 582 */ 583 public double getDomainUpperBound(boolean includeInterval) { 584 double result = Double.NaN; 585 Range r = getDomainBounds(includeInterval); 586 if (r != null) { 587 result = r.getUpperBound(); 588 } 589 return result; 590 } 591 592 /** 593 * Returns the range of the values in this dataset's domain. 594 * 595 * @param includeInterval a flag that determines whether or not the 596 * x-interval is taken into account. 597 * 598 * @return The range. 599 */ 600 public Range getDomainBounds(boolean includeInterval) { 601 Range result = null; 602 Iterator iterator = this.data.iterator(); 603 while (iterator.hasNext()) { 604 TimeSeries series = (TimeSeries) iterator.next(); 605 int count = series.getItemCount(); 606 if (count > 0) { 607 RegularTimePeriod start = series.getTimePeriod(0); 608 RegularTimePeriod end = series.getTimePeriod(count - 1); 609 Range temp; 610 if (!includeInterval) { 611 temp = new Range(getX(start), getX(end)); 612 } 613 else { 614 temp = new Range( 615 start.getFirstMillisecond(this.workingCalendar), 616 end.getLastMillisecond(this.workingCalendar)); 617 } 618 result = Range.combine(result, temp); 619 } 620 } 621 return result; 622 } 623 624 /** 625 * Tests this time series collection for equality with another object. 626 * 627 * @param obj the other object. 628 * 629 * @return A boolean. 630 */ 631 public boolean equals(Object obj) { 632 if (obj == this) { 633 return true; 634 } 635 if (!(obj instanceof TimeSeriesCollection)) { 636 return false; 637 } 638 TimeSeriesCollection that = (TimeSeriesCollection) obj; 639 if (this.xPosition != that.xPosition) { 640 return false; 641 } 642 if (this.domainIsPointsInTime != that.domainIsPointsInTime) { 643 return false; 644 } 645 if (!ObjectUtilities.equal(this.data, that.data)) { 646 return false; 647 } 648 return true; 649 } 650 651 /** 652 * Returns a hash code value for the object. 653 * 654 * @return The hashcode 655 */ 656 public int hashCode() { 657 int result; 658 result = this.data.hashCode(); 659 result = 29 * result + (this.workingCalendar != null 660 ? this.workingCalendar.hashCode() : 0); 661 result = 29 * result + (this.xPosition != null 662 ? this.xPosition.hashCode() : 0); 663 result = 29 * result + (this.domainIsPointsInTime ? 1 : 0); 664 return result; 665 } 666 667 }