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