KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > jfree > chart > axis > CyclicNumberAxis


1 /* ===========================================================
2  * JFreeChart : a free chart library for the Java(tm) platform
3  * ===========================================================
4  *
5  * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
6  *
7  * Project Info: http://www.jfree.org/jfreechart/index.html
8  *
9  * This library is free software; you can redistribute it and/or modify it
10  * under the terms of the GNU Lesser General Public License as published by
11  * the Free Software Foundation; either version 2.1 of the License, or
12  * (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful, but
15  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
17  * License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public License
20  * along with this library; if not, write to the Free Software Foundation,
21  * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22  *
23  * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
24  * in the United States and other countries.]
25  *
26  * ---------------------
27  * CyclicNumberAxis.java
28  * ---------------------
29  * (C) Copyright 2003, 2004, by Nicolas Brodu and Contributors.
30  *
31  * Original Author: Nicolas Brodu;
32  * Contributor(s): David Gilbert (for Object Refinery Limited);
33  *
34  * $Id: CyclicNumberAxis.java,v 1.10 2005/05/06 14:55:43 mungady Exp $
35  *
36  * Changes
37  * -------
38  * 19-Nov-2003 : Initial import to JFreeChart from the JSynoptic project (NB);
39  * 16-Mar-2004 : Added plotState to draw() method (DG);
40  * 07-Apr-2004 : Modifed text bounds calculation (DG);
41  * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant
42  * argument in selectAutoTickUnit() (DG);
43  * 22-Apr-2005 : Renamed refreshHorizontalTicks() --> refreshTicksHorizontal
44  * (for consistency with other classes) and removed unused
45  * parameters (DG);
46  *
47  */

48
49 package org.jfree.chart.axis;
50
51 import java.awt.BasicStroke JavaDoc;
52 import java.awt.Color JavaDoc;
53 import java.awt.Font JavaDoc;
54 import java.awt.FontMetrics JavaDoc;
55 import java.awt.Graphics2D JavaDoc;
56 import java.awt.Paint JavaDoc;
57 import java.awt.Stroke JavaDoc;
58 import java.awt.geom.Line2D JavaDoc;
59 import java.awt.geom.Rectangle2D JavaDoc;
60 import java.io.IOException JavaDoc;
61 import java.io.ObjectInputStream JavaDoc;
62 import java.io.ObjectOutputStream JavaDoc;
63 import java.text.NumberFormat JavaDoc;
64 import java.util.List JavaDoc;
65
66 import org.jfree.chart.plot.Plot;
67 import org.jfree.chart.plot.PlotRenderingInfo;
68 import org.jfree.data.Range;
69 import org.jfree.io.SerialUtilities;
70 import org.jfree.text.TextUtilities;
71 import org.jfree.ui.RectangleEdge;
72 import org.jfree.ui.TextAnchor;
73 import org.jfree.util.ObjectUtilities;
74
75 /**
76 This class extends NumberAxis and handles cycling.
77  
78 Traditional representation of data in the range x0..x1
79 <pre>
80 |-------------------------|
81 x0 x1
82 </pre>
83
84 Here, the range bounds are at the axis extremities.
85 With cyclic axis, however, the time is split in
86 "cycles", or "time frames", or the same duration : the period.
87
88 A cycle axis cannot by definition handle a larger interval
89 than the period : <pre>x1 - x0 >= period</pre>. Thus, at most a full
90 period can be represented with such an axis.
91
92 The cycle bound is the number between x0 and x1 which marks
93 the beginning of new time frame:
94 <pre>
95 |---------------------|----------------------------|
96 x0 cb x1
97 <---previous cycle---><-------current cycle-------->
98 </pre>
99
100 It is actually a multiple of the period, plus optionally
101 a start offset: <pre>cb = n * period + offset</pre>
102
103 Thus, by definition, two consecutive cycle bounds
104 period apart, which is precisely why it is called a
105 period.
106
107 The visual representation of a cyclic axis is like that:
108 <pre>
109 |----------------------------|---------------------|
110 cb x1|x0 cb
111 <-------current cycle--------><---previous cycle--->
112 </pre>
113
114 The cycle bound is at the axis ends, then current
115 cycle is shown, then the last cycle. When using
116 dynamic data, the visual effect is the current cycle
117 erases the last cycle as x grows. Then, the next cycle
118 bound is reached, and the process starts over, erasing
119 the previous cycle.
120
121 A Cyclic item renderer is provided to do exactly this.
122
123  */

124 public class CyclicNumberAxis extends NumberAxis {
125
126     /** The default axis line stroke. */
127     public static Stroke JavaDoc DEFAULT_ADVANCE_LINE_STROKE = new BasicStroke JavaDoc(1.0f);
128     
129     /** The default axis line paint. */
130     public static final Paint JavaDoc DEFAULT_ADVANCE_LINE_PAINT = Color.gray;
131     
132     /** The offset. */
133     protected double offset;
134     
135     /** The period.*/
136     protected double period;
137     
138     /** ??. */
139     protected boolean boundMappedToLastCycle;
140     
141     /** A flag that controls whether or not the advance line is visible. */
142     protected boolean advanceLineVisible;
143
144     /** The advance line stroke. */
145     protected transient Stroke JavaDoc advanceLineStroke = DEFAULT_ADVANCE_LINE_STROKE;
146     
147     /** The advance line paint. */
148     protected transient Paint JavaDoc advanceLinePaint;
149     
150     private transient boolean internalMarkerWhenTicksOverlap;
151     private transient Tick internalMarkerCycleBoundTick;
152     
153     /**
154      * Creates a CycleNumberAxis with the given period.
155      *
156      * @param period the period.
157      */

158     public CyclicNumberAxis(double period) {
159         this(period, 0.0);
160     }
161
162     /**
163      * Creates a CycleNumberAxis with the given period and offset.
164      *
165      * @param period the period.
166      * @param offset the offset.
167      */

168     public CyclicNumberAxis(double period, double offset) {
169         this(period, offset, null);
170     }
171
172     /**
173      * Creates a named CycleNumberAxis with the given period.
174      *
175      * @param period the period.
176      * @param label the label.
177      */

178     public CyclicNumberAxis(double period, String JavaDoc label) {
179         this(0, period, label);
180     }
181     
182     /**
183      * Creates a named CycleNumberAxis with the given period and offset.
184      *
185      * @param period the period.
186      * @param offset the offset.
187      * @param label the label.
188      */

189     public CyclicNumberAxis(double period, double offset, String JavaDoc label) {
190         super(label);
191         this.period = period;
192         this.offset = offset;
193         setFixedAutoRange(period);
194         this.advanceLineVisible = true;
195         this.advanceLinePaint = DEFAULT_ADVANCE_LINE_PAINT;
196     }
197         
198     /**
199      * The advance line is the line drawn at the limit of the current cycle,
200      * when erasing the previous cycle.
201      *
202      * @return A boolean.
203      */

204     public boolean isAdvanceLineVisible() {
205         return this.advanceLineVisible;
206     }
207     
208     /**
209      * The advance line is the line drawn at the limit of the current cycle,
210      * when erasing the previous cycle.
211      *
212      * @param visible the flag.
213      */

214     public void setAdvanceLineVisible(boolean visible) {
215         this.advanceLineVisible = visible;
216     }
217     
218     /**
219      * The advance line is the line drawn at the limit of the current cycle,
220      * when erasing the previous cycle.
221      *
222      * @return The paint (never <code>null</code>).
223      */

224     public Paint JavaDoc getAdvanceLinePaint() {
225         return this.advanceLinePaint;
226     }
227
228     /**
229      * The advance line is the line drawn at the limit of the current cycle,
230      * when erasing the previous cycle.
231      *
232      * @param paint the paint (<code>null</code> not permitted).
233      */

234     public void setAdvanceLinePaint(Paint JavaDoc paint) {
235         if (paint == null) {
236             throw new IllegalArgumentException JavaDoc("Null 'paint' argument.");
237         }
238         this.advanceLinePaint = paint;
239     }
240     
241     /**
242      * The advance line is the line drawn at the limit of the current cycle,
243      * when erasing the previous cycle.
244      *
245      * @return The stroke (never <code>null</code>).
246      */

247     public Stroke JavaDoc getAdvanceLineStroke() {
248         return this.advanceLineStroke;
249     }
250     /**
251      * The advance line is the line drawn at the limit of the current cycle,
252      * when erasing the previous cycle.
253      *
254      * @param stroke the stroke (<code>null</code> not permitted).
255      */

256     public void setAdvanceLineStroke(Stroke JavaDoc stroke) {
257         if (stroke == null) {
258             throw new IllegalArgumentException JavaDoc("Null 'stroke' argument.");
259         }
260         this.advanceLineStroke = stroke;
261     }
262     
263     /**
264      * The cycle bound can be associated either with the current or with the
265      * last cycle. It's up to the user's choice to decide which, as this is
266      * just a convention. By default, the cycle bound is mapped to the current
267      * cycle.
268      * <br>
269      * Note that this has no effect on visual appearance, as the cycle bound is
270      * mapped successively for both axis ends. Use this function for correct
271      * results in translateValueToJava2D.
272      *
273      * @return <code>true</code> if the cycle bound is mapped to the last
274      * cycle, <code>false</code> if it is bound to the current cycle
275      * (default)
276      */

277     public boolean isBoundMappedToLastCycle() {
278         return this.boundMappedToLastCycle;
279     }
280     
281     /**
282      * The cycle bound can be associated either with the current or with the
283      * last cycle. It's up to the user's choice to decide which, as this is
284      * just a convention. By default, the cycle bound is mapped to the current
285      * cycle.
286      * <br>
287      * Note that this has no effect on visual appearance, as the cycle bound is
288      * mapped successively for both axis ends. Use this function for correct
289      * results in valueToJava2D.
290      *
291      * @param boundMappedToLastCycle Set it to true to map the cycle bound to
292      * the last cycle.
293      */

294     public void setBoundMappedToLastCycle(boolean boundMappedToLastCycle) {
295         this.boundMappedToLastCycle = boundMappedToLastCycle;
296     }
297     
298     /**
299      * Selects a tick unit when the axis is displayed horizontally.
300      *
301      * @param g2 the graphics device.
302      * @param drawArea the drawing area.
303      * @param dataArea the data area.
304      * @param edge the side of the rectangle on which the axis is displayed.
305      */

306     protected void selectHorizontalAutoTickUnit(Graphics2D JavaDoc g2,
307                                                 Rectangle2D JavaDoc drawArea,
308                                                 Rectangle2D JavaDoc dataArea,
309                                                 RectangleEdge edge) {
310
311         double tickLabelWidth
312             = estimateMaximumTickLabelWidth(g2, getTickUnit());
313         
314         // Compute number of labels
315
double n = getRange().getLength()
316                    * tickLabelWidth / dataArea.getWidth();
317
318         setTickUnit(
319             (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n),
320             false, false
321         );
322         
323      }
324
325     /**
326      * Selects a tick unit when the axis is displayed vertically.
327      *
328      * @param g2 the graphics device.
329      * @param drawArea the drawing area.
330      * @param dataArea the data area.
331      * @param edge the side of the rectangle on which the axis is displayed.
332      */

333     protected void selectVerticalAutoTickUnit(Graphics2D JavaDoc g2,
334                                                 Rectangle2D JavaDoc drawArea,
335                                                 Rectangle2D JavaDoc dataArea,
336                                                 RectangleEdge edge) {
337
338         double tickLabelWidth
339             = estimateMaximumTickLabelWidth(g2, getTickUnit());
340
341         // Compute number of labels
342
double n = getRange().getLength()
343                    * tickLabelWidth / dataArea.getHeight();
344
345         setTickUnit(
346             (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n),
347             false, false
348         );
349         
350      }
351
352     /**
353      * A special Number tick that also hold information about the cycle bound
354      * mapping for this tick. This is especially useful for having a tick at
355      * each axis end with the cycle bound value. See also
356      * isBoundMappedToLastCycle()
357      */

358     protected static class CycleBoundTick extends NumberTick {
359         
360         /** Map to last cycle. */
361         public boolean mapToLastCycle;
362         
363         /**
364          * Creates a new tick.
365          *
366          * @param mapToLastCycle map to last cycle?
367          * @param number the number.
368          * @param label the label.
369          * @param textAnchor the text anchor.
370          * @param rotationAnchor the rotation anchor.
371          * @param angle the rotation angle.
372          */

373         public CycleBoundTick(boolean mapToLastCycle, Number JavaDoc number,
374                               String JavaDoc label, TextAnchor textAnchor,
375                               TextAnchor rotationAnchor, double angle) {
376             super(number, label, textAnchor, rotationAnchor, angle);
377             this.mapToLastCycle = mapToLastCycle;
378         }
379     }
380     
381     /**
382      * Calculates the anchor point for a tick.
383      *
384      * @param tick the tick.
385      * @param cursor the cursor.
386      * @param dataArea the data area.
387      * @param edge the side on which the axis is displayed.
388      *
389      * @return The anchor point.
390      */

391     protected float[] calculateAnchorPoint(ValueTick tick, double cursor,
392                                            Rectangle2D JavaDoc dataArea,
393                                            RectangleEdge edge) {
394         if (tick instanceof CycleBoundTick) {
395             boolean mapsav = this.boundMappedToLastCycle;
396             this.boundMappedToLastCycle
397                 = ((CycleBoundTick) tick).mapToLastCycle;
398             float[] ret = super.calculateAnchorPoint(
399                 tick, cursor, dataArea, edge
400             );
401             this.boundMappedToLastCycle = mapsav;
402             return ret;
403         }
404         return super.calculateAnchorPoint(tick, cursor, dataArea, edge);
405     }
406     
407     
408     
409     /**
410      * Builds a list of ticks for the axis. This method is called when the
411      * axis is at the top or bottom of the chart (so the axis is "horizontal").
412      *
413      * @param g2 the graphics device.
414      * @param dataArea the data area.
415      * @param edge the edge.
416      *
417      * @return A list of ticks.
418      */

419     protected List JavaDoc refreshTicksHorizontal(Graphics2D JavaDoc g2,
420                                           Rectangle2D JavaDoc dataArea,
421                                           RectangleEdge edge) {
422
423         List JavaDoc result = new java.util.ArrayList JavaDoc();
424
425         Font JavaDoc tickLabelFont = getTickLabelFont();
426         g2.setFont(tickLabelFont);
427         
428         if (isAutoTickUnitSelection()) {
429             selectAutoTickUnit(g2, dataArea, edge);
430         }
431
432         double unit = getTickUnit().getSize();
433         double cycleBound = getCycleBound();
434         double currentTickValue = Math.ceil(cycleBound / unit) * unit;
435         double upperValue = getRange().getUpperBound();
436         boolean cycled = false;
437
438         boolean boundMapping = this.boundMappedToLastCycle;
439         this.boundMappedToLastCycle = false;
440         
441         CycleBoundTick lastTick = null;
442         float lastX = 0.0f;
443
444         if (upperValue == cycleBound) {
445             currentTickValue = calculateLowestVisibleTickValue();
446             cycled = true;
447             this.boundMappedToLastCycle = true;
448         }
449         
450         while (currentTickValue <= upperValue) {
451             
452             // Cycle when necessary
453
boolean cyclenow = false;
454             if ((currentTickValue + unit > upperValue) && !cycled) {
455                 cyclenow = true;
456             }
457             
458             double xx = valueToJava2D(currentTickValue, dataArea, edge);
459             String JavaDoc tickLabel;
460             NumberFormat JavaDoc formatter = getNumberFormatOverride();
461             if (formatter != null) {
462                 tickLabel = formatter.format(currentTickValue);
463             }
464             else {
465                 tickLabel = getTickUnit().valueToString(currentTickValue);
466             }
467             float x = (float) xx;
468             TextAnchor anchor = null;
469             TextAnchor rotationAnchor = null;
470             double angle = 0.0;
471             if (isVerticalTickLabels()) {
472                 if (edge == RectangleEdge.TOP) {
473                     angle = Math.PI / 2.0;
474                 }
475                 else {
476                     angle = -Math.PI / 2.0;
477                 }
478                 anchor = TextAnchor.CENTER_RIGHT;
479                 // If tick overlap when cycling, update last tick too
480
if ((lastTick != null) && (lastX == x)
481                         && (currentTickValue != cycleBound)) {
482                     anchor = isInverted()
483                         ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT;
484                     result.remove(result.size() - 1);
485                     result.add(new CycleBoundTick(
486                         this.boundMappedToLastCycle, lastTick.getNumber(),
487                         lastTick.getText(), anchor, anchor,
488                         lastTick.getAngle())
489                     );
490                     this.internalMarkerWhenTicksOverlap = true;
491                     anchor = isInverted()
492                         ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT;
493                 }
494                 rotationAnchor = anchor;
495             }
496             else {
497                 if (edge == RectangleEdge.TOP) {
498                     anchor = TextAnchor.BOTTOM_CENTER;
499                     if ((lastTick != null) && (lastX == x)
500                             && (currentTickValue != cycleBound)) {
501                         anchor = isInverted()
502                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
503                         result.remove(result.size() - 1);
504                         result.add(new CycleBoundTick(
505                             this.boundMappedToLastCycle, lastTick.getNumber(),
506                             lastTick.getText(), anchor, anchor,
507                             lastTick.getAngle())
508                         );
509                         this.internalMarkerWhenTicksOverlap = true;
510                         anchor = isInverted()
511                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
512                     }
513                     rotationAnchor = anchor;
514                 }
515                 else {
516                     anchor = TextAnchor.TOP_CENTER;
517                     if ((lastTick != null) && (lastX == x)
518                             && (currentTickValue != cycleBound)) {
519                         anchor = isInverted()
520                             ? TextAnchor.TOP_LEFT : TextAnchor.TOP_RIGHT;
521                         result.remove(result.size() - 1);
522                         result.add(new CycleBoundTick(
523                             this.boundMappedToLastCycle, lastTick.getNumber(),
524                             lastTick.getText(), anchor, anchor,
525                             lastTick.getAngle())
526                         );
527                         this.internalMarkerWhenTicksOverlap = true;
528                         anchor = isInverted()
529                             ? TextAnchor.TOP_RIGHT : TextAnchor.TOP_LEFT;
530                     }
531                     rotationAnchor = anchor;
532                 }
533             }
534
535             CycleBoundTick tick = new CycleBoundTick(
536                 this.boundMappedToLastCycle,
537                 new Double JavaDoc(currentTickValue), tickLabel, anchor,
538                 rotationAnchor, angle
539             );
540             if (currentTickValue == cycleBound) {
541                 this.internalMarkerCycleBoundTick = tick;
542             }
543             result.add(tick);
544             lastTick = tick;
545             lastX = x;
546             
547             currentTickValue += unit;
548             
549             if (cyclenow) {
550                 currentTickValue = calculateLowestVisibleTickValue();
551                 upperValue = cycleBound;
552                 cycled = true;
553                 this.boundMappedToLastCycle = true;
554             }
555
556         }
557         this.boundMappedToLastCycle = boundMapping;
558         return result;
559         
560     }
561
562     /**
563      * Builds a list of ticks for the axis. This method is called when the
564      * axis is at the left or right of the chart (so the axis is "vertical").
565      *
566      * @param g2 the graphics device.
567      * @param dataArea the data area.
568      * @param edge the edge.
569      *
570      * @return A list of ticks.
571      */

572     protected List JavaDoc refreshVerticalTicks(Graphics2D JavaDoc g2,
573                                         Rectangle2D JavaDoc dataArea,
574                                         RectangleEdge edge) {
575         
576         List JavaDoc result = new java.util.ArrayList JavaDoc();
577         result.clear();
578
579         Font JavaDoc tickLabelFont = getTickLabelFont();
580         g2.setFont(tickLabelFont);
581         if (isAutoTickUnitSelection()) {
582             selectAutoTickUnit(g2, dataArea, edge);
583         }
584
585         double unit = getTickUnit().getSize();
586         double cycleBound = getCycleBound();
587         double currentTickValue = Math.ceil(cycleBound / unit) * unit;
588         double upperValue = getRange().getUpperBound();
589         boolean cycled = false;
590
591         boolean boundMapping = this.boundMappedToLastCycle;
592         this.boundMappedToLastCycle = true;
593
594         NumberTick lastTick = null;
595         float lastY = 0.0f;
596
597         if (upperValue == cycleBound) {
598             currentTickValue = calculateLowestVisibleTickValue();
599             cycled = true;
600             this.boundMappedToLastCycle = true;
601         }
602         
603         while (currentTickValue <= upperValue) {
604             
605             // Cycle when necessary
606
boolean cyclenow = false;
607             if ((currentTickValue + unit > upperValue) && !cycled) {
608                 cyclenow = true;
609             }
610
611             double yy = valueToJava2D(currentTickValue, dataArea, edge);
612             String JavaDoc tickLabel;
613             NumberFormat JavaDoc formatter = getNumberFormatOverride();
614             if (formatter != null) {
615                 tickLabel = formatter.format(currentTickValue);
616             }
617             else {
618                 tickLabel = getTickUnit().valueToString(currentTickValue);
619             }
620
621             float y = (float) yy;
622             TextAnchor anchor = null;
623             TextAnchor rotationAnchor = null;
624             double angle = 0.0;
625             if (isVerticalTickLabels()) {
626
627                 if (edge == RectangleEdge.LEFT) {
628                     anchor = TextAnchor.BOTTOM_CENTER;
629                     if ((lastTick != null) && (lastY == y)
630                             && (currentTickValue != cycleBound)) {
631                         anchor = isInverted()
632                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
633                         result.remove(result.size() - 1);
634                         result.add(new CycleBoundTick(
635                             this.boundMappedToLastCycle, lastTick.getNumber(),
636                             lastTick.getText(), anchor, anchor,
637                             lastTick.getAngle())
638                         );
639                         this.internalMarkerWhenTicksOverlap = true;
640                         anchor = isInverted()
641                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
642                     }
643                     rotationAnchor = anchor;
644                     angle = -Math.PI / 2.0;
645                 }
646                 else {
647                     anchor = TextAnchor.BOTTOM_CENTER;
648                     if ((lastTick != null) && (lastY == y)
649                             && (currentTickValue != cycleBound)) {
650                         anchor = isInverted()
651                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
652                         result.remove(result.size() - 1);
653                         result.add(new CycleBoundTick(
654                             this.boundMappedToLastCycle, lastTick.getNumber(),
655                             lastTick.getText(), anchor, anchor,
656                             lastTick.getAngle())
657                         );
658                         this.internalMarkerWhenTicksOverlap = true;
659                         anchor = isInverted()
660                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
661                     }
662                     rotationAnchor = anchor;
663                     angle = Math.PI / 2.0;
664                 }
665             }
666             else {
667                 if (edge == RectangleEdge.LEFT) {
668                     anchor = TextAnchor.CENTER_RIGHT;
669                     if ((lastTick != null) && (lastY == y)
670                             && (currentTickValue != cycleBound)) {
671                         anchor = isInverted()
672                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT;
673                         result.remove(result.size() - 1);
674                         result.add(new CycleBoundTick(
675                             this.boundMappedToLastCycle, lastTick.getNumber(),
676                             lastTick.getText(), anchor, anchor,
677                             lastTick.getAngle())
678                         );
679                         this.internalMarkerWhenTicksOverlap = true;
680                         anchor = isInverted()
681                             ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT;
682                     }
683                     rotationAnchor = anchor;
684                 }
685                 else {
686                     anchor = TextAnchor.CENTER_LEFT;
687                     if ((lastTick != null) && (lastY == y)
688                             && (currentTickValue != cycleBound)) {
689                         anchor = isInverted()
690                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_LEFT;
691                         result.remove(result.size() - 1);
692                         result.add(new CycleBoundTick(
693                             this.boundMappedToLastCycle, lastTick.getNumber(),
694                             lastTick.getText(), anchor, anchor,
695                             lastTick.getAngle())
696                         );
697                         this.internalMarkerWhenTicksOverlap = true;
698                         anchor = isInverted()
699                             ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_LEFT;
700                     }
701                     rotationAnchor = anchor;
702                 }
703             }
704
705             CycleBoundTick tick = new CycleBoundTick(
706                 this.boundMappedToLastCycle, new Double JavaDoc(currentTickValue),
707                 tickLabel, anchor, rotationAnchor, angle
708             );
709             if (currentTickValue == cycleBound) {
710                 this.internalMarkerCycleBoundTick = tick;
711             }
712             result.add(tick);
713             lastTick = tick;
714             lastY = y;
715             
716             if (currentTickValue == cycleBound) {
717                 this.internalMarkerCycleBoundTick = tick;
718             }
719
720             currentTickValue += unit;
721             
722             if (cyclenow) {
723                 currentTickValue = calculateLowestVisibleTickValue();
724                 upperValue = cycleBound;
725                 cycled = true;
726                 this.boundMappedToLastCycle = false;
727             }
728
729         }
730         this.boundMappedToLastCycle = boundMapping;
731         return result;
732     }
733     
734     /**
735      * Converts a coordinate from Java 2D space to data space.
736      *
737      * @param java2DValue the coordinate in Java2D space.
738      * @param dataArea the data area.
739      * @param edge the edge.
740      *
741      * @return The data value.
742      */

743     public double java2DToValue(double java2DValue, Rectangle2D JavaDoc dataArea,
744                                 RectangleEdge edge) {
745         Range range = getRange();
746         
747         double vmax = range.getUpperBound();
748         double vp = getCycleBound();
749
750         double jmin = 0.0;
751         double jmax = 0.0;
752         if (RectangleEdge.isTopOrBottom(edge)) {
753             jmin = dataArea.getMinX();
754             jmax = dataArea.getMaxX();
755         }
756         else if (RectangleEdge.isLeftOrRight(edge)) {
757             jmin = dataArea.getMaxY();
758             jmax = dataArea.getMinY();
759         }
760         
761         if (isInverted()) {
762             double jbreak = jmax - (vmax - vp) * (jmax - jmin) / this.period;
763             if (java2DValue >= jbreak) {
764                 return vp + (jmax - java2DValue) * this.period / (jmax - jmin);
765             }
766             else {
767                 return vp - (java2DValue - jmin) * this.period / (jmax - jmin);
768             }
769         }
770         else {
771             double jbreak = (vmax - vp) * (jmax - jmin) / this.period + jmin;
772             if (java2DValue <= jbreak) {
773                 return vp + (java2DValue - jmin) * this.period / (jmax - jmin);
774             }
775             else {
776                 return vp - (jmax - java2DValue) * this.period / (jmax - jmin);
777             }
778         }
779     }
780     
781     /**
782      * Translates a value from data space to Java 2D space.
783      *
784      * @param value the data value.
785      * @param dataArea the data area.
786      * @param edge the edge.
787      *
788      * @return The Java 2D value.
789      */

790     public double valueToJava2D(double value, Rectangle2D JavaDoc dataArea,
791                                 RectangleEdge edge) {
792         Range range = getRange();
793         
794         double vmin = range.getLowerBound();
795         double vmax = range.getUpperBound();
796         double vp = getCycleBound();
797
798         if ((value < vmin) || (value > vmax)) {
799             return Double.NaN;
800         }
801         
802         
803         double jmin = 0.0;
804         double jmax = 0.0;
805         if (RectangleEdge.isTopOrBottom(edge)) {
806             jmin = dataArea.getMinX();
807             jmax = dataArea.getMaxX();
808         }
809         else if (RectangleEdge.isLeftOrRight(edge)) {
810             jmax = dataArea.getMinY();
811             jmin = dataArea.getMaxY();
812         }
813
814         if (isInverted()) {
815             if (value == vp) {
816                 return this.boundMappedToLastCycle ? jmin : jmax;
817             }
818             else