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