HOME / Projects / FreeHEP / freehep / org / freehep / j3d / plot / AxisLabelCalculator.java
package org.freehep.j3d.plot;

import java.io.*;
/**
 * @author Joy Kyriakopulos (joyk@fnal.gov)
 * @version $Id: AxisLabelCalculator.java,v 1.2 2001/06/15 20:55:51 joyk Exp $
 */
public class AxisLabelCalculator
{

    /* VARIABLES */
    private double data_min = 0d, data_max = 1d; // actual min/max for the data set
    private double plot_min = 0d, plot_max = 1d; // min/max on the axis itself
    private AxisLabel[] labels;
    private int nDivisions = 0;
    private boolean labelsValid;

    public void createNewLabels(double min, double max)
    {
        data_min = min;
        data_max = max;
        int minNumberOfDivisions = 1;
        int maxNumberOfDivisions = 10;
        double log10 = Math.log(10.0);
        int maxCharsPerLabel = 5;
        labelsValid = true;

        // log_max is the logarithm of the max value of the data
        double log_max = data_max == 0d ? 0d : Math.log(Math.abs(data_max)) / log10;

        // int_log_max is the floored integer equivalent of log_max
        final int int_log_max = (int) Math.floor(log_max);

        // by default, scale_power is 0
        // This number represents the amount by which we scale all labels.  For example,
        // if our range is from 2,000,000,000 to 5,000,000,000 we want scale_power to be
        // 9 so that we get 2.0, 2.5, 3.0, ... or something like that on the labels.
        int scale_power = 0;
        if (int_log_max >= maxCharsPerLabel)
            // we have an order of magnitude that can't be displayed in standard form, so we need to set
            // a value for scale_power
            scale_power = int_log_max;
        else if (int_log_max <= -maxCharsPerLabel)
            // we have an order of magnitude that can't be displayed in standard form, so we need to set
            // a value for scale_power
            scale_power = int_log_max;

        // System.out.println();
        // System.out.println("int_log_max = " + int_log_max + ", maxCharsPerLabel = " + maxCharsPerLabel + ", scale_power = " + scale_power);

        final DoubleNumberFormatter format = new DoubleNumberFormatter(scale_power);
        // the formatter uses scale_power when creating labels

       /*
        * Our strategy here is based on the observation that plotting the range
        * 0.2 to 40 is very similar to the task of plotting the ranges 2 to 400,
        * or 0.02 to 4.  We scale the min and max by an order of ten such that when
        * converted to integers (by a process not quite like trucation) the difference
        * between those two integers is a number greater than or equal to 10 and less
        * than (but not equal to) 100.  In other words, there will be a difference of
        * exactly two digits, which is a sensible precision to see in variation between
        * axis labels.  For example, the ranges listed above would all yield the integer
        * pair 1 to 40 (because the difference between those integers has two digits).
        * Given a pair of integers, the function proceeds to calculate appropriate labels.
        * If we are using the suggested range, we just round the min down to the next nice
        * label and we round the max up to the next nice label (unless the max and min are
        * already on nice labels).
        */

        final double difference = data_max - data_min;
        final double pow = Math.floor(Math.log(difference) / log10) - 1.0;
        // pow is the power of 10 used to get integers of the appropriate range

        int fractDigits = 0;
        if (scale_power > 0)
            fractDigits = scale_power - (int) pow;
        else if (pow < -0.5)
            // we use -0.5 instead of 0.0 in case a Math.floor() returns something that should be 0.0
            // but is really just barely under 0.0
            fractDigits = scale_power - (int) pow;

        final double conversion = Math.pow(10.0, pow);
        // this is the actual conversion factor we used, stored once to keep from
        // having to recalculate it

        int intMin = round(data_min / conversion, false);
        int intMax = round(data_max / conversion, true);
        // we now have intMin and intMax: the integer pair with a two-digit difference

        plot_min = data_min;
        plot_max = data_max;

        final int naturalNumberOfDivisions = intMax - intMin;
        // this number has precisely two digits

        int nDivisions = 0;

        final float idealMinFraction = 0.5f;
        // we will allow as few as this fraction of the maximum number of labels if it is convenient

        int nUnits = 1;
        // this number can represent two things:
        //  a) if naturalNumberOfDivisions < maxNumberOfDivisions
        //     it represents the number of divisions (units) between the natural divisions
        //  b) if naturalNumberOfDivisions > maxNumberOfDivisions
        //     it represents the number of units between divisions (the number to skip between divisions)
        // 1 is the default value, but we will see if a different value might be better

        if (naturalNumberOfDivisions < maxNumberOfDivisions)
        // we might like to put in some new divisions
        {
            final float proximity = (float) naturalNumberOfDivisions / (float) maxNumberOfDivisions;
            // this number measures how close the natural number is to the maximum

            boolean niceDivisionFound = false;
            if (proximity < idealMinFraction)
            // we want to do something because the number we have is below the range we want
            {
            final int[] divisions = {2, 4, 5, 10, 20};
            // These are the numbers of subdivisions we might want to place between natural divisions.
            // The array only goes up to 20 because we would need a plot with at least 200 labels before
            // needing to go any higher.
            for (int i = 0; i < divisions.length; i++)
            {
                    final int candidate = divisions[i];
                    if (proximity * candidate <= 1.0)
                    // this might work, because the number is small enough that we could fit
                    // this many divisions on the axis
                    {
                    niceDivisionFound = true;
                    nUnits = candidate;

                    // the next iteration might be even better, so we...
                    continue;
                    }

                    // if we didn't execute continue, it was because we can't fit this many
                    // divisions on the axis, and so there's no point in trying the next
                    // iteration either because it's even bigger, so we break out of the loop
                    break;
            }
            }
            if (niceDivisionFound)
            {
                    nDivisions = naturalNumberOfDivisions * nUnits + (int) ((plot_max / conversion - intMax) * nUnits);
                    /*
                     * The first term in the expression isn't tough: If nUnits is 2 then we need twice as many
                     * divisions on the axis.  The second term isn't so obvious.  Suppose our axis goes from 0
                     * to 12.7 and we decide to set nUnits to 2.  We therefore get labels 0.0, 0.5, 1.0, ... , 12.0
                     * but we won't get 12.5 on there.  There will instead be empty space where the 12.5 should go.
                     * The last term accounts for this, by taking the difference between the top label and the
                     * axis max (in this case 12.7 - 12.0 = 0.7), multiplying bn nUnits (to get 1.4 in this case)
                     * and truncating (to get 1 in this case).  The result (1 in this case) is the number of labels
                     * extra we need.
                     */

            if (pow < 0.5 || scale_power > 0)
            // we use 0.5 instead of 1.0 in case a Math.floor() returns something
            // that should be 1.0 but is really just barely under 1.0
                    fractDigits++;
                    // we've gone down to one lover decimal level so we have to tell the formatter

            if ((nUnits == 4 || nUnits == 20) && (pow < 1.5 || scale_power > 0)) // we've actually gone down two decimal levels, so...
                    // we use 1.5 instead of 2.0 in case a Math.floor() returns something
                    // that should be 2.0 but is really just barely under 2.0
                    fractDigits++; // we add another
            }
            else
            // we give up and keep the natural number, even though it's smaller that what we'd like
            {
            nDivisions = Math.max(naturalNumberOfDivisions, minNumberOfDivisions);
            }
        }
        else if (naturalNumberOfDivisions > maxNumberOfDivisions)
        // the natural number is larger than what we'd like, so we have to skip over some
        // (typically this is the more common problem)
        {
            nDivisions = 1;
            // we supply this initialization to make the compiler happy, but in the algorithm
            // requires that this initial value change

            final int[] skips = {2, 5, 10, 20, 25, 50};
            // These are the numbers of natural divisions we're going to try skipping.

            for (int i = 0; i < skips.length; i++)
            {
            final int nDivisionsThisTry = naturalNumberOfDivisions / skips[i];
            if (nDivisionsThisTry > maxNumberOfDivisions)
            {
                    // this many skips isn't big enough, so we'll try the next one
                    continue;
            }

            // we now assign to nUnits the number of natural divisions to skip over
            nUnits = skips[i];
            nDivisions = nDivisionsThisTry;

            if (nUnits >= 10 && nUnits != 25 && fractDigits > 0)
                    // we're skipping at least an order of 10, and we're not in quarters, so...
                    fractDigits--; // we can get rid of one fraction digit

            /*
             * We may calculate a new value for the intMin.  Consider, for example,
             * the range 3 to 17.  If we decide that our skip will be 2, we will get
             * labels like 3, 5, 7, 9, etc.  This will look dumb because we would much rather
             * have the first label a nice multiple of our skip (i.e., we would rather
             * have 2, 4, 6, 8, etc., or for multiples of 5 we would rather have 20,
             * 25, 30, 35, etc. over 18, 23, 28, 33, etc.)  Therefore, if the intMin is not
             * a nice multiple of that skip then we increase the intMin, and we may have
             * to decrement nDivisions because of a lost label.
             */
            if (intMin % nUnits != 0)
            // only true if we are not using the suggested range
            {
                    // increase is the amount we will increase intMin by to make it a nice multiple of nUnits
                    final int increase = intMin > 0 ? nUnits - intMin % nUnits : -intMin % nUnits;

                    if (increase > intMax - (intMax - intMin) / nUnits * nUnits - intMin)
                    // we have put the last label over the limit, so...
                    nDivisions--; // we drop the last label

                    intMin += increase;
            }

            // We are happy with what we've got because it gives us an acceptable
            // number of divisions.  We don't want to go any higher because that
            // will just make for fewer divisions, so we...
            break;
         }
        }
        else // hey! they're exactly equal
            nDivisions = Math.max(naturalNumberOfDivisions, minNumberOfDivisions);

        double minLabelValue = intMin * conversion;
        final double inc = naturalNumberOfDivisions < maxNumberOfDivisions ? conversion / nUnits : conversion * nUnits;
        if (naturalNumberOfDivisions < maxNumberOfDivisions && minLabelValue - inc >= plot_min)
        // this happens if we are dividing up divisions, and we get divisions below intMin * conversion
        {
            int nLost = (int) ((minLabelValue - inc) / inc);
            minLabelValue -= nLost * inc;
            nDivisions += nLost;
        }
        labels = new AxisLabel[nDivisions + 1];
        this.nDivisions = nDivisions;

        // System.out.println("fractDigits = " + fractDigits + ", minLabelValue = " + minLabelValue + ", inc = " + inc);

        format.setFractionDigits(fractDigits);
        for (int j = 0; j < labels.length; j++)
        {
            final double labelValue = minLabelValue + j * inc;

            labels[j] = new AxisLabel();
            labels[j].text = format.format(labelValue);
            labels[j].position = (labelValue - plot_min) / (plot_max - plot_min);
            // labels[j].logPosition = Math.log(labels[j].position + Math.E);
            if (labels[j].position < 0.) labels[j].position = 0.;
        }
    }

