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