dynamic candlestick update

Discussion about JFreeChart related to stockmarket charts.
Locked
gigaplex
Posts: 2
Joined: Wed Mar 29, 2006 5:45 am
Location: US
Contact:

dynamic candlestick update

Post by gigaplex » Wed Mar 29, 2006 8:35 am

Note: this message is also in the general jfreechart forum

I am wanting to update the data in the candlestick dataset and have the chart reflect the changes when I do this. It looks like it is currently read-only. I may put in the time and add the code to do this so does anyone have any ideas on this before I get started? David, you said you had some good ideas in extending the candlestick chart, could you elaborate on that? I am somewhat new to the API right now but have the basics down.

Also, if anyone else is interested in helping with this please let me know.

crab bydog
Posts: 1
Joined: Sat Dec 09, 2006 4:40 pm

yep

Post by crab bydog » Sat Dec 09, 2006 4:43 pm

I'd be interested in any further discussion or knowledge of this.

Basically i want to make use of jfreechart as a realtime type chart for stcok data, dynamically updating it nad addng a cross hair and price display.


Anything on this in the developer guide?

vuj2010
Posts: 11
Joined: Fri Apr 07, 2006 11:54 pm

Post by vuj2010 » Sun Dec 10, 2006 8:10 pm

Try this. I modified the DefaultOHLCDataset, DefaultHighLowDataset, and TimeSeries as follows:

Then everything will be as normal, and you just addOrUpdate and get the realtime chart.

I hope David can incorporate so I don't have to modify for every version.
Thank you for providing such a great product. I love it.

Good luck.

J. Vu


/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
*
* Project Info: http://www.jfree.org/jfreechart/index.html
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
* [Java is a trademark or registered trademark of Sun Microsystems, Inc.
* in the United States and other countries.]
*
* -----------------------
* DefaultOHLCDataset.java
* -----------------------
* (C) Copyright 2003, 2004, by Object Refinery Limited.
*
* Original Author: David Gilbert (for Object Refinery Limited);
* Contributor(s): -;
*
* $Id: DefaultOHLCDataset.java,v 1.5.2.1 2005/10/25 21:36:51 mungady Exp $
*
* Changes
* -------
* 03-Dec-2003 : Version 1 (DG);
* 05-May-2004 : Now extends AbstractXYDataset (DG);
* 15-Jul-2004 : Switched getX() with getXValue() and getY() with
* getYValue() (DG);
* 29-Apr-2005 : Added equals() method (DG);
*
*/

package org.jfree.data.xy;

import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;

/**
* A simple implementation of the {@link OHLCDataset} interface. This
* implementation supports only one series.
*/
public class DefaultOHLCDataset extends AbstractXYDataset
implements OHLCDataset {

/** The series key. */
private Comparable key;

/** Storage for the data items. */
private List<OHLCDataItem> data=new ArrayList();

/**
* Creates a new dataset.
*
* @param key the series key.
* @param data the data items.
*/
public DefaultOHLCDataset(Comparable key, OHLCDataItem[] idata) {
this.key = key;

for (int i=0;i<idata.length;++i){
this.data.add(idata);
}

}
public void add(
OHLCDataItem idata) {
this.data.add(idata);
}

/**
* Returns the series key.
*
* @param series the series index (ignored).
*
* @return The series key.
*/
public Comparable getSeriesKey(int series) {
return this.key;
}

/**
* Returns the x-value for a data item.
*
* @param series the series index (ignored).
* @param item the item index (zero-based).
*
* @return The x-value.
*/
public Number getX(int series, int item) {
Date titem= this.data.get(item).getDate();
return new Long(titem.getTime());
}

/**
* Returns the x-value for a data item as a date.
*
* @param series the series index (ignored).
* @param item the item index (zero-based).
*
* @return The x-value as a date.
*/
public Date getXDate(int series, int item) {
return this.data.get(item).getDate();
}

/**
* Returns the y-value.
*
* @param series the series index (ignored).
* @param item the item index (zero-based).
*
* @return The y value.
*/
public Number getY(int series, int item) {
return getClose(series, item);
}

/**
* Returns the high value.
*
* @param series the series index (ignored).
* @param item the item index (zero-based).
*
* @return The high value.
*/
public Number getHigh(int series, int item) {
return this.data.get(item).getHigh();
}

/**
* Returns the high-value (as a double primitive) for an item within a
* series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The high-value.
*/
public double getHighValue(int series, int item) {
double result = Double.NaN;
Number high = getHigh(series, item);
if (high != null) {
result = high.doubleValue();
}
return result;
}

/**
* Returns the low value.
*
* @param series the series index (ignored).
* @param item the item index (zero-based).
*
* @return The low value.
*/
public Number getLow(int series, int item) {
return this.data.get(item).getLow();
}

/**
* Returns the low-value (as a double primitive) for an item within a
* series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The low-value.
*/
public double getLowValue(int series, int item) {
double result = Double.NaN;
Number low = getLow(series, item);
if (low != null) {
result = low.doubleValue();
}
return result;
}

/**
* Returns the open value.
*
* @param series the series index (ignored).
* @param item the item index (zero-based).
*
* @return The open value.
*/
public Number getOpen(int series, int item) {
return this.data.get(item).getOpen();
}

/**
* Returns the open-value (as a double primitive) for an item within a
* series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The open-value.
*/
public double getOpenValue(int series, int item) {
double result = Double.NaN;
Number open = getOpen(series, item);
if (open != null) {
result = open.doubleValue();
}
return result;
}

/**
* Returns the close value.
*
* @param series the series index (ignored).
* @param item the item index (zero-based).
*
* @return The close value.
*/
public Number getClose(int series, int item) {
return this.data.get(item).getClose();
}

/**
* Returns the close-value (as a double primitive) for an item within a
* series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The close-value.
*/
public double getCloseValue(int series, int item) {
double result = Double.NaN;
Number close = getClose(series, item);
if (close != null) {
result = close.doubleValue();
}
return result;
}

/**
* Returns the trading volume.
*
* @param series the series index (ignored).
* @param item the item index (zero-based).
*
* @return The trading volume.
*/
public Number getVolume(int series, int item) {
return this.data.get(item).getVolume();
}

/**
* Returns the volume-value (as a double primitive) for an item within a
* series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The volume-value.
*/
public double getVolumeValue(int series, int item) {
double result = Double.NaN;
Number volume = getVolume(series, item);
if (volume != null) {
result = volume.doubleValue();
}
return result;
}

/**
* Returns the series count.
*
* @return 1.
*/
public int getSeriesCount() {
return 1;
}

/**
* Returns the item count for the specified series.
*
* @param series the series index (ignored).
*
* @return The item count.
*/
public int getItemCount(int series) {
return this.data.size();
}

/**
* Sorts the data into ascending order by date.
*/
public void sortDataByDate() {
Collections.sort(this.data, new DateComparator());
}

/**
* Tests this instance for equality with an arbitrary object.
*
* @param obj the object (<code>null</code> permitted).
*
* @return A boolean.
*/
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DefaultOHLCDataset)) {
return false;
}
DefaultOHLCDataset that = (DefaultOHLCDataset) obj;
if (!this.key.equals(that.key)) {
return false;
}
if (!this.data.equals(that.data)) {
return false;
}
return true;
}

