    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2007, 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-2007, 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     *
    059     */
    060
    061    package org.jfree.data.xy;
    062
    063    import java.util.ArrayList;
    064    import java.util.HashSet;
    065    import java.util.Iterator;
    066    import java.util.List;
    067
    068    import org.jfree.data.DomainInfo;
    069    import org.jfree.data.Range;
    070    import org.jfree.data.general.DatasetChangeEvent;
    071    import org.jfree.data.general.DatasetUtilities;
    072    import org.jfree.data.general.SeriesChangeEvent;
    073    import org.jfree.util.ObjectUtilities;
    074
    075    /**
    076     * An {@link XYDataset} where every series shares the same x-values (required
    077     * for generating stacked area charts).
    078     */
    079    public class DefaultTableXYDataset extends AbstractIntervalXYDataset
    080                                       implements TableXYDataset,
    081                                                  IntervalXYDataset, DomainInfo {
    082
    083        /**
    084         * Storage for the data - this list will contain zero, one or many
    085         * XYSeries objects.
    086         */
    087        private List data = null;
    088
    089        /** Storage for the x values. */
    090        private HashSet xPoints = null;
    091
    092        /** A flag that controls whether or not events are propogated. */
    093        private boolean propagateEvents = true;
    094
    095        /** A flag that controls auto pruning. */
    096        private boolean autoPrune = false;
    097
    098        /** The delegate used to control the interval width. */
    099        private IntervalXYDelegate intervalDelegate;
    100
    101        /**
    102         * Creates a new empty dataset.
    103         */
    104        public DefaultTableXYDataset() {
    105            this(false);
    106        }
    107
    108        /**
    109         * Creates a new empty dataset.
    110         *
    111         * @param autoPrune  a flag that controls whether or not x-values are
    112         *                   removed whenever the corresponding y-values are all
    113         *                   <code>null</code>.
    114         */
    115        public DefaultTableXYDataset(boolean autoPrune) {
    116            this.autoPrune = autoPrune;
    117            this.data = new ArrayList();
    118            this.xPoints = new HashSet();
    119            this.intervalDelegate = new IntervalXYDelegate(this, false);
    120            addChangeListener(this.intervalDelegate);
    121        }
    122
    123        /**
    124         * Returns the flag that controls whether or not x-values are removed from
    125         * the dataset when the corresponding y-values are all <code>null</code>.
    126         *
    127         * @return A boolean.
    128         */
    129        public boolean isAutoPrune() {
    130            return this.autoPrune;
    131        }
    132
    133        /**
    134         * Adds a series to the collection and sends a {@link DatasetChangeEvent}
    135         * to all registered listeners.  The series should be configured to NOT
    136         * allow duplicate x-values.
    137         *
    138         * @param series  the series (<code>null</code> not permitted).
    139         */
    140        public void addSeries(XYSeries series) {
    141            if (series == null) {
    142                throw new IllegalArgumentException("Null 'series' argument.");
    143            }
    144            if (series.getAllowDuplicateXValues()) {
    145                throw new IllegalArgumentException(
    146                    "Cannot accept XYSeries that allow duplicate values. "
    147                    + "Use XYSeries(seriesName, <sort>, false) constructor."
    148                );
    149            }
    150            updateXPoints(series);
    151            this.data.add(series);
    152            series.addChangeListener(this);
    153            fireDatasetChanged();
    154        }
    155
    156        /**
    157         * Adds any unique x-values from 'series' to the dataset, and also adds any
    158         * x-values that are in the dataset but not in 'series' to the series.
    159         *
    160         * @param series  the series (<code>null</code> not permitted).
    161         */
    162        private void updateXPoints(XYSeries series) {
    163            if (series == null) {
    164                throw new IllegalArgumentException("Null 'series' not permitted.");
    165            }
    166            HashSet seriesXPoints = new HashSet();
    167            boolean savedState = this.propagateEvents;
    168            this.propagateEvents = false;
    169            for (int itemNo = 0; itemNo < series.getItemCount(); itemNo++) {
    170                Number xValue = series.getX(itemNo);
    171                seriesXPoints.add(xValue);
    172                if (!this.xPoints.contains(xValue)) {
    173                    this.xPoints.add(xValue);
    174                    int seriesCount = this.data.size();
    175                    for (int seriesNo = 0; seriesNo < seriesCount; seriesNo++) {
    176                        XYSeries dataSeries = (XYSeries) this.data.get(seriesNo);
    177                        if (!dataSeries.equals(series)) {
    178                            dataSeries.add(xValue, null);
    179                        }
    180                    }
    181                }
    182            }
    183            Iterator iterator = this.xPoints.iterator();
    184            while (iterator.hasNext()) {
    185                Number xPoint = (Number) iterator.next();
    186                if (!seriesXPoints.contains(xPoint)) {
    187                    series.add(xPoint, null);
    188                }
    189            }
    190            this.propagateEvents = savedState;
    191        }
    192
    193        /**
    194         * Updates the x-values for all the series in the dataset.
    195         */
    196        public void updateXPoints() {
    197            this.propagateEvents = false;
    198            for (int s = 0; s < this.data.size(); s++) {
    199                updateXPoints((XYSeries) this.data.get(s));
    200            }
    201            if (this.autoPrune) {
    202                prune();
    203            }
    204            this.propagateEvents = true;
    205        }
    206
    207        /**
    208         * Returns the number of series in the collection.
    209         *
    210         * @return The series count.
    211         */
    212        public int getSeriesCount() {
    213            return this.data.size();
    214        }
    215
    216        /**
    217         * Returns the number of x values in the dataset.
    218         *
    219         * @return The number of x values in the dataset.
    220         */
    221        public int getItemCount() {
    222            if (this.xPoints == null) {
    223                return 0;
    224            }
    225            else {
    226                return this.xPoints.size();
    227            }
    228        }
    229
    230        /**
    231         * Returns a series.
    232         *
    233         * @param series  the series (zero-based index).
    234         *
    235         * @return The series (never <code>null</code>).
    236         */
    237        public XYSeries getSeries(int series) {
    238            if ((series < 0) || (series >= getSeriesCount())) {
    239                throw new IllegalArgumentException("Index outside valid range.");
    240            }
    241            return (XYSeries) this.data.get(series);
    242        }
    243
    244        /**
    245         * Returns the key for a series.
    246         *
    247         * @param series  the series (zero-based index).
    248         *
    249         * @return The key for a series.
    250         */
    251        public Comparable getSeriesKey(int series) {
    252            // check arguments...delegated
    253            return getSeries(series).getKey();
    254        }
    255
    256        /**
    257         * Returns the number of items in the specified series.
    258         *
    259         * @param series  the series (zero-based index).
    260         *
    261         * @return The number of items in the specified series.
    262         */
    263        public int getItemCount(int series) {
    264            // check arguments...delegated
    265            return getSeries(series).getItemCount();
    266        }
    267
    268        /**
    269         * Returns the x-value for the specified series and item.
    270         *
    271         * @param series  the series (zero-based index).
    272         * @param item  the item (zero-based index).
    273         *
    274         * @return The x-value for the specified series and item.
    275         */
    276        public Number getX(int series, int item) {
    277            XYSeries s = (XYSeries) this.data.get(series);
    278            XYDataItem dataItem = s.getDataItem(item);
    279            return dataItem.getX();
    280        }
    281
    282        /**
    283         * Returns the starting X value for the specified series and item.
    284         *
    285         * @param series  the series (zero-based index).
    286         * @param item  the item (zero-based index).
    287         *
    288         * @return The starting X value.
    289         */
    290        public Number getStartX(int series, int item) {
    291            return this.intervalDelegate.getStartX(series, item);
    292        }
    293
    294        /**
    295         * Returns the ending X value for the specified series and item.
    296         *
    297         * @param series  the series (zero-based index).
    298         * @param item  the item (zero-based index).
    299         *
    300         * @return The ending X value.
    301         */
    302        public Number getEndX(int series, int item) {
    303            return this.intervalDelegate.getEndX(series, item);
    304        }
    305
    306        /**
    307         * Returns the y-value for the specified series and item.
    308         *
    309         * @param series  the series (zero-based index).
    310         * @param index  the index of the item of interest (zero-based).
    311         *
    312         * @return The y-value for the specified series and item (possibly
    313         *         <code>null</code>).
    314         */
    315        public Number getY(int series, int index) {
    316            XYSeries ts = (XYSeries) this.data.get(series);
    317            XYDataItem dataItem = ts.getDataItem(index);
    318            return dataItem.getY();
    319        }
    320
    321        /**
    322         * Returns the starting Y value for the specified series and item.
    323         *
    324         * @param series  the series (zero-based index).
    325         * @param item  the item (zero-based index).
    326         *
    327         * @return The starting Y value.
    328         */
    329        public Number getStartY(int series, int item) {
    330            return getY(series, item);
    331        }
    332
    333        /**
    334         * Returns the ending Y value for the specified series and item.
    335         *
    336         * @param series  the series (zero-based index).
    337         * @param item  the item (zero-based index).
    338         *
    339         * @return The ending Y value.
    340         */
    341        public Number getEndY(int series, int item) {
    342            return getY(series, item);
    343        }
    344
    345        /**
    346         * Removes all the series from the collection and sends a
    347         * {@link DatasetChangeEvent} to all registered listeners.
    348         */
    349        public void removeAllSeries() {
    350
    351            // Unregister the collection as a change listener to each series in
    352            // the collection.
    353            for (int i = 0; i < this.data.size(); i++) {
    354                XYSeries series = (XYSeries) this.data.get(i);
    355                series.removeChangeListener(this);
    356            }
    357
    358            // Remove all the series from the collection and notify listeners.
    359            this.data.clear();
    360            this.xPoints.clear();
    361            fireDatasetChanged();
    362        }
    363
    364        /**
    365         * Removes a series from the collection and sends a
    366         * {@link DatasetChangeEvent} to all registered listeners.
    367         *
    368         * @param series  the series (<code>null</code> not permitted).
    369         */
    370        public void removeSeries(XYSeries series) {
    371
    372            // check arguments...
    373            if (series == null) {
    374                throw new IllegalArgumentException("Null 'series' argument.");
    375            }
    376
    377            // remove the series...
    378            if (this.data.contains(series)) {
    379                series.removeChangeListener(this);
    380                this.data.remove(series);
    381                if (this.data.size() == 0) {
    382                    this.xPoints.clear();
    383                }
    384                fireDatasetChanged();
    385            }
    386
    387        }
    388
    389        /**
    390         * Removes a series from the collection and sends a
    391         * {@link DatasetChangeEvent} to all registered listeners.
    392         *
    393         * @param series  the series (zero based index).
    394         */
    395        public void removeSeries(int series) {
    396
    397            // check arguments...
    398            if ((series < 0) || (series > getSeriesCount())) {
    399                throw new IllegalArgumentException("Index outside valid range.");
    400            }
    401
    402            // fetch the series, remove the change listener, then remove the series.
    403            XYSeries s = (XYSeries) this.data.get(series);
    404            s.removeChangeListener(this);
    405            this.data.remove(series);
    406            if (this.data.size() == 0) {
    407                this.xPoints.clear();
    408            }
    409            else if (this.autoPrune) {
    410                prune();
    411            }
    412            fireDatasetChanged();
    413
    414        }
    415
    416        /**
    417         * Removes the items from all series for a given x value.
    418         *
    419         * @param x  the x-value.
    420         */
    421        public void removeAllValuesForX(Number x) {
    422            if (x == null) {
    423                throw new IllegalArgumentException("Null 'x' argument.");
    424            }
    425            boolean savedState = this.propagateEvents;
    426            this.propagateEvents = false;
    427            for (int s = 0; s < this.data.size(); s++) {
    428                XYSeries series = (XYSeries) this.data.get(s);
    429                series.remove(x);
    430            }
    431            this.propagateEvents = savedState;
    432            this.xPoints.remove(x);
    433            fireDatasetChanged();
    434        }
    435
    436        /**
    437         * Returns <code>true</code> if all the y-values for the specified x-value
    438         * are <code>null</code> and <code>false</code> otherwise.
    439         *
    440         * @param x  the x-value.
    441         *
    442         * @return A boolean.
    443         */
    444        protected boolean canPrune(Number x) {
    445            for (int s = 0; s < this.data.size(); s++) {
    446                XYSeries series = (XYSeries) this.data.get(s);
    447                if (series.getY(series.indexOf(x)) != null) {
    448                    return false;
    449                }
    450            }
    451            return true;
    452        }
    453
    454        /**
    455         * Removes all x-values for which all the y-values are <code>null</code>.
    456         */
    457        public void prune() {
    458            HashSet hs = (HashSet) this.xPoints.clone();
    459            Iterator iterator = hs.iterator();
    460            while (iterator.hasNext()) {
    461                Number x = (Number) iterator.next();
    462                if (canPrune(x)) {
    463                    removeAllValuesForX(x);
    464                }
    465            }
    466        }
    467
    468        /**
    469         * This method receives notification when a series belonging to the dataset
    470         * changes.  It responds by updating the x-points for the entire dataset
    471         * and sending a {@link DatasetChangeEvent} to all registered listeners.
    472         *
    473         * @param event  information about the change.
    474         */
    475        public void seriesChanged(SeriesChangeEvent event) {
    476            if (this.propagateEvents) {
    477                updateXPoints();
    478                fireDatasetChanged();
    479            }
    480        }
    481
    482        /**
    483         * Tests this collection for equality with an arbitrary object.
    484         *
    485         * @param obj  the object (<code>null</code> permitted).
    486         *
    487         * @return A boolean.
    488         */
    489        public boolean equals(Object obj) {
    490            if (obj == this) {
    491                return true;
    492            }
    493            if (!(obj instanceof DefaultTableXYDataset)) {
    494                return false;
    495            }
    496            DefaultTableXYDataset that = (DefaultTableXYDataset) obj;
    497            if (this.autoPrune != that.autoPrune) {
    498                return false;
    499            }
    500            if (this.propagateEvents != that.propagateEvents) {
    501                return false;
    502            }
    503            if (!this.intervalDelegate.equals(that.intervalDelegate)) {
    504                return false;
    505            }
    506            if (!ObjectUtilities.equal(this.data, that.data)) {
    507                return false;
    508            }
    509            return true;
    510        }
    511
    512        /**
    513         * Returns a hash code.
    514         *
    515         * @return A hash code.
    516         */
    517        public int hashCode() {
    518            int result;
    519            result = (this.data != null ? this.data.hashCode() : 0);
    520            result = 29 * result
    521                     + (this.xPoints != null ? this.xPoints.hashCode() : 0);
    522            result = 29 * result + (this.propagateEvents ? 1 : 0);
    523            result = 29 * result + (this.autoPrune ? 1 : 0);
    524            return result;
    525        }
    526
    527        /**
    528         * Returns the minimum x-value in the dataset.
    529         *
    530         * @param includeInterval  a flag that determines whether or not the
    531         *                         x-interval is taken into account.
    532         *
    533         * @return The minimum value.
    534         */
    535        public double getDomainLowerBound(boolean includeInterval) {
    536            return this.intervalDelegate.getDomainLowerBound(includeInterval);
    537        }
    538
    539        /**
    540         * Returns the maximum x-value in the dataset.
    541         *
    542         * @param includeInterval  a flag that determines whether or not the
    543         *                         x-interval is taken into account.
    544         *
    545         * @return The maximum value.
    546         */
    547        public double getDomainUpperBound(boolean includeInterval) {
    548            return this.intervalDelegate.getDomainUpperBound(includeInterval);
    549        }
    550
    551        /**
    552         * Returns the range of the values in this dataset's domain.
    553         *
    554         * @param includeInterval  a flag that determines whether or not the
    555         *                         x-interval is taken into account.
    556         *
    557         * @return The range.
    558         */
    559        public Range getDomainBounds(boolean includeInterval) {
    560            if (includeInterval) {
    561                return this.intervalDelegate.getDomainBounds(includeInterval);
    562            }
    563            else {
    564                return DatasetUtilities.iterateDomainBounds(this, includeInterval);
    565            }
    566        }
    567
    568        /**
    569         * Returns the interval position factor.
    570         *
    571         * @return The interval position factor.
    572         */
    573        public double getIntervalPositionFactor() {
    574            return this.intervalDelegate.getIntervalPositionFactor();
    575        }
    576
    577        /**
    578         * Sets the interval position factor. Must be between 0.0 and 1.0 inclusive.
    579         * If the factor is 0.5, the gap is in the middle of the x values. If it
    580         * is lesser than 0.5, the gap is farther to the left and if greater than
    581         * 0.5 it gets farther to the right.
    582         *
    583         * @param d the new interval position factor.
    584         */
    585        public void setIntervalPositionFactor(double d) {
    586            this.intervalDelegate.setIntervalPositionFactor(d);
    587            fireDatasetChanged();
    588        }
    589
    590        /**
    591         * returns the full interval width.
    592         *
    593         * @return The interval width to use.
    594         */
    595        public double getIntervalWidth() {
    596            return this.intervalDelegate.getIntervalWidth();
    597        }
    598
    599        /**
    600         * Sets the interval width to a fixed value, and sends a
    601         * {@link DatasetChangeEvent} to all registered listeners.
    602         *
    603         * @param d  the new interval width (must be > 0).
    604         */
    605        public void setIntervalWidth(double d) {
    606            this.intervalDelegate.setFixedIntervalWidth(d);
    607            fireDatasetChanged();
    608        }
    609
    610        /**
    611         * Returns whether the interval width is automatically calculated or not.
    612         *
    613         * @return A flag that determines whether or not the interval width is
    614         *         automatically calculated.
    615         */
    616        public boolean isAutoWidth() {
    617            return this.intervalDelegate.isAutoWidth();
    618        }
    619
    620        /**
    621         * Sets the flag that indicates whether the interval width is automatically
    622         * calculated or not.
    623         *
    624         * @param b  a boolean.
    625         */
    626        public void setAutoWidth(boolean b) {
    627            this.intervalDelegate.setAutoWidth(b);
    628            fireDatasetChanged();
    629        }
    630
    631    }