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 }