1 /* ===========================================================
2  * JFreeChart : a free chart library for the Java(tm) platform
3  * ===========================================================
4  *
5  * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors.
6  *
7  * Project Info:  http://www.jfree.org/jfreechart/index.html
8  *
9  * This library is free software; you can redistribute it and/or modify it
10  * under the terms of the GNU Lesser General Public License as published by
11  * the Free Software Foundation; either version 2.1 of the License, or
12  * (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful, but
15  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
17  * License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with this library; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
22  * USA.
23  *
24  * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
25  * Other names may be trademarks of their respective owners.]
26  *
27  * -----------------------
28  * IntervalXYDelegate.java
29  * -----------------------
30  * (C) Copyright 2004-2013, by Andreas Schroeder and Contributors.
31  *
32  * Original Author:  Andreas Schroeder;
33  * Contributor(s):   David Gilbert (for Object Refinery Limited);
34  *
35  * Changes
36  * -------
37  * 31-Mar-2004 : Version 1 (AS);
38  * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
39  *               getYValue() (DG);
40  * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
41  * 04-Nov-2004 : Added argument check for setIntervalWidth() method (DG);
42  * 17-Nov-2004 : New methods to reflect changes in DomainInfo (DG);
43  * 11-Jan-2005 : Removed deprecated methods in preparation for the 1.0.0
44  *               release (DG);
45  * 21-Feb-2005 : Made public and added equals() method (DG);
46  * 06-Oct-2005 : Implemented DatasetChangeListener to recalculate
47  *               autoIntervalWidth (DG);
48  * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
49  * 06-Mar-2009 : Implemented hashCode() (DG);
50  * 02-Jul-2013 : Use ParamChecks (DG);
51  *
52  */
53 
54 package org.jfree.data.xy;
55 
56 import java.io.Serializable;
57 
58 import org.jfree.chart.HashUtilities;
59 import org.jfree.chart.util.ParamChecks;
60 import org.jfree.data.DomainInfo;
61 import org.jfree.data.Range;
62 import org.jfree.data.RangeInfo;
63 import org.jfree.data.general.DatasetChangeEvent;
64 import org.jfree.data.general.DatasetChangeListener;
65 import org.jfree.data.general.DatasetUtilities;
66 import org.jfree.util.PublicCloneable;
67 
68 /**
69  * A delegate that handles the specification or automatic calculation of the
70  * interval surrounding the x-values in a dataset.  This is used to extend
71  * a regular {@link XYDataset} to support the {@link IntervalXYDataset}
72  * interface.
73  * <p>
74  * The decorator pattern was not used because of the several possibly
75  * implemented interfaces of the decorated instance (e.g.
76  * {@link TableXYDataset}, {@link RangeInfo}, {@link DomainInfo} etc.).
77  * <p>
78  * The width can be set manually or calculated automatically. The switch
79  * autoWidth allows to determine which behavior is used. The auto width
80  * calculation tries to find the smallest gap between two x-values in the
81  * dataset.  If there is only one item in the series, the auto width
82  * calculation fails and falls back on the manually set interval width (which
83  * is itself defaulted to 1.0).
84  */
85 public class IntervalXYDelegate implements DatasetChangeListener,
86         DomainInfo, Serializable, Cloneable, PublicCloneable {
87 
88     /** For serialization. */
89     private static final long serialVersionUID = -685166711639592857L;
90 
91     /**
92      * The dataset to enhance.
93      */
94     private XYDataset dataset;
95 
96     /**
97      * A flag to indicate whether the width should be calculated automatically.
98      */
99     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      */
IntervalXYDelegate(XYDataset dataset)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      */
IntervalXYDelegate(XYDataset dataset, boolean autoWidth)133     public IntervalXYDelegate(XYDataset dataset, boolean autoWidth) {
134         ParamChecks.nullNotPermitted(dataset, "dataset");
135         this.dataset = dataset;
136         this.autoWidth = autoWidth;
137         this.intervalPositionFactor = 0.5;
138         this.autoIntervalWidth = Double.POSITIVE_INFINITY;
139         this.fixedIntervalWidth = 1.0;
140     }
141 
142     /**
143      * Returns <code>true</code> if the interval width is automatically
144      * calculated, and <code>false</code> otherwise.
145      *
146      * @return A boolean.
147      */
isAutoWidth()148     public boolean isAutoWidth() {
149         return this.autoWidth;
150     }
151 
152     /**
153      * Sets the flag that indicates whether the interval width is automatically
154      * calculated.  If the flag is set to <code>true</code>, the interval is
155      * recalculated.
156      * <p>
157      * Note: recalculating the interval amounts to changing the data values
158      * represented by the dataset.  The calling dataset must fire an
159      * appropriate {@link DatasetChangeEvent}.
160      *
161      * @param b  a boolean.
162      */
setAutoWidth(boolean b)163     public void setAutoWidth(boolean b) {
164         this.autoWidth = b;
165         if (b) {
166             this.autoIntervalWidth = recalculateInterval();
167         }
168     }
169 
170     /**
171      * Returns the interval position factor.
172      *
173      * @return The interval position factor.
174      */
getIntervalPositionFactor()175     public double getIntervalPositionFactor() {
176         return this.intervalPositionFactor;
177     }
178 
179     /**
180      * Sets the interval position factor.  This controls how the interval is
181      * aligned to the x-value.  For a value of 0.5, the interval is aligned
182      * with the x-value in the center.  For a value of 0.0, the interval is
183      * aligned with the x-value at the lower end of the interval, and for a
184      * value of 1.0, the interval is aligned with the x-value at the upper
185      * end of the interval.
186      * <br><br>
187      * Note that changing the interval position factor amounts to changing the
188      * data values represented by the dataset.  Therefore, the dataset that is
189      * using this delegate is responsible for generating the
190      * appropriate {@link DatasetChangeEvent}.
191      *
192      * @param d  the new interval position factor (in the range
193      *           <code>0.0</code> to <code>1.0</code> inclusive).
194      */
setIntervalPositionFactor(double d)195     public void setIntervalPositionFactor(double d) {
196         if (d < 0.0 || 1.0 < d) {
197             throw new IllegalArgumentException(
198                     "Argument 'd' outside valid range.");
199         }
200         this.intervalPositionFactor = d;
201     }
202 
203     /**
204      * Returns the fixed interval width.
205      *
206      * @return The fixed interval width.
207      */
getFixedIntervalWidth()208     public double getFixedIntervalWidth() {
209         return this.fixedIntervalWidth;
210     }
211 
212     /**
213      * Sets the fixed interval width and, as a side effect, sets the
214      * <code>autoWidth</code> flag to <code>false</code>.
215      * <br><br>
216      * Note that changing the interval width amounts to changing the data
217      * values represented by the dataset.  Therefore, the dataset
218      * that is using this delegate is responsible for generating the
219      * appropriate {@link DatasetChangeEvent}.
220      *
221      * @param w  the width (negative values not permitted).
222      */
setFixedIntervalWidth(double w)223     public void setFixedIntervalWidth(double w) {
224         if (w < 0.0) {
225             throw new IllegalArgumentException("Negative 'w' argument.");
226         }
227         this.fixedIntervalWidth = w;
228         this.autoWidth = false;
229     }
230 
231     /**
232      * Returns the interval width.  This method will return either the
233      * auto calculated interval width or the manually specified interval
234      * width, depending on the {@link #isAutoWidth()} result.
235      *
236      * @return The interval width to use.
237      */
getIntervalWidth()238     public double getIntervalWidth() {
239         if (isAutoWidth() && !Double.isInfinite(this.autoIntervalWidth)) {
240             // everything is fine: autoWidth is on, and an autoIntervalWidth
241             // was set.
242             return this.autoIntervalWidth;
243         }
244         else {
245             // either autoWidth is off or autoIntervalWidth was not set.
246             return this.fixedIntervalWidth;
247         }
248     }
249 
250     /**
251      * Returns the start value of the x-interval for an item within a series.
252      *
253      * @param series  the series index.
254      * @param item  the item index.
255      *
256      * @return The start value of the x-interval (possibly <code>null</code>).
257      *
258      * @see #getStartXValue(int, int)
259      */
getStartX(int series, int item)260     public Number getStartX(int series, int item) {
261         Number startX = null;
262         Number x = this.dataset.getX(series, item);
263         if (x != null) {
264             startX = new Double(x.doubleValue()
265                      - (getIntervalPositionFactor() * getIntervalWidth()));
266         }
267         return startX;
268     }
269 
270     /**
271      * Returns the start value of the x-interval for an item within a series.
272      *
273      * @param series  the series index.
274      * @param item  the item index.
275      *
276      * @return The start value of the x-interval.
277      *
278      * @see #getStartX(int, int)
279      */
getStartXValue(int series, int item)280     public double getStartXValue(int series, int item) {
281         return this.dataset.getXValue(series, item)
282                 - getIntervalPositionFactor() * getIntervalWidth();
283     }
284 
285     /**
286      * Returns the end value of the x-interval for an item within a series.
287      *
288      * @param series  the series index.
289      * @param item  the item index.
290      *
291      * @return The end value of the x-interval (possibly <code>null</code>).
292      *
293      * @see #getEndXValue(int, int)
294      */
getEndX(int series, int item)295     public Number getEndX(int series, int item) {
296         Number endX = null;
297         Number x = this.dataset.getX(series, item);
298         if (x != null) {
299             endX = new Double(x.doubleValue()
300                 + ((1.0 - getIntervalPositionFactor()) * getIntervalWidth()));
301         }
302         return endX;
303     }
304 
305     /**
306      * Returns the end value of the x-interval for an item within a series.
307      *
308      * @param series  the series index.
309      * @param item  the item index.
310      *
311      * @return The end value of the x-interval.
312      *
313      * @see #getEndX(int, int)
314      */
getEndXValue(int series, int item)315     public double getEndXValue(int series, int item) {
316         return this.dataset.getXValue(series, item)
317                 + (1.0 - getIntervalPositionFactor()) * getIntervalWidth();
318     }
319 
320     /**
321      * Returns the minimum x-value in the dataset.
322      *
323      * @param includeInterval  a flag that determines whether or not the
324      *                         x-interval is taken into account.
325      *
326      * @return The minimum value.
327      */
328     @Override
getDomainLowerBound(boolean includeInterval)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     @Override
getDomainUpperBound(boolean includeInterval)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     @Override
getDomainBounds(boolean includeInterval)366     public Range getDomainBounds(boolean includeInterval) {
367         // first get the range without the interval, then expand it for the
368         // interval width
369         Range range = DatasetUtilities.findDomainBounds(this.dataset, false);
370         if (includeInterval && range != null) {
371             double lowerAdj = getIntervalWidth() * getIntervalPositionFactor();
372             double upperAdj = getIntervalWidth() - lowerAdj;
373             range = new Range(range.getLowerBound() - lowerAdj,
374                 range.getUpperBound() + upperAdj);
375         }
376         return range;
377     }
378 
379     /**
380      * Handles events from the dataset by recalculating the interval if
381      * necessary.
382      *
383      * @param e  the event.
384      */
385     @Override
datasetChanged(DatasetChangeEvent e)386     public void datasetChanged(DatasetChangeEvent e) {
387         // TODO: by coding the event with some information about what changed
388         // in the dataset, we could make the recalculation of the interval
389         // more efficient in some cases (for instance, if the change is
390         // just an update to a y-value, then the x-interval doesn't need
391         // updating)...
392         if (this.autoWidth) {
393             this.autoIntervalWidth = recalculateInterval();
394         }
395     }
396 
397     /**
398      * Recalculate the minimum width "from scratch".
399      *
400      * @return The minimum width.
401      */
recalculateInterval()402     private double recalculateInterval() {
403         double result = Double.POSITIVE_INFINITY;
404         int seriesCount = this.dataset.getSeriesCount();
405         for (int series = 0; series < seriesCount; series++) {
406             result = Math.min(result, calculateIntervalForSeries(series));
407         }
408         return result;
409     }
410 
411     /**
412      * Calculates the interval width for a given series.
413      *
414      * @param series  the series index.
415      *
416      * @return The interval width.
417      */
calculateIntervalForSeries(int series)418     private double calculateIntervalForSeries(int series) {
419         double result = Double.POSITIVE_INFINITY;
420         int itemCount = this.dataset.getItemCount(series);
421         if (itemCount > 1) {
422             double prev = this.dataset.getXValue(series, 0);
423             for (int item = 1; item < itemCount; item++) {
424                 double x = this.dataset.getXValue(series, item);
425                 result = Math.min(result, x - prev);
426                 prev = x;
427             }
428         }
429         return result;
430     }
431 
432     /**
433      * Tests the delegate for equality with an arbitrary object.  The
434      * equality test considers two delegates to be equal if they would
435      * calculate the same intervals for any given dataset (for this reason, the
436      * dataset itself is NOT included in the equality test, because it is just
437      * a reference back to the current 'owner' of the delegate).
438      *
439      * @param obj  the object (<code>null</code> permitted).
440      *
441      * @return A boolean.
442      */
443     @Override
equals(Object obj)444     public boolean equals(Object obj) {
445         if (obj == this) {
446             return true;
447         }
448         if (!(obj instanceof IntervalXYDelegate)) {
449             return false;
450         }
451         IntervalXYDelegate that = (IntervalXYDelegate) obj;
452         if (this.autoWidth != that.autoWidth) {
453             return false;
454         }
455         if (this.intervalPositionFactor != that.intervalPositionFactor) {
456             return false;
457         }
458         if (this.fixedIntervalWidth != that.fixedIntervalWidth) {
459             return false;
460         }
461         return true;
462     }
463 
464     /**
465      * @return A clone of this delegate.
466      *
467      * @throws CloneNotSupportedException if the object cannot be cloned.
468      */
469     @Override
clone()470     public Object clone() throws CloneNotSupportedException {
471         return super.clone();
472     }
473 
474     /**
475      * Returns a hash code for this instance.
476      *
477      * @return A hash code.
478      */
479     @Override
hashCode()480     public int hashCode() {
481         int hash = 5;
482         hash = HashUtilities.hashCode(hash, this.autoWidth);
483         hash = HashUtilities.hashCode(hash, this.intervalPositionFactor);
484         hash = HashUtilities.hashCode(hash, this.fixedIntervalWidth);
485         return hash;
486     }
487 
488 }
489