private class DateComparator implements Comparator{
OHLCDataItem item1, item2;
Date date1, date2;
int dateCompare;
public int compare(Object object1, Object object2){
item1=(OHLCDataItem)object1;
item2=(OHLCDataItem)object2;
date1=item1.getDate();
date2=item2.getDate();

return (date1.compareTo(date2));

}
}
}

========================================


/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
*
* Project Info: http://www.jfree.org/jfreechart/index.html
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
* [Java is a trademark or registered trademark of Sun Microsystems, Inc.
* in the United States and other countries.]
*
* --------------------------
* DefaultHighLowDataset.java
* --------------------------
* (C) Copyright 2002-2005, by Object Refinery Limited.
*
* Original Author: David Gilbert (for Object Refinery Limited);
* Contributor(s): -;
*
* $Id: DefaultHighLowDataset.java,v 1.6.2.1 2005/10/25 21:36:51 mungady Exp $
*
* Changes
* -------
* 21-Mar-2002 : Version 1 (DG);
* 07-Oct-2002 : Fixed errors reported by Checkstyle (DG);
* 06-May-2004 : Now extends AbstractXYDataset and added new methods from
* HighLowDataset (DG);
* 15-Jul-2004 : Switched getX() with getXValue() and getY() with
* getYValue() (DG);
*
*/

package org.jfree.data.xy;

import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

