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 * CyclicNumberAxis.java 029 * --------------------- 030 * (C) Copyright 2003, 2004, by Nicolas Brodu and Contributors. 031 * 032 * Original Author: Nicolas Brodu; 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * 035 * $Id: CyclicNumberAxis.java,v 1.10.2.2 2005/10/25 20:37:34 mungady Exp $ 036 * 037 * Changes 038 * ------- 039 * 19-Nov-2003 : Initial import to JFreeChart from the JSynoptic project (NB); 040 * 16-Mar-2004 : Added plotState to draw() method (DG); 041 * 07-Apr-2004 : Modifed text bounds calculation (DG); 042 * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant 043 * argument in selectAutoTickUnit() (DG); 044 * 22-Apr-2005 : Renamed refreshHorizontalTicks() --> refreshTicksHorizontal 045 * (for consistency with other classes) and removed unused 046 * parameters (DG); 047 * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG); 048 * 049 */ 050 051 package org.jfree.chart.axis; 052 053 import java.awt.BasicStroke; 054 import java.awt.Color; 055 import java.awt.Font; 056 import java.awt.FontMetrics; 057 import java.awt.Graphics2D; 058 import java.awt.Paint; 059 import java.awt.Stroke; 060 import java.awt.geom.Line2D; 061 import java.awt.geom.Rectangle2D; 062 import java.io.IOException; 063 import java.io.ObjectInputStream; 064 import java.io.ObjectOutputStream; 065 import java.text.NumberFormat; 066 import java.util.List; 067 068 import org.jfree.chart.plot.Plot; 069 import org.jfree.chart.plot.PlotRenderingInfo; 070 import org.jfree.data.Range; 071 import org.jfree.io.SerialUtilities; 072 import org.jfree.text.TextUtilities; 073 import org.jfree.ui.RectangleEdge; 074 import org.jfree.ui.TextAnchor; 075 import org.jfree.util.ObjectUtilities; 076 import org.jfree.util.PaintUtilities; 077 078 /** 079 This class extends NumberAxis and handles cycling. 080 081 Traditional representation of data in the range x0..x1 082 <pre> 083 |-------------------------| 084 x0 x1 085 </pre> 086 087 Here, the range bounds are at the axis extremities. 088 With cyclic axis, however, the time is split in 089 "cycles", or "time frames", or the same duration : the period. 090 091 A cycle axis cannot by definition handle a larger interval 092 than the period : <pre>x1 - x0 >= period</pre>. Thus, at most a full 093 period can be represented with such an axis. 094 095 The cycle bound is the number between x0 and x1 which marks 096 the beginning of new time frame: 097 <pre> 098 |---------------------|----------------------------| 099 x0 cb x1 100 <---previous cycle---><-------current cycle--------> 101 </pre> 102 103 It is actually a multiple of the period, plus optionally 104 a start offset: <pre>cb = n * period + offset</pre> 105 106 Thus, by definition, two consecutive cycle bounds 107 period apart, which is precisely why it is called a 108 period. 109 110 The visual representation of a cyclic axis is like that: 111 <pre> 112 |----------------------------|---------------------| 113 cb x1|x0 cb 114 <-------current cycle--------><---previous cycle---> 115 </pre> 116 117 The cycle bound is at the axis ends, then current 118 cycle is shown, then the last cycle. When using 119 dynamic data, the visual effect is the current cycle 120 erases the last cycle as x grows. Then, the next cycle 121 bound is reached, and the process starts over, erasing 122 the previous cycle. 123 124 A Cyclic item renderer is provided to do exactly this. 125 126 */ 127 public class CyclicNumberAxis extends NumberAxis { 128 129 /** The default axis line stroke. */ 130 public static Stroke DEFAULT_ADVANCE_LINE_STROKE = new BasicStroke(1.0f); 131 132 /** The default axis line paint. */ 133 public static final Paint DEFAULT_ADVANCE_LINE_PAINT = Color.gray; 134 135 /** The offset. */ 136 protected double offset; 137 138 /** The period.*/ 139 protected double period; 140 141 /** ??. */ 142 protected boolean boundMappedToLastCycle; 143 144 /** A flag that controls whether or not the advance line is visible. */ 145 protected boolean advanceLineVisible; 146 147 /** The advance line stroke. */ 148 protected transient Stroke advanceLineStroke = DEFAULT_ADVANCE_LINE_STROKE; 149 150 /** The advance line paint. */ 151 protected transient Paint advanceLinePaint; 152 153 private transient boolean internalMarkerWhenTicksOverlap; 154 private transient Tick internalMarkerCycleBoundTick; 155 156 /** 157 * Creates a CycleNumberAxis with the given period. 158 * 159 * @param period the period. 160 */ 161 public CyclicNumberAxis(double period) { 162 this(period, 0.0); 163 } 164 165 /** 166 * Creates a CycleNumberAxis with the given period and offset. 167 * 168 * @param period the period. 169 * @param offset the offset. 170 */ 171 public CyclicNumberAxis(double period, double offset) { 172 this(period, offset, null); 173 } 174 175 /** 176 * Creates a named CycleNumberAxis with the given period. 177 * 178 * @param period the period. 179 * @param label the label. 180 */ 181 public CyclicNumberAxis(double period, String label) { 182 this(0, period, label); 183 } 184 185 /** 186 * Creates a named CycleNumberAxis with the given period and offset. 187 * 188 * @param period the period. 189 * @param offset the offset. 190 * @param label the label. 191 */ 192 public CyclicNumberAxis(double period, double offset, String label) { 193 super(label); 194 this.period = period; 195 this.offset = offset; 196 setFixedAutoRange(period); 197 this.advanceLineVisible = true; 198 this.advanceLinePaint = DEFAULT_ADVANCE_LINE_PAINT; 199 } 200 201 /** 202 * The advance line is the line drawn at the limit of the current cycle, 203 * when erasing the previous cycle. 204 * 205 * @return A boolean. 206 */ 207 public boolean isAdvanceLineVisible() { 208 return this.advanceLineVisible; 209 } 210 211 /** 212 * The advance line is the line drawn at the limit of the current cycle, 213 * when erasing the previous cycle. 214 * 215 * @param visible the flag. 216 */ 217 public void setAdvanceLineVisible(boolean visible) { 218 this.advanceLineVisible = visible; 219 } 220 221 /** 222 * The advance line is the line drawn at the limit of the current cycle, 223 * when erasing the previous cycle. 224 * 225 * @return The paint (never <code>null</code>). 226 */ 227 public Paint getAdvanceLinePaint() { 228 return this.advanceLinePaint; 229 } 230 231 /** 232 * The advance line is the line drawn at the limit of the current cycle, 233 * when erasing the previous cycle. 234 * 235 * @param paint the paint (<code>null</code> not permitted). 236 */ 237 public void setAdvanceLinePaint(Paint paint) { 238 if (paint == null) { 239 throw new IllegalArgumentException("Null 'paint' argument."); 240 } 241 this.advanceLinePaint = paint; 242 } 243 244 /** 245 * The advance line is the line drawn at the limit of the current cycle, 246 * when erasing the previous cycle. 247 * 248 * @return The stroke (never <code>null</code>). 249 */ 250 public Stroke getAdvanceLineStroke() { 251 return this.advanceLineStroke; 252 } 253 /** 254 * The advance line is the line drawn at the limit of the current cycle, 255 * when erasing the previous cycle. 256 * 257 * @param stroke the stroke (<code>null</code> not permitted). 258 */ 259 public void setAdvanceLineStroke(Stroke stroke) { 260 if (stroke == null) { 261 throw new IllegalArgumentException("Null 'stroke' argument."); 262 } 263 this.advanceLineStroke = stroke; 264 } 265 266 /** 267 * The cycle bound can be associated either with the current or with the 268 * last cycle. It's up to the user's choice to decide which, as this is 269 * just a convention. By default, the cycle bound is mapped to the current 270 * cycle. 271 * <br> 272 * Note that this has no effect on visual appearance, as the cycle bound is 273 * mapped successively for both axis ends. Use this function for correct 274 * results in translateValueToJava2D. 275 * 276 * @return <code>true</code> if the cycle bound is mapped to the last 277 * cycle, <code>false</code> if it is bound to the current cycle 278 * (default) 279 */ 280 public boolean isBoundMappedToLastCycle() { 281 return this.boundMappedToLastCycle; 282 } 283 284 /** 285 * The cycle bound can be associated either with the current or with the 286 * last cycle. It's up to the user's choice to decide which, as this is 287 * just a convention. By default, the cycle bound is mapped to the current 288 * cycle. 289 * <br> 290 * Note that this has no effect on visual appearance, as the cycle bound is 291 * mapped successively for both axis ends. Use this function for correct 292 * results in valueToJava2D. 293 * 294 * @param boundMappedToLastCycle Set it to true to map the cycle bound to 295 * the last cycle. 296 */ 297 public void setBoundMappedToLastCycle(boolean boundMappedToLastCycle) { 298 this.boundMappedToLastCycle = boundMappedToLastCycle; 299 } 300 301 /** 302 * Selects a tick unit when the axis is displayed horizontally. 303 * 304 * @param g2 the graphics device. 305 * @param drawArea the drawing area. 306 * @param dataArea the data area. 307 * @param edge the side of the rectangle on which the axis is displayed. 308 */ 309 protected void selectHorizontalAutoTickUnit(Graphics2D g2, 310 Rectangle2D drawArea, 311 Rectangle2D dataArea, 312 RectangleEdge edge) { 313 314 double tickLabelWidth 315 = estimateMaximumTickLabelWidth(g2, getTickUnit()); 316 317 // Compute number of labels 318 double n = getRange().getLength() 319 * tickLabelWidth / dataArea.getWidth(); 320 321 setTickUnit( 322 (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n), 323 false, false 324 ); 325 326 } 327 328 /** 329 * Selects a tick unit when the axis is displayed vertically. 330 * 331 * @param g2 the graphics device. 332 * @param drawArea the drawing area. 333 * @param dataArea the data area. 334 * @param edge the side of the rectangle on which the axis is displayed. 335 */ 336 protected void selectVerticalAutoTickUnit(Graphics2D g2, 337 Rectangle2D drawArea, 338 Rectangle2D dataArea, 339 RectangleEdge edge) { 340 341 double tickLabelWidth 342 = estimateMaximumTickLabelWidth(g2, getTickUnit()); 343 344 // Compute number of labels 345 double n = getRange().getLength() 346 * tickLabelWidth / dataArea.getHeight(); 347 348 setTickUnit( 349 (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n), 350 false, false 351 ); 352 353 } 354 355 /** 356 * A special Number tick that also hold information about the cycle bound 357 * mapping for this tick. This is especially useful for having a tick at 358 * each axis end with the cycle bound value. See also 359 * isBoundMappedToLastCycle() 360 */ 361 protected static class CycleBoundTick extends NumberTick { 362 363 /** Map to last cycle. */ 364 public boolean mapToLastCycle; 365 366 /** 367 * Creates a new tick. 368 * 369 * @param mapToLastCycle map to last cycle? 370 * @param number the number. 371 * @param label the label. 372 * @param textAnchor the text anchor. 373 * @param rotationAnchor the rotation anchor. 374 * @param angle the rotation angle. 375 */ 376 public CycleBoundTick(boolean mapToLastCycle, Number number, 377 String label, TextAnchor textAnchor, 378 TextAnchor rotationAnchor, double angle) { 379 super(number, label, textAnchor, rotationAnchor, angle); 380 this.mapToLastCycle = mapToLastCycle; 381 } 382 } 383 384 /** 385 * Calculates the anchor point for a tick. 386 * 387 * @param tick the tick. 388 * @param cursor the cursor. 389 * @param dataArea the data area. 390 * @param edge the side on which the axis is displayed. 391 * 392 * @return The anchor point. 393 */ 394 protected float[] calculateAnchorPoint(ValueTick tick, double cursor, 395 Rectangle2D dataArea, 396 RectangleEdge edge) { 397 if (tick instanceof CycleBoundTick) { 398 boolean mapsav = this.boundMappedToLastCycle; 399 this.boundMappedToLastCycle 400 = ((CycleBoundTick) tick).mapToLastCycle; 401 float[] ret = super.calculateAnchorPoint( 402 tick, cursor, dataArea, edge 403 ); 404 this.boundMappedToLastCycle = mapsav; 405 return ret; 406 } 407 return super.calculateAnchorPoint(tick, cursor, dataArea, edge); 408 } 409 410 411 412 /** 413 * Builds a list of ticks for the axis. This method is called when the 414 * axis is at the top or bottom of the chart (so the axis is "horizontal"). 415 * 416 * @param g2 the graphics device. 417 * @param dataArea the data area. 418 * @param edge the edge. 419 * 420 * @return A list of ticks. 421 */ 422 protected List refreshTicksHorizontal(Graphics2D g2, 423 Rectangle2D dataArea, 424 RectangleEdge edge) { 425 426 List result = new java.util.ArrayList(); 427 428 Font tickLabelFont = getTickLabelFont(); 429 g2.setFont(tickLabelFont); 430 431 if (isAutoTickUnitSelection()) { 432 selectAutoTickUnit(g2, dataArea, edge); 433 } 434 435 double unit = getTickUnit().getSize(); 436 double cycleBound = getCycleBound(); 437 double currentTickValue = Math.ceil(cycleBound / unit) * unit; 438 double upperValue = getRange().getUpperBound(); 439 boolean cycled = false; 440 441 boolean boundMapping = this.boundMappedToLastCycle; 442 this.boundMappedToLastCycle = false; 443 444 CycleBoundTick lastTick = null; 445 float lastX = 0.0f; 446 447 if (upperValue == cycleBound) { 448 currentTickValue = calculateLowestVisibleTickValue(); 449 cycled = true; 450 this.boundMappedToLastCycle = true; 451 } 452 453 while (currentTickValue <= upperValue) { 454 455 // Cycle when necessary 456 boolean cyclenow = false; 457 if ((currentTickValue + unit > upperValue) && !cycled) { 458 cyclenow = true; 459 } 460 461 double xx = valueToJava2D(currentTickValue, dataArea, edge); 462 String tickLabel; 463 NumberFormat formatter = getNumberFormatOverride(); 464 if (formatter != null) { 465 tickLabel = formatter.format(currentTickValue); 466 } 467 else { 468 tickLabel = getTickUnit().valueToString(currentTickValue); 469 } 470 float x = (float) xx; 471 TextAnchor anchor = null; 472 TextAnchor rotationAnchor = null; 473 double angle = 0.0; 474 if (isVerticalTickLabels()) { 475 if (edge == RectangleEdge.TOP) { 476 angle = Math.PI / 2.0; 477 } 478 else { 479 angle = -Math.PI / 2.0; 480 } 481 anchor = TextAnchor.CENTER_RIGHT; 482 // If tick overlap when cycling, update last tick too 483 if ((lastTick != null) && (lastX == x) 484 && (currentTickValue != cycleBound)) { 485 anchor = isInverted() 486 ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT; 487 result.remove(result.size() - 1); 488 result.add(new CycleBoundTick( 489 this.boundMappedToLastCycle, lastTick.getNumber(), 490 lastTick.getText(), anchor, anchor, 491 lastTick.getAngle()) 492 ); 493 this.internalMarkerWhenTicksOverlap = true; 494 anchor = isInverted() 495 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT; 496 } 497 rotationAnchor = anchor; 498 } 499 else { 500 if (edge == RectangleEdge.TOP) { 501 anchor = TextAnchor.BOTTOM_CENTER; 502 if ((lastTick != null) && (lastX == x) 503 && (currentTickValue != cycleBound)) { 504 anchor = isInverted() 505 ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT; 506 result.remove(result.size() - 1); 507 result.add(new CycleBoundTick( 508 this.boundMappedToLastCycle, lastTick.getNumber(), 509 lastTick.getText(), anchor, anchor, 510 lastTick.getAngle()) 511 ); 512 this.internalMarkerWhenTicksOverlap = true; 513 anchor = isInverted() 514 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT; 515 } 516 rotationAnchor = anchor; 517 } 518 else { 519 anchor = TextAnchor.TOP_CENTER; 520 if ((lastTick != null) && (lastX == x) 521 && (currentTickValue != cycleBound)) { 522 anchor = isInverted() 523 ? TextAnchor.TOP_LEFT : TextAnchor.TOP_RIGHT; 524 result.remove(result.size() - 1); 525 result.add(new CycleBoundTick( 526 this.boundMappedToLastCycle, lastTick.getNumber(), 527 lastTick.getText(), anchor, anchor, 528 lastTick.getAngle()) 529 ); 530 this.internalMarkerWhenTicksOverlap = true; 531 anchor = isInverted() 532 ? TextAnchor.TOP_RIGHT : TextAnchor.TOP_LEFT; 533 } 534 rotationAnchor = anchor; 535 } 536 } 537 538 CycleBoundTick tick = new CycleBoundTick( 539 this.boundMappedToLastCycle, 540 new Double(currentTickValue), tickLabel, anchor, 541 rotationAnchor, angle 542 ); 543 if (currentTickValue == cycleBound) { 544 this.internalMarkerCycleBoundTick = tick; 545 } 546 result.add(tick); 547 lastTick = tick; 548 lastX = x; 549 550 currentTickValue += unit; 551 552 if (cyclenow) { 553 currentTickValue = calculateLowestVisibleTickValue(); 554 upperValue = cycleBound; 555 cycled = true; 556 this.boundMappedToLastCycle = true; 557 } 558 559 } 560 this.boundMappedToLastCycle = boundMapping; 561 return result; 562 563 } 564 565 /** 566 * Builds a list of ticks for the axis. This method is called when the 567 * axis is at the left or right of the chart (so the axis is "vertical"). 568 * 569 * @param g2 the graphics device. 570 * @param dataArea the data area. 571 * @param edge the edge. 572 * 573 * @return A list of ticks. 574 */ 575 protected List refreshVerticalTicks(Graphics2D g2, 576 Rectangle2D dataArea, 577 RectangleEdge edge) { 578 579 List result = new java.util.ArrayList(); 580 result.clear(); 581 582 Font tickLabelFont = getTickLabelFont(); 583 g2.setFont(tickLabelFont); 584 if (isAutoTickUnitSelection()) { 585 selectAutoTickUnit(g2, dataArea, edge); 586 } 587 588 double unit = getTickUnit().getSize(); 589 double cycleBound = getCycleBound(); 590 double currentTickValue = Math.ceil(cycleBound / unit) * unit; 591 double upperValue = getRange().getUpperBound(); 592 boolean cycled = false; 593 594 boolean boundMapping = this.boundMappedToLastCycle; 595 this.boundMappedToLastCycle = true; 596 597 NumberTick lastTick = null; 598 float lastY = 0.0f; 599 600 if (upperValue == cycleBound) { 601 currentTickValue = calculateLowestVisibleTickValue(); 602 cycled = true; 603 this.boundMappedToLastCycle = true; 604 } 605 606 while (currentTickValue <= upperValue) { 607 608 // Cycle when necessary 609 boolean cyclenow = false; 610 if ((currentTickValue + unit > upperValue) && !cycled) { 611 cyclenow = true; 612 } 613 614 double yy = valueToJava2D(currentTickValue, dataArea, edge); 615 String tickLabel; 616 NumberFormat formatter = getNumberFormatOverride(); 617 if (formatter != null) { 618 tickLabel = formatter.format(currentTickValue); 619 } 620 else { 621 tickLabel = getTickUnit().valueToString(currentTickValue); 622 } 623 624 float y = (float) yy; 625 TextAnchor anchor = null; 626 TextAnchor rotationAnchor = null; 627 double angle = 0.0; 628 if (isVerticalTickLabels()) { 629 630 if (edge == RectangleEdge.LEFT) { 631 anchor = TextAnchor.BOTTOM_CENTER; 632 if ((lastTick != null) && (lastY == y) 633 && (currentTickValue != cycleBound)) { 634 anchor = isInverted() 635 ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT; 636 result.remove(result.size() - 1); 637 result.add(new CycleBoundTick( 638 this.boundMappedToLastCycle, lastTick.getNumber(), 639 lastTick.getText(), anchor, anchor, 640 lastTick.getAngle()) 641 ); 642 this.internalMarkerWhenTicksOverlap = true; 643 anchor = isInverted() 644 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT; 645 } 646 rotationAnchor = anchor; 647 angle = -Math.PI / 2.0; 648 } 649 else { 650 anchor = TextAnchor.BOTTOM_CENTER; 651 if ((lastTick != null) && (lastY == y) 652 && (currentTickValue != cycleBound)) { 653 anchor = isInverted() 654 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT; 655 result.remove(result.size() - 1); 656 result.add(new CycleBoundTick( 657 this.boundMappedToLastCycle, lastTick.getNumber(), 658 lastTick.getText(), anchor, anchor, 659 lastTick.getAngle()) 660 ); 661 this.internalMarkerWhenTicksOverlap = true; 662 anchor = isInverted() 663 ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT; 664 } 665 rotationAnchor = anchor; 666 angle = Math.PI / 2.0; 667 } 668 } 669 else { 670 if (edge == RectangleEdge.LEFT) { 671 anchor = TextAnchor.CENTER_RIGHT; 672 if ((lastTick != null) && (lastY == y) 673 && (currentTickValue != cycleBound)) { 674 anchor = isInverted() 675 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT; 676 result.remove(result.size() - 1); 677 result.add(new CycleBoundTick( 678 this.boundMappedToLastCycle, lastTick.getNumber(), 679 lastTick.getText(), anchor, anchor, 680 lastTick.getAngle()) 681 ); 682 this.internalMarkerWhenTicksOverlap = true; 683 anchor = isInverted() 684 ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT; 685 } 686 rotationAnchor = anchor; 687 } 688 else { 689 anchor = TextAnchor.CENTER_LEFT; 690 if ((lastTick != null) && (lastY == y) 691 && (currentTickValue != cycleBound)) { 692 anchor = isInverted() 693 ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_LEFT; 694 result.remove(result.size() - 1); 695 result.add(new CycleBoundTick( 696 this.boundMappedToLastCycle, lastTick.getNumber(), 697 lastTick.getText(), anchor, anchor, 698 lastTick.getAngle()) 699 ); 700 this.internalMarkerWhenTicksOverlap = true; 701 anchor = isInverted() 702 ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_LEFT; 703 } 704 rotationAnchor = anchor; 705 } 706 } 707 708 CycleBoundTick tick = new CycleBoundTick( 709 this.boundMappedToLastCycle, new Double(currentTickValue), 710 tickLabel, anchor, rotationAnchor, angle 711 ); 712 if (currentTickValue == cycleBound) { 713 this.internalMarkerCycleBoundTick = tick; 714 } 715 result.add(tick); 716 lastTick = tick; 717 lastY = y; 718 719 if (currentTickValue == cycleBound) { 720 this.internalMarkerCycleBoundTick = tick; 721 } 722 723 currentTickValue += unit; 724 725 if (cyclenow) { 726 currentTickValue = calculateLowestVisibleTickValue(); 727 upperValue = cycleBound; 728 cycled = true; 729 this.boundMappedToLastCycle = false; 730 } 731 732 } 733 this.boundMappedToLastCycle = boundMapping; 734 return result; 735 } 736 737 /** 738 * Converts a coordinate from Java 2D space to data space. 739 * 740 * @param java2DValue the coordinate in Java2D space. 741 * @param dataArea the data area. 742 * @param edge the edge. 743 * 744 * @return The data value. 745 */ 746 public double java2DToValue(double java2DValue, Rectangle2D dataArea, 747 RectangleEdge edge) { 748 Range range = getRange(); 749 750 double vmax = range.getUpperBound(); 751 double vp = getCycleBound(); 752 753 double jmin = 0.0; 754 double jmax = 0.0; 755 if (RectangleEdge.isTopOrBottom(edge)) { 756 jmin = dataArea.getMinX(); 757 jmax = dataArea.getMaxX(); 758 } 759 else if (RectangleEdge.isLeftOrRight(edge)) { 760 jmin = dataArea.getMaxY(); 761 jmax = dataArea.getMinY(); 762 } 763 764 if (isInverted()) { 765 double jbreak = jmax - (vmax - vp) * (jmax - jmin) / this.period; 766 if (java2DValue >= jbreak) { 767 return vp + (jmax - java2DValue) * this.period / (jmax - jmin); 768 } 769 else { 770 return vp - (java2DValue - jmin) * this.period / (jmax - jmin); 771 } 772 } 773 else { 774 double jbreak = (vmax - vp) * (jmax - jmin) / this.period + jmin; 775 if (java2DValue <= jbreak) { 776 return vp + (java2DValue - jmin) * this.period / (jmax - jmin); 777 } 778 else { 779 return vp - (jmax - java2DValue) * this.period / (jmax - jmin); 780 } 781 } 782 } 783 784 /** 785 * Translates a value from data space to Java 2D space. 786 * 787 * @param value the data value. 788 * @param dataArea the data area. 789 * @param edge the edge. 790 * 791 * @return The Java 2D value. 792 */ 793 public double valueToJava2D(double value, Rectangle2D dataArea, 794 RectangleEdge edge) { 795 Range range = getRange(); 796 797 double vmin = range.getLowerBound(); 798 double vmax = range.getUpperBound(); 799 double vp = getCycleBound(); 800 801 if ((value < vmin) || (value > vmax)) { 802 return Double.NaN; 803 } 804 805 806 double jmin = 0.0; 807 double jmax = 0.0; 808 if (RectangleEdge.isTopOrBottom(edge)) { 809 jmin = dataArea.getMinX(); 810 jmax = dataArea.getMaxX(); 811 } 812 else if (RectangleEdge.isLeftOrRight(edge)) { 813 jmax = dataArea.getMinY(); 814 jmin = dataArea.getMaxY(); 815 } 816 817 if (isInverted()) { 818 if (value == vp) { 819 return this.boundMappedToLastCycle ? jmin : jmax; 820 } 821 else if (value > vp) { 822 return jmax - (value - vp) * (jmax - jmin) / this.period; 823 } 824 else { 825 return jmin + (vp - value) * (jmax - jmin) / this.period; 826 } 827 } 828 else { 829 if (value == vp) { 830 return this.boundMappedToLastCycle ? jmax : jmin; 831 } 832 else if (value >= vp) { 833 return jmin + (value - vp) * (jmax - jmin) / this.period; 834 } 835 else { 836 return jmax - (vp - value) * (jmax - jmin) / this.period; 837 } 838 } 839 } 840 841 /** 842 * Centers the range about the given value. 843 * 844 * @param value the data value. 845 */ 846 public void centerRange(double value) { 847 setRange(value - this.period / 2.0, value + this.period / 2.0); 848 } 849 850 /** 851 * This function is nearly useless since the auto range is fixed for this 852 * class to the period. The period is extended if necessary to fit the 853 * minimum size. 854 * 855 * @param size the size. 856 * @param notify notify? 857 * 858 * @see org.jfree.chart.axis.ValueAxis#setAutoRangeMinimumSize(double, 859 * boolean) 860 */ 861 public void setAutoRangeMinimumSize(double size, boolean notify) { 862 if (size > this.period) { 863 this.period = size; 864 } 865 super.setAutoRangeMinimumSize(size, notify); 866 } 867 868 /** 869 * The auto range is fixed for this class to the period by default. 870 * This function will thus set a new period. 871 * 872 * @param length the length. 873 * 874 * @see org.jfree.chart.axis.ValueAxis#setFixedAutoRange(double) 875 */ 876 public void setFixedAutoRange(double length) { 877 this.period = length; 878 super.setFixedAutoRange(length); 879 } 880 881 /** 882 * Sets a new axis range. The period is extended to fit the range size, if 883 * necessary. 884 * 885 * @param range the range. 886 * @param turnOffAutoRange switch off the auto range. 887 * @param notify notify? 888 * 889 * @see org.jfree.chart.axis.ValueAxis#setRange(Range, boolean, boolean) 890 */ 891 public void setRange(Range range, boolean turnOffAutoRange, 892 boolean notify) { 893 double size = range.getUpperBound() - range.getLowerBound(); 894 if (size > this.period) { 895 this.period = size; 896 } 897 super.setRange(range, turnOffAutoRange, notify); 898 } 899 900 /** 901 * The cycle bound is defined as the higest value x such that 902 * "offset + period * i = x", with i and integer and x < 903 * range.getUpperBound() This is the value which is at both ends of the 904 * axis : x...up|low...x 905 * The values from x to up are the valued in the current cycle. 906 * The values from low to x are the valued in the previous cycle. 907 * 908 * @return The cycle bound. 909 */ 910 public double getCycleBound() { 911 return Math.floor( 912 (getRange().getUpperBound() - this.offset) / this.period 913 ) * this.period + this.offset; 914 } 915 916 /** 917 * The cycle bound is a multiple of the period, plus optionally a start 918 * offset. 919 * <P> 920 * <pre>cb = n * period + offset</pre><br> 921 * 922 * @return The current offset. 923 * 924 * @see #getCycleBound() 925 */ 926 public double getOffset() { 927 return this.offset; 928 } 929 930 /** 931 * The cycle bound is a multiple of the period, plus optionally a start 932 * offset. 933 * <P> 934 * <pre>cb = n * period + offset</pre><br> 935 * 936 * @param offset The offset to set. 937 * 938 * @see #getCycleBound() 939 */ 940 public void setOffset(double offset) { 941 this.offset = offset; 942 } 943 944 /** 945 * The cycle bound is a multiple of the period, plus optionally a start 946 * offset. 947 * <P> 948 * <pre>cb = n * period + offset</pre><br> 949 * 950 * @return The current period. 951 * 952 * @see #getCycleBound() 953 */ 954 public double getPeriod() { 955 return this.period; 956 } 957 958 /** 959 * The cycle bound is a multiple of the period, plus optionally a start 960 * offset. 961 * <P> 962 * <pre>cb = n * period + offset</pre><br> 963 * 964 * @param period The period to set. 965 * 966 * @see #getCycleBound() 967 */ 968 public void setPeriod(double period) { 969 this.period = period; 970 } 971 972 /** 973 * Draws the tick marks and labels. 974 * 975 * @param g2 the graphics device. 976 * @param cursor the cursor. 977 * @param plotArea the plot area. 978 * @param dataArea the area inside the axes. 979 * @param edge the side on which the axis is displayed. 980 * 981 * @return The axis state. 982 */ 983 protected AxisState drawTickMarksAndLabels(Graphics2D g2, double cursor, 984 Rectangle2D plotArea, 985 Rectangle2D dataArea, 986 RectangleEdge edge) { 987 this.internalMarkerWhenTicksOverlap = false; 988 AxisState ret = super.drawTickMarksAndLabels( 989 g2, cursor, plotArea, dataArea, edge 990 ); 991 992 // continue and separate the labels only if necessary 993 if (!this.internalMarkerWhenTicksOverlap) { 994 return ret; 995 } 996 997 double ol = getTickMarkOutsideLength(); 998 FontMetrics fm = g2.getFontMetrics(getTickLabelFont()); 999 1000 if (isVerticalTickLabels()) { 1001 ol = fm.getMaxAdvance(); 1002 } 1003 else { 1004 ol = fm.getHeight(); 1005 } 1006 1007 double il = 0; 1008 if (isTickMarksVisible()) { 1009 float xx = (float) valueToJava2D( 1010 getRange().getUpperBound(), dataArea, edge 1011 ); 1012 Line2D mark = null; 1013 g2.setStroke(getTickMarkStroke()); 1014 g2.setPaint(getTickMarkPaint()); 1015 if (edge == RectangleEdge.LEFT) { 1016 mark = new Line2D.Double(cursor - ol, xx, cursor + il, xx); 1017 } 1018 else if (edge == RectangleEdge.RIGHT) { 1019 mark = new Line2D.Double(cursor + ol, xx, cursor - il, xx); 1020 } 1021 else if (edge == RectangleEdge.TOP) { 1022 mark = new Line2D.Double(xx, cursor - ol, xx, cursor + il); 1023 } 1024 else if (edge == RectangleEdge.BOTTOM) { 1025 mark = new Line2D.Double(xx, cursor + ol, xx, cursor - il); 1026 } 1027 g2.draw(mark); 1028 } 1029 return ret; 1030 } 1031 1032 /** 1033 * Draws the axis. 1034 * 1035 * @param g2 the graphics device (<code>null</code> not permitted). 1036 * @param cursor the cursor position. 1037 * @param plotArea the plot area (<code>null</code> not permitted). 1038 * @param dataArea the data area (<code>null</code> not permitted). 1039 * @param edge the edge (<code>null</code> not permitted). 1040 * @param plotState collects information about the plot 1041 * (<code>null</code> permitted). 1042 * 1043 * @return The axis state (never <code>null</code>). 1044 */ 1045 public AxisState draw(Graphics2D g2, 1046 double cursor, 1047 Rectangle2D plotArea, 1048 Rectangle2D dataArea, 1049 RectangleEdge edge, 1050 PlotRenderingInfo plotState) { 1051 1052 AxisState ret = super.draw( 1053 g2, cursor, plotArea, dataArea, edge, plotState 1054 ); 1055 if (isAdvanceLineVisible()) { 1056 double xx = valueToJava2D( 1057 getRange().getUpperBound(), dataArea, edge 1058 ); 1059 Line2D mark = null; 1060 g2.setStroke(getAdvanceLineStroke()); 1061 g2.setPaint(getAdvanceLinePaint()); 1062 if (edge == RectangleEdge.LEFT) { 1063 mark = new Line2D.Double( 1064 cursor, xx, cursor + dataArea.getWidth(), xx 1065 ); 1066 } 1067 else if (edge == RectangleEdge.RIGHT) { 1068 mark = new Line2D.Double( 1069 cursor - dataArea.getWidth(), xx, cursor, xx 1070 ); 1071 } 1072 else if (edge == RectangleEdge.TOP) { 1073 mark = new Line2D.Double( 1074 xx, cursor + dataArea.getHeight(), xx, cursor 1075 ); 1076 } 1077 else if (edge == RectangleEdge.BOTTOM) { 1078 mark = new Line2D.Double( 1079 xx, cursor, xx, cursor - dataArea.getHeight() 1080 ); 1081 } 1082 g2.draw(mark); 1083 } 1084 return ret; 1085 } 1086 1087 /** 1088 * Reserve some space on each axis side because we draw a centered label at 1089 * each extremity. 1090 * 1091 * @param g2 the graphics device. 1092 * @param plot the plot. 1093 * @param plotArea the plot area. 1094 * @param edge the edge. 1095 * @param space the space already reserved. 1096 * 1097 * @return The reserved space. 1098 */ 1099 public AxisSpace reserveSpace(Graphics2D g2, 1100 Plot plot, 1101 Rectangle2D plotArea, 1102 RectangleEdge edge, 1103 AxisSpace space) { 1104 1105 this.internalMarkerCycleBoundTick = null; 1106 AxisSpace ret = super.reserveSpace(g2, plot, plotArea, edge, space); 1107 if (this.internalMarkerCycleBoundTick == null) { 1108 return ret; 1109 } 1110 1111 FontMetrics fm = g2.getFontMetrics(getTickLabelFont()); 1112 Rectangle2D r = TextUtilities.getTextBounds( 1113 this.internalMarkerCycleBoundTick.getText(), g2, fm 1114 ); 1115 1116 if (RectangleEdge.isTopOrBottom(edge)) { 1117 if (isVerticalTickLabels()) { 1118 space.add(r.getHeight() / 2, RectangleEdge.RIGHT); 1119 } 1120 else { 1121 space.add(r.getWidth() / 2, RectangleEdge.RIGHT); 1122 } 1123 } 1124 else if (RectangleEdge.isLeftOrRight(edge)) { 1125 if (isVerticalTickLabels()) { 1126 space.add(r.getWidth() / 2, RectangleEdge.TOP); 1127 } 1128 else { 1129 space.add(r.getHeight() / 2, RectangleEdge.TOP); 1130 } 1131 } 1132 1133 return ret; 1134 1135 } 1136 1137 /** 1138 * Provides serialization support. 1139 * 1140 * @param stream the output stream. 1141 * 1142 * @throws IOException if there is an I/O error. 1143 */ 1144 private void writeObject(ObjectOutputStream stream) throws IOException { 1145 1146 stream.defaultWriteObject(); 1147 SerialUtilities.writePaint(this.advanceLinePaint, stream); 1148 SerialUtilities.writeStroke(this.advanceLineStroke, stream); 1149 1150 } 1151 1152 /** 1153 * Provides serialization support. 1154 * 1155 * @param stream the input stream. 1156 * 1157 * @throws IOException if there is an I/O error. 1158 * @throws ClassNotFoundException if there is a classpath problem. 1159 */ 1160 private void readObject(ObjectInputStream stream) 1161 throws IOException, ClassNotFoundException { 1162 1163 stream.defaultReadObject(); 1164 this.advanceLinePaint = SerialUtilities.readPaint(stream); 1165 this.advanceLineStroke = SerialUtilities.readStroke(stream); 1166 1167 } 1168 1169 1170 /** 1171 * Tests the axis for equality with another object. 1172 * 1173 * @param obj the object to test against. 1174 * 1175 * @return A boolean. 1176 */ 1177 public boolean equals(Object obj) { 1178 if (obj == this) { 1179 return true; 1180 } 1181 if (!(obj instanceof CyclicNumberAxis)) { 1182 return false; 1183 } 1184 if (!super.equals(obj)) { 1185 return false; 1186 } 1187 CyclicNumberAxis that = (CyclicNumberAxis) obj; 1188 if (this.period != that.period) { 1189 return false; 1190 } 1191 if (this.offset != that.offset) { 1192 return false; 1193 } 1194 if (!PaintUtilities.equal(this.advanceLinePaint, 1195 that.advanceLinePaint)) { 1196 return false; 1197 } 1198 if (!ObjectUtilities.equal(this.advanceLineStroke, 1199 that.advanceLineStroke)) { 1200 return false; 1201 } 1202 if (this.advanceLineVisible != that.advanceLineVisible) { 1203 return false; 1204 } 1205 if (this.boundMappedToLastCycle != that.boundMappedToLastCycle) { 1206 return false; 1207 } 1208 return true; 1209 } 1210 }