Chọn tỷ lệ tuyến tính hấp dẫn cho Trục Y của biểu đồ


84

Tôi đang viết một đoạn mã để hiển thị biểu đồ thanh (hoặc đường) trong phần mềm của chúng tôi. Mọi thứ vẫn ổn. Điều khiến tôi bối rối là gắn nhãn trục Y.

Người gọi có thể cho tôi biết họ muốn dán nhãn thang Y một cách tinh vi như thế nào, nhưng tôi dường như bị mắc kẹt trong việc dán nhãn chính xác cho họ theo cách "hấp dẫn" như thế nào. Tôi không thể mô tả "hấp dẫn", và có lẽ bạn cũng không thể, nhưng chúng ta biết điều đó khi chúng ta nhìn thấy nó, phải không?

Vì vậy, nếu các điểm dữ liệu là:

   15, 234, 140, 65, 90

Và người dùng yêu cầu 10 nhãn trên trục Y, một chút tài chính với giấy và bút chì sẽ có:

  0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250

Vì vậy, có 10 ở đó (không bao gồm 0), giá trị cuối cùng vượt quá giá trị cao nhất (234 <250) và đó là mức tăng "tốt" của mỗi giá trị là 25. Nếu họ yêu cầu 8 nhãn, gia số 30 sẽ trông đẹp:

  0, 30, 60, 90, 120, 150, 180, 210, 240

Chín sẽ rất khó. Có lẽ chỉ cần sử dụng 8 hoặc 10 và gọi nó đủ gần là được. Và phải làm gì khi một số điểm là tiêu cực?

Tôi có thể thấy Excel giải quyết vấn đề này rất tốt.

Có ai biết một thuật toán có mục đích chung (ngay cả một số lực lượng vũ phu là được) để giải quyết điều này? Tôi không cần phải làm nhanh chóng, nhưng nó sẽ trông đẹp.


1
Có một số thông tin về cách Excel chọn các giá trị max và min cho Y của nó trục ở đây: support.microsoft.com/kb/214075
Christopher Orr

Thoải mái thực hiện: stackoverflow.com/a/16363437/829571
assylias

Câu trả lời:


103

Cách đây rất lâu, tôi đã viết một mô-đun đồ thị bao gồm điều này một cách độc đáo. Đào trong khối xám được những thứ sau:

  • Xác định giới hạn dưới và giới hạn trên của dữ liệu. (Hãy cẩn thận với trường hợp đặc biệt trong đó giới hạn dưới = giới hạn trên!
  • Chia phạm vi thành số lượng bọ ve cần thiết.
  • Làm tròn phạm vi đánh dấu lên thành số tiền tốt.
  • Điều chỉnh giới hạn dưới và giới hạn trên cho phù hợp.

Hãy lấy ví dụ của bạn:

15, 234, 140, 65, 90 with 10 ticks
  1. giới hạn dưới = 15
  2. giới hạn trên = 234
  3. phạm vi = 234-15 = 219
  4. khoảng đánh dấu = 21,9. Đây phải là 25.0
  5. giới hạn dưới mới = 25 * vòng (15/25) = 0
  6. giới hạn trên mới = 25 * vòng (1 + 235/25) = 250

Vì vậy, phạm vi = 0,25,50, ..., 225,250

Bạn có thể nhận được phạm vi đánh dấu đẹp với các bước sau:

  1. chia cho 10 ^ x sao cho kết quả nằm trong khoảng 0,1 đến 1,0 (bao gồm 0,1 không bao gồm 1).
  2. dịch cho phù hợp:
    • 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. nhân với 10 ^ x.

Trong trường hợp này, 21,9 chia cho 10 ^ 2 để được 0,219. Đây là <= 0,25 nên bây giờ chúng ta có 0,25. Nhân với 10 ^ 2, ta được 25.

Hãy xem cùng một ví dụ với 8 dấu tích:

15, 234, 140, 65, 90 with 8 ticks
  1. giới hạn dưới = 15
  2. giới hạn trên = 234
  3. phạm vi = 234-15 = 219
  4. phạm vi đánh dấu = 27.375
    1. Chia 10 ^ 2 cho 0,27375, dịch là 0,3, cho (nhân với 10 ^ 2) 30.
  5. giới hạn dưới mới = 30 * vòng (15/30) = 0
  6. giới hạn trên mới = 30 * vòng (1 + 235/30) = 240

Đưa ra kết quả bạn yêu cầu ;-).