/**
* A simple implementation of the {@link OHLCDataset}.
*/
public class DefaultHighLowDataset extends AbstractXYDataset
implements OHLCDataset {

/** The series key. */
private Comparable seriesKey;

/** Storage for the dates. */
private List<Date> date=new ArrayList();

/** Storage for the high values. */
private List<Number> high=new ArrayList();

/** Storage for the low values. */
private List<Number> low=new ArrayList();

/** Storage for the open values. */
private List<Number> open=new ArrayList();

/** Storage for the close values. */
private List<Number> close=new ArrayList();

/** Storage for the volume values. */
private List<Number> volume=new ArrayList();

/**
* Constructs a new high/low/open/close dataset.
* <p>
* The current implementation allows only one series in the dataset.
* This may be extended in a future version.
*
* @param seriesKey the key for the series.
* @param date the dates.
* @param high the high values.
* @param low the low values.
* @param open the open values.
* @param close the close values.
* @param volume the volume values.
*/
public DefaultHighLowDataset(Comparable seriesKey,
Date[] idate,
double[] ihigh, final double[] ilow,
double[] iopen, final double[] iclose,
double[] ivolume) {

this.seriesKey = seriesKey;
for (int i=0;i<idate.length;++i){
this.date.add(idate);
this.high.add(ihigh);
this.low.add(ilow);
this.open.add(iopen);
this.close.add(iclose);
this.volume.add(ivolume);

}

}



public DefaultHighLowDataset(Comparable seriesKey) {

this.seriesKey = seriesKey;
}

public void add(Date idate,
double ihigh, double ilow,
double iopen, double iclose,
double ivolume) {
this.date.add(idate);
this.high.add(ihigh);
this.low.add(ilow);
this.open.add(iopen);
this.close.add(iclose);
this.volume.add(ivolume);
}

public DefaultHighLowDataset compressData(int mult){
DefaultHighLowDataset compress = new DefaultHighLowDataset(this.seriesKey);
int iseries = getSeriesCount();

if (this.date.size()>= 2){

long milliSeconds = (this.date.get(1).getTime()-this.date.get(0).getTime())*mult;

int index=0;
while (index < this.date.size()){
if ((this.date.get(index).getTime()%milliSeconds) == 0){
Date date=getXDate(iseries,index);
double dOpen=getOpenValue(iseries,index);
double dHigh=getHighValue(iseries,index);
double dLow=getLowValue(iseries,index);
double dVolume=getVolumeValue(iseries,index);

for(int i=index+1;i<index+mult;++i){
double dNewHigh=getHighValue(iseries,i);
double dNewLow=getLowValue(iseries,i);
dVolume+=getVolumeValue(iseries,i);

if (dNewHigh>dHigh)dHigh=dNewHigh;
if (dNewLow<dLow)dLow=dNewLow;
}
index+=(mult-1);
double dClose=getCloseValue(iseries,index);
compress.add(date, dHigh, dLow, dOpen, dClose,dVolume);
index++;
}
else {
index++;
}
}

}


return compress;
}


public void addOrUpdate(Date idate,
double ihigh, double ilow,
double iopen, double iclose,
double ivolume) {

int index = Collections.binarySearch(this.date, idate);
if (index >= 0) {
this.date.set(index,idate);
this.high.set(index,ihigh);
this.low.set(index,ilow);
this.open.set(index,iopen);
this.close.set(index,iclose);
this.volume.set(index,ivolume);
} else {
this.add(idate, ihigh, ilow, iopen, iclose, ivolume);

}
}

public void removeLast(){
int dataSize=this.date.size()-1;
if (dataSize >=0 ){
this.date.remove(dataSize);
this.high.remove(dataSize);
this.low.remove(dataSize);
this.open.remove(dataSize);
this.close.remove(dataSize);
this.volume.remove(dataSize);
}
}


/**
* Returns the for the series stored in this dataset.
*
* @param i the index of the series. Currently ignored.
*
* @return The key for this series.
*/
public Comparable getSeriesKey(int i) {
return this.seriesKey;
}

/**
* Returns the x-value for one item in a series. The value returned is a
* <code>Long</code> instance generated from the underlying
* <code>Date</code> object.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The x-value.
*/
public Number getX(int series, int item) {
Date tdate= (Date)this.date.get(item);
return new Long(tdate.getTime());
}

/**
* Returns the x-value for one item in a series, as a Date.
* <p>
* This method is provided for convenience only.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The x-value as a Date.
*/
public Date getXDate(int series, int item) {
return (Date)this.date.get(item);
}

/**
* Returns the y-value for one item in a series.
* <p>
* This method (from the {@link XYDataset} interface) is mapped to the
* {@link #getCloseValue(int, int)} method.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The y-value.
*/
public Number getY(int series, int item) {
return getClose(series, item);
}

/**
* Returns the high-value for one item in a series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The high-value.
*/
public Number getHigh(int series, int item) {
return (Number) this.high.get(item);
}

/**
* Returns the high-value (as a double primitive) for an item within a
* series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The high-value.
*/
public double getHighValue(int series, int item) {
double result = Double.NaN;
Number high = getHigh(series, item);
if (high != null) {
result = high.doubleValue();
}
return result;
}

/**
* Returns the low-value for one item in a series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The low-value.
*/
public Number getLow(int series, int item) {
return (Number) this.low.get(item);
}

/**
* Returns the low-value (as a double primitive) for an item within a
* series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The low-value.
*/
public double getLowValue(int series, int item) {
double result = Double.NaN;
Number low = getLow(series, item);
if (low != null) {
result = low.doubleValue();
}
return result;
}

/**
* Returns the open-value for one item in a series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The open-value.
*/
public Number getOpen(int series, int item) {
return (Number) this.open.get(item);
}

/**
* Returns the open-value (as a double primitive) for an item within a
* series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The open-value.
*/
public double getOpenValue(int series, int item) {
double result = Double.NaN;
Number open = getOpen(series, item);
if (open != null) {
result = open.doubleValue();
}
return result;
}

/**
* Returns the close-value for one item in a series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The close-value.
*/
public Number getClose(int series, int item) {
return (Number) this.close.get(item);
}

/**
* Returns the close-value (as a double primitive) for an item within a
* series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The close-value.
*/
public double getCloseValue(int series, int item) {
double result = Double.NaN;
Number close = getClose(series, item);
if (close != null) {
result = close.doubleValue();
}
return result;
}

/**
* Returns the volume-value for one item in a series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The volume-value.
*/
public Number getVolume(int series, int item) {
return (Number) this.volume.get(item);
}

/**
* Returns the volume-value (as a double primitive) for an item within a
* series.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The volume-value.
*/
public double getVolumeValue(int series, int item) {
double result = Double.NaN;
Number volume = getVolume(series, item);
if (volume != null) {
result = volume.doubleValue();
}
return result;
}

/**
* Returns the number of series in the dataset.
* <p>
* This implementation only allows one series.
*
* @return The number of series.
*/
public int getSeriesCount() {
return 1;
}

/**
* Returns the number of items in the specified series.
*
* @param series the index (zero-based) of the series.
*
* @return The number of items in the specified series.
*/
public int getItemCount(int series) {
return this.date.size();
}

/**
* Constructs an array of Number objects from an array of doubles.
*
* @param data the double values to convert.
*
* @return The data as an array of Number objects.
*/
public static Number[] createNumberArray(double[] data) {
Number[] result = new Number[data.length];
for (int i = 0; i < data.length; i++) {
result = new Double(data);
}
return result;
}

}
============================================

