Search code examples
mathgraphchartschart.jsyaxis

Chart - Y Axis display value and step calculation


So I have a list of pairs: [(0,15.0),(1,17.0),(2,20.0),(3,28.5),(4,29),(5,32.0),(6,33.5),(7,33.6)]

I would like to create a scatter chart where the first numbers will be the X values (and X axis labels) and the second numbers will be the Y axis values.

My reference point is MS Excel, where I can select 2 columns with some rows, and I can insert a scatter graph. In MS Excel, the Y axis labels are nicely calculated so in my example dataset the Y axis labels would be 0, 5, 10, 15, 20, 25, 30, 35.

My problem is that my dataset can have all kinds of Y values (inc. negatives) from the lowest Double value possible to the highest Double value possible.

I was unable to find the logic on how to create this humanly readable Y axis label calculation, so I would get labels like 10, 20, 30, 40, 50 10, 100, 1000, 10000 10, 50, 100, 150 0, 200, 400, 600, 800, 0, 300, 600, 900 etc

I want the Y axis label count to be dynamic to an extent, so if the data range is huge, I don't end up having more than 8 labels, while if the data range is very small, I still see at least 5 rather than just 2. Also a secondary requirement is for example if the Y axis labels would be 0.0, 0.5, 1.0, 1.5, 2.0, 2.5 - I need to be able to change the fraction digit count, so it could potentially be 0.00, 0.50, 1.00, 1.50 - or if I set zero fraction digits, then not just the labels would change, but the y axis steps as well, so they would be 0, 1, 2, 3, 4, 5 etc.

Right now I am looking for a solution in any programming language. Preferably Chart.js, React, Kotlin, Swift, but basically anything goes, I just need some direction/logic that I could implement in my required language. I don't want to depend on Chart.js or a python lib that does not exist for other languages.

Thank you!

This is what I tried in Kotlin, there are scenarios where I end up having 15 or even 20 labels, I want to make sure that the lowest and highest Y axis label is always lower and higher than the lowest and highest y value in the dataset:

private fun calculateStepSize(range: Double, preferredLabelCount: Int): Double {
    // calculate raw step
    val rawStep = range / preferredLabelCount

    // calculate magnitude of the step
    val magnitude = floor(log10(rawStep)).toInt()

    // calculate most significant digit of the new step
    var mostSignificantDigit = (rawStep / 10.0.pow(magnitude.toDouble())).roundToInt()

    // promote most significant digit to 1, 2, or 5
    mostSignificantDigit = if (mostSignificantDigit > 5) {
        10
    } else if (mostSignificantDigit > 2) {
        5
    } else {
        1
    } // 10 20 30 ... 10 50 100 150... 10 100 1000...

    // generate new step
    return mostSignificantDigit * 10.0.pow(magnitude.toDouble())
}


// start at label count 7, that means on the y Axis, there will be 7 values shown
var yAxisLabelCount = 7
var yAxisRange = maxYValue - minYValue
var yAxisValueStep = calculateStepSize(yAxisRange, yAxisLabelCount - 1)

var yAxisRoundedMin = floor(minYValue / yAxisValueStep) * yAxisValueStep

var yAxisMax = yAxisRoundedMin + (yAxisLabelCount - 1) * yAxisValueStep

while (yAxisMax < maxYValue) {
    yAxisLabelCount++
    yAxisRange = maxYValue - minYValue
    yAxisValueStep = calculateStepSize(yAxisRange, yAxisLabelCount - 1)
    yAxisRoundedMin = floor(minYValue / yAxisValueStep) * yAxisValueStep
    yAxisMax = yAxisRoundedMin + (yAxisLabelCount - 1) * yAxisValueStep
}

Solution

  • General idea :

    - Specify how many ticks you want on your axis ( tick_count )
    - Find the range of the data ( max - min )
    - tick_increment = range / tick_count, rounded down to integer
    - tick_value = min
    - LOOP
        tick_value += tick_increment
    

    This seems strightforward. The trick is all the extra code required to handle special cases.

    Here is some C++ code that handles some of them

    std::vector< double > tickValues(
        double mn, double mx )
    {
        std::vector< double > vl;
        double range = mx - mn;
        if( range < minDataRange )
        {
            // plot is single valued
            // display just one tick
            vl.push_back( mn );
            return vl;
        }
        double inc = range / 4;
        double tick;
        if( inc > 1 )
        {
            inc = ( int ) inc;
            tick  = ( int ) mn;
        }
        else
        {
            tick = mn;
        }
        while( true )
        {
            double v = tick;
            if( v > 100)
                v = ((int) v / 100 ) * 100;
            else if( v > 10 )
                v = ((int) v / 10 ) * 10;
            vl.push_back( v );
            tick += inc;
            if( tick >= mx )
                break;
        }
        vl.push_back( mx );
        return vl;
    }
    

    Code for complete 2D plotting class https://github.com/JamesBremner/windex/blob/master/include/plot2d.h

    Recommendation: A full featured, robust 2D plotter ready to handle arbitrary data sets requires a tremendous amount of code, so rather than spinning up your own, it might be better not to reinvent the wheel and find a suitable library or application.