001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * --------------------------------
028 * DynamicTimeSeriesCollection.java
029 * --------------------------------
030 * (C) Copyright 2002-2008, by I. H. Thomae and Contributors.
031 *
032 * Original Author:  I. H. Thomae (ithomae@ists.dartmouth.edu);
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *
035 * Changes
036 * -------
037 * 22-Nov-2002 : Initial version completed
038 *    Jan 2003 : Optimized advanceTime(), added implemnt'n of RangeInfo intfc
039 *               (using cached values for min, max, and range); also added
040 *               getOldestIndex() and getNewestIndex() ftns so client classes
041 *               can use this class as the master "index authority".
042 * 22-Jan-2003 : Made this class stand on its own, rather than extending
043 *               class FastTimeSeriesCollection
044 * 31-Jan-2003 : Changed TimePeriod --> RegularTimePeriod (DG);
045 * 13-Mar-2003 : Moved to com.jrefinery.data.time package (DG);
046 * 29-Apr-2003 : Added small change to appendData method, from Irv Thomae (DG);
047 * 19-Sep-2003 : Added new appendData method, from Irv Thomae (DG);
048 * 05-May-2004 : Now extends AbstractIntervalXYDataset.  This also required a
049 *               change to the return type of the getY() method - I'm slightly
050 *               unsure of the implications of this, so it might require some
051 *               further amendment (DG);
052 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
053 *               getYValue() (DG);
054 * 11-Jan-2004 : Removed deprecated code in preparation for the 1.0.0
055 *               release (DG);
056 * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
057 *
058 */
059
060package org.jfree.data.time;
061
062import java.util.Calendar;
063import java.util.TimeZone;
064
065import org.jfree.data.DomainInfo;
066import org.jfree.data.Range;
067import org.jfree.data.RangeInfo;
068import org.jfree.data.general.SeriesChangeEvent;
069import org.jfree.data.xy.AbstractIntervalXYDataset;
070import org.jfree.data.xy.IntervalXYDataset;
071
072/**
073 * A dynamic dataset.
074 * <p>
075 * Like FastTimeSeriesCollection, this class is a functional replacement
076 * for JFreeChart's TimeSeriesCollection _and_ TimeSeries classes.
077 * FastTimeSeriesCollection is appropriate for a fixed time range; for
078 * real-time applications this subclass adds the ability to append new
079 * data and discard the oldest.
080 * In this class, the arrays used in FastTimeSeriesCollection become FIFO's.
081 * NOTE:As presented here, all data is assumed >= 0, an assumption which is
082 * embodied only in methods associated with interface RangeInfo.
083 */
084public class DynamicTimeSeriesCollection extends AbstractIntervalXYDataset
085                                         implements IntervalXYDataset,
086                                                    DomainInfo,
087                                                    RangeInfo {
088
089    /**
090     * Useful constant for controlling the x-value returned for a time
091     * period.
092     */
093    public static final int START = 0;
094
095    /**
096     * Useful constant for controlling the x-value returned for a time period.
097     */
098    public static final int MIDDLE = 1;
099
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}