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