    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
    006     *
    007     * Project Info:  http://www.jfree.org/jfreechart/index.html
    008     *
    009     * This library is free software; you can redistribute it and/or modify it
    010     * under the terms of the GNU Lesser General Public License as published by
    011     * the Free Software Foundation; either version 2.1 of the License, or
    012     * (at your option) any later version.
    013     *
    014     * This library is distributed in the hope that it will be useful, but
    015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017     * License for more details.
    018     *
    019     * You should have received a copy of the GNU Lesser General Public
    020     * License along with this library; if not, write to the Free Software
    021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022     * USA.
    023     *
    024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * --------------------------
    028     * DefaultTableXYDataset.java
    029     * --------------------------
    030     * (C) Copyright 2003-2005, by Richard Atkinson and Contributors.
    031     *
    032     * Original Author:  Richard Atkinson;
    033     * Contributor(s):   Jody Brownell;
    034     *                   David Gilbert (for Object Refinery Limited);
    035     *                   Andreas Schroeder;
    036     *
    037     * $Id: DefaultTableXYDataset.java,v 1.12.2.2 2005/10/25 21:36:51 mungady Exp $
    038     *
    039     * Changes:
    040     * --------
    041     * 27-Jul-2003 : XYDataset that forces each series to have a value for every
    042     *               X-point which is essential for stacked XY area charts (RA);
    043     * 18-Aug-2003 : Fixed event notification when removing and updating
    044     *               series (RA);
    045     * 22-Sep-2003 : Functionality moved from TableXYDataset to
    046     *               DefaultTableXYDataset (RA);
    047     * 23-Dec-2003 : Added patch for large datasets, submitted by Jody
    048     *               Brownell (DG);
    049     * 16-Feb-2004 : Added pruning methods (DG);
    050     * 31-Mar-2004 : Provisional implementation of IntervalXYDataset (AS);
    051     * 01-Apr-2004 : Sound implementation of IntervalXYDataset (AS);
    052     * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG);
    053     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
    054     *               getYValue() (DG);
    055     * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
    056     * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0
    057     *               release (DG);
    058     * 05-Oct-2005 : Made the interval delegate a dataset listener (DG);
    059     *
    060     */
    061
    062    package org.jfree.data.xy;
    063
    064    import java.util.ArrayList;
    065    import java.util.HashSet;
    066    import java.util.Iterator;
    067    import java.util.List;
    068
    069    import org.jfree.data.DomainInfo;
    070    import org.jfree.data.Range;
    071    import org.jfree.data.general.DatasetChangeEvent;
    072    import org.jfree.data.general.DatasetUtilities;
    073    import org.jfree.data.general.SeriesChangeEvent;
    074    import org.jfree.util.ObjectUtilities;
    075
    076    /**
    077     * An {@link XYDataset} where every series shares the same x-values (required
    078     * for generating stacked area charts).
    079     *
    080     * @author Richard Atkinson
    081     */
    082    public class DefaultTableXYDataset extends AbstractIntervalXYDataset
    083                                       implements TableXYDataset,
    084                                                  IntervalXYDataset, DomainInfo {
    085
    086        /**
    087         * Storage for the data - this list will contain zero, one or many
    088         * XYSeries objects.
    089         */
    090        private List data = null;
    091
    092        /** Storage for the x values. */
    093        private HashSet xPoints = null;
    094
    095        /** A flag that controls whether or not events are propogated. */
    096        private boolean propagateEvents = true;
    097
    098        /** A flag that controls auto pruning. */
    099        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    }