Source for org.jfree.data.time.DynamicTimeSeriesCollection

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  *
  27:  * --------------------------------
  28:  * DynamicTimeSeriesCollection.java
  29:  * --------------------------------
  30:  * (C) Copyright 2002-2007, by I. H. Thomae and Contributors.
  31:  *
  32:  * Original Author:  I. H. Thomae (ithomae@ists.dartmouth.edu);
  33:  * Contributor(s):   David Gilbert (for Object Refinery Limited);
  34:  *
  35:  * Changes
  36:  * -------
  37:  * 22-Nov-2002 : Initial version completed
  38:  *    Jan 2003 : Optimized advanceTime(), added implemnt'n of RangeInfo intfc
  39:  *               (using cached values for min, max, and range); also added
  40:  *               getOldestIndex() and getNewestIndex() ftns so client classes
  41:  *               can use this class as the master "index authority".
  42:  * 22-Jan-2003 : Made this class stand on its own, rather than extending
  43:  *               class FastTimeSeriesCollection
  44:  * 31-Jan-2003 : Changed TimePeriod --> RegularTimePeriod (DG);
  45:  * 13-Mar-2003 : Moved to com.jrefinery.data.time package (DG);
  46:  * 29-Apr-2003 : Added small change to appendData method, from Irv Thomae (DG);
  47:  * 19-Sep-2003 : Added new appendData method, from Irv Thomae (DG);
  48:  * 05-May-2004 : Now extends AbstractIntervalXYDataset.  This also required a
  49:  *               change to the return type of the getY() method - I'm slightly
  50:  *               unsure of the implications of this, so it might require some
  51:  *               further amendment (DG);
  52:  * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
  53:  *               getYValue() (DG);
  54:  * 11-Jan-2004 : Removed deprecated code in preparation for the 1.0.0 
  55:  *               release (DG);
  56:  * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
  57:  * 
  58:  */
  59: 
  60: package org.jfree.data.time;
  61: 
  62: import java.util.Calendar;
  63: import java.util.TimeZone;
  64: 
  65: import org.jfree.data.DomainInfo;
  66: import org.jfree.data.Range;
  67: import org.jfree.data.RangeInfo;
  68: import org.jfree.data.general.SeriesChangeEvent;
  69: import org.jfree.data.xy.AbstractIntervalXYDataset;
  70: import org.jfree.data.xy.IntervalXYDataset;
  71: 
  72: /**
  73:  * A dynamic dataset.
  74:  * <p>
  75:  * Like FastTimeSeriesCollection, this class is a functional replacement
  76:  * for JFreeChart's TimeSeriesCollection _and_ TimeSeries classes.
  77:  * FastTimeSeriesCollection is appropriate for a fixed time range; for
  78:  * real-time applications this subclass adds the ability to append new
  79:  * data and discard the oldest.
  80:  * In this class, the arrays used in FastTimeSeriesCollection become FIFO's.
  81:  * NOTE:As presented here, all data is assumed >= 0, an assumption which is
  82:  * embodied only in methods associated with interface RangeInfo.
  83:  */
  84: public class DynamicTimeSeriesCollection extends AbstractIntervalXYDataset
  85:                                          implements IntervalXYDataset,
  86:                                                     DomainInfo,
  87:                                                     RangeInfo {
  88: 
  89:     /** 
  90:      * Useful constant for controlling the x-value returned for a time 
  91:      * period. 
  92:      */
  93:     public static final int START = 0;
  94: 
  95:     /** 
  96:      * Useful constant for controlling the x-value returned for a time period. 
  97:      */
  98:     public static final int MIDDLE = 1;
  99: 
 100:     /** 
 101:      * Useful constant for controlling the x-value returned for a time period. 
 102:      */
 103:     public static final int END = 2;
 104: 
 105:     /** The maximum number of items for each series (can be overridden). */
 106:     private int maximumItemCount = 2000;  // an arbitrary safe default value
 107: 
 108:     /** The history count. */
 109:     protected int historyCount;
 110: 
 111:     /** Storage for the series keys. */
 112:     private Comparable[] seriesKeys;
 113: 
 114:     /** The time period class - barely used, and could be removed (DG). */
 115:     private Class timePeriodClass = Minute.class;   // default value;
 116: 
 117:     /** Storage for the x-values. */
 118:     protected RegularTimePeriod[] pointsInTime;
 119: 
 120:     /** The number of series. */
 121:     private int seriesCount;
 122: 
 123:     /**
 124:      * A wrapper for a fixed array of float values.
 125:      */
 126:     protected class ValueSequence {
 127: 
 128:         /** Storage for the float values. */
 129:         float[] dataPoints;
 130: 
 131:         /**
 132:          * Default constructor:
 133:          */
 134:         public ValueSequence() {
 135:             this(DynamicTimeSeriesCollection.this.maximumItemCount);
 136:         }
 137: 
 138:         /**
 139:          * Creates a sequence with the specified length.
 140:          *
 141:          * @param length  the length.
 142:          */
 143:         public ValueSequence(int length) {
 144:             this.dataPoints = new float[length];
 145:             for (int i = 0; i < length; i++) {
 146:                 this.dataPoints[i] = 0.0f;
 147:             }
 148:         }
 149: 
 150:         /**
 151:          * Enters data into the storage array.
 152:          *
 153:          * @param index  the index.
 154:          * @param value  the value.
 155:          */
 156:         public void enterData(int index, float value) {
 157:             this.dataPoints[index] = value;
 158:         }
 159: 
 160:         /**
 161:          * Returns a value from the storage array.
 162:          *
 163:          * @param index  the index.
 164:          *
 165:          * @return The value.
 166:          */
 167:         public float getData(int index) {
 168:             return this.dataPoints[index];
 169:         }
 170:     }
 171: 
 172:     /** An array for storing the objects that represent each series. */
 173:     protected ValueSequence[] valueHistory;
 174: 
 175:     /** A working calendar (to recycle) */
 176:     protected Calendar workingCalendar;
 177: 
 178:     /** 
 179:      * The position within a time period to return as the x-value (START, 
 180:      * MIDDLE or END). 
 181:      */
 182:     private int position;
 183: 
 184:     /**
 185:      * A flag that indicates that the domain is 'points in time'.  If this flag 
 186:      * is true, only the x-value is used to determine the range of values in 
 187:      * the domain, the start and end x-values are ignored.
 188:      */
 189:     private boolean domainIsPointsInTime;
 190: 
 191:     /** index for mapping: points to the oldest valid time & data. */
 192:     private int oldestAt;  // as a class variable, initializes == 0
 193: 
 194:     /** Index of the newest data item. */
 195:     private int newestAt;
 196: 
 197:     // cached values used for interface DomainInfo:
 198: 
 199:     /** the # of msec by which time advances. */
 200:     private long deltaTime;
 201: 
 202:     /** Cached domain start (for use by DomainInfo). */
 203:     private Long domainStart;
 204: 
 205:     /** Cached domain end (for use by DomainInfo). */
 206:     private Long domainEnd;
 207: 
 208:     /** Cached domain range (for use by DomainInfo). */
 209:     private Range domainRange;
 210: 
 211:     // Cached values used for interface RangeInfo: (note minValue pinned at 0)
 212:     //   A single set of extrema covers the entire SeriesCollection
 213: 
 214:     /** The minimum value. */
 215:     private Float minValue = new Float(0.0f);
 216: 
 217:     /** The maximum value. */
 218:     private Float maxValue = null;
 219: 
 220:     /** The value range. */
 221:     private Range valueRange;  // autoinit's to null.
 222: 
 223:     /**
 224:      * Constructs a dataset with capacity for N series, tied to default 
 225:      * timezone.
 226:      *
 227:      * @param nSeries the number of series to be accommodated.
 228:      * @param nMoments the number of TimePeriods to be spanned.
 229:      */
 230:     public DynamicTimeSeriesCollection(int nSeries, int nMoments) {
 231: 
 232:         this(nSeries, nMoments, new Millisecond(), TimeZone.getDefault());
 233:         this.newestAt = nMoments - 1;
 234: 
 235:     }
 236: 
 237:     /**
 238:      * Constructs an empty dataset, tied to a specific timezone.
 239:      *
 240:      * @param nSeries the number of series to be accommodated
 241:      * @param nMoments the number of TimePeriods to be spanned
 242:      * @param zone the timezone.
 243:      */
 244:     public DynamicTimeSeriesCollection(int nSeries, int nMoments, 
 245:                                        TimeZone zone) {
 246:         this(nSeries, nMoments, new Millisecond(), zone);
 247:         this.newestAt = nMoments - 1;
 248:     }
 249: 
 250:     /**
 251:      * Creates a new dataset.
 252:      *
 253:      * @param nSeries  the number of series.
 254:      * @param nMoments  the number of items per series.
 255:      * @param timeSample  a time period sample.
 256:      */
 257:     public DynamicTimeSeriesCollection(int nSeries,
 258:                                        int nMoments,
 259:                                        RegularTimePeriod timeSample) {
 260:         this(nSeries, nMoments, timeSample, TimeZone.getDefault());
 261:     }
 262: 
 263:     /**
 264:      * Creates a new dataset.
 265:      *
 266:      * @param nSeries  the number of series.
 267:      * @param nMoments  the number of items per series.
 268:      * @param timeSample  a time period sample.
 269:      * @param zone  the time zone.
 270:      */
 271:     public DynamicTimeSeriesCollection(int nSeries,
 272:                                        int nMoments,
 273:                                        RegularTimePeriod timeSample,
 274:                                        TimeZone zone) {
 275: 
 276:         // the first initialization must precede creation of the ValueSet array:
 277:         this.maximumItemCount = nMoments;  // establishes length of each array
 278:         this.historyCount = nMoments;
 279:         this.seriesKeys = new Comparable[nSeries];
 280:         // initialize the members of "seriesNames" array so they won't be null:
 281:         for (int i = 0; i < nSeries; i++) {
 282:             this.seriesKeys[i] = "";
 283:         }
 284:         this.newestAt = nMoments - 1;
 285:         this.valueHistory = new ValueSequence[nSeries];
 286:         this.timePeriodClass = timeSample.getClass();
 287: 
 288:         /// Expand the following for all defined TimePeriods:
 289:         if (this.timePeriodClass == Second.class) {
 290:             this.pointsInTime = new Second[nMoments];
 291:         }
 292:         else if (this.timePeriodClass == Minute.class) {
 293:             this.pointsInTime = new Minute[nMoments];
 294:         }
 295:         else if (this.timePeriodClass == Hour.class) {
 296:             this.pointsInTime = new Hour[nMoments];
 297:         }
 298:         ///  .. etc....
 299:         this.workingCalendar = Calendar.getInstance(zone);
 300:         this.position = START;
 301:         this.domainIsPointsInTime = true;
 302:     }
 303: 
 304:     /**
 305:      * Fill the pointsInTime with times using TimePeriod.next():
 306:      * Will silently return if the time array was already populated.
 307:      *
 308:      * Also computes the data cached for later use by
 309:      * methods implementing the DomainInfo interface:
 310:      *
 311:      * @param start  the start.
 312:      *
 313:      * @return ??.
 314:      */
 315:     public synchronized long setTimeBase(RegularTimePeriod start) {
 316: 
 317:         if (this.pointsInTime[0] == null) {
 318:             this.pointsInTime[0] = start;
 319:             for (int i = 1; i < this.historyCount; i++) {
 320:                 this.pointsInTime[i] = this.pointsInTime[i - 1].next();
 321:             }
 322:         }
 323:         long oldestL = this.pointsInTime[0].getFirstMillisecond(
 324:             this.workingCalendar
 325:         );
 326:         long nextL = this.pointsInTime[1].getFirstMillisecond(
 327:             this.workingCalendar
 328:         );
 329:         this.deltaTime = nextL - oldestL;
 330:         this.oldestAt = 0;
 331:         this.newestAt = this.historyCount - 1;
 332:         findDomainLimits();
 333:         return this.deltaTime;
 334: 
 335:     }
 336: 
 337:     /**
 338:      * Finds the domain limits.  Note: this doesn't need to be synchronized 
 339:      * because it's called from within another method that already is.
 340:      */
 341:     protected void findDomainLimits() {
 342: 
 343:         long startL = getOldestTime().getFirstMillisecond(this.workingCalendar);
 344:         long endL;
 345:         if (this.domainIsPointsInTime) {
 346:             endL = getNewestTime().getFirstMillisecond(this.workingCalendar);
 347:         }
 348:         else {
 349:             endL = getNewestTime().getLastMillisecond(this.workingCalendar);
 350:         }
 351:         this.domainStart = new Long(startL);
 352:         this.domainEnd = new Long(endL);
 353:         this.domainRange = new Range(startL, endL);
 354: 
 355:     }
 356: 
 357:     /**
 358:      * Returns the x position type (START, MIDDLE or END).
 359:      *
 360:      * @return The x position type.
 361:      */
 362:     public int getPosition() {
 363:         return this.position;
 364:     }
 365: 
 366:     /**
 367:      * Sets the x position type (START, MIDDLE or END).
 368:      *
 369:      * @param position The x position type.
 370:      */
 371:     public void setPosition(int position) {
 372:         this.position = position;
 373:     }
 374: 
 375:     /**
 376:      * Adds a series to the dataset.  Only the y-values are supplied, the 
 377:      * x-values are specified elsewhere.
 378:      *
 379:      * @param values  the y-values.
 380:      * @param seriesNumber  the series index (zero-based).
 381:      * @param seriesKey  the series key.
 382:      *
 383:      * Use this as-is during setup only, or add the synchronized keyword around 
 384:      * the copy loop.
 385:      */
 386:     public void addSeries(float[] values,
 387:                           int seriesNumber, Comparable seriesKey) {
 388: 
 389:         invalidateRangeInfo();
 390:         int i;
 391:         if (values == null) {
 392:             throw new IllegalArgumentException("TimeSeriesDataset.addSeries(): "
 393:                 + "cannot add null array of values.");
 394:         }
 395:         if (seriesNumber >= this.valueHistory.length) {
 396:             throw new IllegalArgumentException("TimeSeriesDataset.addSeries(): "
 397:                 + "cannot add more series than specified in c'tor");
 398:         }
 399:         if (this.valueHistory[seriesNumber] == null) {
 400:             this.valueHistory[seriesNumber] 
 401:                 = new ValueSequence(this.historyCount);
 402:             this.seriesCount++;
 403:         }   
 404:         // But if that series array already exists, just overwrite its contents
 405: 
 406:         // Avoid IndexOutOfBoundsException:
 407:         int srcLength = values.length;
 408:         int copyLength = this.historyCount;
 409:         boolean fillNeeded = false;
 410:         if (srcLength < this.historyCount) {
 411:             fillNeeded = true;
 412:             copyLength = srcLength;
 413:         }
 414:         //{
 415:         for (i = 0; i < copyLength; i++) { // deep copy from values[], caller 
 416:                                            // can safely discard that array
 417:             this.valueHistory[seriesNumber].enterData(i, values[i]);
 418:         }
 419:         if (fillNeeded) {
 420:             for (i = copyLength; i < this.historyCount; i++) {
 421:                 this.valueHistory[seriesNumber].enterData(i, 0.0f);
 422:             }
 423:         }
 424:       //}
 425:         if (seriesKey != null) {
 426:             this.seriesKeys[seriesNumber] = seriesKey;
 427:         }
 428:         fireSeriesChanged();
 429: 
 430:     }
 431: 
 432:     /**
 433:      * Sets the name of a series.  If planning to add values individually.
 434:      *
 435:      * @param seriesNumber  the series.
 436:      * @param key  the new key.
 437:      */
 438:     public void setSeriesKey(int seriesNumber, Comparable key) {
 439:         this.seriesKeys[seriesNumber] = key;
 440:     }
 441: 
 442:     /**
 443:      * Adds a value to a series.
 444:      *
 445:      * @param seriesNumber  the series index.
 446:      * @param index  ??.
 447:      * @param value  the value.
 448:      */
 449:     public void addValue(int seriesNumber, int index, float value) {
 450: 
 451:         invalidateRangeInfo();
 452:         if (seriesNumber >= this.valueHistory.length) {
 453:             throw new IllegalArgumentException(
 454:                 "TimeSeriesDataset.addValue(): series #"
 455:                 + seriesNumber + "unspecified in c'tor"
 456:             );
 457:         }
 458:         if (this.valueHistory[seriesNumber] == null) {
 459:             this.valueHistory[seriesNumber] 
 460:                 = new ValueSequence(this.historyCount);
 461:             this.seriesCount++;
 462:         }  
 463:         // But if that series array already exists, just overwrite its contents
 464:         //synchronized(this)
 465:         //{
 466:             this.valueHistory[seriesNumber].enterData(index, value);
 467:         //}
 468:         fireSeriesChanged();
 469:     }
 470: 
 471:     /**
 472:      * Returns the number of series in the collection.
 473:      *
 474:      * @return The series count.
 475:      */
 476:     public int getSeriesCount() {
 477:         return this.seriesCount;
 478:     }
 479: 
 480:     /**
 481:      * Returns the number of items in a series.
 482:      * <p>
 483:      * For this implementation, all series have the same number of items.
 484:      *
 485:      * @param series  the series index (zero-based).
 486:      *
 487:      * @return The item count.
 488:      */
 489:     public int getItemCount(int series) {  // all arrays equal length, 
 490:                                            // so ignore argument:
 491:         return this.historyCount;
 492:     }
 493: 
 494:     // Methods for managing the FIFO's:
 495: 
 496:     /**
 497:      * Re-map an index, for use in retrieving data.
 498:      *
 499:      * @param toFetch  the index.
 500:      *
 501:      * @return The translated index.
 502:      */
 503:     protected int translateGet(int toFetch) {
 504:         if (this.oldestAt == 0) {
 505:             return toFetch;  // no translation needed
 506:         }
 507:         // else  [implicit here]
 508:         int newIndex = toFetch + this.oldestAt;
 509:         if (newIndex >= this.historyCount) {
 510:             newIndex -= this.historyCount;
 511:         }
 512:         return newIndex;
 513:     }
 514: 
 515:     /**
 516:      * Returns the actual index to a time offset by "delta" from newestAt.
 517:      *
 518:      * @param delta  the delta.
 519:      *
 520:      * @return The offset.
 521:      */
 522:     public int offsetFromNewest(int delta) {
 523:         return wrapOffset(this.newestAt + delta);
 524:     }
 525: 
 526:     /**
 527:      * ??
 528:      *
 529:      * @param delta ??
 530:      *
 531:      * @return The offset.
 532:      */
 533:     public int offsetFromOldest(int delta) {
 534:         return wrapOffset(this.oldestAt + delta);
 535:     }
 536: 
 537:     /**
 538:      * ??
 539:      *
 540:      * @param protoIndex  the index.
 541:      *
 542:      * @return The offset.
 543:      */
 544:     protected int wrapOffset(int protoIndex) {
 545:         int tmp = protoIndex;
 546:         if (tmp >= this.historyCount) {
 547:             tmp -= this.historyCount;
 548:         }
 549:         else if (tmp < 0) {
 550:             tmp += this.historyCount;
 551:         }
 552:         return tmp;
 553:     }
 554: 
 555:     /**
 556:      * Adjust the array offset as needed when a new time-period is added:
 557:      * Increments the indices "oldestAt" and "newestAt", mod(array length),
 558:      * zeroes the series values at newestAt, returns the new TimePeriod.
 559:      *
 560:      * @return The new time period.
 561:      */
 562:     public synchronized RegularTimePeriod advanceTime() {
 563:         RegularTimePeriod nextInstant = this.pointsInTime[this.newestAt].next();
 564:         this.newestAt = this.oldestAt;  // newestAt takes value previously held 
 565:                                         // by oldestAT
 566:         /*** 
 567:          * The next 10 lines or so should be expanded if data can be negative 
 568:          ***/
 569:         // if the oldest data contained a maximum Y-value, invalidate the stored
 570:         //   Y-max and Y-range data:
 571:         boolean extremaChanged = false;
 572:         float oldMax = 0.0f;
 573:         if (this.maxValue != null) {
 574:             oldMax = this.maxValue.floatValue();
 575:         }
 576:         for (int s = 0; s < getSeriesCount(); s++) {
 577:             if (this.valueHistory[s].getData(this.oldestAt) == oldMax) {
 578:                 extremaChanged = true;
 579:             }
 580:             if (extremaChanged) {
 581:                 break;
 582:             }
 583:         }  /*** If data can be < 0, add code here to check the minimum    **/
 584:         if (extremaChanged) {
 585:             invalidateRangeInfo();
 586:         }
 587:         //  wipe the next (about to be used) set of data slots
 588:         float wiper = (float) 0.0;
 589:         for (int s = 0; s < getSeriesCount(); s++) {
 590:             this.valueHistory[s].enterData(this.newestAt, wiper);
 591:         }
 592:         // Update the array of TimePeriods:
 593:         this.pointsInTime[this.newestAt] = nextInstant;
 594:         // Now advance "oldestAt", wrapping at end of the array
 595:         this.oldestAt++;
 596:         if (this.oldestAt >= this.historyCount) {
 597:             this.oldestAt = 0;
 598:         }
 599:         // Update the domain limits:
 600:         long startL = this.domainStart.longValue();  //(time is kept in msec)
 601:         this.domainStart = new Long(startL + this.deltaTime);
 602:         long endL = this.domainEnd.longValue();
 603:         this.domainEnd = new Long(endL + this.deltaTime);
 604:         this.domainRange = new Range(startL, endL);
 605:         fireSeriesChanged();
 606:         return nextInstant;
 607:     }
 608: 
 609:     //  If data can be < 0, the next 2 methods should be modified
 610: 
 611:     /**
 612:      * Invalidates the range info.
 613:      */
 614:     public void invalidateRangeInfo() {
 615:         this.maxValue = null;
 616:         this.valueRange = null;
 617:     }
 618: 
 619:     /**
 620:      * Returns the maximum value.
 621:      *
 622:      * @return The maximum value.
 623:      */
 624:     protected double findMaxValue() {
 625:         double max = 0.0f;
 626:         for (int s = 0; s < getSeriesCount(); s++) {
 627:             for (int i = 0; i < this.historyCount; i++) {
 628:                 double tmp = getYValue(s, i);
 629:                 if (tmp > max) {
 630:                     max = tmp;
 631:                 }
 632:             }
 633:         }
 634:         return max;
 635:     }
 636: 
 637:     /** End, positive-data-only code  **/
 638: 
 639:     /**
 640:      * Returns the index of the oldest data item.
 641:      *
 642:      * @return The index.
 643:      */
 644:     public int getOldestIndex() {
 645:         return this.oldestAt;
 646:     }
 647: 
 648:     /**
 649:      * Returns the index of the newest data item.
 650:      *
 651:      * @return The index.
 652:      */
 653:     public int getNewestIndex() {
 654:         return this.newestAt;
 655:     }
 656: 
 657:     // appendData() writes new data at the index position given by newestAt/
 658:     // When adding new data dynamically, use advanceTime(), followed by this:
 659:     /**
 660:      * Appends new data.
 661:      *
 662:      * @param newData  the data.
 663:      */
 664:     public void appendData(float[] newData) {
 665:         int nDataPoints = newData.length;
 666:         if (nDataPoints > this.valueHistory.length) {
 667:             throw new IllegalArgumentException(
 668:                "More data than series to put them in"
 669:             );
 670:         }
 671:         int s;   // index to select the "series"
 672:         for (s = 0; s < nDataPoints; s++) {
 673:             // check whether the "valueHistory" array member exists; if not, 
 674:             // create them:
 675:             if (this.valueHistory[s] == null) {
 676:                 this.valueHistory[s] = new ValueSequence(this.historyCount);
 677:             }
 678:             this.valueHistory[s].enterData(this.newestAt, newData[s]);
 679:         }
 680:         fireSeriesChanged();
 681:     }
 682: 
 683:     /**
 684:      * Appends data at specified index, for loading up with data from file(s).
 685:      *
 686:      * @param  newData  the data
 687:      * @param  insertionIndex  the index value at which to put it
 688:      * @param  refresh  value of n in "refresh the display on every nth call"
 689:      *                 (ignored if <= 0 )
 690:      */
 691:      public void appendData(float[] newData, int insertionIndex, int refresh) {
 692:          int nDataPoints = newData.length;
 693:          if (nDataPoints > this.valueHistory.length) {
 694:              throw new IllegalArgumentException(
 695:                  "More data than series to put them " + "in"
 696:              );
 697:          }
 698:          for (int s = 0; s < nDataPoints; s++) {
 699:              if (this.valueHistory[s] == null) {
 700:                 this.valueHistory[s] = new ValueSequence(this.historyCount);
 701:              }
 702:              this.valueHistory[s].enterData(insertionIndex, newData[s]);
 703:          }
 704:          if (refresh > 0) {
 705:              insertionIndex++;
 706:              if (insertionIndex % refresh == 0) {
 707:                  fireSeriesChanged();
 708:              }
 709:          }
 710:     }
 711: 
 712:     /**
 713:      * Returns the newest time.
 714:      *
 715:      * @return The newest time.
 716:      */
 717:     public RegularTimePeriod getNewestTime() {
 718:         return this.pointsInTime[this.newestAt];
 719:     }
 720: 
 721:     /**
 722:      * Returns the oldest time.
 723:      *
 724:      * @return The oldest time.
 725:      */
 726:     public RegularTimePeriod getOldestTime() {
 727:         return this.pointsInTime[this.oldestAt];
 728:     }
 729: 
 730:     /**
 731:      * Returns the x-value.
 732:      *
 733:      * @param series  the series index (zero-based).
 734:      * @param item  the item index (zero-based).
 735:      *
 736:      * @return The value.
 737:      */
 738:     // getXxx() ftns can ignore the "series" argument:
 739:     // Don't synchronize this!! Instead, synchronize the loop that calls it.
 740:     public Number getX(int series, int item) {
 741:         RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
 742:         return new Long(getX(tp));
 743:     }
 744: 
 745:     /**
 746:      * Returns the y-value.
 747:      *
 748:      * @param series  the series index (zero-based).
 749:      * @param item  the item index (zero-based).
 750:      *
 751:      * @return The value.
 752:      */
 753:     public double getYValue(int series, int item) {  
 754:         // Don't synchronize this!!
 755:         // Instead, synchronize the loop that calls it.
 756:         ValueSequence values = this.valueHistory[series];
 757:         return values.getData(translateGet(item)); 
 758:     }
 759: 
 760:     /**
 761:      * Returns the y-value.
 762:      *
 763:      * @param series  the series index (zero-based).
 764:      * @param item  the item index (zero-based).
 765:      *
 766:      * @return The value.
 767:      */
 768:     public Number getY(int series, int item) {
 769:         return new Float(getYValue(series, item));
 770:     }
 771: 
 772:     /**
 773:      * Returns the start x-value.
 774:      *
 775:      * @param series  the series index (zero-based).
 776:      * @param item  the item index (zero-based).
 777:      *
 778:      * @return The value.
 779:      */
 780:     public Number getStartX(int series, int item) {
 781:         RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
 782:         return new Long(tp.getFirstMillisecond(this.workingCalendar));
 783:     }
 784: 
 785:     /**
 786:      * Returns the end x-value.
 787:      *
 788:      * @param series  the series index (zero-based).
 789:      * @param item  the item index (zero-based).
 790:      *
 791:      * @return The value.
 792:      */
 793:     public Number getEndX(int series, int item) {
 794:         RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
 795:         return new Long(tp.getLastMillisecond(this.workingCalendar));
 796:     }
 797: 
 798:     /**
 799:      * Returns the start y-value.
 800:      *
 801:      * @param series  the series index (zero-based).
 802:      * @param item  the item index (zero-based).
 803:      *
 804:      * @return The value.
 805:      */
 806:     public Number getStartY(int series, int item) {
 807:         return getY(series, item);
 808:     }
 809: 
 810:     /**
 811:      * Returns the end y-value.
 812:      *
 813:      * @param series  the series index (zero-based).
 814:      * @param item  the item index (zero-based).
 815:      *
 816:      * @return The value.
 817:      */
 818:     public Number getEndY(int series, int item) {
 819:         return getY(series, item);
 820:     }
 821: 
 822:     /* // "Extras" found useful when analyzing/verifying class behavior:
 823:     public Number getUntranslatedXValue(int series, int item)
 824:     {
 825:       return super.getXValue(series, item);
 826:     }
 827: 
 828:     public float getUntranslatedY(int series, int item)
 829:     {
 830:       return super.getY(series, item);
 831:     }  */
 832: 
 833:     /**
 834:      * Returns the key for a series.
 835:      *
 836:      * @param series  the series index (zero-based).
 837:      *
 838:      * @return The key.
 839:      */
 840:     public Comparable getSeriesKey(int series) {
 841:         return this.seriesKeys[series];
 842:     }
 843: 
 844:     /**
 845:      * Sends a {@link SeriesChangeEvent} to all registered listeners.
 846:      */
 847:     protected void fireSeriesChanged() {
 848:         seriesChanged(new SeriesChangeEvent(this));
 849:     }
 850: 
 851:     // The next 3 functions override the base-class implementation of
 852:     // the DomainInfo interface.  Using saved limits (updated by
 853:     // each updateTime() call), improves performance.
 854:     //
 855: 
 856:     /**
 857:      * Returns the minimum x-value in the dataset.
 858:      *
 859:      * @param includeInterval  a flag that determines whether or not the
 860:      *                         x-interval is taken into account.
 861:      * 
 862:      * @return The minimum value.
 863:      */
 864:     public double getDomainLowerBound(boolean includeInterval) {
 865:         return this.domainStart.doubleValue();  
 866:         // a Long kept updated by advanceTime()        
 867:     }
 868: 
 869:     /**
 870:      * Returns the maximum x-value in the dataset.
 871:      *
 872:      * @param includeInterval  a flag that determines whether or not the
 873:      *                         x-interval is taken into account.
 874:      * 
 875:      * @return The maximum value.
 876:      */
 877:     public double getDomainUpperBound(boolean includeInterval) {
 878:         return this.domainEnd.doubleValue();  
 879:         // a Long kept updated by advanceTime()
 880:     }
 881: 
 882:     /**
 883:      * Returns the range of the values in this dataset's domain.
 884:      *
 885:      * @param includeInterval  a flag that determines whether or not the
 886:      *                         x-interval is taken into account.
 887:      * 
 888:      * @return The range.
 889:      */
 890:     public Range getDomainBounds(boolean includeInterval) {
 891:         if (this.domainRange == null) {
 892:             findDomainLimits();
 893:         }
 894:         return this.domainRange;
 895:     }
 896:     
 897:     /**
 898:      * Returns the x-value for a time period.
 899:      *
 900:      * @param period  the period.
 901:      *
 902:      * @return The x-value.
 903:      */
 904:     private long getX(RegularTimePeriod period) {
 905:         switch (this.position) {
 906:             case (START) : 
 907:                 return period.getFirstMillisecond(this.workingCalendar);
 908:             case (MIDDLE) : 
 909:                 return period.getMiddleMillisecond(this.workingCalendar);
 910:             case (END) : 
 911:                 return period.getLastMillisecond(this.workingCalendar);
 912:             default: 
 913:                 return period.getMiddleMillisecond(this.workingCalendar);
 914:         }
 915:      }
 916: 
 917:     // The next 3 functions implement the RangeInfo interface.
 918:     // Using saved limits (updated by each updateTime() call) significantly
 919:     // improves performance.  WARNING: this code makes the simplifying 
 920:     // assumption that data is never negative.  Expand as needed for the 
 921:     // general case.
 922: 
 923:     /**
 924:      * Returns the minimum range value.
 925:      *
 926:      * @param includeInterval  a flag that determines whether or not the
 927:      *                         y-interval is taken into account.
 928:      * 
 929:      * @return The minimum range value.
 930:      */
 931:     public double getRangeLowerBound(boolean includeInterval) {
 932:         double result = Double.NaN;
 933:         if (this.minValue != null) {
 934:             result = this.minValue.doubleValue();
 935:         }
 936:         return result;
 937:     }
 938: 
 939:     /**
 940:      * Returns the maximum range value.
 941:      *
 942:      * @param includeInterval  a flag that determines whether or not the
 943:      *                         y-interval is taken into account.
 944:      * 
 945:      * @return The maximum range value.
 946:      */
 947:     public double getRangeUpperBound(boolean includeInterval) {
 948:         double result = Double.NaN;
 949:         if (this.maxValue != null) {
 950:             result = this.maxValue.doubleValue();
 951:         }
 952:         return result;
 953:     }
 954: 
 955:     /**
 956:      * Returns the value range.
 957:      *
 958:      * @param includeInterval  a flag that determines whether or not the
 959:      *                         y-interval is taken into account.
 960:      * 
 961:      * @return The range.
 962:      */
 963:     public Range getRangeBounds(boolean includeInterval) {
 964:         if (this.valueRange == null) {
 965:             double max = getRangeUpperBound(includeInterval);
 966:             this.valueRange = new Range(0.0, max);
 967:         }
 968:         return this.valueRange;
 969:     }
 970:     
 971: }