/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-2006, by Object Refinery Limited and Contributors.
*
* Project Info: http://www.jfree.org/jfreechart/index.html
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
* [Java is a trademark or registered trademark of Sun Microsystems, Inc.
* in the United States and other countries.]
*
* ---------------
* TimeSeries.java
* ---------------
* (C) Copyright 2001-2006, by Object Refinery Limited.
*
* Original Author: David Gilbert (for Object Refinery Limited);
* Contributor(s): Bryan Scott;
*
* $Id: TimeSeries.java,v 1.10.2.8 2006/07/25 15:55:48 mungady Exp $
*
* Changes
* -------
* 11-Oct-2001 : Version 1 (DG);
* 14-Nov-2001 : Added listener mechanism (DG);
* 15-Nov-2001 : Updated argument checking and exceptions in add() method (DG);
* 29-Nov-2001 : Added properties to describe the domain and range (DG);
* 07-Dec-2001 : Renamed TimeSeries --> BasicTimeSeries (DG);
* 01-Mar-2002 : Updated import statements (DG);
* 28-Mar-2002 : Added a method add(TimePeriod, double) (DG);
* 27-Aug-2002 : Changed return type of delete method to void (DG);
* 04-Oct-2002 : Added itemCount and historyCount attributes, fixed errors
* reported by Checkstyle (DG);
* 29-Oct-2002 : Added series change notification to addOrUpdate() method (DG);
* 28-Jan-2003 : Changed name back to TimeSeries (DG);
* 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented
* Serializable (DG);
* 01-May-2003 : Updated equals() method (see bug report 727575) (DG);
* 14-Aug-2003 : Added ageHistoryCountItems method (copied existing code for
* contents) made a method and added to addOrUpdate. Made a
* public method to enable ageing against a specified time
* (eg now) as opposed to lastest time in series (BS);
* 15-Oct-2003 : Added fix for setItemCount method - see bug report 804425.
* Modified exception message in add() method to be more
* informative (DG);
* 13-Apr-2004 : Added clear() method (DG);
* 21-May-2004 : Added an extra addOrUpdate() method (DG);
* 15-Jun-2004 : Fixed NullPointerException in equals() method (DG);
* 29-Nov-2004 : Fixed bug 1075255 (DG);
* 17-Nov-2005 : Renamed historyCount --> maximumItemAge (DG);
* 28-Nov-2005 : Changed maximumItemAge from int to long (DG);
* 01-Dec-2005 : New add methods accept notify flag (DG);
* ------------- JFREECHART 1.0.0 ---------------------------------------------
* 24-May-2006 : Improved error handling in createCopy() methods (DG);
*
*/

package org.jfree.data.time;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.jfree.data.general.Series;
import org.jfree.data.general.SeriesChangeEvent;
import org.jfree.data.general.SeriesException;
import org.jfree.util.ObjectUtilities;

/**
* Represents a sequence of zero or more data items in the form (period, value).
*/
public class TimeSeries extends Series implements Cloneable, Serializable {

/** For serialization. */
private static final long serialVersionUID = -5032960206869675528L;

/** Default value for the domain description. */
protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time";

/** Default value for the range description. */
protected static final String DEFAULT_RANGE_DESCRIPTION = "Value";

/** A description of the domain. */
private String domain;

/** A description of the range. */
private String range;

/** The type of period for the data. */
protected Class timePeriodClass;

/** The list of data items in the series. */
protected List data;

/** The maximum number of items for the series. */
private int maximumItemCount;

/** The maximum age of items for the series. */
private long maximumItemAge;

/**
* Creates a new (empty) time series. By default, a daily time series is
* created. Use one of the other constructors if you require a different
* time period.
*
* @param name the series name (<code>null</code> not permitted).
*/
public TimeSeries(String name) {
this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
Day.class);
}

/**
* Creates a new (empty) time series with the specified name and class
* of {@link RegularTimePeriod}.
*
* @param name the series name (<code>null</code> not permitted).
* @param timePeriodClass the type of time period (<code>null</code> not
* permitted).
*/
public TimeSeries(String name, Class timePeriodClass) {
this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
timePeriodClass);
}

/**
* Creates a new time series that contains no data.
* <P>
* Descriptions can be specified for the domain and range. One situation
* where this is helpful is when generating a chart for the time series -
* axis labels can be taken from the domain and range description.
*
* @param name the name of the series (<code>null</code> not permitted).
* @param domain the domain description (<code>null</code> permitted).
* @param range the range description (<code>null</code> permitted).
* @param timePeriodClass the type of time period (<code>null</code> not
* permitted).
*/
public TimeSeries(String name, String domain, String range,
Class timePeriodClass) {
super(name);
this.domain = domain;
this.range = range;
this.timePeriodClass = timePeriodClass;
this.data = new java.util.ArrayList();
this.maximumItemCount = Integer.MAX_VALUE;
this.maximumItemAge = Long.MAX_VALUE;
}

