001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2008, 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-2008, 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, Cloneable, PublicCloneable {
083
084 /** For serialization. */
085 private static final long serialVersionUID = -685166711639592857L;
086
087 /**
088 * The dataset to enhance.
089 */
090 private XYDataset dataset;
091
092 /**
093 * A flag to indicate whether the width should be calculated automatically.
094 */
095 private boolean autoWidth;
096
097 /**
098 * A value between 0.0 and 1.0 that indicates the position of the x-value
099 * within the interval.
100 */
101 private double intervalPositionFactor;
102
103 /**
104 * The fixed interval width (defaults to 1.0).
105 */
106 private double fixedIntervalWidth;
107
108 /**
109 * The automatically calculated interval width.
110 */
111 private double autoIntervalWidth;
112
113 /**
114 * Creates a new delegate that.
115 *
116 * @param dataset the underlying dataset (<code>null</code> not permitted).
117 */
118 public IntervalXYDelegate(XYDataset dataset) {
119 this(dataset, true);
120 }
121
122 /**
123 * Creates a new delegate for the specified dataset.
124 *
125 * @param dataset the underlying dataset (<code>null</code> not permitted).
126 * @param autoWidth a flag that controls whether the interval width is
127 * calculated automatically.
128 */
129 public IntervalXYDelegate(XYDataset dataset, boolean autoWidth) {
130 if (dataset == null) {
131 throw new IllegalArgumentException("Null 'dataset' argument.");
132 }
133 this.dataset = dataset;
134 this.autoWidth = autoWidth;
135 this.intervalPositionFactor = 0.5;
136 this.autoIntervalWidth = Double.POSITIVE_INFINITY;
137 this.fixedIntervalWidth = 1.0;
138 }
139
140 /**
141 * Returns <code>true</code> if the interval width is automatically
142 * calculated, and <code>false</code> otherwise.
143 *
144 * @return A boolean.
145 */
146 public boolean isAutoWidth() {
147 return this.autoWidth;
148 }
149
150 /**
151 * Sets the flag that indicates whether the interval width is automatically
152 * calculated. If the flag is set to <code>true</code>, the interval is
153 * recalculated.
154 * <p>
155 * Note: recalculating the interval amounts to changing the data values
156 * represented by the dataset. The calling dataset must fire an
157 * appropriate {@link DatasetChangeEvent}.
158 *
159 * @param b a boolean.
160 */
161 public void setAutoWidth(boolean b) {
162 this.autoWidth = b;
163 if (b) {
164 this.autoIntervalWidth = recalculateInterval();
165 }
166 }
167
168 /**
169 * Returns the interval position factor.
170 *
171 * @return The interval position factor.
172 */
173 public double getIntervalPositionFactor() {
174 return this.intervalPositionFactor;
175 }
176
177 /**
178 * Sets the interval position factor. This controls how the interval is
179 * aligned to the x-value. For a value of 0.5, the interval is aligned
180 * with the x-value in the center. For a value of 0.0, the interval is
181 * aligned with the x-value at the lower end of the interval, and for a
182 * value of 1.0, the interval is aligned with the x-value at the upper
183 * end of the interval.
184 *
185 * Note that changing the interval position factor amounts to changing the
186 * data values represented by the dataset. Therefore, the dataset that is
187 * using this delegate is responsible for generating the
188 * appropriate {@link DatasetChangeEvent}.
189 *
190 * @param d the new interval position factor (in the range
191 * <code>0.0</code> to <code>1.0</code> inclusive).
192 */
193 public void setIntervalPositionFactor(double d) {
194 if (d < 0.0 || 1.0 < d) {
195 throw new IllegalArgumentException(
196 "Argument 'd' outside valid range.");
197 }
198 this.intervalPositionFactor = d;
199 }
200
201 /**
202 * Returns the fixed interval width.
203 *
204 * @return The fixed interval width.
205 */
206 public double getFixedIntervalWidth() {
207 return this.fixedIntervalWidth;
208 }
209
210 /**
211 * Sets the fixed interval width and, as a side effect, sets the
212 * <code>autoWidth</code> flag to <code>false</code>.
213 *
214 * Note that changing the interval width amounts to changing the data
215 * values represented by the dataset. Therefore, the dataset
216 * that is using this delegate is responsible for generating the
217 * appropriate {@link DatasetChangeEvent}.
218 *
219 * @param w the width (negative values not permitted).
220 */
221 public void setFixedIntervalWidth(double w) {
222 if (w < 0.0) {
223 throw new IllegalArgumentException("Negative 'w' argument.");
224 }
225 this.fixedIntervalWidth = w;
226 this.autoWidth = false;
227 }
228
229 /**
230 * Returns the interval width. This method will return either the
231 * auto calculated interval width or the manually specified interval
232 * width, depending on the {@link #isAutoWidth()} result.
233 *
234 * @return The interval width to use.
235 */
236 public double getIntervalWidth() {
237 if (isAutoWidth() && !Double.isInfinite(this.autoIntervalWidth)) {
238 // everything is fine: autoWidth is on, and an autoIntervalWidth
239 // was set.
240 return this.autoIntervalWidth;
241 }
242 else {
243 // either autoWidth is off or autoIntervalWidth was not set.
244 return this.fixedIntervalWidth;
245 }
246 }
247
248 /**
249 * Returns the start value of the x-interval for an item within a series.
250 *
251 * @param series the series index.
252 * @param item the item index.
253 *
254 * @return The start value of the x-interval (possibly <code>null</code>).
255 *
256 * @see #getStartXValue(int, int)
257 */
258 public Number getStartX(int series, int item) {
259 Number startX = null;
260 Number x = this.dataset.getX(series, item);
261 if (x != null) {
262 startX = new Double(x.doubleValue()
263 - (getIntervalPositionFactor() * getIntervalWidth()));
264 }
265 return startX;
266 }
267
268 /**
269 * Returns the start value of the x-interval for an item within a series.
270 *
271 * @param series the series index.
272 * @param item the item index.
273 *
274 * @return The start value of the x-interval.
275 *
276 * @see #getStartX(int, int)
277 */
278 public double getStartXValue(int series, int item) {
279 return this.dataset.getXValue(series, item)
280 - getIntervalPositionFactor() * getIntervalWidth();
281 }
282
283 /**
284 * Returns the end value of the x-interval for an item within a series.
285 *
286 * @param series the series index.
287 * @param item the item index.
288 *
289 * @return The end value of the x-interval (possibly <code>null</code>).
290 *
291 * @see #getEndXValue(int, int)
292 */
293 public Number getEndX(int series, int item) {
294 Number endX = null;
295 Number x = this.dataset.getX(series, item);
296 if (x != null) {
297 endX = new Double(x.doubleValue()
298 + ((1.0 - getIntervalPositionFactor()) * getIntervalWidth()));
299 }
300 return endX;
301 }
302
303 /**
304 * Returns the end value of the x-interval for an item within a series.
305 *
306 * @param series the series index.
307 * @param item the item index.
308 *
309 * @return The end value of the x-interval.
310 *
311 * @see #getEndX(int, int)
312 */
313 public double getEndXValue(int series, int item) {
314 return this.dataset.getXValue(series, item)
315 + (1.0 - getIntervalPositionFactor()) * getIntervalWidth();
316 }
317
318 /**
319 * Returns the minimum x-value in the dataset.
320 *
321 * @param includeInterval a flag that determines whether or not the
322 * x-interval is taken into account.
323 *
324 * @return The minimum value.
325 */
326 public double getDomainLowerBound(boolean includeInterval) {
327 double result = Double.NaN;
328 Range r = getDomainBounds(includeInterval);
329 if (r != null) {
330 result = r.getLowerBound();
331 }
332 return result;
333 }
334
335 /**
336 * Returns the maximum x-value in the dataset.
337 *
338 * @param includeInterval a flag that determines whether or not the
339 * x-interval is taken into account.
340 *
341 * @return The maximum value.
342 */
343 public double getDomainUpperBound(boolean includeInterval) {
344 double result = Double.NaN;
345 Range r = getDomainBounds(includeInterval);
346 if (r != null) {
347 result = r.getUpperBound();
348 }
349 return result;
350 }
351
352 /**
353 * Returns the range of the values in the dataset's domain, including
354 * or excluding the interval around each x-value as specified.
355 *
356 * @param includeInterval a flag that determines whether or not the
357 * x-interval should be taken into account.
358 *
359 * @return The range.
360 */
361 public Range getDomainBounds(boolean includeInterval) {
362 // first get the range without the interval, then expand it for the
363 // interval width
364 Range range = DatasetUtilities.findDomainBounds(this.dataset, false);
365 if (includeInterval && range != null) {
366 double lowerAdj = getIntervalWidth() * getIntervalPositionFactor();
367 double upperAdj = getIntervalWidth() - lowerAdj;
368 range = new Range(range.getLowerBound() - lowerAdj,
369 range.getUpperBound() + upperAdj);
370 }
371 return range;
372 }
373
374 /**
375 * Handles events from the dataset by recalculating the interval if
376 * necessary.
377 *
378 * @param e the event.
379 */
380 public void datasetChanged(DatasetChangeEvent e) {
381 // TODO: by coding the event with some information about what changed
382 // in the dataset, we could make the recalculation of the interval
383 // more efficient in some cases...
384 if (this.autoWidth) {
385 this.autoIntervalWidth = recalculateInterval();
386 }
387 }
388
389 /**
390 * Recalculate the minimum width "from scratch".
391 *
392 * @return The minimum width.
393 */
394 private double recalculateInterval() {
395 double result = Double.POSITIVE_INFINITY;
396 int seriesCount = this.dataset.getSeriesCount();
397 for (int series = 0; series < seriesCount; series++) {
398 result = Math.min(result, calculateIntervalForSeries(series));
399 }
400 return result;
401 }
402
403 /**
404 * Calculates the interval width for a given series.
405 *
406 * @param series the series index.
407 *
408 * @return The interval width.
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 }