Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors. 6: * 7: * Project Info: http://www.jfree.org/jfreechart/index.html 8: * 9: * This library is free software; you can redistribute it and/or modify it 10: * under the terms of the GNU Lesser General Public License as published by 11: * the Free Software Foundation; either version 2.1 of the License, or 12: * (at your option) any later version. 13: * 14: * This library is distributed in the hope that it will be useful, but 15: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17: * License for more details. 18: * 19: * You should have received a copy of the GNU Lesser General Public 20: * License along with this library; if not, write to the Free Software 21: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22: * USA. 23: * 24: * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 25: * in the United States and other countries.] 26: * 27: * ----------------------- 28: * IntervalXYDelegate.java 29: * ----------------------- 30: * (C) Copyright 2004, 2005, 2007, by Andreas Schroeder and Contributors. 31: * 32: * Original Author: Andreas Schroeder; 33: * Contributor(s): David Gilbert (for Object Refinery Limited); 34: * 35: * Changes 36: * ------- 37: * 31-Mar-2004 : Version 1 (AS); 38: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 39: * getYValue() (DG); 40: * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG); 41: * 04-Nov-2004 : Added argument check for setIntervalWidth() method (DG); 42: * 17-Nov-2004 : New methods to reflect changes in DomainInfo (DG); 43: * 11-Jan-2005 : Removed deprecated methods in preparation for the 1.0.0 44: * release (DG); 45: * 21-Feb-2005 : Made public and added equals() method (DG); 46: * 06-Oct-2005 : Implemented DatasetChangeListener to recalculate 47: * autoIntervalWidth (DG); 48: * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG); 49: * 50: */ 51: 52: package org.jfree.data.xy; 53: 54: import java.io.Serializable; 55: 56: import org.jfree.data.DomainInfo; 57: import org.jfree.data.Range; 58: import org.jfree.data.RangeInfo; 59: import org.jfree.data.general.DatasetChangeEvent; 60: import org.jfree.data.general.DatasetChangeListener; 61: import org.jfree.data.general.DatasetUtilities; 62: import org.jfree.util.PublicCloneable; 63: 64: /** 65: * A delegate that handles the specification or automatic calculation of the 66: * interval surrounding the x-values in a dataset. This is used to extend 67: * a regular {@link XYDataset} to support the {@link IntervalXYDataset} 68: * interface. 69: * <p> 70: * The decorator pattern was not used because of the several possibly 71: * implemented interfaces of the decorated instance (e.g. 72: * {@link TableXYDataset}, {@link RangeInfo}, {@link DomainInfo} etc.). 73: * <p> 74: * The width can be set manually or calculated automatically. The switch 75: * autoWidth allows to determine which behavior is used. The auto width 76: * calculation tries to find the smallest gap between two x-values in the 77: * dataset. If there is only one item in the series, the auto width 78: * calculation fails and falls back on the manually set interval width (which 79: * is itself defaulted to 1.0). 80: */ 81: public class IntervalXYDelegate implements DatasetChangeListener, 82: DomainInfo, Serializable, 83: Cloneable, PublicCloneable { 84: 85: /** For serialization. */ 86: private static final long serialVersionUID = -685166711639592857L; 87: 88: /** 89: * The dataset to enhance. 90: */ 91: private XYDataset dataset; 92: 93: /** 94: * A flag to indicate whether the width should be calculated automatically. 95: */ 96: private boolean autoWidth; 97: 98: /** 99: * A value between 0.0 and 1.0 that indicates the position of the x-value 100: * within the interval. 101: */ 102: private double intervalPositionFactor; 103: 104: /** 105: * The fixed interval width (defaults to 1.0). 106: */ 107: private double fixedIntervalWidth; 108: 109: /** 110: * The automatically calculated interval width. 111: */ 112: private double autoIntervalWidth; 113: 114: /** 115: * Creates a new delegate that. 116: * 117: * @param dataset the underlying dataset (<code>null</code> not permitted). 118: */ 119: public IntervalXYDelegate(XYDataset dataset) { 120: this(dataset, true); 121: } 122: 123: /** 124: * Creates a new delegate for the specified dataset. 125: * 126: * @param dataset the underlying dataset (<code>null</code> not permitted). 127: * @param autoWidth a flag that controls whether the interval width is 128: * calculated automatically. 129: */ 130: public IntervalXYDelegate(XYDataset dataset, boolean autoWidth) { 131: if (dataset == null) { 132: throw new IllegalArgumentException("Null 'dataset' argument."); 133: } 134: this.dataset = dataset; 135: this.autoWidth = autoWidth; 136: this.intervalPositionFactor = 0.5; 137: this.autoIntervalWidth = Double.POSITIVE_INFINITY; 138: this.fixedIntervalWidth = 1.0; 139: } 140: 141: /** 142: * Returns <code>true</code> if the interval width is automatically 143: * calculated, and <code>false</code> otherwise. 144: * 145: * @return A boolean. 146: */ 147: public boolean isAutoWidth() { 148: return this.autoWidth; 149: } 150: 151: /** 152: * Sets the flag that indicates whether the interval width is automatically 153: * calculated. If the flag is set to <code>true</code>, the interval is 154: * recalculated. 155: * <p> 156: * Note: recalculating the interval amounts to changing the data values 157: * represented by the dataset. The calling dataset must fire an 158: * appropriate {@link DatasetChangeEvent}. 159: * 160: * @param b a boolean. 161: */ 162: public void setAutoWidth(boolean b) { 163: this.autoWidth = b; 164: if (b) { 165: this.autoIntervalWidth = recalculateInterval(); 166: } 167: } 168: 169: /** 170: * Returns the interval position factor. 171: * 172: * @return The interval position factor. 173: */ 174: public double getIntervalPositionFactor() { 175: return this.intervalPositionFactor; 176: } 177: 178: /** 179: * Sets the interval position factor. This controls how the interval is 180: * aligned to the x-value. For a value of 0.5, the interval is aligned 181: * with the x-value in the center. For a value of 0.0, the interval is 182: * aligned with the x-value at the lower end of the interval, and for a 183: * value of 1.0, the interval is aligned with the x-value at the upper 184: * end of the interval. 185: * 186: * Note that changing the interval position factor amounts to changing the 187: * data values represented by the dataset. Therefore, the dataset that is 188: * using this delegate is responsible for generating the 189: * appropriate {@link DatasetChangeEvent}. 190: * 191: * @param d the new interval position factor (in the range 192: * <code>0.0</code> to <code>1.0</code> inclusive). 193: */ 194: public void setIntervalPositionFactor(double d) { 195: if (d < 0.0 || 1.0 < d) { 196: throw new IllegalArgumentException( 197: "Argument 'd' outside valid range."); 198: } 199: this.intervalPositionFactor = d; 200: } 201: 202: /** 203: * Returns the fixed interval width. 204: * 205: * @return The fixed interval width. 206: */ 207: public double getFixedIntervalWidth() { 208: return this.fixedIntervalWidth; 209: } 210: 211: /** 212: * Sets the fixed interval width and, as a side effect, sets the 213: * <code>autoWidth</code> flag to <code>false</code>. 214: * 215: * Note that changing the interval width amounts to changing the data 216: * values represented by the dataset. Therefore, the dataset 217: * that is using this delegate is responsible for generating the 218: * appropriate {@link DatasetChangeEvent}. 219: * 220: * @param w the width (negative values not permitted). 221: */ 222: public void setFixedIntervalWidth(double w) { 223: if (w < 0.0) { 224: throw new IllegalArgumentException("Negative 'w' argument."); 225: } 226: this.fixedIntervalWidth = w; 227: this.autoWidth = false; 228: } 229: 230: /** 231: * Returns the interval width. This method will return either the 232: * auto calculated interval width or the manually specified interval 233: * width, depending on the {@link #isAutoWidth()} result. 234: * 235: * @return The interval width to use. 236: */ 237: public double getIntervalWidth() { 238: if (isAutoWidth() && !Double.isInfinite(this.autoIntervalWidth)) { 239: // everything is fine: autoWidth is on, and an autoIntervalWidth 240: // was set. 241: return this.autoIntervalWidth; 242: } 243: else { 244: // either autoWidth is off or autoIntervalWidth was not set. 245: return this.fixedIntervalWidth; 246: } 247: } 248: 249: /** 250: * Returns the start value of the x-interval for an item within a series. 251: * 252: * @param series the series index. 253: * @param item the item index. 254: * 255: * @return The start value of the x-interval (possibly <code>null</code>). 256: * 257: * @see #getStartXValue(int, int) 258: */ 259: public Number getStartX(int series, int item) { 260: Number startX = null; 261: Number x = this.dataset.getX(series, item); 262: if (x != null) { 263: startX = new Double(x.doubleValue() 264: - (getIntervalPositionFactor() * getIntervalWidth())); 265: } 266: return startX; 267: } 268: 269: /** 270: * Returns the start value of the x-interval for an item within a series. 271: * 272: * @param series the series index. 273: * @param item the item index. 274: * 275: * @return The start value of the x-interval. 276: * 277: * @see #getStartX(int, int) 278: */ 279: public double getStartXValue(int series, int item) { 280: return this.dataset.getXValue(series, item) 281: - getIntervalPositionFactor() * getIntervalWidth(); 282: } 283: 284: /** 285: * Returns the end value of the x-interval for an item within a series. 286: * 287: * @param series the series index. 288: * @param item the item index. 289: * 290: * @return The end value of the x-interval (possibly <code>null</code>). 291: * 292: * @see #getEndXValue(int, int) 293: */ 294: public Number getEndX(int series, int item) { 295: Number endX = null; 296: Number x = this.dataset.getX(series, item); 297: if (x != null) { 298: endX = new Double(x.doubleValue() 299: + ((1.0 - getIntervalPositionFactor()) * getIntervalWidth())); 300: } 301: return endX; 302: } 303: 304: /** 305: * Returns the end value of the x-interval for an item within a series. 306: * 307: * @param series the series index. 308: * @param item the item index. 309: * 310: * @return The end value of the x-interval. 311: * 312: * @see #getEndX(int, int) 313: */ 314: public double getEndXValue(int series, int item) { 315: return this.dataset.getXValue(series, item) 316: + (1.0 - getIntervalPositionFactor()) * getIntervalWidth(); 317: } 318: 319: /** 320: * Returns the minimum 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 minimum value. 326: */ 327: public double getDomainLowerBound(boolean includeInterval) { 328: double result = Double.NaN; 329: Range r = getDomainBounds(includeInterval); 330: if (r != null) { 331: result = r.getLowerBound(); 332: } 333: return result; 334: } 335: 336: /** 337: * Returns the maximum x-value in the dataset. 338: * 339: * @param includeInterval a flag that determines whether or not the 340: * x-interval is taken into account. 341: * 342: * @return The maximum value. 343: */ 344: public double getDomainUpperBound(boolean includeInterval) { 345: double result = Double.NaN; 346: Range r = getDomainBounds(includeInterval); 347: if (r != null) { 348: result = r.getUpperBound(); 349: } 350: return result; 351: } 352: 353: /** 354: * Returns the range of the values in the dataset's domain, including 355: * or excluding the interval around each x-value as specified. 356: * 357: * @param includeInterval a flag that determines whether or not the 358: * x-interval should be taken into account. 359: * 360: * @return The range. 361: */ 362: public Range getDomainBounds(boolean includeInterval) { 363: // first get the range without the interval, then expand it for the 364: // interval width 365: Range range = DatasetUtilities.findDomainBounds(this.dataset, false); 366: if (includeInterval && range != null) { 367: double lowerAdj = getIntervalWidth() * getIntervalPositionFactor(); 368: double upperAdj = getIntervalWidth() - lowerAdj; 369: range = new Range(range.getLowerBound() - lowerAdj, 370: range.getUpperBound() + upperAdj); 371: } 372: return range; 373: } 374: 375: /** 376: * Handles events from the dataset by recalculating the interval if 377: * necessary. 378: * 379: * @param e the event. 380: */ 381: public void datasetChanged(DatasetChangeEvent e) { 382: // TODO: by coding the event with some information about what changed 383: // in the dataset, we could make the recalculation of the interval 384: // more efficient in some cases... 385: if (this.autoWidth) { 386: this.autoIntervalWidth = recalculateInterval(); 387: } 388: } 389: 390: /** 391: * Recalculate the minimum width "from scratch". 392: * 393: * @return The minimum width. 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: * @return The interval width. 410: */ 411: private double calculateIntervalForSeries(int series) { 412: double result = Double.POSITIVE_INFINITY; 413: int itemCount = this.dataset.getItemCount(series); 414: if (itemCount > 1) { 415: double prev = this.dataset.getXValue(series, 0); 416: for (int item = 1; item < itemCount; item++) { 417: double x = this.dataset.getXValue(series, item); 418: result = Math.min(result, x - prev); 419: prev = x; 420: } 421: } 422: return result; 423: } 424: 425: /** 426: * Tests the delegate for equality with an arbitrary object. 427: * 428: * @param obj the object (<code>null</code> permitted). 429: * 430: * @return A boolean. 431: */ 432: public boolean equals(Object obj) { 433: if (obj == this) { 434: return true; 435: } 436: if (!(obj instanceof IntervalXYDelegate)) { 437: return false; 438: } 439: IntervalXYDelegate that = (IntervalXYDelegate) obj; 440: if (this.autoWidth != that.autoWidth) { 441: return false; 442: } 443: if (this.intervalPositionFactor != that.intervalPositionFactor) { 444: return false; 445: } 446: if (this.fixedIntervalWidth != that.fixedIntervalWidth) { 447: return false; 448: } 449: return true; 450: } 451: 452: /** 453: * @return A clone of this delegate. 454: * 455: * @throws CloneNotSupportedException if the object cannot be cloned. 456: */ 457: public Object clone() throws CloneNotSupportedException { 458: return super.clone(); 459: } 460: 461: }