DynamicTimeSeriesCollection

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
raviteja

DynamicTimeSeriesCollection

Post by raviteja » Sat Dec 28, 2002 4:24 pm

Hi Irv Thomae,

Can u tell me where the code for "DynamicTimeSeriesCollection"

is available.I could get the code for "FastTimeSeriesCollection".


thanks

regards
RaviTeja

raviteja

Re: DynamicTimeSeriesCollection

Post by raviteja » Mon Jan 06, 2003 6:17 am

Hi david,

Can u post me the URL "DynamicTimeSeriesCollection.java" developed by
Irv Thomae...


thanks in advance

regards
RaviTeja

David Gilbert

Re: DynamicTimeSeriesCollection

Post by David Gilbert » Tue Jan 07, 2003 11:58 am

Hi Ravi,

I think Irv is still working on it...

Regards,

Dave Gilbert

Irv Thomae

Re: DynamicTimeSeriesCollection

Post by Irv Thomae » Tue Jan 07, 2003 10:26 pm

Hi David,
Yes, that's correct - still working on it. BTW I sent you an obscure question by email yesterday, but perhaps you haven't recvd it yet?
Thanks,
Irv Thomae

David Gilbert

Re: DynamicTimeSeriesCollection

Post by David Gilbert » Tue Jan 07, 2003 11:33 pm

Hi Irv,

I have received your e-mail, it's in a long queue!

Regards,

DG

Ravi Teja

Re: DynamicTimeSeriesCollection

Post by Ravi Teja » Mon Jan 20, 2003 8:53 am

hi Irv,

I received Your E-mail too...
Thanks and Any Latest Update?????

Good Luck.


regards
Ravi Teja

Irv Thomae

Re: DynamicTimeSeriesCollection

Post by Irv Thomae » Wed Jan 22, 2003 11:31 pm

Hello, David (and Ravi), the DynamicTimeSeriesCollection class is ready at last.
In earlier postings on this phorum, I had described it as a subclass of the "FastTimeSeriesCollection" class I posted several weeks ago. However, although the core data structures used in the two classes are essentially identical, I have since decided to write it independently, using only AbstractSeriesDataset as base class.

By its very nature, this class is likely to be used in situations that require thread synchronization. The methods provided for appending data and advancing the current-time index are synchronized. For performance reasons, however, getX() and getY() are not. Instead, I recommend modifying the render() method of class XYPlot, so that when refreshing the display, the latter simply locks the entire dataset object for the duration of its read-and-plot loop. (illustrated below, after the ===== marking the end of the class file.)

Notice also that because all data in my application are >= 0, I have taken some shortcuts in supporting the RangeInfo interface . However, I believe the necessary expansions are clearly indicated by the comments.

File DynamicTimeSeriesCollection.java:

/* DynamicTimeSeriesCollection.java V2.0 I. H. Thomae
*
* Irv Thomae ithomae@ists.dartmouth.edu
* (C) 2002 ISTS/Dartmouth College
*
* This module is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
*
* History:
* 22-Nov-2002: Initial version completed
* Jan 2003: optimized advanceTime(), added implemnt'n of RangeInfo intfc
* (using cached values for min, max, and range); also added
* getOldestIndex() and getNewestIndex() ftns so client classes
* can use this class as the master "index authority".
* 1-22-2003: (V2.0:)Made this class stand on its own, rather than extending
* class FastTimeSeriesCollection
*
* Like FastTimeSeriesCollection, this class is a functional replacement
* for JFreeChart's TimeSeriesCollection _and_ BasicTimeSeries classes.
* FastTimeSeriesCollection is appropriate for a fixed time range; for
* real-time applications this class adds the ability to append new
* data, automatically discarding the oldest. That capability depends on a pair
* of array-index variables, "oldestAt" and "newestAt".
* Typically, this class will be used in real time, which raises synchronization
* issues. The methods provided for appending data and for advancing the indices
* are synchronized, but those used by jFreeChart plotting classes are _not_. It is
* much more efficient instead to synchronize the code loop in XYPlot.render() which
* reads the successive values from the dataset.
* In this class, the arrays used in FastTimeSeriesCollection become FIFO's.
* NOTE:As presented here, all data is assumed >= 0, an assumption which is
* embodied only in methods associated with interface RangeInfo.
*/

package com.jrefinery.data;
import com.jrefinery.data.TimePeriod;
import java.util.List;
import java.util.Iterator;
import java.util.Calendar;
import java.util.TimeZone;

