001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * --------------- 028 * TimeSeries.java 029 * --------------- 030 * (C) Copyright 2001-2014, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Bryan Scott; 034 * Nick Guenther; 035 * 036 * Changes 037 * ------- 038 * 11-Oct-2001 : Version 1 (DG); 039 * 14-Nov-2001 : Added listener mechanism (DG); 040 * 15-Nov-2001 : Updated argument checking and exceptions in add() method (DG); 041 * 29-Nov-2001 : Added properties to describe the domain and range (DG); 042 * 07-Dec-2001 : Renamed TimeSeries --> BasicTimeSeries (DG); 043 * 01-Mar-2002 : Updated import statements (DG); 044 * 28-Mar-2002 : Added a method add(TimePeriod, double) (DG); 045 * 27-Aug-2002 : Changed return type of delete method to void (DG); 046 * 04-Oct-2002 : Added itemCount and historyCount attributes, fixed errors 047 * reported by Checkstyle (DG); 048 * 29-Oct-2002 : Added series change notification to addOrUpdate() method (DG); 049 * 28-Jan-2003 : Changed name back to TimeSeries (DG); 050 * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented 051 * Serializable (DG); 052 * 01-May-2003 : Updated equals() method (see bug report 727575) (DG); 053 * 14-Aug-2003 : Added ageHistoryCountItems method (copied existing code for 054 * contents) made a method and added to addOrUpdate. Made a 055 * public method to enable ageing against a specified time 056 * (eg now) as opposed to lastest time in series (BS); 057 * 15-Oct-2003 : Added fix for setItemCount method - see bug report 804425. 058 * Modified exception message in add() method to be more 059 * informative (DG); 060 * 13-Apr-2004 : Added clear() method (DG); 061 * 21-May-2004 : Added an extra addOrUpdate() method (DG); 062 * 15-Jun-2004 : Fixed NullPointerException in equals() method (DG); 063 * 29-Nov-2004 : Fixed bug 1075255 (DG); 064 * 17-Nov-2005 : Renamed historyCount --> maximumItemAge (DG); 065 * 28-Nov-2005 : Changed maximumItemAge from int to long (DG); 066 * 01-Dec-2005 : New add methods accept notify flag (DG); 067 * ------------- JFREECHART 1.0.x --------------------------------------------- 068 * 24-May-2006 : Improved error handling in createCopy() methods (DG); 069 * 01-Sep-2006 : Fixed bugs in removeAgedItems() methods - see bug report 070 * 1550045 (DG); 071 * 22-Mar-2007 : Simplified getDataItem(RegularTimePeriod) - see patch 1685500 072 * by Nick Guenther (DG); 073 * 31-Oct-2007 : Implemented faster hashCode() (DG); 074 * 21-Nov-2007 : Fixed clone() method (bug 1832432) (DG); 075 * 10-Jan-2008 : Fixed createCopy(RegularTimePeriod, RegularTimePeriod) (bug 076 * 1864222) (DG); 077 * 13-Jan-2009 : Fixed constructors so that timePeriodClass doesn't need to 078 * be specified in advance (DG); 079 * 26-May-2009 : Added cache for minY and maxY values (DG); 080 * 09-Jun-2009 : Ensure that TimeSeriesDataItem objects used in underlying 081 * storage are cloned to keep series isolated from external 082 * changes (DG); 083 * 10-Jun-2009 : Added addOrUpdate(TimeSeriesDataItem) method (DG); 084 * 31-Aug-2009 : Clear minY and maxY cache values in createCopy (DG); 085 * 03-Dec-2011 : Fixed bug 3446965 which affects the y-range calculation for 086 * the series (DG); 087 * 02-Jul-2013 : Use ParamChecks (DG); 088 * 089 */ 090 091package org.jfree.data.time; 092 093import java.io.Serializable; 094import java.lang.reflect.InvocationTargetException; 095import java.lang.reflect.Method; 096import java.util.Calendar; 097import java.util.Collection; 098import java.util.Collections; 099import java.util.Date; 100import java.util.Iterator; 101import java.util.List; 102import java.util.TimeZone; 103 104import org.jfree.chart.util.ParamChecks; 105import org.jfree.data.Range; 106import org.jfree.data.general.Series; 107import org.jfree.data.general.SeriesChangeEvent; 108import org.jfree.data.general.SeriesException; 109import org.jfree.util.ObjectUtilities; 110 111/** 112 * Represents a sequence of zero or more data items in the form (period, value) 113 * where 'period' is some instance of a subclass of {@link RegularTimePeriod}. 114 * The time series will ensure that (a) all data items have the same type of 115 * period (for example, {@link Day}) and (b) that each period appears at 116 * most one time in the series. 117 */ 118public class TimeSeries extends Series implements Cloneable, Serializable { 119 120 /** For serialization. */ 121 private static final long serialVersionUID = -5032960206869675528L; 122 123 /** Default value for the domain description. */ 124 protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time"; 125 126 /** Default value for the range description. */ 127 protected static final String DEFAULT_RANGE_DESCRIPTION = "Value"; 128 129 /** A description of the domain. */ 130 private String domain; 131 132 /** A description of the range. */ 133 private String range; 134 135 /** The type of period for the data. */ 136 protected Class timePeriodClass; 137 138 /** The list of data items in the series. */ 139 protected List data; 140 141 /** The maximum number of items for the series. */ 142 private int maximumItemCount; 143 144 /** 145 * The maximum age of items for the series, specified as a number of 146 * time periods. 147 */ 148 private long maximumItemAge; 149 150 /** 151 * The minimum y-value in the series. 152 * 153 * @since 1.0.14 154 */ 155 private double minY; 156 157 /** 158 * The maximum y-value in the series. 159 * 160 * @since 1.0.14 161 */ 162 private double maxY; 163 164 /** 165 * Creates a new (empty) time series. By default, a daily time series is 166 * created. Use one of the other constructors if you require a different 167 * time period. 168 * 169 * @param name the series name (<code>null</code> not permitted). 170 */ 171 public TimeSeries(Comparable name) { 172 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION); 173 } 174 175 /** 176 * Creates a new time series that contains no data. 177 * <P> 178 * Descriptions can be specified for the domain and range. One situation 179 * where this is helpful is when generating a chart for the time series - 180 * axis labels can be taken from the domain and range description. 181 * 182 * @param name the name of the series (<code>null</code> not permitted). 183 * @param domain the domain description (<code>null</code> permitted). 184 * @param range the range description (<code>null</code> permitted). 185 * 186 * @since 1.0.13 187 */ 188 public TimeSeries(Comparable name, String domain, String range) { 189 super(name); 190 this.domain = domain; 191 this.range = range; 192 this.timePeriodClass = null; 193 this.data = new java.util.ArrayList(); 194 this.maximumItemCount = Integer.MAX_VALUE; 195 this.maximumItemAge = Long.MAX_VALUE; 196 this.minY = Double.NaN; 197 this.maxY = Double.NaN; 198 } 199 200 /** 201 * Returns the domain description. 202 * 203 * @return The domain description (possibly <code>null</code>). 204 * 205 * @see #setDomainDescription(String) 206 */ 207 public String getDomainDescription() { 208 return this.domain; 209 } 210 211 /** 212 * Sets the domain description and sends a <code>PropertyChangeEvent</code> 213 * (with the property name <code>Domain</code>) to all registered 214 * property change listeners. 215 * 216 * @param description the description (<code>null</code> permitted). 217 * 218 * @see #getDomainDescription() 219 */ 220 public void setDomainDescription(String description) { 221 String old = this.domain; 222 this.domain = description; 223 firePropertyChange("Domain", old, description); 224 } 225 226 /** 227 * Returns the range description. 228 * 229 * @return The range description (possibly <code>null</code>). 230 * 231 * @see #setRangeDescription(String) 232 */ 233 public String getRangeDescription() { 234 return this.range; 235 } 236 237 /** 238 * Sets the range description and sends a <code>PropertyChangeEvent</code> 239 * (with the property name <code>Range</code>) to all registered listeners. 240 * 241 * @param description the description (<code>null</code> permitted). 242 * 243 * @see #getRangeDescription() 244 */ 245 public void setRangeDescription(String description) { 246 String old = this.range; 247 this.range = description; 248 firePropertyChange("Range", old, description); 249 } 250 251 /** 252 * Returns the number of items in the series. 253 * 254 * @return The item count. 255 */ 256 @Override 257 public int getItemCount() { 258 return this.data.size(); 259 } 260 261 /** 262 * Returns the list of data items for the series (the list contains 263 * {@link TimeSeriesDataItem} objects and is unmodifiable). 264 * 265 * @return The list of data items. 266 */ 267 public List getItems() { 268 // FIXME: perhaps we should clone the data list 269 return Collections.unmodifiableList(this.data); 270 } 271 272 /** 273 * Returns the maximum number of items that will be retained in the series. 274 * The default value is <code>Integer.MAX_VALUE</code>. 275 * 276 * @return The maximum item count. 277 * 278 * @see #setMaximumItemCount(int) 279 */ 280 public int getMaximumItemCount() { 281 return this.maximumItemCount; 282 } 283 284 /** 285 * Sets the maximum number of items that will be retained in the series. 286 * If you add a new item to the series such that the number of items will 287 * exceed the maximum item count, then the FIRST element in the series is 288 * automatically removed, ensuring that the maximum item count is not 289 * exceeded. 290 * 291 * @param maximum the maximum (requires >= 0). 292 * 293 * @see #getMaximumItemCount() 294 */ 295 public void setMaximumItemCount(int maximum) { 296 if (maximum < 0) { 297 throw new IllegalArgumentException("Negative 'maximum' argument."); 298 } 299 this.maximumItemCount = maximum; 300 int count = this.data.size(); 301 if (count > maximum) { 302 delete(0, count - maximum - 1); 303 } 304 } 305 306 /** 307 * Returns the maximum item age (in time periods) for the series. 308 * 309 * @return The maximum item age. 310 * 311 * @see #setMaximumItemAge(long) 312 */ 313 public long getMaximumItemAge() { 314 return this.maximumItemAge; 315 } 316 317 /** 318 * Sets the number of time units in the 'history' for the series. This 319 * provides one mechanism for automatically dropping old data from the 320 * time series. For example, if a series contains daily data, you might set 321 * the history count to 30. Then, when you add a new data item, all data 322 * items more than 30 days older than the latest value are automatically 323 * dropped from the series. 324 * 325 * @param periods the number of time periods. 326 * 327 * @see #getMaximumItemAge() 328 */ 329 public void setMaximumItemAge(long periods) { 330 if (periods < 0) { 331 throw new IllegalArgumentException("Negative 'periods' argument."); 332 } 333 this.maximumItemAge = periods; 334 removeAgedItems(true); // remove old items and notify if necessary 335 } 336 337 /** 338 * Returns the range of y-values in the time series. Any <code>null</code> 339 * data values in the series will be ignored (except for the special case 340 * where all data values are <code>null</code>, in which case the return 341 * value is <code>Range(Double.NaN, Double.NaN)</code>). If the time 342 * series contains no items, this method will return <code>null</code>. 343 * 344 * @return The range of y-values in the time series (possibly 345 * <code>null</code>). 346 * 347 * @since 1.0.18 348 */ 349 public Range findValueRange() { 350 if (this.data.isEmpty()) { 351 return null; 352 } 353 return new Range(this.minY, this.maxY); 354 } 355 356 /** 357 * Returns the range of y-values in the time series that fall within 358 * the specified range of x-values. This is equivalent to 359 * <code>findValueRange(xRange, TimePeriodAnchor.MIDDLE, timeZone)</code>. 360 * 361 * @param xRange the subrange of x-values (<code>null</code> not 362 * permitted). 363 * @param timeZone the time zone used to convert x-values to time periods 364 * (<code>null</code> not permitted). 365 * 366 * @return The range. 367 * 368 * @since 1.0.18 369 */ 370 public Range findValueRange(Range xRange, TimeZone timeZone) { 371 return findValueRange(xRange, TimePeriodAnchor.MIDDLE, timeZone); 372 } 373 374 /** 375 * Finds the range of y-values that fall within the specified range of 376 * x-values (where the x-values are interpreted as milliseconds since the 377 * epoch and converted to time periods using the specified timezone). 378 * 379 * @param xRange the subset of x-values to use (<code>null</code> not 380 * permitted). 381 * @param xAnchor the anchor point for the x-values (<code>null</code> 382 * not permitted). 383 * @param zone the time zone (<code>null</code> not permitted). 384 * 385 * @return The range of y-values. 386 * 387 * @since 1.0.18 388 */ 389 public Range findValueRange(Range xRange, TimePeriodAnchor xAnchor, 390 TimeZone zone) { 391 ParamChecks.nullNotPermitted(xRange, "xRange"); 392 ParamChecks.nullNotPermitted(xAnchor, "xAnchor"); 393 ParamChecks.nullNotPermitted(zone, "zone"); 394 if (this.data.isEmpty()) { 395 return null; 396 } 397 Calendar calendar = Calendar.getInstance(zone); 398 // since the items are ordered, we could be more clever here and avoid 399 // iterating over all the data 400 double lowY = Double.POSITIVE_INFINITY; 401 double highY = Double.NEGATIVE_INFINITY; 402 for (int i = 0; i < this.data.size(); i++) { 403 TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(i); 404 long millis = item.getPeriod().getMillisecond(xAnchor, calendar); 405 if (xRange.contains(millis)) { 406 Number n = item.getValue(); 407 if (n != null) { 408 double v = n.doubleValue(); 409 lowY = Math.min(lowY, v); 410 highY = Math.max(highY, v); 411 } 412 } 413 } 414 if (Double.isInfinite(lowY) && Double.isInfinite(highY)) { 415 if (lowY < highY) { 416 return new Range(lowY, highY); 417 } else { 418 return new Range(Double.NaN, Double.NaN); 419 } 420 } 421 return new Range(lowY, highY); 422 } 423 424 /** 425 * Returns the smallest y-value in the series, ignoring any 426 * <code>null</code> and <code>Double.NaN</code> values. This method 427 * returns <code>Double.NaN</code> if there is no smallest y-value (for 428 * example, when the series is empty). 429 * 430 * @return The smallest y-value. 431 * 432 * @see #getMaxY() 433 * 434 * @since 1.0.14 435 */ 436 public double getMinY() { 437 return this.minY; 438 } 439 440 /** 441 * Returns the largest y-value in the series, ignoring any 442 * <code>null</code> and <code>Double.NaN</code> values. This method 443 * returns <code>Double.NaN</code> if there is no largest y-value 444 * (for example, when the series is empty). 445 * 446 * @return The largest y-value. 447 * 448 * @see #getMinY() 449 * 450 * @since 1.0.14 451 */ 452 public double getMaxY() { 453 return this.maxY; 454 } 455 456 /** 457 * Returns the time period class for this series. 458 * <p> 459 * Only one time period class can be used within a single series (enforced). 460 * If you add a data item with a {@link Year} for the time period, then all 461 * subsequent data items must also have a {@link Year} for the time period. 462 * 463 * @return The time period class (may be <code>null</code> but only for 464 * an empty series). 465 */ 466 public Class getTimePeriodClass() { 467 return this.timePeriodClass; 468 } 469 470 /** 471 * Returns a data item from the dataset. Note that the returned object 472 * is a clone of the item in the series, so modifying it will have no 473 * effect on the data series. 474 * 475 * @param index the item index. 476 * 477 * @return The data item. 478 */ 479 public TimeSeriesDataItem getDataItem(int index) { 480 TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index); 481 return (TimeSeriesDataItem) item.clone(); 482 } 483 484 /** 485 * Returns the data item for a specific period. Note that the returned 486 * object is a clone of the item in the series, so modifying it will have 487 * no effect on the data series. 488 * 489 * @param period the period of interest (<code>null</code> not allowed). 490 * 491 * @return The data item matching the specified period (or 492 * <code>null</code> if there is no match). 493 * 494 * @see #getDataItem(int) 495 */ 496 public TimeSeriesDataItem getDataItem(RegularTimePeriod period) { 497 int index = getIndex(period); 498 if (index >= 0) { 499 return getDataItem(index); 500 } 501 return null; 502 } 503 504 /** 505 * Returns a data item for the series. This method returns the object 506 * that is used for the underlying storage - you should not modify the 507 * contents of the returned value unless you know what you are doing. 508 * 509 * @param index the item index (zero-based). 510 * 511 * @return The data item. 512 * 513 * @see #getDataItem(int) 514 * 515 * @since 1.0.14 516 */ 517 TimeSeriesDataItem getRawDataItem(int index) { 518 return (TimeSeriesDataItem) this.data.get(index); 519 } 520 521 /** 522 * Returns a data item for the series. This method returns the object 523 * that is used for the underlying storage - you should not modify the 524 * contents of the returned value unless you know what you are doing. 525 * 526 * @param period the item index (zero-based). 527 * 528 * @return The data item. 529 * 530 * @see #getDataItem(RegularTimePeriod) 531 * 532 * @since 1.0.14 533 */ 534 TimeSeriesDataItem getRawDataItem(RegularTimePeriod period) { 535 int index = getIndex(period); 536 if (index >= 0) { 537 return (TimeSeriesDataItem) this.data.get(index); 538 } 539 return null; 540 } 541 542 /** 543 * Returns the time period at the specified index. 544 * 545 * @param index the index of the data item. 546 * 547 * @return The time period. 548 */ 549 public RegularTimePeriod getTimePeriod(int index) { 550 return getRawDataItem(index).getPeriod(); 551 } 552 553 /** 554 * Returns a time period that would be the next in sequence on the end of 555 * the time series. 556 * 557 * @return The next time period. 558 */ 559 public RegularTimePeriod getNextTimePeriod() { 560 RegularTimePeriod last = getTimePeriod(getItemCount() - 1); 561 return last.next(); 562 } 563 564 /** 565 * Returns a collection of all the time periods in the time series. 566 * 567 * @return A collection of all the time periods. 568 */ 569 public Collection getTimePeriods() { 570 Collection result = new java.util.ArrayList(); 571 for (int i = 0; i < getItemCount(); i++) { 572 result.add(getTimePeriod(i)); 573 } 574 return result; 575 } 576 577 /** 578 * Returns a collection of time periods in the specified series, but not in 579 * this series, and therefore unique to the specified series. 580 * 581 * @param series the series to check against this one. 582 * 583 * @return The unique time periods. 584 */ 585 public Collection getTimePeriodsUniqueToOtherSeries(TimeSeries series) { 586 Collection result = new java.util.ArrayList(); 587 for (int i = 0; i < series.getItemCount(); i++) { 588 RegularTimePeriod period = series.getTimePeriod(i); 589 int index = getIndex(period); 590 if (index < 0) { 591 result.add(period); 592 } 593 } 594 return result; 595 } 596 597 /** 598 * Returns the index for the item (if any) that corresponds to a time 599 * period. 600 * 601 * @param period the time period (<code>null</code> not permitted). 602 * 603 * @return The index. 604 */ 605 public int getIndex(RegularTimePeriod period) { 606 ParamChecks.nullNotPermitted(period, "period"); 607 TimeSeriesDataItem dummy = new TimeSeriesDataItem( 608 period, Integer.MIN_VALUE); 609 return Collections.binarySearch(this.data, dummy); 610 } 611 612 /** 613 * Returns the value at the specified index. 614 * 615 * @param index index of a value. 616 * 617 * @return The value (possibly <code>null</code>). 618 */ 619 public Number getValue(int index) { 620 return getRawDataItem(index).getValue(); 621 } 622 623 /** 624 * Returns the value for a time period. If there is no data item with the 625 * specified period, this method will return <code>null</code>. 626 * 627 * @param period time period (<code>null</code> not permitted). 628 * 629 * @return The value (possibly <code>null</code>). 630 */ 631 public Number getValue(RegularTimePeriod period) { 632 int index = getIndex(period); 633 if (index >= 0) { 634 return getValue(index); 635 } 636 return null; 637 } 638 639 /** 640 * Adds a data item to the series and sends a {@link SeriesChangeEvent} to 641 * all registered listeners. 642 * 643 * @param item the (timeperiod, value) pair (<code>null</code> not 644 * permitted). 645 */ 646 public void add(TimeSeriesDataItem item) { 647 add(item, true); 648 } 649 650 /** 651 * Adds a data item to the series and sends a {@link SeriesChangeEvent} to 652 * all registered listeners. 653 * 654 * @param item the (timeperiod, value) pair (<code>null</code> not 655 * permitted). 656 * @param notify notify listeners? 657 */ 658 public void add(TimeSeriesDataItem item, boolean notify) { 659 ParamChecks.nullNotPermitted(item, "item"); 660 item = (TimeSeriesDataItem) item.clone(); 661 Class c = item.getPeriod().getClass(); 662 if (this.timePeriodClass == null) { 663 this.timePeriodClass = c; 664 } 665 else if (!this.timePeriodClass.equals(c)) { 666 StringBuilder b = new StringBuilder(); 667 b.append("You are trying to add data where the time period class "); 668 b.append("is "); 669 b.append(item.getPeriod().getClass().getName()); 670 b.append(", but the TimeSeries is expecting an instance of "); 671 b.append(this.timePeriodClass.getName()); 672 b.append("."); 673 throw new SeriesException(b.toString()); 674 } 675 676 // make the change (if it's not a duplicate time period)... 677 boolean added = false; 678 int count = getItemCount(); 679 if (count == 0) { 680 this.data.add(item); 681 added = true; 682 } 683 else { 684 RegularTimePeriod last = getTimePeriod(getItemCount() - 1); 685 if (item.getPeriod().compareTo(last) > 0) { 686 this.data.add(item); 687 added = true; 688 } 689 else { 690 int index = Collections.binarySearch(this.data, item); 691 if (index < 0) { 692 this.data.add(-index - 1, item); 693 added = true; 694 } 695 else { 696 StringBuilder b = new StringBuilder(); 697 b.append("You are attempting to add an observation for "); 698 b.append("the time period "); 699 b.append(item.getPeriod().toString()); 700 b.append(" but the series already contains an observation"); 701 b.append(" for that time period. Duplicates are not "); 702 b.append("permitted. Try using the addOrUpdate() method."); 703 throw new SeriesException(b.toString()); 704 } 705 } 706 } 707 if (added) { 708 updateBoundsForAddedItem(item); 709 // check if this addition will exceed the maximum item count... 710 if (getItemCount() > this.maximumItemCount) { 711 TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0); 712 updateBoundsForRemovedItem(d); 713 } 714 715 removeAgedItems(false); // remove old items if necessary, but 716 // don't notify anyone, because that 717 // happens next anyway... 718 if (notify) { 719 fireSeriesChanged(); 720 } 721 } 722 723 } 724 725 /** 726 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 727 * to all registered listeners. 728 * 729 * @param period the time period (<code>null</code> not permitted). 730 * @param value the value. 731 */ 732 public void add(RegularTimePeriod period, double value) { 733 // defer argument checking... 734 add(period, value, true); 735 } 736 737 /** 738 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 739 * to all registered listeners. 740 * 741 * @param period the time period (<code>null</code> not permitted). 742 * @param value the value. 743 * @param notify notify listeners? 744 */ 745 public void add(RegularTimePeriod period, double value, boolean notify) { 746 // defer argument checking... 747 TimeSeriesDataItem item = new TimeSeriesDataItem(period, value); 748 add(item, notify); 749 } 750 751 /** 752 * Adds a new data item to the series and sends 753 * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered 754 * listeners. 755 * 756 * @param period the time period (<code>null</code> not permitted). 757 * @param value the value (<code>null</code> permitted). 758 */ 759 public void add(RegularTimePeriod period, Number value) { 760 // defer argument checking... 761 add(period, value, true); 762 } 763 764 /** 765 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 766 * to all registered listeners. 767 * 768 * @param period the time period (<code>null</code> not permitted). 769 * @param value the value (<code>null</code> permitted). 770 * @param notify notify listeners? 771 */ 772 public void add(RegularTimePeriod period, Number value, boolean notify) { 773 // defer argument checking... 774 TimeSeriesDataItem item = new TimeSeriesDataItem(period, value); 775 add(item, notify); 776 } 777 778 /** 779 * Updates (changes) the value for a time period. Throws a 780 * {@link SeriesException} if the period does not exist. 781 * 782 * @param period the period (<code>null</code> not permitted). 783 * @param value the value. 784 * 785 * @since 1.0.14 786 */ 787 public void update(RegularTimePeriod period, double value) { 788 update(period, new Double(value)); 789 } 790 791 /** 792 * Updates (changes) the value for a time period. Throws a 793 * {@link SeriesException} if the period does not exist. 794 * 795 * @param period the period (<code>null</code> not permitted). 796 * @param value the value (<code>null</code> permitted). 797 */ 798 public void update(RegularTimePeriod period, Number value) { 799 TimeSeriesDataItem temp = new TimeSeriesDataItem(period, value); 800 int index = Collections.binarySearch(this.data, temp); 801 if (index < 0) { 802 throw new SeriesException("There is no existing value for the " 803 + "specified 'period'."); 804 } 805 update(index, value); 806 } 807 808 /** 809 * Updates (changes) the value of a data item. 810 * 811 * @param index the index of the data item. 812 * @param value the new value (<code>null</code> permitted). 813 */ 814 public void update(int index, Number value) { 815 TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index); 816 boolean iterate = false; 817 Number oldYN = item.getValue(); 818 if (oldYN != null) { 819 double oldY = oldYN.doubleValue(); 820 if (!Double.isNaN(oldY)) { 821 iterate = oldY <= this.minY || oldY >= this.maxY; 822 } 823 } 824 item.setValue(value); 825 if (iterate) { 826 updateMinMaxYByIteration(); 827 } 828 else if (value != null) { 829 double yy = value.doubleValue(); 830 this.minY = minIgnoreNaN(this.minY, yy); 831 this.maxY = maxIgnoreNaN(this.maxY, yy); 832 } 833 fireSeriesChanged(); 834 } 835 836 /** 837 * Adds or updates data from one series to another. Returns another series 838 * containing the values that were overwritten. 839 * 840 * @param series the series to merge with this. 841 * 842 * @return A series containing the values that were overwritten. 843 */ 844 public TimeSeries addAndOrUpdate(TimeSeries series) { 845 TimeSeries overwritten = new TimeSeries("Overwritten values from: " 846 + getKey()); 847 for (int i = 0; i < series.getItemCount(); i++) { 848 TimeSeriesDataItem item = series.getRawDataItem(i); 849 TimeSeriesDataItem oldItem = addOrUpdate(item.getPeriod(), 850 item.getValue()); 851 if (oldItem != null) { 852 overwritten.add(oldItem); 853 } 854 } 855 return overwritten; 856 } 857 858 /** 859 * Adds or updates an item in the times series and sends a 860 * {@link SeriesChangeEvent} to all registered listeners. 861 * 862 * @param period the time period to add/update (<code>null</code> not 863 * permitted). 864 * @param value the new value. 865 * 866 * @return A copy of the overwritten data item, or <code>null</code> if no 867 * item was overwritten. 868 */ 869 public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period, 870 double value) { 871 return addOrUpdate(period, new Double(value)); 872 } 873 874 /** 875 * Adds or updates an item in the times series and sends a 876 * {@link SeriesChangeEvent} to all registered listeners. 877 * 878 * @param period the time period to add/update (<code>null</code> not 879 * permitted). 880 * @param value the new value (<code>null</code> permitted). 881 * 882 * @return A copy of the overwritten data item, or <code>null</code> if no 883 * item was overwritten. 884 */ 885 public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period, 886 Number value) { 887 return addOrUpdate(new TimeSeriesDataItem(period, value)); 888 } 889 890 /** 891 * Adds or updates an item in the times series and sends a 892 * {@link SeriesChangeEvent} to all registered listeners. 893 * 894 * @param item the data item (<code>null</code> not permitted). 895 * 896 * @return A copy of the overwritten data item, or <code>null</code> if no 897 * item was overwritten. 898 * 899 * @since 1.0.14 900 */ 901 public TimeSeriesDataItem addOrUpdate(TimeSeriesDataItem item) { 902 903 ParamChecks.nullNotPermitted(item, "item"); 904 Class periodClass = item.getPeriod().getClass(); 905 if (this.timePeriodClass == null) { 906 this.timePeriodClass = periodClass; 907 } 908 else if (!this.timePeriodClass.equals(periodClass)) { 909 String msg = "You are trying to add data where the time " 910 + "period class is " + periodClass.getName() 911 + ", but the TimeSeries is expecting an instance of " 912 + this.timePeriodClass.getName() + "."; 913 throw new SeriesException(msg); 914 } 915 TimeSeriesDataItem overwritten = null; 916 int index = Collections.binarySearch(this.data, item); 917 if (index >= 0) { 918 TimeSeriesDataItem existing 919 = (TimeSeriesDataItem) this.data.get(index); 920 overwritten = (TimeSeriesDataItem) existing.clone(); 921 // figure out if we need to iterate through all the y-values 922 // to find the revised minY / maxY 923 boolean iterate = false; 924 Number oldYN = existing.getValue(); 925 double oldY = oldYN != null ? oldYN.doubleValue() : Double.NaN; 926 if (!Double.isNaN(oldY)) { 927 iterate = oldY <= this.minY || oldY >= this.maxY; 928 } 929 existing.setValue(item.getValue()); 930 if (iterate) { 931 updateMinMaxYByIteration(); 932 } 933 else if (item.getValue() != null) { 934 double yy = item.getValue().doubleValue(); 935 this.minY = minIgnoreNaN(this.minY, yy); 936 this.maxY = maxIgnoreNaN(this.maxY, yy); 937 } 938 } 939 else { 940 item = (TimeSeriesDataItem) item.clone(); 941 this.data.add(-index - 1, item); 942 updateBoundsForAddedItem(item); 943 944 // check if this addition will exceed the maximum item count... 945 if (getItemCount() > this.maximumItemCount) { 946 TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0); 947 updateBoundsForRemovedItem(d); 948 } 949 } 950 removeAgedItems(false); // remove old items if necessary, but 951 // don't notify anyone, because that 952 // happens next anyway... 953 fireSeriesChanged(); 954 return overwritten; 955 956 } 957 958 /** 959 * Age items in the series. Ensure that the timespan from the youngest to 960 * the oldest record in the series does not exceed maximumItemAge time 961 * periods. Oldest items will be removed if required. 962 * 963 * @param notify controls whether or not a {@link SeriesChangeEvent} is 964 * sent to registered listeners IF any items are removed. 965 */ 966 public void removeAgedItems(boolean notify) { 967 // check if there are any values earlier than specified by the history 968 // count... 969 if (getItemCount() > 1) { 970 long latest = getTimePeriod(getItemCount() - 1).getSerialIndex(); 971 boolean removed = false; 972 while ((latest - getTimePeriod(0).getSerialIndex()) 973 > this.maximumItemAge) { 974 this.data.remove(0); 975 removed = true; 976 } 977 if (removed) { 978 updateMinMaxYByIteration(); 979 if (notify) { 980 fireSeriesChanged(); 981 } 982 } 983 } 984 } 985 986 /** 987 * Age items in the series. Ensure that the timespan from the supplied 988 * time to the oldest record in the series does not exceed history count. 989 * oldest items will be removed if required. 990 * 991 * @param latest the time to be compared against when aging data 992 * (specified in milliseconds). 993 * @param notify controls whether or not a {@link SeriesChangeEvent} is 994 * sent to registered listeners IF any items are removed. 995 */ 996 public void removeAgedItems(long latest, boolean notify) { 997 if (this.data.isEmpty()) { 998 return; // nothing to do 999 } 1000 // find the serial index of the period specified by 'latest' 1001 long index = Long.MAX_VALUE; 1002 try { 1003 Method m = RegularTimePeriod.class.getDeclaredMethod( 1004 "createInstance", new Class[] {Class.class, Date.class, 1005 TimeZone.class}); 1006 RegularTimePeriod newest = (RegularTimePeriod) m.invoke( 1007 this.timePeriodClass, new Object[] {this.timePeriodClass, 1008 new Date(latest), TimeZone.getDefault()}); 1009 index = newest.getSerialIndex(); 1010 } 1011 catch (NoSuchMethodException e) { 1012 throw new RuntimeException(e); 1013 } 1014 catch (IllegalAccessException e) { 1015 throw new RuntimeException(e); 1016 } 1017 catch (InvocationTargetException e) { 1018 throw new RuntimeException(e); 1019 } 1020 1021 // check if there are any values earlier than specified by the history 1022 // count... 1023 boolean removed = false; 1024 while (getItemCount() > 0 && (index 1025 - getTimePeriod(0).getSerialIndex()) > this.maximumItemAge) { 1026 this.data.remove(0); 1027 removed = true; 1028 } 1029 if (removed) { 1030 updateMinMaxYByIteration(); 1031 if (notify) { 1032 fireSeriesChanged(); 1033 } 1034 } 1035 } 1036 1037 /** 1038 * Removes all data items from the series and sends a 1039 * {@link SeriesChangeEvent} to all registered listeners. 1040 */ 1041 public void clear() { 1042 if (this.data.size() > 0) { 1043 this.data.clear(); 1044 this.timePeriodClass = null; 1045 this.minY = Double.NaN; 1046 this.maxY = Double.NaN; 1047 fireSeriesChanged(); 1048 } 1049 } 1050 1051 /** 1052 * Deletes the data item for the given time period and sends a 1053 * {@link SeriesChangeEvent} to all registered listeners. If there is no 1054 * item with the specified time period, this method does nothing. 1055 * 1056 * @param period the period of the item to delete (<code>null</code> not 1057 * permitted). 1058 */ 1059 public void delete(RegularTimePeriod period) { 1060 int index = getIndex(period); 1061 if (index >= 0) { 1062 TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.remove( 1063 index); 1064 updateBoundsForRemovedItem(item); 1065 if (this.data.isEmpty()) { 1066 this.timePeriodClass = null; 1067 } 1068 fireSeriesChanged(); 1069 } 1070 } 1071 1072 /** 1073 * Deletes data from start until end index (end inclusive). 1074 * 1075 * @param start the index of the first period to delete. 1076 * @param end the index of the last period to delete. 1077 */ 1078 public void delete(int start, int end) { 1079 delete(start, end, true); 1080 } 1081 1082 /** 1083 * Deletes data from start until end index (end inclusive). 1084 * 1085 * @param start the index of the first period to delete. 1086 * @param end the index of the last period to delete. 1087 * @param notify notify listeners? 1088 * 1089 * @since 1.0.14 1090 */ 1091 public void delete(int start, int end, boolean notify) { 1092 if (end < start) { 1093 throw new IllegalArgumentException("Requires start <= end."); 1094 } 1095 for (int i = 0; i <= (end - start); i++) { 1096 this.data.remove(start); 1097 } 1098 updateMinMaxYByIteration(); 1099 if (this.data.isEmpty()) { 1100 this.timePeriodClass = null; 1101 } 1102 if (notify) { 1103 fireSeriesChanged(); 1104 } 1105 } 1106 1107 /** 1108 * Returns a clone of the time series. 1109 * <P> 1110 * Notes: 1111 * <ul> 1112 * <li>no need to clone the domain and range descriptions, since String 1113 * object is immutable;</li> 1114 * <li>we pass over to the more general method clone(start, end).</li> 1115 * </ul> 1116 * 1117 * @return A clone of the time series. 1118 * 1119 * @throws CloneNotSupportedException not thrown by this class, but 1120 * subclasses may differ. 1121 */ 1122 @Override 1123 public Object clone() throws CloneNotSupportedException { 1124 TimeSeries clone = (TimeSeries) super.clone(); 1125 clone.data = (List) ObjectUtilities.deepClone(this.data); 1126 return clone; 1127 } 1128 1129 /** 1130 * Creates a new timeseries by copying a subset of the data in this time 1131 * series. 1132 * 1133 * @param start the index of the first time period to copy. 1134 * @param end the index of the last time period to copy. 1135 * 1136 * @return A series containing a copy of this times series from start until 1137 * end. 1138 * 1139 * @throws CloneNotSupportedException if there is a cloning problem. 1140 */ 1141 public TimeSeries createCopy(int start, int end) 1142 throws CloneNotSupportedException { 1143 if (start < 0) { 1144 throw new IllegalArgumentException("Requires start >= 0."); 1145 } 1146 if (end < start) { 1147 throw new IllegalArgumentException("Requires start <= end."); 1148 } 1149 TimeSeries copy = (TimeSeries) super.clone(); 1150 copy.minY = Double.NaN; 1151 copy.maxY = Double.NaN; 1152 copy.data = new java.util.ArrayList(); 1153 if (this.data.size() > 0) { 1154 for (int index = start; index <= end; index++) { 1155 TimeSeriesDataItem item 1156 = (TimeSeriesDataItem) this.data.get(index); 1157 TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone(); 1158 try { 1159 copy.add(clone); 1160 } 1161 catch (SeriesException e) { 1162 throw new RuntimeException(e); 1163 } 1164 } 1165 } 1166 return copy; 1167 } 1168 1169 /** 1170 * Creates a new timeseries by copying a subset of the data in this time 1171 * series. 1172 * 1173 * @param start the first time period to copy (<code>null</code> not 1174 * permitted). 1175 * @param end the last time period to copy (<code>null</code> not 1176 * permitted). 1177 * 1178 * @return A time series containing a copy of this time series from start 1179 * until end. 1180 * 1181 * @throws CloneNotSupportedException if there is a cloning problem. 1182 */ 1183 public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end) 1184 throws CloneNotSupportedException { 1185 1186 ParamChecks.nullNotPermitted(start, "start"); 1187 ParamChecks.nullNotPermitted(end, "end"); 1188 if (start.compareTo(end) > 0) { 1189 throw new IllegalArgumentException( 1190 "Requires start on or before end."); 1191 } 1192 boolean emptyRange = false; 1193 int startIndex = getIndex(start); 1194 if (startIndex < 0) { 1195 startIndex = -(startIndex + 1); 1196 if (startIndex == this.data.size()) { 1197 emptyRange = true; // start is after last data item 1198 } 1199 } 1200 int endIndex = getIndex(end); 1201 if (endIndex < 0) { // end period is not in original series 1202 endIndex = -(endIndex + 1); // this is first item AFTER end period 1203 endIndex = endIndex - 1; // so this is last item BEFORE end 1204 } 1205 if ((endIndex < 0) || (endIndex < startIndex)) { 1206 emptyRange = true; 1207 } 1208 if (emptyRange) { 1209 TimeSeries copy = (TimeSeries) super.clone(); 1210 copy.data = new java.util.ArrayList(); 1211 return copy; 1212 } 1213 return createCopy(startIndex, endIndex); 1214 } 1215 1216 /** 1217 * Tests the series for equality with an arbitrary object. 1218 * 1219 * @param obj the object to test against (<code>null</code> permitted). 1220 * 1221 * @return A boolean. 1222 */ 1223 @Override 1224 public boolean equals(Object obj) { 1225 if (obj == this) { 1226 return true; 1227 } 1228 if (!(obj instanceof TimeSeries)) { 1229 return false; 1230 } 1231 TimeSeries that = (TimeSeries) obj; 1232 if (!ObjectUtilities.equal(getDomainDescription(), 1233 that.getDomainDescription())) { 1234 return false; 1235 } 1236 if (!ObjectUtilities.equal(getRangeDescription(), 1237 that.getRangeDescription())) { 1238 return false; 1239 } 1240 if (!ObjectUtilities.equal(this.timePeriodClass, 1241 that.timePeriodClass)) { 1242 return false; 1243 } 1244 if (getMaximumItemAge() != that.getMaximumItemAge()) { 1245 return false; 1246 } 1247 if (getMaximumItemCount() != that.getMaximumItemCount()) { 1248 return false; 1249 } 1250 int count = getItemCount(); 1251 if (count != that.getItemCount()) { 1252 return false; 1253 } 1254 if (!ObjectUtilities.equal(this.data, that.data)) { 1255 return false; 1256 } 1257 return super.equals(obj); 1258 } 1259 1260 /** 1261 * Returns a hash code value for the object. 1262 * 1263 * @return The hashcode 1264 */ 1265 @Override 1266 public int hashCode() { 1267 int result = super.hashCode(); 1268 result = 29 * result + (this.domain != null ? this.domain.hashCode() 1269 : 0); 1270 result = 29 * result + (this.range != null ? this.range.hashCode() : 0); 1271 result = 29 * result + (this.timePeriodClass != null 1272 ? this.timePeriodClass.hashCode() : 0); 1273 // it is too slow to look at every data item, so let's just look at 1274 // the first, middle and last items... 1275 int count = getItemCount(); 1276 if (count > 0) { 1277 TimeSeriesDataItem item = getRawDataItem(0); 1278 result = 29 * result + item.hashCode(); 1279 } 1280 if (count > 1) { 1281 TimeSeriesDataItem item = getRawDataItem(count - 1); 1282 result = 29 * result + item.hashCode(); 1283 } 1284 if (count > 2) { 1285 TimeSeriesDataItem item = getRawDataItem(count / 2); 1286 result = 29 * result + item.hashCode(); 1287 } 1288 result = 29 * result + this.maximumItemCount; 1289 result = 29 * result + (int) this.maximumItemAge; 1290 return result; 1291 } 1292 1293 /** 1294 * Updates the cached values for the minimum and maximum data values. 1295 * 1296 * @param item the item added (<code>null</code> not permitted). 1297 * 1298 * @since 1.0.14 1299 */ 1300 private void updateBoundsForAddedItem(TimeSeriesDataItem item) { 1301 Number yN = item.getValue(); 1302 if (item.getValue() != null) { 1303 double y = yN.doubleValue(); 1304 this.minY = minIgnoreNaN(this.minY, y); 1305 this.maxY = maxIgnoreNaN(this.maxY, y); 1306 } 1307 } 1308 1309 /** 1310 * Updates the cached values for the minimum and maximum data values on 1311 * the basis that the specified item has just been removed. 1312 * 1313 * @param item the item added (<code>null</code> not permitted). 1314 * 1315 * @since 1.0.14 1316 */ 1317 private void updateBoundsForRemovedItem(TimeSeriesDataItem item) { 1318 Number yN = item.getValue(); 1319 if (yN != null) { 1320 double y = yN.doubleValue(); 1321 if (!Double.isNaN(y)) { 1322 if (y <= this.minY || y >= this.maxY) { 1323 updateMinMaxYByIteration(); 1324 } 1325 } 1326 } 1327 } 1328 1329 /** 1330 * Finds the bounds of the x and y values for the series, by iterating 1331 * through all the data items. 1332 * 1333 * @since 1.0.14 1334 */ 1335 private void updateMinMaxYByIteration() { 1336 this.minY = Double.NaN; 1337 this.maxY = Double.NaN; 1338 Iterator iterator = this.data.iterator(); 1339 while (iterator.hasNext()) { 1340 TimeSeriesDataItem item = (TimeSeriesDataItem) iterator.next(); 1341 updateBoundsForAddedItem(item); 1342 } 1343 } 1344 1345 /** 1346 * A function to find the minimum of two values, but ignoring any 1347 * Double.NaN values. 1348 * 1349 * @param a the first value. 1350 * @param b the second value. 1351 * 1352 * @return The minimum of the two values. 1353 */ 1354 private double minIgnoreNaN(double a, double b) { 1355 if (Double.isNaN(a)) { 1356 return b; 1357 } 1358 if (Double.isNaN(b)) { 1359 return a; 1360 } 1361 return Math.min(a, b); 1362 } 1363 1364 /** 1365 * A function to find the maximum of two values, but ignoring any 1366 * Double.NaN values. 1367 * 1368 * @param a the first value. 1369 * @param b the second value. 1370 * 1371 * @return The maximum of the two values. 1372 */ 1373 private double maxIgnoreNaN(double a, double b) { 1374 if (Double.isNaN(a)) { 1375 return b; 1376 } 1377 if (Double.isNaN(b)) { 1378 return a; 1379 } 1380 else { 1381 return Math.max(a, b); 1382 } 1383 } 1384 1385 1386 /** 1387 * Creates a new (empty) time series with the specified name and class 1388 * of {@link RegularTimePeriod}. 1389 * 1390 * @param name the series name (<code>null</code> not permitted). 1391 * @param timePeriodClass the type of time period (<code>null</code> not 1392 * permitted). 1393 * 1394 * @deprecated As of 1.0.13, it is not necessary to specify the 1395 * <code>timePeriodClass</code> as this will be inferred when the 1396 * first data item is added to the dataset. 1397 */ 1398 public TimeSeries(Comparable name, Class timePeriodClass) { 1399 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION, 1400 timePeriodClass); 1401 } 1402 1403 /** 1404 * Creates a new time series that contains no data. 1405 * <P> 1406 * Descriptions can be specified for the domain and range. One situation 1407 * where this is helpful is when generating a chart for the time series - 1408 * axis labels can be taken from the domain and range description. 1409 * 1410 * @param name the name of the series (<code>null</code> not permitted). 1411 * @param domain the domain description (<code>null</code> permitted). 1412 * @param range the range description (<code>null</code> permitted). 1413 * @param timePeriodClass the type of time period (<code>null</code> not 1414 * permitted). 1415 * 1416 * @deprecated As of 1.0.13, it is not necessary to specify the 1417 * <code>timePeriodClass</code> as this will be inferred when the 1418 * first data item is added to the dataset. 1419 */ 1420 public TimeSeries(Comparable name, String domain, String range, 1421 Class timePeriodClass) { 1422 super(name); 1423 this.domain = domain; 1424 this.range = range; 1425 this.timePeriodClass = timePeriodClass; 1426 this.data = new java.util.ArrayList(); 1427 this.maximumItemCount = Integer.MAX_VALUE; 1428 this.maximumItemAge = Long.MAX_VALUE; 1429 this.minY = Double.NaN; 1430 this.maxY = Double.NaN; 1431 } 1432 1433}