------ Thêm bởi KD ------

Đây là mã đạt được thuật toán này mà không cần sử dụng bảng tra cứu, v.v.:

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;

Nói chung, số lượng bọ ve bao gồm cả dấu tích dưới cùng, do đó, các đoạn trục y thực tế ít hơn số lượng dấu tích.


1
Điều này là đúng. Bước 3, tôi phải giảm X đi 1. Để có phạm vi từ 219 đến .1-> 1, tôi phải chia cho 10 ^ 3 (1000) chứ không phải 10 ^ 2 (100). Nếu không, ngay tại chỗ.
Clinton Pierce

2
Bạn tham khảo chia cho 10 ^ x và nhân với 10 ^ x. Cần lưu ý rằng x có thể được tìm thấy theo cách này: 'double x = Math.Ceiling (Math.Log10 (tickRange));'
Bryan

1
Rất hữu ích. Mặc dù tôi không hiểu - 'giới hạn dưới mới = 30 * vòng (15/30) = 0' (Tôi nghĩ sẽ đến 30) và làm thế nào bạn có được 235 trong 'giới hạn trên mới = 30 * vòng (1 + 235/30) = 240' 235 được nhắc đến đâu, nó phải là 234.
Mutant

4
Đây là một câu trả lời tuyệt vời. Rất nhiều đánh giá cao.
Joel Anair

4
@JoelAnair Cảm ơn bạn vừa làm cho một ngày buồn tươi sáng hơn một chút.
Toon Krijthe

22

Đây là một ví dụ PHP mà tôi đang sử dụng. Hàm này trả về một mảng các giá trị trục Y đẹp bao gồm các giá trị Y tối thiểu và tối đa được truyền vào. Tất nhiên, quy trình này cũng có thể được sử dụng cho các giá trị trục X.

Nó cho phép bạn "gợi ý" số lượng bọ ve mà bạn có thể muốn, nhưng quy trình sẽ trả lại những gì có vẻ tốt. Tôi đã thêm một số dữ liệu mẫu và hiển thị kết quả cho những dữ liệu này.

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

Kết quả đầu ra từ dữ liệu mẫu

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

sếp của tôi sẽ rất vui với điều này - tôi cũng ủng hộ tôi. CẢM ƠN !!
Stephen Hazel

Câu trả lời chính xác! Tôi chuyển đổi nó thành Swift 4 stackoverflow.com/a/55151115/2670547
Petr Syrov

@Scott Guthrie: Điều này là tuyệt vời trừ khi các đầu vào không phải là số nguyên và là số nhỏ, ví dụ: nếu yMin = 0,03 và yMax = 0,11.
Greg

9

Hãy thử mã này. Tôi đã sử dụng nó trong một số tình huống biểu đồ và nó hoạt động tốt. Nó cũng khá nhanh.

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

6

Có vẻ như người gọi không cho bạn biết phạm vi họ muốn.

Vì vậy, bạn có thể tự do thay đổi điểm kết thúc cho đến khi bạn nhận được nó chia hết cho số nhãn của bạn.

Hãy định nghĩa "tốt đẹp". Tôi sẽ gọi là tốt nếu các nhãn bị tắt bởi:

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, ...

Tìm giá trị lớn nhất và tối thiểu của chuỗi dữ liệu của bạn. Hãy gọi những điểm này:

min_point and max_point.

Bây giờ tất cả những gì bạn cần làm là tìm 3 giá trị:

- 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"

