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 * ModuloAxis.java 029 * --------------- 030 * (C) Copyright 2004, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * $Id: ModuloAxis.java,v 1.3.2.1 2005/10/25 20:37:34 mungady Exp $ 036 * 037 * Changes 038 * ------- 039 * 13-Aug-2004 : Version 1 (DG); 040 * 041 */ 042 043 package org.jfree.chart.axis; 044 045 import java.awt.geom.Rectangle2D; 046 047 import org.jfree.chart.event.AxisChangeEvent; 048 import org.jfree.data.Range; 049 import org.jfree.ui.RectangleEdge; 050 051 /** 052 * An axis that displays numerical values within a fixed range using a modulo 053 * calculation. 054 */ 055 public class ModuloAxis extends NumberAxis { 056 057 /** 058 * The fixed range for the axis - all data values will be mapped to this 059 * range using a modulo calculation. 060 */ 061 private Range fixedRange; 062 063 /** 064 * The display start value (this will sometimes be > displayEnd, in which 065 * case the axis wraps around at some point in the middle of the axis). 066 */ 067 private double displayStart; 068 069 /** 070 * The display end value. 071 */ 072 private double displayEnd; 073 074 /** 075 * Creates a new axis. 076 * 077 * @param label the axis label (<code>null</code> permitted). 078 * @param fixedRange the fixed range (<code>null</code> not permitted). 079 */ 080 public ModuloAxis(String label, Range fixedRange) { 081 super(label); 082 this.fixedRange = fixedRange; 083 this.displayStart = 270.0; 084 this.displayEnd = 90.0; 085 } 086 087 /** 088 * Returns the display start value. 089 * 090 * @return The display start value. 091 */ 092 public double getDisplayStart() { 093 return this.displayStart; 094 } 095 096 /** 097 * Returns the display end value. 098 * 099 * @return The display end value. 100 */ 101 public double getDisplayEnd() { 102 return this.displayEnd; 103 } 104 105 /** 106 * Sets the display range. The values will be mapped to the fixed range if 107 * necessary. 108 * 109 * @param start the start value. 110 * @param end the end value. 111 */ 112 public void setDisplayRange(double start, double end) { 113 this.displayStart = mapValueToFixedRange(start); 114 this.displayEnd = mapValueToFixedRange(end); 115 if (this.displayStart < this.displayEnd) { 116 setRange(this.displayStart, this.displayEnd); 117 } 118 else { 119 setRange( 120 this.displayStart, 121 this.fixedRange.getUpperBound() 122 + (this.displayEnd - this.fixedRange.getLowerBound()) 123 ); 124 } 125 notifyListeners(new AxisChangeEvent(this)); 126 } 127 128 /** 129 * This method should calculate a range that will show all the data values. 130 * For now, it just sets the axis range to the fixedRange. 131 */ 132 protected void autoAdjustRange() { 133 setRange(this.fixedRange, false, false); 134 } 135 136 /** 137 * Translates a data value to a Java2D coordinate. 138 * 139 * @param value the value. 140 * @param area the area. 141 * @param edge the edge. 142 * 143 * @return A Java2D coordinate. 144 */ 145 public double valueToJava2D(double value, Rectangle2D area, 146 RectangleEdge edge) { 147 double result = 0.0; 148 double v = mapValueToFixedRange(value); 149 if (this.displayStart < this.displayEnd) { // regular number axis 150 result = trans(v, area, edge); 151 } 152 else { // displayStart > displayEnd, need to handle split 153 double cutoff = (this.displayStart + this.displayEnd) / 2.0; 154 double length1 = this.fixedRange.getUpperBound() 155 - this.displayStart; 156 double length2 = this.displayEnd - this.fixedRange.getLowerBound(); 157 if (v > cutoff) { 158 result = transStart(v, area, edge, length1, length2); 159 } 160 else { 161 result = transEnd(v, area, edge, length1, length2); 162 } 163 } 164 return result; 165 } 166 167 /** 168 * A regular translation from a data value to a Java2D value. 169 * 170 * @param value the value. 171 * @param area the data area. 172 * @param edge the edge along which the axis lies. 173 * 174 * @return The Java2D coordinate. 175 */ 176 private double trans(double value, Rectangle2D area, RectangleEdge edge) { 177 double min = 0.0; 178 double max = 0.0; 179 if (RectangleEdge.isTopOrBottom(edge)) { 180 min = area.getX(); 181 max = area.getX() + area.getWidth(); 182 } 183 else if (RectangleEdge.isLeftOrRight(edge)) { 184 min = area.getMaxY(); 185 max = area.getMaxY() - area.getHeight(); 186 } 187 if (isInverted()) { 188 return max - ((value - this.displayStart) 189 / (this.displayEnd - this.displayStart)) * (max - min); 190 } 191 else { 192 return min + ((value - this.displayStart) 193 / (this.displayEnd - this.displayStart)) * (max - min); 194 } 195 196 } 197 198 /** 199 * Translates a data value to a Java2D value for the first section of the 200 * axis. 201 * 202 * @param value the value. 203 * @param area the data area. 204 * @param edge the edge along which the axis lies. 205 * @param length1 the length of the first section. 206 * @param length2 the length of the second section. 207 * 208 * @return The Java2D coordinate. 209 */ 210 private double transStart(double value, Rectangle2D area, 211 RectangleEdge edge, 212 double length1, double length2) { 213 double min = 0.0; 214 double max = 0.0; 215 if (RectangleEdge.isTopOrBottom(edge)) { 216 min = area.getX(); 217 max = area.getX() + area.getWidth() * length1 / (length1 + length2); 218 } 219 else if (RectangleEdge.isLeftOrRight(edge)) { 220 min = area.getMaxY(); 221 max = area.getMaxY() - area.getHeight() * length1 222 / (length1 + length2); 223 } 224 if (isInverted()) { 225 return max - ((value - this.displayStart) 226 / (this.fixedRange.getUpperBound() - this.displayStart)) 227 * (max - min); 228 } 229 else { 230 return min + ((value - this.displayStart) 231 / (this.fixedRange.getUpperBound() - this.displayStart)) 232 * (max - min); 233 } 234 235 } 236 237 /** 238 * Translates a data value to a Java2D value for the second section of the 239 * axis. 240 * 241 * @param value the value. 242 * @param area the data area. 243 * @param edge the edge along which the axis lies. 244 * @param length1 the length of the first section. 245 * @param length2 the length of the second section. 246 * 247 * @return The Java2D coordinate. 248 */ 249 private double transEnd(double value, Rectangle2D area, RectangleEdge edge, 250 double length1, double length2) { 251 double min = 0.0; 252 double max = 0.0; 253 if (RectangleEdge.isTopOrBottom(edge)) { 254 max = area.getMaxX(); 255 min = area.getMaxX() - area.getWidth() * length2 256 / (length1 + length2); 257 } 258 else if (RectangleEdge.isLeftOrRight(edge)) { 259 max = area.getMinY(); 260 min = area.getMinY() + area.getHeight() * length2 261 / (length1 + length2); 262 } 263 if (isInverted()) { 264 return max - ((value - this.fixedRange.getLowerBound()) 265 / (this.displayEnd - this.fixedRange.getLowerBound())) 266 * (max - min); 267 } 268 else { 269 return min + ((value - this.fixedRange.getLowerBound()) 270 / (this.displayEnd - this.fixedRange.getLowerBound())) 271 * (max - min); 272 } 273 274 } 275 276 /** 277 * Maps a data value into the fixed range. 278 * 279 * @param value the value. 280 * 281 * @return The mapped value. 282 */ 283 private double mapValueToFixedRange(double value) { 284 double lower = this.fixedRange.getLowerBound(); 285 double length = this.fixedRange.getLength(); 286 if (value < lower) { 287 return lower + length + ((value - lower) % length); 288 } 289 else { 290 return lower + ((value - lower) % length); 291 } 292 } 293 294 /** 295 * Translates a Java2D coordinate into a data value. 296 * 297 * @param java2DValue the Java2D coordinate. 298 * @param area the area. 299 * @param edge the edge. 300 * 301 * @return The Java2D coordinate. 302 */ 303 public double java2DToValue(double java2DValue, Rectangle2D area, 304 RectangleEdge edge) { 305 double result = 0.0; 306 if (this.displayStart < this.displayEnd) { // regular number axis 307 result = super.java2DToValue(java2DValue, area, edge); 308 } 309 else { // displayStart > displayEnd, need to handle split 310 311 } 312 return result; 313 } 314 315 /** 316 * Returns the display length for the axis. 317 * 318 * @return The display length. 319 */ 320 private double getDisplayLength() { 321 if (this.displayStart < this.displayEnd) { 322 return (this.displayEnd - this.displayStart); 323 } 324 else { 325 return (this.fixedRange.getUpperBound() - this.displayStart) 326 + (this.displayEnd - this.fixedRange.getLowerBound()); 327 } 328 } 329 330 /** 331 * Returns the central value of the current display range. 332 * 333 * @return The central value. 334 */ 335 private double getDisplayCentralValue() { 336 return mapValueToFixedRange( 337 this.displayStart + (getDisplayLength() / 2) 338 ); 339 } 340 341 /** 342 * Increases or decreases the axis range by the specified percentage about 343 * the central value and sends an {@link AxisChangeEvent} to all registered 344 * listeners. 345 * <P> 346 * To double the length of the axis range, use 200% (2.0). 347 * To halve the length of the axis range, use 50% (0.5). 348 * 349 * @param percent the resize factor. 350 */ 351 public void resizeRange(double percent) { 352 resizeRange(percent, getDisplayCentralValue()); 353 } 354 355 /** 356 * Increases or decreases the axis range by the specified percentage about 357 * the specified anchor value and sends an {@link AxisChangeEvent} to all 358 * registered listeners. 359 * <P> 360 * To double the length of the axis range, use 200% (2.0). 361 * To halve the length of the axis range, use 50% (0.5). 362 * 363 * @param percent the resize factor. 364 * @param anchorValue the new central value after the resize. 365 */ 366 public void resizeRange(double percent, double anchorValue) { 367 368 if (percent > 0.0) { 369 double halfLength = getDisplayLength() * percent / 2; 370 setDisplayRange(anchorValue - halfLength, anchorValue + halfLength); 371 } 372 else { 373 setAutoRange(true); 374 } 375 376 } 377 378 /** 379 * Converts a length in data coordinates into the corresponding length in 380 * Java2D coordinates. 381 * 382 * @param length the length. 383 * @param area the plot area. 384 * @param edge the edge along which the axis lies. 385 * 386 * @return The length in Java2D coordinates. 387 */ 388 public double lengthToJava2D(double length, Rectangle2D area, 389 RectangleEdge edge) { 390 double axisLength = 0.0; 391 if (this.displayEnd > this.displayStart) { 392 axisLength = this.displayEnd - this.displayStart; 393 } 394 else { 395 axisLength = (this.fixedRange.getUpperBound() - this.displayStart) 396 + (this.displayEnd - this.fixedRange.getLowerBound()); 397 } 398 double areaLength = 0.0; 399 if (RectangleEdge.isLeftOrRight(edge)) { 400 areaLength = area.getHeight(); 401 } 402 else { 403 areaLength = area.getWidth(); 404 } 405 return (length / axisLength) * areaLength; 406 } 407 408 }