소프트웨어에서 막대 (또는 선) 그래프를 표시하는 코드를 작성하고 있습니다. 모든 것이 잘되고 있습니다. 저를 당황하게 만드는 것은 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에서 시작하는 것이 좋습니다. 그러나 이것은 내 해고 나는 그것을 당신에게 맡깁니다.
도움이되기를 바랍니다. 건배!
답변
이 답변 을 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
}
}