001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2009, 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-2009, 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 * 06-Mar-2009 : Implemented hashCode() (DG);
050 *
051 */
052
053 package org.jfree.data.xy;
054
055 import java.io.Serializable;
056
057 import org.jfree.chart.HashUtilities;
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, Cloneable, PublicCloneable {
085
086 /** For serialization. */
087 private static final long serialVersionUID = -685166711639592857L;
088
089 /**
090 * The dataset to enhance.
091 */
092 private XYDataset dataset;
093
094 /**
095 * A flag to indicate whether the width should be calculated automatically.
096 */
097 private boolean autoWidth;
098
099 /**
100 * A value between 0.0 and 1.0 that indicates the position of the x-value
101 * within the interval.
102 */
103 private double intervalPositionFactor;
104
105 /**
106 * The fixed interval width (defaults to 1.0).
107 */
108 private double fixedIntervalWidth;
109
110 /**
111 * The automatically calculated interval width.
112 */
113 private double autoIntervalWidth;
114
115 /**
116 * Creates a new delegate that.
117 *
118 * @param dataset the underlying dataset (<code>null</code> not permitted).
119 */
120 public IntervalXYDelegate(XYDataset dataset) {
121 this(dataset, true);
122 }
123
124 /**
125 * Creates a new delegate for the specified dataset.
126 *
127 * @param dataset the underlying dataset (<code>null</code> not permitted).
128 * @param autoWidth a flag that controls whether the interval width is
129 * calculated automatically.
130 */
131 public IntervalXYDelegate(XYDataset dataset, boolean autoWidth) {
132 if (dataset == null) {
133 throw new IllegalArgumentException("Null 'dataset' argument.");
134 }
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 public double getDomainLowerBound(boolean includeInterval) {
329 double result = Double.NaN;
330 Range r = getDomainBounds(includeInterval);
331 if (r != null) {
332 result = r.getLowerBound();
333 }
334 return result;
335 }
336
337 /**
338 * Returns the maximum x-value in the dataset.
339 *
340 * @param includeInterval a flag that determines whether or not the
341 * x-interval is taken into account.
342 *
343 * @return The maximum value.
344 */
345 public double getDomainUpperBound(boolean includeInterval) {
346 double result = Double.NaN;
347 Range r = getDomainBounds(includeInterval);
348 if (r != null) {
349 result = r.getUpperBound();
350 }
351 return result;
352 }
353
354 /**
355 * Returns the range of the values in the dataset's domain, including
356 * or excluding the interval around each x-value as specified.
357 *
358 * @param includeInterval a flag that determines whether or not the
359 * x-interval should be taken into account.
360 *
361 * @return The range.
362 */
363 public Range getDomainBounds(boolean includeInterval) {
364 // first get the range without the interval, then expand it for the
365 // interval width
366 Range range = DatasetUtilities.findDomainBounds(this.dataset, false);
367 if (includeInterval && range != null) {
368 double lowerAdj = getIntervalWidth() * getIntervalPositionFactor();
369 double upperAdj = getIntervalWidth() - lowerAdj;
370 range = new Range(range.getLowerBound() - lowerAdj,
371 range.getUpperBound() + upperAdj);
372 }
373 return range;
374 }
375
376 /**
377 * Handles events from the dataset by recalculating the interval if
378 * necessary.
379 *
380 * @param e the event.
381 */
382 public void datasetChanged(DatasetChangeEvent e) {
383 // TODO: by coding the event with some information about what changed
384 // in the dataset, we could make the recalculation of the interval
385 // more efficient in some cases (for instance, if the change is
386 // just an update to a y-value, then the x-interval doesn't need
387 // updating)...
388 if (this.autoWidth) {
389 this.autoIntervalWidth = recalculateInterval();
390 }
391 }
392
393 /**
394 * Recalculate the minimum width "from scratch".
395 *
396 * @return The minimum width.
397 */
398 private double recalculateInterval() {
399 double result = Double.POSITIVE_INFINITY;
400 int seriesCount = this.dataset.getSeriesCount();
401 for (int series = 0; series < seriesCount; series++) {
402 result = Math.min(result, calculateIntervalForSeries(series));
403 }
404 return result;
405 }
406
407 /**
408 * Calculates the interval width for a given series.
409 *
410 * @param series the series index.
411 *
412 * @return The interval width.
413 */
414 private double calculateIntervalForSeries(int series) {
415 double result = Double.POSITIVE_INFINITY;
416 int itemCount = this.dataset.getItemCount(series);
417 if (itemCount > 1) {
418 double prev = this.dataset.getXValue(series, 0);
419 for (int item = 1; item < itemCount; item++) {
420 double x = this.dataset.getXValue(series, item);
421 result = Math.min(result, x - prev);
422 prev = x;
423 }
424 }
425 return result;
426 }
427
428 /**
429 * Tests the delegate for equality with an arbitrary object. The
430 * equality test considers two delegates to be equal if they would
431 * calculate the same intervals for any given dataset (for this reason, the
432 * dataset itself is NOT included in the equality test, because it is just
433 * a reference back to the current 'owner' of the delegate).
434 *
435 * @param obj the object (<code>null</code> permitted).
436 *
437 * @return A boolean.
438 */
439 public boolean equals(Object obj) {
440 if (obj == this) {
441 return true;
442 }
443 if (!(obj instanceof IntervalXYDelegate)) {
444 return false;
445 }
446 IntervalXYDelegate that = (IntervalXYDelegate) obj;
447 if (this.autoWidth != that.autoWidth) {
448 return false;
449 }
450 if (this.intervalPositionFactor != that.intervalPositionFactor) {
451 return false;
452 }
453 if (this.fixedIntervalWidth != that.fixedIntervalWidth) {
454 return false;
455 }
456 return true;
457 }
458
459 /**
460 * @return A clone of this delegate.
461 *
462 * @throws CloneNotSupportedException if the object cannot be cloned.
463 */
464 public Object clone() throws CloneNotSupportedException {
465 return super.clone();
466 }
467
468 /**
469 * Returns a hash code for this instance.
470 *
471 * @return A hash code.
472 */
473 public int hashCode() {
474 int hash = 5;
475 hash = HashUtilities.hashCode(hash, this.autoWidth);
476 hash = HashUtilities.hashCode(hash, this.intervalPositionFactor);
477 hash = HashUtilities.hashCode(hash, this.fixedIntervalWidth);
478 return hash;
479 }
480
481 }