88

我正在编写一些代码来在我们的软件中显示条形图(或线形图)。一切都很顺利。让我难过的是标记 Y 轴。

来电者可以告诉我他们想要标记 Y 刻度的精细程度,但我似乎被困在以“有吸引力”的方式标记它们的确切内容上。我无法形容“有吸引力”,也许你也不能,但我们一看到就知道,对吧?

因此,如果数据点是:

   15, 234, 140, 65, 90

用户在 Y 轴上要求 10 个标签,用纸和铅笔稍微摸索得出:

  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 并称它足够接近就可以了。当某些点为负数时该怎么办?

我可以看到 Excel 很好地解决了这个问题。

有谁知道解决这个问题的通用算法(即使是一些蛮力也可以)?我不必很快做,但它应该看起来不错。

4

13 回答 13

107

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

  • 确定数据的下限和上限。(请注意下限 = 上限的特殊情况!
  • 将范围划分为所需的刻度数。
  • 将刻度范围向上舍入为合适的数量。
  • 相应地调整下限和上限。

让我们举个例子:

15, 234, 140, 65, 90 with 10 ticks
  1. 下限 = 15
  2. 上限 = 234
  3. 范围 = 234-15 = 219
  4. 刻度范围 = 21.9。这应该是 25.0
  5. 新的下限 = 25 * round(15/25) = 0
  6. 新上限 = 25 * 轮次 (1+235/25) = 250

所以范围 = 0,25,50,...,225,250

您可以通过以下步骤获得不错的刻度范围:

  1. 除以 10^x 使得结果介于 0.1 和 1.0 之间(包括 0.1,不包括 1)。
  2. 相应地翻译:
    • 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. 乘以 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。
  5. 新的下限 = 30 * round(15/30) = 0
  6. 新上限 = 30 * 轮次 (1+235/30) = 240

这给出了您要求的结果;-)。

------ 由 KD 添加 ------

这是在不使用查找表等的情况下实现此算法的代码......:

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 轴段比刻度数少一。

于 2008-11-28T21:44:39.220 回答
22

这是我正在使用的 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
)
于 2012-01-25T17:49:26.763 回答
9

试试这个代码。我已经在一些图表场景中使用它并且效果很好。它也非常快。

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;
    }
}
于 2009-10-06T12:07:51.030 回答
6

听起来来电者没有告诉你它想要的范围。

因此,您可以自由更改端点,直到您的标签数可以很好地整除为止。

让我们定义“好”。如果标签关闭,我会打电话给:

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.

现在您需要做的就是找到 3 个值:

- 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

直到偏移量“不错”

于 2008-11-28T21:33:08.540 回答
3

我仍在与此作斗争:)

最初的 Gamecat 答案似乎在大多数情况下都有效,但尝试插入说,“3 个滴答声”作为所需滴答声的数量(对于相同的数据值 15、234、140、65、90)......似乎给出的刻度范围为 73,除以 10^2 后得到 0.73,映射到 0.75,给出了 75 的“不错”刻度范围。

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

下限:75 * round(15/75) = 0

但很明显,如果你从 0 开始,并以 75 的步长一直到 300 的上限,你最终会得到 0,75,150,225,300 ....这无疑是有用的,但它是 4 个刻度(不包括 0)而不是需要 3 个刻度。

只是令人沮丧的是它不能 100% 的工作......当然这很可能是我在某个地方的错误!

于 2012-11-10T11:43:37.677 回答
3

Toon Krijthe的答案在大多数情况下确实有效。但有时它会产生过多的滴答声。它也不适用于负数。解决问题的总体方法是可以的,但有更好的方法来处理这个问题。您要使用的算法将取决于您真正想要得到的。下面我将向您展示我在 JS Ploting 库中使用的代码。我已经对其进行了测试,并且它始终有效(希望 ;) )。以下是主要步骤:

  • 获取全局极值 xMin 和 xMax(包括要在算法中打印的所有图)
  • 计算 xMin 和 xMax 之间的范围
  • 计算范围的数量级
  • 通过将范围除以刻度数减一来计算刻度大小
  • 这个是可选的。如果您希望始终打印零刻度,您可以使用刻度大小来计算正刻度和负刻度的数量。刻度的总数将是它们的总和 + 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);

我将最小值和最大值四舍五入为 100%,确保我的绘图将涵盖所有数据。将范围的 log10 取底也很重要,无论它是否为负数,然后再减去 1。否则,您的算法将不适用于小于一的数字。

    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;
    }
}

这里只有一件事不包括在内。这是“好看的界限”。这些是与“漂亮的刻度”中的数字相似的数字的下限。例如,下限从 5 开始且刻度大小为 5 比从 6 开始且刻度大小相同的绘图要好。但这我被解雇了,我把它留给你。

希望能帮助到你。干杯!

于 2017-04-02T11:49:59.917 回答
3

将此答案转换为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
    }

}
于 2019-03-13T21:00:50.553 回答
1

这就像一个魅力,如果你想要 10 步 + 零

//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
于 2012-07-01T16:59:10.693 回答
1

对于任何在 ES5 Javascript 中需要它的人来说,都有些挣扎,但这里是:

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);    

基于 Toon Krijtje 的出色回答。

于 2019-06-04T10:36:30.683 回答
1

该解决方案基于我找到 的一个Java 示例。

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]

于 2019-10-28T15:22:13.820 回答
0

基于@Gamecat的算法,我生成了以下帮助类

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);
    }
}
于 2012-12-21T11:26:01.103 回答
0

上述算法没有考虑最小值和最大值之间的范围太小的情况。如果这些值远高于零怎么办?然后,我们有可能以高于零的值开始 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;
}
于 2016-09-30T12:08:08.357 回答
0

已接受答案的演示

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>

于 2021-08-09T07:59:38.037 回答