Created
March 16, 2020 23:28
-
-
Save erisonliang/4a600df761c1647eb1030edeb4be2bd5 to your computer and use it in GitHub Desktop.
Nice Scale in C#
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
public class Program | |
{ | |
private static Tuple<decimal, decimal, decimal> GetScaleDetails(decimal min, decimal max) | |
{ | |
// Minimal increment to avoid round extreme values to be on the edge of the chart | |
decimal epsilon = (max - min) / 1e6m; | |
max += epsilon; | |
min -= epsilon; | |
decimal range = max - min; | |
// Target number of values to be displayed on the Y axis (it may be less) | |
int stepCount = 20; | |
// First approximation | |
decimal roughStep = range / (stepCount - 1); | |
// Set best step for the range | |
decimal[] goodNormalizedSteps = { 1, 1.5m, 2, 2.5m, 10/3m, 5, 7.5m, 10 }; // keep the 10 at the end | |
// Or use these if you prefer: { 1, 2, 5, 10 }; | |
// Normalize rough step to find the normalized one that fits best | |
decimal stepPower = (decimal)Math.Pow(10, -Math.Floor(Math.Log10((double)Math.Abs(roughStep)))); | |
var normalizedStep = roughStep * stepPower; | |
var goodNormalizedStep = goodNormalizedSteps.First(n => n >= normalizedStep); | |
decimal step = goodNormalizedStep / stepPower; | |
// Determine the scale limits based on the chosen step. | |
decimal scaleMax = Math.Ceiling(max / step) * step; | |
decimal scaleMin = Math.Floor(min / step) * step; | |
return new Tuple<decimal, decimal, decimal>(scaleMin, scaleMax, step); | |
} | |
public static void Main() | |
{ | |
// Dummy code to show a usage example. | |
var data = new double[]{0.33, 0.55}; | |
data.Dump(); | |
var minimumValue = data.Min(); | |
var maximumValue = data.Max(); | |
var results = GetScaleDetails((decimal)minimumValue, (decimal)maximumValue); | |
results.Dump(); | |
double range, tickSpacing, niceMin, niceMax; | |
NiceScale2 niceScale2 = new NiceScale2(minimumValue, maximumValue); | |
niceScale2.Dump(); | |
//Console.WriteLine($"range:{range}, tickSpacing:{tickSpacing}, niceMin:{niceMin}, niceMax:{niceMax}"); | |
//new {range=range, tickSpacing=tickSpacing, niceMin=niceMin, niceMax=niceMax}.Dump(); | |
} | |
} | |
//ref: https://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks/16363437#16363437 | |
public class NiceScale2 | |
{ | |
public double NiceMin { get; set; } | |
public double NiceMax { get; set; } | |
public double TickSpacing { get; private set; } | |
private double _minPoint; | |
private double _maxPoint; | |
private double _maxTicks = 5; | |
private double _range; | |
/** | |
* Instantiates a new instance of the NiceScale class. | |
* | |
* @param min the minimum data point on the axis | |
* @param max the maximum data point on the axis | |
*/ | |
public NiceScale2(double min, double max) | |
{ | |
_minPoint = min; | |
_maxPoint = max; | |
Calculate(); | |
} | |
/** | |
* Calculate and update values for tick spacing and nice | |
* minimum and maximum data points on the axis. | |
*/ | |
private void Calculate() | |
{ | |
_range = NiceNum(_maxPoint - _minPoint, false); | |
TickSpacing = NiceNum(_range / (_maxTicks - 1), true); | |
NiceMin = Math.Floor(_minPoint / TickSpacing) * TickSpacing; | |
NiceMax = Math.Ceiling(_maxPoint / TickSpacing) * TickSpacing; | |
} | |
/** | |
* Returns a "nice" number approximately equal to range Rounds | |
* the number if round = true Takes the ceiling if round = false. | |
* | |
* @param range the data range | |
* @param round whether to round the result | |
* @return a "nice" number to be used for the data range | |
*/ | |
private 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); | |
} | |
/** | |
* Sets the minimum and maximum data points for the axis. | |
* | |
* @param minPoint the minimum data point on the axis | |
* @param maxPoint the maximum data point on the axis | |
*/ | |
public void SetMinMaxPoints(double minPoint, double maxPoint) | |
{ | |
_minPoint = minPoint; | |
_maxPoint = maxPoint; | |
Calculate(); | |
} | |
/** | |
* Sets maximum number of tick marks we're comfortable with | |
* | |
* @param maxTicks the maximum number of tick marks for the axis | |
*/ | |
public void SetMaxTicks(double maxTicks) | |
{ | |
_maxTicks = maxTicks; | |
Calculate(); | |
} | |
} | |
//ref: https://stackoverflow.com/questions/237220/tickmark-algorithm-for-a-graph-axis | |
public static class NiceScale { | |
public static void Calculate(double min, double max, int maxTicks, out double range, out double tickSpacing, out double niceMin, out double niceMax) { | |
range = niceNum(max - min, false); | |
tickSpacing = niceNum(range / (maxTicks - 1), true); | |
niceMin = Math.Floor(min / tickSpacing) * tickSpacing; | |
niceMax = Math.Ceiling(max / tickSpacing) * tickSpacing; | |
} | |
private static double niceNum(double range, bool round) { | |
double pow = Math.Pow(10, Math.Floor(Math.Log10(range))); | |
double fraction = range / pow; | |
double niceFraction; | |
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 * pow; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment