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