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 }