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