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