그래프의 Y 축에 대한 매력적인 선형 스케일 선택
소프트웨어에서 막대 (또는 선) 그래프를 표시하는 코드를 작성하고 있습니다. 모든 것이 잘되고 있습니다. 저를 당황하게 만드는 것은 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
9 개는 까다 로웠을 것입니다. 아마 8 또는 10을 사용하고 충분히 가깝게 호출하면 괜찮을 것입니다. 일부 포인트가 부정적 일 때 어떻게해야합니까?
Excel이이 문제를 잘 해결하는 것을 볼 수 있습니다.
누구든지이 문제를 해결하기위한 범용 알고리즘 (무차별 대입도 괜찮음)을 알고 있습니까? 빨리 할 필요는 없지만 멋져 보일 것입니다.
오래 전에 나는 이것을 잘 다루는 그래프 모듈을 작성했습니다. 회색 덩어리를 파는 것은 다음을 얻습니다.
- 데이터의 하한과 상한을 결정합니다. (하한 = 상한 인 특수한 경우에주의하십시오!
- 범위를 필요한 틱 수로 나눕니다.
- 눈금 범위를 좋은 양으로 반올림하십시오.
- 그에 따라 하한과 상한을 조정합니다.
예를 들어 보겠습니다.
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
다음 단계를 통해 멋진 눈금 범위를 얻을 수 있습니다.
- 결과가 0.1과 1.0 사이가되도록 10 ^ x로 나눕니다 (1을 제외한 0.1 포함).
- 그에 따라 번역 :
- 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
- 10 ^ x를 곱합니다.
이 경우 21.9를 10 ^ 2로 나누면 0.219가됩니다. 이것은 <= 0.25이므로 이제 0.25입니다. 10 ^ 2를 곱하면 25가됩니다.
8 틱이있는 동일한 예제를 살펴 보겠습니다.
15, 234, 140, 65, 90 with 8 ticks
- 하한 = 15
- 상한 = 234
- 범위 = 234-15 = 219
- 눈금 범위 = 27.375
- 0.27375에 대해 10 ^ 2로 나누면 0.3으로 변환되어 (10 ^ 2 곱하기) 30이됩니다.
- 새 하한 = 30 * round (15/30) = 0
- 새 상한 = 30 * round (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 축 세그먼트는 틱 수보다 하나 적습니다.
다음은 내가 사용하는 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
)
이 코드를 사용해보십시오. 몇 가지 차트 시나리오에서 사용했으며 잘 작동합니다. 너무 빠릅니다.
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;
}
}
발신자가 원하는 범위를 알려주지 않는 것 같습니다.
따라서 레이블 수로 잘 나눌 때까지 끝점을 자유롭게 변경할 수 있습니다.
"nice"를 정의합시다. 레이블이 다음과 같이 꺼지면 nice라고 부를 것입니다.
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
오프셋이 "nice"가 될 때까지
나는 아직도 이것과 싸우고있다 :)
원래 Gamecat 답변은 대부분 작동하는 것처럼 보이지만 필요한 틱 수 (동일한 데이터 값 15, 234, 140, 65, 90)로 "3 틱"이라고 말하여 연결해보십시오 .... it 틱 범위가 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 % 작동하지 않는다는 사실이 실망 스러웠습니다. 물론 어딘가에서 내 실수 일 수 있습니다!
Toon Krijthe 의 대답은 대부분의 경우 작동합니다. 그러나 때로는 과도한 수의 진드기를 생성합니다. 음수에서도 작동하지 않습니다. 문제에 대한 전반적인 접근 방식은 괜찮지 만이를 처리하는 더 좋은 방법이 있습니다. 사용하려는 알고리즘은 실제로 얻고 자하는 것에 따라 달라집니다. 아래에서는 JS Ploting 라이브러리에서 사용한 코드를 보여 드리겠습니다. 나는 그것을 테스트했으며 항상 작동합니다 (희망적으로;)). 주요 단계는 다음과 같습니다.
- 전역 극한값 xMin 및 xMax 가져 오기 (알고리즘에서 인쇄하려는 모든 플롯을 포함하지 않음)
- xMin과 xMax 사이의 범위 계산
- 범위의 크기 차수 계산
- 범위를 틱 수에서 1을 뺀 값으로 나누어 틱 크기 계산
- 이것은 선택 사항입니다. 항상 0 눈금을 인쇄하려면 눈금 크기를 사용하여 양수 및 음수 눈금 수를 계산합니다. 총 틱 수는 합계 + 1 (제로 틱)입니다.
- 항상 눈금이 0 인 경우에는 필요하지 않습니다. 하한과 상한을 계산하지만 플롯을 중앙에 배치해야합니다.
시작하자. 먼저 기본 계산
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;
}
}
여기에 포함되지 않은 것이 하나 더 있습니다. 이것은 "멋져 보이는 경계"입니다. 이것은 "멋진 틱"의 숫자와 유사한 숫자 인 하한입니다. 예를 들어 동일한 틱 크기로 6에서 시작하는 플롯을 갖는 것보다 틱 크기가 5 인 5에서 시작하는 하한을 갖는 것이 좋습니다. 그러나 이것은 내 해고 나는 그것을 당신에게 맡깁니다.
도움이 되었기를 바랍니다. 건배!
이것은 매력처럼 작동합니다. 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
질문과 답변에 감사드립니다. 매우 도움이되었습니다. Gamecat, 틱 범위를 반올림해야하는 방법을 어떻게 결정하는지 궁금합니다.
눈금 범위 = 21.9. 25.0이어야합니다.
이를 알고리즘 적으로 수행하려면 위의 알고리즘에 논리를 추가하여 더 큰 숫자에 대해이 스케일을 멋지게 만들어야합니까? 예를 들어 10 틱의 경우 범위가 3346이면 틱 범위는 334.6으로 평가되고 가장 가까운 10으로 반올림하면 350이 더 좋을 때 340이됩니다.
어떻게 생각해?
@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);
}
}
위의 알고리즘은 최소값과 최대 값 사이의 범위가 너무 작은 경우를 고려하지 않습니다. 그리고이 값이 0보다 훨씬 크다면 어떻게 될까요? 그런 다음 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;
}
이 답변 을 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
}
}
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의 우수한 답변을 기반으로합니다.
참고 URL : https://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axis
'Nice programing' 카테고리의 다른 글
OAuth 권한 부여 및 인증 (0) | 2020.10.29 |
---|---|
Fedora 29 업그레이드 후 Slack이 분할 오류를 반환하는 이유는 무엇입니까? (0) | 2020.10.29 |
JavaScript로 모든 쿠키를 삭제하려면 어떻게해야합니까? (0) | 2020.10.28 |
JavaScript에서 String.Format 사용? (0) | 2020.10.28 |
ASP.NET MVC 전역 변수 (0) | 2020.10.28 |