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