phù hợp với phương trình:

(end_label - start_label)/label_offset == label_count

Có lẽ có nhiều giải pháp, vì vậy chỉ cần chọn một. Hầu hết thời gian tôi cá là bạn có thể đặt

start_label to 0

vì vậy chỉ cần thử số nguyên khác

end_label

cho đến khi phần bù là "đẹp"


3

Tôi vẫn đang chiến đấu với điều này :)

Câu trả lời gốc của Gamecat dường như hoạt động trong hầu hết thời gian, nhưng hãy thử cắm "3 tick" là số lượng tick cần thiết (cho cùng các giá trị dữ liệu 15, 234, 140, 65, 90) .... nó dường như cung cấp phạm vi đánh dấu là 73, sau khi chia cho 10 ^ 2 thu được 0,73, ánh xạ thành 0,75, cung cấp phạm vi đánh dấu 'đẹp' là 75.

Sau đó, tính giới hạn trên: 75 * round (1 + 234/75) = 300

và giới hạn dưới: 75 * round (15/75) = 0

Nhưng rõ ràng nếu bạn bắt đầu từ 0 và tiếp tục theo các bước từ 75 đến giới hạn trên của 300, bạn kết thúc với 0,75.150.225.300 .... không nghi ngờ gì là hữu ích, nhưng đó là 4 dấu (không bao gồm 0) không phải Yêu cầu 3 tick.

Chỉ bực bội rằng nó không hoạt động 100% thời gian .... mà tất nhiên có thể là do lỗi của tôi ở đâu đó!


Ban đầu người ta nghĩ rằng vấn đề có thể liên quan đến phương pháp suy ra x được gợi ý của Bryan, nhưng điều này tất nhiên là hoàn toàn chính xác.
StillPondering

3

Câu trả lời của Toon Krijthe hầu hết đều hoạt động. Nhưng đôi khi nó sẽ tạo ra số lượng bọ ve dư thừa. Nó cũng sẽ không hoạt động với số âm. Cách tiếp cận tổng quát đối với vấn đề là ổn nhưng có một cách tốt hơn để xử lý vấn đề này. Thuật toán bạn muốn sử dụng sẽ phụ thuộc vào những gì bạn thực sự muốn nhận được. Dưới đây tôi trình bày cho bạn mã của tôi mà tôi đã sử dụng trong thư viện JS Ploting của mình. Tôi đã thử nghiệm nó và nó luôn hoạt động (hy vọng;)). Dưới đây là các bước chính:

  • lấy các cực trị toàn cục xMin và xMax (inlucde tất cả các ô bạn muốn in trong thuật toán)
  • tính toán phạm vi giữa xMin và xMax
  • tính toán thứ tự độ lớn của phạm vi của bạn
  • tính toán kích thước bọ ve bằng cách chia phạm vi cho số lượng bọ ve trừ đi một
  • cái này là tùy chọn. Nếu bạn muốn in tất cả các dấu tích bằng không, bạn sử dụng kích thước đánh dấu để tính số lượng dấu tích dương và tiêu cực. Tổng số dấu tích sẽ là tổng của chúng + 1 (dấu tích số không)
  • cái này không cần thiết nếu bạn không có dấu tích nào được in. Tính giới hạn dưới và giới hạn trên nhưng nhớ căn giữa cốt truyện

Hãy bắt đầu. Đầu tiên các tính toán cơ bản

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

Tôi làm tròn các giá trị tối thiểu và tối đa để chắc chắn 100% rằng âm mưu của tôi sẽ bao gồm tất cả dữ liệu. Nó cũng rất quan trọng đối với sàn log10 của dải ô dù có hay không nó là số âm và trừ đi 1 sau đó. Nếu không, thuật toán của bạn sẽ không hoạt động đối với các số nhỏ hơn một.

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