/**
* Returns the domain description.
*
* @return The domain description (possibly <code>null</code>).
*
* @see #setDomainDescription(String)
*/
public String getDomainDescription() {
return this.domain;
}

/**
* Sets the domain description and sends a <code>PropertyChangeEvent</code>
* (with the property name <code>Domain</code>) to all registered
* property change listeners.
*
* @param description the description (<code>null</code> permitted).
*
* @see #getDomainDescription()
*/
public void setDomainDescription(String description) {
String old = this.domain;
this.domain = description;
firePropertyChange("Domain", old, description);
}

/**
* Returns the range description.
*
* @return The range description (possibly <code>null</code>).
*
* @see #setRangeDescription(String)
*/
public String getRangeDescription() {
return this.range;
}

/**
* Sets the range description and sends a <code>PropertyChangeEvent</code>
* (with the property name <code>Range</code>) to all registered listeners.
*
* @param description the description (<code>null</code> permitted).
*
* @see #getRangeDescription()
*/
public void setRangeDescription(String description) {
String old = this.range;
this.range = description;
firePropertyChange("Range", old, description);
}

/**
* Returns the number of items in the series.
*
* @return The item count.
*/
public int getItemCount() {
return this.data.size();
}

/**
* Returns the list of data items for the series (the list contains
* {@link TimeSeriesDataItem} objects and is unmodifiable).
*
* @return The list of data items.
*/
public List getItems() {
return Collections.unmodifiableList(this.data);
}

/**
* Returns the maximum number of items that will be retained in the series.
* The default value is <code>Integer.MAX_VALUE</code>.
*
* @return The maximum item count.
*
* @see #setMaximumItemCount(int)
*/
public int getMaximumItemCount() {
return this.maximumItemCount;
}

/**
* Sets the maximum number of items that will be retained in the series.
* If you add a new item to the series such that the number of items will
* exceed the maximum item count, then the FIRST element in the series is
* automatically removed, ensuring that the maximum item count is not
* exceeded.
*
* @param maximum the maximum (requires >= 0).
*
* @see #getMaximumItemCount()
*/
public void setMaximumItemCount(int maximum) {
if (maximum < 0) {
throw new IllegalArgumentException("Negative 'maximum' argument.");
}
this.maximumItemCount = maximum;
int count = this.data.size();
if (count > maximum) {
delete(0, count - maximum - 1);
}
}

/**
* Returns the maximum item age (in time periods) for the series.
*
* @return The maximum item age.
*
* @see #setMaximumItemAge(long)
*/
public long getMaximumItemAge() {
return this.maximumItemAge;
}

/**
* Sets the number of time units in the 'history' for the series. This
* provides one mechanism for automatically dropping old data from the
* time series. For example, if a series contains daily data, you might set
* the history count to 30. Then, when you add a new data item, all data
* items more than 30 days older than the latest value are automatically
* dropped from the series.
*
* @param periods the number of time periods.
*
* @see #getMaximumItemAge()
*/
public void setMaximumItemAge(long periods) {
if (periods < 0) {
throw new IllegalArgumentException("Negative 'periods' argument.");
}
this.maximumItemAge = periods;
removeAgedItems(true); // remove old items and notify if necessary
}

/**
* Returns the time period class for this series.
* <p>
* Only one time period class can be used within a single series (enforced).
* If you add a data item with a {@link Year} for the time period, then all
* subsequent data items must also have a {@link Year} for the time period.
*
* @return The time period class (never <code>null</code>).
*/
public Class getTimePeriodClass() {
return this.timePeriodClass;
}

/**
* Returns a data item for the series.
*
* @param index the item index (zero-based).
*
* @return The data item.
*
* @see #getDataItem(RegularTimePeriod)
*/
public TimeSeriesDataItem getDataItem(int index) {
return (TimeSeriesDataItem) this.data.get(index);
}

/**
* Returns the data item for a specific period.
*
* @param period the period of interest (<code>null</code> not allowed).
*
* @return The data item matching the specified period (or
* <code>null</code> if there is no match).
*
* @see #getDataItem(int)
*/
public TimeSeriesDataItem getDataItem(RegularTimePeriod period) {
if (period == null) {
throw new IllegalArgumentException("Null 'period' argument");
}
TimeSeriesDataItem dummy = new TimeSeriesDataItem(period,
Integer.MIN_VALUE);
int index = Collections.binarySearch(this.data, dummy);
if (index >= 0) {
return (TimeSeriesDataItem) this.data.get(index);
}
else {
return null;
}
}

/**
* Returns the time period at the specified index.
*
* @param index the index of the data item.
*
* @return The time period.
*/
public RegularTimePeriod getTimePeriod(int index) {
return getDataItem(index).getPeriod();
}

/**
* Returns a time period that would be the next in sequence on the end of
* the time series.
*
* @return The next time period.
*/
public RegularTimePeriod getNextTimePeriod() {
RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
return last.next();
}

