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