Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2005, 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, 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.2 2005/10/25 21:36:51 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: * 51: */ 52: 53: package org.jfree.data.xy; 54: 55: import java.io.Serializable; 56: 57: import org.jfree.data.DomainInfo; 58: import org.jfree.data.Range; 59: import org.jfree.data.RangeInfo; 60: import org.jfree.data.general.DatasetChangeEvent; 61: import org.jfree.data.general.DatasetChangeListener; 62: import org.jfree.data.general.DatasetUtilities; 63: import org.jfree.util.PublicCloneable; 64: 65: /** 66: * A delegate that handles the specification or automatic calculation of the 67: * interval surrounding the x-values in a dataset. This is used to extend 68: * a regular {@link XYDataset} to support the {@link IntervalXYDataset} 69: * interface. 70: * <p> 71: * The decorator pattern was not used because of the several possibly 72: * implemented interfaces of the decorated instance (e.g. 73: * {@link TableXYDataset}, {@link RangeInfo}, {@link DomainInfo} etc.). 74: * <p> 75: * The width can be set manually or calculated automatically. The switch 76: * autoWidth allows to determine which behavior is used. The auto width 77: * calculation tries to find the smallest gap between two x-values in the 78: * dataset. If there is only one item in the series, the auto width 79: * calculation fails and falls back on the manually set interval width (which 80: * is itself defaulted to 1.0). 81: * 82: * @author andreas.schroeder 83: */ 84: public class IntervalXYDelegate implements DatasetChangeListener, 85: DomainInfo, Serializable, 86: Cloneable, PublicCloneable { 87: 88: /** For serialization. */ 89: private static final long serialVersionUID = -685166711639592857L; 90: 91: /** 92: * The dataset to enhance. 93: */ 94: private XYDataset dataset; 95: 96: /** 97: * A flag to indicate whether the width should be calculated automatically. 98: */ 99: 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: }