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 * Chris Boek;
039 *
040 * $Id: DateAxis.java,v 1.17.2.11 2007/05/03 14:27:11 mungady Exp $
041 *
042 * Changes (from 23-Jun-2001)
043 * --------------------------
044 * 23-Jun-2001 : Modified to work with null data source (DG);
045 * 18-Sep-2001 : Updated header (DG);
046 * 27-Nov-2001 : Changed constructors from public to protected, updated Javadoc
047 * comments (DG);
048 * 16-Jan-2002 : Added an optional crosshair, based on the implementation by
049 * Jonathan Nash (DG);
050 * 26-Feb-2002 : Updated import statements (DG);
051 * 22-Apr-2002 : Added a setRange() method (DG);
052 * 25-Jun-2002 : Removed redundant local variable (DG);
053 * 25-Jul-2002 : Changed order of parameters in ValueAxis constructor (DG);
054 * 21-Aug-2002 : The setTickUnit() method now turns off auto-tick unit
055 * selection (fix for bug id 528885) (DG);
056 * 05-Sep-2002 : Updated the constructors to reflect changes in the Axis
057 * class (DG);
058 * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG);
059 * 25-Sep-2002 : Added new setRange() methods, and deprecated
060 * setAxisRange() (DG);
061 * 04-Oct-2002 : Changed auto tick selection to parallel number axis
062 * classes (DG);
063 * 24-Oct-2002 : Added a date format override (DG);
064 * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
065 * 14-Jan-2003 : Changed autoRangeMinimumSize from Number --> double, moved
066 * crosshair settings to the plot (DG);
067 * 15-Jan-2003 : Removed anchor date (DG);
068 * 20-Jan-2003 : Removed unnecessary constructors (DG);
069 * 26-Mar-2003 : Implemented Serializable (DG);
070 * 02-May-2003 : Added additional units to createStandardDateTickUnits()
071 * method, as suggested by mhilpert in bug report 723187 (DG);
072 * 13-May-2003 : Merged HorizontalDateAxis and VerticalDateAxis (DG);
073 * 24-May-2003 : Added support for underlying timeline for
074 * SegmentedTimeline (BK);
075 * 16-Jul-2003 : Applied patch from Pawel Pabis to fix overlapping dates (DG);
076 * 22-Jul-2003 : Applied patch from Pawel Pabis for monthly ticks (DG);
077 * 25-Jul-2003 : Fixed bug 777561 and 777586 (DG);
078 * 13-Aug-2003 : Implemented Cloneable and added equals() method (DG);
079 * 02-Sep-2003 : Fixes for bug report 790506 (DG);
080 * 04-Sep-2003 : Fixed tick label alignment when axis appears at the top (DG);
081 * 10-Sep-2003 : Fixes for segmented timeline (DG);
082 * 17-Sep-2003 : Fixed a layout bug when multiple domain axes are used (DG);
083 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
084 * 07-Nov-2003 : Modified to use new tick classes (DG);
085 * 12-Nov-2003 : Modified tick labelling to use roll unit from DateTickUnit
086 * when a calculated tick value is hidden (which can occur in
087 * segmented date axes) (DG);
088 * 24-Nov-2003 : Fixed some problems with the auto tick unit selection, and
089 * fixed bug 846277 (labels missing for inverted axis) (DG);
090 * 30-Dec-2003 : Fixed bug in refreshTicksHorizontal() when start of time unit
091 * (ex. 1st of month) was hidden, causing infinite loop (BK);
092 * 13-Jan-2004 : Fixed bug in previousStandardDate() method (fix by Richard
093 * Wardle) (DG);
094 * 21-Jan-2004 : Renamed translateJava2DToValue --> java2DToValue, and
095 * translateValueToJava2D --> valueToJava2D (DG);
096 * 12-Mar-2004 : Fixed bug where date format override is ignored for vertical
097 * axis (DG);
098 * 16-Mar-2004 : Added plotState to draw() method (DG);
099 * 07-Apr-2004 : Changed string width calculation (DG);
100 * 21-Apr-2004 : Fixed bug in estimateMaximumTickLabelWidth() method (bug id
101 * 939148) (DG);
102 * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
103 * release (DG);
104 * 13-Jan-2005 : Fixed bug (see
105 * http://www.jfree.org/forum/viewtopic.php?t=11330) (DG);
106 * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant
107 * argument from selectAutoTickUnit() (DG);
108 * ------------- JFREECHART 1.0.x ---------------------------------------------
109 * 10-Feb-2006 : Added some API doc comments in respect of bug 821046 (DG);
110 * 19-Apr-2006 : Fixed bug 1472942 in equals() method (DG);
111 * 25-Sep-2006 : Fixed bug 1564977 missing tick labels (DG);
112 * 15-Jan-2007 : Added get/setTimeZone() suggested by 'skunk' (DG);
113 * 18-Jan-2007 : Fixed bug 1638678, time zone for calendar in
114 * previousStandardDate() (DG);
115 * 04-Apr-2007 : Use time zone in date calculations (CB);
116 * 19-Apr-2007 : Fix exceptions in setMinimum/MaximumDate() (DG);
117 * 03-May-2007 : Fixed minor bugs in previousStandardDate(), with new JUnit
118 * tests (DG);
119 *
120 */
121
122 package org.jfree.chart.axis;
123
124 import java.awt.Font;
125 import java.awt.FontMetrics;
126 import java.awt.Graphics2D;
127 import java.awt.font.FontRenderContext;
128 import java.awt.font.LineMetrics;
129 import java.awt.geom.Rectangle2D;
130 import java.io.Serializable;
131 import java.text.DateFormat;
132 import java.text.SimpleDateFormat;
133 import java.util.Calendar;
134 import java.util.Date;
135 import java.util.List;
136 import java.util.TimeZone;
137
138 import org.jfree.chart.event.AxisChangeEvent;
139 import org.jfree.chart.plot.Plot;
140 import org.jfree.chart.plot.PlotRenderingInfo;
141 import org.jfree.chart.plot.ValueAxisPlot;
142 import org.jfree.data.Range;
143 import org.jfree.data.time.DateRange;
144 import org.jfree.data.time.Month;
145 import org.jfree.data.time.RegularTimePeriod;
146 import org.jfree.data.time.Year;
147 import org.jfree.ui.RectangleEdge;
148 import org.jfree.ui.RectangleInsets;
149 import org.jfree.ui.TextAnchor;
150 import org.jfree.util.ObjectUtilities;
151
152 /**
153 * The base class for axes that display dates. You will find it easier to
154 * understand how this axis works if you bear in mind that it really
155 * displays/measures integer (or long) data, where the integers are
156 * milliseconds since midnight, 1-Jan-1970. When displaying tick labels, the
157 * millisecond values are converted back to dates using a
158 * <code>DateFormat</code> instance.
159 * <P>
160 * You can also create a {@link org.jfree.chart.axis.Timeline} and supply in
161 * the constructor to create an axis that only contains certain domain values.
162 * For example, this allows you to create a date axis that only contains
163 * working days.
164 */
165 public class DateAxis extends ValueAxis implements Cloneable, Serializable {
166
167 /** For serialization. */
168 private static final long serialVersionUID = -1013460999649007604L;
169
170 /** The default axis range. */
171 public static final DateRange DEFAULT_DATE_RANGE = new DateRange();
172
173 /** The default minimum auto range size. */
174 public static final double
175 DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS = 2.0;
176
177 /** The default date tick unit. */
178 public static final DateTickUnit DEFAULT_DATE_TICK_UNIT
179 = new DateTickUnit(DateTickUnit.DAY, 1, new SimpleDateFormat());
180
181 /** The default anchor date. */
182 public static final Date DEFAULT_ANCHOR_DATE = new Date();
183
184 /** The current tick unit. */
185 private DateTickUnit tickUnit;
186
187 /** The override date format. */
188 private DateFormat dateFormatOverride;
189
190 /**
191 * Tick marks can be displayed at the start or the middle of the time
192 * period.
193 */
194 private DateTickMarkPosition tickMarkPosition = DateTickMarkPosition.START;
195
196 /**
197 * A timeline that includes all milliseconds (as defined by
198 * <code>java.util.Date</code>) in the real time line.
199 */
200 private static class DefaultTimeline implements Timeline, Serializable {
201
202 /**
203 * Converts a millisecond into a timeline value.
204 *
205 * @param millisecond the millisecond.
206 *
207 * @return The timeline value.
208 */
209 public long toTimelineValue(long millisecond) {
210 return millisecond;
211 }
212
213 /**
214 * Converts a date into a timeline value.
215 *
216 * @param date the domain value.
217 *
218 * @return The timeline value.
219 */
220 public long toTimelineValue(Date date) {
221 return date.getTime();
222 }
223
224 /**
225 * Converts a timeline value into a millisecond (as encoded by
226 * <code>java.util.Date</code>).
227 *
228 * @param value the value.
229 *
230 * @return The millisecond.
231 */
232 public long toMillisecond(long value) {
233 return value;
234 }
235
236 /**
237 * Returns <code>true</code> if the timeline includes the specified
238 * domain value.
239 *
240 * @param millisecond the millisecond.
241 *
242 * @return <code>true</code>.
243 */
244 public boolean containsDomainValue(long millisecond) {
245 return true;
246 }
247
248 /**
249 * Returns <code>true</code> if the timeline includes the specified
250 * domain value.
251 *
252 * @param date the date.
253 *
254 * @return <code>true</code>.
255 */
256 public boolean containsDomainValue(Date date) {
257 return true;
258 }
259
260 /**
261 * Returns <code>true</code> if the timeline includes the specified
262 * domain value range.
263 *
264 * @param from the start value.
265 * @param to the end value.
266 *
267 * @return <code>true</code>.
268 */
269 public boolean containsDomainRange(long from, long to) {
270 return true;
271 }
272
273 /**
274 * Returns <code>true</code> if the timeline includes the specified
275 * domain value range.
276 *
277 * @param from the start date.
278 * @param to the end date.
279 *
280 * @return <code>true</code>.
281 */
282 public boolean containsDomainRange(Date from, Date to) {
283 return true;
284 }
285
286 /**
287 * Tests an object for equality with this instance.
288 *
289 * @param object the object.
290 *
291 * @return A boolean.
292 */
293 public boolean equals(Object object) {
294 if (object == null) {
295 return false;
296 }
297 if (object == this) {
298 return true;
299 }
300 if (object instanceof DefaultTimeline) {
301 return true;
302 }
303 return false;
304 }
305 }
306
307 /** A static default timeline shared by all standard DateAxis */
308 private static final Timeline DEFAULT_TIMELINE = new DefaultTimeline();
309
310 /** The time zone for the axis. */
311 private TimeZone timeZone;
312
313 /** Our underlying timeline. */
314 private Timeline timeline;
315
316 /**
317 * Creates a date axis with no label.
318 */
319 public DateAxis() {
320 this(null);
321 }
322
323 /**
324 * Creates a date axis with the specified label.
325 *
326 * @param label the axis label (<code>null</code> permitted).
327 */
328 public DateAxis(String label) {
329 this(label, TimeZone.getDefault());
330 }
331
332 /**
333 * Creates a date axis. A timeline is specified for the axis. This allows
334 * special transformations to occur between a domain of values and the
335 * values included in the axis.
336 *
337 * @see org.jfree.chart.axis.SegmentedTimeline
338 *
339 * @param label the axis label (<code>null</code> permitted).
340 * @param zone the time zone.
341 */
342 public DateAxis(String label, TimeZone zone) {
343 super(label, DateAxis.createStandardDateTickUnits(zone));
344 setTickUnit(DateAxis.DEFAULT_DATE_TICK_UNIT, false, false);
345 setAutoRangeMinimumSize(
346 DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS);
347 setRange(DEFAULT_DATE_RANGE, false, false);
348 this.dateFormatOverride = null;
349 this.timeZone = zone;
350 this.timeline = DEFAULT_TIMELINE;
351 }
352
353 /**
354 * Returns the time zone for the axis.
355 *
356 * @return The time zone.
357 *
358 * @since 1.0.4
359 * @see #setTimeZone(TimeZone)
360 */
361 public TimeZone getTimeZone() {
362 return this.timeZone;
363 }
364
365 /**
366 * Sets the time zone for the axis and sends an {@link AxisChangeEvent} to
367 * all registered listeners.
368 *
369 * @param zone the time zone (<code>null</code> not permitted).
370 *
371 * @since 1.0.4
372 * @see #getTimeZone()
373 */
374 public void setTimeZone(TimeZone zone) {
375 if (!this.timeZone.equals(zone)) {
376 this.timeZone = zone;
377 setStandardTickUnits(createStandardDateTickUnits(zone));
378 notifyListeners(new AxisChangeEvent(this));
379 }
380 }
381
382 /**
383 * Returns the underlying timeline used by this axis.
384 *
385 * @return The timeline.
386 */
387 public Timeline getTimeline() {
388 return this.timeline;
389 }
390
391 /**
392 * Sets the underlying timeline to use for this axis.
393 * <P>
394 * If the timeline is changed, an {@link AxisChangeEvent} is sent to all
395 * registered listeners.
396 *
397 * @param timeline the timeline.
398 */
399 public void setTimeline(Timeline timeline) {
400 if (this.timeline != timeline) {
401 this.timeline = timeline;
402 notifyListeners(new AxisChangeEvent(this));
403 }
404 }
405
406 /**
407 * Returns the tick unit for the axis.
408 * <p>
409 * Note: if the <code>autoTickUnitSelection</code> flag is
410 * <code>true</code> the tick unit may be changed while the axis is being
411 * drawn, so in that case the return value from this method may be
412 * irrelevant if the method is called before the axis has been drawn.
413 *
414 * @return The tick unit (possibly <code>null</code>).
415 *
416 * @see #setTickUnit(DateTickUnit)
417 * @see ValueAxis#isAutoTickUnitSelection()
418 */
419 public DateTickUnit getTickUnit() {
420 return this.tickUnit;
421 }
422
423 /**
424 * Sets the tick unit for the axis. The auto-tick-unit-selection flag is
425 * set to <code>false</code>, and registered listeners are notified that
426 * the axis has been changed.
427 *
428 * @param unit the tick unit.
429 *
430 * @see #getTickUnit()
431 * @see #setTickUnit(DateTickUnit, boolean, boolean)
432 */
433 public void setTickUnit(DateTickUnit unit) {
434 setTickUnit(unit, true, true);
435 }
436
437 /**
438 * Sets the tick unit attribute.
439 *
440 * @param unit the new tick unit.
441 * @param notify notify registered listeners?
442 * @param turnOffAutoSelection turn off auto selection?
443 *
444 * @see #getTickUnit()
445 */
446 public void setTickUnit(DateTickUnit unit, boolean notify,
447 boolean turnOffAutoSelection) {
448
449 this.tickUnit = unit;
450 if (turnOffAutoSelection) {
451 setAutoTickUnitSelection(false, false);
452 }
453 if (notify) {
454 notifyListeners(new AxisChangeEvent(this));
455 }
456
457 }
458
459 /**
460 * Returns the date format override. If this is non-null, then it will be
461 * used to format the dates on the axis.
462 *
463 * @return The formatter (possibly <code>null</code>).
464 */
465 public DateFormat getDateFormatOverride() {
466 return this.dateFormatOverride;
467 }
468
469 /**
470 * Sets the date format override. If this is non-null, then it will be
471 * used to format the dates on the axis.
472 *
473 * @param formatter the date formatter (<code>null</code> permitted).
474 */
475 public void setDateFormatOverride(DateFormat formatter) {
476 this.dateFormatOverride = formatter;
477 notifyListeners(new AxisChangeEvent(this));
478 }
479
480 /**
481 * Sets the upper and lower bounds for the axis and sends an
482 * {@link AxisChangeEvent} to all registered listeners. As a side-effect,
483 * the auto-range flag is set to false.
484 *
485 * @param range the new range (<code>null</code> not permitted).
486 */
487 public void setRange(Range range) {
488 setRange(range, true, true);
489 }
490
491 /**
492 * Sets the range for the axis, if requested, sends an
493 * {@link AxisChangeEvent} to all registered listeners. As a side-effect,
494 * the auto-range flag is set to <code>false</code> (optional).
495 *
496 * @param range the range (<code>null</code> not permitted).
497 * @param turnOffAutoRange a flag that controls whether or not the auto
498 * range is turned off.
499 * @param notify a flag that controls whether or not listeners are
500 * notified.
501 */
502 public void setRange(Range range, boolean turnOffAutoRange,
503 boolean notify) {
504 if (range == null) {
505 throw new IllegalArgumentException("Null 'range' argument.");
506 }
507 // usually the range will be a DateRange, but if it isn't do a
508 // conversion...
509 if (!(range instanceof DateRange)) {
510 range = new DateRange(range);
511 }
512 super.setRange(range, turnOffAutoRange, notify);
513 }
514
515 /**
516 * Sets the axis range and sends an {@link AxisChangeEvent} to all
517 * registered listeners.
518 *
519 * @param lower the lower bound for the axis.
520 * @param upper the upper bound for the axis.
521 */
522 public void setRange(Date lower, Date upper) {
523 if (lower.getTime() >= upper.getTime()) {
524 throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
525 }
526 setRange(new DateRange(lower, upper));
527 }
528
529 /**
530 * Sets the axis range and sends an {@link AxisChangeEvent} to all
531 * registered listeners.
532 *
533 * @param lower the lower bound for the axis.
534 * @param upper the upper bound for the axis.
535 */
536 public void setRange(double lower, double upper) {
537 if (lower >= upper) {
538 throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
539 }
540 setRange(new DateRange(lower, upper));
541 }
542
543 /**
544 * Returns the earliest date visible on the axis.
545 *
546 * @return The date.
547 *
548 * @see #setMinimumDate(Date)
549 * @see #getMaximumDate()
550 */
551 public Date getMinimumDate() {
552 Date result = null;
553 Range range = getRange();
554 if (range instanceof DateRange) {
555 DateRange r = (DateRange) range;
556 result = r.getLowerDate();
557 }
558 else {
559 result = new Date((long) range.getLowerBound());
560 }
561 return result;
562 }
563
564 /**
565 * Sets the minimum date visible on the axis and sends an
566 * {@link AxisChangeEvent} to all registered listeners. If
567 * <code>date</code> is on or after the current maximum date for
568 * the axis, the maximum date will be shifted to preserve the current
569 * length of the axis.
570 *
571 * @param date the date (<code>null</code> not permitted).
572 *
573 * @see #getMinimumDate()
574 * @see #setMaximumDate(Date)
575 */
576 public void setMinimumDate(Date date) {
577 if (date == null) {
578 throw new IllegalArgumentException("Null 'date' argument.");
579 }
580 // check the new minimum date relative to the current maximum date
581 Date maxDate = getMaximumDate();
582 long maxMillis = maxDate.getTime();
583 long newMinMillis = date.getTime();
584 if (maxMillis <= newMinMillis) {
585 Date oldMin = getMinimumDate();
586 long length = maxMillis - oldMin.getTime();
587 maxDate = new Date(newMinMillis + length);
588 }
589 setRange(new DateRange(date, maxDate), true, false);
590 notifyListeners(new AxisChangeEvent(this));
591 }
592
593 /**
594 * Returns the latest date visible on the axis.
595 *
596 * @return The date.
597 *
598 * @see #setMaximumDate(Date)
599 * @see #getMinimumDate()
600 */
601 public Date getMaximumDate() {
602 Date result = null;
603 Range range = getRange();
604 if (range instanceof DateRange) {
605 DateRange r = (DateRange) range;
606 result = r.getUpperDate();
607 }
608 else {
609 result = new Date((long) range.getUpperBound());
610 }
611 return result;
612 }
613
614 /**
615 * Sets the maximum date visible on the axis and sends an
616 * {@link AxisChangeEvent} to all registered listeners. If
617 * <code>maximumDate</code> is on or before the current minimum date for
618 * the axis, the minimum date will be shifted to preserve the current
619 * length of the axis.
620 *
621 * @param maximumDate the date (<code>null</code> not permitted).
622 *
623 * @see #getMinimumDate()
624 * @see #setMinimumDate(Date)
625 */
626 public void setMaximumDate(Date maximumDate) {
627 if (maximumDate == null) {
628 throw new IllegalArgumentException("Null 'maximumDate' argument.");
629 }
630 // check the new maximum date relative to the current minimum date
631 Date minDate = getMinimumDate();
632 long minMillis = minDate.getTime();
633 long newMaxMillis = maximumDate.getTime();
634 if (minMillis >= newMaxMillis) {
635 Date oldMax = getMaximumDate();
636 long length = oldMax.getTime() - minMillis;
637 minDate = new Date(newMaxMillis - length);
638 }
639 setRange(new DateRange(minDate, maximumDate), true, false);
640 notifyListeners(new AxisChangeEvent(this));
641 }
642
643 /**
644 * Returns the tick mark position (start, middle or end of the time period).
645 *
646 * @return The position (never <code>null</code>).
647 */
648 public DateTickMarkPosition getTickMarkPosition() {
649 return this.tickMarkPosition;
650 }
651
652 /**
653 * Sets the tick mark position (start, middle or end of the time period)
654 * and sends an {@link AxisChangeEvent} to all registered listeners.
655 *
656 * @param position the position (<code>null</code> not permitted).
657 */
658 public void setTickMarkPosition(DateTickMarkPosition position) {
659 if (position == null) {
660 throw new IllegalArgumentException("Null 'position' argument.");
661 }
662 this.tickMarkPosition = position;
663 notifyListeners(new AxisChangeEvent(this));
664 }
665
666 /**
667 * Configures the axis to work with the specified plot. If the axis has
668 * auto-scaling, then sets the maximum and minimum values.
669 */
670 public void configure() {
671 if (isAutoRange()) {
672 autoAdjustRange();
673 }
674 }
675
676 /**
677 * Returns <code>true</code> if the axis hides this value, and
678 * <code>false</code> otherwise.
679 *
680 * @param millis the data value.
681 *
682 * @return A value.
683 */
684 public boolean isHiddenValue(long millis) {
685 return (!this.timeline.containsDomainValue(new Date(millis)));
686 }
687
688 /**
689 * Translates the data value to the display coordinates (Java 2D User Space)
690 * of the chart.
691 *
692 * @param value the date to be plotted.
693 * @param area the rectangle (in Java2D space) where the data is to be
694 * plotted.
695 * @param edge the axis location.
696 *
697 * @return The coordinate corresponding to the supplied data value.
698 */
699 public double valueToJava2D(double value, Rectangle2D area,
700 RectangleEdge edge) {
701
702 value = this.timeline.toTimelineValue((long) value);
703
704 DateRange range = (DateRange) getRange();
705 double axisMin = this.timeline.toTimelineValue(range.getLowerDate());
706 double axisMax = this.timeline.toTimelineValue(range.getUpperDate());
707 double result = 0.0;
708 if (RectangleEdge.isTopOrBottom(edge)) {
709 double minX = area.getX();
710 double maxX = area.getMaxX();
711 if (isInverted()) {
712 result = maxX + ((value - axisMin) / (axisMax - axisMin))
713 * (minX - maxX);
714 }
715 else {
716 result = minX + ((value - axisMin) / (axisMax - axisMin))
717 * (maxX - minX);
718 }
719 }
720 else if (RectangleEdge.isLeftOrRight(edge)) {
721 double minY = area.getMinY();
722 double maxY = area.getMaxY();
723 if (isInverted()) {
724 result = minY + (((value - axisMin) / (axisMax - axisMin))
725 * (maxY - minY));
726 }
727 else {
728 result = maxY - (((value - axisMin) / (axisMax - axisMin))
729 * (maxY - minY));
730 }
731 }
732 return result;
733
734 }
735
736 /**
737 * Translates a date to Java2D coordinates, based on the range displayed by
738 * this axis for the specified data area.
739 *
740 * @param date the date.
741 * @param area the rectangle (in Java2D space) where the data is to be
742 * plotted.
743 * @param edge the axis location.
744 *
745 * @return The coordinate corresponding to the supplied date.
746 */
747 public double dateToJava2D(Date date, Rectangle2D area,
748 RectangleEdge edge) {
749 double value = date.getTime();
750 return valueToJava2D(value, area, edge);
751 }
752
753 /**
754 * Translates a Java2D coordinate into the corresponding data value. To
755 * perform this translation, you need to know the area used for plotting
756 * data, and which edge the axis is located on.
757 *
758 * @param java2DValue the coordinate in Java2D space.
759 * @param area the rectangle (in Java2D space) where the data is to be
760 * plotted.
761 * @param edge the axis location.
762 *
763 * @return A data value.
764 */
765 public double java2DToValue(double java2DValue, Rectangle2D area,
766 RectangleEdge edge) {
767
768 DateRange range = (DateRange) getRange();
769 double axisMin = this.timeline.toTimelineValue(range.getLowerDate());
770 double axisMax = this.timeline.toTimelineValue(range.getUpperDate());
771
772 double min = 0.0;
773 double max = 0.0;
774 if (RectangleEdge.isTopOrBottom(edge)) {
775 min = area.getX();
776 max = area.getMaxX();
777 }
778 else if (RectangleEdge.isLeftOrRight(edge)) {
779 min = area.getMaxY();
780 max = area.getY();
781 }
782
783 double result;
784 if (isInverted()) {
785 result = axisMax - ((java2DValue - min) / (max - min)
786 * (axisMax - axisMin));
787 }
788 else {
789 result = axisMin + ((java2DValue - min) / (max - min)
790 * (axisMax - axisMin));
791 }
792
793 return this.timeline.toMillisecond((long) result);
794 }
795
796 /**
797 * Calculates the value of the lowest visible tick on the axis.
798 *
799 * @param unit date unit to use.
800 *
801 * @return The value of the lowest visible tick on the axis.
802 */
803 public Date calculateLowestVisibleTickValue(DateTickUnit unit) {
804 return nextStandardDate(getMinimumDate(), unit);
805 }
806
807 /**
808 * Calculates the value of the highest visible tick on the axis.
809 *
810 * @param unit date unit to use.
811 *
812 * @return The value of the highest visible tick on the axis.
813 */
814 public Date calculateHighestVisibleTickValue(DateTickUnit unit) {
815 return previousStandardDate(getMaximumDate(), unit);
816 }
817
818 /**
819 * Returns the previous "standard" date, for a given date and tick unit.
820 *
821 * @param date the reference date.
822 * @param unit the tick unit.
823 *
824 * @return The previous "standard" date.
825 */
826 protected Date previousStandardDate(Date date, DateTickUnit unit) {
827
828 int milliseconds;
829 int seconds;
830 int minutes;
831 int hours;
832 int days;
833 int months;
834 int years;
835
836 Calendar calendar = Calendar.getInstance(this.timeZone);
837 calendar.setTime(date);
838 int count = unit.getCount();
839 int current = calendar.get(unit.getCalendarField());
840 int value = count * (current / count);
841
842 switch (unit.getUnit()) {
843
844 case (DateTickUnit.MILLISECOND) :
845 years = calendar.get(Calendar.YEAR);
846 months = calendar.get(Calendar.MONTH);
847 days = calendar.get(Calendar.DATE);
848 hours = calendar.get(Calendar.HOUR_OF_DAY);
849 minutes = calendar.get(Calendar.MINUTE);
850 seconds = calendar.get(Calendar.SECOND);
851 calendar.set(years, months, days, hours, minutes, seconds);
852 calendar.set(Calendar.MILLISECOND, value);
853 Date mm = calendar.getTime();
854 if (mm.getTime() >= date.getTime()) {
855 calendar.set(Calendar.MILLISECOND, value - 1);
856 mm = calendar.getTime();
857 }
858 return calendar.getTime();
859
860 case (DateTickUnit.SECOND) :
861 years = calendar.get(Calendar.YEAR);
862 months = calendar.get(Calendar.MONTH);
863 days = calendar.get(Calendar.DATE);
864 hours = calendar.get(Calendar.HOUR_OF_DAY);
865 minutes = calendar.get(Calendar.MINUTE);
866 if (this.tickMarkPosition == DateTickMarkPosition.START) {
867 milliseconds = 0;
868 }
869 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
870 milliseconds = 500;
871 }
872 else {
873 milliseconds = 999;
874 }
875 calendar.set(Calendar.MILLISECOND, milliseconds);
876 calendar.set(years, months, days, hours, minutes, value);
877 Date dd = calendar.getTime();
878 if (dd.getTime() >= date.getTime()) {
879 calendar.set(Calendar.SECOND, value - 1);
880 dd = calendar.getTime();
881 }
882 return calendar.getTime();
883
884 case (DateTickUnit.MINUTE) :
885 years = calendar.get(Calendar.YEAR);
886 months = calendar.get(Calendar.MONTH);
887 days = calendar.get(Calendar.DATE);
888 hours = calendar.get(Calendar.HOUR_OF_DAY);
889 if (this.tickMarkPosition == DateTickMarkPosition.START) {
890 seconds = 0;
891 }
892 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
893 seconds = 30;
894 }
895 else {
896 seconds = 59;
897 }
898 calendar.clear(Calendar.MILLISECOND);
899 calendar.set(years, months, days, hours, value, seconds);
900 Date d0 = calendar.getTime();
901 if (d0.getTime() >= date.getTime()) {
902 calendar.set(Calendar.MINUTE, value - 1);
903 d0 = calendar.getTime();
904 }
905 return d0;
906
907 case (DateTickUnit.HOUR) :
908 years = calendar.get(Calendar.YEAR);
909 months = calendar.get(Calendar.MONTH);
910 days = calendar.get(Calendar.DATE);
911 if (this.tickMarkPosition == DateTickMarkPosition.START) {
912 minutes = 0;
913 seconds = 0;
914 }
915 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
916 minutes = 30;
917 seconds = 0;
918 }
919 else {
920 minutes = 59;
921 seconds = 59;
922 }
923 calendar.clear(Calendar.MILLISECOND);
924 calendar.set(years, months, days, value, minutes, seconds);
925 Date d1 = calendar.getTime();
926 if (d1.getTime() >= date.getTime()) {
927 calendar.set(Calendar.HOUR_OF_DAY, value - 1);
928 d1 = calendar.getTime();
929 }
930 return d1;
931
932 case (DateTickUnit.DAY) :
933 years = calendar.get(Calendar.YEAR);
934 months = calendar.get(Calendar.MONTH);
935 if (this.tickMarkPosition == DateTickMarkPosition.START) {
936 hours = 0;
937 minutes = 0;
938 seconds = 0;
939 }
940 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
941 hours = 12;
942 minutes = 0;
943 seconds = 0;
944 }
945 else {
946 hours = 23;
947 minutes = 59;
948 seconds = 59;
949 }
950 calendar.clear(Calendar.MILLISECOND);
951 calendar.set(years, months, value, hours, 0, 0);
952 // long result = calendar.getTimeInMillis();
953 // won't work with JDK 1.3
954 Date d2 = calendar.getTime();
955 if (d2.getTime() >= date.getTime()) {
956 calendar.set(Calendar.DATE, value - 1);
957 d2 = calendar.getTime();
958 }
959 return d2;
960
961 case (DateTickUnit.MONTH) :
962 years = calendar.get(Calendar.YEAR);
963 calendar.clear(Calendar.MILLISECOND);
964 calendar.set(years, value, 1, 0, 0, 0);
965 Month month = new Month(calendar.getTime(), this.timeZone);
966 Date standardDate = calculateDateForPosition(
967 month, this.tickMarkPosition);
968 long millis = standardDate.getTime();
969 if (millis >= date.getTime()) {
970 month = (Month) month.previous();
971 standardDate = calculateDateForPosition(
972 month, this.tickMarkPosition);
973 }
974 return standardDate;
975
976 case(DateTickUnit.YEAR) :
977 if (this.tickMarkPosition == DateTickMarkPosition.START) {
978 months = 0;
979 days = 1;
980 }
981 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
982 months = 6;
983 days = 1;
984 }
985 else {
986 months = 11;
987 days = 31;
988 }
989 calendar.clear(Calendar.MILLISECOND);
990 calendar.set(value, months, days, 0, 0, 0);
991 Date d3 = calendar.getTime();
992 if (d3.getTime() >= date.getTime()) {
993 calendar.set(Calendar.YEAR, value - 1);
994 d3 = calendar.getTime();
995 }
996 return d3;
997
998 default: return null;
999
1000 }
1001
1002 }
1003
1004 /**
1005 * Returns a {@link java.util.Date} corresponding to the specified position
1006 * within a {@link RegularTimePeriod}.
1007 *
1008 * @param period the period.
1009 * @param position the position (<code>null</code> not permitted).
1010 *
1011 * @return A date.
1012 */
1013 private Date calculateDateForPosition(RegularTimePeriod period,
1014 DateTickMarkPosition position) {
1015
1016 if (position == null) {
1017 throw new IllegalArgumentException("Null 'position' argument.");
1018 }
1019 Date result = null;
1020 if (position == DateTickMarkPosition.START) {
1021 result = new Date(period.getFirstMillisecond());
1022 }
1023 else if (position == DateTickMarkPosition.MIDDLE) {
1024 result = new Date(period.getMiddleMillisecond());
1025 }
1026 else if (position == DateTickMarkPosition.END) {
1027 result = new Date(period.getLastMillisecond());
1028 }
1029 return result;
1030
1031 }
1032
1033 /**
1034 * Returns the first "standard" date (based on the specified field and
1035 * units).
1036 *
1037 * @param date the reference date.
1038 * @param unit the date tick unit.
1039 *
1040 * @return The next "standard" date.
1041 */
1042 protected Date nextStandardDate(Date date, DateTickUnit unit) {
1043 Date previous = previousStandardDate(date, unit);
1044 Calendar calendar = Calendar.getInstance(this.timeZone);
1045 calendar.setTime(previous);
1046 calendar.add(unit.getCalendarField(), unit.getCount());
1047 return calendar.getTime();
1048 }
1049
1050 /**
1051 * Returns a collection of standard date tick units that uses the default
1052 * time zone. This collection will be used by default, but you are free
1053 * to create your own collection if you want to (see the
1054 * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited
1055 * from the {@link ValueAxis} class).
1056 *
1057 * @return A collection of standard date tick units.
1058 */
1059 public static TickUnitSource createStandardDateTickUnits() {
1060 return createStandardDateTickUnits(TimeZone.getDefault());
1061 }
1062
1063 /**
1064 * Returns a collection of standard date tick units. This collection will
1065 * be used by default, but you are free to create your own collection if
1066 * you want to (see the
1067 * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited
1068 * from the {@link ValueAxis} class).
1069 *
1070 * @param zone the time zone (<code>null</code> not permitted).
1071 *
1072 * @return A collection of standard date tick units.
1073 */
1074 public static TickUnitSource createStandardDateTickUnits(TimeZone zone) {
1075
1076 if (zone == null) {
1077 throw new IllegalArgumentException("Null 'zone' argument.");
1078 }
1079 TickUnits units = new TickUnits();
1080
1081 // date formatters
1082 DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS");
1083 DateFormat f2 = new SimpleDateFormat("HH:mm:ss");
1084 DateFormat f3 = new SimpleDateFormat("HH:mm");
1085 DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm");
1086 DateFormat f5 = new SimpleDateFormat("d-MMM");
1087 DateFormat f6 = new SimpleDateFormat("MMM-yyyy");
1088 DateFormat f7 = new SimpleDateFormat("yyyy");
1089
1090 f1.setTimeZone(zone);
1091 f2.setTimeZone(zone);
1092 f3.setTimeZone(zone);
1093 f4.setTimeZone(zone);
1094 f5.setTimeZone(zone);
1095 f6.setTimeZone(zone);
1096 f7.setTimeZone(zone);
1097
1098 // milliseconds
1099 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 1, f1));
1100 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 5,
1101 DateTickUnit.MILLISECOND, 1, f1));
1102 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 10,
1103 DateTickUnit.MILLISECOND, 1, f1));
1104 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 25,
1105 DateTickUnit.MILLISECOND, 5, f1));
1106 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 50,
1107 DateTickUnit.MILLISECOND, 10, f1));
1108 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 100,
1109 DateTickUnit.MILLISECOND, 10, f1));
1110 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 250,
1111 DateTickUnit.MILLISECOND, 10, f1));
1112 units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 500,
1113 DateTickUnit.MILLISECOND, 50, f1));
1114
1115 // seconds
1116 units.add(new DateTickUnit(DateTickUnit.SECOND, 1,
1117 DateTickUnit.MILLISECOND, 50, f2));
1118 units.add(new DateTickUnit(DateTickUnit.SECOND, 5,
1119 DateTickUnit.SECOND, 1, f2));
1120 units.add(new DateTickUnit(DateTickUnit.SECOND, 10,
1121 DateTickUnit.SECOND, 1, f2));
1122 units.add(new DateTickUnit(DateTickUnit.SECOND, 30,
1123 DateTickUnit.SECOND, 5, f2));
1124
1125 // minutes
1126 units.add(new DateTickUnit(DateTickUnit.MINUTE, 1,
1127 DateTickUnit.SECOND, 5, f3));
1128 units.add(new DateTickUnit(DateTickUnit.MINUTE, 2,
1129 DateTickUnit.SECOND, 10, f3));
1130 units.add(new DateTickUnit(DateTickUnit.MINUTE, 5,
1131 DateTickUnit.MINUTE, 1, f3));
1132 units.add(new DateTickUnit(DateTickUnit.MINUTE, 10,
1133 DateTickUnit.MINUTE, 1, f3));
1134 units.add(new DateTickUnit(DateTickUnit.MINUTE, 15,
1135 DateTickUnit.MINUTE, 5, f3));
1136 units.add(new DateTickUnit(DateTickUnit.MINUTE, 20,
1137 DateTickUnit.MINUTE, 5, f3));
1138 units.add(new DateTickUnit(DateTickUnit.MINUTE, 30,
1139 DateTickUnit.MINUTE, 5, f3));
1140
1141 // hours
1142 units.add(new DateTickUnit(DateTickUnit.HOUR, 1,
1143 DateTickUnit.MINUTE, 5, f3));
1144 units.add(new DateTickUnit(DateTickUnit.HOUR, 2,
1145 DateTickUnit.MINUTE, 10, f3));
1146 units.add(new DateTickUnit(DateTickUnit.HOUR, 4,
1147 DateTickUnit.MINUTE, 30, f3));
1148 units.add(new DateTickUnit(DateTickUnit.HOUR, 6,
1149 DateTickUnit.HOUR, 1, f3));
1150 units.add(new DateTickUnit(DateTickUnit.HOUR, 12,
1151 DateTickUnit.HOUR, 1, f4));
1152
1153 // days
1154 units.add(new DateTickUnit(DateTickUnit.DAY, 1,
1155 DateTickUnit.HOUR, 1, f5));
1156 units.add(new DateTickUnit(DateTickUnit.DAY, 2,
1157 DateTickUnit.HOUR, 1, f5));
1158 units.add(new DateTickUnit(DateTickUnit.DAY, 7,
1159 DateTickUnit.DAY, 1, f5));
1160 units.add(new DateTickUnit(DateTickUnit.DAY, 15,
1161 DateTickUnit.DAY, 1, f5));
1162
1163 // months
1164 units.add(new DateTickUnit(DateTickUnit.MONTH, 1,
1165 DateTickUnit.DAY, 1, f6));
1166 units.add(new DateTickUnit(DateTickUnit.MONTH, 2,
1167 DateTickUnit.DAY, 1, f6));
1168 units.add(new DateTickUnit(DateTickUnit.MONTH, 3,
1169 DateTickUnit.MONTH, 1, f6));
1170 units.add(new DateTickUnit(DateTickUnit.MONTH, 4,
1171 DateTickUnit.MONTH, 1, f6));
1172 units.add(new DateTickUnit(DateTickUnit.MONTH, 6,
1173 DateTickUnit.MONTH, 1, f6));
1174
1175 // years
1176 units.add(new DateTickUnit(DateTickUnit.YEAR, 1,
1177 DateTickUnit.MONTH, 1, f7));
1178 units.add(new DateTickUnit(DateTickUnit.YEAR, 2,
1179 DateTickUnit.MONTH, 3, f7));
1180 units.add(new DateTickUnit(DateTickUnit.YEAR, 5,
1181 DateTickUnit.YEAR, 1, f7));
1182 units.add(new DateTickUnit(DateTickUnit.YEAR, 10,
1183 DateTickUnit.YEAR, 1, f7));
1184 units.add(new DateTickUnit(DateTickUnit.YEAR, 25,
1185 DateTickUnit.YEAR, 5, f7));
1186 units.add(new DateTickUnit(DateTickUnit.YEAR, 50,
1187 DateTickUnit.YEAR, 10, f7));
1188 units.add(new DateTickUnit(DateTickUnit.YEAR, 100,
1189 DateTickUnit.YEAR, 20, f7));
1190
1191 return units;
1192
1193 }
1194
1195 /**
1196 * Rescales the axis to ensure that all data is visible.
1197 */
1198 protected void autoAdjustRange() {
1199
1200 Plot plot = getPlot();
1201
1202 if (plot == null) {
1203 return; // no plot, no data
1204 }
1205
1206 if (plot instanceof ValueAxisPlot) {
1207 ValueAxisPlot vap = (ValueAxisPlot) plot;
1208
1209 Range r = vap.getDataRange(this);
1210 if (r == null) {
1211 if (this.timeline instanceof SegmentedTimeline) {
1212 //Timeline hasn't method getStartTime()
1213 r = new DateRange((
1214 (SegmentedTimeline) this.timeline).getStartTime(),
1215 ((SegmentedTimeline) this.timeline).getStartTime()
1216 + 1);
1217 }
1218 else {
1219 r = new DateRange();
1220 }
1221 }
1222
1223 long upper = this.timeline.toTimelineValue(
1224 (long) r.getUpperBound());
1225 long lower;
1226 long fixedAutoRange = (long) getFixedAutoRange();
1227 if (fixedAutoRange > 0.0) {
1228 lower = upper - fixedAutoRange;
1229 }
1230 else {
1231 lower = this.timeline.toTimelineValue((long) r.getLowerBound());
1232 double range = upper - lower;
1233 long minRange = (long) getAutoRangeMinimumSize();
1234 if (range < minRange) {
1235 long expand = (long) (minRange - range) / 2;
1236 upper = upper + expand;
1237 lower = lower - expand;
1238 }
1239 upper = upper + (long) (range * getUpperMargin());
1240 lower = lower - (long) (range * getLowerMargin());
1241 }
1242
1243 upper = this.timeline.toMillisecond(upper);
1244 lower = this.timeline.toMillisecond(lower);
1245 DateRange dr = new DateRange(new Date(lower), new Date(upper));
1246 setRange(dr, false, false);
1247 }
1248
1249 }
1250
1251 /**
1252 * Selects an appropriate tick value for the axis. The strategy is to
1253 * display as many ticks as possible (selected from an array of 'standard'
1254 * tick units) without the labels overlapping.
1255 *
1256 * @param g2 the graphics device.
1257 * @param dataArea the area defined by the axes.
1258 * @param edge the axis location.
1259 */
1260 protected void selectAutoTickUnit(Graphics2D g2,
1261 Rectangle2D dataArea,
1262 RectangleEdge edge) {
1263
1264 if (RectangleEdge.isTopOrBottom(edge)) {
1265 selectHorizontalAutoTickUnit(g2, dataArea, edge);
1266 }
1267 else if (RectangleEdge.isLeftOrRight(edge)) {
1268 selectVerticalAutoTickUnit(g2, dataArea, edge);
1269 }
1270
1271 }
1272
1273 /**
1274 * Selects an appropriate tick size for the axis. The strategy is to
1275 * display as many ticks as possible (selected from a collection of
1276 * 'standard' tick units) without the labels overlapping.
1277 *
1278 * @param g2 the graphics device.
1279 * @param dataArea the area defined by the axes.
1280 * @param edge the axis location.
1281 */
1282 protected void selectHorizontalAutoTickUnit(Graphics2D g2,
1283 Rectangle2D dataArea,
1284 RectangleEdge edge) {
1285
1286 long shift = 0;
1287 if (this.timeline instanceof SegmentedTimeline) {
1288 shift = ((SegmentedTimeline) this.timeline).getStartTime();
1289 }
1290 double zero = valueToJava2D(shift + 0.0, dataArea, edge);
1291 double tickLabelWidth
1292 = estimateMaximumTickLabelWidth(g2, getTickUnit());
1293
1294 // start with the current tick unit...
1295 TickUnitSource tickUnits = getStandardTickUnits();
1296 TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit());
1297 double x1 = valueToJava2D(shift + unit1.getSize(), dataArea, edge);
1298 double unit1Width = Math.abs(x1 - zero);
1299
1300 // then extrapolate...
1301 double guess = (tickLabelWidth / unit1Width) * unit1.getSize();
1302 DateTickUnit unit2 = (DateTickUnit) tickUnits.getCeilingTickUnit(guess);
1303 double x2 = valueToJava2D(shift + unit2.getSize(), dataArea, edge);
1304 double unit2Width = Math.abs(x2 - zero);
1305 tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit2);
1306 if (tickLabelWidth > unit2Width) {
1307 unit2 = (DateTickUnit) tickUnits.getLargerTickUnit(unit2);
1308 }
1309 setTickUnit(unit2, false, false);
1310 }
1311
1312 /**
1313 * Selects an appropriate tick size for the axis. The strategy is to
1314 * display as many ticks as possible (selected from a collection of
1315 * 'standard' tick units) without the labels overlapping.
1316 *
1317 * @param g2 the graphics device.
1318 * @param dataArea the area in which the plot should be drawn.
1319 * @param edge the axis location.
1320 */
1321 protected void selectVerticalAutoTickUnit(Graphics2D g2,
1322 Rectangle2D dataArea,
1323 RectangleEdge edge) {
1324
1325 // start with the current tick unit...
1326 TickUnitSource tickUnits = getStandardTickUnits();
1327 double zero = valueToJava2D(0.0, dataArea, edge);
1328
1329 // start with a unit that is at least 1/10th of the axis length
1330 double estimate1 = getRange().getLength() / 10.0;
1331 DateTickUnit candidate1
1332 = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate1);
1333 double labelHeight1 = estimateMaximumTickLabelHeight(g2, candidate1);
1334 double y1 = valueToJava2D(candidate1.getSize(), dataArea, edge);
1335 double candidate1UnitHeight = Math.abs(y1 - zero);
1336
1337 // now extrapolate based on label height and unit height...
1338 double estimate2
1339 = (labelHeight1 / candidate1UnitHeight) * candidate1.getSize();
1340 DateTickUnit candidate2
1341 = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate2);
1342 double labelHeight2 = estimateMaximumTickLabelHeight(g2, candidate2);
1343 double y2 = valueToJava2D(candidate2.getSize(), dataArea, edge);
1344 double unit2Height = Math.abs(y2 - zero);
1345
1346 // make final selection...
1347 DateTickUnit finalUnit;
1348 if (labelHeight2 < unit2Height) {
1349 finalUnit = candidate2;
1350 }
1351 else {
1352 finalUnit = (DateTickUnit) tickUnits.getLargerTickUnit(candidate2);
1353 }
1354 setTickUnit(finalUnit, false, false);
1355
1356 }
1357
1358 /**
1359 * Estimates the maximum width of the tick labels, assuming the specified
1360 * tick unit is used.
1361 * <P>
1362 * Rather than computing the string bounds of every tick on the axis, we
1363 * just look at two values: the lower bound and the upper bound for the
1364 * axis. These two values will usually be representative.
1365 *
1366 * @param g2 the graphics device.
1367 * @param unit the tick unit to use for calculation.
1368 *
1369 * @return The estimated maximum width of the tick labels.
1370 */
1371 private double estimateMaximumTickLabelWidth(Graphics2D g2,
1372 DateTickUnit unit) {
1373
1374 RectangleInsets tickLabelInsets = getTickLabelInsets();
1375 double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight();
1376
1377 Font tickLabelFont = getTickLabelFont();
1378 FontRenderContext frc = g2.getFontRenderContext();
1379 LineMetrics lm = tic