Choosing an attractive linear scale for a graph's Y Axis

后端 未结 13 1451
予麋鹿
予麋鹿 2020-12-07 08:42

I\'m writing a bit of code to display a bar (or line) graph in our software. Everything\'s going fine. The thing that\'s got me stumped is labeling the Y axis.

The

13条回答
  •  执念已碎
    2020-12-07 09:07

    The answer by Toon Krijthe does work most of the time. But sometimes it will produce excess number of ticks. It won't work with negative numbers as well. The overal approach to the problem is ok but there is a better way to handle this. The algorithm you want to use will depend on what you really want to get. Below I'm presenting you my code which I used in my JS Ploting library. I've tested it and it always works (hopefully ;) ). Here are the major steps:

    • get global extremas xMin and xMax (inlucde all the plots you want to print in the algorithm )
    • calculate range between xMin and xMax
    • calculate the order of magnitude of your range
    • calculate tick size by dividing range by number of ticks minus one
    • this one is optional. If you want to have zero tick allways printed you use tick size to calculate number of positive and negative ticks. Total number of ticks will be their sum + 1 (the zero tick)
    • this one is not needed if you have zero tick allways printed. Calculate lower and upper bound but remember to center the plot

    Lets start. First the basic calculations

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

    I round minimum and maximum values to be 100% sure that my plot will cover all the data. It is also very important to floor log10 of range wheter or not it is negative and substract 1 later. Otherwise your algorithm won't work for numbers that are lesser than one.

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

    I use "nice looking ticks" to avoid ticks like 7, 13, 17 etc. Method I use here is pretty simple. It is also nice to have zeroTick when needed. Plot looks much more professional this way. You will find all the methods at the end of this answer.

    Now you have to calculate upper and lower bounds. This is very easy with zero tick but requires a little bit more effort in other case. Why? Because we want to center the plot within upper and lower bound nicely. Have a look at my code. Some of the variables are defined outside of this scope and some of them are properties of an object in which whole presented code is kept.

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

    And here are methods I mentioned before which you can write by yourself but you can also use mine

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

    There is only one more thing that is not included here. This is the "nice looking bounds". These are lower bounds that are numbers similar to the numbers in "nice looking ticks". For example it is better to have the lower bound starting at 5 with tick size 5 than having a plot that starts at 6 with the same tick size. But this my fired I leave it to you.

    Hope it helps. Cheers!

提交回复
热议问题