为图形的 Y 轴选择有吸引力的线性比例
Posted
技术标签:
【中文标题】为图形的 Y 轴选择有吸引力的线性比例【英文标题】:Choosing an attractive linear scale for a graph's Y Axis 【发布时间】:2010-09-24 12:14:31 【问题描述】:我正在编写一些代码来在我们的软件中显示条形(或线)图。一切都很顺利。让我难过的是标记 Y 轴。
来电者可以告诉我他们想要标记 Y 刻度的精细程度,但我似乎被困在以“有吸引力”的方式标记它们的确切内容上。我无法形容“有吸引力”,你可能也不会,但我们看到它就知道了,对吧?
所以如果数据点是:
15, 234, 140, 65, 90
而用户在 Y 轴上要求 10 个标签,用纸和铅笔稍微摸索得出:
0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250
所以那里有 10 个(不包括 0),最后一个超出最大值(234
0, 30, 60, 90, 120, 150, 180, 210, 240
九个会很棘手。也许只是使用了 8 或 10 并称它足够接近就可以了。当有些点是负数时该怎么办?
我可以看到 Excel 很好地解决了这个问题。
有谁知道解决这个问题的通用算法(即使是一些蛮力也可以)?我不必很快做,但它应该看起来不错。
【问题讨论】:
这里有一些关于 Excel 如何为其 Y 轴选择最大值和最小值的信息:support.microsoft.com/kb/214075 不错的实现:***.com/a/16363437/829571 【参考方案1】:听起来调用者没有告诉你它想要的范围。
因此,您可以随意更改端点,直到您的标签数可以很好地整除为止。
让我们定义“好”。如果标签关闭,我会打电话给:
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
直到偏移量“不错”
【讨论】:
【参考方案2】:很久以前,我编写了一个很好地涵盖了这一点的图形模块。挖掘灰色块得到以下结果:
确定数据的下限和上限。 (请注意下限 = 上限的特殊情况! 将范围划分为所需的刻度数。 将刻度范围向上取整。 相应地调整下限和上限。让我们举个例子:
15, 234, 140, 65, 90 with 10 ticks
-
下限 = 15
上限 = 234
范围 = 234-15 = 219
刻度范围 = 21.9。这应该是 25.0
新的下限 = 25 * round(15/25) = 0
新的上限 = 25 * round(1+235/25) = 250
所以范围 = 0,25,50,...,225,250
您可以通过以下步骤获得漂亮的刻度范围:
-
除以 10^x 使得结果介于 0.1 和 1.0 之间(包括 0.1,不包括 1)。
相应地翻译:
0.1 -> 0.1
0.2
0.25
0.3
0.4
0.5
0.6
0.7
0.75
0.8
0.9
1.0
乘以 10^x。
在这种情况下,21.9 除以 10^2 得到 0.219。这是
让我们看一下同一个例子,它有 8 个刻度:
15, 234, 140, 65, 90 with 8 ticks
-
下限 = 15
上限 = 234
范围 = 234-15 = 219
刻度范围 = 27.375
-
除以 10^2 得到 0.27375,转换为 0.3,得到(乘以 10^2)30。
这给出了您要求的结果 ;-)。
-----由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 轴段比刻度数少一。
【讨论】:
这几乎是正确的。第 3 步,我必须将 X 减 1。要获得 219 到 .1->1 的范围,我必须除以 10^3 (1000) 而不是 10^2 (100)。否则,当场。 您引用除以 10^x 并乘以 10^x。需要注意的是,x可以这样求:'double x = Math.Ceiling(Math.Log10(tickRange));' 非常有帮助。虽然不明白 - '新的下限 = 30 * 回合(15/30)= 0'(我想它会是 30)以及你如何在'新的上限 = 30 * 回合(1+235/30)= 中得到 235 240' 235 没有提到,应该是 234。 这是一个很好的答案。非常感谢。 @JoelAnaair 谢谢你让悲伤的一天变得更加美好。【参考方案3】:感谢您的提问和回答,非常有帮助。 Gamecat,我想知道你是如何确定刻度范围应该四舍五入的。
刻度范围 = 21.9。这应该是 25.0
要在算法上做到这一点,是否必须在上面的算法中添加逻辑才能使这个比例很好地适应更大的数字? 例如,有 10 个刻度,如果范围是 3346,那么刻度范围将计算为 334.6,四舍五入到最接近的 10 会得到 340,而 350 可能更好。
你怎么看?
【讨论】:
在@Gamecat 的例子中,334.6 => 0.3346,应该是 0.4。所以刻度范围实际上是 400,这是一个相当不错的数字。【参考方案4】:试试这个代码。我已经在一些图表场景中使用它并且效果很好。速度也很快。
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;
【讨论】:
【参考方案5】:这是我正在使用的 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
)
【讨论】:
我的老板会很高兴的 - 也给我点赞谢谢!! 很好的答案!我将其转换为 Swift 4 ***.com/a/55151115/2670547 @Scott Guthrie:这很好,除非输入不是整数而是小数,例如,如果 yMin = 0.03 和 yMax = 0.11。【参考方案6】:这就像一个魅力,如果你想要 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
【讨论】:
【参考方案7】:我还在为此奋斗:)
最初的 Gamecat 答案似乎在大多数情况下都有效,但尝试插入说,“3 个滴答声”作为所需的滴答声数(对于相同的数据值 15、234、140、65、90).. ..它似乎给出了 73 的刻度范围,除以 10^2 后得到 0.73,映射到 0.75,这给出了 75 的“不错”刻度范围。
然后计算上限: 75*轮(1+234/75) = 300
和下限: 75 * 轮次(15/75) = 0
但很明显,如果您从 0 开始,并以 75 的步长一直到 300 的上限,您最终会得到 0,75,150,225,300 ....这无疑是有用的,但它是 4 个滴答声(不包括 0)而不是所需的 3 个滴答声。
只是令人沮丧的是它不能 100% 工作......当然这很可能是我在某个地方的错误!
【讨论】:
原本认为这个问题可能与 Bryan 建议的推导 x 的方法有关,但这当然是完全准确的。【参考方案8】:基于@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);
【讨论】:
【参考方案9】:上述算法没有考虑最小值和最大值之间的范围太小的情况。如果这些值远高于零怎么办?然后,我们有可能以高于零的值开始 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;
【讨论】:
【参考方案10】: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。否则,您的算法将不适用于小于 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 开始且刻度大小相同的绘图要好。但这件事我被解雇了,我把它留给你。
希望对您有所帮助。 干杯!
【讨论】:
【参考方案11】:将此 answer 转换为 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
【讨论】:
这很好,除非输入不是整数并且是小数,例如,如果 yMin = 0.03 和 yMax = 0.11。【参考方案12】:对于任何在 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 的出色回答。
【讨论】:
【参考方案13】:此解决方案基于我找到的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]
【讨论】:
【参考方案14】: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" />
<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>
【讨论】:
以上是关于为图形的 Y 轴选择有吸引力的线性比例的主要内容,如果未能解决你的问题,请参考以下文章