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