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: * DefaultTableXYDataset.java 29: * -------------------------- 30: * (C) Copyright 2003-2007, by Richard Atkinson and Contributors. 31: * 32: * Original Author: Richard Atkinson; 33: * Contributor(s): Jody Brownell; 34: * David Gilbert (for Object Refinery Limited); 35: * Andreas Schroeder; 36: * 37: * Changes: 38: * -------- 39: * 27-Jul-2003 : XYDataset that forces each series to have a value for every 40: * X-point which is essential for stacked XY area charts (RA); 41: * 18-Aug-2003 : Fixed event notification when removing and updating 42: * series (RA); 43: * 22-Sep-2003 : Functionality moved from TableXYDataset to 44: * DefaultTableXYDataset (RA); 45: * 23-Dec-2003 : Added patch for large datasets, submitted by Jody 46: * Brownell (DG); 47: * 16-Feb-2004 : Added pruning methods (DG); 48: * 31-Mar-2004 : Provisional implementation of IntervalXYDataset (AS); 49: * 01-Apr-2004 : Sound implementation of IntervalXYDataset (AS); 50: * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG); 51: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 52: * getYValue() (DG); 53: * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG); 54: * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 55: * release (DG); 56: * 05-Oct-2005 : Made the interval delegate a dataset listener (DG); 57: * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG); 58: * 59: */ 60: 61: package org.jfree.data.xy; 62: 63: import java.util.ArrayList; 64: import java.util.HashSet; 65: import java.util.Iterator; 66: import java.util.List; 67: 68: import org.jfree.data.DomainInfo; 69: import org.jfree.data.Range; 70: import org.jfree.data.general.DatasetChangeEvent; 71: import org.jfree.data.general.DatasetUtilities; 72: import org.jfree.data.general.SeriesChangeEvent; 73: import org.jfree.util.ObjectUtilities; 74: 75: /** 76: * An {@link XYDataset} where every series shares the same x-values (required 77: * for generating stacked area charts). 78: */ 79: public class DefaultTableXYDataset extends AbstractIntervalXYDataset 80: implements TableXYDataset, 81: IntervalXYDataset, DomainInfo { 82: 83: /** 84: * Storage for the data - this list will contain zero, one or many 85: * XYSeries objects. 86: */ 87: private List data = null; 88: 89: /** Storage for the x values. */ 90: private HashSet xPoints = null; 91: 92: /** A flag that controls whether or not events are propogated. */ 93: private boolean propagateEvents = true; 94: 95: /** A flag that controls auto pruning. */ 96: private boolean autoPrune = false; 97: 98: /** The delegate used to control the interval width. */ 99: private IntervalXYDelegate intervalDelegate; 100: 101: /** 102: * Creates a new empty dataset. 103: */ 104: public DefaultTableXYDataset() { 105: this(false); 106: } 107: 108: /** 109: * Creates a new empty dataset. 110: * 111: * @param autoPrune a flag that controls whether or not x-values are 112: * removed whenever the corresponding y-values are all 113: * <code>null</code>. 114: */ 115: public DefaultTableXYDataset(boolean autoPrune) { 116: this.autoPrune = autoPrune; 117: this.data = new ArrayList(); 118: this.xPoints = new HashSet(); 119: this.intervalDelegate = new IntervalXYDelegate(this, false); 120: addChangeListener(this.intervalDelegate); 121: } 122: 123: /** 124: * Returns the flag that controls whether or not x-values are removed from 125: * the dataset when the corresponding y-values are all <code>null</code>. 126: * 127: * @return A boolean. 128: */ 129: public boolean isAutoPrune() { 130: return this.autoPrune; 131: } 132: 133: /** 134: * Adds a series to the collection and sends a {@link DatasetChangeEvent} 135: * to all registered listeners. The series should be configured to NOT 136: * allow duplicate x-values. 137: * 138: * @param series the series (<code>null</code> not permitted). 139: */ 140: public void addSeries(XYSeries series) { 141: if (series == null) { 142: throw new IllegalArgumentException("Null 'series' argument."); 143: } 144: if (series.getAllowDuplicateXValues()) { 145: throw new IllegalArgumentException( 146: "Cannot accept XYSeries that allow duplicate values. " 147: + "Use XYSeries(seriesName, <sort>, false) constructor." 148: ); 149: } 150: updateXPoints(series); 151: this.data.add(series); 152: series.addChangeListener(this); 153: fireDatasetChanged(); 154: } 155: 156: /** 157: * Adds any unique x-values from 'series' to the dataset, and also adds any 158: * x-values that are in the dataset but not in 'series' to the series. 159: * 160: * @param series the series (<code>null</code> not permitted). 161: */ 162: private void updateXPoints(XYSeries series) { 163: if (series == null) { 164: throw new IllegalArgumentException("Null 'series' not permitted."); 165: } 166: HashSet seriesXPoints = new HashSet(); 167: boolean savedState = this.propagateEvents; 168: this.propagateEvents = false; 169: for (int itemNo = 0; itemNo < series.getItemCount(); itemNo++) { 170: Number xValue = series.getX(itemNo); 171: seriesXPoints.add(xValue); 172: if (!this.xPoints.contains(xValue)) { 173: this.xPoints.add(xValue); 174: int seriesCount = this.data.size(); 175: for (int seriesNo = 0; seriesNo < seriesCount; seriesNo++) { 176: XYSeries dataSeries = (XYSeries) this.data.get(seriesNo); 177: if (!dataSeries.equals(series)) { 178: dataSeries.add(xValue, null); 179: } 180: } 181: } 182: } 183: Iterator iterator = this.xPoints.iterator(); 184: while (iterator.hasNext()) { 185: Number xPoint = (Number) iterator.next(); 186: if (!seriesXPoints.contains(xPoint)) { 187: series.add(xPoint, null); 188: } 189: } 190: this.propagateEvents = savedState; 191: } 192: 193: /** 194: * Updates the x-values for all the series in the dataset. 195: */ 196: public void updateXPoints() { 197: this.propagateEvents = false; 198: for (int s = 0; s < this.data.size(); s++) { 199: updateXPoints((XYSeries) this.data.get(s)); 200: } 201: if (this.autoPrune) { 202: prune(); 203: } 204: this.propagateEvents = true; 205: } 206: 207: /** 208: * Returns the number of series in the collection. 209: * 210: * @return The series count. 211: */ 212: public int getSeriesCount() { 213: return this.data.size(); 214: } 215: 216: /** 217: * Returns the number of x values in the dataset. 218: * 219: * @return The number of x values in the dataset. 220: */ 221: public int getItemCount() { 222: if (this.xPoints == null) { 223: return 0; 224: } 225: else { 226: return this.xPoints.size(); 227: } 228: } 229: 230: /** 231: * Returns a series. 232: * 233: * @param series the series (zero-based index). 234: * 235: * @return The series (never <code>null</code>). 236: */ 237: public XYSeries getSeries(int series) { 238: if ((series < 0) || (series >= getSeriesCount())) { 239: throw new IllegalArgumentException("Index outside valid range."); 240: } 241: return (XYSeries) this.data.get(series); 242: } 243: 244: /** 245: * Returns the key for a series. 246: * 247: * @param series the series (zero-based index). 248: * 249: * @return The key for a series. 250: */ 251: public Comparable getSeriesKey(int series) { 252: // check arguments...delegated 253: return getSeries(series).getKey(); 254: } 255: 256: /** 257: * Returns the number of items in the specified series. 258: * 259: * @param series the series (zero-based index). 260: * 261: * @return The number of items in the specified series. 262: */ 263: public int getItemCount(int series) { 264: // check arguments...delegated 265: return getSeries(series).getItemCount(); 266: } 267: 268: /** 269: * Returns the x-value for the specified series and item. 270: * 271: * @param series the series (zero-based index). 272: * @param item the item (zero-based index). 273: * 274: * @return The x-value for the specified series and item. 275: */ 276: public Number getX(int series, int item) { 277: XYSeries s = (XYSeries) this.data.get(series); 278: XYDataItem dataItem = s.getDataItem(item); 279: return dataItem.getX(); 280: } 281: 282: /** 283: * Returns the starting X value for the specified series and item. 284: * 285: * @param series the series (zero-based index). 286: * @param item the item (zero-based index). 287: * 288: * @return The starting X value. 289: */ 290: public Number getStartX(int series, int item) { 291: return this.intervalDelegate.getStartX(series, item); 292: } 293: 294: /** 295: * Returns the ending X value for the specified series and item. 296: * 297: * @param series the series (zero-based index). 298: * @param item the item (zero-based index). 299: * 300: * @return The ending X value. 301: */ 302: public Number getEndX(int series, int item) { 303: return this.intervalDelegate.getEndX(series, item); 304: } 305: 306: /** 307: * Returns the y-value for the specified series and item. 308: * 309: * @param series the series (zero-based index). 310: * @param index the index of the item of interest (zero-based). 311: * 312: * @return The y-value for the specified series and item (possibly 313: * <code>null</code>). 314: */ 315: public Number getY(int series, int index) { 316: XYSeries ts = (XYSeries) this.data.get(series); 317: XYDataItem dataItem = ts.getDataItem(index); 318: return dataItem.getY(); 319: } 320: 321: /** 322: * Returns the starting Y value for the specified series and item. 323: * 324: * @param series the series (zero-based index). 325: * @param item the item (zero-based index). 326: * 327: * @return The starting Y value. 328: */ 329: public Number getStartY(int series, int item) { 330: return getY(series, item); 331: } 332: 333: /** 334: * Returns the ending Y value for the specified series and item. 335: * 336: * @param series the series (zero-based index). 337: * @param item the item (zero-based index). 338: * 339: * @return The ending Y value. 340: */ 341: public Number getEndY(int series, int item) { 342: return getY(series, item); 343: } 344: 345: /** 346: * Removes all the series from the collection and sends a 347: * {@link DatasetChangeEvent} to all registered listeners. 348: */ 349: public void removeAllSeries() { 350: 351: // Unregister the collection as a change listener to each series in 352: // the collection. 353: for (int i = 0; i < this.data.size(); i++) { 354: XYSeries series = (XYSeries) this.data.get(i); 355: series.removeChangeListener(this); 356: } 357: 358: // Remove all the series from the collection and notify listeners. 359: this.data.clear(); 360: this.xPoints.clear(); 361: fireDatasetChanged(); 362: } 363: 364: /** 365: * Removes a series from the collection and sends a 366: * {@link DatasetChangeEvent} to all registered listeners. 367: * 368: * @param series the series (<code>null</code> not permitted). 369: */ 370: public void removeSeries(XYSeries series) { 371: 372: // check arguments... 373: if (series == null) { 374: throw new IllegalArgumentException("Null 'series' argument."); 375: } 376: 377: // remove the series... 378: if (this.data.contains(series)) { 379: series.removeChangeListener(this); 380: this.data.remove(series); 381: if (this.data.size() == 0) { 382: this.xPoints.clear(); 383: } 384: fireDatasetChanged(); 385: } 386: 387: } 388: 389: /** 390: * Removes a series from the collection and sends a 391: * {@link DatasetChangeEvent} to all registered listeners. 392: * 393: * @param series the series (zero based index). 394: */ 395: public void removeSeries(int series) { 396: 397: // check arguments... 398: if ((series < 0) || (series > getSeriesCount())) { 399: throw new IllegalArgumentException("Index outside valid range."); 400: } 401: 402: // fetch the series, remove the change listener, then remove the series. 403: XYSeries s = (XYSeries) this.data.get(series); 404: s.removeChangeListener(this); 405: this.data.remove(series); 406: if (this.data.size() == 0) { 407: this.xPoints.clear(); 408: } 409: else if (this.autoPrune) { 410: prune(); 411: } 412: fireDatasetChanged(); 413: 414: } 415: 416: /** 417: * Removes the items from all series for a given x value. 418: * 419: * @param x the x-value. 420: */ 421: public void removeAllValuesForX(Number x) { 422: if (x == null) { 423: throw new IllegalArgumentException("Null 'x' argument."); 424: } 425: boolean savedState = this.propagateEvents; 426: this.propagateEvents = false; 427: for (int s = 0; s < this.data.size(); s++) { 428: XYSeries series = (XYSeries) this.data.get(s); 429: series.remove(x); 430: } 431: this.propagateEvents = savedState; 432: this.xPoints.remove(x); 433: fireDatasetChanged(); 434: } 435: 436: /** 437: * Returns <code>true</code> if all the y-values for the specified x-value 438: * are <code>null</code> and <code>false</code> otherwise. 439: * 440: * @param x the x-value. 441: * 442: * @return A boolean. 443: */ 444: protected boolean canPrune(Number x) { 445: for (int s = 0; s < this.data.size(); s++) { 446: XYSeries series = (XYSeries) this.data.get(s); 447: if (series.getY(series.indexOf(x)) != null) { 448: return false; 449: } 450: } 451: return true; 452: } 453: 454: /** 455: * Removes all x-values for which all the y-values are <code>null</code>. 456: */ 457: public void prune() { 458: HashSet hs = (HashSet) this.xPoints.clone(); 459: Iterator iterator = hs.iterator(); 460: while (iterator.hasNext()) { 461: Number x = (Number) iterator.next(); 462: if (canPrune(x)) { 463: removeAllValuesForX(x); 464: } 465: } 466: } 467: 468: /** 469: * This method receives notification when a series belonging to the dataset 470: * changes. It responds by updating the x-points for the entire dataset 471: * and sending a {@link DatasetChangeEvent} to all registered listeners. 472: * 473: * @param event information about the change. 474: */ 475: public void seriesChanged(SeriesChangeEvent event) { 476: if (this.propagateEvents) { 477: updateXPoints(); 478: fireDatasetChanged(); 479: } 480: } 481: 482: /** 483: * Tests this collection for equality with an arbitrary object. 484: * 485: * @param obj the object (<code>null</code> permitted). 486: * 487: * @return A boolean. 488: */ 489: public boolean equals(Object obj) { 490: if (obj == this) { 491: return true; 492: } 493: if (!(obj instanceof DefaultTableXYDataset)) { 494: return false; 495: } 496: DefaultTableXYDataset that = (DefaultTableXYDataset) obj; 497: if (this.autoPrune != that.autoPrune) { 498: return false; 499: } 500: if (this.propagateEvents != that.propagateEvents) { 501: return false; 502: } 503: if (!this.intervalDelegate.equals(that.intervalDelegate)) { 504: return false; 505: } 506: if (!ObjectUtilities.equal(this.data, that.data)) { 507: return false; 508: } 509: return true; 510: } 511: 512: /** 513: * Returns a hash code. 514: * 515: * @return A hash code. 516: */ 517: public int hashCode() { 518: int result; 519: result = (this.data != null ? this.data.hashCode() : 0); 520: result = 29 * result 521: + (this.xPoints != null ? this.xPoints.hashCode() : 0); 522: result = 29 * result + (this.propagateEvents ? 1 : 0); 523: result = 29 * result + (this.autoPrune ? 1 : 0); 524: return result; 525: } 526: 527: /** 528: * Returns the minimum x-value in the dataset. 529: * 530: * @param includeInterval a flag that determines whether or not the 531: * x-interval is taken into account. 532: * 533: * @return The minimum value. 534: */ 535: public double getDomainLowerBound(boolean includeInterval) { 536: return this.intervalDelegate.getDomainLowerBound(includeInterval); 537: } 538: 539: /** 540: * Returns the maximum x-value in the dataset. 541: * 542: * @param includeInterval a flag that determines whether or not the 543: * x-interval is taken into account. 544: * 545: * @return The maximum value. 546: */ 547: public double getDomainUpperBound(boolean includeInterval) { 548: return this.intervalDelegate.getDomainUpperBound(includeInterval); 549: } 550: 551: /** 552: * Returns the range of the values in this dataset's domain. 553: * 554: * @param includeInterval a flag that determines whether or not the 555: * x-interval is taken into account. 556: * 557: * @return The range. 558: */ 559: public Range getDomainBounds(boolean includeInterval) { 560: if (includeInterval) { 561: return this.intervalDelegate.getDomainBounds(includeInterval); 562: } 563: else { 564: return DatasetUtilities.iterateDomainBounds(this, includeInterval); 565: } 566: } 567: 568: /** 569: * Returns the interval position factor. 570: * 571: * @return The interval position factor. 572: */ 573: public double getIntervalPositionFactor() { 574: return this.intervalDelegate.getIntervalPositionFactor(); 575: } 576: 577: /** 578: * Sets the interval position factor. Must be between 0.0 and 1.0 inclusive. 579: * If the factor is 0.5, the gap is in the middle of the x values. If it 580: * is lesser than 0.5, the gap is farther to the left and if greater than 581: * 0.5 it gets farther to the right. 582: * 583: * @param d the new interval position factor. 584: */ 585: public void setIntervalPositionFactor(double d) { 586: this.intervalDelegate.setIntervalPositionFactor(d); 587: fireDatasetChanged(); 588: } 589: 590: /** 591: * returns the full interval width. 592: * 593: * @return The interval width to use. 594: */ 595: public double getIntervalWidth() { 596: return this.intervalDelegate.getIntervalWidth(); 597: } 598: 599: /** 600: * Sets the interval width to a fixed value, and sends a 601: * {@link DatasetChangeEvent} to all registered listeners. 602: * 603: * @param d the new interval width (must be > 0). 604: */ 605: public void setIntervalWidth(double d) { 606: this.intervalDelegate.setFixedIntervalWidth(d); 607: fireDatasetChanged(); 608: } 609: 610: /** 611: * Returns whether the interval width is automatically calculated or not. 612: * 613: * @return A flag that determines whether or not the interval width is 614: * automatically calculated. 615: */ 616: public boolean isAutoWidth() { 617: return this.intervalDelegate.isAutoWidth(); 618: } 619: 620: /** 621: * Sets the flag that indicates whether the interval width is automatically 622: * calculated or not. 623: * 624: * @param b a boolean. 625: */ 626: public void setAutoWidth(boolean b) { 627: this.intervalDelegate.setAutoWidth(b); 628: fireDatasetChanged(); 629: } 630: 631: }