Мне нужно вручную вычислить метки Ticklabels и Tickrange для диаграмм.
Я знаю "стандартный" алгоритм хороших тиков (см. http://books.google.de/books?id=fvA7zLEFWZgC&pg=PA61&lpg=PA61&redir_esc=y#v=onepage&q&f=false ) и Я также знаю эту реализацию Java .
Проблема в том, что с этим алгоритмом тики "слишком умны". Это означает, что алгоритм решает, сколько тиков должно отображаться. Мое требование: что всегда есть 5 тиков, но они, конечно, должны быть "красивыми". Наивный подход заключался бы в том, чтобы получить максимальное значение, разделить на 5 и умножить на число тиков. Значения здесь - конечно - не оптимальные, и тики довольно уродливы.
Кто-нибудь знает решение проблемы или имеет подсказку для формального описания алгоритма?
Вы должны быть в состоянии использовать реализацию Java с небольшими исправлениями.
Изменить максимальные значения на 5.
Измените метод расчета следующим образом:
private void calculate() {
this.range = niceNum(maxPoint - minPoint, false);
this.tickSpacing = niceNum(range / (maxTicks - 1), true);
this.niceMin =
Math.floor(minPoint / tickSpacing) * tickSpacing;
this.niceMax = this.niceMin + tickSpacing * (maxticks - 1); // Always display maxticks
}
Отказ от ответственности: обратите внимание, что я не проверял это, поэтому вам, возможно, придется настроить его, чтобы он выглядел хорошо. Мое предлагаемое решение добавляет дополнительное пространство в верхней части графика, чтобы всегда было место для 5 тиков. Это может выглядеть уродливо в некоторых случаях.
Здесь лучше организован код C #.
public class NiceScale
{
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 NiceScale(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();
}
}
Вот версия Kotlin!
import java.lang.Math.*
/**
* Instantiates a new instance of the NiceScale class.
*
* @param min Double The minimum data point.
* @param max Double The maximum data point.
*/
class NiceScale(private var minPoint: Double, private var maxPoint: Double) {
private var maxTicks = 15.0
private var range: Double = 0.0
var niceMin: Double = 0.0
var niceMax: Double = 0.0
var tickSpacing: Double = 0.0
init {
calculate()
}
/**
* Calculate and update values for tick spacing and nice
* minimum and maximum data points on the axis.
*/
private fun calculate() {
range = niceNum(maxPoint - minPoint, false)
tickSpacing = niceNum(range / (maxTicks - 1), true)
niceMin = floor(minPoint / tickSpacing) * tickSpacing
niceMax = ceil(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 Double The data range.
* @param round Boolean Whether to round the result.
* @return Double A "nice" number to be used for the data range.
*/
private fun niceNum(range: Double, round: Boolean): Double {
/** Exponent of range */
val exponent: Double = floor(log10(range))
/** Fractional part of range */
val fraction: Double
/** Nice, rounded fraction */
val niceFraction: Double
fraction = range / pow(10.0, exponent)
niceFraction = if (round) {
when {
fraction < 1.5 -> 1.0
fraction < 3 -> 2.0
fraction < 7 -> 5.0
else -> 10.0
}
} else {
when {
fraction <= 1 -> 1.0
fraction <= 2 -> 2.0
fraction <= 5 -> 5.0
else -> 10.0
}
}
return niceFraction * pow(10.0, exponent)
}
/**
* Sets the minimum and maximum data points.
*
* @param minPoint Double The minimum data point.
* @param maxPoint Double The maximum data point.
*/
fun setMinMaxPoints(minPoint: Double, maxPoint: Double) {
this.minPoint = minPoint
this.maxPoint = maxPoint
calculate()
}
/**
* Sets maximum number of tick marks we're comfortable with.
*
* @param maxTicks Double The maximum number of tick marks.
*/
fun setMaxTicks(maxTicks: Double) {
this.maxTicks = maxTicks
calculate()
}
}
Это версия Swift:
class NiceScale {
private var minPoint: Double
private var maxPoint: Double
private var maxTicks = 10
private(set) var tickSpacing: Double = 0
private(set) var range: Double = 0
private(set) var niceMin: Double = 0
private(set) var niceMax: Double = 0
init(min: Double, max: Double) {
minPoint = min
maxPoint = max
calculate()
}
func setMinMaxPoints(min: Double, max: Double) {
minPoint = min
maxPoint = max
calculate()
}
private func calculate() {
range = niceNum(maxPoint - minPoint, round: false)
tickSpacing = niceNum(range / Double((maxTicks - 1)), round: true)
niceMin = floor(minPoint / tickSpacing) * tickSpacing
niceMax = floor(maxPoint / tickSpacing) * tickSpacing
}
private func niceNum(range: Double, round: Bool) -> Double {
let exponent = floor(log10(range))
let fraction = range / pow(10, exponent)
let niceFraction: Double
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(10, exponent)
}
}
Мне нужно было преобразовать этот алгоритм в C #, так что вот он ...
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;
}
}
Здесь то же самое в Objective C
YFRNiceScale.h
#import <Foundation/Foundation.h>
@interface YFRNiceScale : NSObject
@property (nonatomic, readonly) CGFloat minPoint;
@property (nonatomic, readonly) CGFloat maxPoint;
@property (nonatomic, readonly) CGFloat maxTicks;
@property (nonatomic, readonly) CGFloat tickSpacing;
@property (nonatomic, readonly) CGFloat range;
@property (nonatomic, readonly) CGFloat niceRange;
@property (nonatomic, readonly) CGFloat niceMin;
@property (nonatomic, readonly) CGFloat niceMax;
- (id) initWithMin: (CGFloat) min andMax: (CGFloat) max;
- (id) initWithNSMin: (NSDecimalNumber*) min andNSMax: (NSDecimalNumber*) max;
@end
YFRNiceScale.m
#import "YFRNiceScale.h"
@implementation YFRNiceScale
@synthesize minPoint = _minPoint;
@synthesize maxPoint = _maxPoint;
@synthesize maxTicks = _maxTicks;
@synthesize tickSpacing = _tickSpacing;
@synthesize range = _range;
@synthesize niceRange = _niceRange;
@synthesize niceMin = _niceMin;
@synthesize niceMax = _niceMax;
- (id)init {
self = [super init];
if (self) {
}
return self;
}
- (id) initWithMin: (CGFloat) min andMax: (CGFloat) max {
if (self) {
_maxTicks = 10;
_minPoint = min;
_maxPoint = max;
[self calculate];
}
return [self init];
}
- (id) initWithNSMin: (NSDecimalNumber*) min andNSMax: (NSDecimalNumber*) max {
if (self) {
_maxTicks = 10;
_minPoint = [min doubleValue];
_maxPoint = [max doubleValue];
[self calculate];
}
return [self init];
}
/**
* Calculate and update values for tick spacing and nice minimum and maximum
* data points on the axis.
*/
- (void) calculate {
_range = [self niceNumRange: (_maxPoint-_minPoint) roundResult:NO];
_tickSpacing = [self niceNumRange: (_range / (_maxTicks - 1)) roundResult:YES];
_niceMin = floor(_minPoint / _tickSpacing) * _tickSpacing;
_niceMax = ceil(_maxPoint / _tickSpacing) * _tickSpacing;
_niceRange = _niceMax - _niceMin;
}
/**
* 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
*/
- (CGFloat) niceNumRange:(CGFloat) aRange roundResult:(BOOL) round {
CGFloat exponent;
CGFloat fraction;
CGFloat niceFraction;
exponent = floor(log10(aRange));
fraction = aRange / 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 = 2;
} else {
niceFraction = 10;
}
}
return niceFraction * pow(10, exponent);
}
- (NSString*) description {
return [NSString stringWithFormat:@"NiceScale [minPoint=%.2f, maxPoint=%.2f, maxTicks=%.2f, tickSpacing=%.2f, range=%.2f, niceMin=%.2f, niceMax=%.2f]", _minPoint, _maxPoint, _maxTicks, _tickSpacing, _range, _niceMin, _niceMax ];
}
@end
Использование:
YFRNiceScale* niceScale = [[YFRNiceScale alloc] initWithMin:0 andMax:500];
NSLog(@"Nice: %@", niceScale);
Поскольку каждый и его собака публикуют перевод на другие популярные языки, вот моя версия языка программирования Nimrod . Я также добавил обработку случаев, когда количество тиков меньше двух:
import math, strutils
const
defaultMaxTicks = 10
type NiceScale = object
minPoint: float
maxPoint: float
maxTicks: int
tickSpacing: float
niceMin: float
niceMax: float
proc ff(x: float): string =
result = x.formatFloat(ffDecimal, 3)
proc ` Поскольку каждый и его собака публикуют перевод на другие популярные языки, вот моя версия языка программирования Nimrod . Я также добавил обработку случаев, когда количество тиков меньше двух:
*(x: NiceScale): string =
result = "Input minPoint: " & x.minPoint.ff &
"\nInput maxPoint: " & x.maxPoint.ff &
"\nInput maxTicks: " & $x.maxTicks &
"\nOutput niceMin: " & x.niceMin.ff &
"\nOutput niceMax: " & x.niceMax.ff &
"\nOutput tickSpacing: " & x.tickSpacing.ff &
"\n"
proc calculate*(x: var NiceScale)
proc init*(x: var NiceScale; minPoint, maxPoint: float;
maxTicks = defaultMaxTicks) =
x.minPoint = minPoint
x.maxPoint = maxPoint
x.maxTicks = maxTicks
x.calculate
proc initScale*(minPoint, maxPoint: float;
maxTicks = defaultMaxTicks): NiceScale =
result.init(minPoint, maxPoint, maxTicks)
proc niceNum(scaleRange: float; doRound: bool): float =
var
exponent: float ## Exponent of scaleRange.
fraction: float ## Fractional part of scaleRange.
niceFraction: float ## Nice, rounded fraction.
exponent = floor(log10(scaleRange));
fraction = scaleRange / pow(10, exponent);
if doRound:
if fraction < 1.5:
niceFraction = 1
elif fraction < 3:
niceFraction = 2
elif fraction < 7:
niceFraction = 5
else:
niceFraction = 10
else:
if fraction <= 1:
niceFraction = 1
elif fraction <= 2:
niceFraction = 2
elif fraction <= 5:
niceFraction = 5
else:
niceFraction = 10
return niceFraction * pow(10, exponent)
proc calculate*(x: var NiceScale) =
assert x.maxPoint > x.minPoint, "Wrong input range!"
assert x.maxTicks >= 0, "Sorry, can't have imaginary ticks!"
let scaleRange = niceNum(x.maxPoint - x.minPoint, false)
if x.maxTicks < 2:
x.niceMin = floor(x.minPoint)
x.niceMax = ceil(x.maxPoint)
x.tickSpacing = (x.niceMax - x.niceMin) /
(if x.maxTicks == 1: 2.0 else: 1.0)
else:
x.tickSpacing = niceNum(scaleRange / (float(x.maxTicks - 1)), true)
x.niceMin = floor(x.minPoint / x.tickSpacing) * x.tickSpacing
x.niceMax = ceil(x.maxPoint / x.tickSpacing) * x.tickSpacing
when isMainModule:
var s = initScale(57.2, 103.3)
echo s
Это версия с комментариями. Полный текст можно прочитать на GitHub , интегрированном в мой проект.