/**
* Returns a collection of all the time periods in the time series.
*
* @return A collection of all the time periods.
*/
public Collection getTimePeriods() {
Collection result = new java.util.ArrayList();
for (int i = 0; i < getItemCount(); i++) {
result.add(getTimePeriod(i));
}
return result;
}

/**
* Returns a collection of time periods in the specified series, but not in
* this series, and therefore unique to the specified series.
*
* @param series the series to check against this one.
*
* @return The unique time periods.
*/
public Collection getTimePeriodsUniqueToOtherSeries(TimeSeries series) {

Collection result = new java.util.ArrayList();
for (int i = 0; i < series.getItemCount(); i++) {
RegularTimePeriod period = series.getTimePeriod(i);
int index = getIndex(period);
if (index < 0) {
result.add(period);
}
}
return result;

}

/**
* Returns the index for the item (if any) that corresponds to a time
* period.
*
* @param period the time period (<code>null</code> not permitted).
*
* @return The index.
*/
public int getIndex(RegularTimePeriod period) {
if (period == null) {
throw new IllegalArgumentException("Null 'period' argument.");
}
TimeSeriesDataItem dummy = new TimeSeriesDataItem(
period, Integer.MIN_VALUE);
return Collections.binarySearch(this.data, dummy);
}

/**
* Returns the value at the specified index.
*
* @param index index of a value.
*
* @return The value (possibly <code>null</code>).
*/
public Number getValue(int index) {
return getDataItem(index).getValue();
}

/**
* Returns the value for a time period. If there is no data item with the
* specified period, this method will return <code>null</code>.
*
* @param period time period (<code>null</code> not permitted).
*
* @return The value (possibly <code>null</code>).
*/
public Number getValue(RegularTimePeriod period) {

int index = getIndex(period);
if (index >= 0) {
return getValue(index);
}
else {
return null;
}

}

/**
* Adds a data item to the series and sends a
* {@link org.jfree.data.general.SeriesChangeEvent} to all registered
* listeners.
*
* @param item the (timeperiod, value) pair (<code>null</code> not
* permitted).
*/
public void add(TimeSeriesDataItem item) {
add(item, true);
}

/**
* Adds a data item to the series and sends a
* {@link org.jfree.data.general.SeriesChangeEvent} to all registered
* listeners.
*
* @param item the (timeperiod, value) pair (<code>null</code> not
* permitted).
* @param notify notify listeners?
*/
public void add(TimeSeriesDataItem item, boolean notify) {
if (item == null) {
throw new IllegalArgumentException("Null 'item' argument.");
}
if (!item.getPeriod().getClass().equals(this.timePeriodClass)) {
StringBuffer b = new StringBuffer();
b.append("You are trying to add data where the time period class ");
b.append("is ");
b.append(item.getPeriod().getClass().getName());
b.append(", but the TimeSeries is expecting an instance of ");
b.append(this.timePeriodClass.getName());
b.append(".");
throw new SeriesException(b.toString());
}

// make the change (if it's not a duplicate time period)...
boolean added = false;
int count = getItemCount();
if (count == 0) {
this.data.add(item);
added = true;
}
else {
RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
if (item.getPeriod().compareTo(last) > 0) {
this.data.add(item);
added = true;
}
else {
int index = Collections.binarySearch(this.data, item);
if (index < 0) {
this.data.add(-index - 1, item);
added = true;
}
else {
StringBuffer b = new StringBuffer();
b.append("You are attempting to add an observation for ");
b.append("the time period ");
b.append(item.getPeriod().toString());
b.append(" but the series already contains an observation");
b.append(" for that time period. Duplicates are not ");
b.append("permitted. Try using the addOrUpdate() method.");
throw new SeriesException(b.toString());
}
}
}
if (added) {
// check if this addition will exceed the maximum item count...
if (getItemCount() > this.maximumItemCount) {
this.data.remove(0);
}

removeAgedItems(false); // remove old items if necessary, but
// don't notify anyone, because that
// happens next anyway...
if (notify) {
fireSeriesChanged();
}
}

}

/**
* Adds a new data item to the series and sends a {@link SeriesChangeEvent}
* to all registered listeners.
*
* @param period the time period (<code>null</code> not permitted).
* @param value the value.
*/
public void add(RegularTimePeriod period, double value) {
// defer argument checking...
add(period, value, true);
}

/**
* Adds a new data item to the series and sends a {@link SeriesChangeEvent}
* to all registered listeners.
*
* @param period the time period (<code>null</code> not permitted).
* @param value the value.
* @param notify notify listeners?
*/
public void add(RegularTimePeriod period, double value, boolean notify) {
// defer argument checking...
TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
add(item, notify);
}

/**
* Adds a new data item to the series and sends
* a {@link org.jfree.data.general.SeriesChangeEvent} to all registered
* listeners.
*
* @param period the time period (<code>null</code> not permitted).
* @param value the value (<code>null</code> permitted).
*/
public void add(RegularTimePeriod period, Number value) {
// defer argument checking...
add(period, value, true);
}

