    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2008, 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-2008, 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     * Changes:
    038     * --------
    039     * 27-Jul-2003 : XYDataset that forces each series to have a value for every
    040     *               X-point which is essential for stacked XY area charts (RA);
    041     * 18-Aug-2003 : Fixed event notification when removing and updating
    042     *               series (RA);
    043     * 22-Sep-2003 : Functionality moved from TableXYDataset to
    044     *               DefaultTableXYDataset (RA);
    045     * 23-Dec-2003 : Added patch for large datasets, submitted by Jody
    046     *               Brownell (DG);
    047     * 16-Feb-2004 : Added pruning methods (DG);
    048     * 31-Mar-2004 : Provisional implementation of IntervalXYDataset (AS);
    049     * 01-Apr-2004 : Sound implementation of IntervalXYDataset (AS);
    050     * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG);
    051     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
    052     *               getYValue() (DG);
    053     * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
    054     * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0
    055     *               release (DG);
    056     * 05-Oct-2005 : Made the interval delegate a dataset listener (DG);
    057     * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
    058     * 22-Apr-2008 : Implemented PublicCloneable (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    import org.jfree.util.PublicCloneable;
    076
    077    /**
    078     * An {@link XYDataset} where every series shares the same x-values (required
    079     * for generating stacked area charts).
    080     */
    081    public class DefaultTableXYDataset extends AbstractIntervalXYDataset
    082            implements TableXYDataset, IntervalXYDataset, DomainInfo,
    083                       PublicCloneable {
    084
    085        /**
    086         * Storage for the data - this list will contain zero, one or many
    087         * XYSeries objects.
    088         */
    089        private List data = null;
    090
    091        /** Storage for the x values. */
    092        private HashSet xPoints = null;
    093
    094        /** A flag that controls whether or not events are propogated. */
    095        private boolean propagateEvents = true;
    096
    097        /** A flag that controls auto pruning. */
    098        private boolean autoPrune = false;
    099
    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    }