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     * XYSeries.java
029     * -------------
030     * (C) Copyright 2001-2008, Object Refinery Limited and Contributors.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   Aaron Metzger;
034     *                   Jonathan Gabbai;
035     *                   Richard Atkinson;
036     *                   Michel Santos;
037     *                   Ted Schwartz (fix for bug 1955483);
038     *
039     * Changes
040     * -------
041     * 15-Nov-2001 : Version 1 (DG);
042     * 03-Apr-2002 : Added an add(double, double) method (DG);
043     * 29-Apr-2002 : Added a clear() method (ARM);
044     * 06-Jun-2002 : Updated Javadoc comments (DG);
045     * 29-Aug-2002 : Modified to give user control over whether or not duplicate
046     *               x-values are allowed (DG);
047     * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG);
048     * 11-Nov-2002 : Added maximum item count, code contributed by Jonathan
049     *               Gabbai (DG);
050     * 26-Mar-2003 : Implemented Serializable (DG);
051     * 04-Aug-2003 : Added getItems() method (DG);
052     * 15-Aug-2003 : Changed 'data' from private to protected, added new add()
053     *               methods with a 'notify' argument (DG);
054     * 22-Sep-2003 : Added getAllowDuplicateXValues() method (RA);
055     * 29-Jan-2004 : Added autoSort attribute, based on a contribution by
056     *               Michel Santos - see patch 886740 (DG);
057     * 03-Feb-2004 : Added indexOf() method (DG);
058     * 16-Feb-2004 : Added remove() method (DG);
059     * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
060     * 21-Feb-2005 : Added update(Number, Number) and addOrUpdate(Number, Number)
061     *               methods (DG);
062     * 03-May-2005 : Added a new constructor, fixed the setMaximumItemCount()
063     *               method to remove items (and notify listeners) if necessary,
064     *               fixed the add() and addOrUpdate() methods to handle unsorted
065     *               series (DG);
066     * ------------- JFreeChart 1.0.x ---------------------------------------------
067     * 11-Jan-2005 : Renamed update(int, Number) --> updateByIndex() (DG);
068     * 15-Jan-2007 : Added toArray() method (DG);
069     * 31-Oct-2007 : Implemented faster hashCode() (DG);
070     * 22-Nov-2007 : Reimplemented clone() (DG);
071     * 01-May-2008 : Fixed bug 1955483 in addOrUpdate() method, thanks to
072     *               Ted Schwartz (DG);
073     *
074     */
075    
076    package org.jfree.data.xy;
077    
078    import java.io.Serializable;
079    import java.util.Collections;
080    import java.util.List;
081    
082    import org.jfree.data.general.Series;
083    import org.jfree.data.general.SeriesChangeEvent;
084    import org.jfree.data.general.SeriesException;
085    import org.jfree.util.ObjectUtilities;
086    
087    /**
088     * Represents a sequence of zero or more data items in the form (x, y).  By
089     * default, items in the series will be sorted into ascending order by x-value,
090     * and duplicate x-values are permitted.  Both the sorting and duplicate
091     * defaults can be changed in the constructor.  Y-values can be
092     * <code>null</code> to represent missing values.
093     */
094    public class XYSeries extends Series implements Cloneable, Serializable {
095    
096        /** For serialization. */
097        static final long serialVersionUID = -5908509288197150436L;
098    
099        // In version 0.9.12, in response to several developer requests, I changed
100        // the 'data' attribute from 'private' to 'protected', so that others can
101        // make subclasses that work directly with the underlying data structure.
102    
103        /** Storage for the data items in the series. */
104        protected List data;
105    
106        /** The maximum number of items for the series. */
107        private int maximumItemCount = Integer.MAX_VALUE;
108    
109        /** A flag that controls whether the items are automatically sorted. */
110        private boolean autoSort;
111    
112        /** A flag that controls whether or not duplicate x-values are allowed. */
113        private boolean allowDuplicateXValues;
114    
115        /**
116         * Creates a new empty series.  By default, items added to the series will
117         * be sorted into ascending order by x-value, and duplicate x-values will
118         * be allowed (these defaults can be modified with another constructor.
119         *
120         * @param key  the series key (<code>null</code> not permitted).
121         */
122        public XYSeries(Comparable key) {
123            this(key, true, true);
124        }
125    
126        /**
127         * Constructs a new empty series, with the auto-sort flag set as requested,
128         * and duplicate values allowed.
129         *
130         * @param key  the series key (<code>null</code> not permitted).
131         * @param autoSort  a flag that controls whether or not the items in the
132         *                  series are sorted.
133         */
134        public XYSeries(Comparable key, boolean autoSort) {
135            this(key, autoSort, true);
136        }
137    
138        /**
139         * Constructs a new xy-series that contains no data.  You can specify
140         * whether or not duplicate x-values are allowed for the series.
141         *
142         * @param key  the series key (<code>null</code> not permitted).
143         * @param autoSort  a flag that controls whether or not the items in the
144         *                  series are sorted.
145         * @param allowDuplicateXValues  a flag that controls whether duplicate
146         *                               x-values are allowed.
147         */
148        public XYSeries(Comparable key,
149                        boolean autoSort,
150                        boolean allowDuplicateXValues) {
151            super(key);
152            this.data = new java.util.ArrayList();
153            this.autoSort = autoSort;
154            this.allowDuplicateXValues = allowDuplicateXValues;
155        }
156    
157        /**
158         * Returns the flag that controls whether the items in the series are
159         * automatically sorted.  There is no setter for this flag, it must be
160         * defined in the series constructor.
161         *
162         * @return A boolean.
163         */
164        public boolean getAutoSort() {
165            return this.autoSort;
166        }
167    
168        /**
169         * Returns a flag that controls whether duplicate x-values are allowed.
170         * This flag can only be set in the constructor.
171         *
172         * @return A boolean.
173         */
174        public boolean getAllowDuplicateXValues() {
175            return this.allowDuplicateXValues;
176        }
177    
178        /**
179         * Returns the number of items in the series.
180         *
181         * @return The item count.
182         */
183        public int getItemCount() {
184            return this.data.size();
185        }
186    
187        /**
188         * Returns the list of data items for the series (the list contains
189         * {@link XYDataItem} objects and is unmodifiable).
190         *
191         * @return The list of data items.
192         */
193        public List getItems() {
194            return Collections.unmodifiableList(this.data);
195        }
196    
197        /**
198         * Returns the maximum number of items that will be retained in the series.
199         * The default value is <code>Integer.MAX_VALUE</code>.
200         *
201         * @return The maximum item count.
202         * @see #setMaximumItemCount(int)
203         */
204        public int getMaximumItemCount() {
205            return this.maximumItemCount;
206        }
207    
208        /**
209         * Sets the maximum number of items that will be retained in the series.
210         * If you add a new item to the series such that the number of items will
211         * exceed the maximum item count, then the first element in the series is
212         * automatically removed, ensuring that the maximum item count is not
213         * exceeded.
214         * <p>
215         * Typically this value is set before the series is populated with data,
216         * but if it is applied later, it may cause some items to be removed from
217         * the series (in which case a {@link SeriesChangeEvent} will be sent to
218         * all registered listeners.
219         *
220         * @param maximum  the maximum number of items for the series.
221         */
222        public void setMaximumItemCount(int maximum) {
223            this.maximumItemCount = maximum;
224            boolean dataRemoved = false;
225            while (this.data.size() > maximum) {
226                this.data.remove(0);
227                dataRemoved = true;
228            }
229            if (dataRemoved) {
230                fireSeriesChanged();
231            }
232        }
233    
234        /**
235         * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
236         * all registered listeners.
237         *
238         * @param item  the (x, y) item (<code>null</code> not permitted).
239         */
240        public void add(XYDataItem item) {
241            // argument checking delegated...
242            add(item, true);
243        }
244    
245        /**
246         * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
247         * all registered listeners.
248         *
249         * @param x  the x value.
250         * @param y  the y value.
251         */
252        public void add(double x, double y) {
253            add(new Double(x), new Double(y), true);
254        }
255    
256        /**
257         * Adds a data item to the series and, if requested, sends a
258         * {@link SeriesChangeEvent} to all registered listeners.
259         *
260         * @param x  the x value.
261         * @param y  the y value.
262         * @param notify  a flag that controls whether or not a
263         *                {@link SeriesChangeEvent} is sent to all registered
264         *                listeners.
265         */
266        public void add(double x, double y, boolean notify) {
267            add(new Double(x), new Double(y), notify);
268        }
269    
270        /**
271         * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
272         * all registered listeners.  The unusual pairing of parameter types is to
273         * make it easier to add <code>null</code> y-values.
274         *
275         * @param x  the x value.
276         * @param y  the y value (<code>null</code> permitted).
277         */
278        public void add(double x, Number y) {
279            add(new Double(x), y);
280        }
281    
282        /**
283         * Adds a data item to the series and, if requested, sends a
284         * {@link SeriesChangeEvent} to all registered listeners.  The unusual
285         * pairing of parameter types is to make it easier to add null y-values.
286         *
287         * @param x  the x value.
288         * @param y  the y value (<code>null</code> permitted).
289         * @param notify  a flag that controls whether or not a
290         *                {@link SeriesChangeEvent} is sent to all registered
291         *                listeners.
292         */
293        public void add(double x, Number y, boolean notify) {
294            add(new Double(x), y, notify);
295        }
296    
297        /**
298         * Adds new data to the series and sends a {@link SeriesChangeEvent} to
299         * all registered listeners.
300         * <P>
301         * Throws an exception if the x-value is a duplicate AND the
302         * allowDuplicateXValues flag is false.
303         *
304         * @param x  the x-value (<code>null</code> not permitted).
305         * @param y  the y-value (<code>null</code> permitted).
306         */
307        public void add(Number x, Number y) {
308            // argument checking delegated...
309            add(x, y, true);
310        }
311    
312        /**
313         * Adds new data to the series and, if requested, sends a
314         * {@link SeriesChangeEvent} to all registered listeners.
315         * <P>
316         * Throws an exception if the x-value is a duplicate AND the
317         * allowDuplicateXValues flag is false.
318         *
319         * @param x  the x-value (<code>null</code> not permitted).
320         * @param y  the y-value (<code>null</code> permitted).
321         * @param notify  a flag the controls whether or not a
322         *                {@link SeriesChangeEvent} is sent to all registered
323         *                listeners.
324         */
325        public void add(Number x, Number y, boolean notify) {
326            // delegate argument checking to XYDataItem...
327            XYDataItem item = new XYDataItem(x, y);
328            add(item, notify);
329        }
330    
331        /**
332         * Adds a data item to the series and, if requested, sends a
333         * {@link SeriesChangeEvent} to all registered listeners.
334         *
335         * @param item  the (x, y) item (<code>null</code> not permitted).
336         * @param notify  a flag that controls whether or not a
337         *                {@link SeriesChangeEvent} is sent to all registered
338         *                listeners.
339         */
340        public void add(XYDataItem item, boolean notify) {
341    
342            if (item == null) {
343                throw new IllegalArgumentException("Null 'item' argument.");
344            }
345    
346            if (this.autoSort) {
347                int index = Collections.binarySearch(this.data, item);
348                if (index < 0) {
349                    this.data.add(-index - 1, item);
350                }
351                else {
352                    if (this.allowDuplicateXValues) {
353                        // need to make sure we are adding *after* any duplicates
354                        int size = this.data.size();
355                        while (index < size
356                               && item.compareTo(this.data.get(index)) == 0) {
357                            index++;
358                        }
359                        if (index < this.data.size()) {
360                            this.data.add(index, item);
361                        }
362                        else {
363                            this.data.add(item);
364                        }
365                    }
366                    else {
367                        throw new SeriesException("X-value already exists.");
368                    }
369                }
370            }
371            else {
372                if (!this.allowDuplicateXValues) {
373                    // can't allow duplicate values, so we need to check whether
374                    // there is an item with the given x-value already
375                    int index = indexOf(item.getX());
376                    if (index >= 0) {
377                        throw new SeriesException("X-value already exists.");
378                    }
379                }
380                this.data.add(item);
381            }
382            if (getItemCount() > this.maximumItemCount) {
383                this.data.remove(0);
384            }
385            if (notify) {
386                fireSeriesChanged();
387            }
388        }
389    
390        /**
391         * Deletes a range of items from the series and sends a
392         * {@link SeriesChangeEvent} to all registered listeners.
393         *
394         * @param start  the start index (zero-based).
395         * @param end  the end index (zero-based).
396         */
397        public void delete(int start, int end) {
398            for (int i = start; i <= end; i++) {
399                this.data.remove(start);
400            }
401            fireSeriesChanged();
402        }
403    
404        /**
405         * Removes the item at the specified index and sends a
406         * {@link SeriesChangeEvent} to all registered listeners.
407         *
408         * @param index  the index.
409         *
410         * @return The item removed.
411         */
412        public XYDataItem remove(int index) {
413            XYDataItem result = (XYDataItem) this.data.remove(index);
414            fireSeriesChanged();
415            return result;
416        }
417    
418        /**
419         * Removes the item with the specified x-value and sends a
420         * {@link SeriesChangeEvent} to all registered listeners.
421         *
422         * @param x  the x-value.
423    
424         * @return The item removed.
425         */
426        public XYDataItem remove(Number x) {
427            return remove(indexOf(x));
428        }
429    
430        /**
431         * Removes all data items from the series.
432         */
433        public void clear() {
434            if (this.data.size() > 0) {
435                this.data.clear();
436                fireSeriesChanged();
437            }
438        }
439    
440        /**
441         * Return the data item with the specified index.
442         *
443         * @param index  the index.
444         *
445         * @return The data item with the specified index.
446         */
447        public XYDataItem getDataItem(int index) {
448            return (XYDataItem) this.data.get(index);
449        }
450    
451        /**
452         * Returns the x-value at the specified index.
453         *
454         * @param index  the index (zero-based).
455         *
456         * @return The x-value (never <code>null</code>).
457         */
458        public Number getX(int index) {
459            return getDataItem(index).getX();
460        }
461    
462        /**
463         * Returns the y-value at the specified index.
464         *
465         * @param index  the index (zero-based).
466         *
467         * @return The y-value (possibly <code>null</code>).
468         */
469        public Number getY(int index) {
470            return getDataItem(index).getY();
471        }
472    
473        /**
474         * Updates the value of an item in the series and sends a
475         * {@link SeriesChangeEvent} to all registered listeners.
476         *
477         * @param index  the item (zero based index).
478         * @param y  the new value (<code>null</code> permitted).
479         *
480         * @deprecated Renamed {@link #updateByIndex(int, Number)} to avoid
481         *         confusion with the {@link #update(Number, Number)} method.
482         */
483        public void update(int index, Number y) {
484            XYDataItem item = getDataItem(index);
485            item.setY(y);
486            fireSeriesChanged();
487        }
488    
489        /**
490         * Updates the value of an item in the series and sends a
491         * {@link SeriesChangeEvent} to all registered listeners.
492         *
493         * @param index  the item (zero based index).
494         * @param y  the new value (<code>null</code> permitted).
495         *
496         * @since 1.0.1
497         */
498        public void updateByIndex(int index, Number y) {
499            update(index, y);
500        }
501    
502        /**
503         * Updates an item in the series.
504         *
505         * @param x  the x-value (<code>null</code> not permitted).
506         * @param y  the y-value (<code>null</code> permitted).
507         *
508         * @throws SeriesException if there is no existing item with the specified
509         *         x-value.
510         */
511        public void update(Number x, Number y) {
512            int index = indexOf(x);
513            if (index < 0) {
514                throw new SeriesException("No observation for x = " + x);
515            }
516            else {
517                XYDataItem item = getDataItem(index);
518                item.setY(y);
519                fireSeriesChanged();
520            }
521        }
522    
523        /**
524         * Adds or updates an item in the series and sends a
525         * {@link SeriesChangeEvent} to all registered listeners.
526         *
527         * @param x  the x-value.
528         * @param y  the y-value.
529         *
530         * @return The item that was overwritten, if any.
531         *
532         * @since 1.0.10
533         */
534        public XYDataItem addOrUpdate(double x, double y) {
535            return addOrUpdate(new Double(x), new Double(y));
536        }
537    
538        /**
539         * Adds or updates an item in the series and sends a
540         * {@link org.jfree.data.general.SeriesChangeEvent} to all registered
541         * listeners.
542         *
543         * @param x  the x-value (<code>null</code> not permitted).
544         * @param y  the y-value (<code>null</code> permitted).
545         *
546         * @return A copy of the overwritten data item, or <code>null</code> if no
547         *         item was overwritten.
548         */
549        public XYDataItem addOrUpdate(Number x, Number y) {
550            if (x == null) {
551                throw new IllegalArgumentException("Null 'x' argument.");
552            }
553            XYDataItem overwritten = null;
554            int index = indexOf(x);
555            if (index >= 0 && !this.allowDuplicateXValues) {
556                XYDataItem existing = (XYDataItem) this.data.get(index);
557                try {
558                    overwritten = (XYDataItem) existing.clone();
559                }
560                catch (CloneNotSupportedException e) {
561                    throw new SeriesException("Couldn't clone XYDataItem!");
562                }
563                existing.setY(y);
564            }
565            else {
566                // if the series is sorted, the negative index is a result from
567                // Collections.binarySearch() and tells us where to insert the
568                // new item...otherwise it will be just -1 and we should just
569                // append the value to the list...
570                if (this.autoSort) {
571                    this.data.add(-index - 1, new XYDataItem(x, y));
572                }
573                else {
574                    this.data.add(new XYDataItem(x, y));
575                }
576                // check if this addition will exceed the maximum item count...
577                if (getItemCount() > this.maximumItemCount) {
578                    this.data.remove(0);
579                }
580            }
581            fireSeriesChanged();
582            return overwritten;
583        }
584    
585        /**
586         * Returns the index of the item with the specified x-value, or a negative
587         * index if the series does not contain an item with that x-value.  Be
588         * aware that for an unsorted series, the index is found by iterating
589         * through all items in the series.
590         *
591         * @param x  the x-value (<code>null</code> not permitted).
592         *
593         * @return The index.
594         */
595        public int indexOf(Number x) {
596            if (this.autoSort) {
597                return Collections.binarySearch(this.data, new XYDataItem(x, null));
598            }
599            else {
600                for (int i = 0; i < this.data.size(); i++) {
601                    XYDataItem item = (XYDataItem) this.data.get(i);
602                    if (item.getX().equals(x)) {
603                        return i;
604                    }
605                }
606                return -1;
607            }
608        }
609    
610        /**
611         * Returns a new array containing the x and y values from this series.
612         *
613         * @return A new array containing the x and y values from this series.
614         *
615         * @since 1.0.4
616         */
617        public double[][] toArray() {
618            int itemCount = getItemCount();
619            double[][] result = new double[2][itemCount];
620            for (int i = 0; i < itemCount; i++) {
621                result[0][i] = this.getX(i).doubleValue();
622                Number y = getY(i);
623                if (y != null) {
624                    result[1][i] = y.doubleValue();
625                }
626                else {
627                    result[1][i] = Double.NaN;
628                }
629            }
630            return result;
631        }
632    
633        /**
634         * Returns a clone of the series.
635         *
636         * @return A clone of the series.
637         *
638         * @throws CloneNotSupportedException if there is a cloning problem.
639         */
640        public Object clone() throws CloneNotSupportedException {
641            XYSeries clone = (XYSeries) super.clone();
642            clone.data = (List) ObjectUtilities.deepClone(this.data);
643            return clone;
644        }
645    
646        /**
647         * Creates a new series by copying a subset of the data in this time series.
648         *
649         * @param start  the index of the first item to copy.
650         * @param end  the index of the last item to copy.
651         *
652         * @return A series containing a copy of this series from start until end.
653         *
654         * @throws CloneNotSupportedException if there is a cloning problem.
655         */
656        public XYSeries createCopy(int start, int end)
657            throws CloneNotSupportedException {
658    
659            XYSeries copy = (XYSeries) super.clone();
660            copy.data = new java.util.ArrayList();
661            if (this.data.size() > 0) {
662                for (int index = start; index <= end; index++) {
663                    XYDataItem item = (XYDataItem) this.data.get(index);
664                    XYDataItem clone = (XYDataItem) item.clone();
665                    try {
666                        copy.add(clone);
667                    }
668                    catch (SeriesException e) {
669                        System.err.println("Unable to add cloned data item.");
670                    }
671                }
672            }
673            return copy;
674    
675        }
676    
677        /**
678         * Tests this series for equality with an arbitrary object.
679         *
680         * @param obj  the object to test against for equality
681         *             (<code>null</code> permitted).
682         *
683         * @return A boolean.
684         */
685        public boolean equals(Object obj) {
686            if (obj == this) {
687                return true;
688            }
689            if (!(obj instanceof XYSeries)) {
690                return false;
691            }
692            if (!super.equals(obj)) {
693                return false;
694            }
695            XYSeries that = (XYSeries) obj;
696            if (this.maximumItemCount != that.maximumItemCount) {
697                return false;
698            }
699            if (this.autoSort != that.autoSort) {
700                return false;
701            }
702            if (this.allowDuplicateXValues != that.allowDuplicateXValues) {
703                return false;
704            }
705            if (!ObjectUtilities.equal(this.data, that.data)) {
706                return false;
707            }
708            return true;
709        }
710    
711        /**
712         * Returns a hash code.
713         *
714         * @return A hash code.
715         */
716        public int hashCode() {
717            int result = super.hashCode();
718            // it is too slow to look at every data item, so let's just look at
719            // the first, middle and last items...
720            int count = getItemCount();
721            if (count > 0) {
722                XYDataItem item = getDataItem(0);
723                result = 29 * result + item.hashCode();
724            }
725            if (count > 1) {
726                XYDataItem item = getDataItem(count - 1);
727                result = 29 * result + item.hashCode();
728            }
729            if (count > 2) {
730                XYDataItem item = getDataItem(count / 2);
731                result = 29 * result + item.hashCode();
732            }
733            result = 29 * result + this.maximumItemCount;
734            result = 29 * result + (this.autoSort ? 1 : 0);
735            result = 29 * result + (this.allowDuplicateXValues ? 1 : 0);
736            return result;
737        }
738    
739    }
740