/**
* Adds a new data item to the series and sends
* a {@link org.jfree.data.general.SeriesChangeEvent} to all registered
* listeners.
*
* @param period the time period (<code>null</code> not permitted).
* @param value the value (<code>null</code> permitted).
* @param notify notify listeners?
*/
public void add(RegularTimePeriod period, Number value, boolean notify) {
// defer argument checking...
TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
add(item, notify);
}

/**
* Updates (changes) the value for a time period. Throws a
* {@link SeriesException} if the period does not exist.
*
* @param period the period (<code>null</code> not permitted).
* @param value the value (<code>null</code> permitted).
*/
public void update(RegularTimePeriod period, Number value) {
TimeSeriesDataItem temp = new TimeSeriesDataItem(period, value);
int index = Collections.binarySearch(this.data, temp);
if (index >= 0) {
TimeSeriesDataItem pair = (TimeSeriesDataItem) this.data.get(index);
pair.setValue(value);
fireSeriesChanged();
}
else {
throw new SeriesException(
"TimeSeries.update(TimePeriod, Number): period does not exist."
);
}

}

/**
* Updates (changes) the value of a data item.
*
* @param index the index of the data item.
* @param value the new value (<code>null</code> permitted).
*/
public void update(int index, Number value) {
TimeSeriesDataItem item = getDataItem(index);
item.setValue(value);
fireSeriesChanged();
}

/**
* Adds or updates data from one series to another. Returns another series
* containing the values that were overwritten.
*
* @param series the series to merge with this.
*
* @return A series containing the values that were overwritten.
*/
public TimeSeries addAndOrUpdate(TimeSeries series) {
TimeSeries overwritten = new TimeSeries("Overwritten values from: "
+ getKey(), series.getTimePeriodClass());
for (int i = 0; i < series.getItemCount(); i++) {
TimeSeriesDataItem item = series.getDataItem(i);
TimeSeriesDataItem oldItem = addOrUpdate(item.getPeriod(),
item.getValue());
if (oldItem != null) {
overwritten.add(oldItem);
}
}
return overwritten;
}

/**
* Adds or updates an item in the times series and sends a
* {@link org.jfree.data.general.SeriesChangeEvent} to all registered
* listeners.
*
* @param period the time period to add/update (<code>null</code> not
* permitted).
* @param value the new value.
*
* @return A copy of the overwritten data item, or <code>null</code> if no
* item was overwritten.
*/
public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
double value) {
return this.addOrUpdate(period, new Double(value));
}

/**
* Adds or updates an item in the times series and sends a
* {@link org.jfree.data.general.SeriesChangeEvent} to all registered
* listeners.
*
* @param period the time period to add/update (<code>null</code> not
* permitted).
* @param value the new value (<code>null</code> permitted).
*
* @return A copy of the overwritten data item, or <code>null</code> if no
* item was overwritten.
*/
public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
Number value) {

if (period == null) {
throw new IllegalArgumentException("Null 'period' argument.");
}
TimeSeriesDataItem overwritten = null;

TimeSeriesDataItem key = new TimeSeriesDataItem(period, value);
int index = Collections.binarySearch(this.data, key);
if (index >= 0) {
TimeSeriesDataItem existing
= (TimeSeriesDataItem) this.data.get(index);
overwritten = (TimeSeriesDataItem) existing.clone();
existing.setValue(value);
removeAgedItems(false); // remove old items if necessary, but
// don't notify anyone, because that
// happens next anyway...
fireSeriesChanged();
}
else {
this.data.add(-index - 1, new TimeSeriesDataItem(period, value));

// check if this addition will exceed the maximum item count...
if (getItemCount() > this.maximumItemCount) {
this.data.remove(0);
}

removeAgedItems(false); // remove old items if necessary, but
// don't notify anyone, because that
// happens next anyway...
fireSeriesChanged();
}
return overwritten;

}

/**
* Age items in the series. Ensure that the timespan from the youngest to
* the oldest record in the series does not exceed maximumItemAge time
* periods. Oldest items will be removed if required.
*
* @param notify controls whether or not a {@link SeriesChangeEvent} is
* sent to registered listeners IF any items are removed.
*/
public void removeAgedItems(boolean notify) {
// check if there are any values earlier than specified by the history
// count...
if (getItemCount() > 1) {
long latest = getTimePeriod(getItemCount() - 1).getSerialIndex();
boolean removed = false;
while ((latest - getTimePeriod(0).getSerialIndex())
>= this.maximumItemAge) {
this.data.remove(0);
removed = true;
}
if (removed && notify) {
fireSeriesChanged();
}
}
}

/**
* Age items in the series. Ensure that the timespan from the supplied
* time to the oldest record in the series does not exceed history count.
* oldest items will be removed if required.
*
* @param latest the time to be compared against when aging data.
* @param notify controls whether or not a {@link SeriesChangeEvent} is
* sent to registered listeners IF any items are removed.
*/
public void removeAgedItems(long latest, boolean notify) {
// check if there are any values earlier than specified by the history
// count...
if (getItemCount() > 1) {
while ((latest - getTimePeriod(0).getSerialIndex())
>= this.maximumItemAge) {
this.data.remove(0);
}
}
}

/**
* Removes all data items from the series and sends a
* {@link SeriesChangeEvent} to all registered listeners.
*/
public void clear() {
if (this.data.size() > 0) {
this.data.clear();
fireSeriesChanged();
}
}

