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 }