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 * DateAxis.java
029 * -------------
030 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
031 *
032 * Original Author: David Gilbert;
033 * Contributor(s): Jonathan Nash;
034 * David Li;
035 * Michael Rauch;
036 * Bill Kelemen;
037 * Pawel Pabis;
038 *
039 * $Id: DateAxis.java,v 1.17.2.8 2007/01/18 15:20:34 mungady Exp $
040 *
041 * Changes (from 23-Jun-2001)
042 * --------------------------
043 * 23-Jun-2001 : Modified to work with null data source (DG);
044 * 18-Sep-2001 : Updated header (DG);
045 * 27-Nov-2001 : Changed constructors from public to protected, updated Javadoc
046 * comments (DG);
047 * 16-Jan-2002 : Added an optional crosshair, based on the implementation by
048 * Jonathan Nash (DG);
049 * 26-Feb-2002 : Updated import statements (DG);
050 * 22-Apr-2002 : Added a setRange() method (DG);
051 * 25-Jun-2002 : Removed redundant local variable (DG);
052 * 25-Jul-2002 : Changed order of parameters in ValueAxis constructor (DG);
053 * 21-Aug-2002 : The setTickUnit() method now turns off auto-tick unit
054 * selection (fix for bug id 528885) (DG);
055 * 05-Sep-2002 : Updated the constructors to reflect changes in the Axis
056 * class (DG);
057 * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG);
058 * 25-Sep-2002 : Added new setRange() methods, and deprecated
059 * setAxisRange() (DG);
060 * 04-Oct-2002 : Changed auto tick selection to parallel number axis
061 * classes (DG);
062 * 24-Oct-2002 : Added a date format override (DG);
063 * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
064 * 14-Jan-2003 : Changed autoRangeMinimumSize from Number --> double, moved
065 * crosshair settings to the plot (DG);
066 * 15-Jan-2003 : Removed anchor date (DG);
067 * 20-Jan-2003 : Removed unnecessary constructors (DG);
068 * 26-Mar-2003 : Implemented Serializable (DG);
069 * 02-May-2003 : Added additional units to createStandardDateTickUnits()
070 * method, as suggested by mhilpert in bug report 723187 (DG);
071 * 13-May-2003 : Merged HorizontalDateAxis and VerticalDateAxis (DG);
072 * 24-May-2003 : Added support for underlying timeline for
073 * SegmentedTimeline (BK);
074 * 16-Jul-2003 : Applied patch from Pawel Pabis to fix overlapping dates (DG);
075 * 22-Jul-2003 : Applied patch from Pawel Pabis for monthly ticks (DG);
076 * 25-Jul-2003 : Fixed bug 777561 and 777586 (DG);
077 * 13-Aug-2003 : Implemented Cloneable and added equals() method (DG);
078 * 02-Sep-2003 : Fixes for bug report 790506 (DG);
079 * 04-Sep-2003 : Fixed tick label alignment when axis appears at the top (DG);
080 * 10-Sep-2003 : Fixes for segmented timeline (DG);
081 * 17-Sep-2003 : Fixed a layout bug when multiple domain axes are used (DG);
082 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
083 * 07-Nov-2003 : Modified to use new tick classes (DG);
084 * 12-Nov-2003 : Modified tick labelling to use roll unit from DateTickUnit
085 * when a calculated tick value is hidden (which can occur in
086 * segmented date axes) (DG);
087 * 24-Nov-2003 : Fixed some problems with the auto tick unit selection, and
088 * fixed bug 846277 (labels missing for inverted axis) (DG);
089 * 30-Dec-2003 : Fixed bug in refreshTicksHorizontal() when start of time unit
090 * (ex. 1st of month) was hidden, causing infinite loop (BK);
091 * 13-Jan-2004 : Fixed bug in previousStandardDate() method (fix by Richard
092 * Wardle) (DG);
093 * 21-Jan-2004 : Renamed translateJava2DToValue --> java2DToValue, and
094 * translateValueToJava2D --> valueToJava2D (DG);
095 * 12-Mar-2004 : Fixed bug where date format override is ignored for vertical
096 * axis (DG);
097 * 16-Mar-2004 : Added plotState to draw() method (DG);
098 * 07-Apr-2004 : Changed string width calculation (DG);
099 * 21-Apr-2004 : Fixed bug in estimateMaximumTickLabelWidth() method (bug id
100 * 939148) (DG);
101 * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
102 * release (DG);
103 * 13-Jan-2005 : Fixed bug (see
104 * http://www.jfree.org/forum/viewtopic.php?t=11330) (DG);
105 * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant
106 * argument from selectAutoTickUnit() (DG);
107 * ------------- JFREECHART 1.0.x ---------------------------------------------
108 * 10-Feb-2006 : Added some API doc comments in respect of bug 821046 (DG);
109 * 19-Apr-2006 : Fixed bug 1472942 in equals() method (DG);
110 * 25-Sep-2006 : Fixed bug 1564977 missing tick labels (DG);
111 * 15-Jan-2007 : Added get/setTimeZone() suggested by 'skunk' (DG);
112 * 18-Jan-2007 : Fixed bug 1638678, time zone for calendar in
113 * previousStandardDate() (DG);
114 *
115 */
116
117 package org.jfree.chart.axis;
118
119 import java.awt.Font;
120 import java.awt.FontMetrics;
121 import java.awt.Graphics2D;
122 import java.awt.font.FontRenderContext;
123 import java.awt.font.LineMetrics;
124 import java.awt.geom.Rectangle2D;
125 import java.io.Serializable;
126 import java.text.DateFormat;
127 import java.text.SimpleDateFormat;
128 import java.util.Calendar;
129 import java.util.Date;
130 import java.util.List;
131 import java.util.TimeZone;
132
133 import org.jfree.chart.event.AxisChangeEvent;
134 import org.jfree.chart.plot.Plot;
135 import org.jfree.chart.plot.PlotRenderingInfo;
136 import org.jfree.chart.plot.ValueAxisPlot;
137 import org.jfree.data.Range;
138 import org.jfree.data.time.DateRange;
139 import org.jfree.data.time.Month;
140 import org.jfree.data.time.RegularTimePeriod;
141 import org.jfree.data.time.Year;
142 import org.jfree.ui.RectangleEdge;
143 import org.jfree.ui.RectangleInsets;
144 import org.jfree.ui.TextAnchor;
145 import org.jfree.util.ObjectUtilities;
146
147 /**
148 * The base class for axes that display dates. You will find it easier to
149 * understand how this axis works if you bear in mind that it really
150 * displays/measures integer (or long) data, where the integers are
151 * milliseconds since midnight, 1-Jan-1970. When displaying tick labels, the
152 * millisecond values are converted back to dates using a
153 * <code>DateFormat</code> instance.
154 * <P>
155 * You can also create a {@link org.jfree.chart.axis.Timeline} and supply in
156 * the constructor to create an axis that only contains certain domain values.
157 * For example, this allows you to create a date axis that only contains
158 * working days.
159 */
160 public class DateAxis extends ValueAxis implements Cloneable, Serializable {
161
162 /** For serialization. */
163 private static final long serialVersionUID = -1013460999649007604L;
164
165 /** The default axis range. */
166 public static final DateRange DEFAULT_DATE_RANGE = new DateRange();
167
168 /** The default minimum auto range size. */
169 public static final double
170 DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS = 2.0;
171
172 /** The default date tick unit. */
173 public static final DateTickUnit DEFAULT_DATE_TICK_UNIT
174 = new DateTickUnit(DateTickUnit.DAY, 1, new SimpleDateFormat());
175
176 /** The default anchor date. */
177 public static final Date DEFAULT_ANCHOR_DATE = new Date();
178
179 /** The current tick unit. */
180 private DateTickUnit tickUnit;
181
182 /** The override date format. */
183 private DateFormat dateFormatOverride;
184
185 /**
186 * Tick marks can be displayed at the start or the middle of the time
187 * period.
188 */
189 private DateTickMarkPosition tickMarkPosition = DateTickMarkPosition.START;
190
191 /**
192 * A timeline that includes all milliseconds (as defined by
193 * <code>java.util.Date</code>) in the real time line.
194 */
195 private static class DefaultTimeline implements Timeline, Serializable {
196
197 /**
198 * Converts a millisecond into a timeline value.
199 *
200 * @param millisecond the millisecond.
201 *
202 * @return The timeline value.
203 */
204 public long toTimelineValue(long millisecond) {
205 return millisecond;
206 }
207
208 /**
209 * Converts a date into a timeline value.
210 *
211 * @param date the domain value.
212 *
213 * @return The timeline value.
214 */
215 public long toTimelineValue(Date date) {
216 return date.getTime();
217 }
218
219 /**
220 * Converts a timeline value into a millisecond (as encoded by
221 * <code>java.util.Date</code>).
222 *
223 * @param value the value.
224 *
225 * @return The millisecond.
226 */
227 public long toMillisecond(long value) {
228 return value;
229 }
230
231 /**
232 * Returns <code>true</code> if the timeline includes the specified
233 * domain value.
234 *
235 * @param millisecond the millisecond.
236 *
237 * @return <code>true</code>.
238 */
239 public boolean containsDomainValue(long millisecond) {
240 return true;
241 }
242
243 /**
244 * Returns <code>true</code> if the timeline includes the specified
245 * domain value.
246 *
247 * @param date the date.
248 *
249 * @return <code>true</code>.
250 */
251 public boolean containsDomainValue(Date date) {
252 return true;
253 }
254
255 /**
256 * Returns <code>true</code> if the timeline includes the specified
257 * domain value range.
258 *
259 * @param from the start value.
260 * @param to the end value.
261 *
262 * @return <code>true</code>.
263 */
264 public boolean containsDomainRange(long from, long to) {
265 return true;
266 }
267
268 /**
269 * Returns <code>true</code> if the timeline includes the specified
270 * domain value range.
271 *
272 * @param from the start date.
273 * @param to the end date.
274 *
275 * @return <code>true</code>.
276 */
277 public boolean containsDomainRange(Date from, Date to) {
278 return true;
279 }
280
281 /**
282 * Tests an object for equality with this instance.
283 *
284 * @param object the object.
285 *
286 * @return A boolean.
287 */
288 public boolean equals(Object object) {
289 if (object == null) {
290 return false;
291 }
292 if (object == this) {
293 return true;
294 }
295 if (object instanceof DefaultTimeline) {
296 return true;
297 }
298 return false;
299 }
300 }
301
302 /** A static default timeline shared by all standard DateAxis */
303 private static final Timeline DEFAULT_TIMELINE = new DefaultTimeline();
304
305 /** The time zone for the axis. */
306 private TimeZone timeZone;
307
308 /** Our underlying timeline. */
309 private Timeline timeline;
310
311 /**
312 * Creates a date axis with no label.
313 */
314 public DateAxis() {
315 this(null);
316 }
317
318 /**
319 * Creates a date axis with the specified label.
320 *
321 * @param label the axis label (<code>null</code> permitted).
322 */
323 public DateAxis(String label) {
324 this(label, TimeZone.getDefault());
325 }
326
327 /**
328 * Creates a date axis. A timeline is specified for the axis. This allows
329 * special transformations to occur between a domain of values and the
330 * values included in the axis.
331 *
332 * @see org.jfree.chart.axis.SegmentedTimeline
333 *
334 * @param label the axis label (<code>null</code> permitted).
335 * @param zone the time zone.
336 */
337 public DateAxis(String label, TimeZone zone) {
338 super(label, DateAxis.createStandardDateTickUnits(zone));
339 setTickUnit(DateAxis.DEFAULT_DATE_TICK_UNIT, false, false);
340 setAutoRangeMinimumSize(
341 DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS);
342 setRange(DEFAULT_DATE_RANGE, false, false);
343 this.dateFormatOverride = null;
344 this.timeZone = zone;
345 this.timeline = DEFAULT_TIMELINE;
346 }
347
348 /**
349 * Returns the time zone for the axis.
350 *
351 * @return The time zone.
352 *
353 * @since 1.0.4
354 * @see #setTimeZone(TimeZone)
355 */
356 public TimeZone getTimeZone() {
357 return this.timeZone;
358 }
359
360 /**
361 * Sets the time zone for the axis and sends an {@link AxisChangeEvent} to
362 * all registered listeners.
363 *
364 * @param zone the time zone (<code>null</code> not permitted).
365 *
366 * @since 1.0.4
367 * @see #getTimeZone()
368 */
369 public void setTimeZone(TimeZone zone) {
370 if (!this.timeZone.equals(zone)) {
371 this.timeZone = zone;
372 setStandardTickUnits(createStandardDateTickUnits(zone));
373 notifyListeners(new AxisChangeEvent(this));
374 }
375 }
376
377 /**
378 * Returns the underlying timeline used by this axis.
379 *
380 * @return The timeline.
381 */
382 public Timeline getTimeline() {
383 return this.timeline;
384 }
385
386 /**
387 * Sets the underlying timeline to use for this axis.
388 * <P>
389 * If the timeline is changed, an {@link AxisChangeEvent} is sent to all
390 * registered listeners.
391 *
392 * @param timeline the timeline.
393 */
394 public void setTimeline(Timeline timeline) {
395 if (this.timeline != timeline) {
396 this.timeline = timeline;
397 notifyListeners(new AxisChangeEvent(this));
398 }
399 }
400
401 /**
402 * Returns the tick unit for the axis.
403 * <p>
404 * Note: if the <code>autoTickUnitSelection</code> flag is
405 * <code>true</code> the tick unit may be changed while the axis is being
406 * drawn, so in that case the return value from this method may be
407 * irrelevant if the method is called before the axis has been drawn.
408 *
409 * @return The tick unit (possibly <code>null</code>).
410 *
411 * @see #setTickUnit(DateTickUnit)
412 * @see ValueAxis#isAutoTickUnitSelection()
413 */
414 public DateTickUnit getTickUnit() {
415 return this.tickUnit;
416 }
417
418 /**
419 * Sets the tick unit for the axis. The auto-tick-unit-selection flag is
420 * set to <code>false</code>, and registered listeners are notified that
421 * the axis has been changed.
422 *
423 * @param unit the tick unit.
424 *
425 * @see #getTickUnit()
426 * @see #setTickUnit(DateTickUnit, boolean, boolean)
427 */
428 public void setTickUnit(DateTickUnit unit) {
429 setTickUnit(unit, true, true);
430 }
431
432 /**
433 * Sets the tick unit attribute.
434 *
435 * @param unit the new tick unit.
436 * @param notify notify registered listeners?
437 * @param turnOffAutoSelection turn off auto selection?
438 *
439 * @see #getTickUnit()
440 */
441 public void setTickUnit(DateTickUnit unit, boolean notify,
442 boolean turnOffAutoSelection) {
443
444 this.tickUnit = unit;
445 if (turnOffAutoSelection) {
446 setAutoTickUnitSelection(false, false);
447 }
448 if (notify) {
449 notifyListeners(new AxisChangeEvent(this));
450 }
451
452 }
453
454 /**
455 * Returns the date format override. If this is non-null, then it will be
456 * used to format the dates on the axis.
457 *
458 * @return The formatter (possibly <code>null</code>).
459 */
460 public DateFormat getDateFormatOverride() {
461 return this.dateFormatOverride;
462 }
463
464 /**
465 * Sets the date format override. If this is non-null, then it will be
466 * used to format the dates on the axis.
467 *
468 * @param formatter the date formatter (<code>null</code> permitted).
469 */
470 public void setDateFormatOverride(DateFormat formatter) {
471 this.dateFormatOverride = formatter;
472 notifyListeners(new AxisChangeEvent(this));
473 }
474
475 /**
476 * Sets the upper and lower bounds for the axis and sends an
477 * {@link AxisChangeEvent} to all registered listeners. As a side-effect,
478 * the auto-range flag is set to false.
479 *
480 * @param range the new range (<code>null</code> not permitted).
481 */
482 public void setRange(Range range) {
483 setRange(range, true, true);
484 }
485
486 /**
487 * Sets the range for the axis, if requested, sends an
488 * {@link AxisChangeEvent} to all registered listeners. As a side-effect,
489 * the auto-range flag is set to <code>false</code> (optional).
490 *
491 * @param range the range (<code>null</code> not permitted).
492 * @param turnOffAutoRange a flag that controls whether or not the auto
493 * range is turned off.
494 * @param notify a flag that controls whether or not listeners are
495 * notified.
496 */
497 public void setRange(Range range, boolean turnOffAutoRange,
498 boolean notify) {
499 if (range == null) {
500 throw new IllegalArgumentException("Null 'range' argument.");
501 }
502 // usually the range will be a DateRange, but if it isn't do a
503 // conversion...
504 if (!(range instanceof DateRange)) {
505 range = new DateRange(range);
506 }
507 super.setRange(range, turnOffAutoRange, notify);
508 }
509
510 /**
511 * Sets the axis range and sends an {@link AxisChangeEvent} to all
512 * registered listeners.
513 *
514 * @param lower the lower bound for the axis.
515 * @param upper the upper bound for the axis.
516 */
517 public void setRange(Date lower, Date upper) {
518 if (lower.getTime() >= upper.getTime()) {
519 throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
520 }
521 setRange(new DateRange(lower, upper));
522 }
523
524 /**
525 * Sets the axis range and sends an {@link AxisChangeEvent} to all
526 * registered listeners.
527 *
528 * @param lower the lower bound for the axis.
529 * @param upper the upper bound for the axis.
530 */
531 public void setRange(double lower, double upper) {
532 if (lower >= upper) {
533 throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
534 }
535 setRange(new DateRange(lower, upper));
536 }
537
538 /**
539 * Returns the earliest date visible on the axis.
540 *
541 * @return The date.
542 */
543 public Date getMinimumDate() {
544 Date result = null;
545 Range range = getRange();
546 if (range instanceof DateRange) {
547 DateRange r = (DateRange) range;
548 result = r.getLowerDate();
549 }
550 else {
551 result = new Date((long) range.getLowerBound());
552 }
553 return result;
554 }
555
556 /**
557 * Sets the minimum date visible on the axis and sends an
558 * {@link AxisChangeEvent} to all registered listeners.
559 *
560 * @param date the date (<code>null</code> not permitted).
561 */
562 public void setMinimumDate(Date date) {
563 setRange(new DateRange(date, getMaximumDate()), true, false);
564 notifyListeners(new AxisChangeEvent(this));
565 }
566
567 /**
568 * Returns the latest date visible on the axis.
569 *
570 * @return The date.
571 */
572 public Date getMaximumDate() {
573
574 Date result = null;
575 Range range = getRange();
576 if (range instanceof DateRange) {
577 DateRange r = (DateRange) range;
578 result = r.getUpperDate();
579 }
580 else {
581 result = new Date((long) range.getUpperBound());
582 }
583 return result;
584
585 }
586
587 /**
588 * Sets the maximum date visible on the axis. An {@link AxisChangeEvent}
589 * is sent to all registered listeners.
590 *
591 * @param maximumDate the date (<code>null</code> not permitted).
592 */
593 public void setMaximumDate(Date maximumDate) {
594 setRange(new DateRange(getMinimumDate(), maximumDate), true, false);
595 notifyListeners(new AxisChangeEvent(this));
596 }
597
598 /**
599 * Returns the tick mark position (start, middle or end of the time period).
600 *
601 * @return The position (never <code>null</code>).
602 */
603 public DateTickMarkPosition getTickMarkPosition() {
604 return this.tickMarkPosition;
605 }
606
607 /**
608 * Sets the tick mark position (start, middle or end of the time period)
609 * and sends an {@link AxisChangeEvent} to all registered listeners.
610 *
611 * @param position the position (<code>null</code> not permitted).
612 */
613 public void setTickMarkPosition(DateTickMarkPosition position) {
614 if (position == null) {
615 throw new IllegalArgumentException("Null 'position' argument.");
616 }
617 this.tickMarkPosition = position;
618 notifyListeners(new AxisChangeEvent(this));
619 }
620
621 /**
622 * Configures the axis to work with the specified plot. If the axis has
623 * auto-scaling, then sets the maximum and minimum values.
624 */
625 public void configure() {
626 if (isAutoRange()) {
627 autoAdjustRange();
628 }
629 }
630
631 /**
632 * Returns <code>true</code> if the axis hides this value, and
633 * <code>false</code> otherwise.
634 *
635 * @param millis the data value.
636 *
637 * @return A value.
638 */
639 public boolean isHiddenValue(long millis) {
640 return (!this.timeline.containsDomainValue(new Date(millis)));
641 }
642
643 /**
644 * Translates the data value to the display coordinates (Java 2D User Space)
645 * of the chart.
646 *
647 * @param value the date to be plotted.
648 * @param area the rectangle (in Java2D space) where the data is to be
649 * plotted.
650 * @param edge the axis location.
651 *
652 * @return The coordinate corresponding to the supplied data value.
653 */
654 public double valueToJava2D(double value, Rectangle2D area,
655 RectangleEdge edge) {
656
657 value = this.timeline.toTimelineValue((long) value);
658
659 DateRange range = (DateRange) getRange();
660 double axisMin = this.timeline.toTimelineValue(range.getLowerDate());
661 double axisMax = this.timeline.toTimelineValue(range.getUpperDate());
662 double result = 0.0;
663 if (RectangleEdge.isTopOrBottom(edge)) {
664 double minX = area.getX();
665 double maxX = area.getMaxX();
666 if (isInverted()) {
667 result = maxX + ((value - axisMin) / (axisMax - axisMin))
668 * (minX - maxX);
669 }
670 else {
671 result = minX + ((value - axisMin) / (axisMax - axisMin))
672 * (maxX - minX);
673 }
674 }
675 else if (RectangleEdge.isLeftOrRight(edge)) {
676 double minY = area.getMinY();
677 double maxY = area.getMaxY();
678 if (isInverted()) {
679 result = minY + (((value - axisMin) / (axisMax - axisMin))
680 * (maxY - minY));
681 }
682 else {
683 result = maxY - (((value - axisMin) / (axisMax - axisMin))
684 * (maxY - minY));
685 }
686 }
687 return result;
688
689 }
690
691 /**
692 * Translates a date to Java2D coordinates, based on the range displayed by
693 * this axis for the specified data area.
694 *
695 * @param date the date.
696 * @param area the rectangle (in Java2D space) where the data is to be
697 * plotted.
698 * @param edge the axis location.
699 *
700 * @return The coordinate corresponding to the supplied date.
701 */
702 public double dateToJava2D(Date date, Rectangle2D area,
703 RectangleEdge edge) {
704 double value = date.getTime();
705 return valueToJava2D(value, area, edge);
706 }
707
708 /**
709 * Translates a Java2D coordinate into the corresponding data value. To
710 * perform this translation, you need to know the area used for plotting
711 * data, and which edge the axis is located on.
712 *
713 * @param java2DValue the coordinate in Java2D space.
714 * @param area the rectangle (in Java2D space) where the data is to be
715 * plotted.
716 * @param edge the axis location.
717 *
718 * @return A data value.
719 */
720 public double java2DToValue(double java2DValue, Rectangle2D area,
721 RectangleEdge edge) {
722
723 DateRange range = (DateRange) getRange();
724 double axisMin = this.timeline.toTimelineValue(range.getLowerDate());
725 double axisMax = this.timeline.toTimelineValue(range.getUpperDate());
726
727 double min = 0.0;
728 double max = 0.0;
729 if (RectangleEdge.isTopOrBottom(edge)) {
730 min = area.getX();
731 max = area.getMaxX();
732 }
733 else if (RectangleEdge.isLeftOrRight(edge)) {
734 min = area.getMaxY();
735 max = area.getY();
736 }
737
738 double result;
739 if (isInverted()) {
740 result = axisMax - ((java2DValue - min) / (max - min)
741 * (axisMax - axisMin));
742 }
743 else {
744 result = axisMin + ((java2DValue - min) / (max - min)
745 * (axisMax - axisMin));
746 }
747
748 return this.timeline.toMillisecond((long) result);
749 }
750
751 /**
752 * Calculates the value of the lowest visible tick on the axis.
753 *
754 * @param unit date unit to use.
755 *
756 * @return The value of the lowest visible tick on the axis.
757 */
758 public Date calculateLowestVisibleTickValue(DateTickUnit unit) {
759 return nextStandardDate(getMinimumDate(), unit);
760 }
761
762 /**
763 * Calculates the value of the highest visible tick on the axis.
764 *
765 * @param unit date unit to use.
766 *
767 * @return The value of the highest visible tick on the axis.
768 */
769 public Date calculateHighestVisibleTickValue(DateTickUnit unit) {
770 return previousStandardDate(getMaximumDate(), unit);
771 }
772
773 /**
774 * Returns the previous "standard" date, for a given date and tick unit.
775 *
776 * @param date the reference date.
777 * @param unit the tick unit.
778 *
779 * @return The previous "standard" date.
780 */
781 protected Date previousStandardDate(Date date, DateTickUnit unit) {
782
783 int milliseconds;
784 int seconds;
785 int minutes;
786 int hours;
787 int days;
788 int months;
789 int years;
790
791 Calendar calendar = Calendar.getInstance(this.timeZone);
792 calendar.setTime(date);
793 int count = unit.getCount();
794 int current = calendar.get(unit.getCalendarField());
795 int value = count * (current / count);
796
797 switch (unit.getUnit()) {
798
799 case (DateTickUnit.MILLISECOND) :
800 years = calendar.get(Calendar.YEAR);
801 months = calendar.get(Calendar.MONTH);
802 days = calendar.get(Calendar.DATE);
803 hours = calendar.get(Calendar.HOUR_OF_DAY);
804 minutes = calendar.get(Calendar.MINUTE);
805 seconds = calendar.get(Calendar.SECOND);
806 calendar.set(years, months, days, hours, minutes, seconds);
807 calendar.set(Calendar.MILLISECOND, value);
808 return calendar.getTime();
809
810 case (DateTickUnit.SECOND) :
811 years = calendar.get(Calendar.YEAR);
812 months = calendar.get(Calendar.MONTH);
813 days = calendar.get(Calendar.DATE);
814 hours = calendar.get(Calendar.HOUR_OF_DAY);
815 minutes = calendar.get(Calendar.MINUTE);
816 if (this.tickMarkPosition == DateTickMarkPosition.START) {
817 milliseconds = 0;
818 }
819 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
820 milliseconds = 500;
821 }
822 else {
823 milliseconds = 999;
824 }
825 calendar.set(Calendar.MILLISECOND, milliseconds);
826 calendar.set(years, months, days, hours, minutes, value);
827 return calendar.getTime();
828
829 case (DateTickUnit.MINUTE) :
830 years = calendar.get(Calendar.YEAR);
831 months = calendar.get(Calendar.MONTH);
832 days = calendar.get(Calendar.DATE);
833 hours = calendar.get(Calendar.HOUR_OF_DAY);
834 if (this.tickMarkPosition == DateTickMarkPosition.START) {
835 seconds = 0;
836 }
837 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
838 seconds = 30;
839 }
840 else {
841 seconds = 59;
842 }
843 calendar.clear(Calendar.MILLISECOND);
844 calendar.set(years, months, days, hours, value, seconds);
845 Date d0 = calendar.getTime();
846 if (d0.getTime() >= date.getTime()) {
847 calendar.set(Calendar.MINUTE, value - 1);
848 d0 = calendar.getTime();
849 }
850 return d0;
851
852 case (DateTickUnit.HOUR) :
853 years = calendar.get(Calendar.YEAR);
854 months = calendar.get(Calendar.MONTH);
855 days = calendar.get(Calendar.DATE);
856 if (this.tickMarkPosition == DateTickMarkPosition.START) {
857 minutes = 0;
858 seconds = 0;
859 }
860 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
861 minutes = 30;
862 seconds = 0;
863 }
864 else {
865 minutes = 59;
866 seconds = 59;
867 }
868 calendar.clear(Calendar.MILLISECOND);
869 calendar.set(years, months, days, value, minutes, seconds);
870 Date d1 = calendar.getTime();
871 if (d1.getTime() >= date.getTime()) {
872 calendar.set(Calendar.HOUR_OF_DAY, value - 1);
873 d1 = calendar.getTime();
874 }
875 return d1;
876
877 case (DateTickUnit.DAY) :
878 years = calendar.get(Calendar.YEAR);
879 months = calendar.get(Calendar.MONTH);
880 if (this.tickMarkPosition == DateTickMarkPosition.START) {
881 hours = 0;
882 minutes = 0;
883 seconds = 0;
884 }
885 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
886 hours = 12;
887 minutes = 0;
888 seconds = 0;
889 }
890 else {
891 hours = 23;
892 minutes = 59;
893 seconds = 59;
894 }
895 calendar.clear(Calendar.MILLISECOND);
896 calendar.set(years, months, value, hours, 0, 0);
897 // long result = calendar.getTimeInMillis();
898 // won't work with JDK 1.3
899 Date d2 = calendar.getTime();
900 if (d2.getTime() >= date.getTime()) {
901 calendar.set(Calendar.DATE, value - 1);
902 d2 = calendar.getTime();
903 }
904 return d2;
905
906 case (DateTickUnit.MONTH) :
907 years = calendar.get(Calendar.YEAR);
908 calendar.clear(Calendar.MILLISECOND);
909 calendar.set(years, value, 1, 0, 0, 0);
910 Month month = new Month(calendar.getTime());
911 Date standardDate = calculateDateForPosition(
912 month, this.tickMarkPosition);
913 long millis = standardDate.getTime();
914 if (millis > date.getTime()) {
915 month = (Month) month.previous();
916 standardDate = calculateDateForPosition(
917 month, this.tickMarkPosition);
918 }
919 return standardDate;
920
921 case(DateTickUnit.YEAR) :
922 if (this.tickMarkPosition == DateTickMarkPosition.START) {
923 months = 0;
924 days = 1;
925 }
926 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
927 months = 6;
928 days = 1;
929 }
930 else {
931 months = 11;
932 days = 31;
933 }
934 calendar.clear(Calendar.MILLISECOND);
935 calendar.set(value, months, days, 0, 0, 0);
936 Date d3 = calendar.getTime();
937 if (d3.getTime() >= date.getTime()) {
938 calendar.set(Calendar.YEAR, value - 1);
939 d3 = calendar.getTime();
940 }
941 return d3;
942
943 default: return null;
944
945 }
946
947 }
948
949 /**
950 * Returns a {@link java.util.Date} corresponding to the specified position
951 * within a {@link RegularTimePeriod}.
952 *
953 * @param period the period.
954 * @param position the position (<code>null</code> not permitted).
955 *
956 * @return A date.
957 */
958 private Date calculateDateForPosition(RegularTimePeriod period,
959 DateTickMarkPosition position) {
960
961 if (position == null) {
962 throw new IllegalArgumentException("Null 'position' argument.");
963 }
964 Date result = null;
965 if (position == DateTickMarkPosition.START) {
966 result = new Date(period.getFirstMillisecond());
967 }
968 else if (position == DateTickMarkPosition.MIDDLE) {
969 result = new Date(period.getMiddleMillisecond());
970 }
971 else if (position == DateTickMarkPosition.END) {
972 result = new Date(period.getLastMillisecond());
973 }
974 return result;
975
976 }
977
978 /**
979 * Returns the first "standard" date (based on the specified field and
980 * units).
981 *
982 * @param date the reference date.
983 * @param unit the date tick unit.
984 *
985 * @return The next "standard" date.
986 */
987 protected Date nextStandardDate(Date date, DateTickUnit unit) {
988 Date previous = previousStandardDate(date, unit);
989 Calendar calendar = Calendar.getInstance(this.timeZone);
990 calendar.setTime(previous);
991 calendar.add(unit.getCalendarField(), unit.getCount());
992 return calendar.getTime();
993 }
994
995 /**
996 * Returns a collection of standard date tick units that uses the default
997 * time zone. This collection will be used by default, but you are free
998 * to create your own collection if you want to (see the
999 * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited
1000 * from the {@link ValueAxis} class).
1001 *
1002 * @return A collection of standard date tick units.
1003 */
1004 public static TickUnitSource createStandardDateTickUnits() {
1005 return createStandardDateTickUnits(TimeZone.getDefault());
1006 }
1007
1008 /**
1009 * Returns a collection of standard date tick units. This collection will
1010 * be used by default, but you are free to create your own collection if
1011 * you want to (see the
1012 * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited
1013 * from the {@link ValueAxis} class).
1014 *
1015 * @param zone the time zone (<code>null</code> not permitted).
1016 *
1017 * @return A collection of standard date tick units.
1018 */
1019 public static TickUnitSource createStandardDateTickUnits(TimeZone zone) {
1020
1021 if (zone == null) {
1022 throw new IllegalArgumentException("Null 'zone' argument.");
1023 }
1024 TickUnits units = new TickUnits();
1025
1026 // date formatters
1027 DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS");
1028 DateFormat f2 = new SimpleDateFormat("HH:mm:ss");
1029 DateFormat f3 = new SimpleDateFormat("HH:mm");
1030 DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm");
1031 DateFormat f5 = new SimpleDateFormat("d-MMM");
1032 DateFormat f6 = new SimpleDateFormat("MMM-yyyy");
1033 DateFormat f7 = new SimpleDateFormat("yyyy");
1034
1035 f1.setTimeZone(zone);
1036 f2.setTimeZone(zone);
1037 f3.setTimeZone(zone);
1038 f4.setTimeZone(zone);
1039 f5.setTimeZone(zone);
1040 f6.setTimeZone(zone);
1041 f7.setTimeZone(zone);
1042
1043 // milliseconds
1044 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 1, f1));
1045 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 5,
1046 DateTickUnit.MILLISECOND, 1, f1));
1047 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 10,
1048 DateTickUnit.MILLISECOND, 1, f1));
1049 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 25,
1050 DateTickUnit.MILLISECOND, 5, f1));
1051 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 50,
1052 DateTickUnit.MILLISECOND, 10, f1));
1053 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 100,
1054 DateTickUnit.MILLISECOND, 10, f1));
1055 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 250,
1056 DateTickUnit.MILLISECOND, 10, f1));
1057 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 500,
1058 DateTickUnit.MILLISECOND, 50, f1));
1059
1060 // seconds
1061 units.add(new DateTickUnit(DateTickUnit.SECOND, 1,
1062 DateTickUnit.MILLISECOND, 50, f2));
1063 units.add(new DateTickUnit(DateTickUnit.SECOND, 5,
1064 DateTickUnit.SECOND, 1, f2));
1065 units.add(new DateTickUnit(DateTickUnit.SECOND, 10,
1066 DateTickUnit.SECOND, 1, f2));
1067 units.add(new DateTickUnit(DateTickUnit.SECOND, 30,
1068 DateTickUnit.SECOND, 5, f2));
1069
1070 // minutes
1071 units.add(new DateTickUnit(DateTickUnit.MINUTE, 1,
1072 DateTickUnit.SECOND, 5, f3));
1073 units.add(new DateTickUnit(DateTickUnit.MINUTE, 2,
1074 DateTickUnit.SECOND, 10, f3));
1075 units.add(new DateTickUnit(DateTickUnit.MINUTE, 5,
1076 DateTickUnit.MINUTE, 1, f3));
1077 units.add(new DateTickUnit(DateTickUnit.MINUTE, 10,
1078 DateTickUnit.MINUTE, 1, f3));
1079 units.add(new DateTickUnit(DateTickUnit.MINUTE, 15,
1080 DateTickUnit.MINUTE, 5, f3));
1081 units.add(new DateTickUnit(DateTickUnit.MINUTE, 20,
1082 DateTickUnit.MINUTE, 5, f3));
1083 units.add(new DateTickUnit(DateTickUnit.MINUTE, 30,
1084 DateTickUnit.MINUTE, 5, f3));
1085
1086 // hours
1087 units.add(new DateTickUnit(DateTickUnit.HOUR, 1,
1088 DateTickUnit.MINUTE, 5, f3));
1089 units.add(new DateTickUnit(DateTickUnit.HOUR, 2,
1090 DateTickUnit.MINUTE, 10, f3));
1091 units.add(new DateTickUnit(DateTickUnit.HOUR, 4,
1092 DateTickUnit.MINUTE, 30, f3));
1093 units.add(new DateTickUnit(DateTickUnit.HOUR, 6,
1094 DateTickUnit.HOUR, 1, f3));
1095 units.add(new DateTickUnit(DateTickUnit.HOUR, 12,
1096 DateTickUnit.HOUR, 1, f4));
1097
1098 // days
1099 units.add(new DateTickUnit(DateTickUnit.DAY, 1,
1100 DateTickUnit.HOUR, 1, f5));
1101 units.add(new DateTickUnit(DateTickUnit.DAY, 2,
1102 DateTickUnit.HOUR, 1, f5));
1103 units.add(new DateTickUnit(DateTickUnit.DAY, 7,
1104 DateTickUnit.DAY, 1, f5));
1105 units.add(new DateTickUnit(DateTickUnit.DAY, 15,
1106 DateTickUnit.DAY, 1, f5));
1107
1108 // months
1109 units.add(new DateTickUnit(DateTickUnit.MONTH, 1,
1110 DateTickUnit.DAY, 1, f6));
1111 units.add(new DateTickUnit(DateTickUnit.MONTH, 2,
1112 DateTickUnit.DAY, 1, f6));
1113 units.add(new DateTickUnit(DateTickUnit.MONTH, 3,
1114 DateTickUnit.MONTH, 1, f6));
1115 units.add(new DateTickUnit(DateTickUnit.MONTH, 4,
1116 DateTickUnit.MONTH, 1, f6));
1117 units.add(new DateTickUnit(DateTickUnit.MONTH, 6,
1118 DateTickUnit.MONTH, 1, f6));
1119
1120 // years
1121 units.add(new DateTickUnit(DateTickUnit.YEAR, 1,
1122 DateTickUnit.MONTH, 1, f7));
1123 units.add(new DateTickUnit(DateTickUnit.YEAR, 2,
1124 DateTickUnit.MONTH, 3, f7));
1125 units.add(new DateTickUnit(DateTickUnit.YEAR, 5,
1126 DateTickUnit.YEAR, 1, f7));
1127 units.add(new DateTickUnit(DateTickUnit.YEAR, 10,
1128 DateTickUnit.YEAR, 1, f7));
1129 units.add(new DateTickUnit(DateTickUnit.YEAR, 25,
1130 DateTickUnit.YEAR, 5, f7));
1131 units.add(new DateTickUnit(DateTickUnit.YEAR, 50,
1132 DateTickUnit.YEAR, 10, f7));
1133 units.add(new DateTickUnit(DateTickUnit.YEAR, 100,
1134 DateTickUnit.YEAR, 20, f7));
1135
1136 return units;
1137
1138 }
1139
1140 /**
1141 * Rescales the axis to ensure that all data is visible.
1142 */
1143 protected void autoAdjustRange() {
1144
1145 Plot plot = getPlot();
1146
1147 if (plot == null) {
1148 return; // no plot, no data
1149 }
1150
1151 if (plot instanceof ValueAxisPlot) {
1152 ValueAxisPlot vap = (ValueAxisPlot) plot;
1153
1154 Range r = vap.getDataRange(this);
1155 if (r == null) {
1156 if (this.timeline instanceof SegmentedTimeline) {
1157 //Timeline hasn't method getStartTime()
1158 r = new DateRange((
1159 (SegmentedTimeline) this.timeline).getStartTime(),
1160 ((SegmentedTimeline) this.timeline).getStartTime()
1161 + 1);
1162 }
1163 else {
1164 r = new DateRange();
1165 }
1166 }
1167
1168 long upper = this.timeline.toTimelineValue(
1169 (long) r.getUpperBound());
1170 long lower;
1171 long fixedAutoRange = (long) getFixedAutoRange();
1172 if (fixedAutoRange > 0.0) {
1173 lower = upper - fixedAutoRange;
1174 }
1175 else {
1176 lower = this.timeline.toTimelineValue((long) r.getLowerBound());
1177 double range = upper - lower;
1178 long minRange = (long) getAutoRangeMinimumSize();
1179 if (range < minRange) {
1180 long expand = (long) (minRange - range) / 2;
1181 upper = upper + expand;
1182 lower = lower - expand;
1183 }
1184 upper = upper + (long) (range * getUpperMargin());
1185 lower = lower - (long) (range * getLowerMargin());
1186 }
1187
1188 upper = this.timeline.toMillisecond(upper);
1189 lower = this.timeline.toMillisecond(lower);
1190 DateRange dr = new DateRange(new Date(lower), new Date(upper));
1191 setRange(dr, false, false);
1192 }
1193
1194 }
1195
1196 /**
1197 * Selects an appropriate tick value for the axis. The strategy is to
1198 * display as many ticks as possible (selected from an array of 'standard'
1199 * tick units) without the labels overlapping.
1200 *
1201 * @param g2 the graphics device.
1202 * @param dataArea the area defined by the axes.
1203 * @param edge the axis location.
1204 */
1205 protected void selectAutoTickUnit(Graphics2D g2,
1206 Rectangle2D dataArea,
1207 RectangleEdge edge) {
1208
1209 if (RectangleEdge.isTopOrBottom(edge)) {
1210 selectHorizontalAutoTickUnit(g2, dataArea, edge);
1211 }
1212 else if (RectangleEdge.isLeftOrRight(edge)) {
1213 selectVerticalAutoTickUnit(g2, dataArea, edge);
1214 }
1215
1216 }
1217
1218 /**
1219 * Selects an appropriate tick size for the axis. The strategy is to
1220 * display as many ticks as possible (selected from a collection of
1221 * 'standard' tick units) without the labels overlapping.
1222 *
1223 * @param g2 the graphics device.
1224 * @param dataArea the area defined by the axes.
1225 * @param edge the axis location.
1226 */
1227 protected void selectHorizontalAutoTickUnit(Graphics2D g2,
1228 Rectangle2D dataArea,
1229 RectangleEdge edge) {
1230
1231 long shift = 0;
1232 if (this.timeline instanceof SegmentedTimeline) {
1233 shift = ((SegmentedTimeline) this.timeline).getStartTime();
1234 }
1235 double zero = valueToJava2D(shift + 0.0, dataArea, edge);
1236 double tickLabelWidth
1237 = estimateMaximumTickLabelWidth(g2, getTickUnit());
1238
1239 // start with the current tick unit...
1240 TickUnitSource tickUnits = getStandardTickUnits();
1241 TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit());
1242 double x1 = valueToJava2D(shift + unit1.getSize(), dataArea, edge);
1243 double unit1Width = Math.abs(x1 - zero);
1244
1245 // then extrapolate...
1246 double guess = (tickLabelWidth / unit1Width) * unit1.getSize();
1247 DateTickUnit unit2 = (DateTickUnit) tickUnits.getCeilingTickUnit(guess);
1248 double x2 = valueToJava2D(shift + unit2.getSize(), dataArea, edge);
1249 double unit2Width = Math.abs(x2 - zero);
1250 tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit2);
1251 if (tickLabelWidth > unit2Width) {
1252 unit2 = (DateTickUnit) tickUnits.getLargerTickUnit(unit2);
1253 }
1254 setTickUnit(unit2, false, false);
1255 }
1256
1257 /**
1258 * Selects an appropriate tick size for the axis. The strategy is to
1259 * display as many ticks as possible (selected from a collection of
1260 * 'standard' tick units) without the labels overlapping.
1261 *
1262 * @param g2 the graphics device.
1263 * @param dataArea the area in which the plot should be drawn.
1264 * @param edge the axis location.
1265 */
1266 protected void selectVerticalAutoTickUnit(Graphics2D g2,
1267 Rectangle2D dataArea,
1268 RectangleEdge edge) {
1269
1270 // start with the current tick unit...
1271 TickUnitSource tickUnits = getStandardTickUnits();
1272 double zero = valueToJava2D(0.0, dataArea, edge);
1273
1274 // start with a unit that is at least 1/10th of the axis length
1275 double estimate1 = getRange().getLength() / 10.0;
1276 DateTickUnit candidate1
1277 = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate1);
1278 double labelHeight1 = estimateMaximumTickLabelHeight(g2, candidate1);
1279 double y1 = valueToJava2D(candidate1.getSize(), dataArea, edge);
1280 double candidate1UnitHeight = Math.abs(y1 - zero);
1281
1282 // now extrapolate based on label height and unit height...
1283 double estimate2
1284 = (labelHeight1 / candidate1UnitHeight) * candidate1.getSize();
1285 DateTickUnit candidate2
1286 = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate2);
1287 double labelHeight2 = estimateMaximumTickLabelHeight(g2, candidate2);
1288 double y2 = valueToJava2D(candidate2.getSize(), dataArea, edge);
1289 double unit2Height = Math.abs(y2 - zero);
1290
1291 // make final selection...
1292 DateTickUnit finalUnit;
1293 if (labelHeight2 < unit2Height) {
1294 finalUnit = candidate2;
1295 }
1296 else {
1297 finalUnit = (DateTickUnit) tickUnits.getLargerTickUnit(candidate2);
1298 }
1299 setTickUnit(finalUnit, false, false);
1300
1301 }
1302
1303 /**
1304 * Estimates the maximum width of the tick labels, assuming the specified
1305 * tick unit is used.
1306 * <P>
1307 * Rather than computing the string bounds of every tick on the axis, we
1308 * just look at two values: the lower bound and the upper bound for the
1309 * axis. These two values will usually be representative.
1310 *
1311 * @param g2 the graphics device.
1312 * @param unit the tick unit to use for calculation.
1313 *
1314 * @return The estimated maximum width of the tick labels.
1315 */
1316 private double estimateMaximumTickLabelWidth(Graphics2D g2,
1317 DateTickUnit unit) {
1318
1319 RectangleInsets tickLabelInsets = getTickLabelInsets();
1320 double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight();
1321
1322 Font tickLabelFont = getTickLabelFont();
1323 FontRenderContext frc = g2.getFontRenderContext();
1324 LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc);
1325 if (isVerticalTickLabels()) {
1326 // all tick labels have the same width (equal to the height of
1327 // the font)...
1328 result += lm.getHeight();
1329 }
1330 else {
1331 // look at lower and upper bounds...
1332 DateRange range = (DateRange) getRange();
1333 Date lower = range.getLowerDate();
1334 Date upper = range.getUpperDate();
1335 String lowerStr = null;
1336 String upperStr = null;
1337 DateFormat formatter = getDateFormatOverride();
1338 if (formatter != null) {
1339 lowerStr = formatter.format(lower);
1340 upperStr = formatter.format(upper);
1341 }
1342 else {
1343 lowerStr = unit.dateToString(lower);
1344 upperStr = unit.dateToString(upper);
1345 }
1346 FontMetrics fm = g2.getFontMetrics(tickLabelFont);
1347 double w1 = fm.stringWidth(lowerStr);
1348 double w2 = fm.stringWidth(upperStr);
1349 result += Math.max(w1, w2);
1350 }
1351
1352 return result;
1353
1354 }
1355
1356 /**
1357 * Estimates the maximum width of the tick labels, assuming the specified
1358 * tick unit is used.
1359 * <P>
1360 * Rather than computing the string bounds of every tick on the axis, we
1361 * just look at two values: the lower bound and the upper bound for the
1362 * axis. These two values will usually be representative.
1363 *
1364 * @param g2 the graphics device.
1365 * @param unit the tick unit to use for calculation.
1366 *
1367 * @return The estimated maximum width of the tick labels.
1368 */
1369 private double estimateMaximumTickLabelHeight(Graphics2D g2,
1370 DateTickUnit unit) {
1371
1372 RectangleInsets tickLabelInsets = getTickLabelInsets();
1373 double result = tickLabelInsets.getTop() + tickLabelInsets.getBottom();
1374
1375 Font tickLabelFont = getTickLabelFont();
1376 FontRenderContext frc = g2.getFontRenderContext();
1377 LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc);
1378 if (!isVerticalTickLabels()) {
1379 // all tick labels have the same width (equal to the height of
1380 // the font)...
1381 result += lm.getHeight();
1382 }
1383 else {
1384 // look at lower and upper bounds...
1385 DateRange range = (DateRange) getRange();
1386 Date lower = range.getLowerDate();
1387 Date upper = range.getUpperDate();
1388 String lowerStr = null;
1389 String upperStr = null;
1390 DateFormat formatter = getDateFormatOverride();
1391 if (formatter != null) {
1392 lowerStr = formatter.format(lower);
1393 upperStr = formatter.format(upper);
1394 }
1395 else {
1396 lowerStr = unit.dateToString(lower);
1397 upperStr = unit.dateToString(upper);
1398 }
1399 FontMetrics fm = g2.getFontMetrics(tickLabelFont);
1400 double w1 = fm.stringWidth(lowerStr);
1401 double w2 = fm.stringWidth(upperStr);
1402 result += Math.max(w1, w2);
1403 }
1404
1405 return result;
1406
1407 }
1408
1409 /**
1410 * Calculates the positions of the tick labels for the axis, storing the
1411 * results in the tick label list (ready for drawing).
1412 *
1413 * @param g2 the graphics device.
1414 * @param state the axis state.
1415 * @param dataArea the area in which the plot should be drawn.
1416 * @param edge the location of the axis.
1417 *
1418 * @return A list of ticks.
1419 */
1420 public List refreshTicks(Graphics2D g2,
1421 AxisState state,
1422 Rectangle2D dataArea,
1423 RectangleEdge edge) {
1424
1425 List result = null;
1426 if (RectangleEdge.isTopOrBottom(edge)) {
1427 result = refreshTicksHorizontal(g2, dataArea, edge);
1428 }
1429 else if (RectangleEdge.isLeftOrRight(edge)) {
1430 result = refreshTicksVertical(g2, dataArea, edge);
1431 }
1432 return result;
1433
1434 }
1435
1436 /**
1437 * Recalculates the ticks for the date axis.
1438 *
1439 * @param g2 the graphics device.
1440 * @param dataArea the area in which the data is to be drawn.
1441 * @param edge the location of the axis.
1442 *
1443 * @return A list of ticks.
1444 */
1445 protected List refreshTicksHorizontal(Graphics2D g2,
1446 Rectangle2D dataArea,
1447 RectangleEdge edge) {
1448
1449 List result = new java.util.ArrayList();
1450
1451 Font tickLabelFont = getTickLabelFont();
1452 g2.setFont(tickLabelFont);
1453
1454 if (isAutoTickUnitSelection()) {
1455 selectAutoTickUnit(g2, dataArea, edge);
1456 }
1457
1458 DateTickUnit unit = getTickUnit();
1459 Date tickDate = calculateLowestVisibleTickValue(unit);
1460 Date upperDate = getMaximumDate();
1461 // float lastX = Float.MIN_VALUE;
1462 while (tickDate.before(upperDate)) {
1463
1464 if (!isHiddenValue(tickDate.getTime())) {
1465 // work out the value, label and position
1466 String tickLabel;
1467 DateFormat formatter = getDateFormatOverride();
1468 if (formatter != null) {
1469 tickLabel = formatter.format(tickDate);
1470 }
1471 else {
1472 tickLabel = this.tickUnit.dateToString(tickDate);
1473 }
1474 TextAnchor anchor = null;
1475 TextAnchor rotationAnchor = null;
1476 double angle = 0.0;
1477 if (isVerticalTickLabels()) {
1478 anchor = TextAnchor.CENTER_RIGHT;
1479 rotationAnchor = TextAnchor.CENTER_RIGHT;
1480 if (edge == RectangleEdge.TOP) {
1481 angle = Math.PI / 2.0;
1482 }
1483 else {
1484 angle = -Math.PI / 2.0;
1485 }
1486 }
1487 else {
1488 if (edge == RectangleEdge.TOP) {
1489 anchor = TextAnchor.BOTTOM_CENTER;
1490 rotationAnchor = TextAnchor.BOTTOM_CENTER;
1491 }
1492 else {
1493 anchor = TextAnchor.TOP_CENTER;
1494 rotationAnchor = TextAnchor.TOP_CENTER;
1495 }
1496 }
1497
1498 Tick tick = new DateTick(
1499 tickDate, tickLabel, anchor, rotationAnchor, angle
1500 );
1501 result.add(tick);
1502 tickDate = unit.addToDate(tickDate);
1503 }
1504 else {
1505 tickDate = unit.rollDate(tickDate);
1506 continue;
1507 }
1508
1509 // could add a flag to make the following correction optional...
1510 switch (unit.getUnit()) {
1511
1512 case (DateTickUnit.MILLISECOND) :
1513 case (DateTickUnit.SECOND) :
1514 case (DateTickUnit.MINUTE) :
1515 case (DateTickUnit.HOUR) :
1516 case (DateTickUnit.DAY) :
1517 break;
1518 case (DateTickUnit.MONTH) :
1519 tickDate = calculateDateForPosition(new Month(tickDate),
1520 this.tickMarkPosition);
1521 break;
1522 case(DateTickUnit.YEAR) :
1523 tickDate = calculateDateForPosition(
1524 new Year(tickDate), this.tickMarkPosition);
1525 break;
1526
1527 default: break;
1528
1529 }
1530
1531 }
1532 return result;
1533
1534 }
1535
1536 /**
1537 * Recalculates the ticks for the date axis.
1538 *
1539 * @param g2 the graphics device.
1540 * @param dataArea the area in which the plot should be drawn.
1541 * @param edge the location of the axis.
1542 *
1543 * @return A list of ticks.
1544 */
1545 protected List refreshTicksVertical(Graphics2D g2,
1546 Rectangle2D dataArea,
1547 RectangleEdge edge) {
1548
1549 List result = new java.util.ArrayList();
1550
1551 Font tickLabelFont = getTickLabelFont();
1552 g2.setFont(tickLabelFont);
1553
1554 if (isAutoTickUnitSelection()) {
1555 selectAutoTickUnit(g2, dataArea, edge);
1556 }
1557 DateTickUnit unit = getTickUnit();
1558 Date tickDate = calculateLowestVisibleTickValue(unit);
1559 //Date upperDate = calculateHighestVisibleTickValue(unit);
1560 Date upperDate = getMaximumDate();
1561 while (tickDate.before(upperDate)) {
1562
1563 if (!isHiddenValue(tickDate.getTime())) {
1564 // work out the value, label and position
1565 String tickLabel;
1566 DateFormat formatter = getDateFormatOverride();
1567 if (formatter != null) {
1568 tickLabel = formatter.format(tickDate);
1569 }
1570 else {
1571 tickLabel = this.tickUnit.dateToString(tickDate);
1572 }
1573 TextAnchor anchor = null;
1574 TextAnchor rotationAnchor = null;
1575 double angle = 0.0;
1576 if (isVerticalTickLabels()) {
1577 anchor = TextAnchor.BOTTOM_CENTER;
1578 rotationAnchor = TextAnchor.BOTTOM_CENTER;
1579 if (edge == RectangleEdge.LEFT) {
1580 angle = -Math.PI / 2.0;
1581 }
1582 else {
1583 angle = Math.PI / 2.0;
1584 }
1585 }
1586 else {
1587 if (edge == RectangleEdge.LEFT) {
1588 anchor = TextAnchor.CENTER_RIGHT;
1589 rotationAnchor = TextAnchor.CENTER_RIGHT;
1590 }
1591 else {
1592 anchor = TextAnchor.CENTER_LEFT;
1593 rotationAnchor = TextAnchor.CENTER_LEFT;
1594 }
1595 }
1596
1597 Tick tick = new DateTick(tickDate, tickLabel, anchor,
1598 rotationAnchor, angle);
1599 result.add(tick);
1600 tickDate = unit.addToDate(tickDate);
1601 }
1602 else {
1603 tickDate = unit.rollDate(tickDate);
1604 }
1605 }
1606 return result;
1607 }
1608
1609 /**
1610 * Draws the axis on a Java 2D graphics device (such as the screen or a
1611 * printer).
1612 *
1613 * @param g2 the graphics device (<code>null</code> not permitted).
1614 * @param cursor the cursor location.
1615 * @param plotArea the area within which the axes and data should be
1616 * drawn (<code>null</code> not permitted).
1617 * @param dataArea the area within which the data should be drawn
1618 * (<code>null</code> not permitted).
1619 * @param edge the location of the axis (<code>null</code> not permitted).
1620 * @param plotState collects information about the plot
1621 * (<code>null</code> permitted).
1622 *
1623 * @return The axis state (never <code>null</code>).
1624 */
1625 public AxisState draw(Graphics2D g2,
1626 double cursor,
1627 Rectangle2D plotArea,
1628 Rectangle2D dataArea,
1629 RectangleEdge edge,
1630 PlotRenderingInfo plotState) {
1631
1632 // if the axis is not visible, don't draw it...
1633 if (!isVisible()) {
1634 AxisState state = new AxisState(cursor);
1635 // even though the axis is not visible, we need to refresh ticks in
1636 // case the grid is being drawn...
1637 List ticks = refreshTicks(g2, state, dataArea, edge);
1638 state.setTicks(ticks);
1639 return state;
1640 }
1641
1642 // draw the tick marks and labels...
1643 AxisState state = drawTickMarksAndLabels(g2, cursor, plotArea,
1644 dataArea, edge);
1645
1646 // draw the axis label (note that 'state' is passed in *and*
1647 // returned)...
1648 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
1649
1650 return state;
1651
1652 }
1653
1654 /**
1655 * Zooms in on the current range.
1656 *
1657 * @param lowerPercent the new lower bound.
1658 * @param upperPercent the new upper bound.
1659 */
1660 public void zoomRange(double lowerPercent, double upperPercent) {
1661 double start = this.timeline.toTimelineValue(
1662 (long) getRange().getLowerBound()
1663 );
1664 double length = (this.timeline.toTimelineValue(
1665 (long) getRange().getUpperBound())
1666 - this.timeline.toTimelineValue(
1667 (long) getRange().getLowerBound()));
1668 Range adjusted = null;
1669 if (isInverted()) {
1670 adjusted = new DateRange(this.timeline.toMillisecond((long) (start
1671 + (length * (1 - upperPercent)))),
1672 this.timeline.toMillisecond((long) (start + (length
1673 * (1 - lowerPercent)))));
1674 }
1675 else {
1676 adjusted = new DateRange(this.timeline.toMillisecond(
1677 (long) (start + length * lowerPercent)),
1678 this.timeline.toMillisecond((long) (start + length
1679 * upperPercent)));
1680 }
1681 setRange(adjusted);
1682 }
1683
1684 /**
1685 * Tests this axis for equality with an arbitrary object.
1686 *
1687 * @param obj the object (<code>null</code> permitted).
1688 *
1689 * @return A boolean.
1690 */
1691 public boolean equals(Object obj) {
1692 if (obj == this) {
1693 return true;
1694 }
1695 if (!(obj instanceof DateAxis)) {
1696 return false;
1697 }
1698 DateAxis that = (DateAxis) obj;
1699 if (!ObjectUtilities.equal(this.tickUnit, that.tickUnit)) {
1700 return false;
1701 }
1702 if (!ObjectUtilities.equal(this.dateFormatOverride,
1703 that.dateFormatOverride)) {
1704 return false;
1705 }
1706 if (!ObjectUtilities.equal(this.tickMarkPosition,
1707 that.tickMarkPosition)) {
1708 return false;
1709 }
1710 if (!ObjectUtilities.equal(this.timeline, that.timeline)) {
1711 return false;
1712 }
1713 if (!super.equals(obj)) {
1714 return false;
1715 }
1716 return true;
1717 }
1718
1719 /**
1720 * Returns a hash code for this object.
1721 *
1722 * @return A hash code.
1723 */
1724 public int hashCode() {
1725 if (getLabel() != null) {
1726 return getLabel().hashCode();
1727 }
1728 else {
1729 return 0;
1730 }
1731 }
1732
1733 /**
1734 * Returns a clone of the object.
1735 *
1736 * @return A clone.
1737 *
1738 * @throws CloneNotSupportedException if some component of the axis does
1739 * not support cloning.
1740 */
1741 public Object clone() throws CloneNotSupportedException {
1742
1743 DateAxis clone = (DateAxis) super.clone();
1744
1745 // 'dateTickUnit' is immutable : no need to clone
1746 if (this.dateFormatOverride != null) {
1747 clone.dateFormatOverride
1748 = (DateFormat) this.dateFormatOverride.clone();
1749 }
1750 // 'tickMarkPosition' is immutable : no need to clone
1751
1752 return clone;
1753
1754 }
1755
1756 }