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