changeset 1420:a533662c17f4

Merge from branch "scale-ticks"
author Chris Cannam
date Mon, 10 Jul 2017 14:23:19 +0100
parents 09751743647e (current diff) e7e626a87a1e (diff)
children 8b7d6c1e1ab7 91001e2bb96a
files
diffstat 5 files changed, 872 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/ScaleTickIntervals.h	Mon Jul 10 14:23:19 2017 +0100
@@ -0,0 +1,309 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2017 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_SCALE_TICK_INTERVALS_H
+#define SV_SCALE_TICK_INTERVALS_H
+
+#include <string>
+#include <vector>
+#include <cmath>
+
+#include "LogRange.h"
+#include "Debug.h"
+
+// Can't have this on by default, as we're called on every refresh
+//#define DEBUG_SCALE_TICK_INTERVALS 1
+
+class ScaleTickIntervals
+{
+public:
+    struct Range {
+	double min;        // start of value range
+	double max;        // end of value range
+	int n;             // number of divisions (approximate only)
+    };
+
+    struct Tick {
+	double value;      // value this tick represents
+	std::string label; // value as written 
+    };
+
+    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
+	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)
+    {
+        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;
+
+        double digInc = log10(inc);
+        double digMax = log10(fabs(r.max));
+        double digMin = log10(fabs(r.min));
+
+        int precInc = int(floor(digInc));
+	double roundTo = pow(10.0, precInc);
+
+        if (precInc > -4 && precInc < 4) {
+            display = Fixed;
+        } else if ((digMax >= -2.0 && digMax <= 3.0) &&
+                   (digMin >= -3.0 && digMin <= 3.0)) {
+            display = Fixed;
+        } else {
+            display = Scientific;
+        }
+        
+        int precRange = int(ceil(digMax - digInc));
+
+        int prec = 1;
+        
+        if (display == Fixed) {
+            if (digInc < 0) {
+                prec = -precInc;
+            } else {
+                prec = 0;
+            }
+        } else {
+            prec = precRange;
+        }
+
+#ifdef DEBUG_SCALE_TICK_INTERVALS
+        SVDEBUG << "ScaleTickIntervals: calculating linearInstruction" << endl
+                << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max
+                << ", n = " << r.n << ", inc = " << inc << endl;
+        SVDEBUG << "ScaleTickIntervals: digMax = " << digMax
+                << ", digInc = " << digInc << endl;
+        SVDEBUG << "ScaleTickIntervals: display = " << display
+                << ", inc = " << inc << ", precInc = " << precInc
+                << ", precRange = " << precRange
+                << ", prec = " << prec << ", roundTo = " << roundTo
+                << endl;
+#endif
+
+        double min = r.min;
+        
+        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 (display == Scientific && min != 0.0) {
+            double digNewMin = log10(fabs(min));
+            if (digNewMin < digInc) {
+                prec = int(ceil(digMax - digNewMin));
+#ifdef DEBUG_SCALE_TICK_INTERVALS
+                SVDEBUG << "ScaleTickIntervals: min is smaller than increment, adjusting prec to " << prec << endl;
+#endif
+            }
+        }
+        
+        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
+        SVDEBUG << "ScaleTickIntervals: calculating logInstruction" << endl
+                << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max
+                << ", n = " << r.n << ", inc = " << inc
+                << ", minDispInc = " << minDispInc << ", digInc = " << digInc
+                << endl;
+        SVDEBUG << "ScaleTickIntervals: display = " << display
+                << ", inc = " << inc << ", precInc = " << precInc
+                << ", prec = " << prec << endl;
+        SVDEBUG << "ScaleTickIntervals: roundTo = " << roundTo << 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) {
+        Instruction instruction = linearInstruction(r);
+        Ticks ticks = explode(instruction);
+        return ticks;
+    }
+
+    static Ticks logTicks(Range r) {
+        Instruction instruction = logInstruction(r);
+        Ticks ticks = explode(instruction);
+        return ticks;
+    }
+    
+    static Tick makeTick(Display display, int precision, double value) {
+        const int buflen = 40;
+        char buffer[buflen];
+        snprintf(buffer, buflen,
+                 display == Auto ? "%.*g" :
+                 display == Fixed ? "%.*f" :
+                 "%.*e",
+                 precision, value);
+        return Tick({ value, std::string(buffer) });
+    }
+    
+    static Ticks explode(Instruction instruction) {
+
+#ifdef DEBUG_SCALE_TICK_INTERVALS
+	SVDEBUG << "ScaleTickIntervals::explode:" << endl
+                << "initial = " << instruction.initial
+                << ", limit = " << instruction.limit
+                << ", spacing = " << instruction.spacing
+                << ", roundTo = " << instruction.roundTo
+                << ", display = " << instruction.display
+                << ", precision = " << instruction.precision
+                << ", logUnmap = " << instruction.logUnmap
+                << endl;
+#endif
+
+        if (instruction.spacing == 0.0) {
+            return {};
+        }
+
+        double eps = 1e-7;
+        if (instruction.spacing < eps * 10.0) {
+            eps = instruction.spacing / 10.0;
+        }
+
+        double max = instruction.limit;
+        int n = 0;
+
+        Ticks ticks;
+        
+        while (true) {
+            double value = instruction.initial + n * instruction.spacing;
+            if (value >= max + eps) {
+                break;
+            }
+            if (instruction.logUnmap) {
+                value = pow(10.0, value);
+            }
+            if (instruction.roundTo != 0.0) {
+                value = instruction.roundTo * round(value / instruction.roundTo);
+            }
+	    ticks.push_back(makeTick(instruction.display,
+                                     instruction.precision,
+                                     value));
+            ++n;
+	}
+
+        return ticks;
+    }
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/TestScaleTickIntervals.h	Mon Jul 10 14:23:19 2017 +0100
@@ -0,0 +1,555 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef TEST_SCALE_TICK_INTERVALS_H
+#define TEST_SCALE_TICK_INTERVALS_H
+
+#include "../ScaleTickIntervals.h"
+
+#include <QObject>
+#include <QtTest>
+#include <QDir>
+
+#include <iostream>
+
+using namespace std;
+
+class TestScaleTickIntervals : public QObject
+{
+    Q_OBJECT
+
+    void printDiff(vector<ScaleTickIntervals::Tick> ticks,
+		   vector<ScaleTickIntervals::Tick> expected) {
+
+	cerr << "Have " << ticks.size() << " ticks, expected "
+	     << expected.size() << endl;
+	for (int i = 0; i < int(ticks.size()); ++i) {
+            cerr << i << ": have " << ticks[i].value << " \""
+                 << ticks[i].label << "\", expected ";
+	    if (i < int(expected.size())) {
+                cerr << expected[i].value << " \"" << expected[i].label
+		     << "\"" << endl;
+            } else {
+                cerr << "(n/a)" << endl;
+	    }
+	}
+    }
+    
+    void compareTicks(ScaleTickIntervals::Ticks ticks,
+		      ScaleTickIntervals::Ticks expected,
+                      bool fuzzier = false)
+    {
+        double eps = 1e-7;
+	for (int i = 0; i < int(expected.size()); ++i) {
+	    if (i < int(ticks.size())) {
+                bool pass = true;
+		if (ticks[i].label != expected[i].label) {
+                    pass = false;
+                } else if (!fuzzier) {
+		    if (fabs(ticks[i].value - expected[i].value) > eps) {
+                        pass = false;
+                    }
+                } else {
+		    if (fabs(ticks[i].value - expected[i].value) >
+                        fabs(ticks[i].value) * 1e-5) {
+                        pass = false;
+                    }
+                }
+                if (!pass) {
+		    printDiff(ticks, expected);
+                    QCOMPARE(ticks[i].label, expected[i].label);
+                    QCOMPARE(ticks[i].value, expected[i].value);
+                }
+	    }
+	}
+        if (ticks.size() != expected.size()) {
+            printDiff(ticks, expected);
+        }
+	QCOMPARE(ticks.size(), expected.size());
+    }
+    
+private slots:
+    void linear_0_1_10()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 0, 1, 10 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0.0, "0.0" },
+	    { 0.1, "0.1" },
+	    { 0.2, "0.2" },
+	    { 0.3, "0.3" },
+	    { 0.4, "0.4" },
+	    { 0.5, "0.5" },
+	    { 0.6, "0.6" },
+	    { 0.7, "0.7" },
+	    { 0.8, "0.8" },
+	    { 0.9, "0.9" },
+	    { 1.0, "1.0" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_0_5_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 0, 5, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0, "0" },
+	    { 1, "1" },
+	    { 2, "2" },
+	    { 3, "3" },
+	    { 4, "4" },
+	    { 5, "5" },
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_0_10_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 0, 10, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0, "0" },
+	    { 2, "2" },
+	    { 4, "4" },
+	    { 6, "6" },
+	    { 8, "8" },
+	    { 10, "10" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_10_0_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 10, 0, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0, "0" },
+	    { 2, "2" },
+	    { 4, "4" },
+	    { 6, "6" },
+	    { 8, "8" },
+	    { 10, "10" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_m10_0_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ -10, 0, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { -10, "-10" },
+	    { -8, "-8" },
+	    { -6, "-6" },
+	    { -4, "-4" },
+	    { -2, "-2" },
+	    { 0, "0" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_0_m10_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 0, -10, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { -10, "-10" },
+	    { -8, "-8" },
+	    { -6, "-6" },
+	    { -4, "-4" },
+	    { -2, "-2" },
+	    { 0, "0" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_0_0p1_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 0, 0.1, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0.00, "0.00" },
+	    { 0.02, "0.02" },
+	    { 0.04, "0.04" },
+	    { 0.06, "0.06" },
+	    { 0.08, "0.08" },
+	    { 0.10, "0.10" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_0_0p01_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 0, 0.01, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0.000, "0.000" },
+	    { 0.002, "0.002" },
+	    { 0.004, "0.004" },
+	    { 0.006, "0.006" },
+	    { 0.008, "0.008" },
+	    { 0.010, "0.010" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_0_0p005_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 0, 0.005, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0.000, "0.000" },
+	    { 0.001, "0.001" },
+	    { 0.002, "0.002" },
+	    { 0.003, "0.003" },
+	    { 0.004, "0.004" },
+	    { 0.005, "0.005" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_0_0p001_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 0, 0.001, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0.0000, "0.0e+00" },
+	    { 0.0002, "2.0e-04" },
+	    { 0.0004, "4.0e-04" },
+	    { 0.0006, "6.0e-04" },
+	    { 0.0008, "8.0e-04" },
+	    { 0.0010, "1.0e-03" }
+	};
+	compareTicks(ticks, expected);
+    }
+    
+    void linear_1_1p001_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 1, 1.001, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 1.0000, "1.0000" },
+	    { 1.0002, "1.0002" },
+	    { 1.0004, "1.0004" },
+	    { 1.0006, "1.0006" },
+	    { 1.0008, "1.0008" },
+	    { 1.0010, "1.0010" }
+	};
+	compareTicks(ticks, expected);
+    }
+    
+    void linear_0p001_1_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 0.001, 1, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0.1, "0.1" },
+	    { 0.3, "0.3" },
+	    { 0.5, "0.5" },
+	    { 0.7, "0.7" },
+	    { 0.9, "0.9" },
+	};
+	compareTicks(ticks, expected);
+    }
+        
+    void linear_10000_10010_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 10000, 10010, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 10000, "10000" },
+	    { 10002, "10002" },
+	    { 10004, "10004" },
+	    { 10006, "10006" },
+	    { 10008, "10008" },
+	    { 10010, "10010" },
+	};
+	compareTicks(ticks, expected);
+    }
+    
+    void linear_10000_20000_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 10000, 20000, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 10000, "10000" },
+	    { 12000, "12000" },
+	    { 14000, "14000" },
+	    { 16000, "16000" },
+	    { 18000, "18000" },
+	    { 20000, "20000" },
+	};
+	compareTicks(ticks, expected);
+    }
+    
+    void linear_m1_1_10()
+    {
+	auto ticks = ScaleTickIntervals::linear({ -1, 1, 10 });
+	ScaleTickIntervals::Ticks expected {
+	    { -1.0, "-1.0" },
+	    { -0.8, "-0.8" },
+	    { -0.6, "-0.6" },
+	    { -0.4, "-0.4" },
+	    { -0.2, "-0.2" },
+	    { 0.0, "0.0" },
+	    { 0.2, "0.2" },
+	    { 0.4, "0.4" },
+	    { 0.6, "0.6" },
+	    { 0.8, "0.8" },
+	    { 1.0, "1.0" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_221p23_623p7_57p4()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 221.23, 623.7, 4 });
+        // only 4 ticks, not 5, because none of the rounded tick
+        // values lies on an end value
+	ScaleTickIntervals::Ticks expected {
+            { 300, "300" },
+            { 400, "400" },
+            { 500, "500" },
+            { 600, "600" },
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_sqrt2_pi_7()
+    {
+	auto ticks = ScaleTickIntervals::linear({ sqrt(2.0), M_PI, 7 });
+        // This would be better in steps of 0.25, but we only round to
+        // integral powers of ten
+	ScaleTickIntervals::Ticks expected {
+            { 1.5, "1.5" },
+            { 1.7, "1.7" },
+            { 1.9, "1.9" },
+            { 2.1, "2.1" },
+            { 2.3, "2.3" },
+            { 2.5, "2.5" },
+            { 2.7, "2.7" },
+            { 2.9, "2.9" },
+            { 3.1, "3.1" },
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_pi_avogadro_7()
+    {
+	auto ticks = ScaleTickIntervals::linear({ M_PI, 6.022140857e23, 7 });
+	ScaleTickIntervals::Ticks expected {
+            // 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);
+    }
+
+    void linear_2_3_1()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 2, 3, 1 });
+	ScaleTickIntervals::Ticks expected {
+	    { 2.0, "2" },
+	    { 3.0, "3" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_2_3_2()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 2, 3, 2 });
+	ScaleTickIntervals::Ticks expected {
+	    { 2.0, "2.0" },
+	    { 2.5, "2.5" },
+	    { 3.0, "3.0" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_2_3_3()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 2, 3, 3 });
+	ScaleTickIntervals::Ticks expected {
+	    { 2.0, "2.0" },
+	    { 2.3, "2.3" },
+	    { 2.6, "2.6" },
+	    { 2.9, "2.9" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_2_3_4()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 2, 3, 4 });
+        // This would be better in steps of 0.25, but we only round to
+        // integral powers of ten
+	ScaleTickIntervals::Ticks expected {
+	    { 2.0, "2.0" },
+	    { 2.3, "2.3" },
+	    { 2.6, "2.6" },
+	    { 2.9, "2.9" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_2_3_5()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 2, 3, 5 });
+	ScaleTickIntervals::Ticks expected {
+	    { 2.0, "2.0" },
+	    { 2.2, "2.2" },
+	    { 2.4, "2.4" },
+	    { 2.6, "2.6" },
+	    { 2.8, "2.8" },
+	    { 3.0, "3.0" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_2_3_6()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 2, 3, 6 });
+	ScaleTickIntervals::Ticks expected {
+	    { 2.0, "2.0" },
+	    { 2.2, "2.2" },
+	    { 2.4, "2.4" },
+	    { 2.6, "2.6" },
+	    { 2.8, "2.8" },
+	    { 3.0, "3.0" }
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void linear_1_1_10()
+    {
+        // pathological range
+	auto ticks = ScaleTickIntervals::linear({ 1, 1, 10 });
+	ScaleTickIntervals::Ticks expected {
+	    { 1.0, "1" }
+	};
+	compareTicks(ticks, expected);
+    }
+    
+    void linear_0_0_10()
+    {
+        // pathological range
+	auto ticks = ScaleTickIntervals::linear({ 0, 0, 10 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0.0, "0" }
+	};
+	compareTicks(ticks, expected);
+    }
+    
+    void linear_0_1_1()
+    {
+	auto ticks = ScaleTickIntervals::linear({ 0, 1, 1 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0.0, "0" },
+	    { 1.0, "1" }
+	};
+	compareTicks(ticks, expected);
+    }
+    
+    void linear_0_1_0()
+    {
+        // senseless input
+	auto ticks = ScaleTickIntervals::linear({ 0, 1, 0 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0.0, "0" },
+	};
+	compareTicks(ticks, expected);
+    }
+    
+    void linear_0_1_m1()
+    {
+        // senseless input
+	auto ticks = ScaleTickIntervals::linear({ 0, 1, -1 });
+	ScaleTickIntervals::Ticks expected {
+	    { 0.0, "0" },
+	};
+	compareTicks(ticks, expected);
+    }
+
+    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);
+    }
+    
+    void log_1_10_2()
+    {
+        auto ticks = ScaleTickIntervals::logarithmic({ 1, 10, 2 });
+	ScaleTickIntervals::Ticks expected {
+            { 1.0, "1.0" },
+            { pow(10.0, 0.5), "3.2" },
+            { 10.0, "10.0" },
+	};
+	compareTicks(ticks, expected);
+    }
+    
+    void log_0_10_2()
+    {
+        auto ticks = ScaleTickIntervals::logarithmic({ 0, 10, 2 });
+	ScaleTickIntervals::Ticks expected {
+            { 1e-6, "1e-06" },
+            { 1, "1" },
+	};
+	compareTicks(ticks, expected);
+    }
+
+    void log_pi_avogadro_7()
+    {
+	auto ticks = ScaleTickIntervals::logarithmic({ M_PI, 6.022140857e23, 7 });
+	ScaleTickIntervals::Ticks expected {
+            { 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
+
+
--- a/base/test/files.pri	Tue Mar 07 13:52:37 2017 +0000
+++ b/base/test/files.pri	Mon Jul 10 14:23:19 2017 +0100
@@ -4,6 +4,7 @@
 	     TestRangeMapper.h \
 	     TestOurRealTime.h \
 	     TestPitch.h \
+	     TestScaleTickIntervals.h \
 	     TestStringBits.h \
 	     TestVampRealTime.h
 	     
--- a/base/test/svcore-base-test.cpp	Tue Mar 07 13:52:37 2017 +0000
+++ b/base/test/svcore-base-test.cpp	Mon Jul 10 14:23:19 2017 +0100
@@ -14,6 +14,7 @@
 #include "TestLogRange.h"
 #include "TestRangeMapper.h"
 #include "TestPitch.h"
+#include "TestScaleTickIntervals.h"
 #include "TestStringBits.h"
 #include "TestOurRealTime.h"
 #include "TestVampRealTime.h"
@@ -66,6 +67,11 @@
 	if (QTest::qExec(&t, argc, argv) == 0) ++good;
 	else ++bad;
     }
+    {
+	TestScaleTickIntervals t;
+	if (QTest::qExec(&t, argc, argv) == 0) ++good;
+	else ++bad;
+    }
 
     if (bad > 0) {
 	cerr << "\n********* " << bad << " test suite(s) failed!\n" << endl;
--- a/files.pri	Tue Mar 07 13:52:37 2017 +0000
+++ b/files.pri	Mon Jul 10 14:23:19 2017 +0100
@@ -26,6 +26,7 @@
            base/RecentFiles.h \
            base/ResourceFinder.h \
            base/RingBuffer.h \
+           base/ScaleTickIntervals.h \
            base/Scavenger.h \
            base/Selection.h \
            base/Serialiser.h \