/**
* Deletes the data item for the given time period and sends a
* {@link SeriesChangeEvent} to all registered listeners. If there is no
* item with the specified time period, this method does nothing.
*
* @param period the period of the item to delete (<code>null</code> not
* permitted).
*/
public void delete(RegularTimePeriod period) {
int index = getIndex(period);
if (index >= 0) {
this.data.remove(index);
fireSeriesChanged();
}
}

/**
* Deletes data from start until end index (end inclusive).
*
* @param start the index of the first period to delete.
* @param end the index of the last period to delete.
*/
public void delete(int start, int end) {
if (end < start) {
throw new IllegalArgumentException("Requires start <= end.");
}
for (int i = 0; i <= (end - start); i++) {
this.data.remove(start);
}
fireSeriesChanged();
}
public void removeLast(){
int index=this.getItemCount()-1;
if (index >= 0){
this.data.remove(index);
fireSeriesChanged();
}
}
/**
* Returns a clone of the time series.
* <P>
* Notes:
* <ul>
* <li>no need to clone the domain and range descriptions, since String
* object is immutable;</li>
* <li>we pass over to the more general method clone(start, end).</li>
* </ul>
*
* @return A clone of the time series.
*
* @throws CloneNotSupportedException not thrown by this class, but
* subclasses may differ.
*/
public Object clone() throws CloneNotSupportedException {
Object clone = createCopy(0, getItemCount() - 1);
return clone;
}

/**
* Creates a new timeseries by copying a subset of the data in this time
* series.
*
* @param start the index of the first time period to copy.
* @param end the index of the last time period to copy.
*
* @return A series containing a copy of this times series from start until
* end.
*
* @throws CloneNotSupportedException if there is a cloning problem.
*/
public TimeSeries createCopy(int start, int end)
throws CloneNotSupportedException {

if (start < 0) {
throw new IllegalArgumentException("Requires start >= 0.");
}
if (end < start) {
throw new IllegalArgumentException("Requires start <= end.");
}
TimeSeries copy = (TimeSeries) super.clone();

copy.data = new java.util.ArrayList();
if (this.data.size() > 0) {
for (int index = start; index <= end; index++) {
TimeSeriesDataItem item
= (TimeSeriesDataItem) this.data.get(index);
TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone();
try {
copy.add(clone);
}
catch (SeriesException e) {
e.printStackTrace();
}
}
}
return copy;
}

/**
* Creates a new timeseries by copying a subset of the data in this time
* series.
*
* @param start the first time period to copy.
* @param end the last time period to copy.
*
* @return A time series containing a copy of this time series from start
* until end.
*
* @throws CloneNotSupportedException if there is a cloning problem.
*/
public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end)
throws CloneNotSupportedException {

if (start == null) {
throw new IllegalArgumentException("Null 'start' argument.");
}
if (end == null) {
throw new IllegalArgumentException("Null 'end' argument.");
}
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(
"Requires start on or before end.");
}
boolean emptyRange = false;
int startIndex = getIndex(start);
if (startIndex < 0) {
startIndex = -(startIndex + 1);
if (startIndex == this.data.size()) {
emptyRange = true; // start is after last data item
}
}
int endIndex = getIndex(end);
if (endIndex < 0) { // end period is not in original series
endIndex = -(endIndex + 1); // this is first item AFTER end period
endIndex = endIndex - 1; // so this is last item BEFORE end
}
if (endIndex < 0) {
emptyRange = true;
}
if (emptyRange) {
TimeSeries copy = (TimeSeries) super.clone();
copy.data = new java.util.ArrayList();
return copy;
}
else {
return createCopy(startIndex, endIndex);
}

}

/**
* Tests the series for equality with an arbitrary object.
*
* @param object the object to test against (<code>null</code> permitted).
*
* @return A boolean.
*/
public boolean equals(Object object) {
if (object == this) {
return true;
}
if (!(object instanceof TimeSeries) || !super.equals(object)) {
return false;
}
TimeSeries s = (TimeSeries) object;
if (!ObjectUtilities.equal(
getDomainDescription(), s.getDomainDescription()
)) {
return false;
}

if (!ObjectUtilities.equal(
getRangeDescription(), s.getRangeDescription()
)) {
return false;
}

if (!getClass().equals(s.getClass())) {
return false;
}

if (getMaximumItemAge() != s.getMaximumItemAge()) {
return false;
}

if (getMaximumItemCount() != s.getMaximumItemCount()) {
return false;
}

int count = getItemCount();
if (count != s.getItemCount()) {
return false;
}
for (int i = 0; i < count; i++) {
if (!getDataItem(i).equals(s.getDataItem(i))) {
return false;
}
}
return true;
}

/**
* Returns a hash code value for the object.
*
* @return The hashcode
*/
public int hashCode() {
int result;
result = (this.domain != null ? this.domain.hashCode() : 0);
result = 29 * result + (this.range != null ? this.range.hashCode() : 0);
result = 29 * result + (this.timePeriodClass != null
? this.timePeriodClass.hashCode() : 0);
result = 29 * result + this.data.hashCode();
result = 29 * result + this.maximumItemCount;
result = 29 * result + (int) this.maximumItemAge;
return result;
}

}

Locked