Saturday 16 March 2019

C# - Picking nice graph axes functionally

So I am revising XAML and I wanted to write a graph control. The first problem I encountered is how to pick nice numbers for your graph axis, the algorithm is not obvious and I had to google before I found a popular answer on Stack Overflow.

The original given code was quite classy when it needn't be given that we are not holding state. I think a functional implementation is more appropriate and so I have rewritten the C# code a class with static members only. I have abolished the class member variables and instead I am returning all the facts (short for artefacts) found in the function via an object array. I have been influenced by Python of late with its capability to return tuples but I have avoided the Tuple<> generic class because I didn't want to have to specify all the types ( I appreciate there is a small performance hot with the boxing).

using System;

namespace GraphIntervalTest
{
    class Program
    {
        static void Main(string[] args)
        {

            object[] facts = NiceScaleStatic.CalculateNiceScale(1.2813, 1.331, 8);  

            System.Console.Out.WriteLine("Tick Spacing:\t" + facts[0]);
            System.Console.Out.WriteLine("Nice Minimum:\t" + facts[1]);
            System.Console.Out.WriteLine("Nice Maximum:\t" + facts[2]);
            System.Console.Out.WriteLine("Tick Count:\t" + facts[3]);
            System.Console.Out.WriteLine("Min:\t" + facts[4]);
            System.Console.Out.WriteLine("Max:\t" + facts[5]);
            System.Console.Out.WriteLine("MaxTicks:\t" + facts[6]);
        }
    }

    public class NiceScaleStatic
    {
        public static object[] CalculateNiceScale(double min, double max, long maxTicks = 10)
        {
            double tickSpacing;
            double range;
            double niceMin;
            double niceMax;
            long tickCount;

            range = niceNum(max - min, false);
            tickSpacing = niceNum(range / (maxTicks - 1), true);
            niceMin = Math.Floor(min / tickSpacing) * tickSpacing;
            niceMax = Math.Ceiling(max / tickSpacing) * tickSpacing;

            tickCount = (long)((niceMax - niceMin) / tickSpacing);

            return new object[] { tickSpacing, niceMin, niceMax, tickCount, min, max, maxTicks };
        }

        static double niceNum(double range, bool round)
        {
            double exponent; /** exponent of range */
            double fraction; /** fractional part of range */
            double niceFraction;  /** nice, rounded fraction */

            exponent = Math.Floor(Math.Log10(range));
            fraction = range / 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);
        }
    }
}