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