    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     * IntervalXYDelegate.java
    029     * -----------------------
    030     * (C) Copyright 2004, 2005, 2007, by Andreas Schroeder and Contributors.
    031     *
    032     * Original Author:  Andreas Schroeder;
    033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
    034     *
    035     * Changes
    036     * -------
    037     * 31-Mar-2004 : Version 1 (AS);
    038     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
    039     *               getYValue() (DG);
    040     * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
    041     * 04-Nov-2004 : Added argument check for setIntervalWidth() method (DG);
    042     * 17-Nov-2004 : New methods to reflect changes in DomainInfo (DG);
    043     * 11-Jan-2005 : Removed deprecated methods in preparation for the 1.0.0
    044     *               release (DG);
    045     * 21-Feb-2005 : Made public and added equals() method (DG);
    046     * 06-Oct-2005 : Implemented DatasetChangeListener to recalculate
    047     *               autoIntervalWidth (DG);
    048     * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
    049     *
    050     */
    051
    052    package org.jfree.data.xy;
    053
    054    import java.io.Serializable;
    055
    056    import org.jfree.data.DomainInfo;
    057    import org.jfree.data.Range;
    058    import org.jfree.data.RangeInfo;
    059    import org.jfree.data.general.DatasetChangeEvent;
    060    import org.jfree.data.general.DatasetChangeListener;
    061    import org.jfree.data.general.DatasetUtilities;
    062    import org.jfree.util.PublicCloneable;
    063
    064    /**
    065     * A delegate that handles the specification or automatic calculation of the
    066     * interval surrounding the x-values in a dataset.  This is used to extend
    067     * a regular {@link XYDataset} to support the {@link IntervalXYDataset}
    068     * interface.
    069     * <p>
    070     * The decorator pattern was not used because of the several possibly
    071     * implemented interfaces of the decorated instance (e.g.
    072     * {@link TableXYDataset}, {@link RangeInfo}, {@link DomainInfo} etc.).
    073     * <p>
    074     * The width can be set manually or calculated automatically. The switch
    075     * autoWidth allows to determine which behavior is used. The auto width
    076     * calculation tries to find the smallest gap between two x-values in the
    077     * dataset.  If there is only one item in the series, the auto width
    078     * calculation fails and falls back on the manually set interval width (which
    079     * is itself defaulted to 1.0).
    080     */
    081    public class IntervalXYDelegate implements DatasetChangeListener,
    082                                               DomainInfo, Serializable,
    083                                               Cloneable, PublicCloneable {
    084
    085        /** For serialization. */
    086        private static final long serialVersionUID = -685166711639592857L;
    087
    088        /**
    089         * The dataset to enhance.
    090         */
    091        private XYDataset dataset;
    092
    093        /**
    094         * A flag to indicate whether the width should be calculated automatically.
    095         */
    096        private boolean autoWidth;
    097
    098        /**
    099         * A value between 0.0 and 1.0 that indicates the position of the x-value
    100         * within the interval.
    101         */
    102        private double intervalPositionFactor;
    103
    104        /**
    105         * The fixed interval width (defaults to 1.0).
    106         */
    107        private double fixedIntervalWidth;
    108
    109        /**
    110         * The automatically calculated interval width.
    111         */
    112        private double autoIntervalWidth;
    113
    114        /**
    115         * Creates a new delegate that.
    116         *
    117         * @param dataset  the underlying dataset (<code>null</code> not permitted).
    118         */
    119        public IntervalXYDelegate(XYDataset dataset) {
    120            this(dataset, true);
    121        }
    122
    123        /**
    124         * Creates a new delegate for the specified dataset.
    125         *
    126         * @param dataset  the underlying dataset (<code>null</code> not permitted).
    127         * @param autoWidth  a flag that controls whether the interval width is
    128         *                   calculated automatically.
    129         */
    130        public IntervalXYDelegate(XYDataset dataset, boolean autoWidth) {
    131            if (dataset == null) {
    132                throw new IllegalArgumentException("Null 'dataset' argument.");
    133            }
    134            this.dataset = dataset;
    135            this.autoWidth = autoWidth;
    136            this.intervalPositionFactor = 0.5;
    137            this.autoIntervalWidth = Double.POSITIVE_INFINITY;
    138            this.fixedIntervalWidth = 1.0;
    139        }
    140
    141        /**
    142         * Returns <code>true</code> if the interval width is automatically
    143         * calculated, and <code>false</code> otherwise.
    144         *
    145         * @return A boolean.
    146         */
    147        public boolean isAutoWidth() {
    148            return this.autoWidth;
    149        }
    150
    151        /**
    152         * Sets the flag that indicates whether the interval width is automatically
    153         * calculated.  If the flag is set to <code>true</code>, the interval is
    154         * recalculated.
    155         * <p>
    156         * Note: recalculating the interval amounts to changing the data values
    157         * represented by the dataset.  The calling dataset must fire an
    158         * appropriate {@link DatasetChangeEvent}.
    159         *
    160         * @param b  a boolean.
    161         */
    162        public void setAutoWidth(boolean b) {
    163            this.autoWidth = b;
    164            if (b) {
    165                this.autoIntervalWidth = recalculateInterval();
    166            }
    167        }
    168
    169        /**
    170         * Returns the interval position factor.
    171         *
    172         * @return The interval position factor.
    173         */
    174        public double getIntervalPositionFactor() {
    175            return this.intervalPositionFactor;
    176        }
    177
    178        /**
    179         * Sets the interval position factor.  This controls how the interval is
    180         * aligned to the x-value.  For a value of 0.5, the interval is aligned
    181         * with the x-value in the center.  For a value of 0.0, the interval is
    182         * aligned with the x-value at the lower end of the interval, and for a
    183         * value of 1.0, the interval is aligned with the x-value at the upper
    184         * end of the interval.
    185         *
    186         * Note that changing the interval position factor amounts to changing the
    187         * data values represented by the dataset.  Therefore, the dataset that is
    188         * using this delegate is responsible for generating the
    189         * appropriate {@link DatasetChangeEvent}.
    190         *
    191         * @param d  the new interval position factor (in the range
    192         *           <code>0.0</code> to <code>1.0</code> inclusive).
    193         */
    194        public void setIntervalPositionFactor(double d) {
    195            if (d < 0.0 || 1.0 < d) {
    196                throw new IllegalArgumentException(
    197                        "Argument 'd' outside valid range.");
    198            }
    199            this.intervalPositionFactor = d;
    200        }
    201
    202        /**
    203         * Returns the fixed interval width.
    204         *
    205         * @return The fixed interval width.
    206         */
    207        public double getFixedIntervalWidth() {
    208            return this.fixedIntervalWidth;
    209        }
    210
    211        /**
    212         * Sets the fixed interval width and, as a side effect, sets the
    213         * <code>autoWidth</code> flag to <code>false</code>.
    214         *
    215         * Note that changing the interval width amounts to changing the data
    216         * values represented by the dataset.  Therefore, the dataset
    217         * that is using this delegate is responsible for generating the
    218         * appropriate {@link DatasetChangeEvent}.
    219         *
    220         * @param w  the width (negative values not permitted).
    221         */
    222        public void setFixedIntervalWidth(double w) {
    223            if (w < 0.0) {
    224                throw new IllegalArgumentException("Negative 'w' argument.");
    225            }
    226            this.fixedIntervalWidth = w;
    227            this.autoWidth = false;
    228        }
    229
    230        /**
    231         * Returns the interval width.  This method will return either the
    232         * auto calculated interval width or the manually specified interval
    233         * width, depending on the {@link #isAutoWidth()} result.
    234         *
    235         * @return The interval width to use.
    236         */
    237        public double getIntervalWidth() {
    238            if (isAutoWidth() && !Double.isInfinite(this.autoIntervalWidth)) {
    239                // everything is fine: autoWidth is on, and an autoIntervalWidth
    240                // was set.
    241                return this.autoIntervalWidth;
    242            }
    243            else {
    244                // either autoWidth is off or autoIntervalWidth was not set.
    245                return this.fixedIntervalWidth;
    246            }
    247        }
    248
    249        /**
    250         * Returns the start value of the x-interval for an item within a series.
    251         *
    252         * @param series  the series index.
    253         * @param item  the item index.
    254         *
    255         * @return The start value of the x-interval (possibly <code>null</code>).
    256         *
    257         * @see #getStartXValue(int, int)
    258         */
    259        public Number getStartX(int series, int item) {
    260            Number startX = null;
    261            Number x = this.dataset.getX(series, item);
    262            if (x != null) {
    263                startX = new Double(x.doubleValue()
    264                         - (getIntervalPositionFactor() * getIntervalWidth()));
    265            }
    266            return startX;
    267        }
    268
    269        /**
    270         * Returns the start value of the x-interval for an item within a series.
    271         *
    272         * @param series  the series index.
    273         * @param item  the item index.
    274         *
    275         * @return The start value of the x-interval.
    276         *
    277         * @see #getStartX(int, int)
    278         */
    279        public double getStartXValue(int series, int item) {
    280            return this.dataset.getXValue(series, item)
    281                    - getIntervalPositionFactor() * getIntervalWidth();
    282        }
    283
    284        /**
    285         * Returns the end value of the x-interval for an item within a series.
    286         *
    287         * @param series  the series index.
    288         * @param item  the item index.
    289         *
    290         * @return The end value of the x-interval (possibly <code>null</code>).
    291         *
    292         * @see #getEndXValue(int, int)
    293         */
    294        public Number getEndX(int series, int item) {
    295            Number endX = null;
    296            Number x = this.dataset.getX(series, item);
    297            if (x != null) {
    298                endX = new Double(x.doubleValue()
    299                    + ((1.0 - getIntervalPositionFactor()) * getIntervalWidth()));
    300            }
    301            return endX;
    302        }
    303
    304        /**
    305         * Returns the end value of the x-interval for an item within a series.
    306         *
    307         * @param series  the series index.
    308         * @param item  the item index.
    309         *
    310         * @return The end value of the x-interval.
    311         *
    312         * @see #getEndX(int, int)
    313         */
    314        public double getEndXValue(int series, int item) {
    315            return this.dataset.getXValue(series, item)
    316                    + (1.0 - getIntervalPositionFactor()) * getIntervalWidth();
    317        }
    318
    319        /**
    320         * Returns the minimum x-value in the dataset.
    321         *
    322         * @param includeInterval  a flag that determines whether or not the
    323         *                         x-interval is taken into account.
    324         *
    325         * @return The minimum value.
    326         */
    327        public double getDomainLowerBound(boolean includeInterval) {
    328            double result = Double.NaN;
    329            Range r = getDomainBounds(includeInterval);
    330            if (r != null) {
    331                result = r.getLowerBound();
    332            }
    333            return result;
    334        }
    335
    336        /**
    337         * Returns the maximum x-value in the dataset.
    338         *
    339         * @param includeInterval  a flag that determines whether or not the
    340         *                         x-interval is taken into account.
    341         *
    342         * @return The maximum value.
    343         */
    344        public double getDomainUpperBound(boolean includeInterval) {
    345            double result = Double.NaN;
    346            Range r = getDomainBounds(includeInterval);
    347            if (r != null) {
    348                result = r.getUpperBound();
    349            }
    350            return result;
    351        }
    352
    353        /**
    354         * Returns the range of the values in the dataset's domain, including
    355         * or excluding the interval around each x-value as specified.
    356         *
    357         * @param includeInterval  a flag that determines whether or not the
    358         *                         x-interval should be taken into account.
    359         *
    360         * @return The range.
    361         */
    362        public Range getDomainBounds(boolean includeInterval) {
    363            // first get the range without the interval, then expand it for the
    364            // interval width
    365            Range range = DatasetUtilities.findDomainBounds(this.dataset, false);
    366            if (includeInterval && range != null) {
    367                double lowerAdj = getIntervalWidth() * getIntervalPositionFactor();
    368                double upperAdj = getIntervalWidth() - lowerAdj;
    369                range = new Range(range.getLowerBound() - lowerAdj,
    370                    range.getUpperBound() + upperAdj);
    371            }
    372            return range;
    373        }
    374
    375        /**
    376         * Handles events from the dataset by recalculating the interval if
    377         * necessary.
    378         *
    379         * @param e  the event.
    380         */
    381        public void datasetChanged(DatasetChangeEvent e) {
    382            // TODO: by coding the event with some information about what changed
    383            // in the dataset, we could make the recalculation of the interval
    384            // more efficient in some cases...
    385            if (this.autoWidth) {
    386                this.autoIntervalWidth = recalculateInterval();
    387            }
    388        }
    389
    390        /**
    391         * Recalculate the minimum width "from scratch".
    392         *
    393         * @return The minimum width.
    394         */
    395        private double recalculateInterval() {
    396            double result = Double.POSITIVE_INFINITY;
    397            int seriesCount = this.dataset.getSeriesCount();
    398            for (int series = 0; series < seriesCount; series++) {
    399                result = Math.min(result, calculateIntervalForSeries(series));
    400            }
    401            return result;
    402        }
    403
    404        /**
    405         * Calculates the interval width for a given series.
    406         *
    407         * @param series  the series index.
    408         *
    409         * @return The interval width.
    410         */
    411        private double calculateIntervalForSeries(int series) {
    412            double result = Double.POSITIVE_INFINITY;
    413            int itemCount = this.dataset.getItemCount(series);
    414            if (itemCount > 1) {
    415                double prev = this.dataset.getXValue(series, 0);
    416                for (int item = 1; item < itemCount; item++) {
    417                    double x = this.dataset.getXValue(series, item);
    418                    result = Math.min(result, x - prev);
    419                    prev = x;
    420                }
    421            }
    422            return result;
    423        }
    424
    425        /**
    426         * Tests the delegate for equality with an arbitrary object.
    427         *
    428         * @param obj  the object (<code>null</code> permitted).
    429         *
    430         * @return A boolean.
    431         */
    432        public boolean equals(Object obj) {
    433            if (obj == this) {
    434                return true;
    435            }
    436            if (!(obj instanceof IntervalXYDelegate)) {
    437                return false;
    438            }
    439            IntervalXYDelegate that = (IntervalXYDelegate) obj;
    440            if (this.autoWidth != that.autoWidth) {
    441                return false;
    442            }
    443            if (this.intervalPositionFactor != that.intervalPositionFactor) {
    444                return false;
    445            }
    446            if (this.fixedIntervalWidth != that.fixedIntervalWidth) {
    447                return false;
    448            }
    449            return true;
    450        }
    451
    452        /**
    453         * @return A clone of this delegate.
    454         *
    455         * @throws CloneNotSupportedException if the object cannot be cloned.
    456         */
    457        public Object clone() throws CloneNotSupportedException {
    458            return super.clone();
    459        }
    460
    461    }