代码之家  ›  专栏  ›  技术社区  ›  Clinton Pierce

为图形的Y轴选择有吸引力的线性比例

  •  66
  • Clinton Pierce  · 技术社区  · 16 年前

    我正在编写一段代码,以便在我们的软件中显示条形图(或线形图)。一切都很好。让我困惑的是给Y轴贴标签。

    打电话的人可以告诉我他们想把Y刻度标得有多细,但我似乎不知道该用什么样的“吸引人的”方式给他们标上标签。我不能形容“有吸引力”,也许你也不能,但我们看到它就知道了,对吗?

    因此,如果数据点为:

       15, 234, 140, 65, 90
    

      0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250
    

    所以这里有10个(不包括0),最后一个扩展到最高值(234<250)之外,每个值的“不错”增量为25。如果他们要求8个标签,那么增加30个标签看起来不错:

      0, 30, 60, 90, 120, 150, 180, 210, 240
    

    九个可能会很棘手。也许只要使用8或10,并称之为足够接近就可以了。当某些点为负值时,该怎么办?

    有人知道解决这个问题的通用算法吗(即使使用蛮力也可以)?我不需要很快就完成,但它看起来应该很漂亮。

    10 回复  |  直到 16 年前
        1
  •  103
  •   Toon Krijthe    6 年前

    很久以前,我编写了一个图形模块,很好地涵盖了这一点。挖掘灰色体量可获得以下结果:

    • 将范围划分为所需的刻度数。
    • 将蜱虫范围四舍五入到合适的数量。
    • 相应地调整下限和上限。

    15, 234, 140, 65, 90 with 10 ticks
    
    1. 下限=15
    2. 上限=234
    3. 新下限=25*轮(15/25)=0
    4. 新上限=25*轮(1+235/25)=250

    所以范围=0,25,50,…,225250

    通过以下步骤,您可以获得良好的刻度范围:

    1. 除以10^x,使结果介于0.1和1.0之间(包括0.1,不包括1)。
      • 0.1->0.1
      • <=0.2->0.2
      • <
      • <=0.3->0.3
      • <=0.4->0.4
      • <
      • <
      • <=0.7->0.7
      • <=0.75->0.75
      • <=0.8->0.8
      • <=0.9->0.9
      • <
    2. 乘以10^x。

    在这种情况下,21.9除以10^2得到0.219。这是<=0.25,我们现在有0.25。乘以10^2等于25。

    让我们看一看带有8个刻度的相同示例:

    15, 234, 140, 65, 90 with 8 ticks
    
    1. 下限=15
    2. 上限=234
    3. 范围=234-15=219
    4. 刻度范围=27.375
      1. 除以10^2等于0.27375,转换为0.3,即(乘以10^2)30。

    下面是实现此算法而不使用查找表等的代码:

    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;
    

    一般来说,记号数包括底部记号,因此实际的y轴段比记号数少一个。

        2
  •  22
  •   Spooky Muscothym    10 年前

    下面是我正在使用的一个PHP示例。此函数返回包含传入的最小和最大Y值的漂亮Y轴值数组。当然,此例程也可以用于X轴值。

    看起来不错的。我添加了一些示例数据,并展示了这些示例的结果。

    #!/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);
    ?>
    

    样本数据的结果输出

    # ./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
    )
    
        3
  •  9
  •   Spooky Muscothym    10 年前

    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;
        }
    }
    
        4
  •  6
  •   Pyrolistical    16 年前

    听起来打电话的人没有告诉你他想要的范围。

    因此,您可以自由更改端点,直到它可以很好地被标签计数整除。

    1. 2^n, for some integer n. eg. ..., .25, .5, 1, 2, 4, 8, 16, ...
    2. 10^n, for some integer n. eg. ..., .01, .1, 1, 10, 100
    3. n/5 == 0, for some positive integer n, eg, 5, 10, 15, 20, 25, ...
    4. n/2 == 0, for some positive integer n, eg, 2, 4, 6, 8, 10, 12, 14, ...
    

    查找数据系列的最大值和最小值。让我们把这几点称为:

    min_point and max_point.
    

    - start_label, where start_label < min_point and start_label is an integer
    - end_label, where end_label > max_point and end_label is an integer
    - label_offset, where label_offset is "nice"
    

    这符合方程式:

    (end_label - start_label)/label_offset == label_count
    

    可能有很多解决方案,所以就挑一个吧。大多数时候,我打赌你可以设置

    start_label to 0
    

    所以试试不同的整数

    end_label
    

    直到偏移量“很好”

        5
  •  3
  •   StillPondering    12 年前

    Gamecat最初的答案似乎大部分时间都有效,但试着插入“3个刻度”作为所需的刻度数(对于相同的数据值15、234、140、65、90)……它似乎给出了73的刻度范围,除以10^2后得到0.73,对应于0.75,这给出了75的“不错”刻度范围。

    然后计算上限: 75*圆形(1+234/75)=300

    下限是: 75*圆形(15/75)=0

    …这无疑是有用的,但它是4个刻度(不包括0)而不是所需的3个刻度。

    只是令人沮丧的是,它没有100%的时间工作…这很可能是由于我的错误,当然!

        6
  •  3
  •   Community CDub    8 年前

    答案是 Toon Krijthe 他大部分时间都在工作。但有时它会产生过多的蜱虫。负数也不行。解决这个问题的总体方法是可以的,但是有更好的方法来处理这个问题。您想要使用的算法将取决于您真正想要得到什么。下面我将向您展示我在JS绘图库中使用的代码。我已经测试过了,而且它总是有效的(希望如此)。以下是主要步骤:

    • 获取全局极值xMin和xMax(包括要在算法中打印的所有绘图)
    • 计算xMin和xMax之间的范围
    • 通过将范围除以记号数减1来计算记号大小
    • 这个是可选的。如果要始终打印零刻度,请使用刻度大小来计算正刻度和负刻度的数量。勾号总数将为其总和+1(零勾号)
    • 如果一直打印零刻度,则不需要此选项。计算下限和上限,但记住将绘图居中

    让我们开始吧。首先是基本计算

        var range = Math.abs(xMax - xMin); //both can be negative
        var rangeOrder = Math.floor(Math.log10(range)) - 1; 
        var power10 = Math.pow(10, rangeOrder);
        var maxRound = (xMax > 0) ? Math.ceil(xMax / power10) : Math.floor(xMax / power10);
        var minRound = (xMin < 0) ? Math.floor(xMin / power10) : Math.ceil(xMin / power10);
    

        var fullRange = Math.abs(maxRound - minRound);
        var tickSize = Math.ceil(fullRange / (this.XTickCount - 1));
    
        //You can set nice looking ticks if you want
        //You can find exemplary method below 
        tickSize = this.NiceLookingTick(tickSize);
    
        //Here you can write a method to determine if you need zero tick
        //You can find exemplary method below
        var isZeroNeeded = this.HasZeroTick(maxRound, minRound, tickSize);
    

    我使用“好看的蜱虫”来避免像7、13、17等蜱虫。我在这里使用的方法非常简单。在需要时使用zeroTick也很好。这样的情节看起来更专业。你会在这个答案的末尾找到所有的方法。

    现在你必须计算上界和下界。这对于零滴答非常容易,但在其他情况下需要更多的努力。为什么?因为我们想把图的中心放在上限和下限之间。看看我的代码。有些变量是在这个范围之外定义的,有些变量是保存整个代码的对象的属性。

        if (isZeroNeeded) {
    
            var positiveTicksCount = 0;
            var negativeTickCount = 0;
    
            if (maxRound != 0) {
    
                positiveTicksCount = Math.ceil(maxRound / tickSize);
                XUpperBound = tickSize * positiveTicksCount * power10;
            }
    
            if (minRound != 0) {
                negativeTickCount = Math.floor(minRound / tickSize);
                XLowerBound = tickSize * negativeTickCount * power10;
            }
    
            XTickRange = tickSize * power10;
            this.XTickCount = positiveTicksCount - negativeTickCount + 1;
        }
        else {
            var delta = (tickSize * (this.XTickCount - 1) - fullRange) / 2.0;
    
            if (delta % 1 == 0) {
                XUpperBound = maxRound + delta;
                XLowerBound = minRound - delta;
            }
            else {
                XUpperBound =  maxRound + Math.ceil(delta);
                XLowerBound =  minRound - Math.floor(delta);
            }
    
            XTickRange = tickSize * power10;
            XUpperBound = XUpperBound * power10;
            XLowerBound = XLowerBound * power10;
        }
    

    this.NiceLookingTick = function (tickSize) {
    
        var NiceArray = [1, 2, 2.5, 3, 4, 5, 10];
    
        var tickOrder = Math.floor(Math.log10(tickSize));
        var power10 = Math.pow(10, tickOrder);
        tickSize = tickSize / power10;
    
        var niceTick;
        var minDistance = 10;
        var index = 0;
    
        for (var i = 0; i < NiceArray.length; i++) {
            var dist = Math.abs(NiceArray[i] - tickSize);
            if (dist < minDistance) {
                minDistance = dist;
                index = i;
            }
        }
    
        return NiceArray[index] * power10;
    }
    
    this.HasZeroTick = function (maxRound, minRound, tickSize) {
    
        if (maxRound * minRound < 0)
        {
            return true;
        }
        else if (Math.abs(maxRound) < tickSize || Math.round(minRound) < tickSize) {
    
            return true;
        }
        else {
    
            return false;
        }
    }
    

    干杯

        7
  •  3
  •   Petr Syrov    6 年前

    转换这个 answer 斯威夫特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
        }
    
    }
    
        8
  •  1
  •   mario    12 年前

    如果你想要10步+0,这就像一个符咒

    //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
    
        9
  •  1
  •   Hjalmar Snoep    6 年前

    var min=52;
    var max=173;
    var actualHeight=500; // 500 pixels high graph
    
    var tickCount =Math.round(actualHeight/100); 
    // we want lines about every 100 pixels.
    
    if(tickCount <3) tickCount =3; 
    var range=Math.abs(max-min);
    var unroundedTickSize = range/(tickCount-1);
    var x = Math.ceil(Math.log10(unroundedTickSize)-1);
    var pow10x = Math.pow(10, x);
    var roundedTickRange = Math.ceil(unroundedTickSize / pow10x) * pow10x;
    var min_rounded=roundedTickRange * Math.floor(min/roundedTickRange);
    var max_rounded= roundedTickRange * Math.ceil(max/roundedTickRange);
    var nr=tickCount;
    var str="";
    for(var x=min_rounded;x<=max_rounded;x+=roundedTickRange)
    {
        str+=x+", ";
    }
    console.log("nice Y axis "+str);    
    

        10
  •  1
  •   chickens    5 年前

    此解决方案基于 Java example 我找到了。

    const niceScale = ( minPoint, maxPoint, maxTicks) => {
        const niceNum = ( localRange,  round) => {
            var exponent,fraction,niceFraction;
            exponent = Math.floor(Math.log10(localRange));
            fraction = localRange / Math.pow(10, exponent);
            if (round) {
                if (fraction < 1.5) niceFraction = 1;
                else if (fraction < 3) niceFraction = 2;
                else if (fraction < 7) niceFraction = 5;
                else niceFraction = 10;
            } else {
                if (fraction <= 1) niceFraction = 1;
                else if (fraction <= 2) niceFraction = 2;
                else if (fraction <= 5) niceFraction = 5;
                else niceFraction = 10;
            }
            return niceFraction * Math.pow(10, exponent);
        }
        const result = [];
        const range = niceNum(maxPoint - minPoint, false);
        const stepSize = niceNum(range / (maxTicks - 1), true);
        const lBound = Math.floor(minPoint / stepSize) * stepSize;
        const uBound = Math.ceil(maxPoint / stepSize) * stepSize;
        for(let i=lBound;i<=uBound;i+=stepSize) result.push(i);
        return result;
    };
    console.log(niceScale(15,234,6));
    // > [0, 100, 200, 300]
        11
  •  0
  •   theringostarrs    16 年前

    谢谢你的提问和回答,非常有帮助。Gamecat,我想知道你是如何决定滴答声范围应该四舍五入到什么程度的。

    要在算法上做到这一点,就必须在上面的算法中添加逻辑,以便对较大的数字进行良好的缩放?

    你怎么认为?

        12
  •  0
  •   Neil    12 年前

    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);
        }
    }
    
        13
  •  0
  •   panos    8 年前

    上述算法没有考虑最小值和最大值之间的范围太小的情况。如果这些值远远高于零呢?然后,我们有可能以大于零的值开始y轴。此外,为了避免我们的线条完全位于图表的上方或下方,我们必须给它一些“呼吸的空气”。

    为了涵盖这些情况,我(在PHP上)编写了上述代码:

    function calculateStartingPoint($min, $ticks, $times, $scale) {
    
        $starting_point = $min - floor((($ticks - $times) * $scale)/2);
    
        if ($starting_point < 0) {
            $starting_point = 0;
        } else {
            $starting_point = floor($starting_point / $scale) * $scale;
            $starting_point = ceil($starting_point / $scale) * $scale;
            $starting_point = round($starting_point / $scale) * $scale;
        }
        return $starting_point;
    }
    
    function calculateYaxis($min, $max, $ticks = 7)
    {
        print "Min = " . $min . "\n";
        print "Max = " . $max . "\n";
    
        $range = $max - $min;
        $step = floor($range/$ticks);
        print "First step is " . $step . "\n";
        $available_steps = array(5, 10, 20, 25, 30, 40, 50, 100, 150, 200, 300, 400, 500);
        $distance = 1000;
        $scale = 0;
    
        foreach ($available_steps as $i) {
            if (($i - $step < $distance) && ($i - $step > 0)) {
                $distance = $i - $step;
                $scale = $i;
            }
        }
    
        print "Final scale step is " . $scale . "\n";
    
        $times = floor($range/$scale);
        print "range/scale = " . $times . "\n";
    
        print "floor(times/2) = " . floor($times/2) . "\n";
    
        $starting_point = calculateStartingPoint($min, $ticks, $times, $scale);
    
        if ($starting_point + ($ticks * $scale) < $max) {
            $ticks += 1;
        }
    
        print "starting_point = " . $starting_point . "\n";
    
        // result calculation
        $result = [];
        for ($x = 0; $x <= $ticks; $x++) {
            $result[] = $starting_point + ($x * $scale);
        }
        return $result;
    }
    
        14
  •  0
  •   gogo    3 年前

    演示 accepted answer

    function tickEvery(range, ticks) {
      return Math.ceil((range / ticks) / Math.pow(10, Math.ceil(Math.log10(range / ticks) - 1))) * Math.pow(10, Math.ceil(Math.log10(range / ticks) - 1));
    }
    
    function update() {
      const range = document.querySelector("#range").value;
      const ticks = document.querySelector("#ticks").value;
      const result = tickEvery(range, ticks);
      document.querySelector("#result").textContent = `With range ${range} and ${ticks} ticks, tick every ${result} for a total of ${Math.ceil(range / result)} ticks at ${new Array(Math.ceil(range / result)).fill(0).map((v, n) => Math.round(n * result)).join(", ")}`;
    }
    
    update();
    <input id="range" min="1" max="10000" oninput="update()" style="width:100%" type="range" value="5000" width="40" />
    <br/>
    <input id="ticks" min="1" max="20" oninput="update()" type="range" style="width:100%" value="10" />
    <p id="result" style="font-family:sans-serif"></p>