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