public class DynamicTimeSeriesCollection extends AbstractSeriesDataset
implements IntervalXYDataset, DomainInfo, RangeInfo
{

/** Useful constant for controlling the x-value returned for a time period. */
public static final int START = 0;

/** Useful constant for controlling the x-value returned for a time period. */
public static final int MIDDLE = 1;

/** Useful constant for controlling the x-value returned for a time period. */
public static final int END = 2;

/** The maximum number of items for each series. */
private int maximumItemCount = 2000; // an arbitrary safe default value

protected int historyCount;

private String[] seriesNames;

private Class timePeriodClass = Minute.class; // default value;

protected TimePeriod[] pointsInTime;

private int seriesCount;


protected class ValueSequence
{
float dataPoints[];
/*
* Default constructor:
*/
public ValueSequence()
{
this(maximumItemCount);
}
public ValueSequence(int length)
{
dataPoints = new float[length];
for (int i = 0; i < length; i++)
dataPoints = 0.0f;
}
public void enterData(int index, float value)
{
dataPoints[index] = value;
}
public float getData(int index)
{
return dataPoints[index];
}
}

protected ValueSequence[] valueHistory;

/** A working calendar (to recycle) */
protected Calendar workingCalendar;

/** The position within a time period to return as the x-value (START, MIDDLE or END). */
private int position;
/**
* A flag that indicates that the domain is 'points in time'. If this flag is true, only
* the x-value is used to determine the range of values in the domain, the start and end
* x-values are ignored.
*/
private boolean domainIsPointsInTime;



// index for mapping: points to the oldest valid time & data
private int oldestAt; // as a class variable, initializes == 0
private int newestAt;
// cached values used for interface DomainInfo:
private long deltaTime; // the # of msec by which time advances
private Long domainStart;
private Long domainEnd;
private Range domainRange;
// Cached values used for interface RangeInfo: (note minValue pinned at 0)
// A single set of extrema covers the entire SeriesCollection
private Float minValue = new Float(0.0f);
private Float maxValue = null;
private Range valueRange; // autoinit's to null.

/**
* Constructs a dataset with capacity for N series, tied to default timezone.
* @param nSeries the number of series to be accommodated
* @param nMoments the number of TimePeriods to be spanned
*/
public DynamicTimeSeriesCollection(int nSeries, int nMoments)
{
this(nSeries, nMoments, new Millisecond(), TimeZone.getDefault());
newestAt = nMoments - 1;
}

/**
* Constructs an empty dataset, tied to a specific timezone.
*
* @param nSeries the number of series to be accommodated
* @param nMoments the number of TimePeriods to be spanned
* @param zone the timezone.
*/
public DynamicTimeSeriesCollection(int nSeries, int nMoments, TimeZone zone)
{
this(nSeries, nMoments, new Millisecond(), zone);
newestAt = nMoments - 1;
}

public DynamicTimeSeriesCollection(int nSeries, int nMoments, TimePeriod timeSample)
{
this(nSeries, nMoments, timeSample, TimeZone.getDefault());
}


public DynamicTimeSeriesCollection(int nSeries, int nMoments,
TimePeriod timeSample, TimeZone zone)
{
// the first initialization must precede creation of the ValueSet array:
maximumItemCount = nMoments; // establishes length of each array
historyCount = nMoments;
seriesNames = new String[nSeries];
// Initialize the members of "seriesNames" array so they won't be null:
for (int i = 0; i < nSeries; i++)
seriesNames = "";
newestAt = nMoments - 1;
valueHistory = new ValueSequence[nSeries];
timePeriodClass = timeSample.getClass();

/// Expand the following for all defined TimePeriods:
if (timePeriodClass == Second.class)
pointsInTime = new Second[nMoments];
else if (timePeriodClass == Minute.class)
pointsInTime = new Minute[nMoments];
else if (timePeriodClass == Hour.class)
pointsInTime = new Hour[nMoments];
/// .. etc....
this.workingCalendar = Calendar.getInstance(zone);
this.position = START;
this.domainIsPointsInTime = true;
}

/*
* Fill the pointsInTime with times using TimePeriod.next():
* Will silently return if the time array was already populated.
*
* Also computes the data cached for later use by
* methods implementing the DomainInfo interface:
*/
public synchronized long setTimeBase(TimePeriod start)
{
if (pointsInTime[0] == null)
{
pointsInTime[0] = start;
for (int i = 1; i < historyCount; i++)
{
pointsInTime = pointsInTime[i-1].next();
}
}
long oldestL = pointsInTime[0].getStart(workingCalendar);
long nextL = pointsInTime[1].getStart(workingCalendar);
deltaTime = nextL - oldestL;
oldestAt = 0;
newestAt = historyCount - 1;
findDomainLimits();
return deltaTime;
}

protected void findDomainLimits()
{
long startL = getOldestTime().getStart(workingCalendar);
long endL;
if (domainIsPointsInTime)
endL= getNewestTime().getStart(workingCalendar);
else
endL = getNewestTime().getEnd(workingCalendar);
domainStart = new Long(startL);
domainEnd = new Long(endL);
domainRange = new Range((double)startL,(double)endL);
}

public int getPosition()
{
return position;
}

public void setPosition(int position)
{
this.position = position;
}

// Not intended to be used in real time; but if needed, uncomment the "synchronized" keyword:
public void addSeries(float[] values,
int seriesNumber, String seriesName)
{
invalidateRangeInfo();
int i;
if (values == null)
{
throw new IllegalArgumentException (
"TimeSeriesDataset.addSeries(...): cannot add null array of values.");
}
if (seriesNumber >= valueHistory.length)
{
throw new IllegalArgumentException(
"TimeSeriesDataset.addSeries(...): cannot add more series than specified in c'tor");
}
if (valueHistory[seriesNumber] == null)
{
valueHistory[seriesNumber] = new ValueSequence(historyCount);
seriesCount++;
} // But if that series array already exists, just overwrite its contents
// Avoid IndexOutOfBoundsException:
int srcLength = values.length;
int copyLength = historyCount;
boolean fillNeeded = false;
if (srcLength < historyCount)
{
fillNeeded = true;
copyLength = srcLength;
}
//synchronized(this)
{
for (i = 0; i < copyLength; i++)
{ // deep copy from values[],so the caller can safely discard that array
valueHistory[seriesNumber].enterData(i, values);
}
if (fillNeeded)
{
for (i = copyLength; i < historyCount; i++)
valueHistory[seriesNumber].enterData(i, 0.0f);
}
}
if (seriesName != null)
seriesNames[seriesNumber] = seriesName;
fireSeriesChanged();
}

// If planning to add values individually, use the following to set
// the series names:
public void setSeriesName(int seriesNumber, String newName)
{
seriesNames[seriesNumber] = newName;
}

// and likewise for addValue(int,int,float):

public void addValue(int seriesNumber,
int index, float value)
{
invalidateRangeInfo();
if (seriesNumber >= valueHistory.length)
{
throw new IllegalArgumentException(
"TimeSeriesDataset.addValue(...): series #" + seriesNumber + "unspecified in c'tor");
}
if (valueHistory[seriesNumber] == null)
{
valueHistory[seriesNumber] = new ValueSequence(historyCount);
seriesCount++;
} // But if that series array already exists, just overwrite its contents
//synchronized(this)
{
valueHistory[seriesNumber].enterData(index,value);
}
//fireDatasetChanged();
fireSeriesChanged();
}

/**
* Returns the number of series in the collection.
*
* @return the series count.
*/
public int getSeriesCount()
{
return seriesCount;
}
public int getItemCount(int series)
{ // all arrays are of equal length, so just ignore the argument:
return historyCount;
}


// Methods for managing the FIFO's:
// Re-map an index, for use in retrieving data:
protected int translateGet(int toFetch)
{
if (oldestAt == 0)
return toFetch; // no translation needed
// else [implicit here]
int newIndex = toFetch + oldestAt;
if (newIndex >= historyCount)
newIndex -= historyCount;
return newIndex;
}
// return actual index to a time offset by "delta" from newestAt:
public int offsetFromNewest(int delta)
{
return wrapOffset(newestAt + delta);
}
public int offsetFromOldest(int delta)
{
return wrapOffset(oldestAt + delta);
}

protected int wrapOffset(int protoIndex)
{
int tmp = protoIndex;
if ( tmp >= historyCount)
tmp -= historyCount;
else if (tmp < 0)
tmp += historyCount;
return tmp;
}

// Adjust the array offset as needed when a new time-period is added:
// Increments the indices "oldestAt" and "newestAt", mod(array length),
// zeroes the series values at newestAt, returns the new TimePeriod.
public synchronized TimePeriod advanceTime()
{
int s = 0; // index to successive 'series'
TimePeriod nextInstant = pointsInTime[newestAt].next();
newestAt = oldestAt; // newestAt takes value previously held by oldestAT
/*** The next 10 lines or so should be expanded if data can be negative ***/
// if the oldest data contained a maximum Y-value, invalidate the stored
// Y-max and Y-range data:
boolean extremaChanged = false;
float oldMax = 0.0f;
if (maxValue != null)
oldMax = maxValue.floatValue();
for (s = 0; s < getSeriesCount(); s++)
{
if (valueHistory[s].getData(oldestAt) == oldMax)
extremaChanged = true;
if (extremaChanged) break;
} /*** If data can be < 0, add code here to check the minimum **/
if (extremaChanged)
invalidateRangeInfo();
// wipe the next (about to be used) set of data slots
float wiper = (float)0.0;
for (s = 0; s < getSeriesCount(); s++)
valueHistory[s].enterData(newestAt, wiper);
// Update the array of TimePeriods:
pointsInTime[newestAt] = nextInstant;
// Now advance "oldestAt", wrapping at end of the array
oldestAt++;
if (oldestAt >= historyCount)
oldestAt = 0;
// Update the domain limits:
long startL = domainStart.longValue(); //(time is kept in msec)
domainStart = new Long(startL + deltaTime);
long endL = domainEnd.longValue();
domainEnd = new Long(endL + deltaTime);
domainRange = new Range((double)startL,(double)endL);
fireSeriesChanged();
return nextInstant;
}

/** If data can be < 0, the next 2 methods should be modified **/
public void invalidateRangeInfo()
{
maxValue = null;
valueRange = null;
}

protected float findMaxValue()
{
float max = 0.0f;
float tmp = 0.0f;
for (int s = 0; s < getSeriesCount(); s++)
{
for (int i = 0; i < historyCount; i++)
{
tmp = getY(s,i);
if (tmp > max)
max = tmp;
}
}
return max;
}
/** End, positive-data-only code **/


public int getOldestIndex()
{
return oldestAt;
}

public int getNewestIndex()
{
return newestAt;
}
// appendData() writes new data at the index position given by newestAt/
// When adding new data dynamically, use advanceTime(), followed by this:
public void appendData( float[] newData)
{
int nDataPoints = newData.length;
if (nDataPoints > valueHistory.length)
{
throw new IllegalArgumentException(
"FastTimeSeriesCollection.appendData(...): more data than series to put them in");
}
int s; // index to select the "series"
for (s = 0; s < nDataPoints; s++)
valueHistory[s].enterData(newestAt,newData[s]);
fireSeriesChanged();
}

public TimePeriod getNewestTime()
{
return pointsInTime[newestAt];
}

public TimePeriod getOldestTime()
{
return pointsInTime[oldestAt];
}

// getXxx() ftns can ignore the "series" argument:
public Number getXValue(int series, int item)
{
TimePeriod tp = null;
tp = pointsInTime[translateGet(item)];
return new Long(getX(tp));
}

public float getY(int series, int item)
{
ValueSequence values = valueHistory[series];
}

public Number getYValue(int series, int item)
{
return new Float(getY(series, item));
}

public Number getStartXValue(int series, int item)
{
TimePeriod tp = null;
tp = pointsInTime[translateGet(item)];
return new Long(tp.getStart(workingCalendar));
}

public Number getEndXValue(int series, int item)
{
TimePeriod tp = pointsInTime[translateGet(item)];
return new Long(tp.getEnd(workingCalendar));
}

public Number getStartYValue(int series, int item)
{
return getYValue(series,item);
}

public Number getEndYValue(int series, int item)
{
return getYValue(series,item);
}

/* // "Extras" found useful when analyzing/verifying class behavior:
public Number getUntranslatedXValue(int series, int item)
{
return super.getXValue(series, item);
}

public float getUntranslatedY(int series, int item)
{
return super.getY(series, item);
} */

public String getSeriesName(int series)
{
return seriesNames[series];
}

protected void fireSeriesChanged()
{
seriesChanged(new SeriesChangeEvent(this));
}

/* The next 3 functions override the base-class implementation of
* the DomainInfo interface. Using saved limits (updated by
* each updateTime() call), improves performance.
*/
public Range getDomainRange()
{
if(domainRange == null)
findDomainLimits();
return domainRange;
}

/**
* Returns the minimum value in the dataset (or null if all the values in
* the domain are null).
*
* @return the minimum value.
*/
public Number getMinimumDomainValue()
{
//return new Long(getOldestTime().getStart(workingCalendar));
return domainStart; // a Long kept updated by advanceTime()
}
/**
* Returns the maximum value in the dataset (or null if all the values in
* the domain are null).
*
* @return the maximum value.
*/
public Number getMaximumDomainValue()
{
//return new Long(getNewestTime().getEnd(workingCalendar));
return domainEnd; // a Long kept updated by advanceTime()
}

private long getX(TimePeriod period)
{
long result = 0L;
switch (position)
{
case (START) : result = period.getStart(workingCalendar); break;
case (MIDDLE) : result = period.getMiddle(workingCalendar); break;
case (END) : result = period.getEnd(workingCalendar); break;
default: result = period.getMiddle(workingCalendar);
}
return result;
}


/* The next 3 functions implement the RangeInfo interface.
* Using saved limits (updated by each updateTime() call) significantly
* improves performance. WARNING: this code makes the simplifying assumption
* that data is never negative. Expand as needed for the general case.
***/

public Number getMinimumRangeValue()
{
return minValue;
}

public Number getMaximumRangeValue()
{
if (maxValue == null)
maxValue = new Float(findMaxValue());
return maxValue;
}
public Range getValueRange()
{
if (valueRange == null)
{
Float maxV = (Float)getMaximumRangeValue();
double max = maxV.doubleValue();
valueRange = new Range(0.0, max);
}
return valueRange;
}
}

