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