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