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