Tôi sử dụng "bọ ve đẹp" để tránh bọ ve như 7, 13, 17, v.v. Phương pháp tôi sử dụng ở đây khá đơn giản. Thật tuyệt khi có zeroTick khi cần thiết. Theo cách này, âm mưu trông chuyên nghiệp hơn nhiều. Bạn sẽ tìm thấy tất cả các phương pháp ở cuối câu trả lời này.

Bây giờ bạn phải tính toán giới hạn trên và giới hạn dưới. Điều này rất dễ dàng với số không đánh dấu nhưng đòi hỏi nỗ lực hơn một chút trong trường hợp khác. Tại sao? Bởi vì chúng tôi muốn căn giữa cốt truyện trong giới hạn trên và dưới một cách độc đáo. Hãy xem mã của tôi. Một số biến được định nghĩa bên ngoài phạm vi này và một số trong số chúng là thuộc tính của một đối tượng trong đó toàn bộ mã được trình bày được lưu giữ.

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

Và đây là những phương pháp tôi đã đề cập trước đây mà bạn có thể tự viết nhưng bạn cũng có thể sử dụng

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

Chỉ có một điều nữa mà không được bao gồm ở đây. Đây là "giới hạn đẹp mắt". Đây là những giới hạn thấp hơn là những con số tương tự như những con số trong "tích tắc đẹp". Ví dụ, tốt hơn là có giới hạn thấp hơn bắt đầu từ 5 với kích thước đánh dấu 5 hơn là có một âm mưu bắt đầu từ 6 với cùng kích thước đánh dấu. Nhưng điều này của tôi đã bị sa thải, tôi để nó cho bạn.

Hy vọng nó giúp. Chúc mừng!


2

Đã chuyển đổi câu trả lời này thành 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
    }

}

Điều này là tuyệt vời trừ khi các đầu vào không phải là số nguyên và là số nhỏ, ví dụ: nếu yMin = 0,03 và yMax = 0,11.
Greg

1

điều này hoạt động như một sự quyến rũ, nếu bạn muốn 10 bước + 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

1

Đối với bất kỳ ai cần điều này trong ES5 Javascript, đã phải vật lộn một chút, nhưng đây là:

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

Dựa trên câu trả lời xuất sắc của Toon Krijtje.


1

Giải pháp này dựa trên một ví dụ Java mà tôi tìm thấy.

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]


0

Cảm ơn vì câu hỏi và câu trả lời, rất hữu ích. Gamecat, tôi đang tự hỏi làm thế nào bạn đang xác định phạm vi đánh dấu nên được làm tròn thành.

khoảng đánh dấu = 21,9. Đây phải là 25.0

Để làm điều này theo thuật toán, người ta sẽ phải thêm logic vào thuật toán trên để làm cho tỷ lệ này trở nên độc đáo cho các số lớn hơn? Ví dụ: với 10 tick, nếu phạm vi là 3346 thì phạm vi đánh dấu sẽ đánh giá thành 334,6 và làm tròn đến 10 gần nhất sẽ cho 340 khi 350 có thể đẹp hơn.

Bạn nghĩ sao?


Trong ví dụ của @ Gamecat, 334,6 => 0,3346, sẽ chuyển thành 0,4. Vì vậy, phạm vi đánh dấu thực sự sẽ là 400, đây là một con số khá đẹp.
Bryan

0

Dựa trên thuật toán của @ Gamecat, tôi đã tạo ra lớp trợ giúp sau

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

Các thuật toán trên không tính đến trường hợp phạm vi giữa giá trị min và max quá nhỏ. Và điều gì sẽ xảy ra nếu những giá trị này cao hơn 0 rất nhiều? Sau đó, chúng ta có khả năng bắt đầu trục y với giá trị cao hơn 0. Ngoài ra, để tránh đường thẳng của chúng ta hoàn toàn ở phía trên hoặc phía dưới của biểu đồ, chúng ta phải cung cấp cho nó một chút "không khí để thở".

Để giải quyết những trường hợp đó, tôi đã viết (trên PHP) đoạn mã trên:

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;
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.