Choosing an attractive linear scale for a graph's Y Axis

后端 未结 13 1425
予麋鹿
予麋鹿 2020-12-07 08:42

I\'m writing a bit of code to display a bar (or line) graph in our software. Everything\'s going fine. The thing that\'s got me stumped is labeling the Y axis.

The

相关标签:
13条回答
  • 2020-12-07 08:47

    Based on @Gamecat's algorithm, I produced the following helper class

    public struct Interval
    {
        public readonly double Min, Max, TickRange;
    
        public static Interval Find(double min, double max, int tickCount, double padding = 0.05)
        {
            double range = max - min;
            max += range*padding;
            min -= range*padding;
    
            var attempts = new List<Interval>();
            for (int i = tickCount; i > tickCount / 2; --i)
                attempts.Add(new Interval(min, max, i));
    
            return attempts.MinBy(a => a.Max - a.Min);
        }
    
        private Interval(double min, double max, int tickCount)
        {
            var candidates = (min <= 0 && max >= 0 && tickCount <= 8) ? new[] {2, 2.5, 3, 4, 5, 7.5, 10} : new[] {2, 2.5, 5, 10};
    
            double unroundedTickSize = (max - min) / (tickCount - 1);
            double x = Math.Ceiling(Math.Log10(unroundedTickSize) - 1);
            double pow10X = Math.Pow(10, x);
            TickRange = RoundUp(unroundedTickSize/pow10X, candidates) * pow10X;
            Min = TickRange * Math.Floor(min / TickRange);
            Max = TickRange * Math.Ceiling(max / TickRange);
        }
    
        // 1 < scaled <= 10
        private static double RoundUp(double scaled, IEnumerable<double> candidates)
        {
            return candidates.First(candidate => scaled <= candidate);
        }
    }
    
    0 讨论(0)
  • 2020-12-07 08:49

    A long time ago I have written a graph module that covered this nicely. Digging in the grey mass gets the following:

    • Determine lower and upper bound of the data. (Beware of the special case where lower bound = upper bound!
    • Divide range into the required amount of ticks.
    • Round the tick range up into nice amounts.
    • Adjust the lower and upper bound accordingly.

    Lets take your example:

    15, 234, 140, 65, 90 with 10 ticks
    
    1. lower bound = 15
    2. upper bound = 234
    3. range = 234-15 = 219
    4. tick range = 21.9. This should be 25.0
    5. new lower bound = 25 * round(15/25) = 0
    6. new upper bound = 25 * round(1+235/25) = 250

    So the range = 0,25,50,...,225,250

    You can get the nice tick range with the following steps:

    1. divide by 10^x such that the result lies between 0.1 and 1.0 (including 0.1 excluding 1).
    2. translate accordingly:
      • 0.1 -> 0.1
      • <= 0.2 -> 0.2
      • <= 0.25 -> 0.25
      • <= 0.3 -> 0.3
      • <= 0.4 -> 0.4
      • <= 0.5 -> 0.5
      • <= 0.6 -> 0.6
      • <= 0.7 -> 0.7
      • <= 0.75 -> 0.75
      • <= 0.8 -> 0.8
      • <= 0.9 -> 0.9
      • <= 1.0 -> 1.0
    3. multiply by 10^x.

    In this case, 21.9 is divided by 10^2 to get 0.219. This is <= 0.25 so we now have 0.25. Multiplied by 10^2 this gives 25.

    Lets take a look at the same example with 8 ticks:

    15, 234, 140, 65, 90 with 8 ticks
    
    1. lower bound = 15
    2. upper bound = 234
    3. range = 234-15 = 219
    4. tick range = 27.375
      1. Divide by 10^2 for 0.27375, translates to 0.3, which gives (multiplied by 10^2) 30.
    5. new lower bound = 30 * round(15/30) = 0
    6. new upper bound = 30 * round(1+235/30) = 240

    Which give the result you requested ;-).

    ------ Added by KD ------

    Here's code that achieves this algorithm without using lookup tables, etc...:

    double range = ...;
    int tickCount = ...;
    double unroundedTickSize = range/(tickCount-1);
    double x = Math.ceil(Math.log10(unroundedTickSize)-1);
    double pow10x = Math.pow(10, x);
    double roundedTickRange = Math.ceil(unroundedTickSize / pow10x) * pow10x;
    return roundedTickRange;
    

    Generally speaking, the number of ticks includes the bottom tick, so the actual y-axis segments are one less than the number of ticks.

    0 讨论(0)
  • 2020-12-07 08:49

    Here is a PHP example I am using. This function returns an array of pretty Y axis values that encompass the min and max Y values passed in. Of course, this routine could also be used for X axis values.

    It allows you to "suggest" how many ticks you might want, but the routine will return what looks good. I have added some sample data and shown the results for these.

    #!/usr/bin/php -q
    <?php
    
    function makeYaxis($yMin, $yMax, $ticks = 10)
    {
      // This routine creates the Y axis values for a graph.
      //
      // Calculate Min amd Max graphical labels and graph
      // increments.  The number of ticks defaults to
      // 10 which is the SUGGESTED value.  Any tick value
      // entered is used as a suggested value which is
      // adjusted to be a 'pretty' value.
      //
      // Output will be an array of the Y axis values that
      // encompass the Y values.
      $result = array();
      // If yMin and yMax are identical, then
      // adjust the yMin and yMax values to actually
      // make a graph. Also avoids division by zero errors.
      if($yMin == $yMax)
      {
        $yMin = $yMin - 10;   // some small value
        $yMax = $yMax + 10;   // some small value
      }
      // Determine Range
      $range = $yMax - $yMin;
      // Adjust ticks if needed
      if($ticks < 2)
        $ticks = 2;
      else if($ticks > 2)
        $ticks -= 2;
      // Get raw step value
      $tempStep = $range/$ticks;
      // Calculate pretty step value
      $mag = floor(log10($tempStep));
      $magPow = pow(10,$mag);
      $magMsd = (int)($tempStep/$magPow + 0.5);
      $stepSize = $magMsd*$magPow;
    
      // build Y label array.
      // Lower and upper bounds calculations
      $lb = $stepSize * floor($yMin/$stepSize);
      $ub = $stepSize * ceil(($yMax/$stepSize));
      // Build array
      $val = $lb;
      while(1)
      {
        $result[] = $val;
        $val += $stepSize;
        if($val > $ub)
          break;
      }
      return $result;
    }
    
    // Create some sample data for demonstration purposes
    $yMin = 60;
    $yMax = 330;
    $scale =  makeYaxis($yMin, $yMax);
    print_r($scale);
    
    $scale = makeYaxis($yMin, $yMax,5);
    print_r($scale);
    
    $yMin = 60847326;
    $yMax = 73425330;
    $scale =  makeYaxis($yMin, $yMax);
    print_r($scale);
    ?>
    

    Result output from sample data

    # ./test1.php
    Array
    (
        [0] => 60
        [1] => 90
        [2] => 120
        [3] => 150
        [4] => 180
        [5] => 210
        [6] => 240
        [7] => 270
        [8] => 300
        [9] => 330
    )
    
    Array
    (
        [0] => 0
        [1] => 90
        [2] => 180
        [3] => 270
        [4] => 360
    )
    
    Array
    (
        [0] => 60000000
        [1] => 62000000
        [2] => 64000000
        [3] => 66000000
        [4] => 68000000
        [5] => 70000000
        [6] => 72000000
        [7] => 74000000
    )
    
    0 讨论(0)
  • 2020-12-07 08:54

    Converted this answer as Swift 4

    extension Int {
    
        static func makeYaxis(yMin: Int, yMax: Int, ticks: Int = 10) -> [Int] {
            var yMin = yMin
            var yMax = yMax
            var ticks = ticks
            // This routine creates the Y axis values for a graph.
            //
            // Calculate Min amd Max graphical labels and graph
            // increments.  The number of ticks defaults to
            // 10 which is the SUGGESTED value.  Any tick value
            // entered is used as a suggested value which is
            // adjusted to be a 'pretty' value.
            //
            // Output will be an array of the Y axis values that
            // encompass the Y values.
            var result = [Int]()
            // If yMin and yMax are identical, then
            // adjust the yMin and yMax values to actually
            // make a graph. Also avoids division by zero errors.
            if yMin == yMax {
                yMin -= ticks   // some small value
                yMax += ticks   // some small value
            }
            // Determine Range
            let range = yMax - yMin
            // Adjust ticks if needed
            if ticks < 2 { ticks = 2 }
            else if ticks > 2 { ticks -= 2 }
    
            // Get raw step value
            let tempStep: CGFloat = CGFloat(range) / CGFloat(ticks)
            // Calculate pretty step value
            let mag = floor(log10(tempStep))
            let magPow = pow(10,mag)
            let magMsd = Int(tempStep / magPow + 0.5)
            let stepSize = magMsd * Int(magPow)
    
            // build Y label array.
            // Lower and upper bounds calculations
            let lb = stepSize * Int(yMin/stepSize)
            let ub = stepSize * Int(ceil(CGFloat(yMax)/CGFloat(stepSize)))
            // Build array
            var val = lb
            while true {
                result.append(val)
                val += stepSize
                if val > ub { break }
            }
            return result
        }
    
    }
    
    0 讨论(0)
  • 2020-12-07 08:54

    this works like a charm, if you want 10 steps + zero

    //get proper scale for y
    $maximoyi_temp= max($institucion); //get max value from data array
     for ($i=10; $i< $maximoyi_temp; $i=($i*10)) {   
        if (($divisor = ($maximoyi_temp / $i)) < 2) break; //get which divisor will give a number between 1-2    
     } 
     $factor_d = $maximoyi_temp / $i;
     $factor_d = ceil($factor_d); //round up number to 2
     $maximoyi = $factor_d * $i; //get new max value for y
     if ( ($maximoyi/ $maximoyi_temp) > 2) $maximoyi = $maximoyi /2; //check if max value is too big, then split by 2
    
    0 讨论(0)
  • 2020-12-07 08:56

    Try this code. I've used it in a few charting scenarios and it works well. It's pretty fast too.

    public static class AxisUtil
    {
        public static float CalculateStepSize(float range, float targetSteps)
        {
            // calculate an initial guess at step size
            float tempStep = range/targetSteps;
    
            // get the magnitude of the step size
            float mag = (float)Math.Floor(Math.Log10(tempStep));
            float magPow = (float)Math.Pow(10, mag);
    
            // calculate most significant digit of the new step size
            float magMsd = (int)(tempStep/magPow + 0.5);
    
            // promote the MSD to either 1, 2, or 5
            if (magMsd > 5.0)
                magMsd = 10.0f;
            else if (magMsd > 2.0)
                magMsd = 5.0f;
            else if (magMsd > 1.0)
                magMsd = 2.0f;
    
            return magMsd*magPow;
        }
    }
    
    0 讨论(0)
提交回复
热议问题