Source for org.jfree.data.xy.DefaultTableXYDataset

   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: }