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