    private int charsReq(int pow)
    /*
     * Returns the number of characters required (not using scientific
     * notation) to represent a decade of the given order of magnitude.
     */
    {
        if (pow < 0)
            /*
             * If the power is less than 0, we need one space for the
             * leading zero, one for the decimal, and then -pow spaces
             * for each of the following characters.  For example, if
             * pow = -2 then the result would be 4, because there are
             * four characters in the string "0.01".
             */
            return -pow + 2;
        else
            /*
             * If the power is 0 or greater, we need one character for
             * the character '1' and one '0' for each order of magnitude.
             */
            return  pow + 1;
    }

    private int round(final double d, final boolean down)
    /*
     * Determines an integer value from a double by rounding intelligently.
     * In the logarithmic case, when determining the order of magnitude
     * of the lowest tick, the "down" parameter is false because
     * we want to round up from the minimum point in the axis (so that
     * the tick shows up on the range of the axis) if we are not very close
     * to an integer value.  However, when we are determining the order
     * of magnitude of the highest tick, we want to round down if we are
     * not very close to an integer so that the tick appears within the
     * range of the axis.  Similarly in the case for the linear axis, we round
     * up to get the smallest tick value and we round down to get the largest
     * tick value.  We do exactly the opposite if wew are using the suggested
     * range.  In that case, we round down to get the minimum and up to get
     * the maximum.
     */
    {
        final double minProximity = 0.0001;
        /*
         * A parameter's proximity to the nearest integer must be this
         * fraction of its size in order to be considered that value.
         */
        final double round = Math.round(d); // the closest integer value
        if (d == round || Math.abs(d - round) < (d != 0.0 ? minProximity * Math.abs(d) : 0.000001))
        {
            // we assume here that d is close enough to be an integer, so we round and return
            return (int) round;
        }
        else
        {
            // d is not close enough to be an integer, so we return the
            // floor if we were supposed to round down and ceil
            // otherwise
            return down ? (int) Math.floor(d) : (int) Math.ceil(d);
        }
    }

