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