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 }