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