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