001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2022, 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-2021, 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.api.RectangleEdge; 059import org.jfree.chart.text.TextAnchor; 060import org.jfree.chart.internal.PaintUtils; 061import org.jfree.chart.internal.Args; 062import org.jfree.chart.internal.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(this.boundMappedToLastCycle, 517 currentTickValue, tickLabel, anchor, rotationAnchor, angle); 518 if (currentTickValue == cycleBound) { 519 this.internalMarkerCycleBoundTick = tick; 520 } 521 result.add(tick); 522 lastTick = tick; 523 lastX = x; 524 525 currentTickValue += unit; 526 527 if (cyclenow) { 528 currentTickValue = calculateLowestVisibleTickValue(); 529 upperValue = cycleBound; 530 cycled = true; 531 this.boundMappedToLastCycle = true; 532 } 533 534 } 535 this.boundMappedToLastCycle = boundMapping; 536 return result; 537 538 } 539 540 /** 541 * Builds a list of ticks for the axis. This method is called when the 542 * axis is at the left or right of the chart (so the axis is "vertical"). 543 * 544 * @param g2 the graphics device. 545 * @param dataArea the data area. 546 * @param edge the edge. 547 * 548 * @return A list of ticks. 549 */ 550 protected List refreshVerticalTicks(Graphics2D g2, Rectangle2D dataArea, 551 RectangleEdge edge) { 552 553 List result = new java.util.ArrayList(); 554 result.clear(); 555 556 Font tickLabelFont = getTickLabelFont(); 557 g2.setFont(tickLabelFont); 558 if (isAutoTickUnitSelection()) { 559 selectAutoTickUnit(g2, dataArea, edge); 560 } 561 562 double unit = getTickUnit().getSize(); 563 double cycleBound = getCycleBound(); 564 double currentTickValue = Math.ceil(cycleBound / unit) * unit; 565 double upperValue = getRange().getUpperBound(); 566 boolean cycled = false; 567 568 boolean boundMapping = this.boundMappedToLastCycle; 569 this.boundMappedToLastCycle = true; 570 571 NumberTick lastTick = null; 572 float lastY = 0.0f; 573 574 if (upperValue == cycleBound) { 575 currentTickValue = calculateLowestVisibleTickValue(); 576 cycled = true; 577 this.boundMappedToLastCycle = true; 578 } 579 580 while (currentTickValue <= upperValue) { 581 582 // Cycle when necessary 583 boolean cyclenow = false; 584 if ((currentTickValue + unit > upperValue) && !cycled) { 585 cyclenow = true; 586 } 587 588 double yy = valueToJava2D(currentTickValue, dataArea, edge); 589 String tickLabel; 590 NumberFormat formatter = getNumberFormatOverride(); 591 if (formatter != null) { 592 tickLabel = formatter.format(currentTickValue); 593 } 594 else { 595 tickLabel = getTickUnit().valueToString(currentTickValue); 596 } 597 598 float y = (float) yy; 599 TextAnchor anchor; 600 TextAnchor rotationAnchor; 601 double angle = 0.0; 602 if (isVerticalTickLabels()) { 603 604 if (edge == RectangleEdge.LEFT) { 605 anchor = TextAnchor.BOTTOM_CENTER; 606 if ((lastTick != null) && (lastY == y) 607 && (currentTickValue != cycleBound)) { 608 anchor = isInverted() 609 ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT; 610 result.remove(result.size() - 1); 611 result.add(new CycleBoundTick( 612 this.boundMappedToLastCycle, lastTick.getNumber(), 613 lastTick.getText(), anchor, anchor, 614 lastTick.getAngle()) 615 ); 616 this.internalMarkerWhenTicksOverlap = true; 617 anchor = isInverted() 618 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT; 619 } 620 rotationAnchor = anchor; 621 angle = -Math.PI / 2.0; 622 } 623 else { 624 anchor = TextAnchor.BOTTOM_CENTER; 625 if ((lastTick != null) && (lastY == y) 626 && (currentTickValue != cycleBound)) { 627 anchor = isInverted() 628 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT; 629 result.remove(result.size() - 1); 630 result.add(new CycleBoundTick( 631 this.boundMappedToLastCycle, lastTick.getNumber(), 632 lastTick.getText(), anchor, anchor, 633 lastTick.getAngle()) 634 ); 635 this.internalMarkerWhenTicksOverlap = true; 636 anchor = isInverted() 637 ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT; 638 } 639 rotationAnchor = anchor; 640 angle = Math.PI / 2.0; 641 } 642 } 643 else { 644 if (edge == RectangleEdge.LEFT) { 645 anchor = TextAnchor.CENTER_RIGHT; 646 if ((lastTick != null) && (lastY == y) 647 && (currentTickValue != cycleBound)) { 648 anchor = isInverted() 649 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT; 650 result.remove(result.size() - 1); 651 result.add(new CycleBoundTick( 652 this.boundMappedToLastCycle, lastTick.getNumber(), 653 lastTick.getText(), anchor, anchor, 654 lastTick.getAngle()) 655 ); 656 this.internalMarkerWhenTicksOverlap = true; 657 anchor = isInverted() 658 ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT; 659 } 660 rotationAnchor = anchor; 661 } 662 else { 663 anchor = TextAnchor.CENTER_LEFT; 664 if ((lastTick != null) && (lastY == y) 665 && (currentTickValue != cycleBound)) { 666 anchor = isInverted() 667 ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_LEFT; 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_LEFT : TextAnchor.BOTTOM_LEFT; 677 } 678 rotationAnchor = anchor; 679 } 680 } 681 682 CycleBoundTick tick = new CycleBoundTick(this.boundMappedToLastCycle, 683 currentTickValue, tickLabel, anchor, rotationAnchor, angle); 684 if (currentTickValue == cycleBound) { 685 this.internalMarkerCycleBoundTick = tick; 686 } 687 result.add(tick); 688 lastTick = tick; 689 lastY = y; 690 691 if (currentTickValue == cycleBound) { 692 this.internalMarkerCycleBoundTick = tick; 693 } 694 695 currentTickValue += unit; 696 697 if (cyclenow) { 698 currentTickValue = calculateLowestVisibleTickValue(); 699 upperValue = cycleBound; 700 cycled = true; 701 this.boundMappedToLastCycle = false; 702 } 703 704 } 705 this.boundMappedToLastCycle = boundMapping; 706 return result; 707 } 708 709 /** 710 * Converts a coordinate from Java 2D space to data space. 711 * 712 * @param java2DValue the coordinate in Java2D space. 713 * @param dataArea the data area. 714 * @param edge the edge. 715 * 716 * @return The data value. 717 */ 718 @Override 719 public double java2DToValue(double java2DValue, Rectangle2D dataArea, 720 RectangleEdge edge) { 721 Range range = getRange(); 722 723 double vmax = range.getUpperBound(); 724 double vp = getCycleBound(); 725 726 double jmin = 0.0; 727 double jmax = 0.0; 728 if (RectangleEdge.isTopOrBottom(edge)) { 729 jmin = dataArea.getMinX(); 730 jmax = dataArea.getMaxX(); 731 } 732 else if (RectangleEdge.isLeftOrRight(edge)) { 733 jmin = dataArea.getMaxY(); 734 jmax = dataArea.getMinY(); 735 } 736 737 if (isInverted()) { 738 double jbreak = jmax - (vmax - vp) * (jmax - jmin) / this.period; 739 if (java2DValue >= jbreak) { 740 return vp + (jmax - java2DValue) * this.period / (jmax - jmin); 741 } 742 else { 743 return vp - (java2DValue - jmin) * this.period / (jmax - jmin); 744 } 745 } 746 else { 747 double jbreak = (vmax - vp) * (jmax - jmin) / this.period + jmin; 748 if (java2DValue <= jbreak) { 749 return vp + (java2DValue - jmin) * this.period / (jmax - jmin); 750 } 751 else { 752 return vp - (jmax - java2DValue) * this.period / (jmax - jmin); 753 } 754 } 755 } 756 757 /** 758 * Translates a value from data space to Java 2D space. 759 * 760 * @param value the data value. 761 * @param dataArea the data area. 762 * @param edge the edge. 763 * 764 * @return The Java 2D value. 765 */ 766 @Override 767 public double valueToJava2D(double value, Rectangle2D dataArea, 768 RectangleEdge edge) { 769 Range range = getRange(); 770 771 double vmin = range.getLowerBound(); 772 double vmax = range.getUpperBound(); 773 double vp = getCycleBound(); 774 775 if ((value < vmin) || (value > vmax)) { 776 return Double.NaN; 777 } 778 779 780 double jmin = 0.0; 781 double jmax = 0.0; 782 if (RectangleEdge.isTopOrBottom(edge)) { 783 jmin = dataArea.getMinX(); 784 jmax = dataArea.getMaxX(); 785 } 786 else if (RectangleEdge.isLeftOrRight(edge)) { 787 jmax = dataArea.getMinY(); 788 jmin = dataArea.getMaxY(); 789 } 790 791 if (isInverted()) { 792 if (value == vp) { 793 return this.boundMappedToLastCycle ? jmin : jmax; 794 } 795 else if (value > vp) { 796 return jmax - (value - vp) * (jmax - jmin) / this.period; 797 } 798 else { 799 return jmin + (vp - value) * (jmax - jmin) / this.period; 800 } 801 } 802 else { 803 if (value == vp) { 804 return this.boundMappedToLastCycle ? jmax : jmin; 805 } 806 else if (value >= vp) { 807 return jmin + (value - vp) * (jmax - jmin) / this.period; 808 } 809 else { 810 return jmax - (vp - value) * (jmax - jmin) / this.period; 811 } 812 } 813 } 814 815 /** 816 * Centers the range about the given value. 817 * 818 * @param value the data value. 819 */ 820 @Override 821 public void centerRange(double value) { 822 setRange(value - this.period / 2.0, value + this.period / 2.0); 823 } 824 825 /** 826 * This function is nearly useless since the auto range is fixed for this 827 * class to the period. The period is extended if necessary to fit the 828 * minimum size. 829 * 830 * @param size the size. 831 * @param notify notify? 832 * 833 * @see org.jfree.chart.axis.ValueAxis#setAutoRangeMinimumSize(double, 834 * boolean) 835 */ 836 @Override 837 public void setAutoRangeMinimumSize(double size, boolean notify) { 838 if (size > this.period) { 839 this.period = size; 840 } 841 super.setAutoRangeMinimumSize(size, notify); 842 } 843 844 /** 845 * The auto range is fixed for this class to the period by default. 846 * This function will thus set a new period. 847 * 848 * @param length the length. 849 * 850 * @see org.jfree.chart.axis.ValueAxis#setFixedAutoRange(double) 851 */ 852 @Override 853 public void setFixedAutoRange(double length) { 854 this.period = length; 855 super.setFixedAutoRange(length); 856 } 857 858 /** 859 * Sets a new axis range. The period is extended to fit the range size, if 860 * necessary. 861 * 862 * @param range the range. 863 * @param turnOffAutoRange switch off the auto range. 864 * @param notify notify? 865 * 866 * @see org.jfree.chart.axis.ValueAxis#setRange(Range, boolean, boolean) 867 */ 868 @Override 869 public void setRange(Range range, boolean turnOffAutoRange, 870 boolean notify) { 871 double size = range.getUpperBound() - range.getLowerBound(); 872 if (size > this.period) { 873 this.period = size; 874 } 875 super.setRange(range, turnOffAutoRange, notify); 876 } 877 878 /** 879 * The cycle bound is defined as the higest value x such that 880 * "offset + period * i = x", with i and integer and x < 881 * range.getUpperBound() This is the value which is at both ends of the 882 * axis : x...up|low...x 883 * The values from x to up are the valued in the current cycle. 884 * The values from low to x are the valued in the previous cycle. 885 * 886 * @return The cycle bound. 887 */ 888 public double getCycleBound() { 889 return Math.floor( 890 (getRange().getUpperBound() - this.offset) / this.period 891 ) * this.period + this.offset; 892 } 893 894 /** 895 * The cycle bound is a multiple of the period, plus optionally a start 896 * offset. 897 * <pre>cb = n * period + offset</pre> 898 * 899 * @return The current offset. 900 * 901 * @see #getCycleBound() 902 */ 903 public double getOffset() { 904 return this.offset; 905 } 906 907 /** 908 * The cycle bound is a multiple of the period, plus optionally a start 909 * offset. 910 * <pre>cb = n * period + offset</pre> 911 * 912 * @param offset The offset to set. 913 * 914 * @see #getCycleBound() 915 */ 916 public void setOffset(double offset) { 917 this.offset = offset; 918 } 919 920 /** 921 * The cycle bound is a multiple of the period, plus optionally a start 922 * offset. 923 * <pre>cb = n * period + offset</pre> 924 * 925 * @return The current period. 926 * 927 * @see #getCycleBound() 928 */ 929 public double getPeriod() { 930 return this.period; 931 } 932 933 /** 934 * The cycle bound is a multiple of the period, plus optionally a start 935 * offset. 936 * <pre>cb = n * period + offset</pre> 937 * 938 * @param period The period to set. 939 * 940 * @see #getCycleBound() 941 */ 942 public void setPeriod(double period) { 943 this.period = period; 944 } 945 946 /** 947 * Draws the tick marks and labels. 948 * 949 * @param g2 the graphics device. 950 * @param cursor the cursor. 951 * @param plotArea the plot area. 952 * @param dataArea the area inside the axes. 953 * @param edge the side on which the axis is displayed. 954 * 955 * @return The axis state. 956 */ 957 @Override 958 protected AxisState drawTickMarksAndLabels(Graphics2D g2, double cursor, 959 Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge) { 960 this.internalMarkerWhenTicksOverlap = false; 961 AxisState ret = super.drawTickMarksAndLabels(g2, cursor, plotArea, 962 dataArea, edge); 963 964 // continue and separate the labels only if necessary 965 if (!this.internalMarkerWhenTicksOverlap) { 966 return ret; 967 } 968 969 double ol; 970 FontMetrics fm = g2.getFontMetrics(getTickLabelFont()); 971 if (isVerticalTickLabels()) { 972 ol = fm.getMaxAdvance(); 973 } 974 else { 975 ol = fm.getHeight(); 976 } 977 978 double il = 0; 979 if (isTickMarksVisible()) { 980 float xx = (float) valueToJava2D(getRange().getUpperBound(), 981 dataArea, edge); 982 Line2D mark = null; 983 g2.setStroke(getTickMarkStroke()); 984 g2.setPaint(getTickMarkPaint()); 985 if (edge == RectangleEdge.LEFT) { 986 mark = new Line2D.Double(cursor - ol, xx, cursor + il, xx); 987 } 988 else if (edge == RectangleEdge.RIGHT) { 989 mark = new Line2D.Double(cursor + ol, xx, cursor - il, xx); 990 } 991 else if (edge == RectangleEdge.TOP) { 992 mark = new Line2D.Double(xx, cursor - ol, xx, cursor + il); 993 } 994 else if (edge == RectangleEdge.BOTTOM) { 995 mark = new Line2D.Double(xx, cursor + ol, xx, cursor - il); 996 } 997 g2.draw(mark); 998 } 999 return ret; 1000 } 1001 1002 /** 1003 * Draws the axis. 1004 * 1005 * @param g2 the graphics device ({@code null} not permitted). 1006 * @param cursor the cursor position. 1007 * @param plotArea the plot area ({@code null} not permitted). 1008 * @param dataArea the data area ({@code null} not permitted). 1009 * @param edge the edge ({@code null} not permitted). 1010 * @param plotState collects information about the plot 1011 * ({@code null} permitted). 1012 * 1013 * @return The axis state (never {@code null}). 1014 */ 1015 @Override 1016 public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, 1017 Rectangle2D dataArea, RectangleEdge edge, PlotRenderingInfo plotState) { 1018 1019 AxisState ret = super.draw(g2, cursor, plotArea, dataArea, edge, 1020 plotState); 1021 if (isAdvanceLineVisible()) { 1022 double xx = valueToJava2D(getRange().getUpperBound(), dataArea, 1023 edge); 1024 Line2D mark = null; 1025 g2.setStroke(getAdvanceLineStroke()); 1026 g2.setPaint(getAdvanceLinePaint()); 1027 if (edge == RectangleEdge.LEFT) { 1028 mark = new Line2D.Double(cursor, xx, cursor 1029 + dataArea.getWidth(), xx); 1030 } 1031 else if (edge == RectangleEdge.RIGHT) { 1032 mark = new Line2D.Double(cursor - dataArea.getWidth(), xx, 1033 cursor, xx); 1034 } 1035 else if (edge == RectangleEdge.TOP) { 1036 mark = new Line2D.Double(xx, cursor + dataArea.getHeight(), xx, 1037 cursor); 1038 } 1039 else if (edge == RectangleEdge.BOTTOM) { 1040 mark = new Line2D.Double(xx, cursor, xx, 1041 cursor - dataArea.getHeight()); 1042 } 1043 g2.draw(mark); 1044 } 1045 return ret; 1046 } 1047 1048 /** 1049 * Reserve some space on each axis side because we draw a centered label at 1050 * each extremity. 1051 * 1052 * @param g2 the graphics device. 1053 * @param plot the plot. 1054 * @param plotArea the plot area. 1055 * @param edge the edge. 1056 * @param space the space already reserved. 1057 * 1058 * @return The reserved space. 1059 */ 1060 @Override 1061 public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 1062 Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) { 1063 1064 this.internalMarkerCycleBoundTick = null; 1065 AxisSpace ret = super.reserveSpace(g2, plot, plotArea, edge, space); 1066 if (this.internalMarkerCycleBoundTick == null) { 1067 return ret; 1068 } 1069 1070 FontMetrics fm = g2.getFontMetrics(getTickLabelFont()); 1071 Rectangle2D r = TextUtils.getTextBounds( 1072 this.internalMarkerCycleBoundTick.getText(), g2, fm 1073 ); 1074 1075 if (RectangleEdge.isTopOrBottom(edge)) { 1076 if (isVerticalTickLabels()) { 1077 space.add(r.getHeight() / 2, RectangleEdge.RIGHT); 1078 } 1079 else { 1080 space.add(r.getWidth() / 2, RectangleEdge.RIGHT); 1081 } 1082 } 1083 else if (RectangleEdge.isLeftOrRight(edge)) { 1084 if (isVerticalTickLabels()) { 1085 space.add(r.getWidth() / 2, RectangleEdge.TOP); 1086 } 1087 else { 1088 space.add(r.getHeight() / 2, RectangleEdge.TOP); 1089 } 1090 } 1091 1092 return ret; 1093 1094 } 1095 1096 /** 1097 * Provides serialization support. 1098 * 1099 * @param stream the output stream. 1100 * 1101 * @throws IOException if there is an I/O error. 1102 */ 1103 private void writeObject(ObjectOutputStream stream) throws IOException { 1104 stream.defaultWriteObject(); 1105 SerialUtils.writePaint(this.advanceLinePaint, stream); 1106 SerialUtils.writeStroke(this.advanceLineStroke, stream); 1107 } 1108 1109 /** 1110 * Provides serialization support. 1111 * 1112 * @param stream the input stream. 1113 * 1114 * @throws IOException if there is an I/O error. 1115 * @throws ClassNotFoundException if there is a classpath problem. 1116 */ 1117 private void readObject(ObjectInputStream stream) 1118 throws IOException, ClassNotFoundException { 1119 stream.defaultReadObject(); 1120 this.advanceLinePaint = SerialUtils.readPaint(stream); 1121 this.advanceLineStroke = SerialUtils.readStroke(stream); 1122 } 1123 1124 1125 /** 1126 * Tests the axis for equality with another object. 1127 * 1128 * @param obj the object to test against. 1129 * 1130 * @return A boolean. 1131 */ 1132 @Override 1133 public boolean equals(Object obj) { 1134 if (obj == this) { 1135 return true; 1136 } 1137 if (!(obj instanceof CyclicNumberAxis)) { 1138 return false; 1139 } 1140 if (!super.equals(obj)) { 1141 return false; 1142 } 1143 CyclicNumberAxis that = (CyclicNumberAxis) obj; 1144 if (this.period != that.period) { 1145 return false; 1146 } 1147 if (this.offset != that.offset) { 1148 return false; 1149 } 1150 if (!PaintUtils.equal(this.advanceLinePaint, 1151 that.advanceLinePaint)) { 1152 return false; 1153 } 1154 if (!Objects.equals(this.advanceLineStroke, that.advanceLineStroke)) { 1155 return false; 1156 } 1157 if (this.advanceLineVisible != that.advanceLineVisible) { 1158 return false; 1159 } 1160 if (this.boundMappedToLastCycle != that.boundMappedToLastCycle) { 1161 return false; 1162 } 1163 return true; 1164 } 1165}