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