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
060 package org.jfree.data.time;
061
062 import java.util.Calendar;
063 import java.util.TimeZone;
064
065 import org.jfree.data.DomainInfo;
066 import org.jfree.data.Range;
067 import org.jfree.data.RangeInfo;
068 import org.jfree.data.general.SeriesChangeEvent;
069 import org.jfree.data.xy.AbstractIntervalXYDataset;
070 import 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 */
084 public 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 }