001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2013, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ----------------------- 028 * IntervalXYDelegate.java 029 * ----------------------- 030 * (C) Copyright 2004-2013, 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 * 06-Mar-2009 : Implemented hashCode() (DG); 050 * 02-Jul-2013 : Use ParamChecks (DG); 051 * 052 */ 053 054package org.jfree.data.xy; 055 056import java.io.Serializable; 057 058import org.jfree.chart.HashUtilities; 059import org.jfree.chart.util.ParamChecks; 060import org.jfree.data.DomainInfo; 061import org.jfree.data.Range; 062import org.jfree.data.RangeInfo; 063import org.jfree.data.general.DatasetChangeEvent; 064import org.jfree.data.general.DatasetChangeListener; 065import org.jfree.data.general.DatasetUtilities; 066import org.jfree.util.PublicCloneable; 067 068/** 069 * A delegate that handles the specification or automatic calculation of the 070 * interval surrounding the x-values in a dataset. This is used to extend 071 * a regular {@link XYDataset} to support the {@link IntervalXYDataset} 072 * interface. 073 * <p> 074 * The decorator pattern was not used because of the several possibly 075 * implemented interfaces of the decorated instance (e.g. 076 * {@link TableXYDataset}, {@link RangeInfo}, {@link DomainInfo} etc.). 077 * <p> 078 * The width can be set manually or calculated automatically. The switch 079 * autoWidth allows to determine which behavior is used. The auto width 080 * calculation tries to find the smallest gap between two x-values in the 081 * dataset. If there is only one item in the series, the auto width 082 * calculation fails and falls back on the manually set interval width (which 083 * is itself defaulted to 1.0). 084 */ 085public class IntervalXYDelegate implements DatasetChangeListener, 086 DomainInfo, Serializable, Cloneable, PublicCloneable { 087 088 /** For serialization. */ 089 private static final long serialVersionUID = -685166711639592857L; 090 091 /** 092 * The dataset to enhance. 093 */ 094 private XYDataset dataset; 095 096 /** 097 * A flag to indicate whether the width should be calculated automatically. 098 */ 099 private boolean autoWidth; 100 101 /** 102 * A value between 0.0 and 1.0 that indicates the position of the x-value 103 * within the interval. 104 */ 105 private double intervalPositionFactor; 106 107 /** 108 * The fixed interval width (defaults to 1.0). 109 */ 110 private double fixedIntervalWidth; 111 112 /** 113 * The automatically calculated interval width. 114 */ 115 private double autoIntervalWidth; 116 117 /** 118 * Creates a new delegate that. 119 * 120 * @param dataset the underlying dataset (<code>null</code> not permitted). 121 */ 122 public IntervalXYDelegate(XYDataset dataset) { 123 this(dataset, true); 124 } 125 126 /** 127 * Creates a new delegate for the specified dataset. 128 * 129 * @param dataset the underlying dataset (<code>null</code> not permitted). 130 * @param autoWidth a flag that controls whether the interval width is 131 * calculated automatically. 132 */ 133 public IntervalXYDelegate(XYDataset dataset, boolean autoWidth) { 134 ParamChecks.nullNotPermitted(dataset, "dataset"); 135 this.dataset = dataset; 136 this.autoWidth = autoWidth; 137 this.intervalPositionFactor = 0.5; 138 this.autoIntervalWidth = Double.POSITIVE_INFINITY; 139 this.fixedIntervalWidth = 1.0; 140 } 141 142 /** 143 * Returns <code>true</code> if the interval width is automatically 144 * calculated, and <code>false</code> otherwise. 145 * 146 * @return A boolean. 147 */ 148 public boolean isAutoWidth() { 149 return this.autoWidth; 150 } 151 152 /** 153 * Sets the flag that indicates whether the interval width is automatically 154 * calculated. If the flag is set to <code>true</code>, the interval is 155 * recalculated. 156 * <p> 157 * Note: recalculating the interval amounts to changing the data values 158 * represented by the dataset. The calling dataset must fire an 159 * appropriate {@link DatasetChangeEvent}. 160 * 161 * @param b a boolean. 162 */ 163 public void setAutoWidth(boolean b) { 164 this.autoWidth = b; 165 if (b) { 166 this.autoIntervalWidth = recalculateInterval(); 167 } 168 } 169 170 /** 171 * Returns the interval position factor. 172 * 173 * @return The interval position factor. 174 */ 175 public double getIntervalPositionFactor() { 176 return this.intervalPositionFactor; 177 } 178 179 /** 180 * Sets the interval position factor. This controls how the interval is 181 * aligned to the x-value. For a value of 0.5, the interval is aligned 182 * with the x-value in the center. For a value of 0.0, the interval is 183 * aligned with the x-value at the lower end of the interval, and for a 184 * value of 1.0, the interval is aligned with the x-value at the upper 185 * end of the interval. 186 * <br><br> 187 * Note that changing the interval position factor amounts to changing the 188 * data values represented by the dataset. Therefore, the dataset that is 189 * using this delegate is responsible for generating the 190 * appropriate {@link DatasetChangeEvent}. 191 * 192 * @param d the new interval position factor (in the range 193 * <code>0.0</code> to <code>1.0</code> inclusive). 194 */ 195 public void setIntervalPositionFactor(double d) { 196 if (d < 0.0 || 1.0 < d) { 197 throw new IllegalArgumentException( 198 "Argument 'd' outside valid range."); 199 } 200 this.intervalPositionFactor = d; 201 } 202 203 /** 204 * Returns the fixed interval width. 205 * 206 * @return The fixed interval width. 207 */ 208 public double getFixedIntervalWidth() { 209 return this.fixedIntervalWidth; 210 } 211 212 /** 213 * Sets the fixed interval width and, as a side effect, sets the 214 * <code>autoWidth</code> flag to <code>false</code>. 215 * <br><br> 216 * Note that changing the interval width amounts to changing the data 217 * values represented by the dataset. Therefore, the dataset 218 * that is using this delegate is responsible for generating the 219 * appropriate {@link DatasetChangeEvent}. 220 * 221 * @param w the width (negative values not permitted). 222 */ 223 public void setFixedIntervalWidth(double w) { 224 if (w < 0.0) { 225 throw new IllegalArgumentException("Negative 'w' argument."); 226 } 227 this.fixedIntervalWidth = w; 228 this.autoWidth = false; 229 } 230 231 /** 232 * Returns the interval width. This method will return either the 233 * auto calculated interval width or the manually specified interval 234 * width, depending on the {@link #isAutoWidth()} result. 235 * 236 * @return The interval width to use. 237 */ 238 public double getIntervalWidth() { 239 if (isAutoWidth() && !Double.isInfinite(this.autoIntervalWidth)) { 240 // everything is fine: autoWidth is on, and an autoIntervalWidth 241 // was set. 242 return this.autoIntervalWidth; 243 } 244 else { 245 // either autoWidth is off or autoIntervalWidth was not set. 246 return this.fixedIntervalWidth; 247 } 248 } 249 250 /** 251 * Returns the start value of the x-interval for an item within a series. 252 * 253 * @param series the series index. 254 * @param item the item index. 255 * 256 * @return The start value of the x-interval (possibly <code>null</code>). 257 * 258 * @see #getStartXValue(int, int) 259 */ 260 public Number getStartX(int series, int item) { 261 Number startX = null; 262 Number x = this.dataset.getX(series, item); 263 if (x != null) { 264 startX = new Double(x.doubleValue() 265 - (getIntervalPositionFactor() * getIntervalWidth())); 266 } 267 return startX; 268 } 269 270 /** 271 * Returns the start value of the x-interval for an item within a series. 272 * 273 * @param series the series index. 274 * @param item the item index. 275 * 276 * @return The start value of the x-interval. 277 * 278 * @see #getStartX(int, int) 279 */ 280 public double getStartXValue(int series, int item) { 281 return this.dataset.getXValue(series, item) 282 - getIntervalPositionFactor() * getIntervalWidth(); 283 } 284 285 /** 286 * Returns the end value of the x-interval for an item within a series. 287 * 288 * @param series the series index. 289 * @param item the item index. 290 * 291 * @return The end value of the x-interval (possibly <code>null</code>). 292 * 293 * @see #getEndXValue(int, int) 294 */ 295 public Number getEndX(int series, int item) { 296 Number endX = null; 297 Number x = this.dataset.getX(series, item); 298 if (x != null) { 299 endX = new Double(x.doubleValue() 300 + ((1.0 - getIntervalPositionFactor()) * getIntervalWidth())); 301 } 302 return endX; 303 } 304 305 /** 306 * Returns the end value of the x-interval for an item within a series. 307 * 308 * @param series the series index. 309 * @param item the item index. 310 * 311 * @return The end value of the x-interval. 312 * 313 * @see #getEndX(int, int) 314 */ 315 public double getEndXValue(int series, int item) { 316 return this.dataset.getXValue(series, item) 317 + (1.0 - getIntervalPositionFactor()) * getIntervalWidth(); 318 } 319 320 /** 321 * Returns the minimum x-value in the dataset. 322 * 323 * @param includeInterval a flag that determines whether or not the 324 * x-interval is taken into account. 325 * 326 * @return The minimum value. 327 */ 328 @Override 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 @Override 347 public double getDomainUpperBound(boolean includeInterval) { 348 double result = Double.NaN; 349 Range r = getDomainBounds(includeInterval); 350 if (r != null) { 351 result = r.getUpperBound(); 352 } 353 return result; 354 } 355 356 /** 357 * Returns the range of the values in the dataset's domain, including 358 * or excluding the interval around each x-value as specified. 359 * 360 * @param includeInterval a flag that determines whether or not the 361 * x-interval should be taken into account. 362 * 363 * @return The range. 364 */ 365 @Override 366 public Range getDomainBounds(boolean includeInterval) { 367 // first get the range without the interval, then expand it for the 368 // interval width 369 Range range = DatasetUtilities.findDomainBounds(this.dataset, false); 370 if (includeInterval && range != null) { 371 double lowerAdj = getIntervalWidth() * getIntervalPositionFactor(); 372 double upperAdj = getIntervalWidth() - lowerAdj; 373 range = new Range(range.getLowerBound() - lowerAdj, 374 range.getUpperBound() + upperAdj); 375 } 376 return range; 377 } 378 379 /** 380 * Handles events from the dataset by recalculating the interval if 381 * necessary. 382 * 383 * @param e the event. 384 */ 385 @Override 386 public void datasetChanged(DatasetChangeEvent e) { 387 // TODO: by coding the event with some information about what changed 388 // in the dataset, we could make the recalculation of the interval 389 // more efficient in some cases (for instance, if the change is 390 // just an update to a y-value, then the x-interval doesn't need 391 // updating)... 392 if (this.autoWidth) { 393 this.autoIntervalWidth = recalculateInterval(); 394 } 395 } 396 397 /** 398 * Recalculate the minimum width "from scratch". 399 * 400 * @return The minimum width. 401 */ 402 private double recalculateInterval() { 403 double result = Double.POSITIVE_INFINITY; 404 int seriesCount = this.dataset.getSeriesCount(); 405 for (int series = 0; series < seriesCount; series++) { 406 result = Math.min(result, calculateIntervalForSeries(series)); 407 } 408 return result; 409 } 410 411 /** 412 * Calculates the interval width for a given series. 413 * 414 * @param series the series index. 415 * 416 * @return The interval width. 417 */ 418 private double calculateIntervalForSeries(int series) { 419 double result = Double.POSITIVE_INFINITY; 420 int itemCount = this.dataset.getItemCount(series); 421 if (itemCount > 1) { 422 double prev = this.dataset.getXValue(series, 0); 423 for (int item = 1; item < itemCount; item++) { 424 double x = this.dataset.getXValue(series, item); 425 result = Math.min(result, x - prev); 426 prev = x; 427 } 428 } 429 return result; 430 } 431 432 /** 433 * Tests the delegate for equality with an arbitrary object. The 434 * equality test considers two delegates to be equal if they would 435 * calculate the same intervals for any given dataset (for this reason, the 436 * dataset itself is NOT included in the equality test, because it is just 437 * a reference back to the current 'owner' of the delegate). 438 * 439 * @param obj the object (<code>null</code> permitted). 440 * 441 * @return A boolean. 442 */ 443 @Override 444 public boolean equals(Object obj) { 445 if (obj == this) { 446 return true; 447 } 448 if (!(obj instanceof IntervalXYDelegate)) { 449 return false; 450 } 451 IntervalXYDelegate that = (IntervalXYDelegate) obj; 452 if (this.autoWidth != that.autoWidth) { 453 return false; 454 } 455 if (this.intervalPositionFactor != that.intervalPositionFactor) { 456 return false; 457 } 458 if (this.fixedIntervalWidth != that.fixedIntervalWidth) { 459 return false; 460 } 461 return true; 462 } 463 464 /** 465 * @return A clone of this delegate. 466 * 467 * @throws CloneNotSupportedException if the object cannot be cloned. 468 */ 469 @Override 470 public Object clone() throws CloneNotSupportedException { 471 return super.clone(); 472 } 473 474 /** 475 * Returns a hash code for this instance. 476 * 477 * @return A hash code. 478 */ 479 @Override 480 public int hashCode() { 481 int hash = 5; 482 hash = HashUtilities.hashCode(hash, this.autoWidth); 483 hash = HashUtilities.hashCode(hash, this.intervalPositionFactor); 484 hash = HashUtilities.hashCode(hash, this.fixedIntervalWidth); 485 return hash; 486 } 487 488}