    public String [] getLabels()
    {
        int len = labels.length;
        String [] lab = new String [len];
        for (int i = 0; i < len; ++i)
            lab[i] = labels[i].text;
        return lab;
    }

    public double [] getPositions()
    {
        int len = labels.length;
        double [] pos = new double [len];
        for (int i = 0; i < len; ++i)
            pos[i] = labels[i].position;
        return pos;
    }

    public void printLabels()
    {
      System.out.println("data_min = " + data_min + ", data_max = " + data_max);
     System.out.println("plot_min = " + plot_min + ", plot_max = " + plot_max);
     System.out.println("nDivisions = " + nDivisions + ", labelsValid = " + labelsValid);

     int i;

     for (i = 0; i < labels.length; ++i)
     {
         System.out.println("label "+ (i+1) + ": " + labels[i].text + "  position: " + labels[i].position);
     }
    }



   // This class is used when storing information about
   // the labels on the axis.
   private class AxisLabel
   {
       String text;
       double position;
           // double logPosition;
   }

   /**
    * Just for testing
    */
   public static void main(String[] argv)
   {
       AxisLabelCalculator t = new AxisLabelCalculator();
       t.createNewLabels(0., 1.);
       t.printLabels();
       t.createNewLabels(0., 100.);
       t.printLabels();
       t.createNewLabels(0.0001, 0.001);
       t.printLabels();
       t.createNewLabels(5., 10.);
       t.printLabels();
       t.createNewLabels(.001, .050);
       t.printLabels();
       t.createNewLabels(.00001, .00005);
       t.printLabels();
       t.createNewLabels(-.001, .00005);
       t.printLabels();
       t.createNewLabels(-.001, .005);
       t.printLabels();
       t.createNewLabels(-2., 5.);
       t.printLabels();
       t.createNewLabels(.05, 10.);
       t.printLabels();

    }

}