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