001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2005, 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, 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.2 2005/10/25 21:36:51 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 * 051 */ 052 053 package org.jfree.data.xy; 054 055 import java.io.Serializable; 056 057 import org.jfree.data.DomainInfo; 058 import org.jfree.data.Range; 059 import org.jfree.data.RangeInfo; 060 import org.jfree.data.general.DatasetChangeEvent; 061 import org.jfree.data.general.DatasetChangeListener; 062 import org.jfree.data.general.DatasetUtilities; 063 import org.jfree.util.PublicCloneable; 064 065 /** 066 * A delegate that handles the specification or automatic calculation of the 067 * interval surrounding the x-values in a dataset. This is used to extend 068 * a regular {@link XYDataset} to support the {@link IntervalXYDataset} 069 * interface. 070 * <p> 071 * The decorator pattern was not used because of the several possibly 072 * implemented interfaces of the decorated instance (e.g. 073 * {@link TableXYDataset}, {@link RangeInfo}, {@link DomainInfo} etc.). 074 * <p> 075 * The width can be set manually or calculated automatically. The switch 076 * autoWidth allows to determine which behavior is used. The auto width 077 * calculation tries to find the smallest gap between two x-values in the 078 * dataset. If there is only one item in the series, the auto width 079 * calculation fails and falls back on the manually set interval width (which 080 * is itself defaulted to 1.0). 081 * 082 * @author andreas.schroeder 083 */ 084 public class IntervalXYDelegate implements DatasetChangeListener, 085 DomainInfo, Serializable, 086 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 if (dataset == null) { 135 throw new IllegalArgumentException("Null 'dataset' argument."); 136 } 137 this.dataset = dataset; 138 this.autoWidth = autoWidth; 139 this.intervalPositionFactor = 0.5; 140 this.autoIntervalWidth = Double.POSITIVE_INFINITY; 141 this.fixedIntervalWidth = 1.0; 142 } 143 144 /** 145 * Returns <code>true</code> if the interval width is automatically 146 * calculated, and <code>false</code> otherwise. 147 * 148 * @return A boolean. 149 */ 150 public boolean isAutoWidth() { 151 return this.autoWidth; 152 } 153 154 /** 155 * Sets the flag that indicates whether the interval width is automatically 156 * calculated. If the flag is set to <code>true</code>, the interval is 157 * recalculated. 158 * <p> 159 * Note: recalculating the interval amounts to changing the data values 160 * represented by the dataset. The calling dataset must fire an 161 * appropriate {@link DatasetChangeEvent}. 162 * 163 * @param b a boolean. 164 */ 165 public void setAutoWidth(boolean b) { 166 this.autoWidth = b; 167 if (b) { 168 this.autoIntervalWidth = recalculateInterval(); 169 } 170 } 171 172 /** 173 * Returns the interval position factor. 174 * 175 * @return The interval position factor. 176 */ 177 public double getIntervalPositionFactor() { 178 return this.intervalPositionFactor; 179 } 180 181 /** 182 * Sets the interval position factor. This controls how the interval is 183 * aligned to the x-value. For a value of 0.5, the interval is aligned 184 * with the x-value in the center. For a value of 0.0, the interval is 185 * aligned with the x-value at the lower end of the interval, and for a 186 * value of 1.0, the interval is aligned with the x-value at the upper 187 * end of the interval. 188 * 189 * Note that changing the interval position factor amounts to changing the 190 * data values represented by the dataset. Therefore, the dataset that is 191 * using this delegate is responsible for generating the 192 * appropriate {@link DatasetChangeEvent}. 193 * 194 * @param d the new interval position factor (in the range 195 * <code>0.0</code> to <code>1.0</code> inclusive). 196 */ 197 public void setIntervalPositionFactor(double d) { 198 if (d < 0.0 || 1.0 < d) { 199 throw new IllegalArgumentException( 200 "Argument 'd' outside valid range."); 201 } 202 this.intervalPositionFactor = d; 203 } 204 205 /** 206 * Returns the fixed interval width. 207 * 208 * @return The fixed interval width. 209 */ 210 public double getFixedIntervalWidth() { 211 return this.fixedIntervalWidth; 212 } 213 214 /** 215 * Sets the fixed interval width and, as a side effect, sets the 216 * <code>autoWidth</code> flag to <code>false</code>. 217 * 218 * Note that changing the interval width amounts to changing the data 219 * values represented by the dataset. Therefore, the dataset 220 * that is using this delegate is responsible for generating the 221 * appropriate {@link DatasetChangeEvent}. 222 * 223 * @param w the width (negative values not permitted). 224 */ 225 public void setFixedIntervalWidth(double w) { 226 if (w < 0.0) { 227 throw new IllegalArgumentException("Negative 'w' argument."); 228 } 229 this.fixedIntervalWidth = w; 230 this.autoWidth = false; 231 } 232 233 /** 234 * Returns the interval width. This method will return either the 235 * auto calculated interval width or the manually specified interval 236 * width, depending on the {@link #isAutoWidth()} result. 237 * 238 * @return The interval width to use. 239 */ 240 public double getIntervalWidth() { 241 if (isAutoWidth() && !Double.isInfinite(this.autoIntervalWidth)) { 242 // everything is fine: autoWidth is on, and an autoIntervalWidth 243 // was set. 244 return this.autoIntervalWidth; 245 } 246 else { 247 // either autoWidth is off or autoIntervalWidth was not set. 248 return this.fixedIntervalWidth; 249 } 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 (possibly <code>null</code>). 259 * 260 * @see #getStartXValue(int, int) 261 */ 262 public Number getStartX(int series, int item) { 263 Number startX = null; 264 Number x = this.dataset.getX(series, item); 265 if (x != null) { 266 startX = new Double(x.doubleValue() 267 - (getIntervalPositionFactor() * getIntervalWidth())); 268 } 269 return startX; 270 } 271 272 /** 273 * Returns the start value of the x-interval for an item within a series. 274 * 275 * @param series the series index. 276 * @param item the item index. 277 * 278 * @return The start value of the x-interval. 279 * 280 * @see #getStartX(int, int) 281 */ 282 public double getStartXValue(int series, int item) { 283 return dataset.getXValue(series, item) - getIntervalPositionFactor() 284 * getIntervalWidth(); 285 } 286 287 /** 288 * Returns the end value of the x-interval for an item within a series. 289 * 290 * @param series the series index. 291 * @param item the item index. 292 * 293 * @return The end value of the x-interval (possibly <code>null</code>). 294 * 295 * @see #getEndXValue(int, int) 296 */ 297 public Number getEndX(int series, int item) { 298 Number endX = null; 299 Number x = this.dataset.getX(series, item); 300 if (x != null) { 301 endX = new Double(x.doubleValue() 302 + ((1.0 - getIntervalPositionFactor()) * getIntervalWidth())); 303 } 304 return endX; 305 } 306 307 /** 308 * Returns the end value of the x-interval for an item within a series. 309 * 310 * @param series the series index. 311 * @param item the item index. 312 * 313 * @return The end value of the x-interval. 314 * 315 * @see #getEndX(int, int) 316 */ 317 public double getEndXValue(int series, int item) { 318 return dataset.getXValue(series, item) 319 + (1.0 - getIntervalPositionFactor()) * getIntervalWidth(); 320 } 321 322 /** 323 * Returns the minimum x-value in the dataset. 324 * 325 * @param includeInterval a flag that determines whether or not the 326 * x-interval is taken into account. 327 * 328 * @return The minimum value. 329 */ 330 public double getDomainLowerBound(boolean includeInterval) { 331 double result = Double.NaN; 332 Range r = getDomainBounds(includeInterval); 333 if (r != null) { 334 result = r.getLowerBound(); 335 } 336 return result; 337 } 338 339 /** 340 * Returns the maximum x-value in the dataset. 341 * 342 * @param includeInterval a flag that determines whether or not the 343 * x-interval is taken into account. 344 * 345 * @return The maximum value. 346 */ 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 public Range getDomainBounds(boolean includeInterval) { 366 // first get the range without the interval, then expand it for the 367 // interval width 368 Range range = DatasetUtilities.findDomainBounds(this.dataset, false); 369 if (includeInterval && range != null) { 370 double lowerAdj = getIntervalWidth() * getIntervalPositionFactor(); 371 double upperAdj = getIntervalWidth() - lowerAdj; 372 range = new Range(range.getLowerBound() - lowerAdj, 373 range.getUpperBound() + upperAdj); 374 } 375 return range; 376 } 377 378 /** 379 * Handles events from the dataset by recalculating the interval if 380 * necessary. 381 * 382 * @param e the event. 383 */ 384 public void datasetChanged(DatasetChangeEvent e) { 385 // TODO: by coding the event with some information about what changed 386 // in the dataset, we could make the recalculation of the interval 387 // more efficient in some cases... 388 if (this.autoWidth) { 389 this.autoIntervalWidth = recalculateInterval(); 390 } 391 } 392 393 /** 394 * Recalculate the minimum width "from scratch". 395 */ 396 private double recalculateInterval() { 397 double result = Double.POSITIVE_INFINITY; 398 int seriesCount = this.dataset.getSeriesCount(); 399 for (int series = 0; series < seriesCount; series++) { 400 result = Math.min(result, calculateIntervalForSeries(series)); 401 } 402 return result; 403 } 404 405 /** 406 * Calculates the interval width for a given series. 407 * 408 * @param series the series index. 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 }