Source for org.jfree.data.xy.IntervalXYDelegate

   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: }