Mercurial > hg > svcore
changeset 1418:e7cb4fb2aee4 scale-ticks
Rework log scale calculation, update to changed interface
author | Chris Cannam |
---|---|
date | Thu, 04 May 2017 15:37:43 +0100 |
parents | 359147a50853 |
children | e7e626a87a1e |
files | base/ScaleTickIntervals.h base/test/TestScaleTickIntervals.h |
diffstat | 2 files changed, 187 insertions(+), 72 deletions(-) [+] |
line wrap: on
line diff
--- a/base/ScaleTickIntervals.h Thu May 04 13:32:42 2017 +0100 +++ b/base/ScaleTickIntervals.h Thu May 04 15:37:43 2017 +0100 @@ -43,69 +43,87 @@ }; typedef std::vector<Tick> Ticks; - + + /** + * Return a set of ticks that divide the range r linearly into + * roughly r.n equal divisions, in such a way as to yield + * reasonably human-readable labels. + */ static Ticks linear(Range r) { return linearTicks(r); } + /** + * Return a set of ticks that divide the range r into roughly r.n + * logarithmic divisions, in such a way as to yield reasonably + * human-readable labels. + */ static Ticks logarithmic(Range r) { + LogRange::mapRange(r.min, r.max); + return logarithmicAlready(r); + } + + /** + * Return a set of ticks that divide the range r into roughly r.n + * logarithmic divisions, on the asssumption that r.min and r.max + * already represent the logarithms of the boundary values rather + * than the values themselves. + */ + static Ticks logarithmicAlready(Range r) { return logTicks(r); } - + private: + enum Display { + Fixed, + Scientific, + Auto + }; + struct Instruction { double initial; // value of first tick double limit; // max from original range double spacing; // increment between ticks double roundTo; // what all displayed values should be rounded to - bool fixed; // whether to use fixed precision (%f rather than %e) + Display display; // whether to use fixed precision (%e, %f, or %g) int precision; // number of dp (%f) or sf (%e) bool logUnmap; // true if values represent logs of display values }; static Instruction linearInstruction(Range r) { - if (r.n < 1) { - return {}; - } + Display display = Auto; + if (r.max < r.min) { return linearInstruction({ r.max, r.min, r.n }); } + if (r.n < 1 || r.max == r.min) { + return { r.min, r.min, 1.0, r.min, display, 1, false }; + } double inc = (r.max - r.min) / r.n; - if (inc == 0) { -#ifdef DEBUG_SCALE_TICK_INTERVALS - std::cerr << "inc == 0, using trivial range" << std::endl; -#endif - double roundTo = r.min; - if (roundTo <= 0.0) { - roundTo = 1.0; - } - return { r.min, r.max, 1.0, roundTo, true, 1, false }; - } double digInc = log10(inc); double digMax = log10(fabs(r.max)); double digMin = log10(fabs(r.min)); - int precInc = int(trunc(digInc)); - if (double(precInc) != digInc) { - precInc -= 1; - } + int precInc = int(floor(digInc)); + double roundTo = pow(10.0, precInc); - bool fixed = false; if (precInc > -4 && precInc < 4) { - fixed = true; - } else if ((digMax >= -3.0 && digMax <= 2.0) && + display = Fixed; + } else if ((digMax >= -2.0 && digMax <= 3.0) && (digMin >= -3.0 && digMin <= 3.0)) { - fixed = true; + display = Fixed; + } else { + display = Scientific; } int precRange = int(ceil(digMax - digInc)); int prec = 1; - if (fixed) { + if (display == Fixed) { if (digInc < 0) { prec = -precInc; } else { @@ -115,26 +133,27 @@ prec = precRange; } - double roundTo = pow(10.0, precInc); - #ifdef DEBUG_SCALE_TICK_INTERVALS std::cerr << "\nmin = " << r.min << ", max = " << r.max << ", n = " << r.n << ", inc = " << inc << std::endl; std::cerr << "digMax = " << digMax << ", digInc = " << digInc << std::endl; - std::cerr << "fixed = " << fixed << ", inc = " << inc + std::cerr << "display = " << display << ", inc = " << inc << ", precInc = " << precInc << ", precRange = " << precRange << ", prec = " << prec << std::endl; std::cerr << "roundTo = " << roundTo << std::endl; #endif + + double min = r.min; - inc = round(inc / roundTo) * roundTo; - if (inc < roundTo) inc = roundTo; - - double min = ceil(r.min / roundTo) * roundTo; - if (min > r.max) min = r.max; + if (roundTo != 0.0) { + inc = round(inc / roundTo) * roundTo; + if (inc < roundTo) inc = roundTo; + min = ceil(min / roundTo) * roundTo; + if (min > r.max) min = r.max; + } - if (!fixed && min != 0.0) { + if (display == Scientific && min != 0.0) { double digNewMin = log10(fabs(min)); if (digNewMin < digInc) { prec = int(ceil(digMax - digNewMin)); @@ -145,7 +164,73 @@ } } - return { min, r.max, inc, roundTo, fixed, prec, false }; + return { min, r.max, inc, roundTo, display, prec, false }; + } + + static Instruction logInstruction(Range r) + { + Display display = Auto; + + if (r.n < 1) { + return {}; + } + if (r.max < r.min) { + return logInstruction({ r.max, r.min, r.n }); + } + if (r.max == r.min) { + return { r.min, r.max, 1.0, r.min, display, 1, true }; + } + + double inc = (r.max - r.min) / r.n; + + double digInc = log10(inc); + int precInc = int(floor(digInc)); + double roundTo = pow(10.0, precInc); + + if (roundTo != 0.0) { + inc = round(inc / roundTo) * roundTo; + if (inc < roundTo) inc = roundTo; + } + + // if inc is close to giving us powers of two, nudge it + if (fabs(inc - 0.301) < 0.01) { + inc = log10(2.0); + } + + // smallest increment as displayed + double minDispInc = + LogRange::unmap(r.min + inc) - LogRange::unmap(r.min); + + int prec = 1; + + if (minDispInc > 0.0) { + prec = int(floor(log10(minDispInc))); + if (prec < 0) prec = -prec; + } + + if (r.max >= -2.0 && r.max <= 3.0 && + r.min >= -3.0 && r.min <= 3.0) { + display = Fixed; + if (prec == 0) prec = 1; + } + +#ifdef DEBUG_SCALE_TICK_INTERVALS + std::cerr << "\nmin = " << r.min << ", max = " << r.max << ", n = " << r.n + << ", inc = " << inc << ", minDispInc = " << minDispInc + << ", digInc = " << digInc << std::endl; + std::cerr << "display = " << display << ", inc = " << inc + << ", precInc = " << precInc + << ", prec = " << prec << std::endl; + std::cerr << "roundTo = " << roundTo << std::endl; +#endif + + double min = r.min; + if (inc != 0.0) { + min = ceil(r.min / inc) * inc; + if (min > r.max) min = r.max; + } + + return { min, r.max, inc, 0.0, display, prec, true }; } static Ticks linearTicks(Range r) { @@ -155,22 +240,18 @@ } static Ticks logTicks(Range r) { - Range mapped(r); - LogRange::mapRange(mapped.min, mapped.max); - Instruction instruction = linearInstruction(mapped); - instruction.logUnmap = true; - if (fabs(mapped.min - mapped.max) > 3) { - instruction.fixed = false; - } + Instruction instruction = logInstruction(r); Ticks ticks = explode(instruction); return ticks; } - - static Tick makeTick(bool fixed, int precision, double value) { + + static Tick makeTick(Display display, int precision, double value) { const int buflen = 40; char buffer[buflen]; snprintf(buffer, buflen, - fixed ? "%.*f" : "%.*e", + display == Auto ? "%.*g" : + display == Fixed ? "%.*f" : + "%.*e", precision, value); return Tick({ value, std::string(buffer) }); } @@ -182,7 +263,7 @@ << ", limit = " << instruction.limit << ", spacing = " << instruction.spacing << ", roundTo = " << instruction.roundTo - << ", fixed = " << instruction.fixed + << ", display = " << instruction.display << ", precision = " << instruction.precision << ", logUnmap = " << instruction.logUnmap << std::endl; @@ -204,14 +285,16 @@ while (true) { double value = instruction.initial + n * instruction.spacing; - value = instruction.roundTo * round(value / instruction.roundTo); if (value >= max + eps) { break; } if (instruction.logUnmap) { value = pow(10.0, value); } - ticks.push_back(makeTick(instruction.fixed, + if (instruction.roundTo != 0.0) { + value = instruction.roundTo * round(value / instruction.roundTo); + } + ticks.push_back(makeTick(instruction.display, instruction.precision, value)); ++n;
--- a/base/test/TestScaleTickIntervals.h Thu May 04 13:32:42 2017 +0100 +++ b/base/test/TestScaleTickIntervals.h Thu May 04 15:37:43 2017 +0100 @@ -305,10 +305,10 @@ // only 4 ticks, not 5, because none of the rounded tick // values lies on an end value ScaleTickIntervals::Ticks expected { - { 230, "230" }, - { 330, "330" }, - { 430, "430" }, - { 530, "530" }, + { 300, "300" }, + { 400, "400" }, + { 500, "500" }, + { 600, "600" }, }; compareTicks(ticks, expected); } @@ -336,13 +336,14 @@ { auto ticks = ScaleTickIntervals::linear({ M_PI, 6.022140857e23, 7 }); ScaleTickIntervals::Ticks expected { - { 1e+21, "1.000e+21" }, - { 8.7e+22, "8.700e+22" }, - { 1.73e+23, "1.730e+23" }, - { 2.59e+23, "2.590e+23" }, - { 3.45e+23, "3.450e+23" }, - { 4.31e+23, "4.310e+23" }, - { 5.17e+23, "5.170e+23" }, + // not perfect, but ok-ish + { 1e+22, "1.00e+22" }, + { 1e+23, "1.00e+23" }, + { 1.9e+23, "1.90e+23" }, + { 2.8e+23, "2.80e+23" }, + { 3.7e+23, "3.70e+23" }, + { 4.6e+23, "4.60e+23" }, + { 5.5e+23, "5.50e+23" }, }; compareTicks(ticks, expected); } @@ -427,7 +428,7 @@ // pathological range auto ticks = ScaleTickIntervals::linear({ 1, 1, 10 }); ScaleTickIntervals::Ticks expected { - { 1.0, "1.0" } + { 1.0, "1" } }; compareTicks(ticks, expected); } @@ -437,7 +438,7 @@ // pathological range auto ticks = ScaleTickIntervals::linear({ 0, 0, 10 }); ScaleTickIntervals::Ticks expected { - { 0.0, "0.0" } + { 0.0, "0" } }; compareTicks(ticks, expected); } @@ -457,6 +458,7 @@ // senseless input auto ticks = ScaleTickIntervals::linear({ 0, 1, 0 }); ScaleTickIntervals::Ticks expected { + { 0.0, "0" }, }; compareTicks(ticks, expected); } @@ -466,6 +468,7 @@ // senseless input auto ticks = ScaleTickIntervals::linear({ 0, 1, -1 }); ScaleTickIntervals::Ticks expected { + { 0.0, "0" }, }; compareTicks(ticks, expected); } @@ -473,8 +476,19 @@ void linear_0p465_778_10() { // a case that gave unsatisfactory results in real life + // (initially it had the first tick at 1) auto ticks = ScaleTickIntervals::linear({ 0.465, 778.08, 10 }); ScaleTickIntervals::Ticks expected { + { 10, "10" }, + { 90, "90" }, + { 170, "170" }, + { 250, "250" }, + { 330, "330" }, + { 410, "410" }, + { 490, "490" }, + { 570, "570" }, + { 650, "650" }, + { 730, "730" }, }; compareTicks(ticks, expected); } @@ -494,9 +508,8 @@ { auto ticks = ScaleTickIntervals::logarithmic({ 0, 10, 2 }); ScaleTickIntervals::Ticks expected { - { 1e-10, "1e-10" }, - { pow(10.0, -4.5), "3e-05" }, - { 10.0, "1e+01" }, + { 1e-6, "1e-06" }, + { 1, "1" }, }; compareTicks(ticks, expected); } @@ -505,17 +518,36 @@ { auto ticks = ScaleTickIntervals::logarithmic({ M_PI, 6.022140857e23, 7 }); ScaleTickIntervals::Ticks expected { - { 3.16228, "3e+00" }, - { 6309.57, "6e+03" }, - { 1.25893e+07, "1e+07" }, - { 2.51189e+10, "3e+10" }, - { 5.01187e+13, "5e+13" }, - { 1e+17, "1e+17" }, - { 1.99526e+20, "2e+20" }, - { 3.98107e+23, "4e+23" }, + { 1000, "1e+03" }, + { 1e+06, "1e+06" }, + { 1e+09, "1e+09" }, + { 1e+12, "1e+12" }, + { 1e+15, "1e+15" }, + { 1e+18, "1e+18" }, + { 1e+21, "1e+21" }, }; compareTicks(ticks, expected, true); } + + void log_0p465_778_10() + { + auto ticks = ScaleTickIntervals::logarithmic({ 0.465, 778.08, 10 }); + ScaleTickIntervals::Ticks expected { + { 0.5, "0.5" }, + { 1, "1.0" }, + { 2, "2.0" }, + { 4, "4.0" }, + { 8, "8.0" }, + { 16, "16.0" }, + { 32, "32.0" }, + { 64, "64.0" }, + { 128, "128.0" }, + { 256, "256.0" }, + { 512, "512.0" }, + }; + compareTicks(ticks, expected); + } + }; #endif