Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors. 6: * 7: * Project Info: http://www.jfree.org/jfreechart/index.html 8: * 9: * This library is free software; you can redistribute it and/or modify it 10: * under the terms of the GNU Lesser General Public License as published by 11: * the Free Software Foundation; either version 2.1 of the License, or 12: * (at your option) any later version. 13: * 14: * This library is distributed in the hope that it will be useful, but 15: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17: * License for more details. 18: * 19: * You should have received a copy of the GNU Lesser General Public 20: * License along with this library; if not, write to the Free Software 21: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22: * USA. 23: * 24: * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 25: * in the United States and other countries.] 26: * 27: * ------------- 28: * XYSeries.java 29: * ------------- 30: * (C) Copyright 2001-2008, Object Refinery Limited and Contributors. 31: * 32: * Original Author: David Gilbert (for Object Refinery Limited); 33: * Contributor(s): Aaron Metzger; 34: * Jonathan Gabbai; 35: * Richard Atkinson; 36: * Michel Santos; 37: * Ted Schwartz (fix for bug 1955483); 38: * 39: * Changes 40: * ------- 41: * 15-Nov-2001 : Version 1 (DG); 42: * 03-Apr-2002 : Added an add(double, double) method (DG); 43: * 29-Apr-2002 : Added a clear() method (ARM); 44: * 06-Jun-2002 : Updated Javadoc comments (DG); 45: * 29-Aug-2002 : Modified to give user control over whether or not duplicate 46: * x-values are allowed (DG); 47: * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG); 48: * 11-Nov-2002 : Added maximum item count, code contributed by Jonathan 49: * Gabbai (DG); 50: * 26-Mar-2003 : Implemented Serializable (DG); 51: * 04-Aug-2003 : Added getItems() method (DG); 52: * 15-Aug-2003 : Changed 'data' from private to protected, added new add() 53: * methods with a 'notify' argument (DG); 54: * 22-Sep-2003 : Added getAllowDuplicateXValues() method (RA); 55: * 29-Jan-2004 : Added autoSort attribute, based on a contribution by 56: * Michel Santos - see patch 886740 (DG); 57: * 03-Feb-2004 : Added indexOf() method (DG); 58: * 16-Feb-2004 : Added remove() method (DG); 59: * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG); 60: * 21-Feb-2005 : Added update(Number, Number) and addOrUpdate(Number, Number) 61: * methods (DG); 62: * 03-May-2005 : Added a new constructor, fixed the setMaximumItemCount() 63: * method to remove items (and notify listeners) if necessary, 64: * fixed the add() and addOrUpdate() methods to handle unsorted 65: * series (DG); 66: * ------------- JFreeChart 1.0.x --------------------------------------------- 67: * 11-Jan-2005 : Renamed update(int, Number) --> updateByIndex() (DG); 68: * 15-Jan-2007 : Added toArray() method (DG); 69: * 31-Oct-2007 : Implemented faster hashCode() (DG); 70: * 22-Nov-2007 : Reimplemented clone() (DG); 71: * 01-May-2008 : Fixed bug 1955483 in addOrUpdate() method, thanks to 72: * Ted Schwartz (DG); 73: * 74: */ 75: 76: package org.jfree.data.xy; 77: 78: import java.io.Serializable; 79: import java.util.Collections; 80: import java.util.List; 81: 82: import org.jfree.data.general.Series; 83: import org.jfree.data.general.SeriesChangeEvent; 84: import org.jfree.data.general.SeriesException; 85: import org.jfree.util.ObjectUtilities; 86: 87: /** 88: * Represents a sequence of zero or more data items in the form (x, y). By 89: * default, items in the series will be sorted into ascending order by x-value, 90: * and duplicate x-values are permitted. Both the sorting and duplicate 91: * defaults can be changed in the constructor. Y-values can be 92: * <code>null</code> to represent missing values. 93: */ 94: public class XYSeries extends Series implements Cloneable, Serializable { 95: 96: /** For serialization. */ 97: static final long serialVersionUID = -5908509288197150436L; 98: 99: // 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: