Save This Page
Home » jcommon-1.0.13 » org.jfree » chart » axis » [javadoc | source]
    1   /* ===========================================================
    2    * JFreeChart : a free chart library for the Java(tm) platform
    3    * ===========================================================
    4    *
    5    * (C) Copyright 2000-2007, 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
   20    * License along with this library; if not, write to the Free Software
   21    * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
   22    * USA.  
   23    *
   24    * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
   25    * in the United States and other countries.]
   26    *
   27    * ---------------------
   28    * CyclicNumberAxis.java
   29    * ---------------------
   30    * (C) Copyright 2003-2007, by Nicolas Brodu and Contributors.
   31    *
   32    * Original Author:  Nicolas Brodu;
   33    * Contributor(s):   David Gilbert (for Object Refinery Limited);
   34    *
   35    * Changes
   36    * -------
   37    * 19-Nov-2003 : Initial import to JFreeChart from the JSynoptic project (NB);
   38    * 16-Mar-2004 : Added plotState to draw() method (DG);
   39    * 07-Apr-2004 : Modifed text bounds calculation (DG);
   40    * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant
   41    *               argument in selectAutoTickUnit() (DG);
   42    * 22-Apr-2005 : Renamed refreshHorizontalTicks() --> refreshTicksHorizontal
   43    *               (for consistency with other classes) and removed unused
   44    *               parameters (DG);
   45    * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG);
   46    *
   47    */
   48   
   49   package org.jfree.chart.axis;
   50   
   51   import java.awt.BasicStroke;
   52   import java.awt.Color;
   53   import java.awt.Font;
   54   import java.awt.FontMetrics;
   55   import java.awt.Graphics2D;
   56   import java.awt.Paint;
   57   import java.awt.Stroke;
   58   import java.awt.geom.Line2D;
   59   import java.awt.geom.Rectangle2D;
   60   import java.io.IOException;
   61   import java.io.ObjectInputStream;
   62   import java.io.ObjectOutputStream;
   63   import java.text.NumberFormat;
   64   import java.util.List;
   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   import org.jfree.util.PaintUtilities;
   75   
   76   /**
   77   This class extends NumberAxis and handles cycling.
   78    
   79   Traditional representation of data in the range x0..x1
   80   <pre>
   81   |-------------------------|
   82   x0                       x1
   83   </pre> 
   84   
   85   Here, the range bounds are at the axis extremities.
   86   With cyclic axis, however, the time is split in 
   87   "cycles", or "time frames", or the same duration : the period.
   88   
   89   A cycle axis cannot by definition handle a larger interval 
   90   than the period : <pre>x1 - x0 >= period</pre>. Thus, at most a full 
   91   period can be represented with such an axis.
   92   
   93   The cycle bound is the number between x0 and x1 which marks 
   94   the beginning of new time frame:
   95   <pre>
   96   |---------------------|----------------------------|
   97   x0                   cb                           x1
   98   <---previous cycle---><-------current cycle-------->
   99   </pre>
  100   
  101   It is actually a multiple of the period, plus optionally 
  102   a start offset: <pre>cb = n * period + offset</pre>
  103   
  104   Thus, by definition, two consecutive cycle bounds 
  105   period apart, which is precisely why it is called a 
  106   period.
  107   
  108   The visual representation of a cyclic axis is like that:
  109   <pre>
  110   |----------------------------|---------------------|
  111   cb                         x1|x0                  cb
  112   <-------current cycle--------><---previous cycle--->
  113   </pre>
  114   
  115   The cycle bound is at the axis ends, then current 
  116   cycle is shown, then the last cycle. When using 
  117   dynamic data, the visual effect is the current cycle 
  118   erases the last cycle as x grows. Then, the next cycle 
  119   bound is reached, and the process starts over, erasing 
  120   the previous cycle.
  121   
  122   A Cyclic item renderer is provided to do exactly this.
  123   
  124    */
  125   public class CyclicNumberAxis extends NumberAxis {
  126   
  127       /** For serialization. */
  128       static final long serialVersionUID = -7514160997164582554L;
  129   
  130       /** The default axis line stroke. */
  131       public static Stroke DEFAULT_ADVANCE_LINE_STROKE = new BasicStroke(1.0f);
  132       
  133       /** The default axis line paint. */
  134       public static final Paint DEFAULT_ADVANCE_LINE_PAINT = Color.gray;
  135       
  136       /** The offset. */
  137       protected double offset;
  138       
  139       /** The period.*/
  140       protected double period;
  141       
  142       /** ??. */
  143       protected boolean boundMappedToLastCycle;
  144       
  145       /** A flag that controls whether or not the advance line is visible. */
  146       protected boolean advanceLineVisible;
  147   
  148       /** The advance line stroke. */
  149       protected transient Stroke advanceLineStroke = DEFAULT_ADVANCE_LINE_STROKE;
  150       
  151       /** The advance line paint. */
  152       protected transient Paint advanceLinePaint;
  153       
  154       private transient boolean internalMarkerWhenTicksOverlap;
  155       private transient Tick internalMarkerCycleBoundTick;
  156       
  157       /** 
  158        * Creates a CycleNumberAxis with the given period.
  159        * 
  160        * @param period  the period.
  161        */
  162       public CyclicNumberAxis(double period) {
  163           this(period, 0.0);
  164       }
  165   
  166       /** 
  167        * Creates a CycleNumberAxis with the given period and offset.
  168        * 
  169        * @param period  the period.
  170        * @param offset  the offset.
  171        */
  172       public CyclicNumberAxis(double period, double offset) {
  173           this(period, offset, null);
  174       }
  175   
  176       /** 
  177        * Creates a named CycleNumberAxis with the given period.
  178        * 
  179        * @param period  the period.
  180        * @param label  the label.
  181        */
  182       public CyclicNumberAxis(double period, String label) {
  183           this(0, period, label);
  184       }
  185       
  186       /** 
  187        * Creates a named CycleNumberAxis with the given period and offset.
  188        * 
  189        * @param period  the period.
  190        * @param offset  the offset.
  191        * @param label  the label.
  192        */
  193       public CyclicNumberAxis(double period, double offset, String label) {
  194           super(label);
  195           this.period = period;
  196           this.offset = offset;
  197           setFixedAutoRange(period);
  198           this.advanceLineVisible = true;
  199           this.advanceLinePaint = DEFAULT_ADVANCE_LINE_PAINT;
  200       }
  201           
  202       /**
  203        * The advance line is the line drawn at the limit of the current cycle, 
  204        * when erasing the previous cycle. 
  205        * 
  206        * @return A boolean.
  207        */
  208       public boolean isAdvanceLineVisible() {
  209           return this.advanceLineVisible;
  210       }
  211       
  212       /**
  213        * The advance line is the line drawn at the limit of the current cycle, 
  214        * when erasing the previous cycle. 
  215        * 
  216        * @param visible  the flag.
  217        */
  218       public void setAdvanceLineVisible(boolean visible) {
  219           this.advanceLineVisible = visible;
  220       }
  221       
  222       /**
  223        * The advance line is the line drawn at the limit of the current cycle, 
  224        * when erasing the previous cycle. 
  225        * 
  226        * @return The paint (never <code>null</code>).
  227        */
  228       public Paint getAdvanceLinePaint() {
  229           return this.advanceLinePaint;
  230       }
  231   
  232       /**
  233        * The advance line is the line drawn at the limit of the current cycle, 
  234        * when erasing the previous cycle. 
  235        * 
  236        * @param paint  the paint (<code>null</code> not permitted).
  237        */
  238       public void setAdvanceLinePaint(Paint paint) {
  239           if (paint == null) {
  240               throw new IllegalArgumentException("Null 'paint' argument.");
  241           }
  242           this.advanceLinePaint = paint;
  243       }
  244       
  245       /**
  246        * The advance line is the line drawn at the limit of the current cycle, 
  247        * when erasing the previous cycle. 
  248        * 
  249        * @return The stroke (never <code>null</code>).
  250        */
  251       public Stroke getAdvanceLineStroke() {
  252           return this.advanceLineStroke;
  253       }
  254       /**
  255        * The advance line is the line drawn at the limit of the current cycle, 
  256        * when erasing the previous cycle. 
  257        * 
  258        * @param stroke  the stroke (<code>null</code> not permitted).
  259        */
  260       public void setAdvanceLineStroke(Stroke stroke) {
  261           if (stroke == null) {
  262               throw new IllegalArgumentException("Null 'stroke' argument.");
  263           }
  264           this.advanceLineStroke = stroke;
  265       }
  266       
  267       /**
  268        * The cycle bound can be associated either with the current or with the 
  269        * last cycle.  It's up to the user's choice to decide which, as this is 
  270        * just a convention.  By default, the cycle bound is mapped to the current
  271        * cycle.
  272        * <br>
  273        * Note that this has no effect on visual appearance, as the cycle bound is
  274        * mapped successively for both axis ends. Use this function for correct 
  275        * results in translateValueToJava2D. 
  276        *  
  277        * @return <code>true</code> if the cycle bound is mapped to the last 
  278        *         cycle, <code>false</code> if it is bound to the current cycle 
  279        *         (default)
  280        */
  281       public boolean isBoundMappedToLastCycle() {
  282           return this.boundMappedToLastCycle;
  283       }
  284       
  285       /**
  286        * The cycle bound can be associated either with the current or with the 
  287        * last cycle.  It's up to the user's choice to decide which, as this is 
  288        * just a convention. By default, the cycle bound is mapped to the current 
  289        * cycle. 
  290        * <br>
  291        * Note that this has no effect on visual appearance, as the cycle bound is
  292        * mapped successively for both axis ends. Use this function for correct 
  293        * results in valueToJava2D.
  294        *  
  295        * @param boundMappedToLastCycle Set it to true to map the cycle bound to 
  296        *        the last cycle.
  297        */
  298       public void setBoundMappedToLastCycle(boolean boundMappedToLastCycle) {
  299           this.boundMappedToLastCycle = boundMappedToLastCycle;
  300       }
  301       
  302       /**
  303        * Selects a tick unit when the axis is displayed horizontally.
  304        * 
  305        * @param g2  the graphics device.
  306        * @param drawArea  the drawing area.
  307        * @param dataArea  the data area.
  308        * @param edge  the side of the rectangle on which the axis is displayed.
  309        */
  310       protected void selectHorizontalAutoTickUnit(Graphics2D g2,
  311                                                   Rectangle2D drawArea, 
  312                                                   Rectangle2D dataArea,
  313                                                   RectangleEdge edge) {
  314   
  315           double tickLabelWidth 
  316               = estimateMaximumTickLabelWidth(g2, getTickUnit());
  317           
  318           // Compute number of labels
  319           double n = getRange().getLength() 
  320                      * tickLabelWidth / dataArea.getWidth();
  321   
  322           setTickUnit(
  323               (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n), 
  324               false, false
  325           );
  326           
  327        }
  328   
  329       /**
  330        * Selects a tick unit when the axis is displayed vertically.
  331        * 
  332        * @param g2  the graphics device.
  333        * @param drawArea  the drawing area.
  334        * @param dataArea  the data area.
  335        * @param edge  the side of the rectangle on which the axis is displayed.
  336        */
  337       protected void selectVerticalAutoTickUnit(Graphics2D g2,
  338                                                   Rectangle2D drawArea, 
  339                                                   Rectangle2D dataArea,
  340                                                   RectangleEdge edge) {
  341   
  342           double tickLabelWidth 
  343               = estimateMaximumTickLabelWidth(g2, getTickUnit());
  344   
  345           // Compute number of labels
  346           double n = getRange().getLength() 
  347                      * tickLabelWidth / dataArea.getHeight();
  348   
  349           setTickUnit(
  350               (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n), 
  351               false, false
  352           );
  353           
  354        }
  355   
  356       /** 
  357        * A special Number tick that also hold information about the cycle bound 
  358        * mapping for this tick.  This is especially useful for having a tick at 
  359        * each axis end with the cycle bound value.  See also 
  360        * isBoundMappedToLastCycle()
  361        */
  362       protected static class CycleBoundTick extends NumberTick {
  363           
  364           /** Map to last cycle. */
  365           public boolean mapToLastCycle;
  366           
  367           /**
  368            * Creates a new tick.
  369            * 
  370            * @param mapToLastCycle  map to last cycle?
  371            * @param number  the number.
  372            * @param label  the label.
  373            * @param textAnchor  the text anchor.
  374            * @param rotationAnchor  the rotation anchor.
  375            * @param angle  the rotation angle.
  376            */
  377           public CycleBoundTick(boolean mapToLastCycle, Number number, 
  378                                 String label, TextAnchor textAnchor,
  379                                 TextAnchor rotationAnchor, double angle) {
  380               super(number, label, textAnchor, rotationAnchor, angle);
  381               this.mapToLastCycle = mapToLastCycle;
  382           }
  383       }
  384       
  385       /**
  386        * Calculates the anchor point for a tick.
  387        * 
  388        * @param tick  the tick.
  389        * @param cursor  the cursor.
  390        * @param dataArea  the data area.
  391        * @param edge  the side on which the axis is displayed.
  392        * 
  393        * @return The anchor point.
  394        */
  395       protected float[] calculateAnchorPoint(ValueTick tick, double cursor, 
  396                                              Rectangle2D dataArea, 
  397                                              RectangleEdge edge) {
  398           if (tick instanceof CycleBoundTick) {
  399               boolean mapsav = this.boundMappedToLastCycle;
  400               this.boundMappedToLastCycle 
  401                   = ((CycleBoundTick) tick).mapToLastCycle;
  402               float[] ret = super.calculateAnchorPoint(
  403                   tick, cursor, dataArea, edge
  404               );
  405               this.boundMappedToLastCycle = mapsav;
  406               return ret;
  407           }
  408           return super.calculateAnchorPoint(tick, cursor, dataArea, edge);
  409       }
  410       
  411       
  412       
  413       /**
  414        * Builds a list of ticks for the axis.  This method is called when the 
  415        * axis is at the top or bottom of the chart (so the axis is "horizontal").
  416        * 
  417        * @param g2  the graphics device.
  418        * @param dataArea  the data area.
  419        * @param edge  the edge.
  420        * 
  421        * @return A list of ticks.
  422        */
  423       protected List refreshTicksHorizontal(Graphics2D g2, 
  424                                             Rectangle2D dataArea, 
  425                                             RectangleEdge edge) {
  426   
  427           List result = new java.util.ArrayList();
  428   
  429           Font tickLabelFont = getTickLabelFont();
  430           g2.setFont(tickLabelFont);
  431           
  432           if (isAutoTickUnitSelection()) {
  433               selectAutoTickUnit(g2, dataArea, edge);
  434           }
  435   
  436           double unit = getTickUnit().getSize();
  437           double cycleBound = getCycleBound();
  438           double currentTickValue = Math.ceil(cycleBound / unit) * unit;
  439           double upperValue = getRange().getUpperBound();
  440           boolean cycled = false;
  441   
  442           boolean boundMapping = this.boundMappedToLastCycle; 
  443           this.boundMappedToLastCycle = false; 
  444           
  445           CycleBoundTick lastTick = null; 
  446           float lastX = 0.0f;
  447   
  448           if (upperValue == cycleBound) {
  449               currentTickValue = calculateLowestVisibleTickValue();
  450               cycled = true;
  451               this.boundMappedToLastCycle = true;
  452           }
  453           
  454           while (currentTickValue <= upperValue) {
  455               
  456               // Cycle when necessary
  457               boolean cyclenow = false;
  458               if ((currentTickValue + unit > upperValue) && !cycled) {
  459                   cyclenow = true;
  460               }
  461               
  462               double xx = valueToJava2D(currentTickValue, dataArea, edge);
  463               String tickLabel;
  464               NumberFormat formatter = getNumberFormatOverride();
  465               if (formatter != null) {
  466                   tickLabel = formatter.format(currentTickValue);
  467               }
  468               else {
  469                   tickLabel = getTickUnit().valueToString(currentTickValue);
  470               }
  471               float x = (float) xx;
  472               TextAnchor anchor = null;
  473               TextAnchor rotationAnchor = null;
  474               double angle = 0.0;
  475               if (isVerticalTickLabels()) {
  476                   if (edge == RectangleEdge.TOP) {
  477                       angle = Math.PI / 2.0;
  478                   }
  479                   else {
  480                       angle = -Math.PI / 2.0;
  481                   }
  482                   anchor = TextAnchor.CENTER_RIGHT;
  483                   // If tick overlap when cycling, update last tick too
  484                   if ((lastTick != null) && (lastX == x) 
  485                           && (currentTickValue != cycleBound)) {
  486                       anchor = isInverted() 
  487                           ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT;
  488                       result.remove(result.size() - 1);
  489                       result.add(new CycleBoundTick(
  490                           this.boundMappedToLastCycle, lastTick.getNumber(), 
  491                           lastTick.getText(), anchor, anchor, 
  492                           lastTick.getAngle())
  493                       );
  494                       this.internalMarkerWhenTicksOverlap = true;
  495                       anchor = isInverted() 
  496                           ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT;
  497                   }
  498                   rotationAnchor = anchor;
  499               }
  500               else {
  501                   if (edge == RectangleEdge.TOP) {
  502                       anchor = TextAnchor.BOTTOM_CENTER; 
  503                       if ((lastTick != null) && (lastX == x) 
  504                               && (currentTickValue != cycleBound)) {
  505                           anchor = isInverted() 
  506                               ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
  507                           result.remove(result.size() - 1);
  508                           result.add(new CycleBoundTick(
  509                               this.boundMappedToLastCycle, lastTick.getNumber(),
  510                               lastTick.getText(), anchor, anchor, 
  511                               lastTick.getAngle())
  512                           );
  513                           this.internalMarkerWhenTicksOverlap = true;
  514                           anchor = isInverted() 
  515                               ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
  516                       }
  517                       rotationAnchor = anchor;
  518                   }
  519                   else {
  520                       anchor = TextAnchor.TOP_CENTER; 
  521                       if ((lastTick != null) && (lastX == x) 
  522                               && (currentTickValue != cycleBound)) {
  523                           anchor = isInverted() 
  524                               ? TextAnchor.TOP_LEFT : TextAnchor.TOP_RIGHT;
  525                           result.remove(result.size() - 1);
  526                           result.add(new CycleBoundTick(
  527                               this.boundMappedToLastCycle, lastTick.getNumber(),
  528                               lastTick.getText(), anchor, anchor, 
  529                               lastTick.getAngle())
  530                           );
  531                           this.internalMarkerWhenTicksOverlap = true;
  532                           anchor = isInverted() 
  533                               ? TextAnchor.TOP_RIGHT : TextAnchor.TOP_LEFT;
  534                       }
  535                       rotationAnchor = anchor;
  536                   }
  537               }
  538   
  539               CycleBoundTick tick = new CycleBoundTick(
  540                   this.boundMappedToLastCycle, 
  541                   new Double(currentTickValue), tickLabel, anchor, 
  542                   rotationAnchor, angle
  543               );
  544               if (currentTickValue == cycleBound) {
  545                   this.internalMarkerCycleBoundTick = tick; 
  546               }
  547               result.add(tick);
  548               lastTick = tick;
  549               lastX = x;
  550               
  551               currentTickValue += unit;
  552               
  553               if (cyclenow) {
  554                   currentTickValue = calculateLowestVisibleTickValue();
  555                   upperValue = cycleBound;
  556                   cycled = true;
  557                   this.boundMappedToLastCycle = true; 
  558               }
  559   
  560           }
  561           this.boundMappedToLastCycle = boundMapping; 
  562           return result;
  563           
  564       }
  565   
  566       /**
  567        * Builds a list of ticks for the axis.  This method is called when the 
  568        * axis is at the left or right of the chart (so the axis is "vertical").
  569        * 
  570        * @param g2  the graphics device.
  571        * @param dataArea  the data area.
  572        * @param edge  the edge.
  573        * 
  574        * @return A list of ticks.
  575        */
  576       protected List refreshVerticalTicks(Graphics2D g2, 
  577                                           Rectangle2D dataArea, 
  578                                           RectangleEdge edge) {
  579           
  580           List result = new java.util.ArrayList();
  581           result.clear();
  582   
  583           Font tickLabelFont = getTickLabelFont();
  584           g2.setFont(tickLabelFont);
  585           if (isAutoTickUnitSelection()) {
  586               selectAutoTickUnit(g2, dataArea, edge);
  587           }
  588   
  589           double unit = getTickUnit().getSize();
  590           double cycleBound = getCycleBound();
  591           double currentTickValue = Math.ceil(cycleBound / unit) * unit;
  592           double upperValue = getRange().getUpperBound();
  593           boolean cycled = false;
  594   
  595           boolean boundMapping = this.boundMappedToLastCycle; 
  596           this.boundMappedToLastCycle = true; 
  597   
  598           NumberTick lastTick = null;
  599           float lastY = 0.0f;
  600   
  601           if (upperValue == cycleBound) {
  602               currentTickValue = calculateLowestVisibleTickValue();
  603               cycled = true;
  604               this.boundMappedToLastCycle = true;
  605           }
  606           
  607           while (currentTickValue <= upperValue) {
  608               
  609               // Cycle when necessary
  610               boolean cyclenow = false;
  611               if ((currentTickValue + unit > upperValue) && !cycled) {
  612                   cyclenow = true;
  613               }
  614   
  615               double yy = valueToJava2D(currentTickValue, dataArea, edge);
  616               String tickLabel;
  617               NumberFormat formatter = getNumberFormatOverride();
  618               if (formatter != null) {
  619                   tickLabel = formatter.format(currentTickValue);
  620               }
  621               else {
  622                   tickLabel = getTickUnit().valueToString(currentTickValue);
  623               }
  624   
  625               float y = (float) yy;
  626               TextAnchor anchor = null;
  627               TextAnchor rotationAnchor = null;
  628               double angle = 0.0;
  629               if (isVerticalTickLabels()) {
  630   
  631                   if (edge == RectangleEdge.LEFT) {
  632                       anchor = TextAnchor.BOTTOM_CENTER; 
  633                       if ((lastTick != null) && (lastY == y) 
  634                               && (currentTickValue != cycleBound)) {
  635                           anchor = isInverted() 
  636                               ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
  637                           result.remove(result.size() - 1);
  638                           result.add(new CycleBoundTick(
  639                               this.boundMappedToLastCycle, lastTick.getNumber(),
  640                               lastTick.getText(), anchor, anchor, 
  641                               lastTick.getAngle())
  642                           );
  643                           this.internalMarkerWhenTicksOverlap = true;
  644                           anchor = isInverted() 
  645                               ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
  646                       }
  647                       rotationAnchor = anchor;
  648                       angle = -Math.PI / 2.0;
  649                   }
  650                   else {
  651                       anchor = TextAnchor.BOTTOM_CENTER; 
  652                       if ((lastTick != null) && (lastY == y) 
  653                               && (currentTickValue != cycleBound)) {
  654                           anchor = isInverted() 
  655                               ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
  656                           result.remove(result.size() - 1);
  657                           result.add(new CycleBoundTick(
  658                               this.boundMappedToLastCycle, lastTick.getNumber(),
  659                               lastTick.getText(), anchor, anchor, 
  660                               lastTick.getAngle())
  661                           );
  662                           this.internalMarkerWhenTicksOverlap = true;
  663                           anchor = isInverted() 
  664                               ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
  665                       }
  666                       rotationAnchor = anchor;
  667                       angle = Math.PI / 2.0;
  668                   }
  669               }
  670               else {
  671                   if (edge == RectangleEdge.LEFT) {
  672                       anchor = TextAnchor.CENTER_RIGHT; 
  673                       if ((lastTick != null) && (lastY == y) 
  674                               && (currentTickValue != cycleBound)) {
  675                           anchor = isInverted() 
  676                               ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT;
  677                           result.remove(result.size() - 1);
  678                           result.add(new CycleBoundTick(
  679                               this.boundMappedToLastCycle, lastTick.getNumber(),
  680                               lastTick.getText(), anchor, anchor, 
  681                               lastTick.getAngle())
  682                           );
  683                           this.internalMarkerWhenTicksOverlap = true;
  684                           anchor = isInverted() 
  685                               ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT;
  686                       }
  687                       rotationAnchor = anchor;
  688                   }
  689                   else {
  690                       anchor = TextAnchor.CENTER_LEFT; 
  691                       if ((lastTick != null) && (lastY == y) 
  692                               && (currentTickValue != cycleBound)) {
  693                           anchor = isInverted() 
  694                               ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_LEFT;
  695                           result.remove(result.size() - 1);
  696                           result.add(new CycleBoundTick(
  697                               this.boundMappedToLastCycle, lastTick.getNumber(),
  698                               lastTick.getText(), anchor, anchor, 
  699                               lastTick.getAngle())
  700                           );
  701                           this.internalMarkerWhenTicksOverlap = true;
  702                           anchor = isInverted() 
  703                               ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_LEFT;
  704                       }
  705                       rotationAnchor = anchor;
  706                   }
  707               }
  708   
  709               CycleBoundTick tick = new CycleBoundTick(
  710                   this.boundMappedToLastCycle, new Double(currentTickValue), 
  711                   tickLabel, anchor, rotationAnchor, angle
  712               );
  713               if (currentTickValue == cycleBound) {
  714                   this.internalMarkerCycleBoundTick = tick; 
  715               }
  716               result.add(tick);
  717               lastTick = tick;
  718               lastY = y;
  719               
  720               if (currentTickValue == cycleBound) {
  721                   this.internalMarkerCycleBoundTick = tick;
  722               }
  723   
  724               currentTickValue += unit;
  725               
  726               if (cyclenow) {
  727                   currentTickValue = calculateLowestVisibleTickValue();
  728                   upperValue = cycleBound;
  729                   cycled = true;
  730                   this.boundMappedToLastCycle = false; 
  731               }
  732   
  733           }
  734           this.boundMappedToLastCycle = boundMapping; 
  735           return result;
  736       }
  737       
  738       /**
  739        * Converts a coordinate from Java 2D space to data space.
  740        * 
  741        * @param java2DValue  the coordinate in Java2D space.
  742        * @param dataArea  the data area.
  743        * @param edge  the edge.
  744        * 
  745        * @return The data value.
  746        */
  747       public double java2DToValue(double java2DValue, Rectangle2D dataArea, 
  748                                   RectangleEdge edge) {
  749           Range range = getRange();
  750           
  751           double vmax = range.getUpperBound();
  752           double vp = getCycleBound();
  753   
  754           double jmin = 0.0;
  755           double jmax = 0.0;
  756           if (RectangleEdge.isTopOrBottom(edge)) {
  757               jmin = dataArea.getMinX();
  758               jmax = dataArea.getMaxX();
  759           }
  760           else if (RectangleEdge.isLeftOrRight(edge)) {
  761               jmin = dataArea.getMaxY();
  762               jmax = dataArea.getMinY();
  763           }
  764           
  765           if (isInverted()) {
  766               double jbreak = jmax - (vmax - vp) * (jmax - jmin) / this.period;
  767               if (java2DValue >= jbreak) { 
  768                   return vp + (jmax - java2DValue) * this.period / (jmax - jmin);
  769               } 
  770               else {
  771                   return vp - (java2DValue - jmin) * this.period / (jmax - jmin);
  772               }
  773           }
  774           else {
  775               double jbreak = (vmax - vp) * (jmax - jmin) / this.period + jmin;
  776               if (java2DValue <= jbreak) { 
  777                   return vp + (java2DValue - jmin) * this.period / (jmax - jmin);
  778               } 
  779               else {
  780                   return vp - (jmax - java2DValue) * this.period / (jmax - jmin);
  781               }
  782           }
  783       }
  784       
  785       /**
  786        * Translates a value from data space to Java 2D space.
  787        * 
  788        * @param value  the data value.
  789        * @param dataArea  the data area.
  790        * @param edge  the edge.
  791        * 
  792        * @return The Java 2D value.
  793        */
  794       public double valueToJava2D(double value, Rectangle2D dataArea, 
  795                                   RectangleEdge edge) {
  796           Range range = getRange();
  797           
  798           double vmin = range.getLowerBound();
  799           double vmax = range.getUpperBound();
  800           double vp = getCycleBound();
  801   
  802           if ((value < vmin) || (value > vmax)) {
  803               return Double.NaN;
  804           }
  805           
  806           
  807           double jmin = 0.0;
  808           double jmax = 0.0;
  809           if (RectangleEdge.isTopOrBottom(edge)) {
  810               jmin = dataArea.getMinX();
  811               jmax = dataArea.getMaxX();
  812           }
  813           else if (RectangleEdge.isLeftOrRight(edge)) {
  814               jmax = dataArea.getMinY();
  815               jmin = dataArea.getMaxY();
  816           }
  817   
  818           if (isInverted()) {
  819               if (value == vp) {
  820                   return this.boundMappedToLastCycle ? jmin : jmax; 
  821               }
  822               else if (value > vp) {
  823                   return jmax - (value - vp) * (jmax - jmin) / this.period;
  824               } 
  825               else {
  826                   return jmin + (vp - value) * (jmax - jmin) / this.period;
  827               }
  828           }
  829           else {
  830               if (value == vp) {
  831                   return this.boundMappedToLastCycle ? jmax : jmin; 
  832               }
  833               else if (value >= vp) {
  834                   return jmin + (value - vp) * (jmax - jmin) / this.period;
  835               } 
  836               else {
  837                   return jmax - (vp - value) * (jmax - jmin) / this.period;
  838               }
  839           }
  840       }
  841       
  842       /**
  843        * Centers the range about the given value.
  844        * 
  845        * @param value  the data value.
  846        */
  847       public void centerRange(double value) {
  848           setRange(value - this.period / 2.0, value + this.period / 2.0);
  849       }
  850   
  851       /** 
  852        * This function is nearly useless since the auto range is fixed for this 
  853        * class to the period.  The period is extended if necessary to fit the 
  854        * minimum size.
  855        * 
  856        * @param size  the size.
  857        * @param notify  notify?
  858        * 
  859        * @see org.jfree.chart.axis.ValueAxis#setAutoRangeMinimumSize(double, 
  860        *      boolean)
  861        */
  862       public void setAutoRangeMinimumSize(double size, boolean notify) {
  863           if (size > this.period) {
  864               this.period = size;
  865           }
  866           super.setAutoRangeMinimumSize(size, notify);
  867       }
  868   
  869       /** 
  870        * The auto range is fixed for this class to the period by default. 
  871        * This function will thus set a new period.
  872        * 
  873        * @param length  the length.
  874        * 
  875        * @see org.jfree.chart.axis.ValueAxis#setFixedAutoRange(double)
  876        */
  877       public void setFixedAutoRange(double length) {
  878           this.period = length;
  879           super.setFixedAutoRange(length);
  880       }
  881   
  882       /** 
  883        * Sets a new axis range. The period is extended to fit the range size, if 
  884        * necessary.
  885        * 
  886        * @param range  the range.
  887        * @param turnOffAutoRange  switch off the auto range.
  888        * @param notify notify?
  889        * 
  890        * @see org.jfree.chart.axis.ValueAxis#setRange(Range, boolean, boolean) 
  891        */
  892       public void setRange(Range range, boolean turnOffAutoRange, 
  893                            boolean notify) {
  894           double size = range.getUpperBound() - range.getLowerBound();
  895           if (size > this.period) {
  896               this.period = size;
  897           }
  898           super.setRange(range, turnOffAutoRange, notify);
  899       }
  900       
  901       /**
  902        * The cycle bound is defined as the higest value x such that 
  903        * "offset + period * i = x", with i and integer and x &lt; 
  904        * range.getUpperBound() This is the value which is at both ends of the 
  905        * axis :  x...up|low...x
  906        * The values from x to up are the valued in the current cycle.
  907        * The values from low to x are the valued in the previous cycle.
  908        * 
  909        * @return The cycle bound.
  910        */
  911       public double getCycleBound() {
  912           return Math.floor(
  913               (getRange().getUpperBound() - this.offset) / this.period
  914           ) * this.period + this.offset;
  915       }
  916       
  917       /**
  918        * The cycle bound is a multiple of the period, plus optionally a start 
  919        * offset.
  920        * <P>
  921        * <pre>cb = n * period + offset</pre><br>
  922        * 
  923        * @return The current offset.
  924        * 
  925        * @see #getCycleBound()
  926        */
  927       public double getOffset() {
  928           return this.offset;
  929       }
  930       
  931       /**
  932        * The cycle bound is a multiple of the period, plus optionally a start 
  933        * offset.
  934        * <P>
  935        * <pre>cb = n * period + offset</pre><br>
  936        * 
  937        * @param offset The offset to set.
  938        *
  939        * @see #getCycleBound() 
  940        */
  941       public void setOffset(double offset) {
  942           this.offset = offset;
  943       }
  944       
  945       /**
  946        * The cycle bound is a multiple of the period, plus optionally a start 
  947        * offset.
  948        * <P>
  949        * <pre>cb = n * period + offset</pre><br>
  950        * 
  951        * @return The current period.
  952        * 
  953        * @see #getCycleBound()
  954        */
  955       public double getPeriod() {
  956           return this.period;
  957       }
  958       
  959       /**
  960        * The cycle bound is a multiple of the period, plus optionally a start 
  961        * offset.
  962        * <P>
  963        * <pre>cb = n * period + offset</pre><br>
  964        * 
  965        * @param period The period to set.
  966        * 
  967        * @see #getCycleBound()
  968        */
  969       public void setPeriod(double period) {
  970           this.period = period;
  971       }
  972   
  973       /**
  974        * Draws the tick marks and labels.
  975        * 
  976        * @param g2  the graphics device.
  977        * @param cursor  the cursor.
  978        * @param plotArea  the plot area.
  979        * @param dataArea  the area inside the axes.
  980        * @param edge  the side on which the axis is displayed.
  981        * 
  982        * @return The axis state.
  983        */
  984       protected AxisState drawTickMarksAndLabels(Graphics2D g2, double cursor, 
  985                                                  Rectangle2D plotArea, 
  986                                                  Rectangle2D dataArea, 
  987                                                  RectangleEdge edge) {
  988           this.internalMarkerWhenTicksOverlap = false;
  989           AxisState ret = super.drawTickMarksAndLabels(
  990               g2, cursor, plotArea, dataArea, edge
  991           );
  992           
  993           // continue and separate the labels only if necessary
  994           if (!this.internalMarkerWhenTicksOverlap) {
  995               return ret;
  996           }
  997           
  998           double ol = getTickMarkOutsideLength();
  999           FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
 1000           
 1001           if (isVerticalTickLabels()) {
 1002               ol = fm.getMaxAdvance(); 
 1003           }
 1004           else {
 1005               ol = fm.getHeight();
 1006           }
 1007           
 1008           double il = 0;
 1009           if (isTickMarksVisible()) {
 1010               float xx = (float) valueToJava2D(
 1011                   getRange().getUpperBound(), dataArea, edge
 1012               );
 1013               Line2D mark = null;
 1014               g2.setStroke(getTickMarkStroke());
 1015               g2.setPaint(getTickMarkPaint());
 1016               if (edge == RectangleEdge.LEFT) {
 1017                   mark = new Line2D.Double(cursor - ol, xx, cursor + il, xx);
 1018               }
 1019               else if (edge == RectangleEdge.RIGHT) {
 1020                   mark = new Line2D.Double(cursor + ol, xx, cursor - il, xx);
 1021               }
 1022               else if (edge == RectangleEdge.TOP) {
 1023                   mark = new Line2D.Double(xx, cursor - ol, xx, cursor + il);
 1024               }
 1025               else if (edge == RectangleEdge.BOTTOM) {
 1026                   mark = new Line2D.Double(xx, cursor + ol, xx, cursor - il);
 1027               }
 1028               g2.draw(mark);
 1029           }
 1030           return ret;
 1031       }
 1032       
 1033       /**
 1034        * Draws the axis.
 1035        * 
 1036        * @param g2  the graphics device (<code>null</code> not permitted).
 1037        * @param cursor  the cursor position.
 1038        * @param plotArea  the plot area (<code>null</code> not permitted).
 1039        * @param dataArea  the data area (<code>null</code> not permitted).
 1040        * @param edge  the edge (<code>null</code> not permitted).
 1041        * @param plotState  collects information about the plot 
 1042        *                   (<code>null</code> permitted).
 1043        * 
 1044        * @return The axis state (never <code>null</code>).
 1045        */
 1046       public AxisState draw(Graphics2D g2, 
 1047                             double cursor,
 1048                             Rectangle2D plotArea, 
 1049                             Rectangle2D dataArea, 
 1050                             RectangleEdge edge,
 1051                             PlotRenderingInfo plotState) {
 1052           
 1053           AxisState ret = super.draw(
 1054               g2, cursor, plotArea, dataArea, edge, plotState
 1055           );
 1056           if (isAdvanceLineVisible()) {
 1057               double xx = valueToJava2D(
 1058                   getRange().getUpperBound(), dataArea, edge
 1059               );
 1060               Line2D mark = null;
 1061               g2.setStroke(getAdvanceLineStroke());
 1062               g2.setPaint(getAdvanceLinePaint());
 1063               if (edge == RectangleEdge.LEFT) {
 1064                   mark = new Line2D.Double(
 1065                       cursor, xx, cursor + dataArea.getWidth(), xx
 1066                   );
 1067               }
 1068               else if (edge == RectangleEdge.RIGHT) {
 1069                   mark = new Line2D.Double(
 1070                       cursor - dataArea.getWidth(), xx, cursor, xx
 1071                   );
 1072               }
 1073               else if (edge == RectangleEdge.TOP) {
 1074                   mark = new Line2D.Double(
 1075                       xx, cursor + dataArea.getHeight(), xx, cursor
 1076                   );
 1077               }
 1078               else if (edge == RectangleEdge.BOTTOM) {
 1079                   mark = new Line2D.Double(
 1080                       xx, cursor, xx, cursor - dataArea.getHeight()
 1081                   );
 1082               }
 1083               g2.draw(mark);
 1084           }
 1085           return ret;
 1086       }
 1087   
 1088       /**
 1089        * Reserve some space on each axis side because we draw a centered label at
 1090        * each extremity. 
 1091        * 
 1092        * @param g2  the graphics device.
 1093        * @param plot  the plot.
 1094        * @param plotArea  the plot area.
 1095        * @param edge  the edge.
 1096        * @param space  the space already reserved.
 1097        * 
 1098        * @return The reserved space.
 1099        */
 1100       public AxisSpace reserveSpace(Graphics2D g2, 
 1101                                     Plot plot, 
 1102                                     Rectangle2D plotArea, 
 1103                                     RectangleEdge edge, 
 1104                                     AxisSpace space) {
 1105           
 1106           this.internalMarkerCycleBoundTick = null;
 1107           AxisSpace ret = super.reserveSpace(g2, plot, plotArea, edge, space);
 1108           if (this.internalMarkerCycleBoundTick == null) {
 1109               return ret;
 1110           }
 1111   
 1112           FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
 1113           Rectangle2D r = TextUtilities.getTextBounds(
 1114               this.internalMarkerCycleBoundTick.getText(), g2, fm
 1115           );
 1116   
 1117           if (RectangleEdge.isTopOrBottom(edge)) {
 1118               if (isVerticalTickLabels()) {
 1119                   space.add(r.getHeight() / 2, RectangleEdge.RIGHT);
 1120               }
 1121               else {
 1122                   space.add(r.getWidth() / 2, RectangleEdge.RIGHT);
 1123               }
 1124           }
 1125           else if (RectangleEdge.isLeftOrRight(edge)) {
 1126               if (isVerticalTickLabels()) {
 1127                   space.add(r.getWidth() / 2, RectangleEdge.TOP);
 1128               }
 1129               else {
 1130                   space.add(r.getHeight() / 2, RectangleEdge.TOP);
 1131               }
 1132           }
 1133           
 1134           return ret;
 1135           
 1136       }
 1137   
 1138       /**
 1139        * Provides serialization support.
 1140        *
 1141        * @param stream  the output stream.
 1142        *
 1143        * @throws IOException  if there is an I/O error.
 1144        */
 1145       private void writeObject(ObjectOutputStream stream) throws IOException {
 1146       
 1147           stream.defaultWriteObject();
 1148           SerialUtilities.writePaint(this.advanceLinePaint, stream);
 1149           SerialUtilities.writeStroke(this.advanceLineStroke, stream);
 1150       
 1151       }
 1152       
 1153       /**
 1154        * Provides serialization support.
 1155        *
 1156        * @param stream  the input stream.
 1157        *
 1158        * @throws IOException  if there is an I/O error.
 1159        * @throws ClassNotFoundException  if there is a classpath problem.
 1160        */
 1161       private void readObject(ObjectInputStream stream) 
 1162           throws IOException, ClassNotFoundException {
 1163       
 1164           stream.defaultReadObject();
 1165           this.advanceLinePaint = SerialUtilities.readPaint(stream);
 1166           this.advanceLineStroke = SerialUtilities.readStroke(stream);
 1167       
 1168       }
 1169        
 1170       
 1171       /**
 1172        * Tests the axis for equality with another object.
 1173        * 
 1174        * @param obj  the object to test against.
 1175        * 
 1176        * @return A boolean.
 1177        */
 1178       public boolean equals(Object obj) {
 1179           if (obj == this) {
 1180               return true;
 1181           }
 1182           if (!(obj instanceof CyclicNumberAxis)) {
 1183               return false;
 1184           }
 1185           if (!super.equals(obj)) {
 1186               return false;
 1187           }
 1188           CyclicNumberAxis that = (CyclicNumberAxis) obj;      
 1189           if (this.period != that.period) {
 1190               return false;
 1191           }
 1192           if (this.offset != that.offset) {
 1193               return false;
 1194           }
 1195           if (!PaintUtilities.equal(this.advanceLinePaint, 
 1196                   that.advanceLinePaint)) {
 1197               return false;
 1198           }
 1199           if (!ObjectUtilities.equal(this.advanceLineStroke, 
 1200                   that.advanceLineStroke)) {
 1201               return false;
 1202           }
 1203           if (this.advanceLineVisible != that.advanceLineVisible) {
 1204               return false;
 1205           }
 1206           if (this.boundMappedToLastCycle != that.boundMappedToLastCycle) {
 1207               return false;
 1208           }
 1209           return true;
 1210       }
 1211   }

Save This Page
Home » jcommon-1.0.13 » org.jfree » chart » axis » [javadoc | source]