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