As mentioned in some of my previous posts, my application typically involves plotting three TimeSeries at once, each representing 24 hours' worth of minute-by-minute data, i.e. 1440 data points. We also need to switch the plot rapidly, among several different sets of (3 * 1440) data points. The process of reloading a TimeSeriesCollection (from data arrays already in memory, so I/O speed was irrelevant) was typically taking 9 or 10 seconds.
Reasoning that the current TimeSeriesCollection class contained significant redundancy, I decided to experiment with an alternative implementation based on a single array of TimePeriod objects, paralleled by one simple array for each data series. The resulting speed-up was even better than I had hoped: the data-set switch described above now takes less than 1.5 seconds from selecting new data to its appearance on the screen..
I'll paste in my "FastTimeSeriesCollection" code below. Note that although this class does have a "historyCount" member, it doesn't actually support dynamic data (i.e., the ability to append data representing newer TimePeriods, with the oldest data being discarded to make room for it.) That behavior is implemented in a subclass which I will post as soon as the subclass has been more fully tested.
Irv Thomae
========== FastTimeSeriesCollection.java ===============
/* FastTimeSeriesCollection.java V1.0 I. H. Thomae 11-21-2002
*
* 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:
* 20-Nov-2002: Initial version completed (without support for dynamic plots)
*
* A replacement for JFreeChart's TimeSeriesCollection _and_
* BasicTimeSeries classes, for better performance and reduced memory
* usage. All series must represent the same total time span, and
* be of the same length. Given that constraint, a single array of
* TimePeriod objects can hold the time stamps for all data points.
* Also, each series is represented by a simple array of values,
* rather than by distinct BasicTimeSeries, each in turn containing
* an ArrayList of (time,value) pairs.
*
* This class supports data which are not being dynamically updated in real
* time. For that situation, use DynamicTimeSeriesCollection, a subclass.
*/
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 FastTimeSeriesCollection extends AbstractSeriesDataset
implements IntervalXYDataset, DomainInfo {
/** 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];
}
public void enterData(int index, float value)
{
dataPoints[index] = value;
}
public float getData(int index)
{
return dataPoints[index];
}
}
protected ValueSequence[] valueSet;
/** 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;
//
/**
* 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 FastTimeSeriesCollection(int nSeries, int nMoments)
{
this(nSeries, nMoments, new Millisecond(), TimeZone.getDefault());
}
/**
* 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 FastTimeSeriesCollection(int nSeries, int nMoments, TimeZone zone)
{
this(nSeries, nMoments, new Millisecond(), zone);
}
public FastTimeSeriesCollection(int nSeries, int nMoments, TimePeriod timeSample)
{
this(nSeries, nMoments, timeSample, TimeZone.getDefault());
}
public FastTimeSeriesCollection(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 = "";
valueSet = 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.
*/
public void setTimeBase(TimePeriod start)
{
if (pointsInTime[0] != null) return;
pointsInTime[0] = start;
for (int i = 1; i < historyCount; i++)
{
pointsInTime = pointsInTime[i-1].next();
}
}
public int getPosition()
{
return position;
}
public void setPosition(int position)
{
this.position = position;
}
/*
* If the pointsInTime array is empty, it can be filled from the first
* BasicTimeSeries. (If that BST's length is less than "nMoments", we fill
* sequentially; if it exceeds nMoments, excess data is ignored.)
*/
public void addOthSeries(BasicTimeSeries series)
{
int i;
int copyLength = historyCount;
boolean fillNeeded = false;
if (series.getItemCount() < historyCount)
{
copyLength = series.getItemCount();
fillNeeded = true;
}
for (i = 0; i < copyLength; i++)
pointsInTime = series.getTimePeriod(i);
if (fillNeeded)
{
for (i = copyLength; i < historyCount; i++)
pointsInTime = pointsInTime[i-1].next();
}
addSeries(series, 0);
fireDatasetChanged();
}
/*
* Add a data sequence taken from a standard BasicTimeSeries:
*/
public void addSeries(BasicTimeSeries series, int seriesNumber)
{
if (series == null)
{
throw new IllegalArgumentException(
"TimeSeriesDataset.addSeries(...): cannot add null series.");
}
if (seriesNumber > valueSet.length)
{
throw new IllegalArgumentException(
"TimeSeriesDataset.addSeries(...): cannot add more series than specified in c'tor");
}
if (valueSet[seriesNumber] == null)
{
valueSet[seriesNumber] = new ValueSequence(historyCount);
seriesCount++;
} // But if that series array already exists, just overwrite its contents
for (int i = 0; i < historyCount; i++)
{
valueSet[seriesNumber].enterData(i, (series.getValue(i).floatValue()));
}
seriesNames[seriesNumber] = series.getName();
fireDatasetChanged();
}
/*
* Add a data sequence as a simple array of floats:
*
*/
public void addSeries(float[] values, int seriesNumber, String seriesName)
{
if (values == null)
{
throw new IllegalArgumentException (
"TimeSeriesDataset.addSeries(...): cannot add null array of values.");
}
if (seriesNumber > valueSet.length)
{
throw new IllegalArgumentException(
"TimeSeriesDataset.addSeries(...): cannot add more series than specified in c'tor");
}
if (valueSet[seriesNumber] == null)
{
valueSet[seriesNumber] = new ValueSequence(historyCount);
seriesCount++;
} // But if that series array already exists, just overwrite its contents
for (int i = 0; i < historyCount; i++)
{ // deep copy from values[],so the caller can safely discard that array
valueSet[seriesNumber].enterData(i, values);
}
if (seriesName != null)
seriesNames[seriesNumber] = seriesName;
fireDatasetChanged();
}
// If planning to add values individually, use the following to set
// the series names:
public void setSeriesName(int seriesNumber, String newName)
{
seriesNames[seriesNumber] = newName;
}
public void addValue(int seriesNumber, int index, float value)
{
if (seriesNumber > valueSet.length)
{
throw new IllegalArgumentException(
"TimeSeriesDataset.addValue(...): series #" + seriesNumber + "unspecified in c'tor");
}
if (valueSet[seriesNumber] == null)
{
valueSet[seriesNumber] = new ValueSequence(historyCount);
seriesCount++;
} // But if that series array already exists, just overwrite its contents
valueSet[seriesNumber].enterData(index,value);
fireDatasetChanged();
}
/**
* 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;
}
public Number getXValue(int series, int item)
{ // again, we can ignore the series number:
TimePeriod tp = pointsInTime[item];
return new Long(getX(tp));
}
public float getY(int series, int item)
{
ValueSequence values = valueSet[series];
return values.getData(item);
}
/**
* Returns the y-value for the specified series and item.
*
* @param series The series (zero-based index).
* @param item The item (zero-based index).
*
* @return the y-value for the specified series and item.
*/
public Number getYValue(int series, int item)
{
return new Float(getY(series, item));
}
/**
* Returns the starting X value for the specified series and item.
*
* @param series The series (zero-based index).
* @param item The item (zero-based index).
*
* @return the starting X value for the specified series and item.
*/
public Number getStartXValue(int series, int item)
{
TimePeriod tp = pointsInTime[item];
return new Long(tp.getStart(workingCalendar));
}
/**
* Returns the ending X value for the specified series and item.
*
* @param series The series (zero-based index).
* @param item The item (zero-based index).
*
* @return the ending X value for the specified series and item.
*/
public Number getEndXValue(int series, int item)
{
TimePeriod tp = pointsInTime[item];
return new Long(tp.getEnd(workingCalendar));
}
/**
* Returns the range of the values in the series domain.
*
* @return the range.
*/
public Range getDomainRange()
{
TimePeriod start = pointsInTime[0];
TimePeriod end = pointsInTime[historyCount - 1];
long startL = start.getStart(workingCalendar);
long endL = end.getEnd(workingCalendar);
return new Range( (double)startL, (double)endL);
}
public Number getStartYValue(int series, int item) {
return getYValue(series, item);
}
public Number getEndYValue(int series, int item) {
return getYValue(series, item);
}
public String getSeriesName(int series)
{
return seriesNames[series];
}
/**
* 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(pointsInTime[0].getStart(workingCalendar));
}
/**
* 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(pointsInTime[historyCount - 1].getEnd(workingCalendar));
}
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;
}
}
Improving Performance for Time-Series Plots
Re: Improving Performance for Time-Series Plots
Hi
There are compilation problems, it's propably old version,
Regards,
JG
There are compilation problems, it's propably old version,
Regards,
JG