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: * $Id: IntervalXYDelegate.java,v 1.10.2.4 2007/03/09 16:14:13 mungady Exp $ 36: * 37: * Changes (from 31-Mar-2004) 38: * -------------------------- 39: * 31-Mar-2004 : Version 1 (AS); 40: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 41: * getYValue() (DG); 42: * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG); 43: * 04-Nov-2004 : Added argument check for setIntervalWidth() method (DG); 44: * 17-Nov-2004 : New methods to reflect changes in DomainInfo (DG); 45: * 11-Jan-2005 : Removed deprecated methods in preparation for the 1.0.0 46: * release (DG); 47: * 21-Feb-2005 : Made public and added equals() method (DG); 48: * 06-Oct-2005 : Implemented DatasetChangeListener to recalculate 49: * autoIntervalWidth (DG); 50: * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG); 51: * 52: */ 53: 54: package org.jfree.data.xy; 55: 56: import java.io.Serializable; 57: 58: import org.jfree.data.DomainInfo; 59: import org.jfree.data.Range; 60: import org.jfree.data.RangeInfo; 61: import org.jfree.data.general.DatasetChangeEvent; 62: import org.jfree.data.general.DatasetChangeListener; 63: import org.jfree.data.general.DatasetUtilities; 64: import org.jfree.util.PublicCloneable; 65: 66: /** 67: * A delegate that handles the specification or automatic calculation of the 68: * interval surrounding the x-values in a dataset. This is used to extend 69: * a regular {@link XYDataset} to support the {@link IntervalXYDataset} 70: * interface. 71: * <p> 72: * The decorator pattern was not used because of the several possibly 73: * implemented interfaces of the decorated instance (e.g. 74: * {@link TableXYDataset}, {@link RangeInfo}, {@link DomainInfo} etc.). 75: * <p> 76: * The width can be set manually or calculated automatically. The switch 77: * autoWidth allows to determine which behavior is used. The auto width 78: * calculation tries to find the smallest gap between two x-values in the 79: * dataset. If there is only one item in the series, the auto width 80: * calculation fails and falls back on the manually set interval width (which 81: * is itself defaulted to 1.0). 82: */ 83: public class IntervalXYDelegate implements DatasetChangeListener, 84: DomainInfo, Serializable, 85: Cloneable, PublicCloneable { 86: 87: /** For serialization. */ 88: private static final long serialVersionUID = -685166711639592857L; 89: 90: /** 91: * The dataset to enhance. 92: */ 93: private XYDataset dataset; 94: 95: /** 96: * A flag to indicate whether the width should be calculated automatically. 97: */ 98: private boolean autoWidth; 99: 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: }