为graphics的Y轴select一个有吸引力的线性比例尺

我正在写一些代码来在我们的软件中显示一个条形图(或线条)graphics。 一切都很好。 我难住的东西是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很好地解决了这个问题。

有没有人知道一个通​​用的algorithm(甚至一些蛮力是好的)来解决这个问题? 我不必很快就做,但应该看起来不错。

O我很久以前O写了一个图表模块,很好地覆盖了这一点。 挖掘灰色质量获得以下内容:

  • 确定数据的下限和上限。 (谨防下界=上界的特例!
  • 将范围划分为所需的蜱数量。
  • 将勾号范围整理成好的数量。
  • 相应地调整下限和上限。

让我们来看看你的例子:

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

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

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

  1. 除以10 ^ x,结果在0.1和1.0之间(包括0.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 *轮(15/30)= 0
  6. 新上限= 30 *轮(1 + 235/30)= 240

哪个给你要求的结果;-)。

——由KD添加——

这里的代码,实现这个algorithm,而不使用查找表等…:

 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轴线比蜱的数量less一个。

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

它允许你“build议”你可能需要多less个蜱,但是例程会返回看起来不错的东西。 我已经添加了一些示例数据并显示了这些结果。

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

试试这个代码。 我已经在几个图表的情况下使用它,它运作良好。 它也很快。

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

听起来就像来电者不告诉你想要的范围。

所以你可以自由地改变终点,直到你能很好的被你的标签计数整除。

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

 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. 

现在你需要做的就是find3个值:

 - 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 

有可能有很多解决scheme,所以只需select一个。 大多数时候我敢打赌,你可以设置

 start_label to 0 

所以只是尝试不同的整数

 end_label 

直到偏移是“好”

我还在这个:)

最初的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%的时间工作….这当然可能是我的错误!

这个工程就像一个魅力,如果你想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 

Toon Krijthe的答案大部分时间都在工作。 但是有时会产生过多的蜱虫。 它也不适用于负数。 问题的总体方法是好的,但有一个更好的方法来处理这个问题。 你想使用的algorithm将取决于你真正想要得到什么。 下面我向你介绍我在我的JS Ploting库中使用的代码。 我testing过它,它总是有效(希望;))。 主要步骤如下:

  • 获取全局极值xMin和xMax(包含您要在algorithm中打印的所有图)
  • 计算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。 否则你的algorithm将不适用于小于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。 剧情看起来更专业这种方式。 你会在这个答案的结尾find所有的方法。

现在你必须计算上限和下限。 这是非常容易与零刻度,但需要在其他情况下多一点努力。 为什么? 因为我们想把这个情节很好地集中在上下限内。 看看我的代码。 其中一些variables是在这个范围之外定义的,其中一些variables是保存整个代码的对象的属性。

  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开始的绘图。 但是,我把这一切解雇给你。

希望能帮助到你。 干杯!

感谢您的问题和答案,非常有帮助。 Gamecat,我想知道你是如何决定什么范围应该四舍五入。

刻度范围= 21.9。 这应该是25.0

为了在algorithm上做到这一点,人们不得不在上面的algorithm中添加逻辑来使这个规模很好地适用于更大的数字? 例如,如果有10个刻度,如果范围是3346,那么刻度范围将评估为334.6,当350可能更好时,舍入到最接近的10会得出340。

你怎么看?

基于@Gencat的algorithm,我产生了以下帮助类

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

上述algorithm没有考虑到最小值和最大值之间的范围太小的情况。 如果这些值比零大得多呢? 那么,我们就有可能以大于零的值开始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; }