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