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