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