===============================================
Modification to XYPlot.render():

public void render(Graphics2D g2, Rectangle2D dataArea,
ChartRenderingInfo info, CrosshairInfo crosshairInfo) {

// now get the data and plot it (the visual representation will depend
// on the renderer that has been set)...
XYDataset data = this.getXYDataset();

if (data != null)
{
synchronized(data) // lock the dataset object while reading it
{

renderer.initialise(g2, dataArea, this, data, info);

ValueAxis domainAxis = getDomainAxis();
ValueAxis rangeAxis = getRangeAxis();
int seriesCount = data.getSeriesCount();
for (int series = 0; series < seriesCount; series++) {
int itemCount = data.getItemCount(series);
for (int item = 0; item < itemCount; item++) {
renderer.drawItem(g2, dataArea, info, this,
domainAxis, rangeAxis,
data, series, item,
crosshairInfo);

}
}
} // synchronized block ends here...

// draw vertical crosshair if required...

Jamey Johnston

Re: DynamicTimeSeriesCollection

Post by Jamey Johnston » Wed Jan 29, 2003 10:46 pm

I have tried to compile the code you listed above. I noticed the getY method did not return a Value. I assume the return should be "return values.getData(item);")

Also you left out brackets "" on the following lines:

dataPoints = 0.0f;
seriesNames = "";
pointsInTime = pointsInTime[i-1].next();
valueHistory[seriesNumber].enterData(i, values);


Lines should be (I assume):

dataPoints = 0.0f;
seriesNames = "";
pointsInTime = pointsInTime[i-1].next();
valueHistory[seriesNumber].enterData(i, values);

If this is not correct please advise.

Jamey Johnston

Re: DynamicTimeSeriesCollection

Post by Jamey Johnston » Wed Jan 29, 2003 10:47 pm

Well as I can see from my own reply the phorum strips the "[ i ]" from the code.

Jamey Johnston

Re: DynamicTimeSeriesCollection

Post by Jamey Johnston » Wed Jan 29, 2003 10:55 pm

Lines should be (I assume):

dataPoints[ i ] = 0.0f;
seriesNames[ i ] = "";
pointsInTime[ i ] = pointsInTime[i-1].next();
valueHistory[seriesNumber].enterData(i, values[ i ]);

Irv Thomae

Re: DynamicTimeSeriesCollection

Post by Irv Thomae » Wed Jan 29, 2003 11:11 pm

Jamey,
Apparently I lost a line when cutting and pasting to the browser window.

For getY, the missing line should read :

return values.getData(translateGet(item));
The call to translateGet() is essential for correct operation in a dynamic situation, so I'm really embarassed that it got snipped inadvertently.

Thanks also for noticing that the phorum mangles expressions in square brackets, which I hadn't previously noticed - I think I'd better email the original file to David, and let him find a way to post it in _readable_ form.

Locked