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-2008, 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-2008, 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:  * 22-Apr-2008 : Implemented PublicCloneable (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: import org.jfree.util.PublicCloneable;
  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, IntervalXYDataset, DomainInfo,
  83:                    PublicCloneable {
  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 an independent copy of this dataset.
 531:      *
 532:      * @return A clone.
 533:      *
 534:      * @throws CloneNotSupportedException if there is some reason that cloning
 535:      *     cannot be performed.
 536:      */
 537:     public Object clone() throws CloneNotSupportedException {
 538:         DefaultTableXYDataset clone = (DefaultTableXYDataset) super.clone();
 539:         int seriesCount = this.data.size();
 540:         clone.data = new java.util.ArrayList(seriesCount);
 541:         for (int i = 0; i < seriesCount; i++) {
 542:             XYSeries series = (XYSeries) this.data.get(i);
 543:             clone.data.add(series.clone());
 544:         }
 545: 
 546:         clone.intervalDelegate = new IntervalXYDelegate(clone);
 547:         // need to configure the intervalDelegate to match the original
 548:         clone.intervalDelegate.setFixedIntervalWidth(getIntervalWidth());
 549:         clone.intervalDelegate.setAutoWidth(isAutoWidth());
 550:         clone.intervalDelegate.setIntervalPositionFactor(
 551:                 getIntervalPositionFactor());
 552:         clone.updateXPoints();
 553:         return clone;
 554:     }
 555: 
 556:     /**
 557:      * Returns the minimum x-value in the dataset.
 558:      *
 559:      * @param includeInterval  a flag that determines whether or not the
 560:      *                         x-interval is taken into account.
 561:      *
 562:      * @return The minimum value.
 563:      */
 564:     public double getDomainLowerBound(boolean includeInterval) {
 565:         return this.intervalDelegate.getDomainLowerBound(includeInterval);
 566:     }
 567: 
 568:     /**
 569:      * Returns the maximum x-value in the dataset.
 570:      *
 571:      * @param includeInterval  a flag that determines whether or not the
 572:      *                         x-interval is taken into account.
 573:      *
 574:      * @return The maximum value.
 575:      */
 576:     public double getDomainUpperBound(boolean includeInterval) {
 577:         return this.intervalDelegate.getDomainUpperBound(includeInterval);
 578:     }
 579: 
 580:     /**
 581:      * Returns the range of the values in this dataset's domain.
 582:      *
 583:      * @param includeInterval  a flag that determines whether or not the
 584:      *                         x-interval is taken into account.
 585:      *
 586:      * @return The range.
 587:      */
 588:     public Range getDomainBounds(boolean includeInterval) {
 589:         if (includeInterval) {
 590:             return this.intervalDelegate.getDomainBounds(includeInterval);
 591:         }
 592:         else {
 593:             return DatasetUtilities.iterateDomainBounds(this, includeInterval);
 594:         }
 595:     }
 596: 
 597:     /**
 598:      * Returns the interval position factor.
 599:      *
 600:      * @return The interval position factor.
 601:      */
 602:     public double getIntervalPositionFactor() {
 603:         return this.intervalDelegate.getIntervalPositionFactor();
 604:     }
 605: 
 606:     /**
 607:      * Sets the interval position factor. Must be between 0.0 and 1.0 inclusive.
 608:      * If the factor is 0.5, the gap is in the middle of the x values. If it
 609:      * is lesser than 0.5, the gap is farther to the left and if greater than
 610:      * 0.5 it gets farther to the right.
 611:      *
 612:      * @param d the new interval position factor.
 613:      */
 614:     public void setIntervalPositionFactor(double d) {
 615:         this.intervalDelegate.setIntervalPositionFactor(d);
 616:         fireDatasetChanged();
 617:     }
 618: 
 619:     /**
 620:      * returns the full interval width.
 621:      *
 622:      * @return The interval width to use.
 623:      */
 624:     public double getIntervalWidth() {
 625:         return this.intervalDelegate.getIntervalWidth();
 626:     }
 627: 
 628:     /**
 629:      * Sets the interval width to a fixed value, and sends a
 630:      * {@link DatasetChangeEvent} to all registered listeners.
 631:      *
 632:      * @param d  the new interval width (must be > 0).
 633:      */
 634:     public void setIntervalWidth(double d) {
 635:         this.intervalDelegate.setFixedIntervalWidth(d);
 636:         fireDatasetChanged();
 637:     }
 638: 
 639:     /**
 640:      * Returns whether the interval width is automatically calculated or not.
 641:      *
 642:      * @return A flag that determines whether or not the interval width is
 643:      *         automatically calculated.
 644:      */
 645:     public boolean isAutoWidth() {
 646:         return this.intervalDelegate.isAutoWidth();
 647:     }
 648: 
 649:     /**
 650:      * Sets the flag that indicates whether the interval width is automatically
 651:      * calculated or not.
 652:      *
 653:      * @param b  a boolean.
 654:      */
 655:     public void setAutoWidth(boolean b) {
 656:         this.intervalDelegate.setAutoWidth(b);
 657:         fireDatasetChanged();
 658:     }
 659: 
 660: }