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