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 * PeriodAxis.java 029 * --------------- 030 * (C) Copyright 2004-2022, by David Gilbert and Contributors. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): -; 034 * 035 */ 036 037package org.jfree.chart.axis; 038 039import java.awt.BasicStroke; 040import java.awt.Color; 041import java.awt.FontMetrics; 042import java.awt.Graphics2D; 043import java.awt.Paint; 044import java.awt.Stroke; 045import java.awt.geom.Line2D; 046import java.awt.geom.Rectangle2D; 047import java.io.IOException; 048import java.io.ObjectInputStream; 049import java.io.ObjectOutputStream; 050import java.io.Serializable; 051import java.lang.reflect.Constructor; 052import java.text.DateFormat; 053import java.text.SimpleDateFormat; 054import java.util.ArrayList; 055import java.util.Arrays; 056import java.util.Calendar; 057import java.util.Collections; 058import java.util.Date; 059import java.util.List; 060import java.util.Locale; 061import java.util.TimeZone; 062 063import org.jfree.chart.event.AxisChangeEvent; 064import org.jfree.chart.plot.Plot; 065import org.jfree.chart.plot.PlotRenderingInfo; 066import org.jfree.chart.plot.ValueAxisPlot; 067import org.jfree.chart.text.TextUtils; 068import org.jfree.chart.api.RectangleEdge; 069import org.jfree.chart.text.TextAnchor; 070import org.jfree.chart.internal.Args; 071import org.jfree.chart.api.PublicCloneable; 072import org.jfree.chart.internal.SerialUtils; 073import org.jfree.data.Range; 074import org.jfree.data.time.Day; 075import org.jfree.data.time.Month; 076import org.jfree.data.time.RegularTimePeriod; 077import org.jfree.data.time.Year; 078 079/** 080 * An axis that displays a date scale based on a 081 * {@link org.jfree.data.time.RegularTimePeriod}. This axis works when 082 * displayed across the bottom or top of a plot, but is broken for display at 083 * the left or right of charts. 084 */ 085public class PeriodAxis extends ValueAxis 086 implements Cloneable, PublicCloneable, Serializable { 087 088 /** For serialization. */ 089 private static final long serialVersionUID = 8353295532075872069L; 090 091 /** The first time period in the overall range. */ 092 private RegularTimePeriod first; 093 094 /** The last time period in the overall range. */ 095 private RegularTimePeriod last; 096 097 /** 098 * The time zone used to convert 'first' and 'last' to absolute 099 * milliseconds. 100 */ 101 private TimeZone timeZone; 102 103 /** 104 * The locale (never {@code null}). 105 */ 106 private Locale locale; 107 108 /** 109 * A calendar used for date manipulations in the current time zone and 110 * locale. 111 */ 112 private Calendar calendar; 113 114 /** 115 * The {@link RegularTimePeriod} subclass used to automatically determine 116 * the axis range. 117 */ 118 private Class autoRangeTimePeriodClass; 119 120 /** 121 * Indicates the {@link RegularTimePeriod} subclass that is used to 122 * determine the spacing of the major tick marks. 123 */ 124 private Class majorTickTimePeriodClass; 125 126 /** 127 * A flag that indicates whether or not tick marks are visible for the 128 * axis. 129 */ 130 private boolean minorTickMarksVisible; 131 132 /** 133 * Indicates the {@link RegularTimePeriod} subclass that is used to 134 * determine the spacing of the minor tick marks. 135 */ 136 private Class minorTickTimePeriodClass; 137 138 /** The length of the tick mark inside the data area (zero permitted). */ 139 private float minorTickMarkInsideLength = 0.0f; 140 141 /** The length of the tick mark outside the data area (zero permitted). */ 142 private float minorTickMarkOutsideLength = 2.0f; 143 144 /** The stroke used to draw tick marks. */ 145 private transient Stroke minorTickMarkStroke = new BasicStroke(0.5f); 146 147 /** The paint used to draw tick marks. */ 148 private transient Paint minorTickMarkPaint = Color.BLACK; 149 150 /** Info for each labeling band. */ 151 private PeriodAxisLabelInfo[] labelInfo; 152 153 /** 154 * Creates a new axis. 155 * 156 * @param label the axis label. 157 */ 158 public PeriodAxis(String label) { 159 this(label, new Day(), new Day()); 160 } 161 162 /** 163 * Creates a new axis. 164 * 165 * @param label the axis label ({@code null} permitted). 166 * @param first the first time period in the axis range 167 * ({@code null} not permitted). 168 * @param last the last time period in the axis range 169 * ({@code null} not permitted). 170 */ 171 public PeriodAxis(String label, 172 RegularTimePeriod first, RegularTimePeriod last) { 173 this(label, first, last, TimeZone.getDefault(), Locale.getDefault()); 174 } 175 176 /** 177 * Creates a new axis. 178 * 179 * @param label the axis label ({@code null} permitted). 180 * @param first the first time period in the axis range 181 * ({@code null} not permitted). 182 * @param last the last time period in the axis range 183 * ({@code null} not permitted). 184 * @param timeZone the time zone ({@code null} not permitted). 185 * @param locale the locale ({@code null} not permitted). 186 */ 187 public PeriodAxis(String label, RegularTimePeriod first, 188 RegularTimePeriod last, TimeZone timeZone, Locale locale) { 189 super(label, null); 190 Args.nullNotPermitted(timeZone, "timeZone"); 191 Args.nullNotPermitted(locale, "locale"); 192 this.first = first; 193 this.last = last; 194 this.timeZone = timeZone; 195 this.locale = locale; 196 this.calendar = Calendar.getInstance(timeZone, locale); 197 this.first.peg(this.calendar); 198 this.last.peg(this.calendar); 199 this.autoRangeTimePeriodClass = first.getClass(); 200 this.majorTickTimePeriodClass = first.getClass(); 201 this.minorTickMarksVisible = false; 202 this.minorTickTimePeriodClass = RegularTimePeriod.downsize( 203 this.majorTickTimePeriodClass); 204 setAutoRange(true); 205 this.labelInfo = new PeriodAxisLabelInfo[2]; 206 SimpleDateFormat df0 = new SimpleDateFormat("MMM", locale); 207 df0.setTimeZone(timeZone); 208 this.labelInfo[0] = new PeriodAxisLabelInfo(Month.class, df0); 209 SimpleDateFormat df1 = new SimpleDateFormat("yyyy", locale); 210 df1.setTimeZone(timeZone); 211 this.labelInfo[1] = new PeriodAxisLabelInfo(Year.class, df1); 212 } 213 214 /** 215 * Returns the first time period in the axis range. 216 * 217 * @return The first time period (never {@code null}). 218 */ 219 public RegularTimePeriod getFirst() { 220 return this.first; 221 } 222 223 /** 224 * Sets the first time period in the axis range and sends an 225 * {@link AxisChangeEvent} to all registered listeners. 226 * 227 * @param first the time period ({@code null} not permitted). 228 */ 229 public void setFirst(RegularTimePeriod first) { 230 Args.nullNotPermitted(first, "first"); 231 this.first = first; 232 this.first.peg(this.calendar); 233 fireChangeEvent(); 234 } 235 236 /** 237 * Returns the last time period in the axis range. 238 * 239 * @return The last time period (never {@code null}). 240 */ 241 public RegularTimePeriod getLast() { 242 return this.last; 243 } 244 245 /** 246 * Sets the last time period in the axis range and sends an 247 * {@link AxisChangeEvent} to all registered listeners. 248 * 249 * @param last the time period ({@code null} not permitted). 250 */ 251 public void setLast(RegularTimePeriod last) { 252 Args.nullNotPermitted(last, "last"); 253 this.last = last; 254 this.last.peg(this.calendar); 255 fireChangeEvent(); 256 } 257 258 /** 259 * Returns the time zone used to convert the periods defining the axis 260 * range into absolute milliseconds. 261 * 262 * @return The time zone (never {@code null}). 263 */ 264 public TimeZone getTimeZone() { 265 return this.timeZone; 266 } 267 268 /** 269 * Sets the time zone that is used to convert the time periods into 270 * absolute milliseconds. 271 * 272 * @param zone the time zone ({@code null} not permitted). 273 */ 274 public void setTimeZone(TimeZone zone) { 275 Args.nullNotPermitted(zone, "zone"); 276 this.timeZone = zone; 277 this.calendar = Calendar.getInstance(zone, this.locale); 278 this.first.peg(this.calendar); 279 this.last.peg(this.calendar); 280 fireChangeEvent(); 281 } 282 283 /** 284 * Returns the locale for this axis. 285 * 286 * @return The locale (never ({@code null}). 287 */ 288 public Locale getLocale() { 289 return this.locale; 290 } 291 292 /** 293 * Returns the class used to create the first and last time periods for 294 * the axis range when the auto-range flag is set to {@code true}. 295 * 296 * @return The class (never {@code null}). 297 */ 298 public Class getAutoRangeTimePeriodClass() { 299 return this.autoRangeTimePeriodClass; 300 } 301 302 /** 303 * Sets the class used to create the first and last time periods for the 304 * axis range when the auto-range flag is set to {@code true} and 305 * sends an {@link AxisChangeEvent} to all registered listeners. 306 * 307 * @param c the class ({@code null} not permitted). 308 */ 309 public void setAutoRangeTimePeriodClass(Class c) { 310 Args.nullNotPermitted(c, "c"); 311 this.autoRangeTimePeriodClass = c; 312 fireChangeEvent(); 313 } 314 315 /** 316 * Returns the class that controls the spacing of the major tick marks. 317 * 318 * @return The class (never {@code null}). 319 */ 320 public Class getMajorTickTimePeriodClass() { 321 return this.majorTickTimePeriodClass; 322 } 323 324 /** 325 * Sets the class that controls the spacing of the major tick marks, and 326 * sends an {@link AxisChangeEvent} to all registered listeners. 327 * 328 * @param c the class (a subclass of {@link RegularTimePeriod} is 329 * expected). 330 */ 331 public void setMajorTickTimePeriodClass(Class c) { 332 Args.nullNotPermitted(c, "c"); 333 this.majorTickTimePeriodClass = c; 334 fireChangeEvent(); 335 } 336 337 /** 338 * Returns the flag that controls whether or not minor tick marks 339 * are displayed for the axis. 340 * 341 * @return A boolean. 342 */ 343 @Override 344 public boolean isMinorTickMarksVisible() { 345 return this.minorTickMarksVisible; 346 } 347 348 /** 349 * Sets the flag that controls whether or not minor tick marks 350 * are displayed for the axis, and sends a {@link AxisChangeEvent} 351 * to all registered listeners. 352 * 353 * @param visible the flag. 354 */ 355 @Override 356 public void setMinorTickMarksVisible(boolean visible) { 357 this.minorTickMarksVisible = visible; 358 fireChangeEvent(); 359 } 360 361 /** 362 * Returns the class that controls the spacing of the minor tick marks. 363 * 364 * @return The class (never {@code null}). 365 */ 366 public Class getMinorTickTimePeriodClass() { 367 return this.minorTickTimePeriodClass; 368 } 369 370 /** 371 * Sets the class that controls the spacing of the minor tick marks, and 372 * sends an {@link AxisChangeEvent} to all registered listeners. 373 * 374 * @param c the class (a subclass of {@link RegularTimePeriod} is 375 * expected). 376 */ 377 public void setMinorTickTimePeriodClass(Class c) { 378 Args.nullNotPermitted(c, "c"); 379 this.minorTickTimePeriodClass = c; 380 fireChangeEvent(); 381 } 382 383 /** 384 * Returns the stroke used to display minor tick marks, if they are 385 * visible. 386 * 387 * @return A stroke (never {@code null}). 388 */ 389 public Stroke getMinorTickMarkStroke() { 390 return this.minorTickMarkStroke; 391 } 392 393 /** 394 * Sets the stroke used to display minor tick marks, if they are 395 * visible, and sends a {@link AxisChangeEvent} to all registered 396 * listeners. 397 * 398 * @param stroke the stroke ({@code null} not permitted). 399 */ 400 public void setMinorTickMarkStroke(Stroke stroke) { 401 Args.nullNotPermitted(stroke, "stroke"); 402 this.minorTickMarkStroke = stroke; 403 fireChangeEvent(); 404 } 405 406 /** 407 * Returns the paint used to display minor tick marks, if they are 408 * visible. 409 * 410 * @return A paint (never {@code null}). 411 */ 412 public Paint getMinorTickMarkPaint() { 413 return this.minorTickMarkPaint; 414 } 415 416 /** 417 * Sets the paint used to display minor tick marks, if they are 418 * visible, and sends a {@link AxisChangeEvent} to all registered 419 * listeners. 420 * 421 * @param paint the paint ({@code null} not permitted). 422 */ 423 public void setMinorTickMarkPaint(Paint paint) { 424 Args.nullNotPermitted(paint, "paint"); 425 this.minorTickMarkPaint = paint; 426 fireChangeEvent(); 427 } 428 429 /** 430 * Returns the inside length for the minor tick marks. 431 * 432 * @return The length. 433 */ 434 @Override 435 public float getMinorTickMarkInsideLength() { 436 return this.minorTickMarkInsideLength; 437 } 438 439 /** 440 * Sets the inside length of the minor tick marks and sends an 441 * {@link AxisChangeEvent} to all registered listeners. 442 * 443 * @param length the length. 444 */ 445 @Override 446 public void setMinorTickMarkInsideLength(float length) { 447 this.minorTickMarkInsideLength = length; 448 fireChangeEvent(); 449 } 450 451 /** 452 * Returns the outside length for the minor tick marks. 453 * 454 * @return The length. 455 */ 456 @Override 457 public float getMinorTickMarkOutsideLength() { 458 return this.minorTickMarkOutsideLength; 459 } 460 461 /** 462 * Sets the outside length of the minor tick marks and sends an 463 * {@link AxisChangeEvent} to all registered listeners. 464 * 465 * @param length the length. 466 */ 467 @Override 468 public void setMinorTickMarkOutsideLength(float length) { 469 this.minorTickMarkOutsideLength = length; 470 fireChangeEvent(); 471 } 472 473 /** 474 * Returns an array of label info records. 475 * 476 * @return An array. 477 */ 478 public PeriodAxisLabelInfo[] getLabelInfo() { 479 return this.labelInfo; 480 } 481 482 /** 483 * Sets the array of label info records and sends an 484 * {@link AxisChangeEvent} to all registered listeners. 485 * 486 * @param info the info. 487 */ 488 public void setLabelInfo(PeriodAxisLabelInfo[] info) { 489 this.labelInfo = info; 490 fireChangeEvent(); 491 } 492 493 /** 494 * Sets the range for the axis, if requested, sends an 495 * {@link AxisChangeEvent} to all registered listeners. As a side-effect, 496 * the auto-range flag is set to {@code false} (optional). 497 * 498 * @param range the range ({@code null} not permitted). 499 * @param turnOffAutoRange a flag that controls whether or not the auto 500 * range is turned off. 501 * @param notify a flag that controls whether or not listeners are 502 * notified. 503 */ 504 @Override 505 public void setRange(Range range, boolean turnOffAutoRange, 506 boolean notify) { 507 long upper = Math.round(range.getUpperBound()); 508 long lower = Math.round(range.getLowerBound()); 509 this.first = createInstance(this.autoRangeTimePeriodClass, 510 new Date(lower), this.timeZone, this.locale); 511 this.last = createInstance(this.autoRangeTimePeriodClass, 512 new Date(upper), this.timeZone, this.locale); 513 super.setRange(new Range(this.first.getFirstMillisecond(), 514 this.last.getLastMillisecond() + 1.0), turnOffAutoRange, 515 notify); 516 } 517 518 /** 519 * Configures the axis to work with the current plot. Override this method 520 * to perform any special processing (such as auto-rescaling). 521 */ 522 @Override 523 public void configure() { 524 if (this.isAutoRange()) { 525 autoAdjustRange(); 526 } 527 } 528 529 /** 530 * Estimates the space (height or width) required to draw the axis. 531 * 532 * @param g2 the graphics device. 533 * @param plot the plot that the axis belongs to. 534 * @param plotArea the area within which the plot (including axes) should 535 * be drawn. 536 * @param edge the axis location. 537 * @param space space already reserved. 538 * 539 * @return The space required to draw the axis (including pre-reserved 540 * space). 541 */ 542 @Override 543 public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 544 Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) { 545 // create a new space object if one wasn't supplied... 546 if (space == null) { 547 space = new AxisSpace(); 548 } 549 550 // if the axis is not visible, no additional space is required... 551 if (!isVisible()) { 552 return space; 553 } 554 555 // if the axis has a fixed dimension, return it... 556 double dimension = getFixedDimension(); 557 if (dimension > 0.0) { 558 space.ensureAtLeast(dimension, edge); 559 } 560 561 // get the axis label size and update the space object... 562 Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge); 563 double labelHeight, labelWidth; 564 double tickLabelBandsDimension = 0.0; 565 566 for (PeriodAxisLabelInfo info : this.labelInfo) { 567 FontMetrics fm = g2.getFontMetrics(info.getLabelFont()); 568 tickLabelBandsDimension 569 += info.getPadding().extendHeight(fm.getHeight()); 570 } 571 572 if (RectangleEdge.isTopOrBottom(edge)) { 573 labelHeight = labelEnclosure.getHeight(); 574 space.add(labelHeight + tickLabelBandsDimension, edge); 575 } 576 else if (RectangleEdge.isLeftOrRight(edge)) { 577 labelWidth = labelEnclosure.getWidth(); 578 space.add(labelWidth + tickLabelBandsDimension, edge); 579 } 580 581 // add space for the outer tick labels, if any... 582 double tickMarkSpace = 0.0; 583 if (isTickMarksVisible()) { 584 tickMarkSpace = getTickMarkOutsideLength(); 585 } 586 if (this.minorTickMarksVisible) { 587 tickMarkSpace = Math.max(tickMarkSpace, 588 this.minorTickMarkOutsideLength); 589 } 590 space.add(tickMarkSpace, edge); 591 return space; 592 } 593 594 /** 595 * Draws the axis on a Java 2D graphics device (such as the screen or a 596 * printer). 597 * 598 * @param g2 the graphics device ({@code null} not permitted). 599 * @param cursor the cursor location (determines where to draw the axis). 600 * @param plotArea the area within which the axes and plot should be drawn. 601 * @param dataArea the area within which the data should be drawn. 602 * @param edge the axis location ({@code null} not permitted). 603 * @param plotState collects information about the plot 604 * ({@code null} permitted). 605 * 606 * @return The axis state (never {@code null}). 607 */ 608 @Override 609 public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, 610 Rectangle2D dataArea, RectangleEdge edge, 611 PlotRenderingInfo plotState) { 612 613 // if the axis is not visible, don't draw it... bug#198 614 if (!isVisible()) { 615 AxisState state = new AxisState(cursor); 616 // even though the axis is not visible, we need to refresh ticks in 617 // case the grid is being drawn... 618 List ticks = refreshTicks(g2, state, dataArea, edge); 619 state.setTicks(ticks); 620 return state; 621 } 622 623 AxisState axisState = new AxisState(cursor); 624 if (isAxisLineVisible()) { 625 drawAxisLine(g2, cursor, dataArea, edge); 626 } 627 if (isTickMarksVisible()) { 628 drawTickMarks(g2, axisState, dataArea, edge); 629 } 630 if (isTickLabelsVisible()) { 631 for (int band = 0; band < this.labelInfo.length; band++) { 632 axisState = drawTickLabels(band, g2, axisState, dataArea, edge); 633 } 634 } 635 636 if (getAttributedLabel() != null) { 637 axisState = drawAttributedLabel(getAttributedLabel(), g2, plotArea, 638 dataArea, edge, axisState); 639 } else { 640 axisState = drawLabel(getLabel(), g2, plotArea, dataArea, edge, 641 axisState); 642 } 643 return axisState; 644 645 } 646 647 /** 648 * Draws the tick marks for the axis. 649 * 650 * @param g2 the graphics device. 651 * @param state the axis state. 652 * @param dataArea the data area. 653 * @param edge the edge. 654 */ 655 protected void drawTickMarks(Graphics2D g2, AxisState state, 656 Rectangle2D dataArea, RectangleEdge edge) { 657 if (RectangleEdge.isTopOrBottom(edge)) { 658 drawTickMarksHorizontal(g2, state, dataArea, edge); 659 } 660 else if (RectangleEdge.isLeftOrRight(edge)) { 661 drawTickMarksVertical(g2, state, dataArea, edge); 662 } 663 } 664 665 /** 666 * Draws the major and minor tick marks for an axis that lies at the top or 667 * bottom of the plot. 668 * 669 * @param g2 the graphics device. 670 * @param state the axis state. 671 * @param dataArea the data area. 672 * @param edge the edge. 673 */ 674 protected void drawTickMarksHorizontal(Graphics2D g2, AxisState state, 675 Rectangle2D dataArea, RectangleEdge edge) { 676 List ticks = new ArrayList(); 677 double x0; 678 double y0 = state.getCursor(); 679 double insideLength = getTickMarkInsideLength(); 680 double outsideLength = getTickMarkOutsideLength(); 681 RegularTimePeriod t = createInstance(this.majorTickTimePeriodClass, 682 this.first.getStart(), getTimeZone(), this.locale); 683 long t0 = t.getFirstMillisecond(); 684 Line2D inside = null; 685 Line2D outside = null; 686 long firstOnAxis = getFirst().getFirstMillisecond(); 687 long lastOnAxis = getLast().getLastMillisecond() + 1; 688 while (t0 <= lastOnAxis) { 689 ticks.add(new NumberTick((double) t0, "", TextAnchor.CENTER, 690 TextAnchor.CENTER, 0.0)); 691 x0 = valueToJava2D(t0, dataArea, edge); 692 if (edge == RectangleEdge.TOP) { 693 inside = new Line2D.Double(x0, y0, x0, y0 + insideLength); 694 outside = new Line2D.Double(x0, y0, x0, y0 - outsideLength); 695 } 696 else if (edge == RectangleEdge.BOTTOM) { 697 inside = new Line2D.Double(x0, y0, x0, y0 - insideLength); 698 outside = new Line2D.Double(x0, y0, x0, y0 + outsideLength); 699 } 700 if (t0 >= firstOnAxis) { 701 g2.setPaint(getTickMarkPaint()); 702 g2.setStroke(getTickMarkStroke()); 703 g2.draw(inside); 704 g2.draw(outside); 705 } 706 // draw minor tick marks 707 if (this.minorTickMarksVisible) { 708 RegularTimePeriod tminor = createInstance( 709 this.minorTickTimePeriodClass, new Date(t0), 710 getTimeZone(), this.locale); 711 long tt0 = tminor.getFirstMillisecond(); 712 while (tt0 < t.getLastMillisecond() 713 && tt0 < lastOnAxis) { 714 double xx0 = valueToJava2D(tt0, dataArea, edge); 715 if (edge == RectangleEdge.TOP) { 716 inside = new Line2D.Double(xx0, y0, xx0, 717 y0 + this.minorTickMarkInsideLength); 718 outside = new Line2D.Double(xx0, y0, xx0, 719 y0 - this.minorTickMarkOutsideLength); 720 } 721 else if (edge == RectangleEdge.BOTTOM) { 722 inside = new Line2D.Double(xx0, y0, xx0, 723 y0 - this.minorTickMarkInsideLength); 724 outside = new Line2D.Double(xx0, y0, xx0, 725 y0 + this.minorTickMarkOutsideLength); 726 } 727 if (tt0 >= firstOnAxis) { 728 g2.setPaint(this.minorTickMarkPaint); 729 g2.setStroke(this.minorTickMarkStroke); 730 g2.draw(inside); 731 g2.draw(outside); 732 } 733 tminor = tminor.next(); 734 tminor.peg(this.calendar); 735 tt0 = tminor.getFirstMillisecond(); 736 } 737 } 738 t = t.next(); 739 t.peg(this.calendar); 740 t0 = t.getFirstMillisecond(); 741 } 742 if (edge == RectangleEdge.TOP) { 743 state.cursorUp(Math.max(outsideLength, 744 this.minorTickMarkOutsideLength)); 745 } 746 else if (edge == RectangleEdge.BOTTOM) { 747 state.cursorDown(Math.max(outsideLength, 748 this.minorTickMarkOutsideLength)); 749 } 750 state.setTicks(ticks); 751 } 752 753 /** 754 * Draws the tick marks for a vertical axis. 755 * 756 * @param g2 the graphics device. 757 * @param state the axis state. 758 * @param dataArea the data area. 759 * @param edge the edge. 760 */ 761 protected void drawTickMarksVertical(Graphics2D g2, AxisState state, 762 Rectangle2D dataArea, RectangleEdge edge) { 763 // FIXME: implement this... 764 } 765 766 /** 767 * Draws the tick labels for one "band" of time periods. 768 * 769 * @param band the band index (zero-based). 770 * @param g2 the graphics device. 771 * @param state the axis state. 772 * @param dataArea the data area. 773 * @param edge the edge where the axis is located. 774 * 775 * @return The updated axis state. 776 */ 777 protected AxisState drawTickLabels(int band, Graphics2D g2, AxisState state, 778 Rectangle2D dataArea, RectangleEdge edge) { 779 780 // work out the initial gap 781 double delta1 = 0.0; 782 FontMetrics fm = g2.getFontMetrics(this.labelInfo[band].getLabelFont()); 783 if (edge == RectangleEdge.BOTTOM) { 784 delta1 = this.labelInfo[band].getPadding().calculateTopOutset( 785 fm.getHeight()); 786 } 787 else if (edge == RectangleEdge.TOP) { 788 delta1 = this.labelInfo[band].getPadding().calculateBottomOutset( 789 fm.getHeight()); 790 } 791 state.moveCursor(delta1, edge); 792 long axisMin = this.first.getFirstMillisecond(); 793 long axisMax = this.last.getLastMillisecond(); 794 g2.setFont(this.labelInfo[band].getLabelFont()); 795 g2.setPaint(this.labelInfo[band].getLabelPaint()); 796 797 // work out the number of periods to skip for labelling 798 RegularTimePeriod p1 = this.labelInfo[band].createInstance( 799 new Date(axisMin), this.timeZone, this.locale); 800 RegularTimePeriod p2 = this.labelInfo[band].createInstance( 801 new Date(axisMax), this.timeZone, this.locale); 802 DateFormat df = this.labelInfo[band].getDateFormat(); 803 df.setTimeZone(this.timeZone); 804 String label1 = df.format(new Date(p1.getMiddleMillisecond())); 805 String label2 = df.format(new Date(p2.getMiddleMillisecond())); 806 Rectangle2D b1 = TextUtils.getTextBounds(label1, g2, 807 g2.getFontMetrics()); 808 Rectangle2D b2 = TextUtils.getTextBounds(label2, g2, 809 g2.getFontMetrics()); 810 double w = Math.max(b1.getWidth(), b2.getWidth()); 811 long ww = Math.round(java2DToValue(dataArea.getX() + w + 5.0, 812 dataArea, edge)); 813 if (isInverted()) { 814 ww = axisMax - ww; 815 } 816 else { 817 ww = ww - axisMin; 818 } 819 long length = p1.getLastMillisecond() 820 - p1.getFirstMillisecond(); 821 int periods = (int) (ww / length) + 1; 822 823 RegularTimePeriod p = this.labelInfo[band].createInstance( 824 new Date(axisMin), this.timeZone, this.locale); 825 Rectangle2D b = null; 826 long lastXX = 0L; 827 float y = (float) (state.getCursor()); 828 TextAnchor anchor = TextAnchor.TOP_CENTER; 829 float yDelta = (float) b1.getHeight(); 830 if (edge == RectangleEdge.TOP) { 831 anchor = TextAnchor.BOTTOM_CENTER; 832 yDelta = -yDelta; 833 } 834 while (p.getFirstMillisecond() <= axisMax) { 835 float x = (float) valueToJava2D(p.getMiddleMillisecond(), dataArea, 836 edge); 837 String label = df.format(new Date(p.getMiddleMillisecond())); 838 long first = p.getFirstMillisecond(); 839 long last = p.getLastMillisecond(); 840 if (last > axisMax) { 841 // this is the last period, but it is only partially visible 842 // so check that the label will fit before displaying it... 843 Rectangle2D bb = TextUtils.getTextBounds(label, g2, 844 g2.getFontMetrics()); 845 if ((x + bb.getWidth() / 2) > dataArea.getMaxX()) { 846 float xstart = (float) valueToJava2D(Math.max(first, 847 axisMin), dataArea, edge); 848 if (bb.getWidth() < (dataArea.getMaxX() - xstart)) { 849 x = ((float) dataArea.getMaxX() + xstart) / 2.0f; 850 } 851 else { 852 label = null; 853 } 854 } 855 } 856 if (first < axisMin) { 857 // this is the first period, but it is only partially visible 858 // so check that the label will fit before displaying it... 859 Rectangle2D bb = TextUtils.getTextBounds(label, g2, 860 g2.getFontMetrics()); 861 if ((x - bb.getWidth() / 2) < dataArea.getX()) { 862 float xlast = (float) valueToJava2D(Math.min(last, 863 axisMax), dataArea, edge); 864 if (bb.getWidth() < (xlast - dataArea.getX())) { 865 x = (xlast + (float) dataArea.getX()) / 2.0f; 866 } 867 else { 868 label = null; 869 } 870 } 871 872 } 873 if (label != null) { 874 g2.setPaint(this.labelInfo[band].getLabelPaint()); 875 b = TextUtils.drawAlignedString(label, g2, x, y, anchor); 876 } 877 if (lastXX > 0L) { 878 if (this.labelInfo[band].getDrawDividers()) { 879 long nextXX = p.getFirstMillisecond(); 880 long mid = (lastXX + nextXX) / 2; 881 float mid2d = (float) valueToJava2D(mid, dataArea, edge); 882 g2.setStroke(this.labelInfo[band].getDividerStroke()); 883 g2.setPaint(this.labelInfo[band].getDividerPaint()); 884 g2.draw(new Line2D.Float(mid2d, y, mid2d, y + yDelta)); 885 } 886 } 887 lastXX = last; 888 for (int i = 0; i < periods; i++) { 889 p = p.next(); 890 } 891 p.peg(this.calendar); 892 } 893 double used = 0.0; 894 if (b != null) { 895 used = b.getHeight(); 896 // work out the trailing gap 897 if (edge == RectangleEdge.BOTTOM) { 898 used += this.labelInfo[band].getPadding().calculateBottomOutset( 899 fm.getHeight()); 900 } 901 else if (edge == RectangleEdge.TOP) { 902 used += this.labelInfo[band].getPadding().calculateTopOutset( 903 fm.getHeight()); 904 } 905 } 906 state.moveCursor(used, edge); 907 return state; 908 } 909 910 /** 911 * Calculates the positions of the ticks for the axis, storing the results 912 * in the tick list (ready for drawing). 913 * 914 * @param g2 the graphics device. 915 * @param state the axis state. 916 * @param dataArea the area inside the axes. 917 * @param edge the edge on which the axis is located. 918 * 919 * @return The list of ticks. 920 */ 921 @Override 922 public List refreshTicks(Graphics2D g2, AxisState state, 923 Rectangle2D dataArea, RectangleEdge edge) { 924 return Collections.EMPTY_LIST; 925 } 926 927 /** 928 * Converts a data value to a coordinate in Java2D space, assuming that the 929 * axis runs along one edge of the specified dataArea. 930 * <p> 931 * Note that it is possible for the coordinate to fall outside the area. 932 * 933 * @param value the data value. 934 * @param area the area for plotting the data. 935 * @param edge the edge along which the axis lies. 936 * 937 * @return The Java2D coordinate. 938 */ 939 @Override 940 public double valueToJava2D(double value, Rectangle2D area, 941 RectangleEdge edge) { 942 943 double result = Double.NaN; 944 double axisMin = this.first.getFirstMillisecond(); 945 double axisMax = this.last.getLastMillisecond(); 946 if (RectangleEdge.isTopOrBottom(edge)) { 947 double minX = area.getX(); 948 double maxX = area.getMaxX(); 949 if (isInverted()) { 950 result = maxX + ((value - axisMin) / (axisMax - axisMin)) 951 * (minX - maxX); 952 } 953 else { 954 result = minX + ((value - axisMin) / (axisMax - axisMin)) 955 * (maxX - minX); 956 } 957 } 958 else if (RectangleEdge.isLeftOrRight(edge)) { 959 double minY = area.getMinY(); 960 double maxY = area.getMaxY(); 961 if (isInverted()) { 962 result = minY + (((value - axisMin) / (axisMax - axisMin)) 963 * (maxY - minY)); 964 } 965 else { 966 result = maxY - (((value - axisMin) / (axisMax - axisMin)) 967 * (maxY - minY)); 968 } 969 } 970 return result; 971 972 } 973 974 /** 975 * Converts a coordinate in Java2D space to the corresponding data value, 976 * assuming that the axis runs along one edge of the specified dataArea. 977 * 978 * @param java2DValue the coordinate in Java2D space. 979 * @param area the area in which the data is plotted. 980 * @param edge the edge along which the axis lies. 981 * 982 * @return The data value. 983 */ 984 @Override 985 public double java2DToValue(double java2DValue, Rectangle2D area, 986 RectangleEdge edge) { 987 988 double result; 989 double min = 0.0; 990 double max = 0.0; 991 double axisMin = this.first.getFirstMillisecond(); 992 double axisMax = this.last.getLastMillisecond(); 993 if (RectangleEdge.isTopOrBottom(edge)) { 994 min = area.getX(); 995 max = area.getMaxX(); 996 } 997 else if (RectangleEdge.isLeftOrRight(edge)) { 998 min = area.getMaxY(); 999 max = area.getY(); 1000 } 1001 if (isInverted()) { 1002 result = axisMax - ((java2DValue - min) / (max - min) 1003 * (axisMax - axisMin)); 1004 } 1005 else { 1006 result = axisMin + ((java2DValue - min) / (max - min) 1007 * (axisMax - axisMin)); 1008 } 1009 return result; 1010 } 1011 1012 /** 1013 * Rescales the axis to ensure that all data is visible. 1014 */ 1015 @Override 1016 protected void autoAdjustRange() { 1017 1018 Plot plot = getPlot(); 1019 if (plot == null) { 1020 return; // no plot, no data 1021 } 1022 1023 if (plot instanceof ValueAxisPlot) { 1024 ValueAxisPlot vap = (ValueAxisPlot) plot; 1025 1026 Range r = vap.getDataRange(this); 1027 if (r == null) { 1028 r = getDefaultAutoRange(); 1029 } 1030 1031 long upper = Math.round(r.getUpperBound()); 1032 long lower = Math.round(r.getLowerBound()); 1033 this.first = createInstance(this.autoRangeTimePeriodClass, 1034 new Date(lower), this.timeZone, this.locale); 1035 this.last = createInstance(this.autoRangeTimePeriodClass, 1036 new Date(upper), this.timeZone, this.locale); 1037 setRange(r, false, false); 1038 } 1039 1040 } 1041 1042 /** 1043 * Tests the axis for equality with an arbitrary object. 1044 * 1045 * @param obj the object ({@code null} permitted). 1046 * 1047 * @return A boolean. 1048 */ 1049 @Override 1050 public boolean equals(Object obj) { 1051 if (obj == this) { 1052 return true; 1053 } 1054 if (!(obj instanceof PeriodAxis)) { 1055 return false; 1056 } 1057 PeriodAxis that = (PeriodAxis) obj; 1058 if (!this.first.equals(that.first)) { 1059 return false; 1060 } 1061 if (!this.last.equals(that.last)) { 1062 return false; 1063 } 1064 if (!this.timeZone.equals(that.timeZone)) { 1065 return false; 1066 } 1067 if (!this.locale.equals(that.locale)) { 1068 return false; 1069 } 1070 if (!this.autoRangeTimePeriodClass.equals( 1071 that.autoRangeTimePeriodClass)) { 1072 return false; 1073 } 1074 if (!(isMinorTickMarksVisible() == that.isMinorTickMarksVisible())) { 1075 return false; 1076 } 1077 if (!this.majorTickTimePeriodClass.equals( 1078 that.majorTickTimePeriodClass)) { 1079 return false; 1080 } 1081 if (!this.minorTickTimePeriodClass.equals( 1082 that.minorTickTimePeriodClass)) { 1083 return false; 1084 } 1085 if (!this.minorTickMarkPaint.equals(that.minorTickMarkPaint)) { 1086 return false; 1087 } 1088 if (!this.minorTickMarkStroke.equals(that.minorTickMarkStroke)) { 1089 return false; 1090 } 1091 if (!Arrays.equals(this.labelInfo, that.labelInfo)) { 1092 return false; 1093 } 1094 return super.equals(obj); 1095 } 1096 1097 /** 1098 * Returns a hash code for this object. 1099 * 1100 * @return A hash code. 1101 */ 1102 @Override 1103 public int hashCode() { 1104 return super.hashCode(); 1105 } 1106 1107 /** 1108 * Returns a clone of the axis. 1109 * 1110 * @return A clone. 1111 * 1112 * @throws CloneNotSupportedException this class is cloneable, but 1113 * subclasses may not be. 1114 */ 1115 @Override 1116 public Object clone() throws CloneNotSupportedException { 1117 PeriodAxis clone = (PeriodAxis) super.clone(); 1118 clone.timeZone = (TimeZone) this.timeZone.clone(); 1119 clone.labelInfo = (PeriodAxisLabelInfo[]) this.labelInfo.clone(); 1120 return clone; 1121 } 1122 1123 /** 1124 * A utility method used to create a particular subclass of the 1125 * {@link RegularTimePeriod} class that includes the specified millisecond, 1126 * assuming the specified time zone. 1127 * 1128 * @param periodClass the class. 1129 * @param millisecond the time. 1130 * @param zone the time zone. 1131 * @param locale the locale. 1132 * 1133 * @return The time period. 1134 */ 1135 private RegularTimePeriod createInstance(Class periodClass, 1136 Date millisecond, TimeZone zone, Locale locale) { 1137 RegularTimePeriod result = null; 1138 try { 1139 Constructor c = periodClass.getDeclaredConstructor(new Class[] { 1140 Date.class, TimeZone.class, Locale.class}); 1141 result = (RegularTimePeriod) c.newInstance(new Object[] { 1142 millisecond, zone, locale}); 1143 } 1144 catch (Exception e) { 1145 try { 1146 Constructor c = periodClass.getDeclaredConstructor(new Class[] { 1147 Date.class}); 1148 result = (RegularTimePeriod) c.newInstance(new Object[] { 1149 millisecond}); 1150 } 1151 catch (Exception e2) { 1152 // do nothing 1153 } 1154 } 1155 return result; 1156 } 1157 1158 /** 1159 * Provides serialization support. 1160 * 1161 * @param stream the output stream. 1162 * 1163 * @throws IOException if there is an I/O error. 1164 */ 1165 private void writeObject(ObjectOutputStream stream) throws IOException { 1166 stream.defaultWriteObject(); 1167 SerialUtils.writeStroke(this.minorTickMarkStroke, stream); 1168 SerialUtils.writePaint(this.minorTickMarkPaint, stream); 1169 } 1170 1171 /** 1172 * Provides serialization support. 1173 * 1174 * @param stream the input stream. 1175 * 1176 * @throws IOException if there is an I/O error. 1177 * @throws ClassNotFoundException if there is a classpath problem. 1178 */ 1179 private void readObject(ObjectInputStream stream) 1180 throws IOException, ClassNotFoundException { 1181 stream.defaultReadObject(); 1182 this.minorTickMarkStroke = SerialUtils.readStroke(stream); 1183 this.minorTickMarkPaint = SerialUtils.readPaint(stream); 1184 } 1185 1186}